From 13b0ee735519795e29efe62ea3e021610b9232b3 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 4 Oct 2018 16:22:00 -0700 Subject: [PATCH 001/427] stopping point --- Cargo.toml | 1 + src/lib.rs | 1 + src/server/channel.rs | 5 +- src/server/error.rs | 47 +++-- src/server/h1.rs | 295 +++++++++++++--------------- src/server/h1codec.rs | 251 ++++++++++++++++++++++++ src/server/h1decoder.rs | 54 ++--- src/server/h1disp.rs | 425 ++++++++++++++++++++++++++++++++++++++++ src/server/h2.rs | 2 +- src/server/message.rs | 9 +- src/server/mod.rs | 8 +- src/server/output.rs | 8 +- src/server/service.rs | 9 +- src/server/settings.rs | 5 + tests/test_h1v2.rs | 58 ++++++ 15 files changed, 958 insertions(+), 220 deletions(-) create mode 100644 src/server/h1codec.rs create mode 100644 src/server/h1disp.rs create mode 100644 tests/test_h1v2.rs diff --git a/Cargo.toml b/Cargo.toml index 12f98ac37..d70a65cf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,7 @@ futures = "0.1" futures-cpupool = "0.1" slab = "0.4" tokio = "0.1" +tokio-codec = "0.1" tokio-io = "0.1" tokio-tcp = "0.1" tokio-timer = "0.2" diff --git a/src/lib.rs b/src/lib.rs index 1ed408099..f494c05de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,6 +115,7 @@ extern crate parking_lot; extern crate rand; extern crate slab; extern crate tokio; +extern crate tokio_codec; extern crate tokio_current_thread; extern crate tokio_io; extern crate tokio_reactor; diff --git a/src/server/channel.rs b/src/server/channel.rs index cbbe1a95e..b1fef964e 100644 --- a/src/server/channel.rs +++ b/src/server/channel.rs @@ -9,6 +9,7 @@ use tokio_timer::Delay; use super::error::HttpDispatchError; use super::settings::ServiceConfig; use super::{h1, h2, HttpHandler, IoStream}; +use error::Error; use http::StatusCode; const HTTP2_PREFACE: [u8; 14] = *b"PRI * HTTP/2.0"; @@ -90,7 +91,7 @@ where H: HttpHandler + 'static, { type Item = (); - type Error = HttpDispatchError; + type Error = HttpDispatchError; fn poll(&mut self) -> Poll { // keep-alive timer @@ -242,7 +243,7 @@ where H: HttpHandler + 'static, { type Item = (); - type Error = HttpDispatchError; + type Error = HttpDispatchError; fn poll(&mut self) -> Poll { if !self.node_reg { diff --git a/src/server/error.rs b/src/server/error.rs index 70f100998..3ae9a107b 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -1,11 +1,12 @@ +use std::fmt::{Debug, Display}; use std::io; use futures::{Async, Poll}; use http2; use super::{helpers, HttpHandlerTask, Writer}; +use error::{Error, ParseError}; use http::{StatusCode, Version}; -use Error; /// Errors produced by `AcceptorError` service. #[derive(Debug)] @@ -20,60 +21,70 @@ pub enum AcceptorError { Timeout, } -#[derive(Fail, Debug)] +#[derive(Debug)] /// A set of errors that can occur during dispatching http requests -pub enum HttpDispatchError { +pub enum HttpDispatchError { /// Application error - #[fail(display = "Application specific error: {}", _0)] - App(Error), + // #[fail(display = "Application specific error: {}", _0)] + App(E), /// An `io::Error` that occurred while trying to read or write to a network /// stream. - #[fail(display = "IO error: {}", _0)] + // #[fail(display = "IO error: {}", _0)] Io(io::Error), + /// Http request parse error. + // #[fail(display = "Parse error: {}", _0)] + Parse(ParseError), + /// The first request did not complete within the specified timeout. - #[fail(display = "The first request did not complete within the specified timeout")] + // #[fail(display = "The first request did not complete within the specified timeout")] SlowRequestTimeout, /// Shutdown timeout - #[fail(display = "Connection shutdown timeout")] + // #[fail(display = "Connection shutdown timeout")] ShutdownTimeout, /// HTTP2 error - #[fail(display = "HTTP2 error: {}", _0)] + // #[fail(display = "HTTP2 error: {}", _0)] Http2(http2::Error), /// Payload is not consumed - #[fail(display = "Task is completed but request's payload is not consumed")] + // #[fail(display = "Task is completed but request's payload is not consumed")] PayloadIsNotConsumed, /// Malformed request - #[fail(display = "Malformed request")] + // #[fail(display = "Malformed request")] MalformedRequest, /// Internal error - #[fail(display = "Internal error")] + // #[fail(display = "Internal error")] InternalError, /// Unknown error - #[fail(display = "Unknown error")] + // #[fail(display = "Unknown error")] Unknown, } -impl From for HttpDispatchError { - fn from(err: Error) -> Self { - HttpDispatchError::App(err) +// impl From for HttpDispatchError { +// fn from(err: E) -> Self { +// HttpDispatchError::App(err) +// } +// } + +impl From for HttpDispatchError { + fn from(err: ParseError) -> Self { + HttpDispatchError::Parse(err) } } -impl From for HttpDispatchError { +impl From for HttpDispatchError { fn from(err: io::Error) -> Self { HttpDispatchError::Io(err) } } -impl From for HttpDispatchError { +impl From for HttpDispatchError { fn from(err: http2::Error) -> Self { HttpDispatchError::Http2(err) } diff --git a/src/server/h1.rs b/src/server/h1.rs index 4fb730f71..e2b4bf45e 100644 --- a/src/server/h1.rs +++ b/src/server/h1.rs @@ -7,15 +7,16 @@ use futures::{Async, Future, Poll}; use tokio_current_thread::spawn; use tokio_timer::Delay; -use error::{Error, PayloadError}; +use error::{Error, ParseError, PayloadError}; use http::{StatusCode, Version}; use payload::{Payload, PayloadStatus, PayloadWriter}; use super::error::{HttpDispatchError, ServerError}; -use super::h1decoder::{DecoderError, H1Decoder, Message}; +use super::h1decoder::{H1Decoder, Message}; use super::h1writer::H1Writer; use super::handler::{HttpHandler, HttpHandlerTask, HttpHandlerTaskFut}; use super::input::PayloadType; +use super::message::Request; use super::settings::ServiceConfig; use super::{IoStream, Writer}; @@ -44,7 +45,7 @@ pub struct Http1Dispatcher { payload: Option, buf: BytesMut, tasks: VecDeque>, - error: Option, + error: Option>, ka_expire: Instant, ka_timer: Option, } @@ -109,7 +110,7 @@ where Http1Dispatcher { stream: H1Writer::new(stream, settings.clone()), - decoder: H1Decoder::new(), + decoder: H1Decoder::new(settings.request_pool()), payload: None, tasks: VecDeque::new(), error: None, @@ -133,7 +134,7 @@ where let mut disp = Http1Dispatcher { flags: Flags::STARTED | Flags::READ_DISCONNECTED | Flags::FLUSHED, stream: H1Writer::new(stream, settings.clone()), - decoder: H1Decoder::new(), + decoder: H1Decoder::new(settings.request_pool()), payload: None, tasks: VecDeque::new(), error: None, @@ -201,7 +202,7 @@ where } #[inline] - pub fn poll(&mut self) -> Poll<(), HttpDispatchError> { + pub fn poll(&mut self) -> Poll<(), HttpDispatchError> { // check connection keep-alive self.poll_keep_alive()?; @@ -247,7 +248,7 @@ where } /// Flush stream - fn poll_flush(&mut self, shutdown: bool) -> Poll<(), HttpDispatchError> { + fn poll_flush(&mut self, shutdown: bool) -> Poll<(), HttpDispatchError> { if shutdown || self.flags.contains(Flags::STARTED) { match self.stream.poll_completed(shutdown) { Ok(Async::NotReady) => { @@ -277,7 +278,7 @@ where } /// keep-alive timer. returns `true` is keep-alive, otherwise drop - fn poll_keep_alive(&mut self) -> Result<(), HttpDispatchError> { + fn poll_keep_alive(&mut self) -> Result<(), HttpDispatchError> { if let Some(ref mut timer) = self.ka_timer { match timer.poll() { Ok(Async::Ready(_)) => { @@ -336,7 +337,7 @@ where #[inline] /// read data from the stream - pub(self) fn poll_io(&mut self) -> Result { + pub(self) fn poll_io(&mut self) -> Result> { if !self.flags.contains(Flags::POLLED) { self.flags.insert(Flags::POLLED); if !self.buf.is_empty() { @@ -367,7 +368,7 @@ where Ok(updated) } - pub(self) fn poll_handler(&mut self) -> Result<(), HttpDispatchError> { + pub(self) fn poll_handler(&mut self) -> Result<(), HttpDispatchError> { self.poll_io()?; let mut retry = self.can_read(); @@ -409,7 +410,7 @@ where // it is not possible to recover from error // during pipe handling, so just drop connection self.client_disconnected(false); - return Err(err.into()); + return Err(HttpDispatchError::App(err)); } } } @@ -423,7 +424,7 @@ where Ok(Async::NotReady) => false, Ok(Async::Ready(_)) => true, Err(err) => { - self.error = Some(err.into()); + self.error = Some(HttpDispatchError::App(err)); true } }; @@ -462,66 +463,75 @@ where .push_back(Entry::Error(ServerError::err(Version::HTTP_11, status))); } - pub(self) fn parse(&mut self) -> Result { + fn handle_message( + &mut self, mut msg: Request, payload: bool, + ) -> Result<(), HttpDispatchError> { + self.flags.insert(Flags::STARTED); + + if payload { + let (ps, pl) = Payload::new(false); + *msg.inner.payload.borrow_mut() = Some(pl); + self.payload = Some(PayloadType::new(&msg.inner.headers, ps)); + } + + // stream extensions + msg.inner_mut().stream_extensions = self.stream.get_mut().extensions(); + + // set remote addr + msg.inner_mut().addr = self.addr; + + // search handler for request + match self.settings.handler().handle(msg) { + Ok(mut task) => { + if self.tasks.is_empty() { + match task.poll_io(&mut self.stream) { + Ok(Async::Ready(ready)) => { + // override keep-alive state + if self.stream.keepalive() { + self.flags.insert(Flags::KEEPALIVE); + } else { + self.flags.remove(Flags::KEEPALIVE); + } + // prepare stream for next response + self.stream.reset(); + + if !ready { + // task is done with io operations + // but still needs to do more work + spawn(HttpHandlerTaskFut::new(task)); + } + } + Ok(Async::NotReady) => (), + Err(err) => { + error!("Unhandled error: {}", err); + self.client_disconnected(false); + return Err(HttpDispatchError::App(err)); + } + } + } else { + self.tasks.push_back(Entry::Task(task)); + } + } + Err(_) => { + // handler is not found + self.push_response_entry(StatusCode::NOT_FOUND); + } + } + Ok(()) + } + + pub(self) fn parse(&mut self) -> Result> { let mut updated = false; 'outer: loop { - match self.decoder.decode(&mut self.buf, &self.settings) { - Ok(Some(Message::Message { mut msg, payload })) => { + match self.decoder.decode(&mut self.buf) { + Ok(Some(Message::Message(msg))) => { updated = true; - self.flags.insert(Flags::STARTED); - - if payload { - let (ps, pl) = Payload::new(false); - *msg.inner.payload.borrow_mut() = Some(pl); - self.payload = Some(PayloadType::new(&msg.inner.headers, ps)); - } - - // stream extensions - msg.inner_mut().stream_extensions = - self.stream.get_mut().extensions(); - - // set remote addr - msg.inner_mut().addr = self.addr; - - // search handler for request - match self.settings.handler().handle(msg) { - Ok(mut task) => { - if self.tasks.is_empty() { - match task.poll_io(&mut self.stream) { - Ok(Async::Ready(ready)) => { - // override keep-alive state - if self.stream.keepalive() { - self.flags.insert(Flags::KEEPALIVE); - } else { - self.flags.remove(Flags::KEEPALIVE); - } - // prepare stream for next response - self.stream.reset(); - - if !ready { - // task is done with io operations - // but still needs to do more work - spawn(HttpHandlerTaskFut::new(task)); - } - continue 'outer; - } - Ok(Async::NotReady) => (), - Err(err) => { - error!("Unhandled error: {}", err); - self.client_disconnected(false); - return Err(err.into()); - } - } - } - self.tasks.push_back(Entry::Task(task)); - continue 'outer; - } - Err(_) => { - // handler is not found - self.push_response_entry(StatusCode::NOT_FOUND); - } - } + self.handle_message(msg, false)?; + } + Ok(Some(Message::MessageWithPayload(msg))) => { + updated = true; + self.handle_message(msg, true)?; } Ok(Some(Message::Chunk(chunk))) => { updated = true; @@ -556,8 +566,8 @@ where Err(e) => { if let Some(mut payload) = self.payload.take() { let e = match e { - DecoderError::Io(e) => PayloadError::Io(e), - DecoderError::Error(_) => PayloadError::EncodingCorrupted, + ParseError::Io(e) => PayloadError::Io(e), + _ => PayloadError::EncodingCorrupted, }; payload.set_error(e); } @@ -593,6 +603,7 @@ mod tests { use super::*; use application::{App, HttpApplication}; + use error::ParseError; use httpmessage::HttpMessage; use server::h1decoder::Message; use server::handler::IntoHttpHandler; @@ -612,13 +623,14 @@ mod tests { impl Message { fn message(self) -> Request { match self { - Message::Message { msg, payload: _ } => msg, + Message::Message(msg) => msg, + Message::MessageWithPayload(msg) => msg, _ => panic!("error"), } } fn is_payload(&self) -> bool { match *self { - Message::Message { msg: _, payload } => payload, + Message::MessageWithPayload(_) => true, _ => panic!("error"), } } @@ -639,7 +651,7 @@ mod tests { macro_rules! parse_ready { ($e:expr) => {{ let settings = wrk_settings(); - match H1Decoder::new().decode($e, &settings) { + match H1Decoder::new(settings.request_pool()).decode($e) { Ok(Some(msg)) => msg.message(), Ok(_) => unreachable!("Eof during parsing http request"), Err(err) => unreachable!("Error during parsing http request: {:?}", err), @@ -651,10 +663,10 @@ mod tests { ($e:expr) => {{ let settings = wrk_settings(); - match H1Decoder::new().decode($e, &settings) { + match H1Decoder::new(settings.request_pool()).decode($e) { Err(err) => match err { - DecoderError::Error(_) => (), - _ => unreachable!("Parse error expected"), + ParseError::Io(_) => unreachable!("Parse error expected"), + _ => (), }, _ => unreachable!("Error expected"), } @@ -747,8 +759,8 @@ mod tests { let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { + let mut reader = H1Decoder::new(settings.request_pool()); + match reader.decode(&mut buf) { Ok(Some(msg)) => { let req = msg.message(); assert_eq!(req.version(), Version::HTTP_11); @@ -764,14 +776,14 @@ mod tests { let mut buf = BytesMut::from("PUT /test HTTP/1"); let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { + let mut reader = H1Decoder::new(settings.request_pool()); + match reader.decode(&mut buf) { Ok(None) => (), _ => unreachable!("Error"), } buf.extend(b".1\r\n\r\n"); - match reader.decode(&mut buf, &settings) { + match reader.decode(&mut buf) { Ok(Some(msg)) => { let mut req = msg.message(); assert_eq!(req.version(), Version::HTTP_11); @@ -787,8 +799,8 @@ mod tests { let mut buf = BytesMut::from("POST /test2 HTTP/1.0\r\n\r\n"); let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { + let mut reader = H1Decoder::new(settings.request_pool()); + match reader.decode(&mut buf) { Ok(Some(msg)) => { let mut req = msg.message(); assert_eq!(req.version(), Version::HTTP_10); @@ -805,20 +817,15 @@ mod tests { BytesMut::from("GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { + let mut reader = H1Decoder::new(settings.request_pool()); + match reader.decode(&mut buf) { Ok(Some(msg)) => { let mut req = msg.message(); assert_eq!(req.version(), Version::HTTP_11); assert_eq!(*req.method(), Method::GET); assert_eq!(req.path(), "/test"); assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), + reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), b"body" ); } @@ -832,20 +839,15 @@ mod tests { BytesMut::from("\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { + let mut reader = H1Decoder::new(settings.request_pool()); + match reader.decode(&mut buf) { Ok(Some(msg)) => { let mut req = msg.message(); assert_eq!(req.version(), Version::HTTP_11); assert_eq!(*req.method(), Method::GET); assert_eq!(req.path(), "/test"); assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), + reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), b"body" ); } @@ -857,11 +859,11 @@ mod tests { fn test_parse_partial_eof() { let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); + let mut reader = H1Decoder::new(settings.request_pool()); + assert!(reader.decode(&mut buf).unwrap().is_none()); buf.extend(b"\r\n"); - match reader.decode(&mut buf, &settings) { + match reader.decode(&mut buf) { Ok(Some(msg)) => { let req = msg.message(); assert_eq!(req.version(), Version::HTTP_11); @@ -877,17 +879,17 @@ mod tests { let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - assert!{ reader.decode(&mut buf, &settings).unwrap().is_none() } + let mut reader = H1Decoder::new(settings.request_pool()); + assert!{ reader.decode(&mut buf).unwrap().is_none() } buf.extend(b"t"); - assert!{ reader.decode(&mut buf, &settings).unwrap().is_none() } + assert!{ reader.decode(&mut buf).unwrap().is_none() } buf.extend(b"es"); - assert!{ reader.decode(&mut buf, &settings).unwrap().is_none() } + assert!{ reader.decode(&mut buf).unwrap().is_none() } buf.extend(b"t: value\r\n\r\n"); - match reader.decode(&mut buf, &settings) { + match reader.decode(&mut buf) { Ok(Some(msg)) => { let req = msg.message(); assert_eq!(req.version(), Version::HTTP_11); @@ -907,8 +909,8 @@ mod tests { Set-Cookie: c2=cookie2\r\n\r\n", ); let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let mut reader = H1Decoder::new(settings.request_pool()); + let msg = reader.decode(&mut buf).unwrap().unwrap(); let req = msg.message(); let val: Vec<_> = req @@ -1109,19 +1111,14 @@ mod tests { upgrade: websocket\r\n\r\n\ some raw data", ); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let mut reader = H1Decoder::new(settings.request_pool()); + let msg = reader.decode(&mut buf).unwrap().unwrap(); assert!(msg.is_payload()); let req = msg.message(); assert!(!req.keep_alive()); assert!(req.upgrade()); assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), + reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), b"some raw data" ); } @@ -1169,32 +1166,22 @@ mod tests { transfer-encoding: chunked\r\n\r\n", ); let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let mut reader = H1Decoder::new(settings.request_pool()); + let msg = reader.decode(&mut buf).unwrap().unwrap(); assert!(msg.is_payload()); let req = msg.message(); assert!(req.chunked().unwrap()); buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), + reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), b"data" ); assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), + reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), b"line" ); - assert!(reader.decode(&mut buf, &settings).unwrap().unwrap().eof()); + assert!(reader.decode(&mut buf).unwrap().unwrap().eof()); } #[test] @@ -1204,8 +1191,8 @@ mod tests { transfer-encoding: chunked\r\n\r\n", ); let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let mut reader = H1Decoder::new(settings.request_pool()); + let msg = reader.decode(&mut buf).unwrap().unwrap(); assert!(msg.is_payload()); let req = msg.message(); assert!(req.chunked().unwrap()); @@ -1216,14 +1203,14 @@ mod tests { transfer-encoding: chunked\r\n\r\n" .iter(), ); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); assert_eq!(msg.chunk().as_ref(), b"data"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); assert_eq!(msg.chunk().as_ref(), b"line"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); assert!(msg.eof()); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); assert!(msg.is_payload()); let req2 = msg.message(); assert!(req2.chunked().unwrap()); @@ -1239,30 +1226,30 @@ mod tests { ); let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let mut reader = H1Decoder::new(settings.request_pool()); + let msg = reader.decode(&mut buf).unwrap().unwrap(); assert!(msg.is_payload()); let req = msg.message(); assert!(req.chunked().unwrap()); buf.extend(b"4\r\n1111\r\n"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); assert_eq!(msg.chunk().as_ref(), b"1111"); buf.extend(b"4\r\ndata\r"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); assert_eq!(msg.chunk().as_ref(), b"data"); buf.extend(b"\n4"); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); + assert!(reader.decode(&mut buf).unwrap().is_none()); buf.extend(b"\r"); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); + assert!(reader.decode(&mut buf).unwrap().is_none()); buf.extend(b"\n"); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); + assert!(reader.decode(&mut buf).unwrap().is_none()); buf.extend(b"li"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); assert_eq!(msg.chunk().as_ref(), b"li"); //trailers @@ -1270,12 +1257,12 @@ mod tests { //not_ready!(reader.parse(&mut buf, &mut readbuf)); buf.extend(b"ne\r\n0\r\n"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); assert_eq!(msg.chunk().as_ref(), b"ne"); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); + assert!(reader.decode(&mut buf).unwrap().is_none()); buf.extend(b"\r\n"); - assert!(reader.decode(&mut buf, &settings).unwrap().unwrap().eof()); + assert!(reader.decode(&mut buf).unwrap().unwrap().eof()); } #[test] @@ -1286,17 +1273,17 @@ mod tests { ); let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let mut reader = H1Decoder::new(settings.request_pool()); + let msg = reader.decode(&mut buf).unwrap().unwrap(); assert!(msg.is_payload()); assert!(msg.message().chunked().unwrap()); buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n") - let chunk = reader.decode(&mut buf, &settings).unwrap().unwrap().chunk(); + let chunk = reader.decode(&mut buf).unwrap().unwrap().chunk(); assert_eq!(chunk, Bytes::from_static(b"data")); - let chunk = reader.decode(&mut buf, &settings).unwrap().unwrap().chunk(); + let chunk = reader.decode(&mut buf).unwrap().unwrap().chunk(); assert_eq!(chunk, Bytes::from_static(b"line")); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); assert!(msg.eof()); } } diff --git a/src/server/h1codec.rs b/src/server/h1codec.rs new file mode 100644 index 000000000..ea56110d3 --- /dev/null +++ b/src/server/h1codec.rs @@ -0,0 +1,251 @@ +#![allow(unused_imports, unused_variables, dead_code)] +use std::io::{self, Write}; + +use bytes::{BufMut, Bytes, BytesMut}; +use tokio_codec::{Decoder, Encoder}; + +use super::h1decoder::{H1Decoder, Message}; +use super::helpers; +use super::message::RequestPool; +use super::output::{ResponseInfo, ResponseLength}; +use body::Body; +use error::ParseError; +use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; +use http::Version; +use httpresponse::HttpResponse; + +pub(crate) enum OutMessage { + Response(HttpResponse), + Payload(Bytes), +} + +pub(crate) struct H1Codec { + decoder: H1Decoder, + encoder: H1Writer, +} + +impl H1Codec { + pub fn new(pool: &'static RequestPool) -> Self { + H1Codec { + decoder: H1Decoder::new(pool), + encoder: H1Writer::new(), + } + } +} + +impl Decoder for H1Codec { + type Item = Message; + type Error = ParseError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + self.decoder.decode(src) + } +} + +impl Encoder for H1Codec { + type Item = OutMessage; + type Error = io::Error; + + fn encode( + &mut self, item: Self::Item, dst: &mut BytesMut, + ) -> Result<(), Self::Error> { + match item { + OutMessage::Response(res) => { + self.encoder.encode(res, dst)?; + } + OutMessage::Payload(bytes) => { + dst.extend_from_slice(&bytes); + } + } + Ok(()) + } +} + +bitflags! { + struct Flags: u8 { + const STARTED = 0b0000_0001; + const UPGRADE = 0b0000_0010; + const KEEPALIVE = 0b0000_0100; + const DISCONNECTED = 0b0000_1000; + } +} + +const AVERAGE_HEADER_SIZE: usize = 30; + +struct H1Writer { + flags: Flags, + written: u64, + headers_size: u32, +} + +impl H1Writer { + fn new() -> H1Writer { + H1Writer { + flags: Flags::empty(), + written: 0, + headers_size: 0, + } + } + + fn written(&self) -> u64 { + self.written + } + + pub fn reset(&mut self) { + self.written = 0; + self.flags = Flags::KEEPALIVE; + } + + pub fn upgrade(&self) -> bool { + self.flags.contains(Flags::UPGRADE) + } + + pub fn keepalive(&self) -> bool { + self.flags.contains(Flags::KEEPALIVE) && !self.flags.contains(Flags::UPGRADE) + } + + fn encode( + &mut self, mut msg: HttpResponse, buffer: &mut BytesMut, + ) -> io::Result<()> { + // prepare task + let info = ResponseInfo::new(false); // req.inner.method == Method::HEAD); + + //if msg.keep_alive().unwrap_or_else(|| req.keep_alive()) { + //self.flags = Flags::STARTED | Flags::KEEPALIVE; + //} else { + self.flags = Flags::STARTED; + //} + + // Connection upgrade + let version = msg.version().unwrap_or_else(|| Version::HTTP_11); //req.inner.version); + if msg.upgrade() { + self.flags.insert(Flags::UPGRADE); + msg.headers_mut() + .insert(CONNECTION, HeaderValue::from_static("upgrade")); + } + // keep-alive + else if self.flags.contains(Flags::KEEPALIVE) { + if version < Version::HTTP_11 { + msg.headers_mut() + .insert(CONNECTION, HeaderValue::from_static("keep-alive")); + } + } else if version >= Version::HTTP_11 { + msg.headers_mut() + .insert(CONNECTION, HeaderValue::from_static("close")); + } + let body = msg.replace_body(Body::Empty); + + // render message + { + let reason = msg.reason().as_bytes(); + if let Body::Binary(ref bytes) = body { + buffer.reserve( + 256 + msg.headers().len() * AVERAGE_HEADER_SIZE + + bytes.len() + + reason.len(), + ); + } else { + buffer.reserve( + 256 + msg.headers().len() * AVERAGE_HEADER_SIZE + reason.len(), + ); + } + + // status line + helpers::write_status_line(version, msg.status().as_u16(), buffer); + buffer.extend_from_slice(reason); + + // content length + match info.length { + ResponseLength::Chunked => { + buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") + } + ResponseLength::Zero => { + buffer.extend_from_slice(b"\r\ncontent-length: 0\r\n") + } + ResponseLength::Length(len) => { + helpers::write_content_length(len, buffer) + } + ResponseLength::Length64(len) => { + buffer.extend_from_slice(b"\r\ncontent-length: "); + write!(buffer.writer(), "{}", len)?; + buffer.extend_from_slice(b"\r\n"); + } + ResponseLength::None => buffer.extend_from_slice(b"\r\n"), + } + if let Some(ce) = info.content_encoding { + buffer.extend_from_slice(b"content-encoding: "); + buffer.extend_from_slice(ce.as_ref()); + buffer.extend_from_slice(b"\r\n"); + } + + // write headers + let mut pos = 0; + let mut has_date = false; + let mut remaining = buffer.remaining_mut(); + let mut buf = unsafe { &mut *(buffer.bytes_mut() as *mut [u8]) }; + for (key, value) in msg.headers() { + match *key { + TRANSFER_ENCODING => continue, + CONTENT_LENGTH => match info.length { + ResponseLength::None => (), + _ => continue, + }, + DATE => { + has_date = true; + } + _ => (), + } + + let v = value.as_ref(); + let k = key.as_str().as_bytes(); + let len = k.len() + v.len() + 4; + if len > remaining { + unsafe { + buffer.advance_mut(pos); + } + pos = 0; + buffer.reserve(len); + remaining = buffer.remaining_mut(); + unsafe { + buf = &mut *(buffer.bytes_mut() as *mut _); + } + } + + buf[pos..pos + k.len()].copy_from_slice(k); + pos += k.len(); + buf[pos..pos + 2].copy_from_slice(b": "); + pos += 2; + buf[pos..pos + v.len()].copy_from_slice(v); + pos += v.len(); + buf[pos..pos + 2].copy_from_slice(b"\r\n"); + pos += 2; + remaining -= len; + } + unsafe { + buffer.advance_mut(pos); + } + + // optimized date header, set_date writes \r\n + if !has_date { + // self.settings.set_date(&mut buffer, true); + buffer.extend_from_slice(b"\r\n"); + } else { + // msg eof + buffer.extend_from_slice(b"\r\n"); + } + self.headers_size = buffer.len() as u32; + } + + if let Body::Binary(bytes) = body { + self.written = bytes.len() as u64; + // buffer.write(bytes.as_ref())?; + buffer.extend_from_slice(bytes.as_ref()); + } else { + // capacity, makes sense only for streaming or actor + // self.buffer_capacity = msg.write_buffer_capacity(); + + msg.replace_body(body); + } + Ok(()) + } +} diff --git a/src/server/h1decoder.rs b/src/server/h1decoder.rs index 434dc42df..c6f0974aa 100644 --- a/src/server/h1decoder.rs +++ b/src/server/h1decoder.rs @@ -4,8 +4,7 @@ use bytes::{Bytes, BytesMut}; use futures::{Async, Poll}; use httparse; -use super::message::{MessageFlags, Request}; -use super::settings::ServiceConfig; +use super::message::{MessageFlags, Request, RequestPool}; use error::ParseError; use http::header::{HeaderName, HeaderValue}; use http::{header, HttpTryFrom, Method, Uri, Version}; @@ -16,35 +15,26 @@ const MAX_HEADERS: usize = 96; pub(crate) struct H1Decoder { decoder: Option, + pool: &'static RequestPool, } #[derive(Debug)] -pub(crate) enum Message { - Message { msg: Request, payload: bool }, +pub enum Message { + Message(Request), + MessageWithPayload(Request), Chunk(Bytes), Eof, } -#[derive(Debug)] -pub(crate) enum DecoderError { - Io(io::Error), - Error(ParseError), -} - -impl From for DecoderError { - fn from(err: io::Error) -> DecoderError { - DecoderError::Io(err) - } -} - impl H1Decoder { - pub fn new() -> H1Decoder { - H1Decoder { decoder: None } + pub fn new(pool: &'static RequestPool) -> H1Decoder { + H1Decoder { + pool, + decoder: None, + } } - pub fn decode( - &mut self, src: &mut BytesMut, settings: &ServiceConfig, - ) -> Result, DecoderError> { + pub fn decode(&mut self, src: &mut BytesMut) -> Result, ParseError> { // read payload if self.decoder.is_some() { match self.decoder.as_mut().unwrap().decode(src)? { @@ -57,21 +47,19 @@ impl H1Decoder { } } - match self - .parse_message(src, settings) - .map_err(DecoderError::Error)? - { + match self.parse_message(src)? { Async::Ready((msg, decoder)) => { self.decoder = decoder; - Ok(Some(Message::Message { - msg, - payload: self.decoder.is_some(), - })) + if self.decoder.is_some() { + Ok(Some(Message::MessageWithPayload(msg))) + } else { + Ok(Some(Message::Message(msg))) + } } Async::NotReady => { if src.len() >= MAX_BUFFER_SIZE { error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); - Err(DecoderError::Error(ParseError::TooLarge)) + Err(ParseError::TooLarge) } else { Ok(None) } @@ -79,8 +67,8 @@ impl H1Decoder { } } - fn parse_message( - &self, buf: &mut BytesMut, settings: &ServiceConfig, + fn parse_message( + &self, buf: &mut BytesMut, ) -> Poll<(Request, Option), ParseError> { // Parse http message let mut has_upgrade = false; @@ -119,7 +107,7 @@ impl H1Decoder { let slice = buf.split_to(len).freeze(); // convert headers - let mut msg = settings.get_request(); + let mut msg = RequestPool::get(self.pool); { let inner = msg.inner_mut(); inner diff --git a/src/server/h1disp.rs b/src/server/h1disp.rs new file mode 100644 index 000000000..b1c2c8a21 --- /dev/null +++ b/src/server/h1disp.rs @@ -0,0 +1,425 @@ +// #![allow(unused_imports, unused_variables, dead_code)] +use std::collections::VecDeque; +use std::fmt::{Debug, Display}; +use std::net::SocketAddr; +// use std::time::{Duration, Instant}; + +use actix_net::service::Service; + +use futures::{Async, AsyncSink, Future, Poll, Sink, Stream}; +use tokio_codec::Framed; +// use tokio_current_thread::spawn; +use tokio_io::AsyncWrite; +// use tokio_timer::Delay; + +use error::{ParseError, PayloadError}; +use payload::{Payload, PayloadStatus, PayloadWriter}; + +use body::Body; +use httpresponse::HttpResponse; + +use super::error::HttpDispatchError; +use super::h1codec::{H1Codec, OutMessage}; +use super::h1decoder::Message; +use super::input::PayloadType; +use super::message::{Request, RequestPool}; +use super::IoStream; + +const MAX_PIPELINED_MESSAGES: usize = 16; + +bitflags! { + pub struct Flags: u8 { + const STARTED = 0b0000_0001; + const KEEPALIVE_ENABLED = 0b0000_0010; + const KEEPALIVE = 0b0000_0100; + const SHUTDOWN = 0b0000_1000; + const READ_DISCONNECTED = 0b0001_0000; + const WRITE_DISCONNECTED = 0b0010_0000; + const POLLED = 0b0100_0000; + const FLUSHED = 0b1000_0000; + } +} + +/// Dispatcher for HTTP/1.1 protocol +pub struct Http1Dispatcher +where + S::Error: Debug + Display, +{ + service: S, + flags: Flags, + addr: Option, + framed: Framed, + error: Option>, + + state: State, + payload: Option, + messages: VecDeque, +} + +enum State { + None, + Response(S::Future), + SendResponse(Option), + SendResponseWithPayload(Option<(OutMessage, Body)>), + Payload(Body), +} + +impl State { + fn is_empty(&self) -> bool { + if let State::None = self { + true + } else { + false + } + } +} + +impl Http1Dispatcher +where + T: IoStream, + S: Service, + S::Error: Debug + Display, +{ + pub fn new(stream: T, pool: &'static RequestPool, service: S) -> Self { + let addr = stream.peer_addr(); + let flags = Flags::FLUSHED; + let codec = H1Codec::new(pool); + let framed = Framed::new(stream, codec); + + Http1Dispatcher { + payload: None, + state: State::None, + error: None, + messages: VecDeque::new(), + service, + flags, + addr, + framed, + } + } + + #[inline] + fn can_read(&self) -> bool { + if self.flags.contains(Flags::READ_DISCONNECTED) { + return false; + } + + if let Some(ref info) = self.payload { + info.need_read() == PayloadStatus::Read + } else { + true + } + } + + // if checked is set to true, delay disconnect until all tasks have finished. + fn client_disconnected(&mut self, checked: bool) { + self.flags.insert(Flags::READ_DISCONNECTED); + if let Some(mut payload) = self.payload.take() { + payload.set_error(PayloadError::Incomplete); + } + + // if !checked || self.tasks.is_empty() { + // self.flags + // .insert(Flags::WRITE_DISCONNECTED | Flags::FLUSHED); + + // // notify tasks + // for mut task in self.tasks.drain(..) { + // task.disconnected(); + // match task.poll_completed() { + // Ok(Async::NotReady) => { + // // spawn not completed task, it does not require access to io + // // at this point + // spawn(HttpHandlerTaskFut::new(task.into_task())); + // } + // Ok(Async::Ready(_)) => (), + // Err(err) => { + // error!("Unhandled application error: {}", err); + // } + // } + // } + // } + } + + /// Flush stream + fn poll_flush(&mut self) -> Poll<(), HttpDispatchError> { + if self.flags.contains(Flags::STARTED) && !self.flags.contains(Flags::FLUSHED) { + match self.framed.poll_complete() { + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(err) => { + debug!("Error sending data: {}", err); + self.client_disconnected(false); + Err(err.into()) + } + Ok(Async::Ready(_)) => { + // if payload is not consumed we can not use connection + if self.payload.is_some() && self.state.is_empty() { + return Err(HttpDispatchError::PayloadIsNotConsumed); + } + self.flags.insert(Flags::FLUSHED); + Ok(Async::Ready(())) + } + } + } else { + Ok(Async::Ready(())) + } + } + + pub(self) fn poll_handler(&mut self) -> Result<(), HttpDispatchError> { + self.poll_io()?; + let mut retry = self.can_read(); + + // process + loop { + let state = match self.state { + State::None => loop { + break if let Some(msg) = self.messages.pop_front() { + let mut task = self.service.call(msg); + match task.poll() { + Ok(Async::Ready(res)) => { + if res.body().is_streaming() { + unimplemented!() + } else { + Some(Ok(State::SendResponse(Some( + OutMessage::Response(res), + )))) + } + } + Ok(Async::NotReady) => Some(Ok(State::Response(task))), + Err(err) => Some(Err(HttpDispatchError::App(err))), + } + } else { + None + }; + }, + State::Payload(ref mut body) => unimplemented!(), + State::Response(ref mut fut) => { + match fut.poll() { + Ok(Async::Ready(res)) => { + if res.body().is_streaming() { + unimplemented!() + } else { + Some(Ok(State::SendResponse(Some( + OutMessage::Response(res), + )))) + } + } + Ok(Async::NotReady) => None, + Err(err) => { + // it is not possible to recover from error + // during pipe handling, so just drop connection + Some(Err(HttpDispatchError::App(err))) + } + } + } + State::SendResponse(ref mut item) => { + let msg = item.take().expect("SendResponse is empty"); + match self.framed.start_send(msg) { + Ok(AsyncSink::Ready) => { + self.flags.remove(Flags::FLUSHED); + Some(Ok(State::None)) + } + Ok(AsyncSink::NotReady(msg)) => { + *item = Some(msg); + return Ok(()); + } + Err(err) => Some(Err(HttpDispatchError::Io(err))), + } + } + State::SendResponseWithPayload(ref mut item) => { + let (msg, body) = item.take().expect("SendResponse is empty"); + match self.framed.start_send(msg) { + Ok(AsyncSink::Ready) => { + self.flags.remove(Flags::FLUSHED); + Some(Ok(State::Payload(body))) + } + Ok(AsyncSink::NotReady(msg)) => { + *item = Some((msg, body)); + return Ok(()); + } + Err(err) => Some(Err(HttpDispatchError::Io(err))), + } + } + }; + + match state { + Some(Ok(state)) => self.state = state, + Some(Err(err)) => { + // error!("Unhandled error1: {}", err); + self.client_disconnected(false); + return Err(err); + } + None => { + // if read-backpressure is enabled and we consumed some data. + // we may read more dataand retry + if !retry && self.can_read() && self.poll_io()? { + retry = self.can_read(); + continue; + } + break; + } + } + } + + Ok(()) + } + + fn one_message(&mut self, msg: Message) -> Result<(), HttpDispatchError> { + self.flags.insert(Flags::STARTED); + + match msg { + Message::Message(mut msg) => { + // set remote addr + msg.inner_mut().addr = self.addr; + + // handle request early + if self.state.is_empty() { + let mut task = self.service.call(msg); + match task.poll() { + Ok(Async::Ready(res)) => { + if res.body().is_streaming() { + unimplemented!() + } else { + self.state = + State::SendResponse(Some(OutMessage::Response(res))); + } + } + Ok(Async::NotReady) => self.state = State::Response(task), + Err(err) => { + error!("Unhandled application error: {}", err); + self.client_disconnected(false); + return Err(HttpDispatchError::App(err)); + } + } + } else { + self.messages.push_back(msg); + } + } + Message::MessageWithPayload(mut msg) => { + // set remote addr + msg.inner_mut().addr = self.addr; + + // payload + let (ps, pl) = Payload::new(false); + *msg.inner.payload.borrow_mut() = Some(pl); + self.payload = Some(PayloadType::new(&msg.inner.headers, ps)); + + self.messages.push_back(msg); + } + Message::Chunk(chunk) => { + if let Some(ref mut payload) = self.payload { + payload.feed_data(chunk); + } else { + error!("Internal server error: unexpected payload chunk"); + self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); + // self.push_response_entry(StatusCode::INTERNAL_SERVER_ERROR); + self.error = Some(HttpDispatchError::InternalError); + } + } + Message::Eof => { + if let Some(mut payload) = self.payload.take() { + payload.feed_eof(); + } else { + error!("Internal server error: unexpected eof"); + self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); + // self.push_response_entry(StatusCode::INTERNAL_SERVER_ERROR); + self.error = Some(HttpDispatchError::InternalError); + } + } + } + + Ok(()) + } + + pub(self) fn poll_io(&mut self) -> Result> { + let mut updated = false; + + if self.messages.len() < MAX_PIPELINED_MESSAGES { + 'outer: loop { + match self.framed.poll() { + Ok(Async::Ready(Some(msg))) => { + updated = true; + self.one_message(msg)?; + } + Ok(Async::Ready(None)) => { + if self.flags.contains(Flags::READ_DISCONNECTED) { + self.client_disconnected(true); + } + break; + } + Ok(Async::NotReady) => break, + Err(e) => { + if let Some(mut payload) = self.payload.take() { + let e = match e { + ParseError::Io(e) => PayloadError::Io(e), + _ => PayloadError::EncodingCorrupted, + }; + payload.set_error(e); + } + + // Malformed requests should be responded with 400 + // self.push_response_entry(StatusCode::BAD_REQUEST); + self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); + self.error = Some(HttpDispatchError::MalformedRequest); + break; + } + } + } + } + + Ok(updated) + } +} + +impl Future for Http1Dispatcher +where + T: IoStream, + S: Service, + S::Error: Debug + Display, +{ + type Item = (); + type Error = HttpDispatchError; + + #[inline] + fn poll(&mut self) -> Poll<(), Self::Error> { + // shutdown + if self.flags.contains(Flags::SHUTDOWN) { + if self.flags.contains(Flags::WRITE_DISCONNECTED) { + return Ok(Async::Ready(())); + } + try_ready!(self.poll_flush()); + return Ok(AsyncWrite::shutdown(self.framed.get_mut())?); + } + + // process incoming requests + if !self.flags.contains(Flags::WRITE_DISCONNECTED) { + self.poll_handler()?; + + // flush stream + self.poll_flush()?; + + // deal with keep-alive and stream eof (client-side write shutdown) + if self.state.is_empty() && self.flags.contains(Flags::FLUSHED) { + // handle stream eof + if self + .flags + .intersects(Flags::READ_DISCONNECTED | Flags::WRITE_DISCONNECTED) + { + return Ok(Async::Ready(())); + } + // no keep-alive + if self.flags.contains(Flags::STARTED) + && (!self.flags.contains(Flags::KEEPALIVE_ENABLED) + || !self.flags.contains(Flags::KEEPALIVE)) + { + self.flags.insert(Flags::SHUTDOWN); + return self.poll(); + } + } + Ok(Async::NotReady) + } else if let Some(err) = self.error.take() { + Err(err) + } else { + Ok(Async::Ready(())) + } + } +} diff --git a/src/server/h2.rs b/src/server/h2.rs index 2fe2fa073..27ae47851 100644 --- a/src/server/h2.rs +++ b/src/server/h2.rs @@ -86,7 +86,7 @@ where &self.settings } - pub fn poll(&mut self) -> Poll<(), HttpDispatchError> { + pub fn poll(&mut self) -> Poll<(), HttpDispatchError> { // server if let State::Connection(ref mut conn) = self.state { // keep-alive timer diff --git a/src/server/message.rs b/src/server/message.rs index 9c4bc1ec4..74ec5f17c 100644 --- a/src/server/message.rs +++ b/src/server/message.rs @@ -241,10 +241,7 @@ impl fmt::Debug for Request { } } -pub(crate) struct RequestPool( - RefCell>>, - RefCell, -); +pub struct RequestPool(RefCell>>, RefCell); thread_local!(static POOL: &'static RequestPool = RequestPool::create()); @@ -257,7 +254,7 @@ impl RequestPool { Box::leak(Box::new(pool)) } - pub fn pool(settings: ServerSettings) -> &'static RequestPool { + pub(crate) fn pool(settings: ServerSettings) -> &'static RequestPool { POOL.with(|p| { *p.1.borrow_mut() = settings; *p @@ -275,7 +272,7 @@ impl RequestPool { #[inline] /// Release request instance - pub fn release(&self, msg: Rc) { + pub(crate) fn release(&self, msg: Rc) { let v = &mut self.0.borrow_mut(); if v.len() < 128 { v.push_front(msg); diff --git a/src/server/mod.rs b/src/server/mod.rs index 3277dba5a..ce3f2fbf6 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -122,7 +122,10 @@ pub(crate) mod builder; mod channel; mod error; pub(crate) mod h1; -pub(crate) mod h1decoder; +#[doc(hidden)] +pub mod h1codec; +#[doc(hidden)] +pub mod h1decoder; mod h1writer; mod h2; mod h2writer; @@ -145,6 +148,9 @@ pub use self::ssl::*; pub use self::error::{AcceptorError, HttpDispatchError}; pub use self::settings::ServerSettings; +#[doc(hidden)] +pub mod h1disp; + #[doc(hidden)] pub use self::acceptor::AcceptorTimeout; diff --git a/src/server/output.rs b/src/server/output.rs index 70c24facc..1da7e9025 100644 --- a/src/server/output.rs +++ b/src/server/output.rs @@ -10,7 +10,7 @@ use bytes::BytesMut; use flate2::write::{GzEncoder, ZlibEncoder}; #[cfg(feature = "flate2")] use flate2::Compression; -use http::header::{ACCEPT_ENCODING, CONTENT_LENGTH}; +use http::header::{HeaderValue, ACCEPT_ENCODING, CONTENT_LENGTH}; use http::{StatusCode, Version}; use super::message::InnerRequest; @@ -18,6 +18,12 @@ use body::{Binary, Body}; use header::ContentEncoding; use httpresponse::HttpResponse; +// #[derive(Debug)] +// pub(crate) struct RequestInfo { +// pub version: Version, +// pub accept_encoding: Option, +// } + #[derive(Debug)] pub(crate) enum ResponseLength { Chunked, diff --git a/src/server/service.rs b/src/server/service.rs index e3402e305..a55c33f72 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -10,6 +10,7 @@ use super::error::HttpDispatchError; use super::handler::HttpHandler; use super::settings::ServiceConfig; use super::IoStream; +use error::Error; /// `NewService` implementation for HTTP1/HTTP2 transports pub struct HttpService @@ -42,7 +43,7 @@ where { type Request = Io; type Response = (); - type Error = HttpDispatchError; + type Error = HttpDispatchError; type InitError = (); type Service = HttpServiceHandler; type Future = FutureResult; @@ -81,7 +82,7 @@ where { type Request = Io; type Response = (); - type Error = HttpDispatchError; + type Error = HttpDispatchError; type Future = HttpChannel; fn poll_ready(&mut self) -> Poll<(), Self::Error> { @@ -124,7 +125,7 @@ where { type Request = Io; type Response = (); - type Error = HttpDispatchError; + type Error = HttpDispatchError; type InitError = (); type Service = H1ServiceHandler; type Future = FutureResult; @@ -164,7 +165,7 @@ where { type Request = Io; type Response = (); - type Error = HttpDispatchError; + type Error = HttpDispatchError; type Future = H1Channel; fn poll_ready(&mut self) -> Poll<(), Self::Error> { diff --git a/src/server/settings.rs b/src/server/settings.rs index 9b27ed5e5..9df1e457f 100644 --- a/src/server/settings.rs +++ b/src/server/settings.rs @@ -215,6 +215,11 @@ impl ServiceConfig { RequestPool::get(self.0.messages) } + #[doc(hidden)] + pub fn request_pool(&self) -> &'static RequestPool { + self.0.messages + } + fn update_date(&self) { // Unsafe: WorkerSetting is !Sync and !Send unsafe { (*self.0.date.get()).0 = false }; diff --git a/tests/test_h1v2.rs b/tests/test_h1v2.rs new file mode 100644 index 000000000..7e8e9a42c --- /dev/null +++ b/tests/test_h1v2.rs @@ -0,0 +1,58 @@ +extern crate actix; +extern crate actix_net; +extern crate actix_web; +extern crate futures; + +use std::thread; + +use actix::System; +use actix_net::server::Server; +use actix_net::service::{IntoNewService, IntoService}; +use futures::future; + +use actix_web::server::h1disp::Http1Dispatcher; +use actix_web::server::KeepAlive; +use actix_web::server::ServiceConfig; +use actix_web::{client, test, App, Error, HttpRequest, HttpResponse}; + +#[test] +fn test_h1_v2() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + let app = App::new() + .route("/", http::Method::GET, |_: HttpRequest| "OK") + .finish(); + let settings = ServiceConfig::build(app) + .keep_alive(KeepAlive::Disabled) + .client_timeout(1000) + .client_shutdown(1000) + .server_hostname("localhost") + .server_address(addr) + .finish(); + + (move |io| { + let pool = settings.request_pool(); + Http1Dispatcher::new( + io, + pool, + (|req| { + println!("REQ: {:?}", req); + future::ok::<_, Error>(HttpResponse::Ok().finish()) + }).into_service(), + ) + }).into_new_service() + }).unwrap() + .run(); + }); + + let mut sys = System::new("test"); + { + let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) + .finish() + .unwrap(); + let response = sys.block_on(req.send()).unwrap(); + assert!(response.status().is_success()); + } +} From 6aa2de7b8d655f9fd0dc25d67c90d32a580a101c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 4 Oct 2018 17:00:27 -0700 Subject: [PATCH 002/427] remove actix-web artifacts --- CHANGES.md | 712 +----------- Cargo.toml | 15 +- MIGRATION.md | 176 --- Makefile | 14 - README.md | 44 +- build.rs | 16 - src/application.rs | 879 --------------- src/body.rs | 29 +- src/client/connector.rs | 1344 ---------------------- src/client/mod.rs | 118 -- src/client/parser.rs | 238 ---- src/client/pipeline.rs | 552 --------- src/client/request.rs | 782 ------------- src/client/response.rs | 124 --- src/client/writer.rs | 412 ------- src/context.rs | 294 ----- src/de.rs | 443 -------- src/error.rs | 15 +- src/extractor.rs | 1024 ----------------- src/fs.rs | 1786 ------------------------------ src/handler.rs | 562 ---------- src/helpers.rs | 571 ---------- src/httpmessage.rs | 41 - src/httpresponse.rs | 110 +- src/json.rs | 99 +- src/lib.rs | 101 +- src/middleware/cors.rs | 1183 -------------------- src/middleware/csrf.rs | 275 ----- src/middleware/defaultheaders.rs | 120 -- src/middleware/errhandlers.rs | 141 --- src/middleware/identity.rs | 387 ------- src/middleware/logger.rs | 384 ------- src/middleware/mod.rs | 68 -- src/middleware/session.rs | 617 ----------- src/multipart.rs | 815 -------------- src/param.rs | 303 ----- src/pipeline.rs | 869 --------------- src/pred.rs | 328 ------ src/resource.rs | 324 ------ src/route.rs | 666 ----------- src/router.rs | 1247 --------------------- src/scope.rs | 1236 --------------------- src/server/acceptor.rs | 396 ------- src/server/builder.rs | 117 -- src/server/channel.rs | 436 -------- src/server/error.rs | 29 - src/server/h1writer.rs | 356 ------ src/server/h2.rs | 453 -------- src/server/h2writer.rs | 259 ----- src/server/handler.rs | 208 ---- src/server/http.rs | 584 ---------- src/server/incoming.rs | 69 -- src/server/mod.rs | 147 +-- src/server/output.rs | 2 +- src/server/settings.rs | 44 +- src/server/ssl/mod.rs | 12 - src/server/ssl/nativetls.rs | 34 - src/server/ssl/openssl.rs | 87 -- src/server/ssl/rustls.rs | 87 -- src/with.rs | 383 ------- tests/test_client.rs | 508 --------- tests/test_custom_pipeline.rs | 81 -- tests/test_h1v2.rs | 14 +- tests/test_handlers.rs | 677 ----------- tests/test_middleware.rs | 1055 ------------------ tests/test_server.rs | 1357 ----------------------- tests/test_ws.rs | 394 ------- 67 files changed, 51 insertions(+), 27202 deletions(-) delete mode 100644 MIGRATION.md delete mode 100644 Makefile delete mode 100644 build.rs delete mode 100644 src/application.rs delete mode 100644 src/client/connector.rs delete mode 100644 src/client/mod.rs delete mode 100644 src/client/parser.rs delete mode 100644 src/client/pipeline.rs delete mode 100644 src/client/request.rs delete mode 100644 src/client/response.rs delete mode 100644 src/client/writer.rs delete mode 100644 src/context.rs delete mode 100644 src/de.rs delete mode 100644 src/extractor.rs delete mode 100644 src/fs.rs delete mode 100644 src/handler.rs delete mode 100644 src/helpers.rs delete mode 100644 src/middleware/cors.rs delete mode 100644 src/middleware/csrf.rs delete mode 100644 src/middleware/defaultheaders.rs delete mode 100644 src/middleware/errhandlers.rs delete mode 100644 src/middleware/identity.rs delete mode 100644 src/middleware/logger.rs delete mode 100644 src/middleware/mod.rs delete mode 100644 src/middleware/session.rs delete mode 100644 src/multipart.rs delete mode 100644 src/param.rs delete mode 100644 src/pipeline.rs delete mode 100644 src/pred.rs delete mode 100644 src/resource.rs delete mode 100644 src/route.rs delete mode 100644 src/router.rs delete mode 100644 src/scope.rs delete mode 100644 src/server/acceptor.rs delete mode 100644 src/server/builder.rs delete mode 100644 src/server/channel.rs delete mode 100644 src/server/h1writer.rs delete mode 100644 src/server/h2.rs delete mode 100644 src/server/h2writer.rs delete mode 100644 src/server/handler.rs delete mode 100644 src/server/http.rs delete mode 100644 src/server/incoming.rs delete mode 100644 src/server/ssl/mod.rs delete mode 100644 src/server/ssl/nativetls.rs delete mode 100644 src/server/ssl/openssl.rs delete mode 100644 src/server/ssl/rustls.rs delete mode 100644 src/with.rs delete mode 100644 tests/test_client.rs delete mode 100644 tests/test_custom_pipeline.rs delete mode 100644 tests/test_handlers.rs delete mode 100644 tests/test_middleware.rs delete mode 100644 tests/test_server.rs delete mode 100644 tests/test_ws.rs diff --git a/CHANGES.md b/CHANGES.md index 3c55c3f64..c3c38011c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,713 +1,5 @@ # Changes -## [0.7.9] - 2018-09-x +## [0.1.0] - 2018-09-x -### Added - -* Added client shutdown timeout setting - -* Added slow request timeout setting - -* Respond with 408 response on slow request timeout #523 - - -### Fixed - -* HTTP1 decoding errors are reported to the client. #512 - -* Correctly compose multiple allowed origins in CORS. #517 - -* Websocket server finished() isn't called if client disconnects #511 - -* Responses with the following codes: 100, 101, 102, 204 -- are sent without Content-Length header. #521 - -* Correct usage of `no_http2` flag in `bind_*` methods. #519 - - -## [0.7.8] - 2018-09-17 - -### Added - -* Use server `Keep-Alive` setting as slow request timeout #439 - -### Changed - -* Use 5 seconds keep-alive timer by default. - -### Fixed - -* Fixed wrong error message for i16 type #510 - - -## [0.7.7] - 2018-09-11 - -### Fixed - -* Fix linked list of HttpChannels #504 - -* Fix requests to TestServer fail #508 - - -## [0.7.6] - 2018-09-07 - -### Fixed - -* Fix system_exit in HttpServer #501 - -* Fix parsing of route param containin regexes with repetition #500 - -### Changes - -* Unhide `SessionBackend` and `SessionImpl` traits #455 - - -## [0.7.5] - 2018-09-04 - -### Added - -* Added the ability to pass a custom `TlsConnector`. - -* Allow to register handlers on scope level #465 - - -### Fixed - -* Handle socket read disconnect - -* Handling scoped paths without leading slashes #460 - - -### Changed - -* Read client response until eof if connection header set to close #464 - - -## [0.7.4] - 2018-08-23 - -### Added - -* Added `HttpServer::maxconn()` and `HttpServer::maxconnrate()`, - accept backpressure #250 - -* Allow to customize connection handshake process via `HttpServer::listen_with()` - and `HttpServer::bind_with()` methods - -* Support making client connections via `tokio-uds`'s `UnixStream` when "uds" feature is enabled #472 - -### Changed - -* It is allowed to use function with up to 10 parameters for handler with `extractor parameters`. - `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple - even for handler with one parameter. - -* native-tls - 0.2 - -* `Content-Disposition` is re-worked. Its parser is now more robust and handles quoted content better. See #461 - -### Fixed - -* Use zlib instead of raw deflate for decoding and encoding payloads with - `Content-Encoding: deflate`. - -* Fixed headers formating for CORS Middleware Access-Control-Expose-Headers #436 - -* Fix adding multiple response headers #446 - -* Client includes port in HOST header when it is not default(e.g. not 80 and 443). #448 - -* Panic during access without routing being set #452 - -* Fixed http/2 error handling - -### Deprecated - -* `HttpServer::no_http2()` is deprecated, use `OpensslAcceptor::with_flags()` or - `RustlsAcceptor::with_flags()` instead - -* `HttpServer::listen_tls()`, `HttpServer::listen_ssl()`, `HttpServer::listen_rustls()` have been - deprecated in favor of `HttpServer::listen_with()` with specific `acceptor`. - -* `HttpServer::bind_tls()`, `HttpServer::bind_ssl()`, `HttpServer::bind_rustls()` have been - deprecated in favor of `HttpServer::bind_with()` with specific `acceptor`. - - -## [0.7.3] - 2018-08-01 - -### Added - -* Support HTTP/2 with rustls #36 - -* Allow TestServer to open a websocket on any URL (TestServer::ws_at()) #433 - -### Fixed - -* Fixed failure 0.1.2 compatibility - -* Do not override HOST header for client request #428 - -* Gz streaming, use `flate2::write::GzDecoder` #228 - -* HttpRequest::url_for is not working with scopes #429 - -* Fixed headers' formating for CORS Middleware `Access-Control-Expose-Headers` header value to HTTP/1.1 & HTTP/2 spec-compliant format #436 - - -## [0.7.2] - 2018-07-26 - -### Added - -* Add implementation of `FromRequest` for `Option` and `Result` - -* Allow to handle application prefix, i.e. allow to handle `/app` path - for application with `/app` prefix. - Check [`App::prefix()`](https://actix.rs/actix-web/actix_web/struct.App.html#method.prefix) - api doc. - -* Add `CookieSessionBackend::http_only` method to set `HttpOnly` directive of cookies - -### Changed - -* Upgrade to cookie 0.11 - -* Removed the timestamp from the default logger middleware - -### Fixed - -* Missing response header "content-encoding" #421 - -* Fix stream draining for http/2 connections #290 - - -## [0.7.1] - 2018-07-21 - -### Fixed - -* Fixed default_resource 'not yet implemented' panic #410 - - -## [0.7.0] - 2018-07-21 - -### Added - -* Add `fs::StaticFileConfig` to provide means of customizing static - file services. It allows to map `mime` to `Content-Disposition`, - specify whether to use `ETag` and `Last-Modified` and allowed methods. - -* Add `.has_prefixed_resource()` method to `router::ResourceInfo` - for route matching with prefix awareness - -* Add `HttpMessage::readlines()` for reading line by line. - -* Add `ClientRequestBuilder::form()` for sending `application/x-www-form-urlencoded` requests. - -* Add method to configure custom error handler to `Form` extractor. - -* Add methods to `HttpResponse` to retrieve, add, and delete cookies - -* Add `.set_content_type()` and `.set_content_disposition()` methods - to `fs::NamedFile` to allow overriding the values inferred by default - -* Add `fs::file_extension_to_mime()` helper function to get the MIME - type for a file extension - -* Add `.content_disposition()` method to parse Content-Disposition of - multipart fields - -* Re-export `actix::prelude::*` as `actix_web::actix` module. - -* `HttpRequest::url_for_static()` for a named route with no variables segments - -* Propagation of the application's default resource to scopes that haven't set a default resource. - - -### Changed - -* Min rustc version is 1.26 - -* Use tokio instead of tokio-core - -* `CookieSessionBackend` sets percent encoded cookies for outgoing HTTP messages. - -* Became possible to use enums with query extractor. - Issue [#371](https://github.com/actix/actix-web/issues/371). - [Example](https://github.com/actix/actix-web/blob/master/tests/test_handlers.rs#L94-L134) - -* `HttpResponse::into_builder()` now moves cookies into the builder - instead of dropping them - -* For safety and performance reasons `Handler::handle()` uses `&self` instead of `&mut self` - -* `Handler::handle()` uses `&HttpRequest` instead of `HttpRequest` - -* Added header `User-Agent: Actix-web/` to default headers when building a request - -* port `Extensions` type from http create, we don't need `Send + Sync` - -* `HttpRequest::query()` returns `Ref>` - -* `HttpRequest::cookies()` returns `Ref>>` - -* `StaticFiles::new()` returns `Result, Error>` instead of `StaticFiles` - -* `StaticFiles` uses the default handler if the file does not exist - - -### Removed - -* Remove `Route::with2()` and `Route::with3()` use tuple of extractors instead. - -* Remove `HttpMessage::range()` - - -## [0.6.15] - 2018-07-11 - -### Fixed - -* Fix h2 compatibility #352 - -* Fix duplicate tail of StaticFiles with index_file. #344 - - -## [0.6.14] - 2018-06-21 - -### Added - -* Allow to disable masking for websockets client - -### Fixed - -* SendRequest execution fails with the "internal error: entered unreachable code" #329 - - -## [0.6.13] - 2018-06-11 - -* http/2 end-of-frame is not set if body is empty bytes #307 - -* InternalError can trigger memory unsafety #301 - - -## [0.6.12] - 2018-06-08 - -### Added - -* Add `Host` filter #287 - -* Allow to filter applications - -* Improved failure interoperability with downcasting #285 - -* Allow to use custom resolver for `ClientConnector` - - -## [0.6.11] - 2018-06-05 - -* Support chunked encoding for UrlEncoded body #262 - -* `HttpRequest::url_for()` for a named route with no variables segments #265 - -* `Middleware::response()` is not invoked if error result was returned by another `Middleware::start()` #255 - -* CORS: Do not validate Origin header on non-OPTION requests #271 - -* Fix multipart upload "Incomplete" error #282 - - -## [0.6.10] - 2018-05-24 - -### Added - -* Allow to use path without trailing slashes for scope registration #241 - -* Allow to set encoding for exact NamedFile #239 - -### Fixed - -* `TestServer::post()` actually sends `GET` request #240 - - -## 0.6.9 (2018-05-22) - -* Drop connection if request's payload is not fully consumed #236 - -* Fix streaming response with body compression - - -## 0.6.8 (2018-05-20) - -* Fix scope resource path extractor #234 - -* Re-use tcp listener on pause/resume - - -## 0.6.7 (2018-05-17) - -* Fix compilation with --no-default-features - - -## 0.6.6 (2018-05-17) - -* Panic during middleware execution #226 - -* Add support for listen_tls/listen_ssl #224 - -* Implement extractor for `Session` - -* Ranges header support for NamedFile #60 - - -## 0.6.5 (2018-05-15) - -* Fix error handling during request decoding #222 - - -## 0.6.4 (2018-05-11) - -* Fix segfault in ServerSettings::get_response_builder() - - -## 0.6.3 (2018-05-10) - -* Add `Router::with_async()` method for async handler registration. - -* Added error response functions for 501,502,503,504 - -* Fix client request timeout handling - - -## 0.6.2 (2018-05-09) - -* WsWriter trait is optional. - - -## 0.6.1 (2018-05-08) - -* Fix http/2 payload streaming #215 - -* Fix connector's default `keep-alive` and `lifetime` settings #212 - -* Send `ErrorNotFound` instead of `ErrorBadRequest` when path extractor fails #214 - -* Allow to exclude certain endpoints from logging #211 - - -## 0.6.0 (2018-05-08) - -* Add route scopes #202 - -* Allow to use ssl and non-ssl connections at the same time #206 - -* Websocket CloseCode Empty/Status is ambiguous #193 - -* Add Content-Disposition to NamedFile #204 - -* Allow to access Error's backtrace object - -* Allow to override files listing renderer for `StaticFiles` #203 - -* Various extractor usability improvements #207 - - -## 0.5.6 (2018-04-24) - -* Make flate2 crate optional #200 - - -## 0.5.5 (2018-04-24) - -* Fix panic when Websocket is closed with no error code #191 - -* Allow to use rust backend for flate2 crate #199 - -## 0.5.4 (2018-04-19) - -* Add identity service middleware - -* Middleware response() is not invoked if there was an error in async handler #187 - -* Use Display formatting for InternalError Display implementation #188 - - -## 0.5.3 (2018-04-18) - -* Impossible to quote slashes in path parameters #182 - - -## 0.5.2 (2018-04-16) - -* Allow to configure StaticFiles's CpuPool, via static method or env variable - -* Add support for custom handling of Json extractor errors #181 - -* Fix StaticFiles does not support percent encoded paths #177 - -* Fix Client Request with custom Body Stream halting on certain size requests #176 - - -## 0.5.1 (2018-04-12) - -* Client connector provides stats, `ClientConnector::stats()` - -* Fix end-of-stream handling in parse_payload #173 - -* Fix StaticFiles generate a lot of threads #174 - - -## 0.5.0 (2018-04-10) - -* Type-safe path/query/form parameter handling, using serde #70 - -* HttpResponse builder's methods `.body()`, `.finish()`, `.json()` - return `HttpResponse` instead of `Result` - -* Use more ergonomic `actix_web::Error` instead of `http::Error` for `ClientRequestBuilder::body()` - -* Added `signed` and `private` `CookieSessionBackend`s - -* Added `HttpRequest::resource()`, returns current matched resource - -* Added `ErrorHandlers` middleware - -* Fix router cannot parse Non-ASCII characters in URL #137 - -* Fix client connection pooling - -* Fix long client urls #129 - -* Fix panic on invalid URL characters #130 - -* Fix logger request duration calculation #152 - -* Fix prefix and static file serving #168 - - -## 0.4.10 (2018-03-20) - -* Use `Error` instead of `InternalError` for `error::ErrorXXXX` methods - -* Allow to set client request timeout - -* Allow to set client websocket handshake timeout - -* Refactor `TestServer` configuration - -* Fix server websockets big payloads support - -* Fix http/2 date header generation - - -## 0.4.9 (2018-03-16) - -* Allow to disable http/2 support - -* Wake payload reading task when data is available - -* Fix server keep-alive handling - -* Send Query Parameters in client requests #120 - -* Move brotli encoding to a feature - -* Add option of default handler for `StaticFiles` handler #57 - -* Add basic client connection pooling - - -## 0.4.8 (2018-03-12) - -* Allow to set read buffer capacity for server request - -* Handle WouldBlock error for socket accept call - - -## 0.4.7 (2018-03-11) - -* Fix panic on unknown content encoding - -* Fix connection get closed too early - -* Fix streaming response handling for http/2 - -* Better sleep on error support - - -## 0.4.6 (2018-03-10) - -* Fix client cookie handling - -* Fix json content type detection - -* Fix CORS middleware #117 - -* Optimize websockets stream support - - -## 0.4.5 (2018-03-07) - -* Fix compression #103 and #104 - -* Fix client cookie handling #111 - -* Non-blocking processing of a `NamedFile` - -* Enable compression support for `NamedFile` - -* Better support for `NamedFile` type - -* Add `ResponseError` impl for `SendRequestError`. This improves ergonomics of the client. - -* Add native-tls support for client - -* Allow client connection timeout to be set #108 - -* Allow to use std::net::TcpListener for HttpServer - -* Handle panics in worker threads - - -## 0.4.4 (2018-03-04) - -* Allow to use Arc> as response/request body - -* Fix handling of requests with an encoded body with a length > 8192 #93 - -## 0.4.3 (2018-03-03) - -* Fix request body read bug - -* Fix segmentation fault #79 - -* Set reuse address before bind #90 - - -## 0.4.2 (2018-03-02) - -* Better naming for websockets implementation - -* Add `Pattern::with_prefix()`, make it more usable outside of actix - -* Add csrf middleware for filter for cross-site request forgery #89 - -* Fix disconnect on idle connections - - -## 0.4.1 (2018-03-01) - -* Rename `Route::p()` to `Route::filter()` - -* Better naming for http codes - -* Fix payload parse in situation when socket data is not ready. - -* Fix Session mutable borrow lifetime #87 - - -## 0.4.0 (2018-02-28) - -* Actix 0.5 compatibility - -* Fix request json/urlencoded loaders - -* Simplify HttpServer type definition - -* Added HttpRequest::encoding() method - -* Added HttpRequest::mime_type() method - -* Added HttpRequest::uri_mut(), allows to modify request uri - -* Added StaticFiles::index_file() - -* Added http client - -* Added websocket client - -* Added TestServer::ws(), test websockets client - -* Added TestServer http client support - -* Allow to override content encoding on application level - - -## 0.3.3 (2018-01-25) - -* Stop processing any events after context stop - -* Re-enable write back-pressure for h1 connections - -* Refactor HttpServer::start_ssl() method - -* Upgrade openssl to 0.10 - - -## 0.3.2 (2018-01-21) - -* Fix HEAD requests handling - -* Log request processing errors - -* Always enable content encoding if encoding explicitly selected - -* Allow multiple Applications on a single server with different state #49 - -* CORS middleware: allowed_headers is defaulting to None #50 - - -## 0.3.1 (2018-01-13) - -* Fix directory entry path #47 - -* Do not enable chunked encoding for HTTP/1.0 - -* Allow explicitly disable chunked encoding - - -## 0.3.0 (2018-01-12) - -* HTTP/2 Support - -* Refactor streaming responses - -* Refactor error handling - -* Asynchronous middlewares - -* Refactor logger middleware - -* Content compression/decompression (br, gzip, deflate) - -* Server multi-threading - -* Graceful shutdown support - - -## 0.2.1 (2017-11-03) - -* Allow to start tls server with `HttpServer::serve_tls` - -* Export `Frame` enum - -* Add conversion impl from `HttpResponse` and `BinaryBody` to a `Frame` - - -## 0.2.0 (2017-10-30) - -* Do not use `http::Uri` as it can not parse some valid paths - -* Refactor response `Body` - -* Refactor `RouteRecognizer` usability - -* Refactor `HttpContext::write` - -* Refactor `Payload` stream - -* Re-use `BinaryBody` for `Frame::Payload` - -* Stop http actor on `write_eof` - -* Fix disconnection handling. - - -## 0.1.0 (2017-10-23) - -* First release +* Initial impl diff --git a/Cargo.toml b/Cargo.toml index d70a65cf0..258301daa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "actix-web" -version = "0.7.9" +name = "actix-http" +version = "0.1.0" authors = ["Nikolay Kim "] -description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." +description = "Actix http" readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" @@ -14,7 +14,6 @@ categories = ["network-programming", "asynchronous", "web-programming::websocket"] license = "MIT/Apache-2.0" exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] -build = "build.rs" [package.metadata.docs.rs] features = ["tls", "alpn", "rust-tls", "session", "brotli", "flate2-c"] @@ -25,7 +24,7 @@ appveyor = { repository = "fafhrd91/actix-web-hdy9d" } codecov = { repository = "actix/actix-web", branch = "master", service = "github" } [lib] -name = "actix_web" +name = "actix_http" path = "src/lib.rs" [features] @@ -130,6 +129,7 @@ webpki-roots = { version = "0.15", optional = true } tokio-uds = { version="0.2", optional = true } [dev-dependencies] +actix-web = "0.7" env_logger = "0.5" serde_derive = "1.0" @@ -140,8 +140,3 @@ version_check = "0.1" lto = true opt-level = 3 codegen-units = 1 - -[workspace] -members = [ - "./", -] diff --git a/MIGRATION.md b/MIGRATION.md deleted file mode 100644 index 3c0bdd943..000000000 --- a/MIGRATION.md +++ /dev/null @@ -1,176 +0,0 @@ -## 0.7.4 - -* `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple - even for handler with one parameter. - - -## 0.7 - -* `HttpRequest` does not implement `Stream` anymore. If you need to read request payload - use `HttpMessage::payload()` method. - - instead of - - ```rust - fn index(req: HttpRequest) -> impl Responder { - req - .from_err() - .fold(...) - .... - } - ``` - - use `.payload()` - - ```rust - fn index(req: HttpRequest) -> impl Responder { - req - .payload() // <- get request payload stream - .from_err() - .fold(...) - .... - } - ``` - -* [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html) - trait uses `&HttpRequest` instead of `&mut HttpRequest`. - -* Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead. - - instead of - - ```rust - fn index(query: Query<..>, info: Json impl Responder {} - ``` - - use tuple of extractors and use `.with()` for registration: - - ```rust - fn index((query, json): (Query<..>, Json impl Responder {} - ``` - -* `Handler::handle()` uses `&self` instead of `&mut self` - -* `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value - -* Removed deprecated `HttpServer::threads()`, use - [HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead. - -* Renamed `client::ClientConnectorError::Connector` to - `client::ClientConnectorError::Resolver` - -* `Route::with()` does not return `ExtractorConfig`, to configure - extractor use `Route::with_config()` - - instead of - - ```rust - fn main() { - let app = App::new().resource("/index.html", |r| { - r.method(http::Method::GET) - .with(index) - .limit(4096); // <- limit size of the payload - }); - } - ``` - - use - - ```rust - - fn main() { - let app = App::new().resource("/index.html", |r| { - r.method(http::Method::GET) - .with_config(index, |cfg| { // <- register handler - cfg.limit(4096); // <- limit size of the payload - }) - }); - } - ``` - -* `Route::with_async()` does not return `ExtractorConfig`, to configure - extractor use `Route::with_async_config()` - - -## 0.6 - -* `Path` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest` - -* `ws::Message::Close` now includes optional close reason. - `ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed. - -* `HttpServer::threads()` renamed to `HttpServer::workers()`. - -* `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated. - Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead. - -* `HttpRequest::extensions()` returns read only reference to the request's Extension - `HttpRequest::extensions_mut()` returns mutable reference. - -* Instead of - - `use actix_web::middleware::{ - CookieSessionBackend, CookieSessionError, RequestSession, - Session, SessionBackend, SessionImpl, SessionStorage};` - - use `actix_web::middleware::session` - - `use actix_web::middleware::session{CookieSessionBackend, CookieSessionError, - RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};` - -* `FromRequest::from_request()` accepts mutable reference to a request - -* `FromRequest::Result` has to implement `Into>` - -* [`Responder::respond_to()`]( - https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to) - is generic over `S` - -* Use `Query` extractor instead of HttpRequest::query()`. - - ```rust - fn index(q: Query>) -> Result<..> { - ... - } - ``` - - or - - ```rust - let q = Query::>::extract(req); - ``` - -* Websocket operations are implemented as `WsWriter` trait. - you need to use `use actix_web::ws::WsWriter` - - -## 0.5 - -* `HttpResponseBuilder::body()`, `.finish()`, `.json()` - methods return `HttpResponse` instead of `Result` - -* `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version` - moved to `actix_web::http` module - -* `actix_web::header` moved to `actix_web::http::header` - -* `NormalizePath` moved to `actix_web::http` module - -* `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function, - shortcut for `actix_web::server::HttpServer::new()` - -* `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself - -* `StaticFiles::new()`'s show_index parameter removed, use `show_files_listing()` method instead. - -* `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type - -* `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()` - functions should be used instead - -* `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>` - instead of `Result<_, http::Error>` - -* `Application` renamed to a `App` - -* `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev` diff --git a/Makefile b/Makefile deleted file mode 100644 index e3b8b2cf1..000000000 --- a/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -.PHONY: default build test doc book clean - -CARGO_FLAGS := --features "$(FEATURES) alpn tls" - -default: test - -build: - cargo build $(CARGO_FLAGS) - -test: build clippy - cargo test $(CARGO_FLAGS) - -doc: build - cargo doc --no-deps $(CARGO_FLAGS) diff --git a/README.md b/README.md index 4e396cb91..b092a1723 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,6 @@ -# Actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# Actix http [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Actix web is a simple, pragmatic and extremely fast web framework for Rust. - -* Supported *HTTP/1.x* and [*HTTP/2.0*](https://actix.rs/docs/http2/) protocols -* Streaming and pipelining -* Keep-alive and slow requests handling -* Client/server [WebSockets](https://actix.rs/docs/websockets/) support -* Transparent content compression/decompression (br, gzip, deflate) -* Configurable [request routing](https://actix.rs/docs/url-dispatch/) -* Graceful server shutdown -* Multipart streams -* Static assets -* SSL support with OpenSSL or `native-tls` -* Middlewares ([Logger, Session, CORS, CSRF, etc](https://actix.rs/docs/middleware/)) -* Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) -* Built on top of [Actix actor framework](https://github.com/actix/actix) +Actix http ## Documentation & community resources @@ -44,30 +30,6 @@ fn main() { } ``` -### More examples - -* [Basics](https://github.com/actix/examples/tree/master/basics/) -* [Stateful](https://github.com/actix/examples/tree/master/state/) -* [Protobuf support](https://github.com/actix/examples/tree/master/protobuf/) -* [Multipart streams](https://github.com/actix/examples/tree/master/multipart/) -* [Simple websocket](https://github.com/actix/examples/tree/master/websocket/) -* [Tera](https://github.com/actix/examples/tree/master/template_tera/) / - [Askama](https://github.com/actix/examples/tree/master/template_askama/) templates -* [Diesel integration](https://github.com/actix/examples/tree/master/diesel/) -* [r2d2](https://github.com/actix/examples/tree/master/r2d2/) -* [SSL / HTTP/2.0](https://github.com/actix/examples/tree/master/tls/) -* [Tcp/Websocket chat](https://github.com/actix/examples/tree/master/websocket-chat/) -* [Json](https://github.com/actix/examples/tree/master/json/) - -You may consider checking out -[this directory](https://github.com/actix/examples/tree/master/) for more examples. - -## Benchmarks - -* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r16&hw=ph&test=plaintext) - -* Some basic benchmarks could be found in this [repository](https://github.com/fafhrd91/benchmarks). - ## License This project is licensed under either of @@ -80,5 +42,5 @@ at your option. ## Code of Conduct Contribution to the actix-web crate is organized under the terms of the -Contributor Covenant, the maintainer of actix-web, @fafhrd91, promises to +Contributor Covenant, the maintainer of actix-net, @fafhrd91, promises to intervene to uphold that code of conduct. diff --git a/build.rs b/build.rs deleted file mode 100644 index c8457944c..000000000 --- a/build.rs +++ /dev/null @@ -1,16 +0,0 @@ -extern crate version_check; - -fn main() { - match version_check::is_min_version("1.26.0") { - Some((true, _)) => println!("cargo:rustc-cfg=actix_impl_trait"), - _ => (), - }; - match version_check::is_nightly() { - Some(true) => { - println!("cargo:rustc-cfg=actix_nightly"); - println!("cargo:rustc-cfg=actix_impl_trait"); - } - Some(false) => (), - None => (), - }; -} diff --git a/src/application.rs b/src/application.rs deleted file mode 100644 index d8a6cbe7b..000000000 --- a/src/application.rs +++ /dev/null @@ -1,879 +0,0 @@ -use std::rc::Rc; - -use handler::{AsyncResult, FromRequest, Handler, Responder, WrapHandler}; -use header::ContentEncoding; -use http::Method; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::Middleware; -use pipeline::{Pipeline, PipelineHandler}; -use pred::Predicate; -use resource::Resource; -use router::{ResourceDef, Router}; -use scope::Scope; -use server::{HttpHandler, HttpHandlerTask, IntoHttpHandler, Request}; -use with::WithFactory; - -/// Application -pub struct HttpApplication { - state: Rc, - prefix: String, - prefix_len: usize, - inner: Rc>, - filters: Option>>>, - middlewares: Rc>>>, -} - -#[doc(hidden)] -pub struct Inner { - router: Router, - encoding: ContentEncoding, -} - -impl PipelineHandler for Inner { - #[inline] - fn encoding(&self) -> ContentEncoding { - self.encoding - } - - fn handle(&self, req: &HttpRequest) -> AsyncResult { - self.router.handle(req) - } -} - -impl HttpApplication { - #[cfg(test)] - pub(crate) fn run(&self, req: Request) -> AsyncResult { - let info = self - .inner - .router - .recognize(&req, &self.state, self.prefix_len); - let req = HttpRequest::new(req, Rc::clone(&self.state), info); - - self.inner.handle(&req) - } -} - -impl HttpHandler for HttpApplication { - type Task = Pipeline>; - - fn handle(&self, msg: Request) -> Result>, Request> { - let m = { - if self.prefix_len == 0 { - true - } else { - let path = msg.path(); - path.starts_with(&self.prefix) - && (path.len() == self.prefix_len - || path.split_at(self.prefix_len).1.starts_with('/')) - } - }; - if m { - if let Some(ref filters) = self.filters { - for filter in filters { - if !filter.check(&msg, &self.state) { - return Err(msg); - } - } - } - - let info = self - .inner - .router - .recognize(&msg, &self.state, self.prefix_len); - - let inner = Rc::clone(&self.inner); - let req = HttpRequest::new(msg, Rc::clone(&self.state), info); - Ok(Pipeline::new(req, Rc::clone(&self.middlewares), inner)) - } else { - Err(msg) - } - } -} - -struct ApplicationParts { - state: S, - prefix: String, - router: Router, - encoding: ContentEncoding, - middlewares: Vec>>, - filters: Vec>>, -} - -/// Structure that follows the builder pattern for building application -/// instances. -pub struct App { - parts: Option>, -} - -impl App<()> { - /// Create application with empty state. Application can - /// be configured with a builder-like pattern. - pub fn new() -> App<()> { - App::with_state(()) - } -} - -impl Default for App<()> { - fn default() -> Self { - App::new() - } -} - -impl App -where - S: 'static, -{ - /// Create application with specified state. Application can be - /// configured with a builder-like pattern. - /// - /// State is shared with all resources within same application and - /// could be accessed with `HttpRequest::state()` method. - /// - /// **Note**: http server accepts an application factory rather than - /// an application instance. Http server constructs an application - /// instance for each thread, thus application state must be constructed - /// multiple times. If you want to share state between different - /// threads, a shared object should be used, e.g. `Arc`. Application - /// state does not need to be `Send` or `Sync`. - pub fn with_state(state: S) -> App { - App { - parts: Some(ApplicationParts { - state, - prefix: "".to_owned(), - router: Router::new(ResourceDef::prefix("")), - middlewares: Vec::new(), - filters: Vec::new(), - encoding: ContentEncoding::Auto, - }), - } - } - - /// Get reference to the application state - pub fn state(&self) -> &S { - let parts = self.parts.as_ref().expect("Use after finish"); - &parts.state - } - - /// Set application prefix. - /// - /// Only requests that match the application's prefix get - /// processed by this application. - /// - /// The application prefix always contains a leading slash (`/`). - /// If the supplied prefix does not contain leading slash, it is - /// inserted. - /// - /// Prefix should consist of valid path segments. i.e for an - /// application with the prefix `/app` any request with the paths - /// `/app`, `/app/` or `/app/test` would match, but the path - /// `/application` would not. - /// - /// In the following example only requests with an `/app/` path - /// prefix get handled. Requests with path `/app/test/` would be - /// handled, while requests with the paths `/application` or - /// `/other/...` would return `NOT FOUND`. It is also possible to - /// handle `/app` path, to do this you can register resource for - /// empty string `""` - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new() - /// .prefix("/app") - /// .resource("", |r| r.f(|_| HttpResponse::Ok())) // <- handle `/app` path - /// .resource("/", |r| r.f(|_| HttpResponse::Ok())) // <- handle `/app/` path - /// .resource("/test", |r| { - /// r.get().f(|_| HttpResponse::Ok()); - /// r.head().f(|_| HttpResponse::MethodNotAllowed()); - /// }) - /// .finish(); - /// } - /// ``` - pub fn prefix>(mut self, prefix: P) -> App { - { - let parts = self.parts.as_mut().expect("Use after finish"); - let mut prefix = prefix.into(); - if !prefix.starts_with('/') { - prefix.insert(0, '/') - } - parts.router.set_prefix(&prefix); - parts.prefix = prefix; - } - self - } - - /// Add match predicate to application. - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// # fn main() { - /// App::new() - /// .filter(pred::Host("www.rust-lang.org")) - /// .resource("/path", |r| r.f(|_| HttpResponse::Ok())) - /// # .finish(); - /// # } - /// ``` - pub fn filter + 'static>(mut self, p: T) -> App { - { - let parts = self.parts.as_mut().expect("Use after finish"); - parts.filters.push(Box::new(p)); - } - self - } - - /// Configure route for a specific path. - /// - /// This is a simplified version of the `App::resource()` method. - /// Handler functions need to accept one request extractor - /// argument. - /// - /// This method could be called multiple times, in that case - /// multiple routes would be registered for same resource path. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// let app = App::new() - /// .route("/test", http::Method::GET, |_: HttpRequest| { - /// HttpResponse::Ok() - /// }) - /// .route("/test", http::Method::POST, |_: HttpRequest| { - /// HttpResponse::MethodNotAllowed() - /// }); - /// } - /// ``` - pub fn route(mut self, path: &str, method: Method, f: F) -> App - where - F: WithFactory, - R: Responder + 'static, - T: FromRequest + 'static, - { - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_route(path, method, f); - - self - } - - /// Configure scope for common root path. - /// - /// Scopes collect multiple paths under a common path prefix. - /// Scope path can contain variable path segments as resources. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().scope("/{project_id}", |scope| { - /// scope - /// .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - /// .resource("/path2", |r| r.f(|_| HttpResponse::Ok())) - /// .resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed())) - /// }); - /// } - /// ``` - /// - /// In the above example, three routes get added: - /// * /{project_id}/path1 - /// * /{project_id}/path2 - /// * /{project_id}/path3 - /// - pub fn scope(mut self, path: &str, f: F) -> App - where - F: FnOnce(Scope) -> Scope, - { - let scope = f(Scope::new(path)); - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_scope(scope); - self - } - - /// Configure resource for a specific path. - /// - /// Resources may have variable path segments. For example, a - /// resource with the path `/a/{name}/c` would match all incoming - /// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. - /// - /// A variable segment is specified in the form `{identifier}`, - /// where the identifier can be used later in a request handler to - /// access the matched value for that segment. This is done by - /// looking up the identifier in the `Params` object returned by - /// `HttpRequest.match_info()` method. - /// - /// By default, each segment matches the regular expression `[^{}/]+`. - /// - /// You can also specify a custom regex in the form `{identifier:regex}`: - /// - /// For instance, to route `GET`-requests on any route matching - /// `/users/{userid}/{friend}` and store `userid` and `friend` in - /// the exposed `Params` object: - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().resource("/users/{userid}/{friend}", |r| { - /// r.get().f(|_| HttpResponse::Ok()); - /// r.head().f(|_| HttpResponse::MethodNotAllowed()); - /// }); - /// } - /// ``` - pub fn resource(mut self, path: &str, f: F) -> App - where - F: FnOnce(&mut Resource) -> R + 'static, - { - { - let parts = self.parts.as_mut().expect("Use after finish"); - - // create resource - let mut resource = Resource::new(ResourceDef::new(path)); - - // configure - f(&mut resource); - - parts.router.register_resource(resource); - } - self - } - - /// Configure resource for a specific path. - #[doc(hidden)] - pub fn register_resource(&mut self, resource: Resource) { - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_resource(resource); - } - - /// Default resource to be used if no matching route could be found. - pub fn default_resource(mut self, f: F) -> App - where - F: FnOnce(&mut Resource) -> R + 'static, - { - // create and configure default resource - let mut resource = Resource::new(ResourceDef::new("")); - f(&mut resource); - - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_default_resource(resource.into()); - - self - } - - /// Set default content encoding. `ContentEncoding::Auto` is set by default. - pub fn default_encoding(mut self, encoding: ContentEncoding) -> App { - { - let parts = self.parts.as_mut().expect("Use after finish"); - parts.encoding = encoding; - } - self - } - - /// Register an external resource. - /// - /// External resources are useful for URL generation purposes only - /// and are never considered for matching at request time. Calls to - /// `HttpRequest::url_for()` will work as expected. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest, HttpResponse, Result}; - /// - /// fn index(req: &HttpRequest) -> Result { - /// let url = req.url_for("youtube", &["oHg5SJYRHA0"])?; - /// assert_eq!(url.as_str(), "https://youtube.com/watch/oHg5SJYRHA0"); - /// Ok(HttpResponse::Ok().into()) - /// } - /// - /// fn main() { - /// let app = App::new() - /// .resource("/index.html", |r| r.get().f(index)) - /// .external_resource("youtube", "https://youtube.com/watch/{video_id}") - /// .finish(); - /// } - /// ``` - pub fn external_resource(mut self, name: T, url: U) -> App - where - T: AsRef, - U: AsRef, - { - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_external(name.as_ref(), ResourceDef::external(url.as_ref())); - self - } - - /// Configure handler for specific path prefix. - /// - /// A path prefix consists of valid path segments, i.e for the - /// prefix `/app` any request with the paths `/app`, `/app/` or - /// `/app/test` would match, but the path `/application` would - /// not. - /// - /// Path tail is available as `tail` parameter in request's match_dict. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().handler("/app", |req: &HttpRequest| match *req.method() { - /// http::Method::GET => HttpResponse::Ok(), - /// http::Method::POST => HttpResponse::MethodNotAllowed(), - /// _ => HttpResponse::NotFound(), - /// }); - /// } - /// ``` - pub fn handler>(mut self, path: &str, handler: H) -> App { - { - let mut path = path.trim().trim_right_matches('/').to_owned(); - if !path.is_empty() && !path.starts_with('/') { - path.insert(0, '/'); - }; - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_handler(&path, Box::new(WrapHandler::new(handler)), None); - } - self - } - - /// Register a middleware. - pub fn middleware>(mut self, mw: M) -> App { - self.parts - .as_mut() - .expect("Use after finish") - .middlewares - .push(Box::new(mw)); - self - } - - /// Run external configuration as part of the application building - /// process - /// - /// This function is useful for moving parts of configuration to a - /// different module or event library. For example we can move - /// some of the resources' configuration to different module. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{fs, middleware, App, HttpResponse}; - /// - /// // this function could be located in different module - /// fn config(app: App) -> App { - /// app.resource("/test", |r| { - /// r.get().f(|_| HttpResponse::Ok()); - /// r.head().f(|_| HttpResponse::MethodNotAllowed()); - /// }) - /// } - /// - /// fn main() { - /// let app = App::new() - /// .middleware(middleware::Logger::default()) - /// .configure(config) // <- register resources - /// .handler("/static", fs::StaticFiles::new(".").unwrap()); - /// } - /// ``` - pub fn configure(self, cfg: F) -> App - where - F: Fn(App) -> App, - { - cfg(self) - } - - /// Finish application configuration and create `HttpHandler` object. - pub fn finish(&mut self) -> HttpApplication { - let mut parts = self.parts.take().expect("Use after finish"); - let prefix = parts.prefix.trim().trim_right_matches('/'); - parts.router.finish(); - - let inner = Rc::new(Inner { - router: parts.router, - encoding: parts.encoding, - }); - let filters = if parts.filters.is_empty() { - None - } else { - Some(parts.filters) - }; - - HttpApplication { - inner, - filters, - state: Rc::new(parts.state), - middlewares: Rc::new(parts.middlewares), - prefix: prefix.to_owned(), - prefix_len: prefix.len(), - } - } - - /// Convenience method for creating `Box` instances. - /// - /// This method is useful if you need to register multiple - /// application instances with different state. - /// - /// ```rust - /// # use std::thread; - /// # extern crate actix_web; - /// use actix_web::{server, App, HttpResponse}; - /// - /// struct State1; - /// - /// struct State2; - /// - /// fn main() { - /// # thread::spawn(|| { - /// server::new(|| { - /// vec![ - /// App::with_state(State1) - /// .prefix("/app1") - /// .resource("/", |r| r.f(|r| HttpResponse::Ok())) - /// .boxed(), - /// App::with_state(State2) - /// .prefix("/app2") - /// .resource("/", |r| r.f(|r| HttpResponse::Ok())) - /// .boxed(), - /// ] - /// }).bind("127.0.0.1:8080") - /// .unwrap() - /// .run() - /// # }); - /// } - /// ``` - pub fn boxed(mut self) -> Box>> { - Box::new(BoxedApplication { app: self.finish() }) - } -} - -struct BoxedApplication { - app: HttpApplication, -} - -impl HttpHandler for BoxedApplication { - type Task = Box; - - fn handle(&self, req: Request) -> Result { - self.app.handle(req).map(|t| { - let task: Self::Task = Box::new(t); - task - }) - } -} - -impl IntoHttpHandler for App { - type Handler = HttpApplication; - - fn into_handler(mut self) -> HttpApplication { - self.finish() - } -} - -impl<'a, S: 'static> IntoHttpHandler for &'a mut App { - type Handler = HttpApplication; - - fn into_handler(self) -> HttpApplication { - self.finish() - } -} - -#[doc(hidden)] -impl Iterator for App { - type Item = HttpApplication; - - fn next(&mut self) -> Option { - if self.parts.is_some() { - Some(self.finish()) - } else { - None - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use body::{Binary, Body}; - use http::StatusCode; - use httprequest::HttpRequest; - use httpresponse::HttpResponse; - use pred; - use test::{TestRequest, TestServer}; - - #[test] - fn test_default_resource() { - let app = App::new() - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .finish(); - - let req = TestRequest::with_uri("/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let app = App::new() - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .default_resource(|r| r.f(|_| HttpResponse::MethodNotAllowed())) - .finish(); - let req = TestRequest::with_uri("/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::METHOD_NOT_ALLOWED); - } - - #[test] - fn test_unhandled_prefix() { - let app = App::new() - .prefix("/test") - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .finish(); - let ctx = TestRequest::default().request(); - assert!(app.handle(ctx).is_err()); - } - - #[test] - fn test_state() { - let app = App::with_state(10) - .resource("/", |r| r.f(|_| HttpResponse::Ok())) - .finish(); - let req = TestRequest::with_state(10).request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_prefix() { - let app = App::new() - .prefix("/test") - .resource("/blah", |r| r.f(|_| HttpResponse::Ok())) - .finish(); - let req = TestRequest::with_uri("/test").request(); - let resp = app.handle(req); - assert!(resp.is_ok()); - - let req = TestRequest::with_uri("/test/").request(); - let resp = app.handle(req); - assert!(resp.is_ok()); - - let req = TestRequest::with_uri("/test/blah").request(); - let resp = app.handle(req); - assert!(resp.is_ok()); - - let req = TestRequest::with_uri("/testing").request(); - let resp = app.handle(req); - assert!(resp.is_err()); - } - - #[test] - fn test_handler() { - let app = App::new() - .handler("/test", |_: &_| HttpResponse::Ok()) - .finish(); - - let req = TestRequest::with_uri("/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_handler2() { - let app = App::new() - .handler("test", |_: &_| HttpResponse::Ok()) - .finish(); - - let req = TestRequest::with_uri("/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_handler_with_prefix() { - let app = App::new() - .prefix("prefix") - .handler("/test", |_: &_| HttpResponse::Ok()) - .finish(); - - let req = TestRequest::with_uri("/prefix/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/prefix/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/prefix/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/prefix/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/prefix/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_route() { - let app = App::new() - .route("/test", Method::GET, |_: HttpRequest| HttpResponse::Ok()) - .route("/test", Method::POST, |_: HttpRequest| { - HttpResponse::Created() - }).finish(); - - let req = TestRequest::with_uri("/test").method(Method::GET).request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - - let req = TestRequest::with_uri("/test") - .method(Method::HEAD) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_handler_prefix() { - let app = App::new() - .prefix("/app") - .handler("/test", |_: &_| HttpResponse::Ok()) - .finish(); - - let req = TestRequest::with_uri("/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_option_responder() { - let app = App::new() - .resource("/none", |r| r.f(|_| -> Option<&'static str> { None })) - .resource("/some", |r| r.f(|_| Some("some"))) - .finish(); - - let req = TestRequest::with_uri("/none").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/some").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - assert_eq!(resp.as_msg().body(), &Body::Binary(Binary::Slice(b"some"))); - } - - #[test] - fn test_filter() { - let mut srv = TestServer::with_factory(|| { - App::new() - .filter(pred::Get()) - .handler("/test", |_: &_| HttpResponse::Ok()) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - let request = srv.post().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_prefix_root() { - let mut srv = TestServer::with_factory(|| { - App::new() - .prefix("/test") - .resource("/", |r| r.f(|_| HttpResponse::Ok())) - .resource("", |r| r.f(|_| HttpResponse::Created())) - }); - - let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::CREATED); - } - -} diff --git a/src/body.rs b/src/body.rs index a93db1e92..e689b704c 100644 --- a/src/body.rs +++ b/src/body.rs @@ -3,10 +3,7 @@ use futures::Stream; use std::sync::Arc; use std::{fmt, mem}; -use context::ActorHttpContext; use error::Error; -use handler::Responder; -use httprequest::HttpRequest; use httpresponse::HttpResponse; /// Type represent streaming body @@ -21,8 +18,8 @@ pub enum Body { /// Unspecified streaming response. Developer is responsible for setting /// right `Content-Length` or `Transfer-Encoding` headers. Streaming(BodyStream), - /// Special body type for actor response. - Actor(Box), + // /// Special body type for actor response. + // Actor(Box), } /// Represents various types of binary body. @@ -45,7 +42,7 @@ impl Body { #[inline] pub fn is_streaming(&self) -> bool { match *self { - Body::Streaming(_) | Body::Actor(_) => true, + Body::Streaming(_) => true, _ => false, } } @@ -94,7 +91,7 @@ impl PartialEq for Body { Body::Binary(ref b2) => b == b2, _ => false, }, - Body::Streaming(_) | Body::Actor(_) => false, + Body::Streaming(_) => false, } } } @@ -105,7 +102,6 @@ impl fmt::Debug for Body { Body::Empty => write!(f, "Body::Empty"), Body::Binary(ref b) => write!(f, "Body::Binary({:?})", b), Body::Streaming(_) => write!(f, "Body::Streaming(_)"), - Body::Actor(_) => write!(f, "Body::Actor(_)"), } } } @@ -119,12 +115,6 @@ where } } -impl From> for Body { - fn from(ctx: Box) -> Body { - Body::Actor(ctx) - } -} - impl Binary { #[inline] /// Returns `true` if body is empty @@ -254,17 +244,6 @@ impl AsRef<[u8]> for Binary { } } -impl Responder for Binary { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(HttpResponse::build_from(req) - .content_type("application/octet-stream") - .body(self)) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/client/connector.rs b/src/client/connector.rs deleted file mode 100644 index 07c7b646d..000000000 --- a/src/client/connector.rs +++ /dev/null @@ -1,1344 +0,0 @@ -use std::collections::{HashMap, VecDeque}; -use std::net::Shutdown; -use std::time::{Duration, Instant}; -use std::{fmt, io, mem, time}; - -use actix::resolver::{Connect as ResolveConnect, Resolver, ResolverError}; -use actix::{ - fut, Actor, ActorFuture, ActorResponse, Addr, AsyncContext, Context, - ContextFutureSpawner, Handler, Message, Recipient, StreamHandler, Supervised, - SystemService, WrapFuture, -}; - -use futures::sync::{mpsc, oneshot}; -use futures::{Async, Future, Poll}; -use http::{Error as HttpError, HttpTryFrom, Uri}; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_timer::Delay; - -#[cfg(any(feature = "alpn", feature = "ssl"))] -use { - openssl::ssl::{Error as SslError, SslConnector, SslMethod}, - tokio_openssl::SslConnectorExt, -}; - -#[cfg(all( - feature = "tls", - not(any(feature = "alpn", feature = "ssl", feature = "rust-tls")) -))] -use { - native_tls::{Error as SslError, TlsConnector as NativeTlsConnector}, - tokio_tls::TlsConnector as SslConnector, -}; - -#[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "tls", feature = "ssl")) -))] -use { - rustls::ClientConfig, std::io::Error as SslError, std::sync::Arc, - tokio_rustls::ClientConfigExt, webpki::DNSNameRef, webpki_roots, -}; - -#[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "tls", feature = "ssl")) -))] -type SslConnector = Arc; - -#[cfg(not(any( - feature = "alpn", - feature = "ssl", - feature = "tls", - feature = "rust-tls" -)))] -type SslConnector = (); - -use server::IoStream; -use {HAS_OPENSSL, HAS_RUSTLS, HAS_TLS}; - -/// Client connector usage stats -#[derive(Default, Message)] -pub struct ClientConnectorStats { - /// Number of waited-on connections - pub waits: usize, - /// Size of the wait queue - pub wait_queue: usize, - /// Number of reused connections - pub reused: usize, - /// Number of opened connections - pub opened: usize, - /// Number of closed connections - pub closed: usize, - /// Number of connections with errors - pub errors: usize, - /// Number of connection timeouts - pub timeouts: usize, -} - -#[derive(Debug)] -/// `Connect` type represents a message that can be sent to -/// `ClientConnector` with a connection request. -pub struct Connect { - pub(crate) uri: Uri, - pub(crate) wait_timeout: Duration, - pub(crate) conn_timeout: Duration, -} - -impl Connect { - /// Create `Connect` message for specified `Uri` - pub fn new(uri: U) -> Result - where - Uri: HttpTryFrom, - { - Ok(Connect { - uri: Uri::try_from(uri).map_err(|e| e.into())?, - wait_timeout: Duration::from_secs(5), - conn_timeout: Duration::from_secs(1), - }) - } - - /// Connection timeout, i.e. max time to connect to remote host. - /// Set to 1 second by default. - pub fn conn_timeout(mut self, timeout: Duration) -> Self { - self.conn_timeout = timeout; - self - } - - /// If connection pool limits are enabled, wait time indicates - /// max time to wait for a connection to become available. - /// Set to 5 seconds by default. - pub fn wait_timeout(mut self, timeout: Duration) -> Self { - self.wait_timeout = timeout; - self - } -} - -impl Message for Connect { - type Result = Result; -} - -/// Pause connection process for `ClientConnector` -/// -/// All connect requests enter wait state during connector pause. -pub struct Pause { - time: Option, -} - -impl Pause { - /// Create message with pause duration parameter - pub fn new(time: Duration) -> Pause { - Pause { time: Some(time) } - } -} - -impl Default for Pause { - fn default() -> Pause { - Pause { time: None } - } -} - -impl Message for Pause { - type Result = (); -} - -/// Resume connection process for `ClientConnector` -#[derive(Message)] -pub struct Resume; - -/// A set of errors that can occur while connecting to an HTTP host -#[derive(Fail, Debug)] -pub enum ClientConnectorError { - /// Invalid URL - #[fail(display = "Invalid URL")] - InvalidUrl, - - /// SSL feature is not enabled - #[fail(display = "SSL is not supported")] - SslIsNotSupported, - - /// SSL error - #[cfg(any( - feature = "tls", - feature = "alpn", - feature = "ssl", - feature = "rust-tls", - ))] - #[fail(display = "{}", _0)] - SslError(#[cause] SslError), - - /// Resolver error - #[fail(display = "{}", _0)] - Resolver(#[cause] ResolverError), - - /// Connection took too long - #[fail(display = "Timeout while establishing connection")] - Timeout, - - /// Connector has been disconnected - #[fail(display = "Internal error: connector has been disconnected")] - Disconnected, - - /// Connection IO error - #[fail(display = "{}", _0)] - IoError(#[cause] io::Error), -} - -impl From for ClientConnectorError { - fn from(err: ResolverError) -> ClientConnectorError { - match err { - ResolverError::Timeout => ClientConnectorError::Timeout, - _ => ClientConnectorError::Resolver(err), - } - } -} - -struct Waiter { - tx: oneshot::Sender>, - wait: Instant, - conn_timeout: Duration, -} - -enum Paused { - No, - Yes, - Timeout(Instant, Delay), -} - -impl Paused { - fn is_paused(&self) -> bool { - match *self { - Paused::No => false, - _ => true, - } - } -} - -/// `ClientConnector` type is responsible for transport layer of a -/// client connection. -pub struct ClientConnector { - #[allow(dead_code)] - connector: SslConnector, - - stats: ClientConnectorStats, - subscriber: Option>, - - acq_tx: mpsc::UnboundedSender, - acq_rx: Option>, - - resolver: Option>, - conn_lifetime: Duration, - conn_keep_alive: Duration, - limit: usize, - limit_per_host: usize, - acquired: usize, - acquired_per_host: HashMap, - available: HashMap>, - to_close: Vec, - waiters: Option>>, - wait_timeout: Option<(Instant, Delay)>, - paused: Paused, -} - -impl Actor for ClientConnector { - type Context = Context; - - fn started(&mut self, ctx: &mut Self::Context) { - if self.resolver.is_none() { - self.resolver = Some(Resolver::from_registry()) - } - self.collect_periodic(ctx); - ctx.add_stream(self.acq_rx.take().unwrap()); - ctx.spawn(Maintenance); - } -} - -impl Supervised for ClientConnector {} - -impl SystemService for ClientConnector {} - -impl Default for ClientConnector { - fn default() -> ClientConnector { - let connector = { - #[cfg(all(any(feature = "alpn", feature = "ssl")))] - { - SslConnector::builder(SslMethod::tls()).unwrap().build() - } - - #[cfg(all( - feature = "tls", - not(any(feature = "alpn", feature = "ssl", feature = "rust-tls")) - ))] - { - NativeTlsConnector::builder().build().unwrap().into() - } - - #[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "tls", feature = "ssl")) - ))] - { - let mut config = ClientConfig::new(); - config - .root_store - .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); - Arc::new(config) - } - - #[cfg_attr(rustfmt, rustfmt_skip)] - #[cfg(not(any( - feature = "alpn", feature = "ssl", feature = "tls", feature = "rust-tls")))] - { - () - } - }; - - #[cfg_attr(feature = "cargo-clippy", allow(clippy::let_unit_value))] - ClientConnector::with_connector_impl(connector) - } -} - -impl ClientConnector { - #[cfg(any(feature = "alpn", feature = "ssl"))] - /// Create `ClientConnector` actor with custom `SslConnector` instance. - /// - /// By default `ClientConnector` uses very a simple SSL configuration. - /// With `with_connector` method it is possible to use a custom - /// `SslConnector` object. - /// - /// ```rust,ignore - /// # #![cfg(feature="alpn")] - /// # extern crate actix_web; - /// # extern crate futures; - /// # use futures::{future, Future}; - /// # use std::io::Write; - /// # use actix_web::actix::Actor; - /// extern crate openssl; - /// use actix_web::{actix, client::ClientConnector, client::Connect}; - /// - /// use openssl::ssl::{SslConnector, SslMethod}; - /// - /// fn main() { - /// actix::run(|| { - /// // Start `ClientConnector` with custom `SslConnector` - /// let ssl_conn = SslConnector::builder(SslMethod::tls()).unwrap().build(); - /// let conn = ClientConnector::with_connector(ssl_conn).start(); - /// - /// conn.send( - /// Connect::new("https://www.rust-lang.org").unwrap()) // <- connect to host - /// .map_err(|_| ()) - /// .and_then(|res| { - /// if let Ok(mut stream) = res { - /// stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); - /// } - /// # actix::System::current().stop(); - /// Ok(()) - /// }) - /// }); - /// } - /// ``` - pub fn with_connector(connector: SslConnector) -> ClientConnector { - // keep level of indirection for docstrings matching featureflags - Self::with_connector_impl(connector) - } - - #[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "ssl", feature = "tls")) - ))] - /// Create `ClientConnector` actor with custom `SslConnector` instance. - /// - /// By default `ClientConnector` uses very a simple SSL configuration. - /// With `with_connector` method it is possible to use a custom - /// `SslConnector` object. - /// - /// ```rust - /// # #![cfg(feature = "rust-tls")] - /// # extern crate actix_web; - /// # extern crate futures; - /// # use futures::{future, Future}; - /// # use std::io::Write; - /// # use actix_web::actix::Actor; - /// extern crate rustls; - /// extern crate webpki_roots; - /// use actix_web::{actix, client::ClientConnector, client::Connect}; - /// - /// use rustls::ClientConfig; - /// use std::sync::Arc; - /// - /// fn main() { - /// actix::run(|| { - /// // Start `ClientConnector` with custom `ClientConfig` - /// let mut config = ClientConfig::new(); - /// config - /// .root_store - /// .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); - /// let conn = ClientConnector::with_connector(Arc::new(config)).start(); - /// - /// conn.send( - /// Connect::new("https://www.rust-lang.org").unwrap()) // <- connect to host - /// .map_err(|_| ()) - /// .and_then(|res| { - /// if let Ok(mut stream) = res { - /// stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); - /// } - /// # actix::System::current().stop(); - /// Ok(()) - /// }) - /// }); - /// } - /// ``` - pub fn with_connector(connector: ClientConfig) -> ClientConnector { - // keep level of indirection for docstrings matching featureflags - Self::with_connector_impl(Arc::new(connector)) - } - - #[cfg(all( - feature = "tls", - not(any(feature = "ssl", feature = "alpn", feature = "rust-tls")) - ))] - /// Create `ClientConnector` actor with custom `SslConnector` instance. - /// - /// By default `ClientConnector` uses very a simple SSL configuration. - /// With `with_connector` method it is possible to use a custom - /// `SslConnector` object. - /// - /// ```rust - /// # #![cfg(feature = "tls")] - /// # extern crate actix_web; - /// # extern crate futures; - /// # use futures::{future, Future}; - /// # use std::io::Write; - /// # use actix_web::actix::Actor; - /// extern crate native_tls; - /// extern crate webpki_roots; - /// use native_tls::TlsConnector; - /// use actix_web::{actix, client::ClientConnector, client::Connect}; - /// - /// fn main() { - /// actix::run(|| { - /// let connector = TlsConnector::new().unwrap(); - /// let conn = ClientConnector::with_connector(connector.into()).start(); - /// - /// conn.send( - /// Connect::new("https://www.rust-lang.org").unwrap()) // <- connect to host - /// .map_err(|_| ()) - /// .and_then(|res| { - /// if let Ok(mut stream) = res { - /// stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); - /// } - /// # actix::System::current().stop(); - /// Ok(()) - /// }) - /// }); - /// } - /// ``` - pub fn with_connector(connector: SslConnector) -> ClientConnector { - // keep level of indirection for docstrings matching featureflags - Self::with_connector_impl(connector) - } - - #[inline] - fn with_connector_impl(connector: SslConnector) -> ClientConnector { - let (tx, rx) = mpsc::unbounded(); - - ClientConnector { - connector, - stats: ClientConnectorStats::default(), - subscriber: None, - acq_tx: tx, - acq_rx: Some(rx), - resolver: None, - conn_lifetime: Duration::from_secs(75), - conn_keep_alive: Duration::from_secs(15), - limit: 100, - limit_per_host: 0, - acquired: 0, - acquired_per_host: HashMap::new(), - available: HashMap::new(), - to_close: Vec::new(), - waiters: Some(HashMap::new()), - wait_timeout: None, - paused: Paused::No, - } - } - - /// Set total number of simultaneous connections. - /// - /// If limit is 0, the connector has no limit. - /// The default limit size is 100. - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - /// Set total number of simultaneous connections to the same endpoint. - /// - /// Endpoints are the same if they have equal (host, port, ssl) triplets. - /// If limit is 0, the connector has no limit. The default limit size is 0. - pub fn limit_per_host(mut self, limit: usize) -> Self { - self.limit_per_host = limit; - self - } - - /// Set keep-alive period for opened connection. - /// - /// Keep-alive period is the period between connection usage. If - /// the delay between repeated usages of the same connection - /// exceeds this period, the connection is closed. - /// Default keep-alive period is 15 seconds. - pub fn conn_keep_alive(mut self, dur: Duration) -> Self { - self.conn_keep_alive = dur; - self - } - - /// Set max lifetime period for connection. - /// - /// Connection lifetime is max lifetime of any opened connection - /// until it is closed regardless of keep-alive period. - /// Default lifetime period is 75 seconds. - pub fn conn_lifetime(mut self, dur: Duration) -> Self { - self.conn_lifetime = dur; - self - } - - /// Subscribe for connector stats. Only one subscriber is supported. - pub fn stats(mut self, subs: Recipient) -> Self { - self.subscriber = Some(subs); - self - } - - /// Use custom resolver actor - pub fn resolver(mut self, addr: Addr) -> Self { - self.resolver = Some(addr); - self - } - - fn acquire(&mut self, key: &Key) -> Acquire { - // check limits - if self.limit > 0 { - if self.acquired >= self.limit { - return Acquire::NotAvailable; - } - if self.limit_per_host > 0 { - if let Some(per_host) = self.acquired_per_host.get(key) { - if *per_host >= self.limit_per_host { - return Acquire::NotAvailable; - } - } - } - } else if self.limit_per_host > 0 { - if let Some(per_host) = self.acquired_per_host.get(key) { - if *per_host >= self.limit_per_host { - return Acquire::NotAvailable; - } - } - } - - self.reserve(key); - - // check if open connection is available - // cleanup stale connections at the same time - if let Some(ref mut connections) = self.available.get_mut(key) { - let now = Instant::now(); - while let Some(conn) = connections.pop_back() { - // check if it still usable - if (now - conn.0) > self.conn_keep_alive - || (now - conn.1.ts) > self.conn_lifetime - { - self.stats.closed += 1; - self.to_close.push(conn.1); - } else { - let mut conn = conn.1; - let mut buf = [0; 2]; - match conn.stream().read(&mut buf) { - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => (), - Ok(n) if n > 0 => { - self.stats.closed += 1; - self.to_close.push(conn); - continue; - } - Ok(_) | Err(_) => continue, - } - return Acquire::Acquired(conn); - } - } - } - Acquire::Available - } - - fn reserve(&mut self, key: &Key) { - self.acquired += 1; - let per_host = if let Some(per_host) = self.acquired_per_host.get(key) { - *per_host - } else { - 0 - }; - self.acquired_per_host.insert(key.clone(), per_host + 1); - } - - fn release_key(&mut self, key: &Key) { - if self.acquired > 0 { - self.acquired -= 1; - } - let per_host = if let Some(per_host) = self.acquired_per_host.get(key) { - *per_host - } else { - return; - }; - if per_host > 1 { - self.acquired_per_host.insert(key.clone(), per_host - 1); - } else { - self.acquired_per_host.remove(key); - } - } - - fn collect_periodic(&mut self, ctx: &mut Context) { - // check connections for shutdown - let mut idx = 0; - while idx < self.to_close.len() { - match AsyncWrite::shutdown(&mut self.to_close[idx]) { - Ok(Async::NotReady) => idx += 1, - _ => { - self.to_close.swap_remove(idx); - } - } - } - - // re-schedule next collect period - ctx.run_later(Duration::from_secs(1), |act, ctx| act.collect_periodic(ctx)); - - // send stats - let mut stats = mem::replace(&mut self.stats, ClientConnectorStats::default()); - if let Some(ref mut subscr) = self.subscriber { - if let Some(ref waiters) = self.waiters { - for w in waiters.values() { - stats.wait_queue += w.len(); - } - } - let _ = subscr.do_send(stats); - } - } - - // TODO: waiters should be sorted by deadline. maybe timewheel? - fn collect_waiters(&mut self) { - let now = Instant::now(); - let mut next = None; - - for waiters in self.waiters.as_mut().unwrap().values_mut() { - let mut idx = 0; - while idx < waiters.len() { - let wait = waiters[idx].wait; - if wait <= now { - self.stats.timeouts += 1; - let waiter = waiters.swap_remove_back(idx).unwrap(); - let _ = waiter.tx.send(Err(ClientConnectorError::Timeout)); - } else { - if let Some(n) = next { - if wait < n { - next = Some(wait); - } - } else { - next = Some(wait); - } - idx += 1; - } - } - } - - if next.is_some() { - self.install_wait_timeout(next.unwrap()); - } - } - - fn install_wait_timeout(&mut self, time: Instant) { - if let Some(ref mut wait) = self.wait_timeout { - if wait.0 < time { - return; - } - } - - let mut timeout = Delay::new(time); - let _ = timeout.poll(); - self.wait_timeout = Some((time, timeout)); - } - - fn wait_for( - &mut self, key: Key, wait: Duration, conn_timeout: Duration, - ) -> oneshot::Receiver> { - // connection is not available, wait - let (tx, rx) = oneshot::channel(); - - let wait = Instant::now() + wait; - self.install_wait_timeout(wait); - - let waiter = Waiter { - tx, - wait, - conn_timeout, - }; - self.waiters - .as_mut() - .unwrap() - .entry(key) - .or_insert_with(VecDeque::new) - .push_back(waiter); - rx - } - - fn check_availibility(&mut self, ctx: &mut Context) { - // check waiters - let mut act_waiters = self.waiters.take().unwrap(); - - for (key, ref mut waiters) in &mut act_waiters { - while let Some(waiter) = waiters.pop_front() { - if waiter.tx.is_canceled() { - continue; - } - - match self.acquire(key) { - Acquire::Acquired(mut conn) => { - // use existing connection - self.stats.reused += 1; - conn.pool = - Some(AcquiredConn(key.clone(), Some(self.acq_tx.clone()))); - let _ = waiter.tx.send(Ok(conn)); - } - Acquire::NotAvailable => { - waiters.push_front(waiter); - break; - } - Acquire::Available => { - // create new connection - self.connect_waiter(&key, waiter, ctx); - } - } - } - } - - self.waiters = Some(act_waiters); - } - - fn connect_waiter(&mut self, key: &Key, waiter: Waiter, ctx: &mut Context) { - let key = key.clone(); - let conn = AcquiredConn(key.clone(), Some(self.acq_tx.clone())); - - let key2 = key.clone(); - fut::WrapFuture::::actfuture( - self.resolver.as_ref().unwrap().send( - ResolveConnect::host_and_port(&conn.0.host, conn.0.port) - .timeout(waiter.conn_timeout), - ), - ).map_err(move |_, act, _| { - act.release_key(&key2); - () - }).and_then(move |res, act, _| { - #[cfg(any(feature = "alpn", feature = "ssl"))] - match res { - Err(err) => { - let _ = waiter.tx.send(Err(err.into())); - fut::Either::B(fut::err(())) - } - Ok(stream) => { - act.stats.opened += 1; - if conn.0.ssl { - fut::Either::A( - act.connector - .connect_async(&key.host, stream) - .into_actor(act) - .then(move |res, _, _| { - match res { - Err(e) => { - let _ = waiter.tx.send(Err( - ClientConnectorError::SslError(e), - )); - } - Ok(stream) => { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - } - } - fut::ok(()) - }), - ) - } else { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - fut::Either::B(fut::ok(())) - } - } - } - - #[cfg(all(feature = "tls", not(any(feature = "alpn", feature = "ssl"))))] - match res { - Err(err) => { - let _ = waiter.tx.send(Err(err.into())); - fut::Either::B(fut::err(())) - } - Ok(stream) => { - act.stats.opened += 1; - if conn.0.ssl { - fut::Either::A( - act.connector - .connect(&conn.0.host, stream) - .into_actor(act) - .then(move |res, _, _| { - match res { - Err(e) => { - let _ = waiter.tx.send(Err( - ClientConnectorError::SslError(e), - )); - } - Ok(stream) => { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - } - } - fut::ok(()) - }), - ) - } else { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - fut::Either::B(fut::ok(())) - } - } - } - - #[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "ssl", feature = "tls")) - ))] - match res { - Err(err) => { - let _ = waiter.tx.send(Err(err.into())); - fut::Either::B(fut::err(())) - } - Ok(stream) => { - act.stats.opened += 1; - if conn.0.ssl { - let host = DNSNameRef::try_from_ascii_str(&key.host).unwrap(); - fut::Either::A( - act.connector - .connect_async(host, stream) - .into_actor(act) - .then(move |res, _, _| { - match res { - Err(e) => { - let _ = waiter.tx.send(Err( - ClientConnectorError::SslError(e), - )); - } - Ok(stream) => { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - } - } - fut::ok(()) - }), - ) - } else { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - fut::Either::B(fut::ok(())) - } - } - } - - #[cfg(not(any( - feature = "alpn", - feature = "ssl", - feature = "tls", - feature = "rust-tls" - )))] - match res { - Err(err) => { - let _ = waiter.tx.send(Err(err.into())); - fut::err(()) - } - Ok(stream) => { - act.stats.opened += 1; - if conn.0.ssl { - let _ = - waiter.tx.send(Err(ClientConnectorError::SslIsNotSupported)); - } else { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - }; - fut::ok(()) - } - } - }).spawn(ctx); - } -} - -impl Handler for ClientConnector { - type Result = (); - - fn handle(&mut self, msg: Pause, _: &mut Self::Context) { - if let Some(time) = msg.time { - let when = Instant::now() + time; - let mut timeout = Delay::new(when); - let _ = timeout.poll(); - self.paused = Paused::Timeout(when, timeout); - } else { - self.paused = Paused::Yes; - } - } -} - -impl Handler for ClientConnector { - type Result = (); - - fn handle(&mut self, _: Resume, _: &mut Self::Context) { - self.paused = Paused::No; - } -} - -impl Handler for ClientConnector { - type Result = ActorResponse; - - fn handle(&mut self, msg: Connect, ctx: &mut Self::Context) -> Self::Result { - let uri = &msg.uri; - let wait_timeout = msg.wait_timeout; - let conn_timeout = msg.conn_timeout; - - // host name is required - if uri.host().is_none() { - return ActorResponse::reply(Err(ClientConnectorError::InvalidUrl)); - } - - // supported protocols - let proto = match uri.scheme_part() { - Some(scheme) => match Protocol::from(scheme.as_str()) { - Some(proto) => proto, - None => { - return ActorResponse::reply(Err(ClientConnectorError::InvalidUrl)) - } - }, - None => return ActorResponse::reply(Err(ClientConnectorError::InvalidUrl)), - }; - - // check ssl availability - if proto.is_secure() && !HAS_OPENSSL && !HAS_TLS && !HAS_RUSTLS { - return ActorResponse::reply(Err(ClientConnectorError::SslIsNotSupported)); - } - - let host = uri.host().unwrap().to_owned(); - let port = uri.port().unwrap_or_else(|| proto.port()); - let key = Key { - host, - port, - ssl: proto.is_secure(), - }; - - // check pause state - if self.paused.is_paused() { - let rx = self.wait_for(key.clone(), wait_timeout, conn_timeout); - self.stats.waits += 1; - return ActorResponse::async( - rx.map_err(|_| ClientConnectorError::Disconnected) - .into_actor(self) - .and_then(move |res, act, ctx| match res { - Ok(conn) => fut::ok(conn), - Err(err) => { - match err { - ClientConnectorError::Timeout => (), - _ => { - act.release_key(&key); - } - } - act.stats.errors += 1; - act.check_availibility(ctx); - fut::err(err) - } - }), - ); - } - - // do not re-use websockets connection - if !proto.is_http() { - let (tx, rx) = oneshot::channel(); - let wait = Instant::now() + wait_timeout; - let waiter = Waiter { - tx, - wait, - conn_timeout, - }; - self.connect_waiter(&key, waiter, ctx); - - return ActorResponse::async( - rx.map_err(|_| ClientConnectorError::Disconnected) - .into_actor(self) - .and_then(move |res, act, ctx| match res { - Ok(conn) => fut::ok(conn), - Err(err) => { - act.stats.errors += 1; - act.release_key(&key); - act.check_availibility(ctx); - fut::err(err) - } - }), - ); - } - - // acquire connection - match self.acquire(&key) { - Acquire::Acquired(mut conn) => { - // use existing connection - conn.pool = Some(AcquiredConn(key, Some(self.acq_tx.clone()))); - self.stats.reused += 1; - ActorResponse::async(fut::ok(conn)) - } - Acquire::NotAvailable => { - // connection is not available, wait - let rx = self.wait_for(key.clone(), wait_timeout, conn_timeout); - self.stats.waits += 1; - - ActorResponse::async( - rx.map_err(|_| ClientConnectorError::Disconnected) - .into_actor(self) - .and_then(move |res, act, ctx| match res { - Ok(conn) => fut::ok(conn), - Err(err) => { - match err { - ClientConnectorError::Timeout => (), - _ => { - act.release_key(&key); - } - } - act.stats.errors += 1; - act.check_availibility(ctx); - fut::err(err) - } - }), - ) - } - Acquire::Available => { - let (tx, rx) = oneshot::channel(); - let wait = Instant::now() + wait_timeout; - let waiter = Waiter { - tx, - wait, - conn_timeout, - }; - self.connect_waiter(&key, waiter, ctx); - - ActorResponse::async( - rx.map_err(|_| ClientConnectorError::Disconnected) - .into_actor(self) - .and_then(move |res, act, ctx| match res { - Ok(conn) => fut::ok(conn), - Err(err) => { - act.stats.errors += 1; - act.release_key(&key); - act.check_availibility(ctx); - fut::err(err) - } - }), - ) - } - } - } -} - -impl StreamHandler for ClientConnector { - fn handle(&mut self, msg: AcquiredConnOperation, ctx: &mut Context) { - match msg { - AcquiredConnOperation::Close(conn) => { - self.release_key(&conn.key); - self.to_close.push(conn); - self.stats.closed += 1; - } - AcquiredConnOperation::Release(conn) => { - self.release_key(&conn.key); - if (Instant::now() - conn.ts) < self.conn_lifetime { - self.available - .entry(conn.key.clone()) - .or_insert_with(VecDeque::new) - .push_back(Conn(Instant::now(), conn)); - } else { - self.to_close.push(conn); - self.stats.closed += 1; - } - } - AcquiredConnOperation::ReleaseKey(key) => { - // closed - self.stats.closed += 1; - self.release_key(&key); - } - } - - self.check_availibility(ctx); - } -} - -struct Maintenance; - -impl fut::ActorFuture for Maintenance { - type Item = (); - type Error = (); - type Actor = ClientConnector; - - fn poll( - &mut self, act: &mut ClientConnector, ctx: &mut Context, - ) -> Poll { - // check pause duration - if let Paused::Timeout(inst, _) = act.paused { - if inst <= Instant::now() { - act.paused = Paused::No; - } - } - - // collect wait timers - act.collect_waiters(); - - // check waiters - act.check_availibility(ctx); - - Ok(Async::NotReady) - } -} - -#[derive(PartialEq, Hash, Debug, Clone, Copy)] -enum Protocol { - Http, - Https, - Ws, - Wss, -} - -impl Protocol { - fn from(s: &str) -> Option { - match s { - "http" => Some(Protocol::Http), - "https" => Some(Protocol::Https), - "ws" => Some(Protocol::Ws), - "wss" => Some(Protocol::Wss), - _ => None, - } - } - - fn is_http(self) -> bool { - match self { - Protocol::Https | Protocol::Http => true, - _ => false, - } - } - - fn is_secure(self) -> bool { - match self { - Protocol::Https | Protocol::Wss => true, - _ => false, - } - } - - fn port(self) -> u16 { - match self { - Protocol::Http | Protocol::Ws => 80, - Protocol::Https | Protocol::Wss => 443, - } - } -} - -#[derive(Hash, Eq, PartialEq, Clone, Debug)] -struct Key { - host: String, - port: u16, - ssl: bool, -} - -impl Key { - fn empty() -> Key { - Key { - host: String::new(), - port: 0, - ssl: false, - } - } -} - -#[derive(Debug)] -struct Conn(Instant, Connection); - -enum Acquire { - Acquired(Connection), - Available, - NotAvailable, -} - -enum AcquiredConnOperation { - Close(Connection), - Release(Connection), - ReleaseKey(Key), -} - -struct AcquiredConn(Key, Option>); - -impl AcquiredConn { - fn close(&mut self, conn: Connection) { - if let Some(tx) = self.1.take() { - let _ = tx.unbounded_send(AcquiredConnOperation::Close(conn)); - } - } - fn release(&mut self, conn: Connection) { - if let Some(tx) = self.1.take() { - let _ = tx.unbounded_send(AcquiredConnOperation::Release(conn)); - } - } -} - -impl Drop for AcquiredConn { - fn drop(&mut self) { - if let Some(tx) = self.1.take() { - let _ = tx.unbounded_send(AcquiredConnOperation::ReleaseKey(self.0.clone())); - } - } -} - -/// HTTP client connection -pub struct Connection { - key: Key, - stream: Box, - pool: Option, - ts: Instant, -} - -impl fmt::Debug for Connection { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Connection {}:{}", self.key.host, self.key.port) - } -} - -impl Connection { - fn new(key: Key, pool: Option, stream: Box) -> Self { - Connection { - key, - stream, - pool, - ts: Instant::now(), - } - } - - /// Raw IO stream - pub fn stream(&mut self) -> &mut IoStream { - &mut *self.stream - } - - /// Create a new connection from an IO Stream - /// - /// The stream can be a `UnixStream` if the Unix-only "uds" feature is enabled. - /// - /// See also `ClientRequestBuilder::with_connection()`. - pub fn from_stream(io: T) -> Connection { - Connection::new(Key::empty(), None, Box::new(io)) - } - - /// Close connection - pub fn close(mut self) { - if let Some(mut pool) = self.pool.take() { - pool.close(self) - } - } - - /// Release this connection to the connection pool - pub fn release(mut self) { - if let Some(mut pool) = self.pool.take() { - pool.release(self) - } - } -} - -impl IoStream for Connection { - fn shutdown(&mut self, how: Shutdown) -> io::Result<()> { - IoStream::shutdown(&mut *self.stream, how) - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - IoStream::set_nodelay(&mut *self.stream, nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - IoStream::set_linger(&mut *self.stream, dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - IoStream::set_keepalive(&mut *self.stream, dur) - } -} - -impl io::Read for Connection { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.stream.read(buf) - } -} - -impl AsyncRead for Connection {} - -impl io::Write for Connection { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.stream.write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.stream.flush() - } -} - -impl AsyncWrite for Connection { - fn shutdown(&mut self) -> Poll<(), io::Error> { - self.stream.shutdown() - } -} - -#[cfg(feature = "tls")] -use tokio_tls::TlsStream; - -#[cfg(feature = "tls")] -/// This is temp solution untile actix-net migration -impl IoStream for TlsStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = self.get_mut().shutdown(); - Ok(()) - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().get_mut().set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_keepalive(dur) - } -} diff --git a/src/client/mod.rs b/src/client/mod.rs deleted file mode 100644 index a0713fe32..000000000 --- a/src/client/mod.rs +++ /dev/null @@ -1,118 +0,0 @@ -//! Http client api -//! -//! ```rust -//! # extern crate actix_web; -//! # extern crate futures; -//! # extern crate tokio; -//! # use futures::Future; -//! # use std::process; -//! use actix_web::{actix, client}; -//! -//! fn main() { -//! actix::run( -//! || client::get("http://www.rust-lang.org") // <- Create request builder -//! .header("User-Agent", "Actix-web") -//! .finish().unwrap() -//! .send() // <- Send http request -//! .map_err(|_| ()) -//! .and_then(|response| { // <- server http response -//! println!("Response: {:?}", response); -//! # actix::System::current().stop(); -//! Ok(()) -//! }) -//! ); -//! } -//! ``` -mod connector; -mod parser; -mod pipeline; -mod request; -mod response; -mod writer; - -pub use self::connector::{ - ClientConnector, ClientConnectorError, ClientConnectorStats, Connect, Connection, - Pause, Resume, -}; -pub(crate) use self::parser::{HttpResponseParser, HttpResponseParserError}; -pub(crate) use self::pipeline::Pipeline; -pub use self::pipeline::{SendRequest, SendRequestError}; -pub use self::request::{ClientRequest, ClientRequestBuilder}; -pub use self::response::ClientResponse; -pub(crate) use self::writer::HttpClientWriter; - -use error::ResponseError; -use http::Method; -use httpresponse::HttpResponse; - -/// Convert `SendRequestError` to a `HttpResponse` -impl ResponseError for SendRequestError { - fn error_response(&self) -> HttpResponse { - match *self { - SendRequestError::Timeout => HttpResponse::GatewayTimeout(), - SendRequestError::Connector(_) => HttpResponse::BadGateway(), - _ => HttpResponse::InternalServerError(), - }.into() - } -} - -/// Create request builder for `GET` requests -/// -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate futures; -/// # extern crate tokio; -/// # extern crate env_logger; -/// # use futures::Future; -/// # use std::process; -/// use actix_web::{actix, client}; -/// -/// fn main() { -/// actix::run( -/// || client::get("http://www.rust-lang.org") // <- Create request builder -/// .header("User-Agent", "Actix-web") -/// .finish().unwrap() -/// .send() // <- Send http request -/// .map_err(|_| ()) -/// .and_then(|response| { // <- server http response -/// println!("Response: {:?}", response); -/// # actix::System::current().stop(); -/// Ok(()) -/// }), -/// ); -/// } -/// ``` -pub fn get>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::GET).uri(uri); - builder -} - -/// Create request builder for `HEAD` requests -pub fn head>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::HEAD).uri(uri); - builder -} - -/// Create request builder for `POST` requests -pub fn post>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::POST).uri(uri); - builder -} - -/// Create request builder for `PUT` requests -pub fn put>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::PUT).uri(uri); - builder -} - -/// Create request builder for `DELETE` requests -pub fn delete>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::DELETE).uri(uri); - builder -} diff --git a/src/client/parser.rs b/src/client/parser.rs deleted file mode 100644 index 11252fa52..000000000 --- a/src/client/parser.rs +++ /dev/null @@ -1,238 +0,0 @@ -use std::mem; - -use bytes::{Bytes, BytesMut}; -use futures::{Async, Poll}; -use http::header::{self, HeaderName, HeaderValue}; -use http::{HeaderMap, StatusCode, Version}; -use httparse; - -use error::{ParseError, PayloadError}; - -use server::h1decoder::{EncodingDecoder, HeaderIndex}; -use server::IoStream; - -use super::response::ClientMessage; -use super::ClientResponse; - -const MAX_BUFFER_SIZE: usize = 131_072; -const MAX_HEADERS: usize = 96; - -#[derive(Default)] -pub struct HttpResponseParser { - decoder: Option, - eof: bool, // indicate that we read payload until stream eof -} - -#[derive(Debug, Fail)] -pub enum HttpResponseParserError { - /// Server disconnected - #[fail(display = "Server disconnected")] - Disconnect, - #[fail(display = "{}", _0)] - Error(#[cause] ParseError), -} - -impl HttpResponseParser { - pub fn parse( - &mut self, io: &mut T, buf: &mut BytesMut, - ) -> Poll - where - T: IoStream, - { - loop { - // Don't call parser until we have data to parse. - if !buf.is_empty() { - match HttpResponseParser::parse_message(buf) - .map_err(HttpResponseParserError::Error)? - { - Async::Ready((msg, info)) => { - if let Some((decoder, eof)) = info { - self.eof = eof; - self.decoder = Some(decoder); - } else { - self.eof = false; - self.decoder = None; - } - return Ok(Async::Ready(msg)); - } - Async::NotReady => { - if buf.capacity() >= MAX_BUFFER_SIZE { - return Err(HttpResponseParserError::Error( - ParseError::TooLarge, - )); - } - // Parser needs more data. - } - } - } - // Read some more data into the buffer for the parser. - match io.read_available(buf) { - Ok(Async::Ready((false, true))) => { - return Err(HttpResponseParserError::Disconnect) - } - Ok(Async::Ready(_)) => (), - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(err) => return Err(HttpResponseParserError::Error(err.into())), - } - } - } - - pub fn parse_payload( - &mut self, io: &mut T, buf: &mut BytesMut, - ) -> Poll, PayloadError> - where - T: IoStream, - { - if self.decoder.is_some() { - loop { - // read payload - let (not_ready, stream_finished) = match io.read_available(buf) { - Ok(Async::Ready((_, true))) => (false, true), - Ok(Async::Ready((_, false))) => (false, false), - Ok(Async::NotReady) => (true, false), - Err(err) => return Err(err.into()), - }; - - match self.decoder.as_mut().unwrap().decode(buf) { - Ok(Async::Ready(Some(b))) => return Ok(Async::Ready(Some(b))), - Ok(Async::Ready(None)) => { - self.decoder.take(); - return Ok(Async::Ready(None)); - } - Ok(Async::NotReady) => { - if not_ready { - return Ok(Async::NotReady); - } - if stream_finished { - // read untile eof? - if self.eof { - return Ok(Async::Ready(None)); - } else { - return Err(PayloadError::Incomplete); - } - } - } - Err(err) => return Err(err.into()), - } - } - } else { - Ok(Async::Ready(None)) - } - } - - fn parse_message( - buf: &mut BytesMut, - ) -> Poll<(ClientResponse, Option<(EncodingDecoder, bool)>), ParseError> { - // Unsafe: we read only this data only after httparse parses headers into. - // performance bump for pipeline benchmarks. - let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { mem::uninitialized() }; - - let (len, version, status, headers_len) = { - let mut parsed: [httparse::Header; MAX_HEADERS] = - unsafe { mem::uninitialized() }; - - let mut resp = httparse::Response::new(&mut parsed); - match resp.parse(buf)? { - httparse::Status::Complete(len) => { - let version = if resp.version.unwrap_or(1) == 1 { - Version::HTTP_11 - } else { - Version::HTTP_10 - }; - HeaderIndex::record(buf, resp.headers, &mut headers); - let status = StatusCode::from_u16(resp.code.unwrap()) - .map_err(|_| ParseError::Status)?; - - (len, version, status, resp.headers.len()) - } - httparse::Status::Partial => return Ok(Async::NotReady), - } - }; - - let slice = buf.split_to(len).freeze(); - - // convert headers - let mut hdrs = HeaderMap::new(); - for idx in headers[..headers_len].iter() { - if let Ok(name) = HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]) { - // Unsafe: httparse check header value for valid utf-8 - let value = unsafe { - HeaderValue::from_shared_unchecked( - slice.slice(idx.value.0, idx.value.1), - ) - }; - hdrs.append(name, value); - } else { - return Err(ParseError::Header); - } - } - - let decoder = if status == StatusCode::SWITCHING_PROTOCOLS { - Some((EncodingDecoder::eof(), true)) - } else if let Some(len) = hdrs.get(header::CONTENT_LENGTH) { - // Content-Length - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - Some((EncodingDecoder::length(len), false)) - } else { - debug!("illegal Content-Length: {:?}", len); - return Err(ParseError::Header); - } - } else { - debug!("illegal Content-Length: {:?}", len); - return Err(ParseError::Header); - } - } else if chunked(&hdrs)? { - // Chunked encoding - Some((EncodingDecoder::chunked(), false)) - } else if let Some(value) = hdrs.get(header::CONNECTION) { - let close = if let Ok(s) = value.to_str() { - s == "close" - } else { - false - }; - if close { - Some((EncodingDecoder::eof(), true)) - } else { - None - } - } else { - None - }; - - if let Some(decoder) = decoder { - Ok(Async::Ready(( - ClientResponse::new(ClientMessage { - status, - version, - headers: hdrs, - cookies: None, - }), - Some(decoder), - ))) - } else { - Ok(Async::Ready(( - ClientResponse::new(ClientMessage { - status, - version, - headers: hdrs, - cookies: None, - }), - None, - ))) - } - } -} - -/// Check if request has chunked transfer encoding -pub fn chunked(headers: &HeaderMap) -> Result { - if let Some(encodings) = headers.get(header::TRANSFER_ENCODING) { - if let Ok(s) = encodings.to_str() { - Ok(s.to_lowercase().contains("chunked")) - } else { - Err(ParseError::Header) - } - } else { - Ok(false) - } -} diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs deleted file mode 100644 index 394b7a6cd..000000000 --- a/src/client/pipeline.rs +++ /dev/null @@ -1,552 +0,0 @@ -use bytes::{Bytes, BytesMut}; -use futures::sync::oneshot; -use futures::{Async, Future, Poll, Stream}; -use http::header::CONTENT_ENCODING; -use std::time::{Duration, Instant}; -use std::{io, mem}; -use tokio_timer::Delay; - -use actix::{Addr, Request, SystemService}; - -use super::{ - ClientConnector, ClientConnectorError, ClientRequest, ClientResponse, Connect, - Connection, HttpClientWriter, HttpResponseParser, HttpResponseParserError, -}; -use body::{Body, BodyStream}; -use context::{ActorHttpContext, Frame}; -use error::Error; -use error::PayloadError; -use header::ContentEncoding; -use http::{Method, Uri}; -use httpmessage::HttpMessage; -use server::input::PayloadStream; -use server::WriterState; - -/// A set of errors that can occur during request sending and response reading -#[derive(Fail, Debug)] -pub enum SendRequestError { - /// Response took too long - #[fail(display = "Timeout while waiting for response")] - Timeout, - /// Failed to connect to host - #[fail(display = "Failed to connect to host: {}", _0)] - Connector(#[cause] ClientConnectorError), - /// Error parsing response - #[fail(display = "{}", _0)] - ParseError(#[cause] HttpResponseParserError), - /// Error reading response payload - #[fail(display = "Error reading response payload: {}", _0)] - Io(#[cause] io::Error), -} - -impl From for SendRequestError { - fn from(err: io::Error) -> SendRequestError { - SendRequestError::Io(err) - } -} - -impl From for SendRequestError { - fn from(err: ClientConnectorError) -> SendRequestError { - match err { - ClientConnectorError::Timeout => SendRequestError::Timeout, - _ => SendRequestError::Connector(err), - } - } -} - -enum State { - New, - Connect(Request), - Connection(Connection), - Send(Box), - None, -} - -/// `SendRequest` is a `Future` which represents an asynchronous -/// request sending process. -#[must_use = "SendRequest does nothing unless polled"] -pub struct SendRequest { - req: ClientRequest, - state: State, - conn: Option>, - conn_timeout: Duration, - wait_timeout: Duration, - timeout: Option, -} - -impl SendRequest { - pub(crate) fn new(req: ClientRequest) -> SendRequest { - SendRequest { - req, - conn: None, - state: State::New, - timeout: None, - wait_timeout: Duration::from_secs(5), - conn_timeout: Duration::from_secs(1), - } - } - - pub(crate) fn with_connector( - req: ClientRequest, conn: Addr, - ) -> SendRequest { - SendRequest { - req, - conn: Some(conn), - state: State::New, - timeout: None, - wait_timeout: Duration::from_secs(5), - conn_timeout: Duration::from_secs(1), - } - } - - pub(crate) fn with_connection(req: ClientRequest, conn: Connection) -> SendRequest { - SendRequest { - req, - state: State::Connection(conn), - conn: None, - timeout: None, - wait_timeout: Duration::from_secs(5), - conn_timeout: Duration::from_secs(1), - } - } - - /// Set request timeout - /// - /// Request timeout is the total time before a response must be received. - /// Default value is 5 seconds. - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = Some(timeout); - self - } - - /// Set connection timeout - /// - /// Connection timeout includes resolving hostname and actual connection to - /// the host. - /// Default value is 1 second. - pub fn conn_timeout(mut self, timeout: Duration) -> Self { - self.conn_timeout = timeout; - self - } - - /// Set wait timeout - /// - /// If connections pool limits are enabled, wait time indicates max time - /// to wait for available connection. Default value is 5 seconds. - pub fn wait_timeout(mut self, timeout: Duration) -> Self { - self.wait_timeout = timeout; - self - } -} - -impl Future for SendRequest { - type Item = ClientResponse; - type Error = SendRequestError; - - fn poll(&mut self) -> Poll { - loop { - let state = mem::replace(&mut self.state, State::None); - - match state { - State::New => { - let conn = if let Some(conn) = self.conn.take() { - conn - } else { - ClientConnector::from_registry() - }; - self.state = State::Connect(conn.send(Connect { - uri: self.req.uri().clone(), - wait_timeout: self.wait_timeout, - conn_timeout: self.conn_timeout, - })) - } - State::Connect(mut conn) => match conn.poll() { - Ok(Async::NotReady) => { - self.state = State::Connect(conn); - return Ok(Async::NotReady); - } - Ok(Async::Ready(result)) => match result { - Ok(stream) => self.state = State::Connection(stream), - Err(err) => return Err(err.into()), - }, - Err(_) => { - return Err(SendRequestError::Connector( - ClientConnectorError::Disconnected, - )); - } - }, - State::Connection(conn) => { - let mut writer = HttpClientWriter::new(); - writer.start(&mut self.req)?; - - let body = match self.req.replace_body(Body::Empty) { - Body::Streaming(stream) => IoBody::Payload(stream), - Body::Actor(ctx) => IoBody::Actor(ctx), - _ => IoBody::Done, - }; - - let timeout = self - .timeout - .take() - .unwrap_or_else(|| Duration::from_secs(5)); - - let pl = Box::new(Pipeline { - body, - writer, - conn: Some(conn), - parser: Some(HttpResponseParser::default()), - parser_buf: BytesMut::new(), - disconnected: false, - body_completed: false, - drain: None, - decompress: None, - should_decompress: self.req.response_decompress(), - write_state: RunningState::Running, - timeout: Some(Delay::new(Instant::now() + timeout)), - meth: self.req.method().clone(), - path: self.req.uri().clone(), - }); - self.state = State::Send(pl); - } - State::Send(mut pl) => { - pl.poll_timeout()?; - pl.poll_write().map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("{}", e).as_str()) - })?; - - match pl.parse() { - Ok(Async::Ready(mut resp)) => { - if self.req.method() == Method::HEAD { - pl.parser.take(); - } - resp.set_pipeline(pl); - return Ok(Async::Ready(resp)); - } - Ok(Async::NotReady) => { - self.state = State::Send(pl); - return Ok(Async::NotReady); - } - Err(err) => { - return Err(SendRequestError::ParseError(err)); - } - } - } - State::None => unreachable!(), - } - } - } -} - -pub struct Pipeline { - body: IoBody, - body_completed: bool, - conn: Option, - writer: HttpClientWriter, - parser: Option, - parser_buf: BytesMut, - disconnected: bool, - drain: Option>, - decompress: Option, - should_decompress: bool, - write_state: RunningState, - timeout: Option, - meth: Method, - path: Uri, -} - -enum IoBody { - Payload(BodyStream), - Actor(Box), - Done, -} - -#[derive(Debug, PartialEq)] -enum RunningState { - Running, - Paused, - Done, -} - -impl RunningState { - #[inline] - fn pause(&mut self) { - if *self != RunningState::Done { - *self = RunningState::Paused - } - } - #[inline] - fn resume(&mut self) { - if *self != RunningState::Done { - *self = RunningState::Running - } - } -} - -impl Pipeline { - fn release_conn(&mut self) { - if let Some(conn) = self.conn.take() { - if self.meth == Method::HEAD { - conn.close() - } else { - conn.release() - } - } - } - - #[inline] - fn parse(&mut self) -> Poll { - if let Some(ref mut conn) = self.conn { - match self - .parser - .as_mut() - .unwrap() - .parse(conn, &mut self.parser_buf) - { - Ok(Async::Ready(resp)) => { - // check content-encoding - if self.should_decompress { - if let Some(enc) = resp.headers().get(CONTENT_ENCODING) { - if let Ok(enc) = enc.to_str() { - match ContentEncoding::from(enc) { - ContentEncoding::Auto - | ContentEncoding::Identity => (), - enc => { - self.decompress = Some(PayloadStream::new(enc)) - } - } - } - } - } - - Ok(Async::Ready(resp)) - } - val => val, - } - } else { - Ok(Async::NotReady) - } - } - - #[inline] - pub(crate) fn poll(&mut self) -> Poll, PayloadError> { - if self.conn.is_none() { - return Ok(Async::Ready(None)); - } - let mut need_run = false; - - // need write? - match self - .poll_write() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{}", e)))? - { - Async::NotReady => need_run = true, - Async::Ready(_) => { - self.poll_timeout().map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("{}", e)) - })?; - } - } - - // need read? - if self.parser.is_some() { - let conn: &mut Connection = self.conn.as_mut().unwrap(); - - loop { - match self - .parser - .as_mut() - .unwrap() - .parse_payload(conn, &mut self.parser_buf)? - { - Async::Ready(Some(b)) => { - if let Some(ref mut decompress) = self.decompress { - match decompress.feed_data(b) { - Ok(Some(b)) => return Ok(Async::Ready(Some(b))), - Ok(None) => return Ok(Async::NotReady), - Err(ref err) - if err.kind() == io::ErrorKind::WouldBlock => - { - continue - } - Err(err) => return Err(err.into()), - } - } else { - return Ok(Async::Ready(Some(b))); - } - } - Async::Ready(None) => { - let _ = self.parser.take(); - break; - } - Async::NotReady => return Ok(Async::NotReady), - } - } - } - - // eof - if let Some(mut decompress) = self.decompress.take() { - let res = decompress.feed_eof(); - if let Some(b) = res? { - self.release_conn(); - return Ok(Async::Ready(Some(b))); - } - } - - if need_run { - Ok(Async::NotReady) - } else { - self.release_conn(); - Ok(Async::Ready(None)) - } - } - - fn poll_timeout(&mut self) -> Result<(), SendRequestError> { - if self.timeout.is_some() { - match self.timeout.as_mut().unwrap().poll() { - Ok(Async::Ready(())) => return Err(SendRequestError::Timeout), - Ok(Async::NotReady) => (), - Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e).into()), - } - } - Ok(()) - } - - #[inline] - fn poll_write(&mut self) -> Poll<(), Error> { - if self.write_state == RunningState::Done || self.conn.is_none() { - return Ok(Async::Ready(())); - } - - let mut done = false; - if self.drain.is_none() && self.write_state != RunningState::Paused { - 'outter: loop { - let result = match mem::replace(&mut self.body, IoBody::Done) { - IoBody::Payload(mut body) => match body.poll()? { - Async::Ready(None) => { - self.writer.write_eof()?; - self.body_completed = true; - break; - } - Async::Ready(Some(chunk)) => { - self.body = IoBody::Payload(body); - self.writer.write(chunk.as_ref())? - } - Async::NotReady => { - done = true; - self.body = IoBody::Payload(body); - break; - } - }, - IoBody::Actor(mut ctx) => { - if self.disconnected { - ctx.disconnected(); - } - match ctx.poll()? { - Async::Ready(Some(vec)) => { - if vec.is_empty() { - self.body = IoBody::Actor(ctx); - break; - } - let mut res = None; - for frame in vec { - match frame { - Frame::Chunk(None) => { - self.body_completed = true; - self.writer.write_eof()?; - break 'outter; - } - Frame::Chunk(Some(chunk)) => { - res = - Some(self.writer.write(chunk.as_ref())?) - } - Frame::Drain(fut) => self.drain = Some(fut), - } - } - self.body = IoBody::Actor(ctx); - if self.drain.is_some() { - self.write_state.resume(); - break; - } - res.unwrap() - } - Async::Ready(None) => { - done = true; - break; - } - Async::NotReady => { - done = true; - self.body = IoBody::Actor(ctx); - break; - } - } - } - IoBody::Done => { - self.body_completed = true; - done = true; - break; - } - }; - - match result { - WriterState::Pause => { - self.write_state.pause(); - break; - } - WriterState::Done => self.write_state.resume(), - } - } - } - - // flush io but only if we need to - match self - .writer - .poll_completed(self.conn.as_mut().unwrap(), false) - { - Ok(Async::Ready(_)) => { - if self.disconnected - || (self.body_completed && self.writer.is_completed()) - { - self.write_state = RunningState::Done; - } else { - self.write_state.resume(); - } - - // resolve drain futures - if let Some(tx) = self.drain.take() { - let _ = tx.send(()); - } - // restart io processing - if !done || self.write_state == RunningState::Done { - self.poll_write() - } else { - Ok(Async::NotReady) - } - } - Ok(Async::NotReady) => Ok(Async::NotReady), - Err(err) => Err(err.into()), - } - } -} - -impl Drop for Pipeline { - fn drop(&mut self) { - if let Some(conn) = self.conn.take() { - debug!( - "Client http transaction is not completed, dropping connection: {:?} {:?}", - self.meth, - self.path, - ); - conn.close() - } - } -} - -/// Future that resolves to a complete request body. -impl Stream for Box { - type Item = Bytes; - type Error = PayloadError; - - fn poll(&mut self) -> Poll, Self::Error> { - Pipeline::poll(self) - } -} diff --git a/src/client/request.rs b/src/client/request.rs deleted file mode 100644 index 76fb1be59..000000000 --- a/src/client/request.rs +++ /dev/null @@ -1,782 +0,0 @@ -use std::fmt::Write as FmtWrite; -use std::io::Write; -use std::time::Duration; -use std::{fmt, mem}; - -use actix::Addr; -use bytes::{BufMut, Bytes, BytesMut}; -use cookie::{Cookie, CookieJar}; -use futures::Stream; -use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; -use serde::Serialize; -use serde_json; -use serde_urlencoded; -use url::Url; - -use super::connector::{ClientConnector, Connection}; -use super::pipeline::SendRequest; -use body::Body; -use error::Error; -use header::{ContentEncoding, Header, IntoHeaderValue}; -use http::header::{self, HeaderName, HeaderValue}; -use http::{uri, Error as HttpError, HeaderMap, HttpTryFrom, Method, Uri, Version}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; - -/// An HTTP Client Request -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate futures; -/// # extern crate tokio; -/// # use futures::Future; -/// # use std::process; -/// use actix_web::{actix, client}; -/// -/// fn main() { -/// actix::run( -/// || client::ClientRequest::get("http://www.rust-lang.org") // <- Create request builder -/// .header("User-Agent", "Actix-web") -/// .finish().unwrap() -/// .send() // <- Send http request -/// .map_err(|_| ()) -/// .and_then(|response| { // <- server http response -/// println!("Response: {:?}", response); -/// # actix::System::current().stop(); -/// Ok(()) -/// }), -/// ); -/// } -/// ``` -pub struct ClientRequest { - uri: Uri, - method: Method, - version: Version, - headers: HeaderMap, - body: Body, - chunked: bool, - upgrade: bool, - timeout: Option, - encoding: ContentEncoding, - response_decompress: bool, - buffer_capacity: usize, - conn: ConnectionType, -} - -enum ConnectionType { - Default, - Connector(Addr), - Connection(Connection), -} - -impl Default for ClientRequest { - fn default() -> ClientRequest { - ClientRequest { - uri: Uri::default(), - method: Method::default(), - version: Version::HTTP_11, - headers: HeaderMap::with_capacity(16), - body: Body::Empty, - chunked: false, - upgrade: false, - timeout: None, - encoding: ContentEncoding::Auto, - response_decompress: true, - buffer_capacity: 32_768, - conn: ConnectionType::Default, - } - } -} - -impl ClientRequest { - /// Create request builder for `GET` request - pub fn get>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::GET).uri(uri); - builder - } - - /// Create request builder for `HEAD` request - pub fn head>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::HEAD).uri(uri); - builder - } - - /// Create request builder for `POST` request - pub fn post>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::POST).uri(uri); - builder - } - - /// Create request builder for `PUT` request - pub fn put>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::PUT).uri(uri); - builder - } - - /// Create request builder for `DELETE` request - pub fn delete>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::DELETE).uri(uri); - builder - } -} - -impl ClientRequest { - /// Create client request builder - pub fn build() -> ClientRequestBuilder { - ClientRequestBuilder { - request: Some(ClientRequest::default()), - err: None, - cookies: None, - default_headers: true, - } - } - - /// Create client request builder - pub fn build_from>(source: T) -> ClientRequestBuilder { - source.into() - } - - /// Get the request URI - #[inline] - pub fn uri(&self) -> &Uri { - &self.uri - } - - /// Set client request URI - #[inline] - pub fn set_uri(&mut self, uri: Uri) { - self.uri = uri - } - - /// Get the request method - #[inline] - pub fn method(&self) -> &Method { - &self.method - } - - /// Set HTTP `Method` for the request - #[inline] - pub fn set_method(&mut self, method: Method) { - self.method = method - } - - /// Get HTTP version for the request - #[inline] - pub fn version(&self) -> Version { - self.version - } - - /// Set http `Version` for the request - #[inline] - pub fn set_version(&mut self, version: Version) { - self.version = version - } - - /// Get the headers from the request - #[inline] - pub fn headers(&self) -> &HeaderMap { - &self.headers - } - - /// Get a mutable reference to the headers - #[inline] - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.headers - } - - /// is chunked encoding enabled - #[inline] - pub fn chunked(&self) -> bool { - self.chunked - } - - /// is upgrade request - #[inline] - pub fn upgrade(&self) -> bool { - self.upgrade - } - - /// Content encoding - #[inline] - pub fn content_encoding(&self) -> ContentEncoding { - self.encoding - } - - /// Decompress response payload - #[inline] - pub fn response_decompress(&self) -> bool { - self.response_decompress - } - - /// Requested write buffer capacity - pub fn write_buffer_capacity(&self) -> usize { - self.buffer_capacity - } - - /// Get body of this response - #[inline] - pub fn body(&self) -> &Body { - &self.body - } - - /// Set a body - pub fn set_body>(&mut self, body: B) { - self.body = body.into(); - } - - /// Extract body, replace it with `Empty` - pub(crate) fn replace_body(&mut self, body: Body) -> Body { - mem::replace(&mut self.body, body) - } - - /// Send request - /// - /// This method returns a future that resolves to a ClientResponse - pub fn send(mut self) -> SendRequest { - let timeout = self.timeout.take(); - let send = match mem::replace(&mut self.conn, ConnectionType::Default) { - ConnectionType::Default => SendRequest::new(self), - ConnectionType::Connector(conn) => SendRequest::with_connector(self, conn), - ConnectionType::Connection(conn) => SendRequest::with_connection(self, conn), - }; - if let Some(timeout) = timeout { - send.timeout(timeout) - } else { - send - } - } -} - -impl fmt::Debug for ClientRequest { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!( - f, - "\nClientRequest {:?} {}:{}", - self.version, self.method, self.uri - )?; - writeln!(f, " headers:")?; - for (key, val) in self.headers.iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -/// An HTTP Client request builder -/// -/// This type can be used to construct an instance of `ClientRequest` through a -/// builder-like pattern. -pub struct ClientRequestBuilder { - request: Option, - err: Option, - cookies: Option, - default_headers: bool, -} - -impl ClientRequestBuilder { - /// Set HTTP URI of request. - #[inline] - pub fn uri>(&mut self, uri: U) -> &mut Self { - match Url::parse(uri.as_ref()) { - Ok(url) => self._uri(url.as_str()), - Err(_) => self._uri(uri.as_ref()), - } - } - - fn _uri(&mut self, url: &str) -> &mut Self { - match Uri::try_from(url) { - Ok(uri) => { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.uri = uri; - } - } - Err(e) => self.err = Some(e.into()), - } - self - } - - /// Set HTTP method of this request. - #[inline] - pub fn method(&mut self, method: Method) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.method = method; - } - self - } - - /// Set HTTP method of this request. - #[inline] - pub fn get_method(&mut self) -> &Method { - let parts = self.request.as_ref().expect("cannot reuse request builder"); - &parts.method - } - - /// Set HTTP version of this request. - /// - /// By default requests's HTTP version depends on network stream - #[inline] - pub fn version(&mut self, version: Version) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.version = version; - } - self - } - - /// Set a header. - /// - /// ```rust - /// # extern crate mime; - /// # extern crate actix_web; - /// # use actix_web::client::*; - /// # - /// use actix_web::{client, http}; - /// - /// fn main() { - /// let req = client::ClientRequest::build() - /// .set(http::header::Date::now()) - /// .set(http::header::ContentType(mime::TEXT_HTML)) - /// .finish() - /// .unwrap(); - /// } - /// ``` - #[doc(hidden)] - pub fn set(&mut self, hdr: H) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - match hdr.try_into() { - Ok(value) => { - parts.headers.insert(H::name(), value); - } - Err(e) => self.err = Some(e.into()), - } - } - self - } - - /// Append a header. - /// - /// Header gets appended to existing header. - /// To override header use `set_header()` method. - /// - /// ```rust - /// # extern crate http; - /// # extern crate actix_web; - /// # use actix_web::client::*; - /// # - /// use http::header; - /// - /// fn main() { - /// let req = ClientRequest::build() - /// .header("X-TEST", "value") - /// .header(header::CONTENT_TYPE, "application/json") - /// .finish() - /// .unwrap(); - /// } - /// ``` - pub fn header(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.request, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - parts.headers.append(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set a header. - pub fn set_header(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.request, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - parts.headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set a header only if it is not yet set. - pub fn set_header_if_none(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.request, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => if !parts.headers.contains_key(&key) { - match value.try_into() { - Ok(value) => { - parts.headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - } - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set content encoding. - /// - /// By default `ContentEncoding::Identity` is used. - #[inline] - pub fn content_encoding(&mut self, enc: ContentEncoding) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.encoding = enc; - } - self - } - - /// Enables automatic chunked transfer encoding - #[inline] - pub fn chunked(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.chunked = true; - } - self - } - - /// Enable connection upgrade - #[inline] - pub fn upgrade(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.upgrade = true; - } - self - } - - /// Set request's content type - #[inline] - pub fn content_type(&mut self, value: V) -> &mut Self - where - HeaderValue: HttpTryFrom, - { - if let Some(parts) = parts(&mut self.request, &self.err) { - match HeaderValue::try_from(value) { - Ok(value) => { - parts.headers.insert(header::CONTENT_TYPE, value); - } - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set content length - #[inline] - pub fn content_length(&mut self, len: u64) -> &mut Self { - let mut wrt = BytesMut::new().writer(); - let _ = write!(wrt, "{}", len); - self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze()) - } - - /// Set a cookie - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{client, http}; - /// - /// fn main() { - /// let req = client::ClientRequest::build() - /// .cookie( - /// http::Cookie::build("name", "value") - /// .domain("www.rust-lang.org") - /// .path("/") - /// .secure(true) - /// .http_only(true) - /// .finish(), - /// ) - /// .finish() - /// .unwrap(); - /// } - /// ``` - pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { - if self.cookies.is_none() { - let mut jar = CookieJar::new(); - jar.add(cookie.into_owned()); - self.cookies = Some(jar) - } else { - self.cookies.as_mut().unwrap().add(cookie.into_owned()); - } - self - } - - /// Do not add default request headers. - /// By default `Accept-Encoding` and `User-Agent` headers are set. - pub fn no_default_headers(&mut self) -> &mut Self { - self.default_headers = false; - self - } - - /// Disable automatic decompress response body - pub fn disable_decompress(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.response_decompress = false; - } - self - } - - /// Set write buffer capacity - /// - /// Default buffer capacity is 32kb - pub fn write_buffer_capacity(&mut self, cap: usize) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.buffer_capacity = cap; - } - self - } - - /// Set request timeout - /// - /// Request timeout is a total time before response should be received. - /// Default value is 5 seconds. - pub fn timeout(&mut self, timeout: Duration) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.timeout = Some(timeout); - } - self - } - - /// Send request using custom connector - pub fn with_connector(&mut self, conn: Addr) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.conn = ConnectionType::Connector(conn); - } - self - } - - /// Send request using existing `Connection` - pub fn with_connection(&mut self, conn: Connection) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.conn = ConnectionType::Connection(conn); - } - self - } - - /// This method calls provided closure with builder reference if - /// value is `true`. - pub fn if_true(&mut self, value: bool, f: F) -> &mut Self - where - F: FnOnce(&mut ClientRequestBuilder), - { - if value { - f(self); - } - self - } - - /// This method calls provided closure with builder reference if - /// value is `Some`. - pub fn if_some(&mut self, value: Option, f: F) -> &mut Self - where - F: FnOnce(T, &mut ClientRequestBuilder), - { - if let Some(val) = value { - f(val, self); - } - self - } - - /// Set a body and generate `ClientRequest`. - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn body>(&mut self, body: B) -> Result { - if let Some(e) = self.err.take() { - return Err(e.into()); - } - - if self.default_headers { - // enable br only for https - let https = if let Some(parts) = parts(&mut self.request, &self.err) { - parts - .uri - .scheme_part() - .map(|s| s == &uri::Scheme::HTTPS) - .unwrap_or(true) - } else { - true - }; - - if https { - self.set_header_if_none(header::ACCEPT_ENCODING, "br, gzip, deflate"); - } else { - self.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate"); - } - - // set request host header - if let Some(parts) = parts(&mut self.request, &self.err) { - if let Some(host) = parts.uri.host() { - if !parts.headers.contains_key(header::HOST) { - let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); - - let _ = match parts.uri.port() { - None | Some(80) | Some(443) => write!(wrt, "{}", host), - Some(port) => write!(wrt, "{}:{}", host, port), - }; - - match wrt.get_mut().take().freeze().try_into() { - Ok(value) => { - parts.headers.insert(header::HOST, value); - } - Err(e) => self.err = Some(e.into()), - } - } - } - } - - // user agent - self.set_header_if_none( - header::USER_AGENT, - concat!("actix-web/", env!("CARGO_PKG_VERSION")), - ); - } - - let mut request = self.request.take().expect("cannot reuse request builder"); - - // set cookies - if let Some(ref mut jar) = self.cookies { - let mut cookie = String::new(); - for c in jar.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); - let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - request.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); - } - request.body = body.into(); - Ok(request) - } - - /// Set a JSON body and generate `ClientRequest` - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn json(&mut self, value: T) -> Result { - let body = serde_json::to_string(&value)?; - - let contains = if let Some(parts) = parts(&mut self.request, &self.err) { - parts.headers.contains_key(header::CONTENT_TYPE) - } else { - true - }; - if !contains { - self.header(header::CONTENT_TYPE, "application/json"); - } - - self.body(body) - } - - /// Set a urlencoded body and generate `ClientRequest` - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn form(&mut self, value: T) -> Result { - let body = serde_urlencoded::to_string(&value)?; - - let contains = if let Some(parts) = parts(&mut self.request, &self.err) { - parts.headers.contains_key(header::CONTENT_TYPE) - } else { - true - }; - if !contains { - self.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded"); - } - - self.body(body) - } - - /// Set a streaming body and generate `ClientRequest`. - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn streaming(&mut self, stream: S) -> Result - where - S: Stream + 'static, - E: Into, - { - self.body(Body::Streaming(Box::new(stream.map_err(|e| e.into())))) - } - - /// Set an empty body and generate `ClientRequest` - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn finish(&mut self) -> Result { - self.body(Body::Empty) - } - - /// This method construct new `ClientRequestBuilder` - pub fn take(&mut self) -> ClientRequestBuilder { - ClientRequestBuilder { - request: self.request.take(), - err: self.err.take(), - cookies: self.cookies.take(), - default_headers: self.default_headers, - } - } -} - -#[inline] -fn parts<'a>( - parts: &'a mut Option, err: &Option, -) -> Option<&'a mut ClientRequest> { - if err.is_some() { - return None; - } - parts.as_mut() -} - -impl fmt::Debug for ClientRequestBuilder { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(ref parts) = self.request { - writeln!( - f, - "\nClientRequestBuilder {:?} {}:{}", - parts.version, parts.method, parts.uri - )?; - writeln!(f, " headers:")?; - for (key, val) in parts.headers.iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } else { - write!(f, "ClientRequestBuilder(Consumed)") - } - } -} - -/// Create `ClientRequestBuilder` from `HttpRequest` -/// -/// It is useful for proxy requests. This implementation -/// copies all request headers and the method. -impl<'a, S: 'static> From<&'a HttpRequest> for ClientRequestBuilder { - fn from(req: &'a HttpRequest) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - for (key, value) in req.headers() { - builder.header(key.clone(), value.clone()); - } - builder.method(req.method().clone()); - builder - } -} diff --git a/src/client/response.rs b/src/client/response.rs deleted file mode 100644 index 5f1f42649..000000000 --- a/src/client/response.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::cell::RefCell; -use std::{fmt, str}; - -use cookie::Cookie; -use http::header::{self, HeaderValue}; -use http::{HeaderMap, StatusCode, Version}; - -use error::CookieParseError; -use httpmessage::HttpMessage; - -use super::pipeline::Pipeline; - -pub(crate) struct ClientMessage { - pub status: StatusCode, - pub version: Version, - pub headers: HeaderMap, - pub cookies: Option>>, -} - -impl Default for ClientMessage { - fn default() -> ClientMessage { - ClientMessage { - status: StatusCode::OK, - version: Version::HTTP_11, - headers: HeaderMap::with_capacity(16), - cookies: None, - } - } -} - -/// An HTTP Client response -pub struct ClientResponse(ClientMessage, RefCell>>); - -impl HttpMessage for ClientResponse { - type Stream = Box; - - /// Get the headers from the response. - #[inline] - fn headers(&self) -> &HeaderMap { - &self.0.headers - } - - #[inline] - fn payload(&self) -> Box { - self.1 - .borrow_mut() - .take() - .expect("Payload is already consumed.") - } -} - -impl ClientResponse { - pub(crate) fn new(msg: ClientMessage) -> ClientResponse { - ClientResponse(msg, RefCell::new(None)) - } - - pub(crate) fn set_pipeline(&mut self, pl: Box) { - *self.1.borrow_mut() = Some(pl); - } - - /// Get the HTTP version of this response. - #[inline] - pub fn version(&self) -> Version { - self.0.version - } - - /// Get the status from the server. - #[inline] - pub fn status(&self) -> StatusCode { - self.0.status - } - - /// Load response cookies. - pub fn cookies(&self) -> Result>, CookieParseError> { - let mut cookies = Vec::new(); - for val in self.0.headers.get_all(header::SET_COOKIE).iter() { - let s = str::from_utf8(val.as_bytes()).map_err(CookieParseError::from)?; - cookies.push(Cookie::parse_encoded(s)?.into_owned()); - } - Ok(cookies) - } - - /// Return request cookie. - pub fn cookie(&self, name: &str) -> Option { - if let Ok(cookies) = self.cookies() { - for cookie in cookies { - if cookie.name() == name { - return Some(cookie); - } - } - } - None - } -} - -impl fmt::Debug for ClientResponse { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status())?; - writeln!(f, " headers:")?; - for (key, val) in self.headers().iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_debug() { - let mut resp = ClientResponse::new(ClientMessage::default()); - resp.0 - .headers - .insert(header::COOKIE, HeaderValue::from_static("cookie1=value1")); - resp.0 - .headers - .insert(header::COOKIE, HeaderValue::from_static("cookie2=value2")); - - let dbg = format!("{:?}", resp); - assert!(dbg.contains("ClientResponse")); - } -} diff --git a/src/client/writer.rs b/src/client/writer.rs deleted file mode 100644 index e74f22332..000000000 --- a/src/client/writer.rs +++ /dev/null @@ -1,412 +0,0 @@ -#![cfg_attr( - feature = "cargo-clippy", - allow(clippy::redundant_field_names) -)] - -use std::cell::RefCell; -use std::fmt::Write as FmtWrite; -use std::io::{self, Write}; - -#[cfg(feature = "brotli")] -use brotli2::write::BrotliEncoder; -use bytes::{BufMut, BytesMut}; -#[cfg(feature = "flate2")] -use flate2::write::{GzEncoder, ZlibEncoder}; -#[cfg(feature = "flate2")] -use flate2::Compression; -use futures::{Async, Poll}; -use http::header::{ - HeaderValue, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, -}; -use http::{HttpTryFrom, Version}; -use time::{self, Duration}; -use tokio_io::AsyncWrite; - -use body::{Binary, Body}; -use header::ContentEncoding; -use server::output::{ContentEncoder, Output, TransferEncoding}; -use server::WriterState; - -use client::ClientRequest; - -const AVERAGE_HEADER_SIZE: usize = 30; - -bitflags! { - struct Flags: u8 { - const STARTED = 0b0000_0001; - const UPGRADE = 0b0000_0010; - const KEEPALIVE = 0b0000_0100; - const DISCONNECTED = 0b0000_1000; - } -} - -pub(crate) struct HttpClientWriter { - flags: Flags, - written: u64, - headers_size: u32, - buffer: Output, - buffer_capacity: usize, -} - -impl HttpClientWriter { - pub fn new() -> HttpClientWriter { - HttpClientWriter { - flags: Flags::empty(), - written: 0, - headers_size: 0, - buffer_capacity: 0, - buffer: Output::Buffer(BytesMut::new()), - } - } - - pub fn disconnected(&mut self) { - self.buffer.take(); - } - - pub fn is_completed(&self) -> bool { - self.buffer.is_empty() - } - - // pub fn keepalive(&self) -> bool { - // self.flags.contains(Flags::KEEPALIVE) && - // !self.flags.contains(Flags::UPGRADE) } - - fn write_to_stream( - &mut self, stream: &mut T, - ) -> io::Result { - while !self.buffer.is_empty() { - match stream.write(self.buffer.as_ref().as_ref()) { - Ok(0) => { - self.disconnected(); - return Ok(WriterState::Done); - } - Ok(n) => { - let _ = self.buffer.split_to(n); - } - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { - if self.buffer.len() > self.buffer_capacity { - return Ok(WriterState::Pause); - } else { - return Ok(WriterState::Done); - } - } - Err(err) => return Err(err), - } - } - Ok(WriterState::Done) - } -} - -pub struct Writer<'a>(pub &'a mut BytesMut); - -impl<'a> io::Write for Writer<'a> { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.extend_from_slice(buf); - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -impl HttpClientWriter { - pub fn start(&mut self, msg: &mut ClientRequest) -> io::Result<()> { - // prepare task - self.buffer = content_encoder(self.buffer.take(), msg); - self.flags.insert(Flags::STARTED); - if msg.upgrade() { - self.flags.insert(Flags::UPGRADE); - } - - // render message - { - // output buffer - let buffer = self.buffer.as_mut(); - - // status line - writeln!( - Writer(buffer), - "{} {} {:?}\r", - msg.method(), - msg.uri() - .path_and_query() - .map(|u| u.as_str()) - .unwrap_or("/"), - msg.version() - ).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - - // write headers - if let Body::Binary(ref bytes) = *msg.body() { - buffer.reserve(msg.headers().len() * AVERAGE_HEADER_SIZE + bytes.len()); - } else { - buffer.reserve(msg.headers().len() * AVERAGE_HEADER_SIZE); - } - - for (key, value) in msg.headers() { - let v = value.as_ref(); - let k = key.as_str().as_bytes(); - buffer.reserve(k.len() + v.len() + 4); - buffer.put_slice(k); - buffer.put_slice(b": "); - buffer.put_slice(v); - buffer.put_slice(b"\r\n"); - } - - // set date header - if !msg.headers().contains_key(DATE) { - buffer.extend_from_slice(b"date: "); - set_date(buffer); - buffer.extend_from_slice(b"\r\n\r\n"); - } else { - buffer.extend_from_slice(b"\r\n"); - } - } - self.headers_size = self.buffer.len() as u32; - - if msg.body().is_binary() { - if let Body::Binary(bytes) = msg.replace_body(Body::Empty) { - self.written += bytes.len() as u64; - self.buffer.write(bytes.as_ref())?; - } - } else { - self.buffer_capacity = msg.write_buffer_capacity(); - } - Ok(()) - } - - pub fn write(&mut self, payload: &[u8]) -> io::Result { - self.written += payload.len() as u64; - if !self.flags.contains(Flags::DISCONNECTED) { - self.buffer.write(payload)?; - } - - if self.buffer.len() > self.buffer_capacity { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - pub fn write_eof(&mut self) -> io::Result<()> { - if self.buffer.write_eof()? { - Ok(()) - } else { - Err(io::Error::new( - io::ErrorKind::Other, - "Last payload item, but eof is not reached", - )) - } - } - - #[inline] - pub fn poll_completed( - &mut self, stream: &mut T, shutdown: bool, - ) -> Poll<(), io::Error> { - match self.write_to_stream(stream) { - Ok(WriterState::Done) => { - if shutdown { - stream.shutdown() - } else { - Ok(Async::Ready(())) - } - } - Ok(WriterState::Pause) => Ok(Async::NotReady), - Err(err) => Err(err), - } - } -} - -fn content_encoder(buf: BytesMut, req: &mut ClientRequest) -> Output { - let version = req.version(); - let mut body = req.replace_body(Body::Empty); - let mut encoding = req.content_encoding(); - - let transfer = match body { - Body::Empty => { - req.headers_mut().remove(CONTENT_LENGTH); - return Output::Empty(buf); - } - Body::Binary(ref mut bytes) => { - #[cfg(any(feature = "flate2", feature = "brotli"))] - { - if encoding.is_compression() { - let mut tmp = BytesMut::new(); - let mut transfer = TransferEncoding::eof(tmp); - let mut enc = match encoding { - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => ContentEncoder::Deflate( - ZlibEncoder::new(transfer, Compression::default()), - ), - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => ContentEncoder::Gzip(GzEncoder::new( - transfer, - Compression::default(), - )), - #[cfg(feature = "brotli")] - ContentEncoding::Br => { - ContentEncoder::Br(BrotliEncoder::new(transfer, 5)) - } - ContentEncoding::Auto | ContentEncoding::Identity => { - unreachable!() - } - }; - // TODO return error! - let _ = enc.write(bytes.as_ref()); - let _ = enc.write_eof(); - *bytes = Binary::from(enc.buf_mut().take()); - - req.headers_mut().insert( - CONTENT_ENCODING, - HeaderValue::from_static(encoding.as_str()), - ); - encoding = ContentEncoding::Identity; - } - let mut b = BytesMut::new(); - let _ = write!(b, "{}", bytes.len()); - req.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::try_from(b.freeze()).unwrap()); - TransferEncoding::eof(buf) - } - #[cfg(not(any(feature = "flate2", feature = "brotli")))] - { - let mut b = BytesMut::new(); - let _ = write!(b, "{}", bytes.len()); - req.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::try_from(b.freeze()).unwrap()); - TransferEncoding::eof(buf) - } - } - Body::Streaming(_) | Body::Actor(_) => { - if req.upgrade() { - if version == Version::HTTP_2 { - error!("Connection upgrade is forbidden for HTTP/2"); - } else { - req.headers_mut() - .insert(CONNECTION, HeaderValue::from_static("upgrade")); - } - if encoding != ContentEncoding::Identity { - encoding = ContentEncoding::Identity; - req.headers_mut().remove(CONTENT_ENCODING); - } - TransferEncoding::eof(buf) - } else { - streaming_encoding(buf, version, req) - } - } - }; - - if encoding.is_compression() { - req.headers_mut().insert( - CONTENT_ENCODING, - HeaderValue::from_static(encoding.as_str()), - ); - } - - req.replace_body(body); - let enc = match encoding { - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => { - ContentEncoder::Deflate(ZlibEncoder::new(transfer, Compression::default())) - } - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => { - ContentEncoder::Gzip(GzEncoder::new(transfer, Compression::default())) - } - #[cfg(feature = "brotli")] - ContentEncoding::Br => ContentEncoder::Br(BrotliEncoder::new(transfer, 5)), - ContentEncoding::Identity | ContentEncoding::Auto => return Output::TE(transfer), - }; - Output::Encoder(enc) -} - -fn streaming_encoding( - buf: BytesMut, version: Version, req: &mut ClientRequest, -) -> TransferEncoding { - if req.chunked() { - // Enable transfer encoding - req.headers_mut().remove(CONTENT_LENGTH); - if version == Version::HTTP_2 { - req.headers_mut().remove(TRANSFER_ENCODING); - TransferEncoding::eof(buf) - } else { - req.headers_mut() - .insert(TRANSFER_ENCODING, HeaderValue::from_static("chunked")); - TransferEncoding::chunked(buf) - } - } else { - // if Content-Length is specified, then use it as length hint - let (len, chunked) = if let Some(len) = req.headers().get(CONTENT_LENGTH) { - // Content-Length - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - (Some(len), false) - } else { - error!("illegal Content-Length: {:?}", len); - (None, false) - } - } else { - error!("illegal Content-Length: {:?}", len); - (None, false) - } - } else { - (None, true) - }; - - if !chunked { - if let Some(len) = len { - TransferEncoding::length(len, buf) - } else { - TransferEncoding::eof(buf) - } - } else { - // Enable transfer encoding - match version { - Version::HTTP_11 => { - req.headers_mut() - .insert(TRANSFER_ENCODING, HeaderValue::from_static("chunked")); - TransferEncoding::chunked(buf) - } - _ => { - req.headers_mut().remove(TRANSFER_ENCODING); - TransferEncoding::eof(buf) - } - } - } - } -} - -// "Sun, 06 Nov 1994 08:49:37 GMT".len() -pub const DATE_VALUE_LENGTH: usize = 29; - -fn set_date(dst: &mut BytesMut) { - CACHED.with(|cache| { - let mut cache = cache.borrow_mut(); - let now = time::get_time(); - if now > cache.next_update { - cache.update(now); - } - dst.extend_from_slice(cache.buffer()); - }) -} - -struct CachedDate { - bytes: [u8; DATE_VALUE_LENGTH], - next_update: time::Timespec, -} - -thread_local!(static CACHED: RefCell = RefCell::new(CachedDate { - bytes: [0; DATE_VALUE_LENGTH], - next_update: time::Timespec::new(0, 0), -})); - -impl CachedDate { - fn buffer(&self) -> &[u8] { - &self.bytes[..] - } - - fn update(&mut self, now: time::Timespec) { - write!(&mut self.bytes[..], "{}", time::at_utc(now).rfc822()).unwrap(); - self.next_update = now + Duration::seconds(1); - self.next_update.nsec = 0; - } -} diff --git a/src/context.rs b/src/context.rs deleted file mode 100644 index 71a5af2d8..000000000 --- a/src/context.rs +++ /dev/null @@ -1,294 +0,0 @@ -extern crate actix; - -use futures::sync::oneshot; -use futures::sync::oneshot::Sender; -use futures::{Async, Future, Poll}; -use smallvec::SmallVec; -use std::marker::PhantomData; - -use self::actix::dev::{ - AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope, -}; -use self::actix::fut::ActorFuture; -use self::actix::{ - Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message, SpawnHandle, -}; - -use body::{Binary, Body}; -use error::{Error, ErrorInternalServerError}; -use httprequest::HttpRequest; - -pub trait ActorHttpContext: 'static { - fn disconnected(&mut self); - fn poll(&mut self) -> Poll>, Error>; -} - -#[derive(Debug)] -pub enum Frame { - Chunk(Option), - Drain(oneshot::Sender<()>), -} - -impl Frame { - pub fn len(&self) -> usize { - match *self { - Frame::Chunk(Some(ref bin)) => bin.len(), - _ => 0, - } - } -} - -/// Execution context for http actors -pub struct HttpContext -where - A: Actor>, -{ - inner: ContextParts, - stream: Option>, - request: HttpRequest, - disconnected: bool, -} - -impl ActorContext for HttpContext -where - A: Actor, -{ - fn stop(&mut self) { - self.inner.stop(); - } - fn terminate(&mut self) { - self.inner.terminate() - } - fn state(&self) -> ActorState { - self.inner.state() - } -} - -impl AsyncContext for HttpContext -where - A: Actor, -{ - #[inline] - fn spawn(&mut self, fut: F) -> SpawnHandle - where - F: ActorFuture + 'static, - { - self.inner.spawn(fut) - } - #[inline] - fn wait(&mut self, fut: F) - where - F: ActorFuture + 'static, - { - self.inner.wait(fut) - } - #[doc(hidden)] - #[inline] - fn waiting(&self) -> bool { - self.inner.waiting() - || self.inner.state() == ActorState::Stopping - || self.inner.state() == ActorState::Stopped - } - #[inline] - fn cancel_future(&mut self, handle: SpawnHandle) -> bool { - self.inner.cancel_future(handle) - } - #[inline] - fn address(&self) -> Addr { - self.inner.address() - } -} - -impl HttpContext -where - A: Actor, -{ - #[inline] - /// Create a new HTTP Context from a request and an actor - pub fn create(req: HttpRequest, actor: A) -> Body { - let mb = Mailbox::default(); - let ctx = HttpContext { - inner: ContextParts::new(mb.sender_producer()), - stream: None, - request: req, - disconnected: false, - }; - Body::Actor(Box::new(HttpContextFut::new(ctx, actor, mb))) - } - - /// Create a new HTTP Context - pub fn with_factory(req: HttpRequest, f: F) -> Body - where - F: FnOnce(&mut Self) -> A + 'static, - { - let mb = Mailbox::default(); - let mut ctx = HttpContext { - inner: ContextParts::new(mb.sender_producer()), - stream: None, - request: req, - disconnected: false, - }; - - let act = f(&mut ctx); - Body::Actor(Box::new(HttpContextFut::new(ctx, act, mb))) - } -} - -impl HttpContext -where - A: Actor, -{ - /// Shared application state - #[inline] - pub fn state(&self) -> &S { - self.request.state() - } - - /// Incoming request - #[inline] - pub fn request(&mut self) -> &mut HttpRequest { - &mut self.request - } - - /// Write payload - #[inline] - pub fn write>(&mut self, data: B) { - if !self.disconnected { - self.add_frame(Frame::Chunk(Some(data.into()))); - } else { - warn!("Trying to write to disconnected response"); - } - } - - /// Indicate end of streaming payload. Also this method calls `Self::close`. - #[inline] - pub fn write_eof(&mut self) { - self.add_frame(Frame::Chunk(None)); - } - - /// Returns drain future - pub fn drain(&mut self) -> Drain { - let (tx, rx) = oneshot::channel(); - self.add_frame(Frame::Drain(tx)); - Drain::new(rx) - } - - /// Check if connection still open - #[inline] - pub fn connected(&self) -> bool { - !self.disconnected - } - - #[inline] - fn add_frame(&mut self, frame: Frame) { - if self.stream.is_none() { - self.stream = Some(SmallVec::new()); - } - if let Some(s) = self.stream.as_mut() { - s.push(frame) - } - } - - /// Handle of the running future - /// - /// SpawnHandle is the handle returned by `AsyncContext::spawn()` method. - pub fn handle(&self) -> SpawnHandle { - self.inner.curr_handle() - } -} - -impl AsyncContextParts for HttpContext -where - A: Actor, -{ - fn parts(&mut self) -> &mut ContextParts { - &mut self.inner - } -} - -struct HttpContextFut -where - A: Actor>, -{ - fut: ContextFut>, -} - -impl HttpContextFut -where - A: Actor>, -{ - fn new(ctx: HttpContext, act: A, mailbox: Mailbox) -> Self { - let fut = ContextFut::new(ctx, act, mailbox); - HttpContextFut { fut } - } -} - -impl ActorHttpContext for HttpContextFut -where - A: Actor>, - S: 'static, -{ - #[inline] - fn disconnected(&mut self) { - self.fut.ctx().disconnected = true; - self.fut.ctx().stop(); - } - - fn poll(&mut self) -> Poll>, Error> { - if self.fut.alive() { - match self.fut.poll() { - Ok(Async::NotReady) | Ok(Async::Ready(())) => (), - Err(_) => return Err(ErrorInternalServerError("error")), - } - } - - // frames - if let Some(data) = self.fut.ctx().stream.take() { - Ok(Async::Ready(Some(data))) - } else if self.fut.alive() { - Ok(Async::NotReady) - } else { - Ok(Async::Ready(None)) - } - } -} - -impl ToEnvelope for HttpContext -where - A: Actor> + Handler, - M: Message + Send + 'static, - M::Result: Send, -{ - fn pack(msg: M, tx: Option>) -> Envelope { - Envelope::new(msg, tx) - } -} - -/// Consume a future -pub struct Drain { - fut: oneshot::Receiver<()>, - _a: PhantomData, -} - -impl Drain { - /// Create a drain from a future - pub fn new(fut: oneshot::Receiver<()>) -> Self { - Drain { - fut, - _a: PhantomData, - } - } -} - -impl ActorFuture for Drain { - type Item = (); - type Error = (); - type Actor = A; - - #[inline] - fn poll( - &mut self, _: &mut A, _: &mut ::Context, - ) -> Poll { - self.fut.poll().map_err(|_| ()) - } -} diff --git a/src/de.rs b/src/de.rs deleted file mode 100644 index 59ab79ba9..000000000 --- a/src/de.rs +++ /dev/null @@ -1,443 +0,0 @@ -use serde::de::{self, Deserializer, Error as DeError, Visitor}; - -use httprequest::HttpRequest; -use param::ParamsIter; - -macro_rules! unsupported_type { - ($trait_fn:ident, $name:expr) => { - fn $trait_fn(self, _: V) -> Result - where V: Visitor<'de> - { - Err(de::value::Error::custom(concat!("unsupported type: ", $name))) - } - }; -} - -macro_rules! parse_single_value { - ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { - fn $trait_fn(self, visitor: V) -> Result - where V: Visitor<'de> - { - if self.req.match_info().len() != 1 { - Err(de::value::Error::custom( - format!("wrong number of parameters: {} expected 1", - self.req.match_info().len()).as_str())) - } else { - let v = self.req.match_info()[0].parse().map_err( - |_| de::value::Error::custom( - format!("can not parse {:?} to a {}", - &self.req.match_info()[0], $tp)))?; - visitor.$visit_fn(v) - } - } - } -} - -pub struct PathDeserializer<'de, S: 'de> { - req: &'de HttpRequest, -} - -impl<'de, S: 'de> PathDeserializer<'de, S> { - pub fn new(req: &'de HttpRequest) -> Self { - PathDeserializer { req } - } -} - -impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> { - type Error = de::value::Error; - - fn deserialize_map(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_map(ParamsDeserializer { - params: self.req.match_info().iter(), - current: None, - }) - } - - fn deserialize_struct( - self, _: &'static str, _: &'static [&'static str], visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - self.deserialize_map(visitor) - } - - fn deserialize_unit(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_unit_struct( - self, _: &'static str, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - self.deserialize_unit(visitor) - } - - fn deserialize_newtype_struct( - self, _: &'static str, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_newtype_struct(self) - } - - fn deserialize_tuple( - self, len: usize, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - if self.req.match_info().len() < len { - Err(de::value::Error::custom( - format!( - "wrong number of parameters: {} expected {}", - self.req.match_info().len(), - len - ).as_str(), - )) - } else { - visitor.visit_seq(ParamsSeq { - params: self.req.match_info().iter(), - }) - } - } - - fn deserialize_tuple_struct( - self, _: &'static str, len: usize, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - if self.req.match_info().len() < len { - Err(de::value::Error::custom( - format!( - "wrong number of parameters: {} expected {}", - self.req.match_info().len(), - len - ).as_str(), - )) - } else { - visitor.visit_seq(ParamsSeq { - params: self.req.match_info().iter(), - }) - } - } - - fn deserialize_enum( - self, _: &'static str, _: &'static [&'static str], _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: enum")) - } - - fn deserialize_str(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - if self.req.match_info().len() != 1 { - Err(de::value::Error::custom( - format!( - "wrong number of parameters: {} expected 1", - self.req.match_info().len() - ).as_str(), - )) - } else { - visitor.visit_str(&self.req.match_info()[0]) - } - } - - fn deserialize_seq(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_seq(ParamsSeq { - params: self.req.match_info().iter(), - }) - } - - unsupported_type!(deserialize_any, "'any'"); - unsupported_type!(deserialize_bytes, "bytes"); - unsupported_type!(deserialize_option, "Option"); - unsupported_type!(deserialize_identifier, "identifier"); - unsupported_type!(deserialize_ignored_any, "ignored_any"); - - parse_single_value!(deserialize_bool, visit_bool, "bool"); - parse_single_value!(deserialize_i8, visit_i8, "i8"); - parse_single_value!(deserialize_i16, visit_i16, "i16"); - parse_single_value!(deserialize_i32, visit_i32, "i32"); - parse_single_value!(deserialize_i64, visit_i64, "i64"); - parse_single_value!(deserialize_u8, visit_u8, "u8"); - parse_single_value!(deserialize_u16, visit_u16, "u16"); - parse_single_value!(deserialize_u32, visit_u32, "u32"); - parse_single_value!(deserialize_u64, visit_u64, "u64"); - parse_single_value!(deserialize_f32, visit_f32, "f32"); - parse_single_value!(deserialize_f64, visit_f64, "f64"); - parse_single_value!(deserialize_string, visit_string, "String"); - parse_single_value!(deserialize_byte_buf, visit_string, "String"); - parse_single_value!(deserialize_char, visit_char, "char"); -} - -struct ParamsDeserializer<'de> { - params: ParamsIter<'de>, - current: Option<(&'de str, &'de str)>, -} - -impl<'de> de::MapAccess<'de> for ParamsDeserializer<'de> { - type Error = de::value::Error; - - fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> - where - K: de::DeserializeSeed<'de>, - { - self.current = self.params.next().map(|ref item| (item.0, item.1)); - match self.current { - Some((key, _)) => Ok(Some(seed.deserialize(Key { key })?)), - None => Ok(None), - } - } - - fn next_value_seed(&mut self, seed: V) -> Result - where - V: de::DeserializeSeed<'de>, - { - if let Some((_, value)) = self.current.take() { - seed.deserialize(Value { value }) - } else { - Err(de::value::Error::custom("unexpected item")) - } - } -} - -struct Key<'de> { - key: &'de str, -} - -impl<'de> Deserializer<'de> for Key<'de> { - type Error = de::value::Error; - - fn deserialize_identifier(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_str(self.key) - } - - fn deserialize_any(self, _visitor: V) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("Unexpected")) - } - - forward_to_deserialize_any! { - bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes - byte_buf option unit unit_struct newtype_struct seq tuple - tuple_struct map struct enum ignored_any - } -} - -macro_rules! parse_value { - ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { - fn $trait_fn(self, visitor: V) -> Result - where V: Visitor<'de> - { - let v = self.value.parse().map_err( - |_| de::value::Error::custom( - format!("can not parse {:?} to a {}", self.value, $tp)))?; - visitor.$visit_fn(v) - } - } -} - -struct Value<'de> { - value: &'de str, -} - -impl<'de> Deserializer<'de> for Value<'de> { - type Error = de::value::Error; - - parse_value!(deserialize_bool, visit_bool, "bool"); - parse_value!(deserialize_i8, visit_i8, "i8"); - parse_value!(deserialize_i16, visit_i16, "i16"); - parse_value!(deserialize_i32, visit_i32, "i16"); - parse_value!(deserialize_i64, visit_i64, "i64"); - parse_value!(deserialize_u8, visit_u8, "u8"); - parse_value!(deserialize_u16, visit_u16, "u16"); - parse_value!(deserialize_u32, visit_u32, "u32"); - parse_value!(deserialize_u64, visit_u64, "u64"); - parse_value!(deserialize_f32, visit_f32, "f32"); - parse_value!(deserialize_f64, visit_f64, "f64"); - parse_value!(deserialize_string, visit_string, "String"); - parse_value!(deserialize_byte_buf, visit_string, "String"); - parse_value!(deserialize_char, visit_char, "char"); - - fn deserialize_ignored_any(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_unit(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_unit_struct( - self, _: &'static str, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_bytes(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_borrowed_bytes(self.value.as_bytes()) - } - - fn deserialize_str(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_borrowed_str(self.value) - } - - fn deserialize_option(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_some(self) - } - - fn deserialize_enum( - self, _: &'static str, _: &'static [&'static str], visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_enum(ValueEnum { value: self.value }) - } - - fn deserialize_newtype_struct( - self, _: &'static str, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_newtype_struct(self) - } - - fn deserialize_tuple(self, _: usize, _: V) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: tuple")) - } - - fn deserialize_struct( - self, _: &'static str, _: &'static [&'static str], _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: struct")) - } - - fn deserialize_tuple_struct( - self, _: &'static str, _: usize, _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: tuple struct")) - } - - unsupported_type!(deserialize_any, "any"); - unsupported_type!(deserialize_seq, "seq"); - unsupported_type!(deserialize_map, "map"); - unsupported_type!(deserialize_identifier, "identifier"); -} - -struct ParamsSeq<'de> { - params: ParamsIter<'de>, -} - -impl<'de> de::SeqAccess<'de> for ParamsSeq<'de> { - type Error = de::value::Error; - - fn next_element_seed(&mut self, seed: T) -> Result, Self::Error> - where - T: de::DeserializeSeed<'de>, - { - match self.params.next() { - Some(item) => Ok(Some(seed.deserialize(Value { value: item.1 })?)), - None => Ok(None), - } - } -} - -struct ValueEnum<'de> { - value: &'de str, -} - -impl<'de> de::EnumAccess<'de> for ValueEnum<'de> { - type Error = de::value::Error; - type Variant = UnitVariant; - - fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error> - where - V: de::DeserializeSeed<'de>, - { - Ok((seed.deserialize(Key { key: self.value })?, UnitVariant)) - } -} - -struct UnitVariant; - -impl<'de> de::VariantAccess<'de> for UnitVariant { - type Error = de::value::Error; - - fn unit_variant(self) -> Result<(), Self::Error> { - Ok(()) - } - - fn newtype_variant_seed(self, _seed: T) -> Result - where - T: de::DeserializeSeed<'de>, - { - Err(de::value::Error::custom("not supported")) - } - - fn tuple_variant(self, _len: usize, _visitor: V) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("not supported")) - } - - fn struct_variant( - self, _: &'static [&'static str], _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("not supported")) - } -} diff --git a/src/error.rs b/src/error.rs index 76c8e79ec..724803805 100644 --- a/src/error.rs +++ b/src/error.rs @@ -22,8 +22,7 @@ pub use url::ParseError as UrlParseError; // re-exports pub use cookie::ParseError as CookieParseError; -use handler::Responder; -use httprequest::HttpRequest; +// use httprequest::HttpRequest; use httpresponse::{HttpResponse, HttpResponseParts}; /// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html) @@ -727,18 +726,6 @@ where } } -impl Responder for InternalError -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, _: &HttpRequest) -> Result { - Err(self.into()) - } -} - /// Helper function that creates wrapper of any error and generate *BAD /// REQUEST* response. #[allow(non_snake_case)] diff --git a/src/extractor.rs b/src/extractor.rs deleted file mode 100644 index 7b0b4b003..000000000 --- a/src/extractor.rs +++ /dev/null @@ -1,1024 +0,0 @@ -use std::marker::PhantomData; -use std::ops::{Deref, DerefMut}; -use std::rc::Rc; -use std::{fmt, str}; - -use bytes::Bytes; -use encoding::all::UTF_8; -use encoding::types::{DecoderTrap, Encoding}; -use futures::{future, Async, Future, Poll}; -use mime::Mime; -use serde::de::{self, DeserializeOwned}; -use serde_urlencoded; - -use de::PathDeserializer; -use error::{Error, ErrorBadRequest, ErrorNotFound, UrlencodedError}; -use handler::{AsyncResult, FromRequest}; -use httpmessage::{HttpMessage, MessageBody, UrlEncoded}; -use httprequest::HttpRequest; - -#[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from the request's path. -/// -/// ## Example -/// -/// ```rust -/// # extern crate bytes; -/// # extern crate actix_web; -/// # extern crate futures; -/// use actix_web::{http, App, Path, Result}; -/// -/// /// extract path info from "/{username}/{count}/index.html" url -/// /// {username} - deserializes to a String -/// /// {count} - - deserializes to a u32 -/// fn index(info: Path<(String, u32)>) -> Result { -/// Ok(format!("Welcome {}! {}", info.0, info.1)) -/// } -/// -/// fn main() { -/// let app = App::new().resource( -/// "/{username}/{count}/index.html", // <- define path parameters -/// |r| r.method(http::Method::GET).with(index), -/// ); // <- use `with` extractor -/// } -/// ``` -/// -/// It is possible to extract path information to a specific type that -/// implements `Deserialize` trait from *serde*. -/// -/// ```rust -/// # extern crate bytes; -/// # extern crate actix_web; -/// # extern crate futures; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Path, Result}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// extract path info using serde -/// fn index(info: Path) -> Result { -/// Ok(format!("Welcome {}!", info.username)) -/// } -/// -/// fn main() { -/// let app = App::new().resource( -/// "/{username}/index.html", // <- define path parameters -/// |r| r.method(http::Method::GET).with(index), -/// ); // <- use `with` extractor -/// } -/// ``` -pub struct Path { - inner: T, -} - -impl AsRef for Path { - fn as_ref(&self) -> &T { - &self.inner - } -} - -impl Deref for Path { - type Target = T; - - fn deref(&self) -> &T { - &self.inner - } -} - -impl DerefMut for Path { - fn deref_mut(&mut self) -> &mut T { - &mut self.inner - } -} - -impl Path { - /// Deconstruct to an inner value - pub fn into_inner(self) -> T { - self.inner - } -} - -impl From for Path { - fn from(inner: T) -> Path { - Path { inner } - } -} - -impl FromRequest for Path -where - T: DeserializeOwned, -{ - type Config = (); - type Result = Result; - - #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { - let req = req.clone(); - de::Deserialize::deserialize(PathDeserializer::new(&req)) - .map_err(ErrorNotFound) - .map(|inner| Path { inner }) - } -} - -impl fmt::Debug for Path { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.inner.fmt(f) - } -} - -impl fmt::Display for Path { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.inner.fmt(f) - } -} - -#[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from from the request's query. -/// -/// ## Example -/// -/// ```rust -/// # extern crate bytes; -/// # extern crate actix_web; -/// # extern crate futures; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{App, Query, http}; -/// -/// -///#[derive(Debug, Deserialize)] -///pub enum ResponseType { -/// Token, -/// Code -///} -/// -///#[derive(Deserialize)] -///pub struct AuthRequest { -/// id: u64, -/// response_type: ResponseType, -///} -/// -/// // use `with` extractor for query info -/// // this handler get called only if request's query contains `username` field -/// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` -/// fn index(info: Query) -> String { -/// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) -/// } -/// -/// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| r.method(http::Method::GET).with(index)); // <- use `with` extractor -/// } -/// ``` -pub struct Query(T); - -impl Deref for Query { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Query { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl Query { - /// Deconstruct to a inner value - pub fn into_inner(self) -> T { - self.0 - } -} - -impl FromRequest for Query -where - T: de::DeserializeOwned, -{ - type Config = (); - type Result = Result; - - #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { - serde_urlencoded::from_str::(req.query_string()) - .map_err(|e| e.into()) - .map(Query) - } -} - -impl fmt::Debug for Query { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::Display for Query { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -#[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from the request's body. -/// -/// To extract typed information from request's body, the type `T` must -/// implement the `Deserialize` trait from *serde*. -/// -/// [**FormConfig**](dev/struct.FormConfig.html) allows to configure extraction -/// process. -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{App, Form, Result}; -/// -/// #[derive(Deserialize)] -/// struct FormData { -/// username: String, -/// } -/// -/// /// extract form data using serde -/// /// this handler get called only if content type is *x-www-form-urlencoded* -/// /// and content of the request could be deserialized to a `FormData` struct -/// fn index(form: Form) -> Result { -/// Ok(format!("Welcome {}!", form.username)) -/// } -/// # fn main() {} -/// ``` -pub struct Form(pub T); - -impl Form { - /// Deconstruct to an inner value - pub fn into_inner(self) -> T { - self.0 - } -} - -impl Deref for Form { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Form { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl FromRequest for Form -where - T: DeserializeOwned + 'static, - S: 'static, -{ - type Config = FormConfig; - type Result = Box>; - - #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - let req2 = req.clone(); - let err = Rc::clone(&cfg.ehandler); - Box::new( - UrlEncoded::new(req) - .limit(cfg.limit) - .map_err(move |e| (*err)(e, &req2)) - .map(Form), - ) - } -} - -impl fmt::Debug for Form { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::Display for Form { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -/// Form extractor configuration -/// -/// ```rust -/// # extern crate actix_web; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Form, Result}; -/// -/// #[derive(Deserialize)] -/// struct FormData { -/// username: String, -/// } -/// -/// /// extract form data using serde. -/// /// custom configuration is used for this handler, max payload size is 4k -/// fn index(form: Form) -> Result { -/// Ok(format!("Welcome {}!", form.username)) -/// } -/// -/// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| { -/// r.method(http::Method::GET) -/// // register form handler and change form extractor configuration -/// .with_config(index, |cfg| {cfg.0.limit(4096);}) -/// }, -/// ); -/// } -/// ``` -pub struct FormConfig { - limit: usize, - ehandler: Rc) -> Error>, -} - -impl FormConfig { - /// Change max size of payload. By default max size is 256Kb - pub fn limit(&mut self, limit: usize) -> &mut Self { - self.limit = limit; - self - } - - /// Set custom error handler - pub fn error_handler(&mut self, f: F) -> &mut Self - where - F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static, - { - self.ehandler = Rc::new(f); - self - } -} - -impl Default for FormConfig { - fn default() -> Self { - FormConfig { - limit: 262_144, - ehandler: Rc::new(|e, _| e.into()), - } - } -} - -/// Request payload extractor. -/// -/// Loads request's payload and construct Bytes instance. -/// -/// [**PayloadConfig**](dev/struct.PayloadConfig.html) allows to configure -/// extraction process. -/// -/// ## Example -/// -/// ```rust -/// extern crate bytes; -/// # extern crate actix_web; -/// use actix_web::{http, App, Result}; -/// -/// /// extract text data from request -/// fn index(body: bytes::Bytes) -> Result { -/// Ok(format!("Body {:?}!", body)) -/// } -/// -/// fn main() { -/// let app = App::new() -/// .resource("/index.html", |r| r.method(http::Method::GET).with(index)); -/// } -/// ``` -impl FromRequest for Bytes { - type Config = PayloadConfig; - type Result = Result>, Error>; - - #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - // check content-type - cfg.check_mimetype(req)?; - - Ok(Box::new(MessageBody::new(req).limit(cfg.limit).from_err())) - } -} - -/// Extract text information from the request's body. -/// -/// Text extractor automatically decode body according to the request's charset. -/// -/// [**PayloadConfig**](dev/struct.PayloadConfig.html) allows to configure -/// extraction process. -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{http, App, Result}; -/// -/// /// extract text data from request -/// fn index(body: String) -> Result { -/// Ok(format!("Body {}!", body)) -/// } -/// -/// fn main() { -/// let app = App::new().resource("/index.html", |r| { -/// r.method(http::Method::GET) -/// .with_config(index, |cfg| { // <- register handler with extractor params -/// cfg.0.limit(4096); // <- limit size of the payload -/// }) -/// }); -/// } -/// ``` -impl FromRequest for String { - type Config = PayloadConfig; - type Result = Result>, Error>; - - #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - // check content-type - cfg.check_mimetype(req)?; - - // check charset - let encoding = req.encoding()?; - - Ok(Box::new( - MessageBody::new(req) - .limit(cfg.limit) - .from_err() - .and_then(move |body| { - let enc: *const Encoding = encoding as *const Encoding; - if enc == UTF_8 { - Ok(str::from_utf8(body.as_ref()) - .map_err(|_| ErrorBadRequest("Can not decode body"))? - .to_owned()) - } else { - Ok(encoding - .decode(&body, DecoderTrap::Strict) - .map_err(|_| ErrorBadRequest("Can not decode body"))?) - } - }), - )) - } -} - -/// Optionally extract a field from the request -/// -/// If the FromRequest for T fails, return None rather than returning an error response -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// extern crate rand; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Result, HttpRequest, Error, FromRequest}; -/// use actix_web::error::ErrorBadRequest; -/// -/// #[derive(Debug, Deserialize)] -/// struct Thing { name: String } -/// -/// impl FromRequest for Thing { -/// type Config = (); -/// type Result = Result; -/// -/// #[inline] -/// fn from_request(req: &HttpRequest, _cfg: &Self::Config) -> Self::Result { -/// if rand::random() { -/// Ok(Thing { name: "thingy".into() }) -/// } else { -/// Err(ErrorBadRequest("no luck")) -/// } -/// -/// } -/// } -/// -/// /// extract text data from request -/// fn index(supplied_thing: Option) -> Result { -/// match supplied_thing { -/// // Puns not intended -/// Some(thing) => Ok(format!("Got something: {:?}", thing)), -/// None => Ok(format!("No thing!")) -/// } -/// } -/// -/// fn main() { -/// let app = App::new().resource("/users/:first", |r| { -/// r.method(http::Method::POST).with(index) -/// }); -/// } -/// ``` -impl FromRequest for Option -where - T: FromRequest, -{ - type Config = T::Config; - type Result = Box, Error = Error>>; - - #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - Box::new(T::from_request(req, cfg).into().then(|r| match r { - Ok(v) => future::ok(Some(v)), - Err(_) => future::ok(None), - })) - } -} - -/// Optionally extract a field from the request or extract the Error if unsuccessful -/// -/// If the FromRequest for T fails, inject Err into handler rather than returning an error response -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// extern crate rand; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Result, HttpRequest, Error, FromRequest}; -/// use actix_web::error::ErrorBadRequest; -/// -/// #[derive(Debug, Deserialize)] -/// struct Thing { name: String } -/// -/// impl FromRequest for Thing { -/// type Config = (); -/// type Result = Result; -/// -/// #[inline] -/// fn from_request(req: &HttpRequest, _cfg: &Self::Config) -> Self::Result { -/// if rand::random() { -/// Ok(Thing { name: "thingy".into() }) -/// } else { -/// Err(ErrorBadRequest("no luck")) -/// } -/// -/// } -/// } -/// -/// /// extract text data from request -/// fn index(supplied_thing: Result) -> Result { -/// match supplied_thing { -/// Ok(thing) => Ok(format!("Got thing: {:?}", thing)), -/// Err(e) => Ok(format!("Error extracting thing: {}", e)) -/// } -/// } -/// -/// fn main() { -/// let app = App::new().resource("/users/:first", |r| { -/// r.method(http::Method::POST).with(index) -/// }); -/// } -/// ``` -impl FromRequest for Result -where - T: FromRequest, -{ - type Config = T::Config; - type Result = Box, Error = Error>>; - - #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - Box::new(T::from_request(req, cfg).into().then(future::ok)) - } -} - -/// Payload configuration for request's payload. -pub struct PayloadConfig { - limit: usize, - mimetype: Option, -} - -impl PayloadConfig { - /// Change max size of payload. By default max size is 256Kb - pub fn limit(&mut self, limit: usize) -> &mut Self { - self.limit = limit; - self - } - - /// Set required mime-type of the request. By default mime type is not - /// enforced. - pub fn mimetype(&mut self, mt: Mime) -> &mut Self { - self.mimetype = Some(mt); - self - } - - fn check_mimetype(&self, req: &HttpRequest) -> Result<(), Error> { - // check content-type - if let Some(ref mt) = self.mimetype { - match req.mime_type() { - Ok(Some(ref req_mt)) => { - if mt != req_mt { - return Err(ErrorBadRequest("Unexpected Content-Type")); - } - } - Ok(None) => { - return Err(ErrorBadRequest("Content-Type is expected")); - } - Err(err) => { - return Err(err.into()); - } - } - } - Ok(()) - } -} - -impl Default for PayloadConfig { - fn default() -> Self { - PayloadConfig { - limit: 262_144, - mimetype: None, - } - } -} - -macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { - - /// FromRequest implementation for tuple - impl + 'static),+> FromRequest for ($($T,)+) - where - S: 'static, - { - type Config = ($($T::Config,)+); - type Result = Box>; - - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - Box::new($fut_type { - s: PhantomData, - items: <($(Option<$T>,)+)>::default(), - futs: ($(Some($T::from_request(req, &cfg.$n).into()),)+), - }) - } - } - - struct $fut_type),+> - where - S: 'static, - { - s: PhantomData, - items: ($(Option<$T>,)+), - futs: ($(Option>,)+), - } - - impl),+> Future for $fut_type - where - S: 'static, - { - type Item = ($($T,)+); - type Error = Error; - - fn poll(&mut self) -> Poll { - let mut ready = true; - - $( - if self.futs.$n.is_some() { - match self.futs.$n.as_mut().unwrap().poll() { - Ok(Async::Ready(item)) => { - self.items.$n = Some(item); - self.futs.$n.take(); - } - Ok(Async::NotReady) => ready = false, - Err(e) => return Err(e), - } - } - )+ - - if ready { - Ok(Async::Ready( - ($(self.items.$n.take().unwrap(),)+) - )) - } else { - Ok(Async::NotReady) - } - } - } -}); - -impl FromRequest for () { - type Config = (); - type Result = Self; - fn from_request(_req: &HttpRequest, _cfg: &Self::Config) -> Self::Result {} -} - -tuple_from_req!(TupleFromRequest1, (0, A)); -tuple_from_req!(TupleFromRequest2, (0, A), (1, B)); -tuple_from_req!(TupleFromRequest3, (0, A), (1, B), (2, C)); -tuple_from_req!(TupleFromRequest4, (0, A), (1, B), (2, C), (3, D)); -tuple_from_req!(TupleFromRequest5, (0, A), (1, B), (2, C), (3, D), (4, E)); -tuple_from_req!( - TupleFromRequest6, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F) -); -tuple_from_req!( - TupleFromRequest7, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G) -); -tuple_from_req!( - TupleFromRequest8, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H) -); -tuple_from_req!( - TupleFromRequest9, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H), - (8, I) -); - -#[cfg(test)] -mod tests { - use super::*; - use bytes::Bytes; - use futures::{Async, Future}; - use http::header; - use mime; - use resource::Resource; - use router::{ResourceDef, Router}; - use test::TestRequest; - - #[derive(Deserialize, Debug, PartialEq)] - struct Info { - hello: String, - } - - #[test] - fn test_bytes() { - let cfg = PayloadConfig::default(); - let req = TestRequest::with_header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); - - match Bytes::from_request(&req, &cfg).unwrap().poll().unwrap() { - Async::Ready(s) => { - assert_eq!(s, Bytes::from_static(b"hello=world")); - } - _ => unreachable!(), - } - } - - #[test] - fn test_string() { - let cfg = PayloadConfig::default(); - let req = TestRequest::with_header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); - - match String::from_request(&req, &cfg).unwrap().poll().unwrap() { - Async::Ready(s) => { - assert_eq!(s, "hello=world"); - } - _ => unreachable!(), - } - } - - #[test] - fn test_form() { - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); - - let mut cfg = FormConfig::default(); - cfg.limit(4096); - match Form::::from_request(&req, &cfg).poll().unwrap() { - Async::Ready(s) => { - assert_eq!(s.hello, "world"); - } - _ => unreachable!(), - } - } - - #[test] - fn test_option() { - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).finish(); - - let mut cfg = FormConfig::default(); - cfg.limit(4096); - - match Option::>::from_request(&req, &cfg) - .poll() - .unwrap() - { - Async::Ready(r) => assert_eq!(r, None), - _ => unreachable!(), - } - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "9") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); - - match Option::>::from_request(&req, &cfg) - .poll() - .unwrap() - { - Async::Ready(r) => assert_eq!( - r, - Some(Form(Info { - hello: "world".into() - })) - ), - _ => unreachable!(), - } - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "9") - .set_payload(Bytes::from_static(b"bye=world")) - .finish(); - - match Option::>::from_request(&req, &cfg) - .poll() - .unwrap() - { - Async::Ready(r) => assert_eq!(r, None), - _ => unreachable!(), - } - } - - #[test] - fn test_result() { - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); - - match Result::, Error>::from_request(&req, &FormConfig::default()) - .poll() - .unwrap() - { - Async::Ready(Ok(r)) => assert_eq!( - r, - Form(Info { - hello: "world".into() - }) - ), - _ => unreachable!(), - } - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "9") - .set_payload(Bytes::from_static(b"bye=world")) - .finish(); - - match Result::, Error>::from_request(&req, &FormConfig::default()) - .poll() - .unwrap() - { - Async::Ready(r) => assert!(r.is_err()), - _ => unreachable!(), - } - } - - #[test] - fn test_payload_config() { - let req = TestRequest::default().finish(); - let mut cfg = PayloadConfig::default(); - cfg.mimetype(mime::APPLICATION_JSON); - assert!(cfg.check_mimetype(&req).is_err()); - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).finish(); - assert!(cfg.check_mimetype(&req).is_err()); - - let req = - TestRequest::with_header(header::CONTENT_TYPE, "application/json").finish(); - assert!(cfg.check_mimetype(&req).is_ok()); - } - - #[derive(Deserialize)] - struct MyStruct { - key: String, - value: String, - } - - #[derive(Deserialize)] - struct Id { - id: String, - } - - #[derive(Deserialize)] - struct Test2 { - key: String, - value: u32, - } - - #[test] - fn test_request_extract() { - let req = TestRequest::with_uri("/name/user1/?id=test").finish(); - - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - - let s = Path::::from_request(&req, &()).unwrap(); - assert_eq!(s.key, "name"); - assert_eq!(s.value, "user1"); - - let s = Path::<(String, String)>::from_request(&req, &()).unwrap(); - assert_eq!(s.0, "name"); - assert_eq!(s.1, "user1"); - - let s = Query::::from_request(&req, &()).unwrap(); - assert_eq!(s.id, "test"); - - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - let req = TestRequest::with_uri("/name/32/").finish(); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - - let s = Path::::from_request(&req, &()).unwrap(); - assert_eq!(s.as_ref().key, "name"); - assert_eq!(s.value, 32); - - let s = Path::<(String, u8)>::from_request(&req, &()).unwrap(); - assert_eq!(s.0, "name"); - assert_eq!(s.1, 32); - - let res = Path::>::extract(&req).unwrap(); - assert_eq!(res[0], "name".to_owned()); - assert_eq!(res[1], "32".to_owned()); - } - - #[test] - fn test_extract_path_single() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); - - let req = TestRequest::with_uri("/32/").finish(); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - assert_eq!(*Path::::from_request(&req, &()).unwrap(), 32); - } - - #[test] - fn test_tuple_extract() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - - let req = TestRequest::with_uri("/name/user1/?id=test").finish(); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - - let res = match <(Path<(String, String)>,)>::extract(&req).poll() { - Ok(Async::Ready(res)) => res, - _ => panic!("error"), - }; - assert_eq!((res.0).0, "name"); - assert_eq!((res.0).1, "user1"); - - let res = match <(Path<(String, String)>, Path<(String, String)>)>::extract(&req) - .poll() - { - Ok(Async::Ready(res)) => res, - _ => panic!("error"), - }; - assert_eq!((res.0).0, "name"); - assert_eq!((res.0).1, "user1"); - assert_eq!((res.1).0, "name"); - assert_eq!((res.1).1, "user1"); - - let () = <()>::extract(&req); - } -} diff --git a/src/fs.rs b/src/fs.rs deleted file mode 100644 index 10cdaff7b..000000000 --- a/src/fs.rs +++ /dev/null @@ -1,1786 +0,0 @@ -//! Static files support -use std::fmt::Write; -use std::fs::{DirEntry, File, Metadata}; -use std::io::{Read, Seek}; -use std::marker::PhantomData; -use std::ops::{Deref, DerefMut}; -use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; -use std::{cmp, io}; - -#[cfg(unix)] -use std::os::unix::fs::MetadataExt; - -use bytes::Bytes; -use futures::{Async, Future, Poll, Stream}; -use futures_cpupool::{CpuFuture, CpuPool}; -use htmlescape::encode_minimal as escape_html_entity; -use mime; -use mime_guess::{get_mime_type, guess_mime_type}; -use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; - -use error::{Error, StaticFileError}; -use handler::{AsyncResult, Handler, Responder, RouteHandler, WrapHandler}; -use header; -use header::{ContentDisposition, DispositionParam, DispositionType}; -use http::{ContentEncoding, Method, StatusCode}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use param::FromParam; -use server::settings::DEFAULT_CPUPOOL; - -///Describes `StaticFiles` configiration -/// -///To configure actix's static resources you need -///to define own configiration type and implement any method -///you wish to customize. -///As trait implements reasonable defaults for Actix. -/// -///## Example -/// -///```rust -/// extern crate mime; -/// extern crate actix_web; -/// use actix_web::http::header::DispositionType; -/// use actix_web::fs::{StaticFileConfig, NamedFile}; -/// -/// #[derive(Default)] -/// struct MyConfig; -/// -/// impl StaticFileConfig for MyConfig { -/// fn content_disposition_map(typ: mime::Name) -> DispositionType { -/// DispositionType::Attachment -/// } -/// } -/// -/// let file = NamedFile::open_with_config("foo.txt", MyConfig); -///``` -pub trait StaticFileConfig: Default { - ///Describes mapping for mime type to content disposition header - /// - ///By default `IMAGE`, `TEXT` and `VIDEO` are mapped to Inline. - ///Others are mapped to Attachment - fn content_disposition_map(typ: mime::Name) -> DispositionType { - match typ { - mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, - _ => DispositionType::Attachment, - } - } - - ///Describes whether Actix should attempt to calculate `ETag` - /// - ///Defaults to `true` - fn is_use_etag() -> bool { - true - } - - ///Describes whether Actix should use last modified date of file. - /// - ///Defaults to `true` - fn is_use_last_modifier() -> bool { - true - } - - ///Describes allowed methods to access static resources. - /// - ///By default all methods are allowed - fn is_method_allowed(_method: &Method) -> bool { - true - } -} - -///Default content disposition as described in -///[StaticFileConfig](trait.StaticFileConfig.html) -#[derive(Default)] -pub struct DefaultConfig; -impl StaticFileConfig for DefaultConfig {} - -/// Return the MIME type associated with a filename extension (case-insensitive). -/// If `ext` is empty or no associated type for the extension was found, returns -/// the type `application/octet-stream`. -#[inline] -pub fn file_extension_to_mime(ext: &str) -> mime::Mime { - get_mime_type(ext) -} - -/// A file with an associated name. -#[derive(Debug)] -pub struct NamedFile { - path: PathBuf, - file: File, - content_type: mime::Mime, - content_disposition: header::ContentDisposition, - md: Metadata, - modified: Option, - cpu_pool: Option, - encoding: Option, - status_code: StatusCode, - _cd_map: PhantomData, -} - -impl NamedFile { - /// Attempts to open a file in read-only mode. - /// - /// # Examples - /// - /// ```rust - /// use actix_web::fs::NamedFile; - /// - /// let file = NamedFile::open("foo.txt"); - /// ``` - pub fn open>(path: P) -> io::Result { - Self::open_with_config(path, DefaultConfig) - } -} - -impl NamedFile { - /// Attempts to open a file in read-only mode using provided configiration. - /// - /// # Examples - /// - /// ```rust - /// use actix_web::fs::{DefaultConfig, NamedFile}; - /// - /// let file = NamedFile::open_with_config("foo.txt", DefaultConfig); - /// ``` - pub fn open_with_config>(path: P, _: C) -> io::Result> { - let path = path.as_ref().to_path_buf(); - - // Get the name of the file and use it to construct default Content-Type - // and Content-Disposition values - let (content_type, content_disposition) = { - let filename = match path.file_name() { - Some(name) => name.to_string_lossy(), - None => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Provided path has no filename", - )) - } - }; - - let ct = guess_mime_type(&path); - let disposition_type = C::content_disposition_map(ct.type_()); - let cd = ContentDisposition { - disposition: disposition_type, - parameters: vec![DispositionParam::Filename(filename.into_owned())], - }; - (ct, cd) - }; - - let file = File::open(&path)?; - let md = file.metadata()?; - let modified = md.modified().ok(); - let cpu_pool = None; - let encoding = None; - Ok(NamedFile { - path, - file, - content_type, - content_disposition, - md, - modified, - cpu_pool, - encoding, - status_code: StatusCode::OK, - _cd_map: PhantomData, - }) - } - - /// Returns reference to the underlying `File` object. - #[inline] - pub fn file(&self) -> &File { - &self.file - } - - /// Retrieve the path of this file. - /// - /// # Examples - /// - /// ```rust - /// # use std::io; - /// use actix_web::fs::NamedFile; - /// - /// # fn path() -> io::Result<()> { - /// let file = NamedFile::open("test.txt")?; - /// assert_eq!(file.path().as_os_str(), "foo.txt"); - /// # Ok(()) - /// # } - /// ``` - #[inline] - pub fn path(&self) -> &Path { - self.path.as_path() - } - - /// Set `CpuPool` to use - #[inline] - pub fn set_cpu_pool(mut self, cpu_pool: CpuPool) -> Self { - self.cpu_pool = Some(cpu_pool); - self - } - - /// Set response **Status Code** - pub fn set_status_code(mut self, status: StatusCode) -> Self { - self.status_code = status; - self - } - - /// Set the MIME Content-Type for serving this file. By default - /// the Content-Type is inferred from the filename extension. - #[inline] - pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self { - self.content_type = mime_type; - self - } - - /// Set the Content-Disposition for serving this file. This allows - /// changing the inline/attachment disposition as well as the filename - /// sent to the peer. By default the disposition is `inline` for text, - /// image, and video content types, and `attachment` otherwise, and - /// the filename is taken from the path provided in the `open` method - /// after converting it to UTF-8 using - /// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy). - #[inline] - pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self { - self.content_disposition = cd; - self - } - - /// Set content encoding for serving this file - #[inline] - pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { - self.encoding = Some(enc); - self - } - - fn etag(&self) -> Option { - // This etag format is similar to Apache's. - self.modified.as_ref().map(|mtime| { - let ino = { - #[cfg(unix)] - { - self.md.ino() - } - #[cfg(not(unix))] - { - 0 - } - }; - - let dur = mtime - .duration_since(UNIX_EPOCH) - .expect("modification time must be after epoch"); - header::EntityTag::strong(format!( - "{:x}:{:x}:{:x}:{:x}", - ino, - self.md.len(), - dur.as_secs(), - dur.subsec_nanos() - )) - }) - } - - fn last_modified(&self) -> Option { - self.modified.map(|mtime| mtime.into()) - } -} - -impl Deref for NamedFile { - type Target = File; - - fn deref(&self) -> &File { - &self.file - } -} - -impl DerefMut for NamedFile { - fn deref_mut(&mut self) -> &mut File { - &mut self.file - } -} - -/// Returns true if `req` has no `If-Match` header or one which matches `etag`. -fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { - match req.get_header::() { - None | Some(header::IfMatch::Any) => true, - Some(header::IfMatch::Items(ref items)) => { - if let Some(some_etag) = etag { - for item in items { - if item.strong_eq(some_etag) { - return true; - } - } - } - false - } - } -} - -/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`. -fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { - match req.get_header::() { - Some(header::IfNoneMatch::Any) => false, - Some(header::IfNoneMatch::Items(ref items)) => { - if let Some(some_etag) = etag { - for item in items { - if item.weak_eq(some_etag) { - return false; - } - } - } - true - } - None => true, - } -} - -impl Responder for NamedFile { - type Item = HttpResponse; - type Error = io::Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - if self.status_code != StatusCode::OK { - let mut resp = HttpResponse::build(self.status_code); - resp.set(header::ContentType(self.content_type.clone())) - .header( - header::CONTENT_DISPOSITION, - self.content_disposition.to_string(), - ); - - if let Some(current_encoding) = self.encoding { - resp.content_encoding(current_encoding); - } - let reader = ChunkedReadFile { - size: self.md.len(), - offset: 0, - cpu_pool: self.cpu_pool.unwrap_or_else(|| req.cpu_pool().clone()), - file: Some(self.file), - fut: None, - counter: 0, - }; - return Ok(resp.streaming(reader)); - } - - if !C::is_method_allowed(req.method()) { - return Ok(HttpResponse::MethodNotAllowed() - .header(header::CONTENT_TYPE, "text/plain") - .header(header::ALLOW, "GET, HEAD") - .body("This resource only supports GET and HEAD.")); - } - - let etag = if C::is_use_etag() { self.etag() } else { None }; - let last_modified = if C::is_use_last_modifier() { - self.last_modified() - } else { - None - }; - - // check preconditions - let precondition_failed = if !any_match(etag.as_ref(), req) { - true - } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = - (last_modified, req.get_header()) - { - m > since - } else { - false - }; - - // check last modified - let not_modified = if !none_match(etag.as_ref(), req) { - true - } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = - (last_modified, req.get_header()) - { - m <= since - } else { - false - }; - - let mut resp = HttpResponse::build(self.status_code); - resp.set(header::ContentType(self.content_type.clone())) - .header( - header::CONTENT_DISPOSITION, - self.content_disposition.to_string(), - ); - - if let Some(current_encoding) = self.encoding { - resp.content_encoding(current_encoding); - } - - resp.if_some(last_modified, |lm, resp| { - resp.set(header::LastModified(lm)); - }).if_some(etag, |etag, resp| { - resp.set(header::ETag(etag)); - }); - - resp.header(header::ACCEPT_RANGES, "bytes"); - - let mut length = self.md.len(); - let mut offset = 0; - - // check for range header - if let Some(ranges) = req.headers().get(header::RANGE) { - if let Ok(rangesheader) = ranges.to_str() { - if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) { - length = rangesvec[0].length; - offset = rangesvec[0].start; - resp.content_encoding(ContentEncoding::Identity); - resp.header( - header::CONTENT_RANGE, - format!( - "bytes {}-{}/{}", - offset, - offset + length - 1, - self.md.len() - ), - ); - } else { - resp.header(header::CONTENT_RANGE, format!("bytes */{}", length)); - return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish()); - }; - } else { - return Ok(resp.status(StatusCode::BAD_REQUEST).finish()); - }; - }; - - resp.header(header::CONTENT_LENGTH, format!("{}", length)); - - if precondition_failed { - return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish()); - } else if not_modified { - return Ok(resp.status(StatusCode::NOT_MODIFIED).finish()); - } - - if *req.method() == Method::HEAD { - Ok(resp.finish()) - } else { - let reader = ChunkedReadFile { - offset, - size: length, - cpu_pool: self.cpu_pool.unwrap_or_else(|| req.cpu_pool().clone()), - file: Some(self.file), - fut: None, - counter: 0, - }; - if offset != 0 || length != self.md.len() { - return Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader)); - }; - Ok(resp.streaming(reader)) - } - } -} - -/// A helper created from a `std::fs::File` which reads the file -/// chunk-by-chunk on a `CpuPool`. -pub struct ChunkedReadFile { - size: u64, - offset: u64, - cpu_pool: CpuPool, - file: Option, - fut: Option>, - counter: u64, -} - -impl Stream for ChunkedReadFile { - type Item = Bytes; - type Error = Error; - - fn poll(&mut self) -> Poll, Error> { - if self.fut.is_some() { - return match self.fut.as_mut().unwrap().poll()? { - Async::Ready((file, bytes)) => { - self.fut.take(); - self.file = Some(file); - self.offset += bytes.len() as u64; - self.counter += bytes.len() as u64; - Ok(Async::Ready(Some(bytes))) - } - Async::NotReady => Ok(Async::NotReady), - }; - } - - let size = self.size; - let offset = self.offset; - let counter = self.counter; - - if size == counter { - Ok(Async::Ready(None)) - } else { - let mut file = self.file.take().expect("Use after completion"); - self.fut = Some(self.cpu_pool.spawn_fn(move || { - let max_bytes: usize; - max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; - let mut buf = Vec::with_capacity(max_bytes); - file.seek(io::SeekFrom::Start(offset))?; - let nbytes = - file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; - if nbytes == 0 { - return Err(io::ErrorKind::UnexpectedEof.into()); - } - Ok((file, Bytes::from(buf))) - })); - self.poll() - } - } -} - -type DirectoryRenderer = - Fn(&Directory, &HttpRequest) -> Result; - -/// A directory; responds with the generated directory listing. -#[derive(Debug)] -pub struct Directory { - /// Base directory - pub base: PathBuf, - /// Path of subdirectory to generate listing for - pub path: PathBuf, -} - -impl Directory { - /// Create a new directory - pub fn new(base: PathBuf, path: PathBuf) -> Directory { - Directory { base, path } - } - - /// Is this entry visible from this directory? - pub fn is_visible(&self, entry: &io::Result) -> bool { - if let Ok(ref entry) = *entry { - if let Some(name) = entry.file_name().to_str() { - if name.starts_with('.') { - return false; - } - } - if let Ok(ref md) = entry.metadata() { - let ft = md.file_type(); - return ft.is_dir() || ft.is_file() || ft.is_symlink(); - } - } - false - } -} - -fn directory_listing( - dir: &Directory, req: &HttpRequest, -) -> Result { - let index_of = format!("Index of {}", req.path()); - let mut body = String::new(); - let base = Path::new(req.path()); - - for entry in dir.path.read_dir()? { - if dir.is_visible(&entry) { - let entry = entry.unwrap(); - let p = match entry.path().strip_prefix(&dir.path) { - Ok(p) => base.join(p), - Err(_) => continue, - }; - // show file url as relative to static path - let file_url = utf8_percent_encode(&p.to_string_lossy(), DEFAULT_ENCODE_SET) - .to_string(); - // " -- " & -- & ' -- ' < -- < > -- > - let file_name = escape_html_entity(&entry.file_name().to_string_lossy()); - - // if file is a directory, add '/' to the end of the name - if let Ok(metadata) = entry.metadata() { - if metadata.is_dir() { - let _ = write!( - body, - "
  • {}/
  • ", - file_url, file_name - ); - } else { - let _ = write!( - body, - "
  • {}
  • ", - file_url, file_name - ); - } - } else { - continue; - } - } - } - - let html = format!( - "\ - {}\ -

    {}

    \ -
      \ - {}\ -
    \n", - index_of, index_of, body - ); - Ok(HttpResponse::Ok() - .content_type("text/html; charset=utf-8") - .body(html)) -} - -/// Static files handling -/// -/// `StaticFile` handler must be registered with `App::handler()` method, -/// because `StaticFile` handler requires access sub-path information. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{fs, App}; -/// -/// fn main() { -/// let app = App::new() -/// .handler("/static", fs::StaticFiles::new(".").unwrap()) -/// .finish(); -/// } -/// ``` -pub struct StaticFiles { - directory: PathBuf, - index: Option, - show_index: bool, - cpu_pool: CpuPool, - default: Box>, - renderer: Box>, - _chunk_size: usize, - _follow_symlinks: bool, - _cd_map: PhantomData, -} - -impl StaticFiles { - /// Create new `StaticFiles` instance for specified base directory. - /// - /// `StaticFile` uses `CpuPool` for blocking filesystem operations. - /// By default pool with 20 threads is used. - /// Pool size can be changed by setting ACTIX_CPU_POOL environment variable. - pub fn new>(dir: T) -> Result, Error> { - Self::with_config(dir, DefaultConfig) - } - - /// Create new `StaticFiles` instance for specified base directory and - /// `CpuPool`. - pub fn with_pool>( - dir: T, pool: CpuPool, - ) -> Result, Error> { - Self::with_config_pool(dir, pool, DefaultConfig) - } -} - -impl StaticFiles { - /// Create new `StaticFiles` instance for specified base directory. - /// - /// Identical with `new` but allows to specify configiration to use. - pub fn with_config>( - dir: T, config: C, - ) -> Result, Error> { - // use default CpuPool - let pool = { DEFAULT_CPUPOOL.lock().clone() }; - - StaticFiles::with_config_pool(dir, pool, config) - } - - /// Create new `StaticFiles` instance for specified base directory with config and - /// `CpuPool`. - pub fn with_config_pool>( - dir: T, pool: CpuPool, _: C, - ) -> Result, Error> { - let dir = dir.into().canonicalize()?; - - if !dir.is_dir() { - return Err(StaticFileError::IsNotDirectory.into()); - } - - Ok(StaticFiles { - directory: dir, - index: None, - show_index: false, - cpu_pool: pool, - default: Box::new(WrapHandler::new(|_: &_| { - HttpResponse::new(StatusCode::NOT_FOUND) - })), - renderer: Box::new(directory_listing), - _chunk_size: 0, - _follow_symlinks: false, - _cd_map: PhantomData, - }) - } - - /// Show files listing for directories. - /// - /// By default show files listing is disabled. - pub fn show_files_listing(mut self) -> Self { - self.show_index = true; - self - } - - /// Set custom directory renderer - pub fn files_listing_renderer(mut self, f: F) -> Self - where - for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) - -> Result - + 'static, - { - self.renderer = Box::new(f); - self - } - - /// Set index file - /// - /// Redirects to specific index file for directory "/" instead of - /// showing files listing. - pub fn index_file>(mut self, index: T) -> StaticFiles { - self.index = Some(index.into()); - self - } - - /// Sets default handler which is used when no matched file could be found. - pub fn default_handler>(mut self, handler: H) -> StaticFiles { - self.default = Box::new(WrapHandler::new(handler)); - self - } - - fn try_handle( - &self, req: &HttpRequest, - ) -> Result, Error> { - let tail: String = req.match_info().query("tail")?; - let relpath = PathBuf::from_param(tail.trim_left_matches('/'))?; - - // full filepath - let path = self.directory.join(&relpath).canonicalize()?; - - if path.is_dir() { - if let Some(ref redir_index) = self.index { - // TODO: Don't redirect, just return the index content. - // TODO: It'd be nice if there were a good usable URL manipulation - // library - let mut new_path: String = req.path().to_owned(); - if !new_path.ends_with('/') { - new_path.push('/'); - } - new_path.push_str(redir_index); - HttpResponse::Found() - .header(header::LOCATION, new_path.as_str()) - .finish() - .respond_to(&req) - } else if self.show_index { - let dir = Directory::new(self.directory.clone(), path); - Ok((*self.renderer)(&dir, &req)?.into()) - } else { - Err(StaticFileError::IsDirectory.into()) - } - } else { - NamedFile::open_with_config(path, C::default())? - .set_cpu_pool(self.cpu_pool.clone()) - .respond_to(&req)? - .respond_to(&req) - } - } -} - -impl Handler for StaticFiles { - type Result = Result, Error>; - - fn handle(&self, req: &HttpRequest) -> Self::Result { - self.try_handle(req).or_else(|e| { - debug!("StaticFiles: Failed to handle {}: {}", req.path(), e); - Ok(self.default.handle(req)) - }) - } -} - -/// HTTP Range header representation. -#[derive(Debug, Clone, Copy)] -struct HttpRange { - pub start: u64, - pub length: u64, -} - -static PREFIX: &'static str = "bytes="; -const PREFIX_LEN: usize = 6; - -impl HttpRange { - /// Parses Range HTTP header string as per RFC 2616. - /// - /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). - /// `size` is full size of response (file). - fn parse(header: &str, size: u64) -> Result, ()> { - if header.is_empty() { - return Ok(Vec::new()); - } - if !header.starts_with(PREFIX) { - return Err(()); - } - - let size_sig = size as i64; - let mut no_overlap = false; - - let all_ranges: Vec> = header[PREFIX_LEN..] - .split(',') - .map(|x| x.trim()) - .filter(|x| !x.is_empty()) - .map(|ra| { - let mut start_end_iter = ra.split('-'); - - let start_str = start_end_iter.next().ok_or(())?.trim(); - let end_str = start_end_iter.next().ok_or(())?.trim(); - - if start_str.is_empty() { - // If no start is specified, end specifies the - // range start relative to the end of the file. - let mut length: i64 = try!(end_str.parse().map_err(|_| ())); - - if length > size_sig { - length = size_sig; - } - - Ok(Some(HttpRange { - start: (size_sig - length) as u64, - length: length as u64, - })) - } else { - let start: i64 = start_str.parse().map_err(|_| ())?; - - if start < 0 { - return Err(()); - } - if start >= size_sig { - no_overlap = true; - return Ok(None); - } - - let length = if end_str.is_empty() { - // If no end is specified, range extends to end of the file. - size_sig - start - } else { - let mut end: i64 = end_str.parse().map_err(|_| ())?; - - if start > end { - return Err(()); - } - - if end >= size_sig { - end = size_sig - 1; - } - - end - start + 1 - }; - - Ok(Some(HttpRange { - start: start as u64, - length: length as u64, - })) - } - }).collect::>()?; - - let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); - - if no_overlap && ranges.is_empty() { - return Err(()); - } - - Ok(ranges) - } -} - -#[cfg(test)] -mod tests { - use std::fs; - - use super::*; - use application::App; - use body::{Binary, Body}; - use http::{header, Method, StatusCode}; - use test::{self, TestRequest}; - - #[test] - fn test_file_extension_to_mime() { - let m = file_extension_to_mime("jpg"); - assert_eq!(m, mime::IMAGE_JPEG); - - let m = file_extension_to_mime("invalid extension!!"); - assert_eq!(m, mime::APPLICATION_OCTET_STREAM); - - let m = file_extension_to_mime(""); - assert_eq!(m, mime::APPLICATION_OCTET_STREAM); - } - - #[test] - fn test_named_file_text() { - assert!(NamedFile::open("test--").is_err()); - let mut file = NamedFile::open("Cargo.toml") - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/x-toml" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"Cargo.toml\"" - ); - } - - #[test] - fn test_named_file_set_content_type() { - let mut file = NamedFile::open("Cargo.toml") - .unwrap() - .set_content_type(mime::TEXT_XML) - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/xml" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"Cargo.toml\"" - ); - } - - #[test] - fn test_named_file_image() { - let mut file = NamedFile::open("tests/test.png") - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "image/png" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"test.png\"" - ); - } - - #[test] - fn test_named_file_image_attachment() { - use header::{ContentDisposition, DispositionParam, DispositionType}; - let cd = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::Filename(String::from("test.png"))], - }; - let mut file = NamedFile::open("tests/test.png") - .unwrap() - .set_content_disposition(cd) - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "image/png" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "attachment; filename=\"test.png\"" - ); - } - - #[derive(Default)] - pub struct AllAttachmentConfig; - impl StaticFileConfig for AllAttachmentConfig { - fn content_disposition_map(_typ: mime::Name) -> DispositionType { - DispositionType::Attachment - } - } - - #[derive(Default)] - pub struct AllInlineConfig; - impl StaticFileConfig for AllInlineConfig { - fn content_disposition_map(_typ: mime::Name) -> DispositionType { - DispositionType::Inline - } - } - - #[test] - fn test_named_file_image_attachment_and_custom_config() { - let file = NamedFile::open_with_config("tests/test.png", AllAttachmentConfig) - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "image/png" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "attachment; filename=\"test.png\"" - ); - - let file = NamedFile::open_with_config("tests/test.png", AllInlineConfig) - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "image/png" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"test.png\"" - ); - } - - #[test] - fn test_named_file_binary() { - let mut file = NamedFile::open("tests/test.binary") - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "application/octet-stream" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "attachment; filename=\"test.binary\"" - ); - } - - #[test] - fn test_named_file_status_code_text() { - let mut file = NamedFile::open("Cargo.toml") - .unwrap() - .set_status_code(StatusCode::NOT_FOUND) - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/x-toml" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"Cargo.toml\"" - ); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_named_file_ranges_status_code() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".").unwrap().index_file("Cargo.toml"), - ) - }); - - // Valid range header - let request = srv - .get() - .uri(srv.url("/t%65st/Cargo.toml")) - .header(header::RANGE, "bytes=10-20") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); - - // Invalid range header - let request = srv - .get() - .uri(srv.url("/t%65st/Cargo.toml")) - .header(header::RANGE, "bytes=1-0") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - - assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); - } - - #[test] - fn test_named_file_content_range_headers() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".") - .unwrap() - .index_file("tests/test.binary"), - ) - }); - - // Valid range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .header(header::RANGE, "bytes=10-20") - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentrange = response - .headers() - .get(header::CONTENT_RANGE) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentrange, "bytes 10-20/100"); - - // Invalid range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .header(header::RANGE, "bytes=10-5") - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentrange = response - .headers() - .get(header::CONTENT_RANGE) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentrange, "bytes */100"); - } - - #[test] - fn test_named_file_content_length_headers() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".") - .unwrap() - .index_file("tests/test.binary"), - ) - }); - - // Valid range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .header(header::RANGE, "bytes=10-20") - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentlength = response - .headers() - .get(header::CONTENT_LENGTH) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentlength, "11"); - - // Invalid range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .header(header::RANGE, "bytes=10-8") - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentlength = response - .headers() - .get(header::CONTENT_LENGTH) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentlength, "0"); - - // Without range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .no_default_headers() - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentlength = response - .headers() - .get(header::CONTENT_LENGTH) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentlength, "100"); - - // chunked - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - { - let te = response - .headers() - .get(header::TRANSFER_ENCODING) - .unwrap() - .to_str() - .unwrap(); - assert_eq!(te, "chunked"); - } - let bytes = srv.execute(response.body()).unwrap(); - let data = Bytes::from(fs::read("tests/test.binary").unwrap()); - assert_eq!(bytes, data); - } - - #[derive(Default)] - pub struct OnlyMethodHeadConfig; - impl StaticFileConfig for OnlyMethodHeadConfig { - fn is_method_allowed(method: &Method) -> bool { - match *method { - Method::HEAD => true, - _ => false, - } - } - } - - #[test] - fn test_named_file_not_allowed() { - let file = - NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); - let req = TestRequest::default().method(Method::POST).finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - - let file = - NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); - let req = TestRequest::default().method(Method::PUT).finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - - let file = - NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); - let req = TestRequest::default().method(Method::GET).finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - } - - #[test] - fn test_named_file_content_encoding() { - let req = TestRequest::default().method(Method::GET).finish(); - let file = NamedFile::open("Cargo.toml").unwrap(); - - assert!(file.encoding.is_none()); - let resp = file - .set_content_encoding(ContentEncoding::Identity) - .respond_to(&req) - .unwrap(); - - assert!(resp.content_encoding().is_some()); - assert_eq!(resp.content_encoding().unwrap().as_str(), "identity"); - } - - #[test] - fn test_named_file_any_method() { - let req = TestRequest::default().method(Method::POST).finish(); - let file = NamedFile::open("Cargo.toml").unwrap(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - } - - #[test] - fn test_static_files() { - let mut st = StaticFiles::new(".").unwrap().show_files_listing(); - let req = TestRequest::with_uri("/missing") - .param("tail", "missing") - .finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - - st.show_index = false; - let req = TestRequest::default().finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - - let req = TestRequest::default().param("tail", "").finish(); - - st.show_index = true; - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/html; charset=utf-8" - ); - assert!(resp.body().is_binary()); - assert!(format!("{:?}", resp.body()).contains("README.md")); - } - - #[test] - fn test_static_files_bad_directory() { - let st: Result, Error> = StaticFiles::new("missing"); - assert!(st.is_err()); - - let st: Result, Error> = StaticFiles::new("Cargo.toml"); - assert!(st.is_err()); - } - - #[test] - fn test_default_handler_file_missing() { - let st = StaticFiles::new(".") - .unwrap() - .default_handler(|_: &_| "default content"); - let req = TestRequest::with_uri("/missing") - .param("tail", "missing") - .finish(); - - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.body(), - &Body::Binary(Binary::Slice(b"default content")) - ); - } - - #[test] - fn test_redirect_to_index() { - let st = StaticFiles::new(".").unwrap().index_file("index.html"); - let req = TestRequest::default().uri("/tests").finish(); - - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::FOUND); - assert_eq!( - resp.headers().get(header::LOCATION).unwrap(), - "/tests/index.html" - ); - - let req = TestRequest::default().uri("/tests/").finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::FOUND); - assert_eq!( - resp.headers().get(header::LOCATION).unwrap(), - "/tests/index.html" - ); - } - - #[test] - fn test_redirect_to_index_nested() { - let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); - let req = TestRequest::default().uri("/src/client").finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::FOUND); - assert_eq!( - resp.headers().get(header::LOCATION).unwrap(), - "/src/client/mod.rs" - ); - } - - #[test] - fn integration_redirect_to_index_with_prefix() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .prefix("public") - .handler("/", StaticFiles::new(".").unwrap().index_file("Cargo.toml")) - }); - - let request = srv.get().uri(srv.url("/public")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::FOUND); - let loc = response - .headers() - .get(header::LOCATION) - .unwrap() - .to_str() - .unwrap(); - assert_eq!(loc, "/public/Cargo.toml"); - - let request = srv.get().uri(srv.url("/public/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::FOUND); - let loc = response - .headers() - .get(header::LOCATION) - .unwrap() - .to_str() - .unwrap(); - assert_eq!(loc, "/public/Cargo.toml"); - } - - #[test] - fn integration_redirect_to_index() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".").unwrap().index_file("Cargo.toml"), - ) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::FOUND); - let loc = response - .headers() - .get(header::LOCATION) - .unwrap() - .to_str() - .unwrap(); - assert_eq!(loc, "/test/Cargo.toml"); - - let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::FOUND); - let loc = response - .headers() - .get(header::LOCATION) - .unwrap() - .to_str() - .unwrap(); - assert_eq!(loc, "/test/Cargo.toml"); - } - - #[test] - fn integration_percent_encoded() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".").unwrap().index_file("Cargo.toml"), - ) - }); - - let request = srv - .get() - .uri(srv.url("/test/%43argo.toml")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - } - - struct T(&'static str, u64, Vec); - - #[test] - fn test_parse() { - let tests = vec![ - T("", 0, vec![]), - T("", 1000, vec![]), - T("foo", 0, vec![]), - T("bytes=", 0, vec![]), - T("bytes=7", 10, vec![]), - T("bytes= 7 ", 10, vec![]), - T("bytes=1-", 0, vec![]), - T("bytes=5-4", 10, vec![]), - T("bytes=0-2,5-4", 10, vec![]), - T("bytes=2-5,4-3", 10, vec![]), - T("bytes=--5,4--3", 10, vec![]), - T("bytes=A-", 10, vec![]), - T("bytes=A- ", 10, vec![]), - T("bytes=A-Z", 10, vec![]), - T("bytes= -Z", 10, vec![]), - T("bytes=5-Z", 10, vec![]), - T("bytes=Ran-dom, garbage", 10, vec![]), - T("bytes=0x01-0x02", 10, vec![]), - T("bytes= ", 10, vec![]), - T("bytes= , , , ", 10, vec![]), - T( - "bytes=0-9", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=0-", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=5-", - 10, - vec![HttpRange { - start: 5, - length: 5, - }], - ), - T( - "bytes=0-20", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=15-,0-5", - 10, - vec![HttpRange { - start: 0, - length: 6, - }], - ), - T( - "bytes=1-2,5-", - 10, - vec![ - HttpRange { - start: 1, - length: 2, - }, - HttpRange { - start: 5, - length: 5, - }, - ], - ), - T( - "bytes=-2 , 7-", - 11, - vec![ - HttpRange { - start: 9, - length: 2, - }, - HttpRange { - start: 7, - length: 4, - }, - ], - ), - T( - "bytes=0-0 ,2-2, 7-", - 11, - vec![ - HttpRange { - start: 0, - length: 1, - }, - HttpRange { - start: 2, - length: 1, - }, - HttpRange { - start: 7, - length: 4, - }, - ], - ), - T( - "bytes=-5", - 10, - vec![HttpRange { - start: 5, - length: 5, - }], - ), - T( - "bytes=-15", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=0-499", - 10000, - vec![HttpRange { - start: 0, - length: 500, - }], - ), - T( - "bytes=500-999", - 10000, - vec![HttpRange { - start: 500, - length: 500, - }], - ), - T( - "bytes=-500", - 10000, - vec![HttpRange { - start: 9500, - length: 500, - }], - ), - T( - "bytes=9500-", - 10000, - vec![HttpRange { - start: 9500, - length: 500, - }], - ), - T( - "bytes=0-0,-1", - 10000, - vec![ - HttpRange { - start: 0, - length: 1, - }, - HttpRange { - start: 9999, - length: 1, - }, - ], - ), - T( - "bytes=500-600,601-999", - 10000, - vec![ - HttpRange { - start: 500, - length: 101, - }, - HttpRange { - start: 601, - length: 399, - }, - ], - ), - T( - "bytes=500-700,601-999", - 10000, - vec![ - HttpRange { - start: 500, - length: 201, - }, - HttpRange { - start: 601, - length: 399, - }, - ], - ), - // Match Apache laxity: - T( - "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", - 11, - vec![ - HttpRange { - start: 1, - length: 2, - }, - HttpRange { - start: 4, - length: 2, - }, - HttpRange { - start: 7, - length: 2, - }, - ], - ), - ]; - - for t in tests { - let header = t.0; - let size = t.1; - let expected = t.2; - - let res = HttpRange::parse(header, size); - - if res.is_err() { - if expected.is_empty() { - continue; - } else { - assert!( - false, - "parse({}, {}) returned error {:?}", - header, - size, - res.unwrap_err() - ); - } - } - - let got = res.unwrap(); - - if got.len() != expected.len() { - assert!( - false, - "len(parseRange({}, {})) = {}, want {}", - header, - size, - got.len(), - expected.len() - ); - continue; - } - - for i in 0..expected.len() { - if got[i].start != expected[i].start { - assert!( - false, - "parseRange({}, {})[{}].start = {}, want {}", - header, size, i, got[i].start, expected[i].start - ) - } - if got[i].length != expected[i].length { - assert!( - false, - "parseRange({}, {})[{}].length = {}, want {}", - header, size, i, got[i].length, expected[i].length - ) - } - } - } - } -} diff --git a/src/handler.rs b/src/handler.rs deleted file mode 100644 index 88210fbc0..000000000 --- a/src/handler.rs +++ /dev/null @@ -1,562 +0,0 @@ -use std::marker::PhantomData; -use std::ops::Deref; - -use futures::future::{err, ok, Future}; -use futures::{Async, Poll}; - -use error::Error; -use http::StatusCode; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use resource::DefaultResource; - -/// Trait defines object that could be registered as route handler -#[allow(unused_variables)] -pub trait Handler: 'static { - /// The type of value that handler will return. - type Result: Responder; - - /// Handle request - fn handle(&self, req: &HttpRequest) -> Self::Result; -} - -/// Trait implemented by types that generate responses for clients. -/// -/// Types that implement this trait can be used as the return type of a handler. -pub trait Responder { - /// The associated item which can be returned. - type Item: Into>; - - /// The associated error which can be returned. - type Error: Into; - - /// Convert itself to `AsyncResult` or `Error`. - fn respond_to( - self, req: &HttpRequest, - ) -> Result; -} - -/// Trait implemented by types that can be extracted from request. -/// -/// Types that implement this trait can be used with `Route::with()` method. -pub trait FromRequest: Sized { - /// Configuration for conversion process - type Config: Default; - - /// Future that resolves to a Self - type Result: Into>; - - /// Convert request to a Self - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result; - - /// Convert request to a Self - /// - /// This method uses default extractor configuration - fn extract(req: &HttpRequest) -> Self::Result { - Self::from_request(req, &Self::Config::default()) - } -} - -/// Combines two different responder types into a single type -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate futures; -/// # use futures::future::Future; -/// use actix_web::{AsyncResponder, Either, Error, HttpRequest, HttpResponse}; -/// use futures::future::result; -/// -/// type RegisterResult = -/// Either>>; -/// -/// fn index(req: HttpRequest) -> RegisterResult { -/// if is_a_variant() { -/// // <- choose variant A -/// Either::A(HttpResponse::BadRequest().body("Bad data")) -/// } else { -/// Either::B( -/// // <- variant B -/// result(Ok(HttpResponse::Ok() -/// .content_type("text/html") -/// .body("Hello!"))) -/// .responder(), -/// ) -/// } -/// } -/// # fn is_a_variant() -> bool { true } -/// # fn main() {} -/// ``` -#[derive(Debug)] -pub enum Either { - /// First branch of the type - A(A), - /// Second branch of the type - B(B), -} - -impl Responder for Either -where - A: Responder, - B: Responder, -{ - type Item = AsyncResult; - type Error = Error; - - fn respond_to( - self, req: &HttpRequest, - ) -> Result, Error> { - match self { - Either::A(a) => match a.respond_to(req) { - Ok(val) => Ok(val.into()), - Err(err) => Err(err.into()), - }, - Either::B(b) => match b.respond_to(req) { - Ok(val) => Ok(val.into()), - Err(err) => Err(err.into()), - }, - } - } -} - -impl Future for Either -where - A: Future, - B: Future, -{ - type Item = I; - type Error = E; - - fn poll(&mut self) -> Poll { - match *self { - Either::A(ref mut fut) => fut.poll(), - Either::B(ref mut fut) => fut.poll(), - } - } -} - -impl Responder for Option -where - T: Responder, -{ - type Item = AsyncResult; - type Error = Error; - - fn respond_to( - self, req: &HttpRequest, - ) -> Result, Error> { - match self { - Some(t) => match t.respond_to(req) { - Ok(val) => Ok(val.into()), - Err(err) => Err(err.into()), - }, - None => Ok(req.build_response(StatusCode::NOT_FOUND).finish().into()), - } - } -} - -/// Convenience trait that converts `Future` object to a `Boxed` future -/// -/// For example loading json from request's body is async operation. -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate futures; -/// # #[macro_use] extern crate serde_derive; -/// use actix_web::{ -/// App, AsyncResponder, Error, HttpMessage, HttpRequest, HttpResponse, -/// }; -/// use futures::future::Future; -/// -/// #[derive(Deserialize, Debug)] -/// struct MyObj { -/// name: String, -/// } -/// -/// fn index(mut req: HttpRequest) -> Box> { -/// req.json() // <- get JsonBody future -/// .from_err() -/// .and_then(|val: MyObj| { // <- deserialized value -/// Ok(HttpResponse::Ok().into()) -/// }) -/// // Construct boxed future by using `AsyncResponder::responder()` method -/// .responder() -/// } -/// # fn main() {} -/// ``` -pub trait AsyncResponder: Sized { - /// Convert to a boxed future - fn responder(self) -> Box>; -} - -impl AsyncResponder for F -where - F: Future + 'static, - I: Responder + 'static, - E: Into + 'static, -{ - fn responder(self) -> Box> { - Box::new(self) - } -} - -/// Handler for Fn() -impl Handler for F -where - F: Fn(&HttpRequest) -> R + 'static, - R: Responder + 'static, -{ - type Result = R; - - fn handle(&self, req: &HttpRequest) -> R { - (self)(req) - } -} - -/// Represents async result -/// -/// Result could be in tree different forms. -/// * Ok(T) - ready item -/// * Err(E) - error happen during reply process -/// * Future - reply process completes in the future -pub struct AsyncResult(Option>); - -impl Future for AsyncResult { - type Item = I; - type Error = E; - - fn poll(&mut self) -> Poll { - let res = self.0.take().expect("use after resolve"); - match res { - AsyncResultItem::Ok(msg) => Ok(Async::Ready(msg)), - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Future(mut fut) => match fut.poll() { - Ok(Async::NotReady) => { - self.0 = Some(AsyncResultItem::Future(fut)); - Ok(Async::NotReady) - } - Ok(Async::Ready(msg)) => Ok(Async::Ready(msg)), - Err(err) => Err(err), - }, - } - } -} - -pub(crate) enum AsyncResultItem { - Ok(I), - Err(E), - Future(Box>), -} - -impl AsyncResult { - /// Create async response - #[inline] - pub fn async(fut: Box>) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Future(fut))) - } - - /// Send response - #[inline] - pub fn ok>(ok: R) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Ok(ok.into()))) - } - - /// Send error - #[inline] - pub fn err>(err: R) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Err(err.into()))) - } - - #[inline] - pub(crate) fn into(self) -> AsyncResultItem { - self.0.expect("use after resolve") - } - - #[cfg(test)] - pub(crate) fn as_msg(&self) -> &I { - match self.0.as_ref().unwrap() { - &AsyncResultItem::Ok(ref resp) => resp, - _ => panic!(), - } - } - - #[cfg(test)] - pub(crate) fn as_err(&self) -> Option<&E> { - match self.0.as_ref().unwrap() { - &AsyncResultItem::Err(ref err) => Some(err), - _ => None, - } - } -} - -impl Responder for AsyncResult { - type Item = AsyncResult; - type Error = Error; - - fn respond_to( - self, _: &HttpRequest, - ) -> Result, Error> { - Ok(self) - } -} - -impl Responder for HttpResponse { - type Item = AsyncResult; - type Error = Error; - - #[inline] - fn respond_to( - self, _: &HttpRequest, - ) -> Result, Error> { - Ok(AsyncResult(Some(AsyncResultItem::Ok(self)))) - } -} - -impl From for AsyncResult { - #[inline] - fn from(resp: T) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Ok(resp))) - } -} - -impl> Responder for Result { - type Item = ::Item; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - match self { - Ok(val) => match val.respond_to(req) { - Ok(val) => Ok(val), - Err(err) => Err(err.into()), - }, - Err(err) => Err(err.into()), - } - } -} - -impl> From, E>> for AsyncResult { - #[inline] - fn from(res: Result, E>) -> Self { - match res { - Ok(val) => val, - Err(err) => AsyncResult(Some(AsyncResultItem::Err(err.into()))), - } - } -} - -impl> From> for AsyncResult { - #[inline] - fn from(res: Result) -> Self { - match res { - Ok(val) => AsyncResult(Some(AsyncResultItem::Ok(val))), - Err(err) => AsyncResult(Some(AsyncResultItem::Err(err.into()))), - } - } -} - -impl From>, E>> for AsyncResult -where - T: 'static, - E: Into + 'static, -{ - #[inline] - fn from(res: Result>, E>) -> Self { - match res { - Ok(fut) => AsyncResult(Some(AsyncResultItem::Future(Box::new( - fut.map_err(|e| e.into()), - )))), - Err(err) => AsyncResult(Some(AsyncResultItem::Err(err.into()))), - } - } -} - -impl From>> for AsyncResult { - #[inline] - fn from(fut: Box>) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Future(fut))) - } -} - -/// Convenience type alias -pub type FutureResponse = Box>; - -impl Responder for Box> -where - I: Responder + 'static, - E: Into + 'static, -{ - type Item = AsyncResult; - type Error = Error; - - #[inline] - fn respond_to( - self, req: &HttpRequest, - ) -> Result, Error> { - let req = req.clone(); - let fut = self - .map_err(|e| e.into()) - .then(move |r| match r.respond_to(&req) { - Ok(reply) => match reply.into().into() { - AsyncResultItem::Ok(resp) => ok(resp), - _ => panic!("Nested async replies are not supported"), - }, - Err(e) => err(e), - }); - Ok(AsyncResult::async(Box::new(fut))) - } -} - -pub(crate) trait RouteHandler: 'static { - fn handle(&self, &HttpRequest) -> AsyncResult; - - fn has_default_resource(&self) -> bool { - false - } - - fn default_resource(&mut self, _: DefaultResource) {} - - fn finish(&mut self) {} -} - -/// Route handler wrapper for Handler -pub(crate) struct WrapHandler -where - H: Handler, - R: Responder, - S: 'static, -{ - h: H, - s: PhantomData, -} - -impl WrapHandler -where - H: Handler, - R: Responder, - S: 'static, -{ - pub fn new(h: H) -> Self { - WrapHandler { h, s: PhantomData } - } -} - -impl RouteHandler for WrapHandler -where - H: Handler, - R: Responder + 'static, - S: 'static, -{ - fn handle(&self, req: &HttpRequest) -> AsyncResult { - match self.h.handle(req).respond_to(req) { - Ok(reply) => reply.into(), - Err(err) => AsyncResult::err(err.into()), - } - } -} - -/// Async route handler -pub(crate) struct AsyncHandler -where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - S: 'static, -{ - h: Box, - s: PhantomData, -} - -impl AsyncHandler -where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - S: 'static, -{ - pub fn new(h: H) -> Self { - AsyncHandler { - h: Box::new(h), - s: PhantomData, - } - } -} - -impl RouteHandler for AsyncHandler -where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - S: 'static, -{ - fn handle(&self, req: &HttpRequest) -> AsyncResult { - let req = req.clone(); - let fut = (self.h)(&req).map_err(|e| e.into()).then(move |r| { - match r.respond_to(&req) { - Ok(reply) => match reply.into().into() { - AsyncResultItem::Ok(resp) => Either::A(ok(resp)), - AsyncResultItem::Err(e) => Either::A(err(e)), - AsyncResultItem::Future(fut) => Either::B(fut), - }, - Err(e) => Either::A(err(e)), - } - }); - AsyncResult::async(Box::new(fut)) - } -} - -/// Access an application state -/// -/// `S` - application state type -/// -/// ## Example -/// -/// ```rust -/// # extern crate bytes; -/// # extern crate actix_web; -/// # extern crate futures; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Path, State}; -/// -/// /// Application state -/// struct MyApp { -/// msg: &'static str, -/// } -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// extract path info using serde -/// fn index(state: State, path: Path) -> String { -/// format!("{} {}!", state.msg, path.username) -/// } -/// -/// fn main() { -/// let app = App::with_state(MyApp { msg: "Welcome" }).resource( -/// "/{username}/index.html", // <- define path parameters -/// |r| r.method(http::Method::GET).with(index), -/// ); // <- use `with` extractor -/// } -/// ``` -pub struct State(HttpRequest); - -impl Deref for State { - type Target = S; - - fn deref(&self) -> &S { - self.0.state() - } -} - -impl FromRequest for State { - type Config = (); - type Result = State; - - #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { - State(req.clone()) - } -} diff --git a/src/helpers.rs b/src/helpers.rs deleted file mode 100644 index e82d61616..000000000 --- a/src/helpers.rs +++ /dev/null @@ -1,571 +0,0 @@ -//! Various helpers - -use http::{header, StatusCode}; -use regex::Regex; - -use handler::Handler; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; - -/// Path normalization helper -/// -/// By normalizing it means: -/// -/// - Add a trailing slash to the path. -/// - Remove a trailing slash from the path. -/// - Double slashes are replaced by one. -/// -/// The handler returns as soon as it finds a path that resolves -/// correctly. The order if all enable is 1) merge, 3) both merge and append -/// and 3) append. If the path resolves with -/// at least one of those conditions, it will redirect to the new path. -/// -/// If *append* is *true* append slash when needed. If a resource is -/// defined with trailing slash and the request comes without it, it will -/// append it automatically. -/// -/// If *merge* is *true*, merge multiple consecutive slashes in the path into -/// one. -/// -/// This handler designed to be use as a handler for application's *default -/// resource*. -/// -/// ```rust -/// # extern crate actix_web; -/// # #[macro_use] extern crate serde_derive; -/// # use actix_web::*; -/// use actix_web::http::NormalizePath; -/// -/// # fn index(req: &HttpRequest) -> HttpResponse { -/// # HttpResponse::Ok().into() -/// # } -/// fn main() { -/// let app = App::new() -/// .resource("/test/", |r| r.f(index)) -/// .default_resource(|r| r.h(NormalizePath::default())) -/// .finish(); -/// } -/// ``` -/// In this example `/test`, `/test///` will be redirected to `/test/` url. -pub struct NormalizePath { - append: bool, - merge: bool, - re_merge: Regex, - redirect: StatusCode, - not_found: StatusCode, -} - -impl Default for NormalizePath { - /// Create default `NormalizePath` instance, *append* is set to *true*, - /// *merge* is set to *true* and *redirect* is set to - /// `StatusCode::MOVED_PERMANENTLY` - fn default() -> NormalizePath { - NormalizePath { - append: true, - merge: true, - re_merge: Regex::new("//+").unwrap(), - redirect: StatusCode::MOVED_PERMANENTLY, - not_found: StatusCode::NOT_FOUND, - } - } -} - -impl NormalizePath { - /// Create new `NormalizePath` instance - pub fn new(append: bool, merge: bool, redirect: StatusCode) -> NormalizePath { - NormalizePath { - append, - merge, - redirect, - re_merge: Regex::new("//+").unwrap(), - not_found: StatusCode::NOT_FOUND, - } - } -} - -impl Handler for NormalizePath { - type Result = HttpResponse; - - fn handle(&self, req: &HttpRequest) -> Self::Result { - let query = req.query_string(); - if self.merge { - // merge slashes - let p = self.re_merge.replace_all(req.path(), "/"); - if p.len() != req.path().len() { - if req.resource().has_prefixed_resource(p.as_ref()) { - let p = if !query.is_empty() { - p + "?" + query - } else { - p - }; - return HttpResponse::build(self.redirect) - .header(header::LOCATION, p.as_ref()) - .finish(); - } - // merge slashes and append trailing slash - if self.append && !p.ends_with('/') { - let p = p.as_ref().to_owned() + "/"; - if req.resource().has_prefixed_resource(&p) { - let p = if !query.is_empty() { - p + "?" + query - } else { - p - }; - return HttpResponse::build(self.redirect) - .header(header::LOCATION, p.as_str()) - .finish(); - } - } - - // try to remove trailing slash - if p.ends_with('/') { - let p = p.as_ref().trim_right_matches('/'); - if req.resource().has_prefixed_resource(p) { - let mut req = HttpResponse::build(self.redirect); - return if !query.is_empty() { - req.header( - header::LOCATION, - (p.to_owned() + "?" + query).as_str(), - ) - } else { - req.header(header::LOCATION, p) - }.finish(); - } - } - } else if p.ends_with('/') { - // try to remove trailing slash - let p = p.as_ref().trim_right_matches('/'); - if req.resource().has_prefixed_resource(p) { - let mut req = HttpResponse::build(self.redirect); - return if !query.is_empty() { - req.header( - header::LOCATION, - (p.to_owned() + "?" + query).as_str(), - ) - } else { - req.header(header::LOCATION, p) - }.finish(); - } - } - } - // append trailing slash - if self.append && !req.path().ends_with('/') { - let p = req.path().to_owned() + "/"; - if req.resource().has_prefixed_resource(&p) { - let p = if !query.is_empty() { - p + "?" + query - } else { - p - }; - return HttpResponse::build(self.redirect) - .header(header::LOCATION, p.as_str()) - .finish(); - } - } - HttpResponse::new(self.not_found) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use application::App; - use http::{header, Method}; - use test::TestRequest; - - fn index(_req: &HttpRequest) -> HttpResponse { - HttpResponse::new(StatusCode::OK) - } - - #[test] - fn test_normalize_path_trailing_slashes() { - let app = App::new() - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource2/", |r| r.method(Method::GET).f(index)) - .default_resource(|r| r.h(NormalizePath::default())) - .finish(); - - // trailing slashes - let params = vec![ - ("/resource1", "", StatusCode::OK), - ("/resource1/", "/resource1", StatusCode::MOVED_PERMANENTLY), - ("/resource2", "/resource2/", StatusCode::MOVED_PERMANENTLY), - ("/resource2/", "", StatusCode::OK), - ("/resource1?p1=1&p2=2", "", StatusCode::OK), - ( - "/resource1/?p1=1&p2=2", - "/resource1?p1=1&p2=2", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/resource2?p1=1&p2=2", - "/resource2/?p1=1&p2=2", - StatusCode::MOVED_PERMANENTLY, - ), - ("/resource2/?p1=1&p2=2", "", StatusCode::OK), - ]; - for (path, target, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - if !target.is_empty() { - assert_eq!( - target, - r.headers().get(header::LOCATION).unwrap().to_str().unwrap() - ); - } - } - } - - #[test] - fn test_prefixed_normalize_path_trailing_slashes() { - let app = App::new() - .prefix("/test") - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource2/", |r| r.method(Method::GET).f(index)) - .default_resource(|r| r.h(NormalizePath::default())) - .finish(); - - // trailing slashes - let params = vec![ - ("/test/resource1", "", StatusCode::OK), - ( - "/test/resource1/", - "/test/resource1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/test/resource2", - "/test/resource2/", - StatusCode::MOVED_PERMANENTLY, - ), - ("/test/resource2/", "", StatusCode::OK), - ("/test/resource1?p1=1&p2=2", "", StatusCode::OK), - ( - "/test/resource1/?p1=1&p2=2", - "/test/resource1?p1=1&p2=2", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/test/resource2?p1=1&p2=2", - "/test/resource2/?p1=1&p2=2", - StatusCode::MOVED_PERMANENTLY, - ), - ("/test/resource2/?p1=1&p2=2", "", StatusCode::OK), - ]; - for (path, target, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - if !target.is_empty() { - assert_eq!( - target, - r.headers().get(header::LOCATION).unwrap().to_str().unwrap() - ); - } - } - } - - #[test] - fn test_normalize_path_trailing_slashes_disabled() { - let app = App::new() - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource2/", |r| r.method(Method::GET).f(index)) - .default_resource(|r| { - r.h(NormalizePath::new( - false, - true, - StatusCode::MOVED_PERMANENTLY, - )) - }).finish(); - - // trailing slashes - let params = vec![ - ("/resource1", StatusCode::OK), - ("/resource1/", StatusCode::MOVED_PERMANENTLY), - ("/resource2", StatusCode::NOT_FOUND), - ("/resource2/", StatusCode::OK), - ("/resource1?p1=1&p2=2", StatusCode::OK), - ("/resource1/?p1=1&p2=2", StatusCode::MOVED_PERMANENTLY), - ("/resource2?p1=1&p2=2", StatusCode::NOT_FOUND), - ("/resource2/?p1=1&p2=2", StatusCode::OK), - ]; - for (path, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - } - } - - #[test] - fn test_normalize_path_merge_slashes() { - let app = App::new() - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource1/a/b", |r| r.method(Method::GET).f(index)) - .default_resource(|r| r.h(NormalizePath::default())) - .finish(); - - // trailing slashes - let params = vec![ - ("/resource1/a/b", "", StatusCode::OK), - ("/resource1/", "/resource1", StatusCode::MOVED_PERMANENTLY), - ("/resource1//", "/resource1", StatusCode::MOVED_PERMANENTLY), - ( - "//resource1//a//b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource1//a//b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource1//a//b//", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a//b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ("/resource1/a/b?p=1", "", StatusCode::OK), - ( - "//resource1//a//b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource1//a//b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a//b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a//b//?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ]; - for (path, target, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - if !target.is_empty() { - assert_eq!( - target, - r.headers().get(header::LOCATION).unwrap().to_str().unwrap() - ); - } - } - } - - #[test] - fn test_normalize_path_merge_and_append_slashes() { - let app = App::new() - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource2/", |r| r.method(Method::GET).f(index)) - .resource("/resource1/a/b", |r| r.method(Method::GET).f(index)) - .resource("/resource2/a/b/", |r| r.method(Method::GET).f(index)) - .default_resource(|r| r.h(NormalizePath::default())) - .finish(); - - // trailing slashes - let params = vec![ - ("/resource1/a/b", "", StatusCode::OK), - ( - "/resource1/a/b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b/", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b//", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/resource2/a/b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ("/resource2/a/b/", "", StatusCode::OK), - ( - "//resource2//a//b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b/", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource2//a//b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource2//a//b/", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource2/a///b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource2/a///b/", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ("/resource1/a/b?p=1", "", StatusCode::OK), - ( - "/resource1/a/b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b/?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b//?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/resource2/a/b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b/?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource2//a//b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource2//a//b/?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource2/a///b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource2/a///b/?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ]; - for (path, target, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - if !target.is_empty() { - assert_eq!( - target, - r.headers().get(header::LOCATION).unwrap().to_str().unwrap() - ); - } - } - } -} diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 60f77b07e..8c972bd13 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -15,7 +15,6 @@ use error::{ }; use header::Header; use json::JsonBody; -use multipart::Multipart; /// Trait that implements general purpose operations on http messages pub trait HttpMessage: Sized { @@ -203,46 +202,6 @@ pub trait HttpMessage: Sized { JsonBody::new(self) } - /// Return stream to http payload processes as multipart. - /// - /// Content-type: multipart/form-data; - /// - /// ## Server example - /// - /// ```rust - /// # extern crate actix_web; - /// # extern crate env_logger; - /// # extern crate futures; - /// # use std::str; - /// # use actix_web::*; - /// # use actix_web::actix::fut::FinishStream; - /// # use futures::{Future, Stream}; - /// # use futures::future::{ok, result, Either}; - /// fn index(mut req: HttpRequest) -> Box> { - /// req.multipart().from_err() // <- get multipart stream for current request - /// .and_then(|item| match item { // <- iterate over multipart items - /// multipart::MultipartItem::Field(field) => { - /// // Field in turn is stream of *Bytes* object - /// Either::A(field.from_err() - /// .map(|c| println!("-- CHUNK: \n{:?}", str::from_utf8(&c))) - /// .finish()) - /// }, - /// multipart::MultipartItem::Nested(mp) => { - /// // Or item could be nested Multipart stream - /// Either::B(ok(())) - /// } - /// }) - /// .finish() // <- Stream::finish() combinator from actix - /// .map(|_| HttpResponse::Ok().into()) - /// .responder() - /// } - /// # fn main() {} - /// ``` - fn multipart(&self) -> Multipart { - let boundary = Multipart::boundary(self.headers()); - Multipart::new(boundary, self.payload()) - } - /// Return stream of lines. fn readlines(&self) -> Readlines { Readlines::new(self) diff --git a/src/httpresponse.rs b/src/httpresponse.rs index 59815c58c..73de380ad 100644 --- a/src/httpresponse.rs +++ b/src/httpresponse.rs @@ -13,12 +13,10 @@ use serde::Serialize; use serde_json; use body::Body; -use client::ClientResponse; use error::Error; -use handler::Responder; use header::{ContentEncoding, Header, IntoHeaderValue}; use httpmessage::HttpMessage; -use httprequest::HttpRequest; +// use httprequest::HttpRequest; /// max write buffer size 64k pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; @@ -720,16 +718,6 @@ impl From for HttpResponse { } } -impl Responder for HttpResponseBuilder { - type Item = HttpResponse; - type Error = Error; - - #[inline] - fn respond_to(mut self, _: &HttpRequest) -> Result { - Ok(self.finish()) - } -} - impl From<&'static str> for HttpResponse { fn from(val: &'static str) -> Self { HttpResponse::Ok() @@ -738,18 +726,6 @@ impl From<&'static str> for HttpResponse { } } -impl Responder for &'static str { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("text/plain; charset=utf-8") - .body(self)) - } -} - impl From<&'static [u8]> for HttpResponse { fn from(val: &'static [u8]) -> Self { HttpResponse::Ok() @@ -758,18 +734,6 @@ impl From<&'static [u8]> for HttpResponse { } } -impl Responder for &'static [u8] { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("application/octet-stream") - .body(self)) - } -} - impl From for HttpResponse { fn from(val: String) -> Self { HttpResponse::Ok() @@ -778,18 +742,6 @@ impl From for HttpResponse { } } -impl Responder for String { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("text/plain; charset=utf-8") - .body(self)) - } -} - impl<'a> From<&'a String> for HttpResponse { fn from(val: &'a String) -> Self { HttpResponse::build(StatusCode::OK) @@ -798,18 +750,6 @@ impl<'a> From<&'a String> for HttpResponse { } } -impl<'a> Responder for &'a String { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("text/plain; charset=utf-8") - .body(self)) - } -} - impl From for HttpResponse { fn from(val: Bytes) -> Self { HttpResponse::Ok() @@ -818,18 +758,6 @@ impl From for HttpResponse { } } -impl Responder for Bytes { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("application/octet-stream") - .body(self)) - } -} - impl From for HttpResponse { fn from(val: BytesMut) -> Self { HttpResponse::Ok() @@ -838,40 +766,6 @@ impl From for HttpResponse { } } -impl Responder for BytesMut { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("application/octet-stream") - .body(self)) - } -} - -/// Create `HttpResponseBuilder` from `ClientResponse` -/// -/// It is useful for proxy response. This implementation -/// copies all responses's headers and status. -impl<'a> From<&'a ClientResponse> for HttpResponseBuilder { - fn from(resp: &'a ClientResponse) -> HttpResponseBuilder { - let mut builder = HttpResponse::build(resp.status()); - for (key, value) in resp.headers() { - builder.header(key.clone(), value.clone()); - } - builder - } -} - -impl<'a, S> From<&'a HttpRequest> for HttpResponseBuilder { - fn from(req: &'a HttpRequest) -> HttpResponseBuilder { - req.request() - .server_settings() - .get_response_builder(StatusCode::OK) - } -} - #[derive(Debug)] struct InnerHttpResponse { version: Option, @@ -921,7 +815,7 @@ impl InnerHttpResponse { let body = match mem::replace(&mut self.body, Body::Empty) { Body::Empty => None, Body::Binary(mut bin) => Some(bin.take()), - Body::Streaming(_) | Body::Actor(_) => { + Body::Streaming(_) => { error!("Streaming or Actor body is not support by error response"); None } diff --git a/src/json.rs b/src/json.rs index 178143f11..04dd369eb 100644 --- a/src/json.rs +++ b/src/json.rs @@ -11,10 +11,9 @@ use serde::Serialize; use serde_json; use error::{Error, JsonPayloadError}; -use handler::{FromRequest, Responder}; use http::StatusCode; use httpmessage::HttpMessage; -use httprequest::HttpRequest; +// use httprequest::HttpRequest; use httpresponse::HttpResponse; /// Json helper @@ -116,102 +115,6 @@ where } } -impl Responder for Json { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - let body = serde_json::to_string(&self.0)?; - - Ok(req - .build_response(StatusCode::OK) - .content_type("application/json") - .body(body)) - } -} - -impl FromRequest for Json -where - T: DeserializeOwned + 'static, - S: 'static, -{ - type Config = JsonConfig; - type Result = Box>; - - #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - let req2 = req.clone(); - let err = Rc::clone(&cfg.ehandler); - Box::new( - JsonBody::new(req) - .limit(cfg.limit) - .map_err(move |e| (*err)(e, &req2)) - .map(Json), - ) - } -} - -/// Json extractor configuration -/// -/// ```rust -/// # extern crate actix_web; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{error, http, App, HttpResponse, Json, Result}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// deserialize `Info` from request's body, max payload size is 4kb -/// fn index(info: Json) -> Result { -/// Ok(format!("Welcome {}!", info.username)) -/// } -/// -/// fn main() { -/// let app = App::new().resource("/index.html", |r| { -/// r.method(http::Method::POST) -/// .with_config(index, |cfg| { -/// cfg.0.limit(4096) // <- change json extractor configuration -/// .error_handler(|err, req| { // <- create custom error response -/// error::InternalError::from_response( -/// err, HttpResponse::Conflict().finish()).into() -/// }); -/// }) -/// }); -/// } -/// ``` -pub struct JsonConfig { - limit: usize, - ehandler: Rc) -> Error>, -} - -impl JsonConfig { - /// Change max size of payload. By default max size is 256Kb - pub fn limit(&mut self, limit: usize) -> &mut Self { - self.limit = limit; - self - } - - /// Set custom error handler - pub fn error_handler(&mut self, f: F) -> &mut Self - where - F: Fn(JsonPayloadError, &HttpRequest) -> Error + 'static, - { - self.ehandler = Rc::new(f); - self - } -} - -impl Default for JsonConfig { - fn default() -> Self { - JsonConfig { - limit: 262_144, - ehandler: Rc::new(|e, _| e.into()), - } - } -} - /// Request payload json parser that resolves to a deserialized `T` value. /// /// Returns error: diff --git a/src/lib.rs b/src/lib.rs index f494c05de..6df1a770e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,13 +77,11 @@ //! * `flate2-rust` - experimental rust based implementation for //! `gzip`, `deflate` compression. //! -#![cfg_attr(actix_nightly, feature( - specialization, // for impl ErrorResponse for std::error::Error - extern_prelude, - tool_lints, -))] +#![cfg_attr(actix_nightly, feature(tool_lints))] #![warn(missing_docs)] +#![allow(unused_imports, unused_variables, dead_code)] +extern crate actix; #[macro_use] extern crate log; extern crate base64; @@ -140,109 +138,36 @@ extern crate serde_json; extern crate smallvec; extern crate actix_net; -#[macro_use] -extern crate actix as actix_inner; #[cfg(test)] #[macro_use] extern crate serde_derive; -#[cfg(feature = "tls")] -extern crate native_tls; -#[cfg(feature = "tls")] -extern crate tokio_tls; - -#[cfg(feature = "openssl")] -extern crate openssl; -#[cfg(feature = "openssl")] -extern crate tokio_openssl; - -#[cfg(feature = "rust-tls")] -extern crate rustls; -#[cfg(feature = "rust-tls")] -extern crate tokio_rustls; -#[cfg(feature = "rust-tls")] -extern crate webpki; -#[cfg(feature = "rust-tls")] -extern crate webpki_roots; - -mod application; mod body; -mod context; -mod de; mod extensions; -mod extractor; -mod handler; mod header; -mod helpers; mod httpcodes; mod httpmessage; -mod httprequest; +//mod httprequest; mod httpresponse; mod info; mod json; -mod param; mod payload; -mod pipeline; -mod resource; -mod route; -mod router; -mod scope; mod uri; -mod with; -pub mod client; pub mod error; -pub mod fs; -pub mod middleware; -pub mod multipart; -pub mod pred; pub mod server; -pub mod test; -pub mod ws; -pub use application::App; +//pub mod test; +//pub mod ws; pub use body::{Binary, Body}; -pub use context::HttpContext; pub use error::{Error, ResponseError, Result}; pub use extensions::Extensions; -pub use extractor::{Form, Path, Query}; -pub use handler::{ - AsyncResponder, Either, FromRequest, FutureResponse, Responder, State, -}; pub use httpmessage::HttpMessage; -pub use httprequest::HttpRequest; +//pub use httprequest::HttpRequest; pub use httpresponse::HttpResponse; pub use json::Json; -pub use scope::Scope; pub use server::Request; -pub mod actix { - //! Re-exports [actix's](https://docs.rs/actix/) prelude - - extern crate actix; - pub use self::actix::actors::resolver; - pub use self::actix::actors::signal; - pub use self::actix::fut; - pub use self::actix::msgs; - pub use self::actix::prelude::*; - pub use self::actix::{run, spawn}; -} - -#[cfg(feature = "openssl")] -pub(crate) const HAS_OPENSSL: bool = true; -#[cfg(not(feature = "openssl"))] -pub(crate) const HAS_OPENSSL: bool = false; - -#[cfg(feature = "tls")] -pub(crate) const HAS_TLS: bool = true; -#[cfg(not(feature = "tls"))] -pub(crate) const HAS_TLS: bool = false; - -#[cfg(feature = "rust-tls")] -pub(crate) const HAS_RUSTLS: bool = true; -#[cfg(not(feature = "rust-tls"))] -pub(crate) const HAS_RUSTLS: bool = false; - pub mod dev { //! The `actix-web` prelude for library developers //! @@ -255,19 +180,11 @@ pub mod dev { //! ``` pub use body::BodyStream; - pub use context::Drain; - pub use extractor::{FormConfig, PayloadConfig}; - pub use handler::{AsyncResult, Handler}; pub use httpmessage::{MessageBody, Readlines, UrlEncoded}; pub use httpresponse::HttpResponseBuilder; pub use info::ConnectionInfo; - pub use json::{JsonBody, JsonConfig}; - pub use param::{FromParam, Params}; + pub use json::JsonBody; pub use payload::{Payload, PayloadBuffer}; - pub use pipeline::Pipeline; - pub use resource::Resource; - pub use route::Route; - pub use router::{ResourceDef, ResourceInfo, ResourceType, Router}; } pub mod http { @@ -281,8 +198,6 @@ pub mod http { pub use cookie::{Cookie, CookieBuilder}; - pub use helpers::NormalizePath; - /// Various http headers pub mod header { pub use header::*; diff --git a/src/middleware/cors.rs b/src/middleware/cors.rs deleted file mode 100644 index 953f2911c..000000000 --- a/src/middleware/cors.rs +++ /dev/null @@ -1,1183 +0,0 @@ -//! Cross-origin resource sharing (CORS) for Actix applications -//! -//! CORS middleware could be used with application and with resource. -//! First you need to construct CORS middleware instance. -//! -//! To construct a cors: -//! -//! 1. Call [`Cors::build`](struct.Cors.html#method.build) to start building. -//! 2. Use any of the builder methods to set fields in the backend. -//! 3. Call [finish](struct.Cors.html#method.finish) to retrieve the -//! constructed backend. -//! -//! Cors middleware could be used as parameter for `App::middleware()` or -//! `Resource::middleware()` methods. But you have to use -//! `Cors::for_app()` method to support *preflight* OPTIONS request. -//! -//! -//! # Example -//! -//! ```rust -//! # extern crate actix_web; -//! use actix_web::middleware::cors::Cors; -//! use actix_web::{http, App, HttpRequest, HttpResponse}; -//! -//! fn index(mut req: HttpRequest) -> &'static str { -//! "Hello world" -//! } -//! -//! fn main() { -//! let app = App::new().configure(|app| { -//! Cors::for_app(app) // <- Construct CORS middleware builder -//! .allowed_origin("https://www.rust-lang.org/") -//! .allowed_methods(vec!["GET", "POST"]) -//! .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) -//! .allowed_header(http::header::CONTENT_TYPE) -//! .max_age(3600) -//! .resource("/index.html", |r| { -//! r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -//! r.method(http::Method::HEAD).f(|_| HttpResponse::MethodNotAllowed()); -//! }) -//! .register() -//! }); -//! } -//! ``` -//! In this example custom *CORS* middleware get registered for "/index.html" -//! endpoint. -//! -//! Cors middleware automatically handle *OPTIONS* preflight request. -use std::collections::HashSet; -use std::iter::FromIterator; -use std::rc::Rc; - -use http::header::{self, HeaderName, HeaderValue}; -use http::{self, HttpTryFrom, Method, StatusCode, Uri}; - -use application::App; -use error::{ResponseError, Result}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; -use resource::Resource; -use router::ResourceDef; -use server::Request; - -/// A set of errors that can occur during processing CORS -#[derive(Debug, Fail)] -pub enum CorsError { - /// The HTTP request header `Origin` is required but was not provided - #[fail( - display = "The HTTP request header `Origin` is required but was not provided" - )] - MissingOrigin, - /// The HTTP request header `Origin` could not be parsed correctly. - #[fail(display = "The HTTP request header `Origin` could not be parsed correctly.")] - BadOrigin, - /// The request header `Access-Control-Request-Method` is required but is - /// missing - #[fail( - display = "The request header `Access-Control-Request-Method` is required but is missing" - )] - MissingRequestMethod, - /// The request header `Access-Control-Request-Method` has an invalid value - #[fail( - display = "The request header `Access-Control-Request-Method` has an invalid value" - )] - BadRequestMethod, - /// The request header `Access-Control-Request-Headers` has an invalid - /// value - #[fail( - display = "The request header `Access-Control-Request-Headers` has an invalid value" - )] - BadRequestHeaders, - /// The request header `Access-Control-Request-Headers` is required but is - /// missing. - #[fail( - display = "The request header `Access-Control-Request-Headers` is required but is - missing" - )] - MissingRequestHeaders, - /// Origin is not allowed to make this request - #[fail(display = "Origin is not allowed to make this request")] - OriginNotAllowed, - /// Requested method is not allowed - #[fail(display = "Requested method is not allowed")] - MethodNotAllowed, - /// One or more headers requested are not allowed - #[fail(display = "One or more headers requested are not allowed")] - HeadersNotAllowed, -} - -impl ResponseError for CorsError { - fn error_response(&self) -> HttpResponse { - HttpResponse::with_body(StatusCode::BAD_REQUEST, format!("{}", self)) - } -} - -/// An enum signifying that some of type T is allowed, or `All` (everything is -/// allowed). -/// -/// `Default` is implemented for this enum and is `All`. -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum AllOrSome { - /// Everything is allowed. Usually equivalent to the "*" value. - All, - /// Only some of `T` is allowed - Some(T), -} - -impl Default for AllOrSome { - fn default() -> Self { - AllOrSome::All - } -} - -impl AllOrSome { - /// Returns whether this is an `All` variant - pub fn is_all(&self) -> bool { - match *self { - AllOrSome::All => true, - AllOrSome::Some(_) => false, - } - } - - /// Returns whether this is a `Some` variant - pub fn is_some(&self) -> bool { - !self.is_all() - } - - /// Returns &T - pub fn as_ref(&self) -> Option<&T> { - match *self { - AllOrSome::All => None, - AllOrSome::Some(ref t) => Some(t), - } - } -} - -/// `Middleware` for Cross-origin resource sharing support -/// -/// The Cors struct contains the settings for CORS requests to be validated and -/// for responses to be generated. -#[derive(Clone)] -pub struct Cors { - inner: Rc, -} - -struct Inner { - methods: HashSet, - origins: AllOrSome>, - origins_str: Option, - headers: AllOrSome>, - expose_hdrs: Option, - max_age: Option, - preflight: bool, - send_wildcard: bool, - supports_credentials: bool, - vary_header: bool, -} - -impl Default for Cors { - fn default() -> Cors { - let inner = Inner { - origins: AllOrSome::default(), - origins_str: None, - methods: HashSet::from_iter( - vec![ - Method::GET, - Method::HEAD, - Method::POST, - Method::OPTIONS, - Method::PUT, - Method::PATCH, - Method::DELETE, - ].into_iter(), - ), - headers: AllOrSome::All, - expose_hdrs: None, - max_age: None, - preflight: true, - send_wildcard: false, - supports_credentials: false, - vary_header: true, - }; - Cors { - inner: Rc::new(inner), - } - } -} - -impl Cors { - /// Build a new CORS middleware instance - pub fn build() -> CorsBuilder<()> { - CorsBuilder { - cors: Some(Inner { - origins: AllOrSome::All, - origins_str: None, - methods: HashSet::new(), - headers: AllOrSome::All, - expose_hdrs: None, - max_age: None, - preflight: true, - send_wildcard: false, - supports_credentials: false, - vary_header: true, - }), - methods: false, - error: None, - expose_hdrs: HashSet::new(), - resources: Vec::new(), - app: None, - } - } - - /// Create CorsBuilder for a specified application. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::middleware::cors::Cors; - /// use actix_web::{http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().configure( - /// |app| { - /// Cors::for_app(app) // <- Construct CORS builder - /// .allowed_origin("https://www.rust-lang.org/") - /// .resource("/resource", |r| { // register resource - /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); - /// }) - /// .register() - /// }, // construct CORS and return application instance - /// ); - /// } - /// ``` - pub fn for_app(app: App) -> CorsBuilder { - CorsBuilder { - cors: Some(Inner { - origins: AllOrSome::All, - origins_str: None, - methods: HashSet::new(), - headers: AllOrSome::All, - expose_hdrs: None, - max_age: None, - preflight: true, - send_wildcard: false, - supports_credentials: false, - vary_header: true, - }), - methods: false, - error: None, - expose_hdrs: HashSet::new(), - resources: Vec::new(), - app: Some(app), - } - } - - /// This method register cors middleware with resource and - /// adds route for *OPTIONS* preflight requests. - /// - /// It is possible to register *Cors* middleware with - /// `Resource::middleware()` method, but in that case *Cors* - /// middleware wont be able to handle *OPTIONS* requests. - pub fn register(self, resource: &mut Resource) { - resource - .method(Method::OPTIONS) - .h(|_: &_| HttpResponse::Ok()); - resource.middleware(self); - } - - fn validate_origin(&self, req: &Request) -> Result<(), CorsError> { - if let Some(hdr) = req.headers().get(header::ORIGIN) { - if let Ok(origin) = hdr.to_str() { - return match self.inner.origins { - AllOrSome::All => Ok(()), - AllOrSome::Some(ref allowed_origins) => allowed_origins - .get(origin) - .and_then(|_| Some(())) - .ok_or_else(|| CorsError::OriginNotAllowed), - }; - } - Err(CorsError::BadOrigin) - } else { - return match self.inner.origins { - AllOrSome::All => Ok(()), - _ => Err(CorsError::MissingOrigin), - }; - } - } - - fn validate_allowed_method(&self, req: &Request) -> Result<(), CorsError> { - if let Some(hdr) = req.headers().get(header::ACCESS_CONTROL_REQUEST_METHOD) { - if let Ok(meth) = hdr.to_str() { - if let Ok(method) = Method::try_from(meth) { - return self - .inner - .methods - .get(&method) - .and_then(|_| Some(())) - .ok_or_else(|| CorsError::MethodNotAllowed); - } - } - Err(CorsError::BadRequestMethod) - } else { - Err(CorsError::MissingRequestMethod) - } - } - - fn validate_allowed_headers(&self, req: &Request) -> Result<(), CorsError> { - match self.inner.headers { - AllOrSome::All => Ok(()), - AllOrSome::Some(ref allowed_headers) => { - if let Some(hdr) = - req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) - { - if let Ok(headers) = hdr.to_str() { - let mut hdrs = HashSet::new(); - for hdr in headers.split(',') { - match HeaderName::try_from(hdr.trim()) { - Ok(hdr) => hdrs.insert(hdr), - Err(_) => return Err(CorsError::BadRequestHeaders), - }; - } - - if !hdrs.is_empty() && !hdrs.is_subset(allowed_headers) { - return Err(CorsError::HeadersNotAllowed); - } - return Ok(()); - } - Err(CorsError::BadRequestHeaders) - } else { - Err(CorsError::MissingRequestHeaders) - } - } - } - } -} - -impl Middleware for Cors { - fn start(&self, req: &HttpRequest) -> Result { - if self.inner.preflight && Method::OPTIONS == *req.method() { - self.validate_origin(req)?; - self.validate_allowed_method(&req)?; - self.validate_allowed_headers(&req)?; - - // allowed headers - let headers = if let Some(headers) = self.inner.headers.as_ref() { - Some( - HeaderValue::try_from( - &headers - .iter() - .fold(String::new(), |s, v| s + "," + v.as_str()) - .as_str()[1..], - ).unwrap(), - ) - } else if let Some(hdr) = - req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) - { - Some(hdr.clone()) - } else { - None - }; - - Ok(Started::Response( - HttpResponse::Ok() - .if_some(self.inner.max_age.as_ref(), |max_age, resp| { - let _ = resp.header( - header::ACCESS_CONTROL_MAX_AGE, - format!("{}", max_age).as_str(), - ); - }).if_some(headers, |headers, resp| { - let _ = - resp.header(header::ACCESS_CONTROL_ALLOW_HEADERS, headers); - }).if_true(self.inner.origins.is_all(), |resp| { - if self.inner.send_wildcard { - resp.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - } else { - let origin = req.headers().get(header::ORIGIN).unwrap(); - resp.header( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - origin.clone(), - ); - } - }).if_true(self.inner.origins.is_some(), |resp| { - resp.header( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - self.inner.origins_str.as_ref().unwrap().clone(), - ); - }).if_true(self.inner.supports_credentials, |resp| { - resp.header(header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - }).header( - header::ACCESS_CONTROL_ALLOW_METHODS, - &self - .inner - .methods - .iter() - .fold(String::new(), |s, v| s + "," + v.as_str()) - .as_str()[1..], - ).finish(), - )) - } else { - // Only check requests with a origin header. - if req.headers().contains_key(header::ORIGIN) { - self.validate_origin(req)?; - } - - Ok(Started::Done) - } - } - - fn response( - &self, req: &HttpRequest, mut resp: HttpResponse, - ) -> Result { - match self.inner.origins { - AllOrSome::All => { - if self.inner.send_wildcard { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - HeaderValue::from_static("*"), - ); - } else if let Some(origin) = req.headers().get(header::ORIGIN) { - resp.headers_mut() - .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone()); - } - } - AllOrSome::Some(_) => { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - self.inner.origins_str.as_ref().unwrap().clone(), - ); - } - } - - if let Some(ref expose) = self.inner.expose_hdrs { - resp.headers_mut().insert( - header::ACCESS_CONTROL_EXPOSE_HEADERS, - HeaderValue::try_from(expose.as_str()).unwrap(), - ); - } - if self.inner.supports_credentials { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_CREDENTIALS, - HeaderValue::from_static("true"), - ); - } - if self.inner.vary_header { - let value = if let Some(hdr) = resp.headers_mut().get(header::VARY) { - let mut val: Vec = Vec::with_capacity(hdr.as_bytes().len() + 8); - val.extend(hdr.as_bytes()); - val.extend(b", Origin"); - HeaderValue::try_from(&val[..]).unwrap() - } else { - HeaderValue::from_static("Origin") - }; - resp.headers_mut().insert(header::VARY, value); - } - Ok(Response::Done(resp)) - } -} - -/// Structure that follows the builder pattern for building `Cors` middleware -/// structs. -/// -/// To construct a cors: -/// -/// 1. Call [`Cors::build`](struct.Cors.html#method.build) to start building. -/// 2. Use any of the builder methods to set fields in the backend. -/// 3. Call [finish](struct.Cors.html#method.finish) to retrieve the -/// constructed backend. -/// -/// # Example -/// -/// ```rust -/// # extern crate http; -/// # extern crate actix_web; -/// use actix_web::middleware::cors; -/// use http::header; -/// -/// # fn main() { -/// let cors = cors::Cors::build() -/// .allowed_origin("https://www.rust-lang.org/") -/// .allowed_methods(vec!["GET", "POST"]) -/// .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) -/// .allowed_header(header::CONTENT_TYPE) -/// .max_age(3600) -/// .finish(); -/// # } -/// ``` -pub struct CorsBuilder { - cors: Option, - methods: bool, - error: Option, - expose_hdrs: HashSet, - resources: Vec>, - app: Option>, -} - -fn cors<'a>( - parts: &'a mut Option, err: &Option, -) -> Option<&'a mut Inner> { - if err.is_some() { - return None; - } - parts.as_mut() -} - -impl CorsBuilder { - /// Add an origin that are allowed to make requests. - /// Will be verified against the `Origin` request header. - /// - /// When `All` is set, and `send_wildcard` is set, "*" will be sent in - /// the `Access-Control-Allow-Origin` response header. Otherwise, the - /// client's `Origin` request header will be echoed back in the - /// `Access-Control-Allow-Origin` response header. - /// - /// When `Some` is set, the client's `Origin` request header will be - /// checked in a case-sensitive manner. - /// - /// This is the `list of origins` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// Defaults to `All`. - /// - /// Builder panics if supplied origin is not valid uri. - pub fn allowed_origin(&mut self, origin: &str) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - match Uri::try_from(origin) { - Ok(_) => { - if cors.origins.is_all() { - cors.origins = AllOrSome::Some(HashSet::new()); - } - if let AllOrSome::Some(ref mut origins) = cors.origins { - origins.insert(origin.to_owned()); - } - } - Err(e) => { - self.error = Some(e.into()); - } - } - } - self - } - - /// Set a list of methods which the allowed origins are allowed to access - /// for requests. - /// - /// This is the `list of methods` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// Defaults to `[GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE]` - pub fn allowed_methods(&mut self, methods: U) -> &mut CorsBuilder - where - U: IntoIterator, - Method: HttpTryFrom, - { - self.methods = true; - if let Some(cors) = cors(&mut self.cors, &self.error) { - for m in methods { - match Method::try_from(m) { - Ok(method) => { - cors.methods.insert(method); - } - Err(e) => { - self.error = Some(e.into()); - break; - } - } - } - } - self - } - - /// Set an allowed header - pub fn allowed_header(&mut self, header: H) -> &mut CorsBuilder - where - HeaderName: HttpTryFrom, - { - if let Some(cors) = cors(&mut self.cors, &self.error) { - match HeaderName::try_from(header) { - Ok(method) => { - if cors.headers.is_all() { - cors.headers = AllOrSome::Some(HashSet::new()); - } - if let AllOrSome::Some(ref mut headers) = cors.headers { - headers.insert(method); - } - } - Err(e) => self.error = Some(e.into()), - } - } - self - } - - /// Set a list of header field names which can be used when - /// this resource is accessed by allowed origins. - /// - /// If `All` is set, whatever is requested by the client in - /// `Access-Control-Request-Headers` will be echoed back in the - /// `Access-Control-Allow-Headers` header. - /// - /// This is the `list of headers` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// Defaults to `All`. - pub fn allowed_headers(&mut self, headers: U) -> &mut CorsBuilder - where - U: IntoIterator, - HeaderName: HttpTryFrom, - { - if let Some(cors) = cors(&mut self.cors, &self.error) { - for h in headers { - match HeaderName::try_from(h) { - Ok(method) => { - if cors.headers.is_all() { - cors.headers = AllOrSome::Some(HashSet::new()); - } - if let AllOrSome::Some(ref mut headers) = cors.headers { - headers.insert(method); - } - } - Err(e) => { - self.error = Some(e.into()); - break; - } - } - } - } - self - } - - /// Set a list of headers which are safe to expose to the API of a CORS API - /// specification. This corresponds to the - /// `Access-Control-Expose-Headers` response header. - /// - /// This is the `list of exposed headers` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// This defaults to an empty set. - pub fn expose_headers(&mut self, headers: U) -> &mut CorsBuilder - where - U: IntoIterator, - HeaderName: HttpTryFrom, - { - for h in headers { - match HeaderName::try_from(h) { - Ok(method) => { - self.expose_hdrs.insert(method); - } - Err(e) => { - self.error = Some(e.into()); - break; - } - } - } - self - } - - /// Set a maximum time for which this CORS request maybe cached. - /// This value is set as the `Access-Control-Max-Age` header. - /// - /// This defaults to `None` (unset). - pub fn max_age(&mut self, max_age: usize) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.max_age = Some(max_age) - } - self - } - - /// Set a wildcard origins - /// - /// If send wildcard is set and the `allowed_origins` parameter is `All`, a - /// wildcard `Access-Control-Allow-Origin` response header is sent, - /// rather than the request’s `Origin` header. - /// - /// This is the `supports credentials flag` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// This **CANNOT** be used in conjunction with `allowed_origins` set to - /// `All` and `allow_credentials` set to `true`. Depending on the mode - /// of usage, this will either result in an `Error:: - /// CredentialsWithWildcardOrigin` error during actix launch or runtime. - /// - /// Defaults to `false`. - pub fn send_wildcard(&mut self) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.send_wildcard = true - } - self - } - - /// Allows users to make authenticated requests - /// - /// If true, injects the `Access-Control-Allow-Credentials` header in - /// responses. This allows cookies and credentials to be submitted - /// across domains. - /// - /// This option cannot be used in conjunction with an `allowed_origin` set - /// to `All` and `send_wildcards` set to `true`. - /// - /// Defaults to `false`. - /// - /// Builder panics if credentials are allowed, but the Origin is set to "*". - /// This is not allowed by W3C - pub fn supports_credentials(&mut self) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.supports_credentials = true - } - self - } - - /// Disable `Vary` header support. - /// - /// When enabled the header `Vary: Origin` will be returned as per the W3 - /// implementation guidelines. - /// - /// Setting this header when the `Access-Control-Allow-Origin` is - /// dynamically generated (e.g. when there is more than one allowed - /// origin, and an Origin than '*' is returned) informs CDNs and other - /// caches that the CORS headers are dynamic, and cannot be cached. - /// - /// By default `vary` header support is enabled. - pub fn disable_vary_header(&mut self) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.vary_header = false - } - self - } - - /// Disable *preflight* request support. - /// - /// When enabled cors middleware automatically handles *OPTIONS* request. - /// This is useful application level middleware. - /// - /// By default *preflight* support is enabled. - pub fn disable_preflight(&mut self) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.preflight = false - } - self - } - - /// Configure resource for a specific path. - /// - /// This is similar to a `App::resource()` method. Except, cors middleware - /// get registered for the resource. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::middleware::cors::Cors; - /// use actix_web::{http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().configure( - /// |app| { - /// Cors::for_app(app) // <- Construct CORS builder - /// .allowed_origin("https://www.rust-lang.org/") - /// .allowed_methods(vec!["GET", "POST"]) - /// .allowed_header(http::header::CONTENT_TYPE) - /// .max_age(3600) - /// .resource("/resource1", |r| { // register resource - /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); - /// }) - /// .resource("/resource2", |r| { // register another resource - /// r.method(http::Method::HEAD) - /// .f(|_| HttpResponse::MethodNotAllowed()); - /// }) - /// .register() - /// }, // construct CORS and return application instance - /// ); - /// } - /// ``` - pub fn resource(&mut self, path: &str, f: F) -> &mut CorsBuilder - where - F: FnOnce(&mut Resource) -> R + 'static, - { - // add resource handler - let mut resource = Resource::new(ResourceDef::new(path)); - f(&mut resource); - - self.resources.push(resource); - self - } - - fn construct(&mut self) -> Cors { - if !self.methods { - self.allowed_methods(vec![ - Method::GET, - Method::HEAD, - Method::POST, - Method::OPTIONS, - Method::PUT, - Method::PATCH, - Method::DELETE, - ]); - } - - if let Some(e) = self.error.take() { - panic!("{}", e); - } - - let mut cors = self.cors.take().expect("cannot reuse CorsBuilder"); - - if cors.supports_credentials && cors.send_wildcard && cors.origins.is_all() { - panic!("Credentials are allowed, but the Origin is set to \"*\""); - } - - if let AllOrSome::Some(ref origins) = cors.origins { - let s = origins - .iter() - .fold(String::new(), |s, v| format!("{}, {}", s, v)); - cors.origins_str = Some(HeaderValue::try_from(&s[2..]).unwrap()); - } - - if !self.expose_hdrs.is_empty() { - cors.expose_hdrs = Some( - self.expose_hdrs - .iter() - .fold(String::new(), |s, v| format!("{}, {}", s, v.as_str()))[2..] - .to_owned(), - ); - } - Cors { - inner: Rc::new(cors), - } - } - - /// Finishes building and returns the built `Cors` instance. - /// - /// This method panics in case of any configuration error. - pub fn finish(&mut self) -> Cors { - if !self.resources.is_empty() { - panic!( - "CorsBuilder::resource() was used, - to construct CORS `.register(app)` method should be used" - ); - } - self.construct() - } - - /// Finishes building Cors middleware and register middleware for - /// application - /// - /// This method panics in case of any configuration error or if non of - /// resources are registered. - pub fn register(&mut self) -> App { - if self.resources.is_empty() { - panic!("No resources are registered."); - } - - let cors = self.construct(); - let mut app = self - .app - .take() - .expect("CorsBuilder has to be constructed with Cors::for_app(app)"); - - // register resources - for mut resource in self.resources.drain(..) { - cors.clone().register(&mut resource); - app.register_resource(resource); - } - - app - } -} - -#[cfg(test)] -mod tests { - use super::*; - use test::{self, TestRequest}; - - impl Started { - fn is_done(&self) -> bool { - match *self { - Started::Done => true, - _ => false, - } - } - fn response(self) -> HttpResponse { - match self { - Started::Response(resp) => resp, - _ => panic!(), - } - } - } - impl Response { - fn response(self) -> HttpResponse { - match self { - Response::Done(resp) => resp, - _ => panic!(), - } - } - } - - #[test] - #[should_panic(expected = "Credentials are allowed, but the Origin is set to")] - fn cors_validates_illegal_allow_credentials() { - Cors::build() - .supports_credentials() - .send_wildcard() - .finish(); - } - - #[test] - #[should_panic(expected = "No resources are registered")] - fn no_resource() { - Cors::build() - .supports_credentials() - .send_wildcard() - .register(); - } - - #[test] - #[should_panic(expected = "Cors::for_app(app)")] - fn no_resource2() { - Cors::build() - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .register(); - } - - #[test] - fn validate_origin_allows_all_origins() { - let cors = Cors::default(); - let req = TestRequest::with_header("Origin", "https://www.example.com").finish(); - - assert!(cors.start(&req).ok().unwrap().is_done()) - } - - #[test] - fn test_preflight() { - let mut cors = Cors::build() - .send_wildcard() - .max_age(3600) - .allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST]) - .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) - .allowed_header(header::CONTENT_TYPE) - .finish(); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .method(Method::OPTIONS) - .finish(); - - assert!(cors.start(&req).is_err()); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .header(header::ACCESS_CONTROL_REQUEST_METHOD, "put") - .method(Method::OPTIONS) - .finish(); - - assert!(cors.start(&req).is_err()); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST") - .header( - header::ACCESS_CONTROL_REQUEST_HEADERS, - "AUTHORIZATION,ACCEPT", - ).method(Method::OPTIONS) - .finish(); - - let resp = cors.start(&req).unwrap().response(); - assert_eq!( - &b"*"[..], - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .as_bytes() - ); - assert_eq!( - &b"3600"[..], - resp.headers() - .get(header::ACCESS_CONTROL_MAX_AGE) - .unwrap() - .as_bytes() - ); - //assert_eq!( - // &b"authorization,accept,content-type"[..], - // resp.headers().get(header::ACCESS_CONTROL_ALLOW_HEADERS).unwrap(). - // as_bytes()); assert_eq!( - // &b"POST,GET,OPTIONS"[..], - // resp.headers().get(header::ACCESS_CONTROL_ALLOW_METHODS).unwrap(). - // as_bytes()); - - Rc::get_mut(&mut cors.inner).unwrap().preflight = false; - assert!(cors.start(&req).unwrap().is_done()); - } - - // #[test] - // #[should_panic(expected = "MissingOrigin")] - // fn test_validate_missing_origin() { - // let cors = Cors::build() - // .allowed_origin("https://www.example.com") - // .finish(); - // let mut req = HttpRequest::default(); - // cors.start(&req).unwrap(); - // } - - #[test] - #[should_panic(expected = "OriginNotAllowed")] - fn test_validate_not_allowed_origin() { - let cors = Cors::build() - .allowed_origin("https://www.example.com") - .finish(); - - let req = TestRequest::with_header("Origin", "https://www.unknown.com") - .method(Method::GET) - .finish(); - cors.start(&req).unwrap(); - } - - #[test] - fn test_validate_origin() { - let cors = Cors::build() - .allowed_origin("https://www.example.com") - .finish(); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .method(Method::GET) - .finish(); - - assert!(cors.start(&req).unwrap().is_done()); - } - - #[test] - fn test_no_origin_response() { - let cors = Cors::build().finish(); - - let req = TestRequest::default().method(Method::GET).finish(); - let resp: HttpResponse = HttpResponse::Ok().into(); - let resp = cors.response(&req, resp).unwrap().response(); - assert!( - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .is_none() - ); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .method(Method::OPTIONS) - .finish(); - let resp = cors.response(&req, resp).unwrap().response(); - assert_eq!( - &b"https://www.example.com"[..], - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .as_bytes() - ); - } - - #[test] - fn test_response() { - let exposed_headers = vec![header::AUTHORIZATION, header::ACCEPT]; - let cors = Cors::build() - .send_wildcard() - .disable_preflight() - .max_age(3600) - .allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST]) - .allowed_headers(exposed_headers.clone()) - .expose_headers(exposed_headers.clone()) - .allowed_header(header::CONTENT_TYPE) - .finish(); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .method(Method::OPTIONS) - .finish(); - - let resp: HttpResponse = HttpResponse::Ok().into(); - let resp = cors.response(&req, resp).unwrap().response(); - assert_eq!( - &b"*"[..], - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .as_bytes() - ); - assert_eq!( - &b"Origin"[..], - resp.headers().get(header::VARY).unwrap().as_bytes() - ); - - { - let headers = resp - .headers() - .get(header::ACCESS_CONTROL_EXPOSE_HEADERS) - .unwrap() - .to_str() - .unwrap() - .split(',') - .map(|s| s.trim()) - .collect::>(); - - for h in exposed_headers { - assert!(headers.contains(&h.as_str())); - } - } - - let resp: HttpResponse = - HttpResponse::Ok().header(header::VARY, "Accept").finish(); - let resp = cors.response(&req, resp).unwrap().response(); - assert_eq!( - &b"Accept, Origin"[..], - resp.headers().get(header::VARY).unwrap().as_bytes() - ); - - let cors = Cors::build() - .disable_vary_header() - .allowed_origin("https://www.example.com") - .allowed_origin("https://www.google.com") - .finish(); - let resp: HttpResponse = HttpResponse::Ok().into(); - let resp = cors.response(&req, resp).unwrap().response(); - - let origins_str = resp - .headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .to_str() - .unwrap(); - - if origins_str.starts_with("https://www.example.com") { - assert_eq!( - "https://www.example.com, https://www.google.com", - origins_str - ); - } else { - assert_eq!( - "https://www.google.com, https://www.example.com", - origins_str - ); - } - } - - #[test] - fn cors_resource() { - let mut srv = test::TestServer::with_factory(|| { - App::new().configure(|app| { - Cors::for_app(app) - .allowed_origin("https://www.example.com") - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .register() - }) - }); - - let request = srv - .get() - .uri(srv.url("/test")) - .header("ORIGIN", "https://www.example2.com") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - let request = srv - .get() - .uri(srv.url("/test")) - .header("ORIGIN", "https://www.example.com") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - } -} diff --git a/src/middleware/csrf.rs b/src/middleware/csrf.rs deleted file mode 100644 index 02cd150d5..000000000 --- a/src/middleware/csrf.rs +++ /dev/null @@ -1,275 +0,0 @@ -//! A filter for cross-site request forgery (CSRF). -//! -//! This middleware is stateless and [based on request -//! headers](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Verifying_Same_Origin_with_Standard_Headers). -//! -//! By default requests are allowed only if one of these is true: -//! -//! * The request method is safe (`GET`, `HEAD`, `OPTIONS`). It is the -//! applications responsibility to ensure these methods cannot be used to -//! execute unwanted actions. Note that upgrade requests for websockets are -//! also considered safe. -//! * The `Origin` header (added automatically by the browser) matches one -//! of the allowed origins. -//! * There is no `Origin` header but the `Referer` header matches one of -//! the allowed origins. -//! -//! Use [`CsrfFilter::allow_xhr()`](struct.CsrfFilter.html#method.allow_xhr) -//! if you want to allow requests with unprotected methods via -//! [CORS](../cors/struct.Cors.html). -//! -//! # Example -//! -//! ``` -//! # extern crate actix_web; -//! use actix_web::middleware::csrf; -//! use actix_web::{http, App, HttpRequest, HttpResponse}; -//! -//! fn handle_post(_: &HttpRequest) -> &'static str { -//! "This action should only be triggered with requests from the same site" -//! } -//! -//! fn main() { -//! let app = App::new() -//! .middleware( -//! csrf::CsrfFilter::new().allowed_origin("https://www.example.com"), -//! ) -//! .resource("/", |r| { -//! r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -//! r.method(http::Method::POST).f(handle_post); -//! }) -//! .finish(); -//! } -//! ``` -//! -//! In this example the entire application is protected from CSRF. - -use std::borrow::Cow; -use std::collections::HashSet; - -use bytes::Bytes; -use error::{ResponseError, Result}; -use http::{header, HeaderMap, HttpTryFrom, Uri}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Started}; -use server::Request; - -/// Potential cross-site request forgery detected. -#[derive(Debug, Fail)] -pub enum CsrfError { - /// The HTTP request header `Origin` was required but not provided. - #[fail(display = "Origin header required")] - MissingOrigin, - /// The HTTP request header `Origin` could not be parsed correctly. - #[fail(display = "Could not parse Origin header")] - BadOrigin, - /// The cross-site request was denied. - #[fail(display = "Cross-site request denied")] - CsrDenied, -} - -impl ResponseError for CsrfError { - fn error_response(&self) -> HttpResponse { - HttpResponse::Forbidden().body(self.to_string()) - } -} - -fn uri_origin(uri: &Uri) -> Option { - match (uri.scheme_part(), uri.host(), uri.port()) { - (Some(scheme), Some(host), Some(port)) => { - Some(format!("{}://{}:{}", scheme, host, port)) - } - (Some(scheme), Some(host), None) => Some(format!("{}://{}", scheme, host)), - _ => None, - } -} - -fn origin(headers: &HeaderMap) -> Option, CsrfError>> { - headers - .get(header::ORIGIN) - .map(|origin| { - origin - .to_str() - .map_err(|_| CsrfError::BadOrigin) - .map(|o| o.into()) - }).or_else(|| { - headers.get(header::REFERER).map(|referer| { - Uri::try_from(Bytes::from(referer.as_bytes())) - .ok() - .as_ref() - .and_then(uri_origin) - .ok_or(CsrfError::BadOrigin) - .map(|o| o.into()) - }) - }) -} - -/// A middleware that filters cross-site requests. -/// -/// To construct a CSRF filter: -/// -/// 1. Call [`CsrfFilter::build`](struct.CsrfFilter.html#method.build) to -/// start building. -/// 2. [Add](struct.CsrfFilterBuilder.html#method.allowed_origin) allowed -/// origins. -/// 3. Call [finish](struct.CsrfFilterBuilder.html#method.finish) to retrieve -/// the constructed filter. -/// -/// # Example -/// -/// ``` -/// use actix_web::middleware::csrf; -/// use actix_web::App; -/// -/// # fn main() { -/// let app = App::new() -/// .middleware(csrf::CsrfFilter::new().allowed_origin("https://www.example.com")); -/// # } -/// ``` -#[derive(Default)] -pub struct CsrfFilter { - origins: HashSet, - allow_xhr: bool, - allow_missing_origin: bool, - allow_upgrade: bool, -} - -impl CsrfFilter { - /// Start building a `CsrfFilter`. - pub fn new() -> CsrfFilter { - CsrfFilter { - origins: HashSet::new(), - allow_xhr: false, - allow_missing_origin: false, - allow_upgrade: false, - } - } - - /// Add an origin that is allowed to make requests. Will be verified - /// against the `Origin` request header. - pub fn allowed_origin>(mut self, origin: T) -> CsrfFilter { - self.origins.insert(origin.into()); - self - } - - /// Allow all requests with an `X-Requested-With` header. - /// - /// A cross-site attacker should not be able to send requests with custom - /// headers unless a CORS policy whitelists them. Therefore it should be - /// safe to allow requests with an `X-Requested-With` header (added - /// automatically by many JavaScript libraries). - /// - /// This is disabled by default, because in Safari it is possible to - /// circumvent this using redirects and Flash. - /// - /// Use this method to enable more lax filtering. - pub fn allow_xhr(mut self) -> CsrfFilter { - self.allow_xhr = true; - self - } - - /// Allow requests if the expected `Origin` header is missing (and - /// there is no `Referer` to fall back on). - /// - /// The filter is conservative by default, but it should be safe to allow - /// missing `Origin` headers because a cross-site attacker cannot prevent - /// the browser from sending `Origin` on unprotected requests. - pub fn allow_missing_origin(mut self) -> CsrfFilter { - self.allow_missing_origin = true; - self - } - - /// Allow cross-site upgrade requests (for example to open a WebSocket). - pub fn allow_upgrade(mut self) -> CsrfFilter { - self.allow_upgrade = true; - self - } - - fn validate(&self, req: &Request) -> Result<(), CsrfError> { - let is_upgrade = req.headers().contains_key(header::UPGRADE); - let is_safe = req.method().is_safe() && (self.allow_upgrade || !is_upgrade); - - if is_safe || (self.allow_xhr && req.headers().contains_key("x-requested-with")) - { - Ok(()) - } else if let Some(header) = origin(req.headers()) { - match header { - Ok(ref origin) if self.origins.contains(origin.as_ref()) => Ok(()), - Ok(_) => Err(CsrfError::CsrDenied), - Err(err) => Err(err), - } - } else if self.allow_missing_origin { - Ok(()) - } else { - Err(CsrfError::MissingOrigin) - } - } -} - -impl Middleware for CsrfFilter { - fn start(&self, req: &HttpRequest) -> Result { - self.validate(req)?; - Ok(Started::Done) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use http::Method; - use test::TestRequest; - - #[test] - fn test_safe() { - let csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); - - let req = TestRequest::with_header("Origin", "https://www.w3.org") - .method(Method::HEAD) - .finish(); - - assert!(csrf.start(&req).is_ok()); - } - - #[test] - fn test_csrf() { - let csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); - - let req = TestRequest::with_header("Origin", "https://www.w3.org") - .method(Method::POST) - .finish(); - - assert!(csrf.start(&req).is_err()); - } - - #[test] - fn test_referer() { - let csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); - - let req = TestRequest::with_header( - "Referer", - "https://www.example.com/some/path?query=param", - ).method(Method::POST) - .finish(); - - assert!(csrf.start(&req).is_ok()); - } - - #[test] - fn test_upgrade() { - let strict_csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); - - let lax_csrf = CsrfFilter::new() - .allowed_origin("https://www.example.com") - .allow_upgrade(); - - let req = TestRequest::with_header("Origin", "https://cswsh.com") - .header("Connection", "Upgrade") - .header("Upgrade", "websocket") - .method(Method::GET) - .finish(); - - assert!(strict_csrf.start(&req).is_err()); - assert!(lax_csrf.start(&req).is_ok()); - } -} diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs deleted file mode 100644 index d980a2503..000000000 --- a/src/middleware/defaultheaders.rs +++ /dev/null @@ -1,120 +0,0 @@ -//! Default response headers -use http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; -use http::{HeaderMap, HttpTryFrom}; - -use error::Result; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response}; - -/// `Middleware` for setting default response headers. -/// -/// This middleware does not set header if response headers already contains it. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{http, middleware, App, HttpResponse}; -/// -/// fn main() { -/// let app = App::new() -/// .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) -/// .resource("/test", |r| { -/// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -/// r.method(http::Method::HEAD) -/// .f(|_| HttpResponse::MethodNotAllowed()); -/// }) -/// .finish(); -/// } -/// ``` -pub struct DefaultHeaders { - ct: bool, - headers: HeaderMap, -} - -impl Default for DefaultHeaders { - fn default() -> Self { - DefaultHeaders { - ct: false, - headers: HeaderMap::new(), - } - } -} - -impl DefaultHeaders { - /// Construct `DefaultHeaders` middleware. - pub fn new() -> DefaultHeaders { - DefaultHeaders::default() - } - - /// Set a header. - #[inline] - #[cfg_attr(feature = "cargo-clippy", allow(clippy::match_wild_err_arm))] - pub fn header(mut self, key: K, value: V) -> Self - where - HeaderName: HttpTryFrom, - HeaderValue: HttpTryFrom, - { - match HeaderName::try_from(key) { - Ok(key) => match HeaderValue::try_from(value) { - Ok(value) => { - self.headers.append(key, value); - } - Err(_) => panic!("Can not create header value"), - }, - Err(_) => panic!("Can not create header name"), - } - self - } - - /// Set *CONTENT-TYPE* header if response does not contain this header. - pub fn content_type(mut self) -> Self { - self.ct = true; - self - } -} - -impl Middleware for DefaultHeaders { - fn response(&self, _: &HttpRequest, mut resp: HttpResponse) -> Result { - for (key, value) in self.headers.iter() { - if !resp.headers().contains_key(key) { - resp.headers_mut().insert(key, value.clone()); - } - } - // default content-type - if self.ct && !resp.headers().contains_key(CONTENT_TYPE) { - resp.headers_mut().insert( - CONTENT_TYPE, - HeaderValue::from_static("application/octet-stream"), - ); - } - Ok(Response::Done(resp)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use http::header::CONTENT_TYPE; - use test::TestRequest; - - #[test] - fn test_default_headers() { - let mw = DefaultHeaders::new().header(CONTENT_TYPE, "0001"); - - let req = TestRequest::default().finish(); - - let resp = HttpResponse::Ok().finish(); - let resp = match mw.response(&req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - - let resp = HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish(); - let resp = match mw.response(&req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); - } -} diff --git a/src/middleware/errhandlers.rs b/src/middleware/errhandlers.rs deleted file mode 100644 index c7d19d334..000000000 --- a/src/middleware/errhandlers.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::collections::HashMap; - -use error::Result; -use http::StatusCode; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response}; - -type ErrorHandler = Fn(&HttpRequest, HttpResponse) -> Result; - -/// `Middleware` for allowing custom handlers for responses. -/// -/// You can use `ErrorHandlers::handler()` method to register a custom error -/// handler for specific status code. You can modify existing response or -/// create completely new one. -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::{ErrorHandlers, Response}; -/// use actix_web::{http, App, HttpRequest, HttpResponse, Result}; -/// -/// fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { -/// let mut builder = resp.into_builder(); -/// builder.header(http::header::CONTENT_TYPE, "application/json"); -/// Ok(Response::Done(builder.into())) -/// } -/// -/// fn main() { -/// let app = App::new() -/// .middleware( -/// ErrorHandlers::new() -/// .handler(http::StatusCode::INTERNAL_SERVER_ERROR, render_500), -/// ) -/// .resource("/test", |r| { -/// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -/// r.method(http::Method::HEAD) -/// .f(|_| HttpResponse::MethodNotAllowed()); -/// }) -/// .finish(); -/// } -/// ``` -pub struct ErrorHandlers { - handlers: HashMap>>, -} - -impl Default for ErrorHandlers { - fn default() -> Self { - ErrorHandlers { - handlers: HashMap::new(), - } - } -} - -impl ErrorHandlers { - /// Construct new `ErrorHandlers` instance - pub fn new() -> Self { - ErrorHandlers::default() - } - - /// Register error handler for specified status code - pub fn handler(mut self, status: StatusCode, handler: F) -> Self - where - F: Fn(&HttpRequest, HttpResponse) -> Result + 'static, - { - self.handlers.insert(status, Box::new(handler)); - self - } -} - -impl Middleware for ErrorHandlers { - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(handler) = self.handlers.get(&resp.status()) { - handler(req, resp) - } else { - Ok(Response::Done(resp)) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use error::{Error, ErrorInternalServerError}; - use http::header::CONTENT_TYPE; - use http::StatusCode; - use httpmessage::HttpMessage; - use middleware::Started; - use test::{self, TestRequest}; - - fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { - let mut builder = resp.into_builder(); - builder.header(CONTENT_TYPE, "0001"); - Ok(Response::Done(builder.into())) - } - - #[test] - fn test_handler() { - let mw = - ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); - - let mut req = TestRequest::default().finish(); - let resp = HttpResponse::InternalServerError().finish(); - let resp = match mw.response(&mut req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - - let resp = HttpResponse::Ok().finish(); - let resp = match mw.response(&mut req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert!(!resp.headers().contains_key(CONTENT_TYPE)); - } - - struct MiddlewareOne; - - impl Middleware for MiddlewareOne { - fn start(&self, _: &HttpRequest) -> Result { - Err(ErrorInternalServerError("middleware error")) - } - } - - #[test] - fn test_middleware_start_error() { - let mut srv = test::TestServer::new(move |app| { - app.middleware( - ErrorHandlers::new() - .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), - ).middleware(MiddlewareOne) - .handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "0001"); - } -} diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs deleted file mode 100644 index d890bebef..000000000 --- a/src/middleware/identity.rs +++ /dev/null @@ -1,387 +0,0 @@ -//! Request identity service for Actix applications. -//! -//! [**IdentityService**](struct.IdentityService.html) middleware can be -//! used with different policies types to store identity information. -//! -//! By default, only cookie identity policy is implemented. Other backend -//! implementations can be added separately. -//! -//! [**CookieIdentityPolicy**](struct.CookieIdentityPolicy.html) -//! uses cookies as identity storage. -//! -//! To access current request identity -//! [**RequestIdentity**](trait.RequestIdentity.html) should be used. -//! *HttpRequest* implements *RequestIdentity* trait. -//! -//! ```rust -//! use actix_web::middleware::identity::RequestIdentity; -//! use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; -//! use actix_web::*; -//! -//! fn index(req: HttpRequest) -> Result { -//! // access request identity -//! if let Some(id) = req.identity() { -//! Ok(format!("Welcome! {}", id)) -//! } else { -//! Ok("Welcome Anonymous!".to_owned()) -//! } -//! } -//! -//! fn login(mut req: HttpRequest) -> HttpResponse { -//! req.remember("User1".to_owned()); // <- remember identity -//! HttpResponse::Ok().finish() -//! } -//! -//! fn logout(mut req: HttpRequest) -> HttpResponse { -//! req.forget(); // <- remove identity -//! HttpResponse::Ok().finish() -//! } -//! -//! fn main() { -//! let app = App::new().middleware(IdentityService::new( -//! // <- create identity middleware -//! CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend -//! .name("auth-cookie") -//! .secure(false), -//! )); -//! } -//! ``` -use std::rc::Rc; - -use cookie::{Cookie, CookieJar, Key}; -use futures::future::{err as FutErr, ok as FutOk, FutureResult}; -use futures::Future; -use time::Duration; - -use error::{Error, Result}; -use http::header::{self, HeaderValue}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; - -/// The helper trait to obtain your identity from a request. -/// -/// ```rust -/// use actix_web::middleware::identity::RequestIdentity; -/// use actix_web::*; -/// -/// fn index(req: HttpRequest) -> Result { -/// // access request identity -/// if let Some(id) = req.identity() { -/// Ok(format!("Welcome! {}", id)) -/// } else { -/// Ok("Welcome Anonymous!".to_owned()) -/// } -/// } -/// -/// fn login(mut req: HttpRequest) -> HttpResponse { -/// req.remember("User1".to_owned()); // <- remember identity -/// HttpResponse::Ok().finish() -/// } -/// -/// fn logout(mut req: HttpRequest) -> HttpResponse { -/// req.forget(); // <- remove identity -/// HttpResponse::Ok().finish() -/// } -/// # fn main() {} -/// ``` -pub trait RequestIdentity { - /// Return the claimed identity of the user associated request or - /// ``None`` if no identity can be found associated with the request. - fn identity(&self) -> Option; - - /// Remember identity. - fn remember(&self, identity: String); - - /// This method is used to 'forget' the current identity on subsequent - /// requests. - fn forget(&self); -} - -impl RequestIdentity for HttpRequest { - fn identity(&self) -> Option { - if let Some(id) = self.extensions().get::() { - return id.0.identity().map(|s| s.to_owned()); - } - None - } - - fn remember(&self, identity: String) { - if let Some(id) = self.extensions_mut().get_mut::() { - return id.0.as_mut().remember(identity); - } - } - - fn forget(&self) { - if let Some(id) = self.extensions_mut().get_mut::() { - return id.0.forget(); - } - } -} - -/// An identity -pub trait Identity: 'static { - /// Return the claimed identity of the user associated request or - /// ``None`` if no identity can be found associated with the request. - fn identity(&self) -> Option<&str>; - - /// Remember identity. - fn remember(&mut self, key: String); - - /// This method is used to 'forget' the current identity on subsequent - /// requests. - fn forget(&mut self); - - /// Write session to storage backend. - fn write(&mut self, resp: HttpResponse) -> Result; -} - -/// Identity policy definition. -pub trait IdentityPolicy: Sized + 'static { - /// The associated identity - type Identity: Identity; - - /// The return type of the middleware - type Future: Future; - - /// Parse the session from request and load data from a service identity. - fn from_request(&self, request: &HttpRequest) -> Self::Future; -} - -/// Request identity middleware -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; -/// use actix_web::App; -/// -/// fn main() { -/// let app = App::new().middleware(IdentityService::new( -/// // <- create identity middleware -/// CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend -/// .name("auth-cookie") -/// .secure(false), -/// )); -/// } -/// ``` -pub struct IdentityService { - backend: T, -} - -impl IdentityService { - /// Create new identity service with specified backend. - pub fn new(backend: T) -> Self { - IdentityService { backend } - } -} - -struct IdentityBox(Box); - -impl> Middleware for IdentityService { - fn start(&self, req: &HttpRequest) -> Result { - let req = req.clone(); - let fut = self.backend.from_request(&req).then(move |res| match res { - Ok(id) => { - req.extensions_mut().insert(IdentityBox(Box::new(id))); - FutOk(None) - } - Err(err) => FutErr(err), - }); - Ok(Started::Future(Box::new(fut))) - } - - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(ref mut id) = req.extensions_mut().get_mut::() { - id.0.as_mut().write(resp) - } else { - Ok(Response::Done(resp)) - } - } -} - -#[doc(hidden)] -/// Identity that uses private cookies as identity storage. -pub struct CookieIdentity { - changed: bool, - identity: Option, - inner: Rc, -} - -impl Identity for CookieIdentity { - fn identity(&self) -> Option<&str> { - self.identity.as_ref().map(|s| s.as_ref()) - } - - fn remember(&mut self, value: String) { - self.changed = true; - self.identity = Some(value); - } - - fn forget(&mut self) { - self.changed = true; - self.identity = None; - } - - fn write(&mut self, mut resp: HttpResponse) -> Result { - if self.changed { - let _ = self.inner.set_cookie(&mut resp, self.identity.take()); - } - Ok(Response::Done(resp)) - } -} - -struct CookieIdentityInner { - key: Key, - name: String, - path: String, - domain: Option, - secure: bool, - max_age: Option, -} - -impl CookieIdentityInner { - fn new(key: &[u8]) -> CookieIdentityInner { - CookieIdentityInner { - key: Key::from_master(key), - name: "actix-identity".to_owned(), - path: "/".to_owned(), - domain: None, - secure: true, - max_age: None, - } - } - - fn set_cookie(&self, resp: &mut HttpResponse, id: Option) -> Result<()> { - let some = id.is_some(); - { - let id = id.unwrap_or_else(String::new); - let mut cookie = Cookie::new(self.name.clone(), id); - cookie.set_path(self.path.clone()); - cookie.set_secure(self.secure); - cookie.set_http_only(true); - - if let Some(ref domain) = self.domain { - cookie.set_domain(domain.clone()); - } - - if let Some(max_age) = self.max_age { - cookie.set_max_age(max_age); - } - - let mut jar = CookieJar::new(); - if some { - jar.private(&self.key).add(cookie); - } else { - jar.add_original(cookie.clone()); - jar.private(&self.key).remove(cookie); - } - - for cookie in jar.delta() { - let val = HeaderValue::from_str(&cookie.to_string())?; - resp.headers_mut().append(header::SET_COOKIE, val); - } - } - - Ok(()) - } - - fn load(&self, req: &HttpRequest) -> Option { - if let Ok(cookies) = req.cookies() { - for cookie in cookies.iter() { - if cookie.name() == self.name { - let mut jar = CookieJar::new(); - jar.add_original(cookie.clone()); - - let cookie_opt = jar.private(&self.key).get(&self.name); - if let Some(cookie) = cookie_opt { - return Some(cookie.value().into()); - } - } - } - } - None - } -} - -/// Use cookies for request identity storage. -/// -/// The constructors take a key as an argument. -/// This is the private key for cookie - when this value is changed, -/// all identities are lost. The constructors will panic if the key is less -/// than 32 bytes in length. -/// -/// # Example -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; -/// use actix_web::App; -/// -/// fn main() { -/// let app = App::new().middleware(IdentityService::new( -/// // <- create identity middleware -/// CookieIdentityPolicy::new(&[0; 32]) // <- construct cookie policy -/// .domain("www.rust-lang.org") -/// .name("actix_auth") -/// .path("/") -/// .secure(true), -/// )); -/// } -/// ``` -pub struct CookieIdentityPolicy(Rc); - -impl CookieIdentityPolicy { - /// Construct new `CookieIdentityPolicy` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn new(key: &[u8]) -> CookieIdentityPolicy { - CookieIdentityPolicy(Rc::new(CookieIdentityInner::new(key))) - } - - /// Sets the `path` field in the session cookie being built. - pub fn path>(mut self, value: S) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().path = value.into(); - self - } - - /// Sets the `name` field in the session cookie being built. - pub fn name>(mut self, value: S) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().name = value.into(); - self - } - - /// Sets the `domain` field in the session cookie being built. - pub fn domain>(mut self, value: S) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); - self - } - - /// Sets the `secure` field in the session cookie being built. - /// - /// If the `secure` field is set, a cookie will only be transmitted when the - /// connection is secure - i.e. `https` - pub fn secure(mut self, value: bool) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().secure = value; - self - } - - /// Sets the `max-age` field in the session cookie being built. - pub fn max_age(mut self, value: Duration) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); - self - } -} - -impl IdentityPolicy for CookieIdentityPolicy { - type Identity = CookieIdentity; - type Future = FutureResult; - - fn from_request(&self, req: &HttpRequest) -> Self::Future { - let identity = self.0.load(req); - FutOk(CookieIdentity { - identity, - changed: false, - inner: Rc::clone(&self.0), - }) - } -} diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs deleted file mode 100644 index b7bb1bb80..000000000 --- a/src/middleware/logger.rs +++ /dev/null @@ -1,384 +0,0 @@ -//! Request logging middleware -use std::collections::HashSet; -use std::env; -use std::fmt::{self, Display, Formatter}; - -use regex::Regex; -use time; - -use error::Result; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Finished, Middleware, Started}; - -/// `Middleware` for logging request and response info to the terminal. -/// -/// `Logger` middleware uses standard log crate to log information. You should -/// enable logger for `actix_web` package to see access log. -/// ([`env_logger`](https://docs.rs/env_logger/*/env_logger/) or similar) -/// -/// ## Usage -/// -/// Create `Logger` middleware with the specified `format`. -/// Default `Logger` could be created with `default` method, it uses the -/// default format: -/// -/// ```ignore -/// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T -/// ``` -/// ```rust -/// # extern crate actix_web; -/// extern crate env_logger; -/// use actix_web::middleware::Logger; -/// use actix_web::App; -/// -/// fn main() { -/// std::env::set_var("RUST_LOG", "actix_web=info"); -/// env_logger::init(); -/// -/// let app = App::new() -/// .middleware(Logger::default()) -/// .middleware(Logger::new("%a %{User-Agent}i")) -/// .finish(); -/// } -/// ``` -/// -/// ## Format -/// -/// `%%` The percent sign -/// -/// `%a` Remote IP-address (IP-address of proxy if using reverse proxy) -/// -/// `%t` Time when the request was started to process -/// -/// `%r` First line of request -/// -/// `%s` Response status code -/// -/// `%b` Size of response in bytes, including HTTP headers -/// -/// `%T` Time taken to serve the request, in seconds with floating fraction in -/// .06f format -/// -/// `%D` Time taken to serve the request, in milliseconds -/// -/// `%{FOO}i` request.headers['FOO'] -/// -/// `%{FOO}o` response.headers['FOO'] -/// -/// `%{FOO}e` os.environ['FOO'] -/// -pub struct Logger { - format: Format, - exclude: HashSet, -} - -impl Logger { - /// Create `Logger` middleware with the specified `format`. - pub fn new(format: &str) -> Logger { - Logger { - format: Format::new(format), - exclude: HashSet::new(), - } - } - - /// Ignore and do not log access info for specified path. - pub fn exclude>(mut self, path: T) -> Self { - self.exclude.insert(path.into()); - self - } -} - -impl Default for Logger { - /// Create `Logger` middleware with format: - /// - /// ```ignore - /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T - /// ``` - fn default() -> Logger { - Logger { - format: Format::default(), - exclude: HashSet::new(), - } - } -} - -struct StartTime(time::Tm); - -impl Logger { - fn log(&self, req: &HttpRequest, resp: &HttpResponse) { - if let Some(entry_time) = req.extensions().get::() { - let render = |fmt: &mut Formatter| { - for unit in &self.format.0 { - unit.render(fmt, req, resp, entry_time.0)?; - } - Ok(()) - }; - info!("{}", FormatDisplay(&render)); - } - } -} - -impl Middleware for Logger { - fn start(&self, req: &HttpRequest) -> Result { - if !self.exclude.contains(req.path()) { - req.extensions_mut().insert(StartTime(time::now())); - } - Ok(Started::Done) - } - - fn finish(&self, req: &HttpRequest, resp: &HttpResponse) -> Finished { - self.log(req, resp); - Finished::Done - } -} - -/// A formatting style for the `Logger`, consisting of multiple -/// `FormatText`s concatenated into one line. -#[derive(Clone)] -#[doc(hidden)] -struct Format(Vec); - -impl Default for Format { - /// Return the default formatting style for the `Logger`: - fn default() -> Format { - Format::new(r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#) - } -} - -impl Format { - /// Create a `Format` from a format string. - /// - /// Returns `None` if the format string syntax is incorrect. - pub fn new(s: &str) -> Format { - trace!("Access log format: {}", s); - let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbTD]?)").unwrap(); - - let mut idx = 0; - let mut results = Vec::new(); - for cap in fmt.captures_iter(s) { - let m = cap.get(0).unwrap(); - let pos = m.start(); - if idx != pos { - results.push(FormatText::Str(s[idx..pos].to_owned())); - } - idx = m.end(); - - if let Some(key) = cap.get(2) { - results.push(match cap.get(3).unwrap().as_str() { - "i" => FormatText::RequestHeader(key.as_str().to_owned()), - "o" => FormatText::ResponseHeader(key.as_str().to_owned()), - "e" => FormatText::EnvironHeader(key.as_str().to_owned()), - _ => unreachable!(), - }) - } else { - let m = cap.get(1).unwrap(); - results.push(match m.as_str() { - "%" => FormatText::Percent, - "a" => FormatText::RemoteAddr, - "t" => FormatText::RequestTime, - "r" => FormatText::RequestLine, - "s" => FormatText::ResponseStatus, - "b" => FormatText::ResponseSize, - "T" => FormatText::Time, - "D" => FormatText::TimeMillis, - _ => FormatText::Str(m.as_str().to_owned()), - }); - } - } - if idx != s.len() { - results.push(FormatText::Str(s[idx..].to_owned())); - } - - Format(results) - } -} - -/// A string of text to be logged. This is either one of the data -/// fields supported by the `Logger`, or a custom `String`. -#[doc(hidden)] -#[derive(Debug, Clone)] -pub enum FormatText { - Str(String), - Percent, - RequestLine, - RequestTime, - ResponseStatus, - ResponseSize, - Time, - TimeMillis, - RemoteAddr, - RequestHeader(String), - ResponseHeader(String), - EnvironHeader(String), -} - -impl FormatText { - fn render( - &self, fmt: &mut Formatter, req: &HttpRequest, resp: &HttpResponse, - entry_time: time::Tm, - ) -> Result<(), fmt::Error> { - match *self { - FormatText::Str(ref string) => fmt.write_str(string), - FormatText::Percent => "%".fmt(fmt), - FormatText::RequestLine => { - if req.query_string().is_empty() { - fmt.write_fmt(format_args!( - "{} {} {:?}", - req.method(), - req.path(), - req.version() - )) - } else { - fmt.write_fmt(format_args!( - "{} {}?{} {:?}", - req.method(), - req.path(), - req.query_string(), - req.version() - )) - } - } - FormatText::ResponseStatus => resp.status().as_u16().fmt(fmt), - FormatText::ResponseSize => resp.response_size().fmt(fmt), - FormatText::Time => { - let rt = time::now() - entry_time; - let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000_000.0; - fmt.write_fmt(format_args!("{:.6}", rt)) - } - FormatText::TimeMillis => { - let rt = time::now() - entry_time; - let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000.0; - fmt.write_fmt(format_args!("{:.6}", rt)) - } - FormatText::RemoteAddr => { - if let Some(remote) = req.connection_info().remote() { - return remote.fmt(fmt); - } else { - "-".fmt(fmt) - } - } - FormatText::RequestTime => entry_time - .strftime("[%d/%b/%Y:%H:%M:%S %z]") - .unwrap() - .fmt(fmt), - FormatText::RequestHeader(ref name) => { - let s = if let Some(val) = req.headers().get(name) { - if let Ok(s) = val.to_str() { - s - } else { - "-" - } - } else { - "-" - }; - fmt.write_fmt(format_args!("{}", s)) - } - FormatText::ResponseHeader(ref name) => { - let s = if let Some(val) = resp.headers().get(name) { - if let Ok(s) = val.to_str() { - s - } else { - "-" - } - } else { - "-" - }; - fmt.write_fmt(format_args!("{}", s)) - } - FormatText::EnvironHeader(ref name) => { - if let Ok(val) = env::var(name) { - fmt.write_fmt(format_args!("{}", val)) - } else { - "-".fmt(fmt) - } - } - } - } -} - -pub(crate) struct FormatDisplay<'a>(&'a Fn(&mut Formatter) -> Result<(), fmt::Error>); - -impl<'a> fmt::Display for FormatDisplay<'a> { - fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { - (self.0)(fmt) - } -} - -#[cfg(test)] -mod tests { - use time; - - use super::*; - use http::{header, StatusCode}; - use test::TestRequest; - - #[test] - fn test_logger() { - let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test"); - - let req = TestRequest::with_header( - header::USER_AGENT, - header::HeaderValue::from_static("ACTIX-WEB"), - ).finish(); - let resp = HttpResponse::build(StatusCode::OK) - .header("X-Test", "ttt") - .force_close() - .finish(); - - match logger.start(&req) { - Ok(Started::Done) => (), - _ => panic!(), - }; - match logger.finish(&req, &resp) { - Finished::Done => (), - _ => panic!(), - } - let entry_time = time::now(); - let render = |fmt: &mut Formatter| { - for unit in &logger.format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("ACTIX-WEB ttt")); - } - - #[test] - fn test_default_format() { - let format = Format::default(); - - let req = TestRequest::with_header( - header::USER_AGENT, - header::HeaderValue::from_static("ACTIX-WEB"), - ).finish(); - let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); - let entry_time = time::now(); - - let render = |fmt: &mut Formatter| { - for unit in &format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("GET / HTTP/1.1")); - assert!(s.contains("200 0")); - assert!(s.contains("ACTIX-WEB")); - - let req = TestRequest::with_uri("/?test").finish(); - let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); - let entry_time = time::now(); - - let render = |fmt: &mut Formatter| { - for unit in &format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("GET /?test HTTP/1.1")); - } -} diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs deleted file mode 100644 index c69dbb3e0..000000000 --- a/src/middleware/mod.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Middlewares -use futures::Future; - -use error::{Error, Result}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; - -mod logger; - -pub mod cors; -pub mod csrf; -mod defaultheaders; -mod errhandlers; -#[cfg(feature = "session")] -pub mod identity; -#[cfg(feature = "session")] -pub mod session; -pub use self::defaultheaders::DefaultHeaders; -pub use self::errhandlers::ErrorHandlers; -pub use self::logger::Logger; - -/// Middleware start result -pub enum Started { - /// Middleware is completed, continue to next middleware - Done, - /// New http response got generated. If middleware generates response - /// handler execution halts. - Response(HttpResponse), - /// Execution completed, runs future to completion. - Future(Box, Error = Error>>), -} - -/// Middleware execution result -pub enum Response { - /// New http response got generated - Done(HttpResponse), - /// Result is a future that resolves to a new http response - Future(Box>), -} - -/// Middleware finish result -pub enum Finished { - /// Execution completed - Done, - /// Execution completed, but run future to completion - Future(Box>), -} - -/// Middleware definition -#[allow(unused_variables)] -pub trait Middleware: 'static { - /// Method is called when request is ready. It may return - /// future, which should resolve before next middleware get called. - fn start(&self, req: &HttpRequest) -> Result { - Ok(Started::Done) - } - - /// Method is called when handler returns response, - /// but before sending http message to peer. - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - Ok(Response::Done(resp)) - } - - /// Method is called after body stream get sent to peer. - fn finish(&self, req: &HttpRequest, resp: &HttpResponse) -> Finished { - Finished::Done - } -} diff --git a/src/middleware/session.rs b/src/middleware/session.rs deleted file mode 100644 index e8b0e5558..000000000 --- a/src/middleware/session.rs +++ /dev/null @@ -1,617 +0,0 @@ -//! User sessions. -//! -//! Actix provides a general solution for session management. The -//! [**SessionStorage**](struct.SessionStorage.html) -//! middleware can be used with different backend types to store session -//! data in different backends. -//! -//! By default, only cookie session backend is implemented. Other -//! backend implementations can be added. -//! -//! [**CookieSessionBackend**](struct.CookieSessionBackend.html) -//! uses cookies as session storage. `CookieSessionBackend` creates sessions -//! which are limited to storing fewer than 4000 bytes of data, as the payload -//! must fit into a single cookie. An internal server error is generated if a -//! session contains more than 4000 bytes. -//! -//! A cookie may have a security policy of *signed* or *private*. Each has -//! a respective `CookieSessionBackend` constructor. -//! -//! A *signed* cookie may be viewed but not modified by the client. A *private* -//! cookie may neither be viewed nor modified by the client. -//! -//! The constructors take a key as an argument. This is the private key -//! for cookie session - when this value is changed, all session data is lost. -//! -//! In general, you create a `SessionStorage` middleware and initialize it -//! with specific backend implementation, such as a `CookieSessionBackend`. -//! To access session data, -//! [*HttpRequest::session()*](trait.RequestSession.html#tymethod.session) -//! must be used. This method returns a -//! [*Session*](struct.Session.html) object, which allows us to get or set -//! session data. -//! -//! ```rust -//! # extern crate actix_web; -//! use actix_web::{actix, server, App, HttpRequest, Result}; -//! use actix_web::middleware::session::{RequestSession, SessionStorage, CookieSessionBackend}; -//! -//! fn index(req: HttpRequest) -> Result<&'static str> { -//! // access session data -//! if let Some(count) = req.session().get::("counter")? { -//! println!("SESSION value: {}", count); -//! req.session().set("counter", count+1)?; -//! } else { -//! req.session().set("counter", 1)?; -//! } -//! -//! Ok("Welcome!") -//! } -//! -//! fn main() { -//! actix::System::run(|| { -//! server::new( -//! || App::new().middleware( -//! SessionStorage::new( // <- create session middleware -//! CookieSessionBackend::signed(&[0; 32]) // <- create signed cookie session backend -//! .secure(false) -//! ))) -//! .bind("127.0.0.1:59880").unwrap() -//! .start(); -//! # actix::System::current().stop(); -//! }); -//! } -//! ``` -use std::cell::RefCell; -use std::collections::HashMap; -use std::marker::PhantomData; -use std::rc::Rc; -use std::sync::Arc; - -use cookie::{Cookie, CookieJar, Key, SameSite}; -use futures::future::{err as FutErr, ok as FutOk, FutureResult}; -use futures::Future; -use http::header::{self, HeaderValue}; -use serde::de::DeserializeOwned; -use serde::Serialize; -use serde_json; -use serde_json::error::Error as JsonError; -use time::Duration; - -use error::{Error, ResponseError, Result}; -use handler::FromRequest; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; - -/// The helper trait to obtain your session data from a request. -/// -/// ```rust -/// use actix_web::middleware::session::RequestSession; -/// use actix_web::*; -/// -/// fn index(mut req: HttpRequest) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = req.session().get::("counter")? { -/// req.session().set("counter", count + 1)?; -/// } else { -/// req.session().set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -pub trait RequestSession { - /// Get the session from the request - fn session(&self) -> Session; -} - -impl RequestSession for HttpRequest { - fn session(&self) -> Session { - if let Some(s_impl) = self.extensions().get::>() { - return Session(SessionInner::Session(Arc::clone(&s_impl))); - } - Session(SessionInner::None) - } -} - -/// The high-level interface you use to modify session data. -/// -/// Session object could be obtained with -/// [`RequestSession::session`](trait.RequestSession.html#tymethod.session) -/// method. `RequestSession` trait is implemented for `HttpRequest`. -/// -/// ```rust -/// use actix_web::middleware::session::RequestSession; -/// use actix_web::*; -/// -/// fn index(mut req: HttpRequest) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = req.session().get::("counter")? { -/// req.session().set("counter", count + 1)?; -/// } else { -/// req.session().set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -pub struct Session(SessionInner); - -enum SessionInner { - Session(Arc), - None, -} - -impl Session { - /// Get a `value` from the session. - pub fn get(&self, key: &str) -> Result> { - match self.0 { - SessionInner::Session(ref sess) => { - if let Some(s) = sess.as_ref().0.borrow().get(key) { - Ok(Some(serde_json::from_str(s)?)) - } else { - Ok(None) - } - } - SessionInner::None => Ok(None), - } - } - - /// Set a `value` from the session. - pub fn set(&self, key: &str, value: T) -> Result<()> { - match self.0 { - SessionInner::Session(ref sess) => { - sess.as_ref() - .0 - .borrow_mut() - .set(key, serde_json::to_string(&value)?); - Ok(()) - } - SessionInner::None => Ok(()), - } - } - - /// Remove value from the session. - pub fn remove(&self, key: &str) { - match self.0 { - SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().remove(key), - SessionInner::None => (), - } - } - - /// Clear the session. - pub fn clear(&self) { - match self.0 { - SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().clear(), - SessionInner::None => (), - } - } -} - -/// Extractor implementation for Session type. -/// -/// ```rust -/// # use actix_web::*; -/// use actix_web::middleware::session::Session; -/// -/// fn index(session: Session) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = session.get::("counter")? { -/// session.set("counter", count + 1)?; -/// } else { -/// session.set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -impl FromRequest for Session { - type Config = (); - type Result = Session; - - #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { - req.session() - } -} - -struct SessionImplCell(RefCell>); - -/// Session storage middleware -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::session::{CookieSessionBackend, SessionStorage}; -/// use actix_web::App; -/// -/// fn main() { -/// let app = App::new().middleware(SessionStorage::new( -/// // <- create session middleware -/// CookieSessionBackend::signed(&[0; 32]) // <- create cookie session backend -/// .secure(false), -/// )); -/// } -/// ``` -pub struct SessionStorage(T, PhantomData); - -impl> SessionStorage { - /// Create session storage - pub fn new(backend: T) -> SessionStorage { - SessionStorage(backend, PhantomData) - } -} - -impl> Middleware for SessionStorage { - fn start(&self, req: &HttpRequest) -> Result { - let mut req = req.clone(); - - let fut = self.0.from_request(&mut req).then(move |res| match res { - Ok(sess) => { - req.extensions_mut() - .insert(Arc::new(SessionImplCell(RefCell::new(Box::new(sess))))); - FutOk(None) - } - Err(err) => FutErr(err), - }); - Ok(Started::Future(Box::new(fut))) - } - - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(s_box) = req.extensions().get::>() { - s_box.0.borrow_mut().write(resp) - } else { - Ok(Response::Done(resp)) - } - } -} - -/// A simple key-value storage interface that is internally used by `Session`. -pub trait SessionImpl: 'static { - /// Get session value by key - fn get(&self, key: &str) -> Option<&str>; - - /// Set session value - fn set(&mut self, key: &str, value: String); - - /// Remove specific key from session - fn remove(&mut self, key: &str); - - /// Remove all values from session - fn clear(&mut self); - - /// Write session to storage backend. - fn write(&self, resp: HttpResponse) -> Result; -} - -/// Session's storage backend trait definition. -pub trait SessionBackend: Sized + 'static { - /// Session item - type Session: SessionImpl; - /// Future that reads session - type ReadFuture: Future; - - /// Parse the session from request and load data from a storage backend. - fn from_request(&self, request: &mut HttpRequest) -> Self::ReadFuture; -} - -/// Session that uses signed cookies as session storage -pub struct CookieSession { - changed: bool, - state: HashMap, - inner: Rc, -} - -/// Errors that can occur during handling cookie session -#[derive(Fail, Debug)] -pub enum CookieSessionError { - /// Size of the serialized session is greater than 4000 bytes. - #[fail(display = "Size of the serialized session is greater than 4000 bytes.")] - Overflow, - /// Fail to serialize session. - #[fail(display = "Fail to serialize session")] - Serialize(JsonError), -} - -impl ResponseError for CookieSessionError {} - -impl SessionImpl for CookieSession { - fn get(&self, key: &str) -> Option<&str> { - if let Some(s) = self.state.get(key) { - Some(s) - } else { - None - } - } - - fn set(&mut self, key: &str, value: String) { - self.changed = true; - self.state.insert(key.to_owned(), value); - } - - fn remove(&mut self, key: &str) { - self.changed = true; - self.state.remove(key); - } - - fn clear(&mut self) { - self.changed = true; - self.state.clear() - } - - fn write(&self, mut resp: HttpResponse) -> Result { - if self.changed { - let _ = self.inner.set_cookie(&mut resp, &self.state); - } - Ok(Response::Done(resp)) - } -} - -enum CookieSecurity { - Signed, - Private, -} - -struct CookieSessionInner { - key: Key, - security: CookieSecurity, - name: String, - path: String, - domain: Option, - secure: bool, - http_only: bool, - max_age: Option, - same_site: Option, -} - -impl CookieSessionInner { - fn new(key: &[u8], security: CookieSecurity) -> CookieSessionInner { - CookieSessionInner { - security, - key: Key::from_master(key), - name: "actix-session".to_owned(), - path: "/".to_owned(), - domain: None, - secure: true, - http_only: true, - max_age: None, - same_site: None, - } - } - - fn set_cookie( - &self, resp: &mut HttpResponse, state: &HashMap, - ) -> Result<()> { - let value = - serde_json::to_string(&state).map_err(CookieSessionError::Serialize)?; - if value.len() > 4064 { - return Err(CookieSessionError::Overflow.into()); - } - - let mut cookie = Cookie::new(self.name.clone(), value); - cookie.set_path(self.path.clone()); - cookie.set_secure(self.secure); - cookie.set_http_only(self.http_only); - - if let Some(ref domain) = self.domain { - cookie.set_domain(domain.clone()); - } - - if let Some(max_age) = self.max_age { - cookie.set_max_age(max_age); - } - - if let Some(same_site) = self.same_site { - cookie.set_same_site(same_site); - } - - let mut jar = CookieJar::new(); - - match self.security { - CookieSecurity::Signed => jar.signed(&self.key).add(cookie), - CookieSecurity::Private => jar.private(&self.key).add(cookie), - } - - for cookie in jar.delta() { - let val = HeaderValue::from_str(&cookie.encoded().to_string())?; - resp.headers_mut().append(header::SET_COOKIE, val); - } - - Ok(()) - } - - fn load(&self, req: &mut HttpRequest) -> HashMap { - if let Ok(cookies) = req.cookies() { - for cookie in cookies.iter() { - if cookie.name() == self.name { - let mut jar = CookieJar::new(); - jar.add_original(cookie.clone()); - - let cookie_opt = match self.security { - CookieSecurity::Signed => jar.signed(&self.key).get(&self.name), - CookieSecurity::Private => { - jar.private(&self.key).get(&self.name) - } - }; - if let Some(cookie) = cookie_opt { - if let Ok(val) = serde_json::from_str(cookie.value()) { - return val; - } - } - } - } - } - HashMap::new() - } -} - -/// Use cookies for session storage. -/// -/// `CookieSessionBackend` creates sessions which are limited to storing -/// fewer than 4000 bytes of data (as the payload must fit into a single -/// cookie). An Internal Server Error is generated if the session contains more -/// than 4000 bytes. -/// -/// A cookie may have a security policy of *signed* or *private*. Each has a -/// respective `CookieSessionBackend` constructor. -/// -/// A *signed* cookie is stored on the client as plaintext alongside -/// a signature such that the cookie may be viewed but not modified by the -/// client. -/// -/// A *private* cookie is stored on the client as encrypted text -/// such that it may neither be viewed nor modified by the client. -/// -/// The constructors take a key as an argument. -/// This is the private key for cookie session - when this value is changed, -/// all session data is lost. The constructors will panic if the key is less -/// than 32 bytes in length. -/// -/// The backend relies on `cookie` crate to create and read cookies. -/// By default all cookies are percent encoded, but certain symbols may -/// cause troubles when reading cookie, if they are not properly percent encoded. -/// -/// # Example -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::session::CookieSessionBackend; -/// -/// # fn main() { -/// let backend: CookieSessionBackend = CookieSessionBackend::signed(&[0; 32]) -/// .domain("www.rust-lang.org") -/// .name("actix_session") -/// .path("/") -/// .secure(true); -/// # } -/// ``` -pub struct CookieSessionBackend(Rc); - -impl CookieSessionBackend { - /// Construct new *signed* `CookieSessionBackend` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn signed(key: &[u8]) -> CookieSessionBackend { - CookieSessionBackend(Rc::new(CookieSessionInner::new( - key, - CookieSecurity::Signed, - ))) - } - - /// Construct new *private* `CookieSessionBackend` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn private(key: &[u8]) -> CookieSessionBackend { - CookieSessionBackend(Rc::new(CookieSessionInner::new( - key, - CookieSecurity::Private, - ))) - } - - /// Sets the `path` field in the session cookie being built. - pub fn path>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().path = value.into(); - self - } - - /// Sets the `name` field in the session cookie being built. - pub fn name>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().name = value.into(); - self - } - - /// Sets the `domain` field in the session cookie being built. - pub fn domain>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); - self - } - - /// Sets the `secure` field in the session cookie being built. - /// - /// If the `secure` field is set, a cookie will only be transmitted when the - /// connection is secure - i.e. `https` - pub fn secure(mut self, value: bool) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().secure = value; - self - } - - /// Sets the `http_only` field in the session cookie being built. - pub fn http_only(mut self, value: bool) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().http_only = value; - self - } - - /// Sets the `same_site` field in the session cookie being built. - pub fn same_site(mut self, value: SameSite) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().same_site = Some(value); - self - } - - /// Sets the `max-age` field in the session cookie being built. - pub fn max_age(mut self, value: Duration) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); - self - } -} - -impl SessionBackend for CookieSessionBackend { - type Session = CookieSession; - type ReadFuture = FutureResult; - - fn from_request(&self, req: &mut HttpRequest) -> Self::ReadFuture { - let state = self.0.load(req); - FutOk(CookieSession { - changed: false, - inner: Rc::clone(&self.0), - state, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use application::App; - use test; - - #[test] - fn cookie_session() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/", |r| { - r.f(|req| { - let _ = req.session().set("counter", 100); - "test" - }) - }) - }); - - let request = srv.get().uri(srv.url("/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.cookie("actix-session").is_some()); - } - - #[test] - fn cookie_session_extractor() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/", |r| { - r.with(|ses: Session| { - let _ = ses.set("counter", 100); - "test" - }) - }) - }); - - let request = srv.get().uri(srv.url("/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.cookie("actix-session").is_some()); - } -} diff --git a/src/multipart.rs b/src/multipart.rs deleted file mode 100644 index 862f60ecb..000000000 --- a/src/multipart.rs +++ /dev/null @@ -1,815 +0,0 @@ -//! Multipart requests support -use std::cell::{RefCell, UnsafeCell}; -use std::marker::PhantomData; -use std::rc::Rc; -use std::{cmp, fmt}; - -use bytes::Bytes; -use futures::task::{current as current_task, Task}; -use futures::{Async, Poll, Stream}; -use http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}; -use http::HttpTryFrom; -use httparse; -use mime; - -use error::{MultipartError, ParseError, PayloadError}; -use payload::PayloadBuffer; - -const MAX_HEADERS: usize = 32; - -/// The server-side implementation of `multipart/form-data` requests. -/// -/// This will parse the incoming stream into `MultipartItem` instances via its -/// Stream implementation. -/// `MultipartItem::Field` contains multipart field. `MultipartItem::Multipart` -/// is used for nested multipart streams. -pub struct Multipart { - safety: Safety, - error: Option, - inner: Option>>>, -} - -/// -pub enum MultipartItem { - /// Multipart field - Field(Field), - /// Nested multipart stream - Nested(Multipart), -} - -enum InnerMultipartItem { - None, - Field(Rc>>), - Multipart(Rc>>), -} - -#[derive(PartialEq, Debug)] -enum InnerState { - /// Stream eof - Eof, - /// Skip data until first boundary - FirstBoundary, - /// Reading boundary - Boundary, - /// Reading Headers, - Headers, -} - -struct InnerMultipart { - payload: PayloadRef, - boundary: String, - state: InnerState, - item: InnerMultipartItem, -} - -impl Multipart<()> { - /// Extract boundary info from headers. - pub fn boundary(headers: &HeaderMap) -> Result { - if let Some(content_type) = headers.get(header::CONTENT_TYPE) { - if let Ok(content_type) = content_type.to_str() { - if let Ok(ct) = content_type.parse::() { - if let Some(boundary) = ct.get_param(mime::BOUNDARY) { - Ok(boundary.as_str().to_owned()) - } else { - Err(MultipartError::Boundary) - } - } else { - Err(MultipartError::ParseContentType) - } - } else { - Err(MultipartError::ParseContentType) - } - } else { - Err(MultipartError::NoContentType) - } - } -} - -impl Multipart -where - S: Stream, -{ - /// Create multipart instance for boundary. - pub fn new(boundary: Result, stream: S) -> Multipart { - match boundary { - Ok(boundary) => Multipart { - error: None, - safety: Safety::new(), - inner: Some(Rc::new(RefCell::new(InnerMultipart { - boundary, - payload: PayloadRef::new(PayloadBuffer::new(stream)), - state: InnerState::FirstBoundary, - item: InnerMultipartItem::None, - }))), - }, - Err(err) => Multipart { - error: Some(err), - safety: Safety::new(), - inner: None, - }, - } - } -} - -impl Stream for Multipart -where - S: Stream, -{ - type Item = MultipartItem; - type Error = MultipartError; - - fn poll(&mut self) -> Poll, Self::Error> { - if let Some(err) = self.error.take() { - Err(err) - } else if self.safety.current() { - self.inner.as_mut().unwrap().borrow_mut().poll(&self.safety) - } else { - Ok(Async::NotReady) - } - } -} - -impl InnerMultipart -where - S: Stream, -{ - fn read_headers(payload: &mut PayloadBuffer) -> Poll { - match payload.read_until(b"\r\n\r\n")? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(None) => Err(MultipartError::Incomplete), - Async::Ready(Some(bytes)) => { - let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS]; - match httparse::parse_headers(&bytes, &mut hdrs) { - Ok(httparse::Status::Complete((_, hdrs))) => { - // convert headers - let mut headers = HeaderMap::with_capacity(hdrs.len()); - for h in hdrs { - if let Ok(name) = HeaderName::try_from(h.name) { - if let Ok(value) = HeaderValue::try_from(h.value) { - headers.append(name, value); - } else { - return Err(ParseError::Header.into()); - } - } else { - return Err(ParseError::Header.into()); - } - } - Ok(Async::Ready(headers)) - } - Ok(httparse::Status::Partial) => Err(ParseError::Header.into()), - Err(err) => Err(ParseError::from(err).into()), - } - } - } - } - - fn read_boundary( - payload: &mut PayloadBuffer, boundary: &str, - ) -> Poll { - // TODO: need to read epilogue - match payload.readline()? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(None) => Err(MultipartError::Incomplete), - Async::Ready(Some(chunk)) => { - if chunk.len() == boundary.len() + 4 - && &chunk[..2] == b"--" - && &chunk[2..boundary.len() + 2] == boundary.as_bytes() - { - Ok(Async::Ready(false)) - } else if chunk.len() == boundary.len() + 6 - && &chunk[..2] == b"--" - && &chunk[2..boundary.len() + 2] == boundary.as_bytes() - && &chunk[boundary.len() + 2..boundary.len() + 4] == b"--" - { - Ok(Async::Ready(true)) - } else { - Err(MultipartError::Boundary) - } - } - } - } - - fn skip_until_boundary( - payload: &mut PayloadBuffer, boundary: &str, - ) -> Poll { - let mut eof = false; - loop { - match payload.readline()? { - Async::Ready(Some(chunk)) => { - if chunk.is_empty() { - //ValueError("Could not find starting boundary %r" - //% (self._boundary)) - } - if chunk.len() < boundary.len() { - continue; - } - if &chunk[..2] == b"--" - && &chunk[2..chunk.len() - 2] == boundary.as_bytes() - { - break; - } else { - if chunk.len() < boundary.len() + 2 { - continue; - } - let b: &[u8] = boundary.as_ref(); - if &chunk[..boundary.len()] == b - && &chunk[boundary.len()..boundary.len() + 2] == b"--" - { - eof = true; - break; - } - } - } - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(None) => return Err(MultipartError::Incomplete), - } - } - Ok(Async::Ready(eof)) - } - - fn poll( - &mut self, safety: &Safety, - ) -> Poll>, MultipartError> { - if self.state == InnerState::Eof { - Ok(Async::Ready(None)) - } else { - // release field - loop { - // Nested multipart streams of fields has to be consumed - // before switching to next - if safety.current() { - let stop = match self.item { - InnerMultipartItem::Field(ref mut field) => { - match field.borrow_mut().poll(safety)? { - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(Some(_)) => continue, - Async::Ready(None) => true, - } - } - InnerMultipartItem::Multipart(ref mut multipart) => { - match multipart.borrow_mut().poll(safety)? { - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(Some(_)) => continue, - Async::Ready(None) => true, - } - } - _ => false, - }; - if stop { - self.item = InnerMultipartItem::None; - } - if let InnerMultipartItem::None = self.item { - break; - } - } - } - - let headers = if let Some(payload) = self.payload.get_mut(safety) { - match self.state { - // read until first boundary - InnerState::FirstBoundary => { - match InnerMultipart::skip_until_boundary( - payload, - &self.boundary, - )? { - Async::Ready(eof) => { - if eof { - self.state = InnerState::Eof; - return Ok(Async::Ready(None)); - } else { - self.state = InnerState::Headers; - } - } - Async::NotReady => return Ok(Async::NotReady), - } - } - // read boundary - InnerState::Boundary => { - match InnerMultipart::read_boundary(payload, &self.boundary)? { - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(eof) => { - if eof { - self.state = InnerState::Eof; - return Ok(Async::Ready(None)); - } else { - self.state = InnerState::Headers; - } - } - } - } - _ => (), - } - - // read field headers for next field - if self.state == InnerState::Headers { - if let Async::Ready(headers) = InnerMultipart::read_headers(payload)? - { - self.state = InnerState::Boundary; - headers - } else { - return Ok(Async::NotReady); - } - } else { - unreachable!() - } - } else { - debug!("NotReady: field is in flight"); - return Ok(Async::NotReady); - }; - - // content type - let mut mt = mime::APPLICATION_OCTET_STREAM; - if let Some(content_type) = headers.get(header::CONTENT_TYPE) { - if let Ok(content_type) = content_type.to_str() { - if let Ok(ct) = content_type.parse::() { - mt = ct; - } - } - } - - self.state = InnerState::Boundary; - - // nested multipart stream - if mt.type_() == mime::MULTIPART { - let inner = if let Some(boundary) = mt.get_param(mime::BOUNDARY) { - Rc::new(RefCell::new(InnerMultipart { - payload: self.payload.clone(), - boundary: boundary.as_str().to_owned(), - state: InnerState::FirstBoundary, - item: InnerMultipartItem::None, - })) - } else { - return Err(MultipartError::Boundary); - }; - - self.item = InnerMultipartItem::Multipart(Rc::clone(&inner)); - - Ok(Async::Ready(Some(MultipartItem::Nested(Multipart { - safety: safety.clone(), - error: None, - inner: Some(inner), - })))) - } else { - let field = Rc::new(RefCell::new(InnerField::new( - self.payload.clone(), - self.boundary.clone(), - &headers, - )?)); - self.item = InnerMultipartItem::Field(Rc::clone(&field)); - - Ok(Async::Ready(Some(MultipartItem::Field(Field::new( - safety.clone(), - headers, - mt, - field, - ))))) - } - } - } -} - -impl Drop for InnerMultipart { - fn drop(&mut self) { - // InnerMultipartItem::Field has to be dropped first because of Safety. - self.item = InnerMultipartItem::None; - } -} - -/// A single field in a multipart stream -pub struct Field { - ct: mime::Mime, - headers: HeaderMap, - inner: Rc>>, - safety: Safety, -} - -impl Field -where - S: Stream, -{ - fn new( - safety: Safety, headers: HeaderMap, ct: mime::Mime, - inner: Rc>>, - ) -> Self { - Field { - ct, - headers, - inner, - safety, - } - } - - /// Get a map of headers - pub fn headers(&self) -> &HeaderMap { - &self.headers - } - - /// Get the content type of the field - pub fn content_type(&self) -> &mime::Mime { - &self.ct - } - - /// Get the content disposition of the field, if it exists - pub fn content_disposition(&self) -> Option { - // RFC 7578: 'Each part MUST contain a Content-Disposition header field - // where the disposition type is "form-data".' - if let Some(content_disposition) = - self.headers.get(::http::header::CONTENT_DISPOSITION) - { - ContentDisposition::from_raw(content_disposition).ok() - } else { - None - } - } -} - -impl Stream for Field -where - S: Stream, -{ - type Item = Bytes; - type Error = MultipartError; - - fn poll(&mut self) -> Poll, Self::Error> { - if self.safety.current() { - self.inner.borrow_mut().poll(&self.safety) - } else { - Ok(Async::NotReady) - } - } -} - -impl fmt::Debug for Field { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "\nMultipartField: {}", self.ct)?; - writeln!(f, " boundary: {}", self.inner.borrow().boundary)?; - writeln!(f, " headers:")?; - for (key, val) in self.headers.iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -struct InnerField { - payload: Option>, - boundary: String, - eof: bool, - length: Option, -} - -impl InnerField -where - S: Stream, -{ - fn new( - payload: PayloadRef, boundary: String, headers: &HeaderMap, - ) -> Result, PayloadError> { - let len = if let Some(len) = headers.get(header::CONTENT_LENGTH) { - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - Some(len) - } else { - return Err(PayloadError::Incomplete); - } - } else { - return Err(PayloadError::Incomplete); - } - } else { - None - }; - - Ok(InnerField { - boundary, - payload: Some(payload), - eof: false, - length: len, - }) - } - - /// Reads body part content chunk of the specified size. - /// The body part must has `Content-Length` header with proper value. - fn read_len( - payload: &mut PayloadBuffer, size: &mut u64, - ) -> Poll, MultipartError> { - if *size == 0 { - Ok(Async::Ready(None)) - } else { - match payload.readany() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(None)) => Err(MultipartError::Incomplete), - Ok(Async::Ready(Some(mut chunk))) => { - let len = cmp::min(chunk.len() as u64, *size); - *size -= len; - let ch = chunk.split_to(len as usize); - if !chunk.is_empty() { - payload.unprocessed(chunk); - } - Ok(Async::Ready(Some(ch))) - } - Err(err) => Err(err.into()), - } - } - } - - /// Reads content chunk of body part with unknown length. - /// The `Content-Length` header for body part is not necessary. - fn read_stream( - payload: &mut PayloadBuffer, boundary: &str, - ) -> Poll, MultipartError> { - match payload.read_until(b"\r")? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(None) => Err(MultipartError::Incomplete), - Async::Ready(Some(mut chunk)) => { - if chunk.len() == 1 { - payload.unprocessed(chunk); - match payload.read_exact(boundary.len() + 4)? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(None) => Err(MultipartError::Incomplete), - Async::Ready(Some(mut chunk)) => { - if &chunk[..2] == b"\r\n" - && &chunk[2..4] == b"--" - && &chunk[4..] == boundary.as_bytes() - { - payload.unprocessed(chunk); - Ok(Async::Ready(None)) - } else { - // \r might be part of data stream - let ch = chunk.split_to(1); - payload.unprocessed(chunk); - Ok(Async::Ready(Some(ch))) - } - } - } - } else { - let to = chunk.len() - 1; - let ch = chunk.split_to(to); - payload.unprocessed(chunk); - Ok(Async::Ready(Some(ch))) - } - } - } - } - - fn poll(&mut self, s: &Safety) -> Poll, MultipartError> { - if self.payload.is_none() { - return Ok(Async::Ready(None)); - } - - let result = if let Some(payload) = self.payload.as_ref().unwrap().get_mut(s) { - let res = if let Some(ref mut len) = self.length { - InnerField::read_len(payload, len)? - } else { - InnerField::read_stream(payload, &self.boundary)? - }; - - match res { - Async::NotReady => Async::NotReady, - Async::Ready(Some(bytes)) => Async::Ready(Some(bytes)), - Async::Ready(None) => { - self.eof = true; - match payload.readline()? { - Async::NotReady => Async::NotReady, - Async::Ready(None) => Async::Ready(None), - Async::Ready(Some(line)) => { - if line.as_ref() != b"\r\n" { - warn!("multipart field did not read all the data or it is malformed"); - } - Async::Ready(None) - } - } - } - } - } else { - Async::NotReady - }; - - if Async::Ready(None) == result { - self.payload.take(); - } - Ok(result) - } -} - -struct PayloadRef { - payload: Rc>>, -} - -impl PayloadRef -where - S: Stream, -{ - fn new(payload: PayloadBuffer) -> PayloadRef { - PayloadRef { - payload: Rc::new(payload.into()), - } - } - - fn get_mut<'a, 'b>(&'a self, s: &'b Safety) -> Option<&'a mut PayloadBuffer> - where - 'a: 'b, - { - // Unsafe: Invariant is inforced by Safety Safety is used as ref counter, - // only top most ref can have mutable access to payload. - if s.current() { - let payload: &mut PayloadBuffer = unsafe { &mut *self.payload.get() }; - Some(payload) - } else { - None - } - } -} - -impl Clone for PayloadRef { - fn clone(&self) -> PayloadRef { - PayloadRef { - payload: Rc::clone(&self.payload), - } - } -} - -/// Counter. It tracks of number of clones of payloads and give access to -/// payload only to top most task panics if Safety get destroyed and it not top -/// most task. -#[derive(Debug)] -struct Safety { - task: Option, - level: usize, - payload: Rc>, -} - -impl Safety { - fn new() -> Safety { - let payload = Rc::new(PhantomData); - Safety { - task: None, - level: Rc::strong_count(&payload), - payload, - } - } - - fn current(&self) -> bool { - Rc::strong_count(&self.payload) == self.level - } -} - -impl Clone for Safety { - fn clone(&self) -> Safety { - let payload = Rc::clone(&self.payload); - Safety { - task: Some(current_task()), - level: Rc::strong_count(&payload), - payload, - } - } -} - -impl Drop for Safety { - fn drop(&mut self) { - // parent task is dead - if Rc::strong_count(&self.payload) != self.level { - panic!("Safety get dropped but it is not from top-most task"); - } - if let Some(task) = self.task.take() { - task.notify() - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use bytes::Bytes; - use futures::future::{lazy, result}; - use payload::{Payload, PayloadWriter}; - use tokio::runtime::current_thread::Runtime; - - #[test] - fn test_boundary() { - let headers = HeaderMap::new(); - match Multipart::boundary(&headers) { - Err(MultipartError::NoContentType) => (), - _ => unreachable!("should not happen"), - } - - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("test"), - ); - - match Multipart::boundary(&headers) { - Err(MultipartError::ParseContentType) => (), - _ => unreachable!("should not happen"), - } - - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("multipart/mixed"), - ); - match Multipart::boundary(&headers) { - Err(MultipartError::Boundary) => (), - _ => unreachable!("should not happen"), - } - - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static( - "multipart/mixed; boundary=\"5c02368e880e436dab70ed54e1c58209\"", - ), - ); - - assert_eq!( - Multipart::boundary(&headers).unwrap(), - "5c02368e880e436dab70ed54e1c58209" - ); - } - - #[test] - fn test_multipart() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (mut sender, payload) = Payload::new(false); - - let bytes = Bytes::from( - "testasdadsad\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ - Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ - Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ - test\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ - Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ - data\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n"); - sender.feed_data(bytes); - - let mut multipart = Multipart::new( - Ok("abbc761f78ff4d7cb7573b5a23f96ef0".to_owned()), - payload, - ); - match multipart.poll() { - Ok(Async::Ready(Some(item))) => match item { - MultipartItem::Field(mut field) => { - { - use http::header::{DispositionParam, DispositionType}; - let cd = field.content_disposition().unwrap(); - assert_eq!(cd.disposition, DispositionType::FormData); - assert_eq!( - cd.parameters[0], - DispositionParam::Name("file".into()) - ); - } - assert_eq!(field.content_type().type_(), mime::TEXT); - assert_eq!(field.content_type().subtype(), mime::PLAIN); - - match field.poll() { - Ok(Async::Ready(Some(chunk))) => { - assert_eq!(chunk, "test") - } - _ => unreachable!(), - } - match field.poll() { - Ok(Async::Ready(None)) => (), - _ => unreachable!(), - } - } - _ => unreachable!(), - }, - _ => unreachable!(), - } - - match multipart.poll() { - Ok(Async::Ready(Some(item))) => match item { - MultipartItem::Field(mut field) => { - assert_eq!(field.content_type().type_(), mime::TEXT); - assert_eq!(field.content_type().subtype(), mime::PLAIN); - - match field.poll() { - Ok(Async::Ready(Some(chunk))) => { - assert_eq!(chunk, "data") - } - _ => unreachable!(), - } - match field.poll() { - Ok(Async::Ready(None)) => (), - _ => unreachable!(), - } - } - _ => unreachable!(), - }, - _ => unreachable!(), - } - - match multipart.poll() { - Ok(Async::Ready(None)) => (), - _ => unreachable!(), - } - - let res: Result<(), ()> = Ok(()); - result(res) - })).unwrap(); - } -} diff --git a/src/param.rs b/src/param.rs deleted file mode 100644 index d0664df99..000000000 --- a/src/param.rs +++ /dev/null @@ -1,303 +0,0 @@ -use std; -use std::ops::Index; -use std::path::PathBuf; -use std::rc::Rc; -use std::str::FromStr; - -use http::StatusCode; -use smallvec::SmallVec; - -use error::{InternalError, ResponseError, UriSegmentError}; -use uri::Url; - -/// A trait to abstract the idea of creating a new instance of a type from a -/// path parameter. -pub trait FromParam: Sized { - /// The associated error which can be returned from parsing. - type Err: ResponseError; - - /// Parses a string `s` to return a value of this type. - fn from_param(s: &str) -> Result; -} - -#[derive(Debug, Clone)] -pub(crate) enum ParamItem { - Static(&'static str), - UrlSegment(u16, u16), -} - -/// Route match information -/// -/// If resource path contains variable patterns, `Params` stores this variables. -#[derive(Debug, Clone)] -pub struct Params { - url: Url, - pub(crate) tail: u16, - pub(crate) segments: SmallVec<[(Rc, ParamItem); 3]>, -} - -impl Params { - pub(crate) fn new() -> Params { - Params { - url: Url::default(), - tail: 0, - segments: SmallVec::new(), - } - } - - pub(crate) fn with_url(url: &Url) -> Params { - Params { - url: url.clone(), - tail: 0, - segments: SmallVec::new(), - } - } - - pub(crate) fn clear(&mut self) { - self.segments.clear(); - } - - pub(crate) fn set_tail(&mut self, tail: u16) { - self.tail = tail; - } - - pub(crate) fn set_url(&mut self, url: Url) { - self.url = url; - } - - pub(crate) fn add(&mut self, name: Rc, value: ParamItem) { - self.segments.push((name, value)); - } - - pub(crate) fn add_static(&mut self, name: &str, value: &'static str) { - self.segments - .push((Rc::new(name.to_string()), ParamItem::Static(value))); - } - - /// Check if there are any matched patterns - pub fn is_empty(&self) -> bool { - self.segments.is_empty() - } - - /// Check number of extracted parameters - pub fn len(&self) -> usize { - self.segments.len() - } - - /// Get matched parameter by name without type conversion - pub fn get(&self, key: &str) -> Option<&str> { - for item in self.segments.iter() { - if key == item.0.as_str() { - return match item.1 { - ParamItem::Static(ref s) => Some(&s), - ParamItem::UrlSegment(s, e) => { - Some(&self.url.path()[(s as usize)..(e as usize)]) - } - }; - } - } - if key == "tail" { - Some(&self.url.path()[(self.tail as usize)..]) - } else { - None - } - } - - /// Get unprocessed part of path - pub fn unprocessed(&self) -> &str { - &self.url.path()[(self.tail as usize)..] - } - - /// Get matched `FromParam` compatible parameter by name. - /// - /// If keyed parameter is not available empty string is used as default - /// value. - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// fn index(req: HttpRequest) -> Result { - /// let ivalue: isize = req.match_info().query("val")?; - /// Ok(format!("isuze value: {:?}", ivalue)) - /// } - /// # fn main() {} - /// ``` - pub fn query(&self, key: &str) -> Result::Err> { - if let Some(s) = self.get(key) { - T::from_param(s) - } else { - T::from_param("") - } - } - - /// Return iterator to items in parameter container - pub fn iter(&self) -> ParamsIter { - ParamsIter { - idx: 0, - params: self, - } - } -} - -#[derive(Debug)] -pub struct ParamsIter<'a> { - idx: usize, - params: &'a Params, -} - -impl<'a> Iterator for ParamsIter<'a> { - type Item = (&'a str, &'a str); - - #[inline] - fn next(&mut self) -> Option<(&'a str, &'a str)> { - if self.idx < self.params.len() { - let idx = self.idx; - let res = match self.params.segments[idx].1 { - ParamItem::Static(ref s) => &s, - ParamItem::UrlSegment(s, e) => { - &self.params.url.path()[(s as usize)..(e as usize)] - } - }; - self.idx += 1; - return Some((&self.params.segments[idx].0, res)); - } - None - } -} - -impl<'a> Index<&'a str> for Params { - type Output = str; - - fn index(&self, name: &'a str) -> &str { - self.get(name) - .expect("Value for parameter is not available") - } -} - -impl Index for Params { - type Output = str; - - fn index(&self, idx: usize) -> &str { - match self.segments[idx].1 { - ParamItem::Static(ref s) => &s, - ParamItem::UrlSegment(s, e) => &self.url.path()[(s as usize)..(e as usize)], - } - } -} - -/// Creates a `PathBuf` from a path parameter. The returned `PathBuf` is -/// percent-decoded. If a segment is equal to "..", the previous segment (if -/// any) is skipped. -/// -/// For security purposes, if a segment meets any of the following conditions, -/// an `Err` is returned indicating the condition met: -/// -/// * Decoded segment starts with any of: `.` (except `..`), `*` -/// * Decoded segment ends with any of: `:`, `>`, `<` -/// * Decoded segment contains any of: `/` -/// * On Windows, decoded segment contains any of: '\' -/// * Percent-encoding results in invalid UTF8. -/// -/// As a result of these conditions, a `PathBuf` parsed from request path -/// parameter is safe to interpolate within, or use as a suffix of, a path -/// without additional checks. -impl FromParam for PathBuf { - type Err = UriSegmentError; - - fn from_param(val: &str) -> Result { - let mut buf = PathBuf::new(); - for segment in val.split('/') { - if segment == ".." { - buf.pop(); - } else if segment.starts_with('.') { - return Err(UriSegmentError::BadStart('.')); - } else if segment.starts_with('*') { - return Err(UriSegmentError::BadStart('*')); - } else if segment.ends_with(':') { - return Err(UriSegmentError::BadEnd(':')); - } else if segment.ends_with('>') { - return Err(UriSegmentError::BadEnd('>')); - } else if segment.ends_with('<') { - return Err(UriSegmentError::BadEnd('<')); - } else if segment.is_empty() { - continue; - } else if cfg!(windows) && segment.contains('\\') { - return Err(UriSegmentError::BadChar('\\')); - } else { - buf.push(segment) - } - } - - Ok(buf) - } -} - -macro_rules! FROM_STR { - ($type:ty) => { - impl FromParam for $type { - type Err = InternalError<<$type as FromStr>::Err>; - fn from_param(val: &str) -> Result { - <$type as FromStr>::from_str(val) - .map_err(|e| InternalError::new(e, StatusCode::BAD_REQUEST)) - } - } - }; -} - -FROM_STR!(u8); -FROM_STR!(u16); -FROM_STR!(u32); -FROM_STR!(u64); -FROM_STR!(usize); -FROM_STR!(i8); -FROM_STR!(i16); -FROM_STR!(i32); -FROM_STR!(i64); -FROM_STR!(isize); -FROM_STR!(f32); -FROM_STR!(f64); -FROM_STR!(String); -FROM_STR!(std::net::IpAddr); -FROM_STR!(std::net::Ipv4Addr); -FROM_STR!(std::net::Ipv6Addr); -FROM_STR!(std::net::SocketAddr); -FROM_STR!(std::net::SocketAddrV4); -FROM_STR!(std::net::SocketAddrV6); - -#[cfg(test)] -mod tests { - use super::*; - use std::iter::FromIterator; - - #[test] - fn test_path_buf() { - assert_eq!( - PathBuf::from_param("/test/.tt"), - Err(UriSegmentError::BadStart('.')) - ); - assert_eq!( - PathBuf::from_param("/test/*tt"), - Err(UriSegmentError::BadStart('*')) - ); - assert_eq!( - PathBuf::from_param("/test/tt:"), - Err(UriSegmentError::BadEnd(':')) - ); - assert_eq!( - PathBuf::from_param("/test/tt<"), - Err(UriSegmentError::BadEnd('<')) - ); - assert_eq!( - PathBuf::from_param("/test/tt>"), - Err(UriSegmentError::BadEnd('>')) - ); - assert_eq!( - PathBuf::from_param("/seg1/seg2/"), - Ok(PathBuf::from_iter(vec!["seg1", "seg2"])) - ); - assert_eq!( - PathBuf::from_param("/seg1/../seg2/"), - Ok(PathBuf::from_iter(vec!["seg2"])) - ); - } -} diff --git a/src/pipeline.rs b/src/pipeline.rs deleted file mode 100644 index 1940f9308..000000000 --- a/src/pipeline.rs +++ /dev/null @@ -1,869 +0,0 @@ -use std::marker::PhantomData; -use std::rc::Rc; -use std::{io, mem}; - -use futures::sync::oneshot; -use futures::{Async, Future, Poll, Stream}; -use log::Level::Debug; - -use body::{Body, BodyStream}; -use context::{ActorHttpContext, Frame}; -use error::Error; -use handler::{AsyncResult, AsyncResultItem}; -use header::ContentEncoding; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Finished, Middleware, Response, Started}; -use server::{HttpHandlerTask, Writer, WriterState}; - -#[doc(hidden)] -pub trait PipelineHandler { - fn encoding(&self) -> ContentEncoding; - - fn handle(&self, &HttpRequest) -> AsyncResult; -} - -#[doc(hidden)] -pub struct Pipeline( - PipelineInfo, - PipelineState, - Rc>>>, -); - -enum PipelineState { - None, - Error, - Starting(StartMiddlewares), - Handler(WaitingResponse), - RunMiddlewares(RunMiddlewares), - Response(ProcessResponse), - Finishing(FinishingMiddlewares), - Completed(Completed), -} - -impl> PipelineState { - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - match *self { - PipelineState::Starting(ref mut state) => state.poll(info, mws), - PipelineState::Handler(ref mut state) => state.poll(info, mws), - PipelineState::RunMiddlewares(ref mut state) => state.poll(info, mws), - PipelineState::Finishing(ref mut state) => state.poll(info, mws), - PipelineState::Completed(ref mut state) => state.poll(info), - PipelineState::Response(ref mut state) => state.poll(info, mws), - PipelineState::None | PipelineState::Error => None, - } - } -} - -struct PipelineInfo { - req: HttpRequest, - count: u16, - context: Option>, - error: Option, - disconnected: Option, - encoding: ContentEncoding, -} - -impl PipelineInfo { - fn poll_context(&mut self) -> Poll<(), Error> { - if let Some(ref mut context) = self.context { - match context.poll() { - Err(err) => Err(err), - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(_)) => Ok(Async::Ready(())), - } - } else { - Ok(Async::Ready(())) - } - } -} - -impl> Pipeline { - pub(crate) fn new( - req: HttpRequest, mws: Rc>>>, handler: Rc, - ) -> Pipeline { - let mut info = PipelineInfo { - req, - count: 0, - error: None, - context: None, - disconnected: None, - encoding: handler.encoding(), - }; - let state = StartMiddlewares::init(&mut info, &mws, handler); - - Pipeline(info, state, mws) - } -} - -impl Pipeline { - #[inline] - fn is_done(&self) -> bool { - match self.1 { - PipelineState::None - | PipelineState::Error - | PipelineState::Starting(_) - | PipelineState::Handler(_) - | PipelineState::RunMiddlewares(_) - | PipelineState::Response(_) => true, - PipelineState::Finishing(_) | PipelineState::Completed(_) => false, - } - } -} - -impl> HttpHandlerTask for Pipeline { - fn disconnected(&mut self) { - self.0.disconnected = Some(true); - } - - fn poll_io(&mut self, io: &mut Writer) -> Poll { - let mut state = mem::replace(&mut self.1, PipelineState::None); - - loop { - if let PipelineState::Response(st) = state { - match st.poll_io(io, &mut self.0, &self.2) { - Ok(state) => { - self.1 = state; - if let Some(error) = self.0.error.take() { - return Err(error); - } else { - return Ok(Async::Ready(self.is_done())); - } - } - Err(state) => { - self.1 = state; - return Ok(Async::NotReady); - } - } - } - match state { - PipelineState::None => return Ok(Async::Ready(true)), - PipelineState::Error => { - return Err( - io::Error::new(io::ErrorKind::Other, "Internal error").into() - ) - } - _ => (), - } - - match state.poll(&mut self.0, &self.2) { - Some(st) => state = st, - None => { - return { - self.1 = state; - Ok(Async::NotReady) - } - } - } - } - } - - fn poll_completed(&mut self) -> Poll<(), Error> { - let mut state = mem::replace(&mut self.1, PipelineState::None); - loop { - match state { - PipelineState::None | PipelineState::Error => { - return Ok(Async::Ready(())) - } - _ => (), - } - - if let Some(st) = state.poll(&mut self.0, &self.2) { - state = st; - } else { - self.1 = state; - return Ok(Async::NotReady); - } - } - } -} - -type Fut = Box, Error = Error>>; - -/// Middlewares start executor -struct StartMiddlewares { - hnd: Rc, - fut: Option, - _s: PhantomData, -} - -impl> StartMiddlewares { - fn init( - info: &mut PipelineInfo, mws: &[Box>], hnd: Rc, - ) -> PipelineState { - // execute middlewares, we need this stage because middlewares could be - // non-async and we can move to next state immediately - let len = mws.len() as u16; - - loop { - if info.count == len { - let reply = hnd.handle(&info.req); - return WaitingResponse::init(info, mws, reply); - } else { - match mws[info.count as usize].start(&info.req) { - Ok(Started::Done) => info.count += 1, - Ok(Started::Response(resp)) => { - return RunMiddlewares::init(info, mws, resp); - } - Ok(Started::Future(fut)) => { - return PipelineState::Starting(StartMiddlewares { - hnd, - fut: Some(fut), - _s: PhantomData, - }) - } - Err(err) => { - return RunMiddlewares::init(info, mws, err.into()); - } - } - } - } - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - let len = mws.len() as u16; - - 'outer: loop { - match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => { - return None; - } - Ok(Async::Ready(resp)) => { - info.count += 1; - if let Some(resp) = resp { - return Some(RunMiddlewares::init(info, mws, resp)); - } - loop { - if info.count == len { - let reply = self.hnd.handle(&info.req); - return Some(WaitingResponse::init(info, mws, reply)); - } else { - let res = mws[info.count as usize].start(&info.req); - match res { - Ok(Started::Done) => info.count += 1, - Ok(Started::Response(resp)) => { - return Some(RunMiddlewares::init(info, mws, resp)); - } - Ok(Started::Future(fut)) => { - self.fut = Some(fut); - continue 'outer; - } - Err(err) => { - return Some(RunMiddlewares::init( - info, - mws, - err.into(), - )); - } - } - } - } - } - Err(err) => { - return Some(RunMiddlewares::init(info, mws, err.into())); - } - } - } - } -} - -// waiting for response -struct WaitingResponse { - fut: Box>, - _s: PhantomData, - _h: PhantomData, -} - -impl WaitingResponse { - #[inline] - fn init( - info: &mut PipelineInfo, mws: &[Box>], - reply: AsyncResult, - ) -> PipelineState { - match reply.into() { - AsyncResultItem::Ok(resp) => RunMiddlewares::init(info, mws, resp), - AsyncResultItem::Err(err) => RunMiddlewares::init(info, mws, err.into()), - AsyncResultItem::Future(fut) => PipelineState::Handler(WaitingResponse { - fut, - _s: PhantomData, - _h: PhantomData, - }), - } - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - match self.fut.poll() { - Ok(Async::NotReady) => None, - Ok(Async::Ready(resp)) => Some(RunMiddlewares::init(info, mws, resp)), - Err(err) => Some(RunMiddlewares::init(info, mws, err.into())), - } - } -} - -/// Middlewares response executor -struct RunMiddlewares { - curr: usize, - fut: Option>>, - _s: PhantomData, - _h: PhantomData, -} - -impl RunMiddlewares { - #[inline] - fn init( - info: &mut PipelineInfo, mws: &[Box>], mut resp: HttpResponse, - ) -> PipelineState { - if info.count == 0 { - return ProcessResponse::init(resp); - } - let mut curr = 0; - let len = mws.len(); - - loop { - let state = mws[curr].response(&info.req, resp); - resp = match state { - Err(err) => { - info.count = (curr + 1) as u16; - return ProcessResponse::init(err.into()); - } - Ok(Response::Done(r)) => { - curr += 1; - if curr == len { - return ProcessResponse::init(r); - } else { - r - } - } - Ok(Response::Future(fut)) => { - return PipelineState::RunMiddlewares(RunMiddlewares { - curr, - fut: Some(fut), - _s: PhantomData, - _h: PhantomData, - }); - } - }; - } - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - let len = mws.len(); - - loop { - // poll latest fut - let mut resp = match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => return None, - Ok(Async::Ready(resp)) => { - self.curr += 1; - resp - } - Err(err) => return Some(ProcessResponse::init(err.into())), - }; - - loop { - if self.curr == len { - return Some(ProcessResponse::init(resp)); - } else { - let state = mws[self.curr].response(&info.req, resp); - match state { - Err(err) => return Some(ProcessResponse::init(err.into())), - Ok(Response::Done(r)) => { - self.curr += 1; - resp = r - } - Ok(Response::Future(fut)) => { - self.fut = Some(fut); - break; - } - } - } - } - } - } -} - -struct ProcessResponse { - resp: Option, - iostate: IOState, - running: RunningState, - drain: Option>, - _s: PhantomData, - _h: PhantomData, -} - -#[derive(PartialEq, Debug)] -enum RunningState { - Running, - Paused, - Done, -} - -impl RunningState { - #[inline] - fn pause(&mut self) { - if *self != RunningState::Done { - *self = RunningState::Paused - } - } - #[inline] - fn resume(&mut self) { - if *self != RunningState::Done { - *self = RunningState::Running - } - } -} - -enum IOState { - Response, - Payload(BodyStream), - Actor(Box), - Done, -} - -impl ProcessResponse { - #[inline] - fn init(resp: HttpResponse) -> PipelineState { - PipelineState::Response(ProcessResponse { - resp: Some(resp), - iostate: IOState::Response, - running: RunningState::Running, - drain: None, - _s: PhantomData, - _h: PhantomData, - }) - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - // connection is dead at this point - match mem::replace(&mut self.iostate, IOState::Done) { - IOState::Response => Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )), - IOState::Payload(_) => Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )), - IOState::Actor(mut ctx) => { - if info.disconnected.take().is_some() { - ctx.disconnected(); - } - loop { - match ctx.poll() { - Ok(Async::Ready(Some(vec))) => { - if vec.is_empty() { - continue; - } - for frame in vec { - match frame { - Frame::Chunk(None) => { - info.context = Some(ctx); - return Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - Frame::Chunk(Some(_)) => (), - Frame::Drain(fut) => { - let _ = fut.send(()); - } - } - } - } - Ok(Async::Ready(None)) => { - return Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )) - } - Ok(Async::NotReady) => { - self.iostate = IOState::Actor(ctx); - return None; - } - Err(err) => { - info.context = Some(ctx); - info.error = Some(err); - return Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - } - } - } - IOState::Done => Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )), - } - } - - fn poll_io( - mut self, io: &mut Writer, info: &mut PipelineInfo, - mws: &[Box>], - ) -> Result, PipelineState> { - loop { - if self.drain.is_none() && self.running != RunningState::Paused { - // if task is paused, write buffer is probably full - 'inner: loop { - let result = match mem::replace(&mut self.iostate, IOState::Done) { - IOState::Response => { - let encoding = self - .resp - .as_ref() - .unwrap() - .content_encoding() - .unwrap_or(info.encoding); - - let result = match io.start( - &info.req, - self.resp.as_mut().unwrap(), - encoding, - ) { - Ok(res) => res, - Err(err) => { - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - }; - - if let Some(err) = self.resp.as_ref().unwrap().error() { - if self.resp.as_ref().unwrap().status().is_server_error() - { - error!( - "Error occured during request handling, status: {} {}", - self.resp.as_ref().unwrap().status(), err - ); - } else { - warn!( - "Error occured during request handling: {}", - err - ); - } - if log_enabled!(Debug) { - debug!("{:?}", err); - } - } - - // always poll stream or actor for the first time - match self.resp.as_mut().unwrap().replace_body(Body::Empty) { - Body::Streaming(stream) => { - self.iostate = IOState::Payload(stream); - continue 'inner; - } - Body::Actor(ctx) => { - self.iostate = IOState::Actor(ctx); - continue 'inner; - } - _ => (), - } - - result - } - IOState::Payload(mut body) => match body.poll() { - Ok(Async::Ready(None)) => { - if let Err(err) = io.write_eof() { - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - break; - } - Ok(Async::Ready(Some(chunk))) => { - self.iostate = IOState::Payload(body); - match io.write(&chunk.into()) { - Err(err) => { - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - Ok(result) => result, - } - } - Ok(Async::NotReady) => { - self.iostate = IOState::Payload(body); - break; - } - Err(err) => { - info.error = Some(err); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - }, - IOState::Actor(mut ctx) => { - if info.disconnected.take().is_some() { - ctx.disconnected(); - } - match ctx.poll() { - Ok(Async::Ready(Some(vec))) => { - if vec.is_empty() { - self.iostate = IOState::Actor(ctx); - break; - } - let mut res = None; - for frame in vec { - match frame { - Frame::Chunk(None) => { - info.context = Some(ctx); - if let Err(err) = io.write_eof() { - info.error = Some(err.into()); - return Ok( - FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - ), - ); - } - break 'inner; - } - Frame::Chunk(Some(chunk)) => match io - .write(&chunk) - { - Err(err) => { - info.context = Some(ctx); - info.error = Some(err.into()); - return Ok( - FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - ), - ); - } - Ok(result) => res = Some(result), - }, - Frame::Drain(fut) => self.drain = Some(fut), - } - } - self.iostate = IOState::Actor(ctx); - if self.drain.is_some() { - self.running.resume(); - break 'inner; - } - res.unwrap() - } - Ok(Async::Ready(None)) => break, - Ok(Async::NotReady) => { - self.iostate = IOState::Actor(ctx); - break; - } - Err(err) => { - info.context = Some(ctx); - info.error = Some(err); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - } - } - IOState::Done => break, - }; - - match result { - WriterState::Pause => { - self.running.pause(); - break; - } - WriterState::Done => self.running.resume(), - } - } - } - - // flush io but only if we need to - if self.running == RunningState::Paused || self.drain.is_some() { - match io.poll_completed(false) { - Ok(Async::Ready(_)) => { - self.running.resume(); - - // resolve drain futures - if let Some(tx) = self.drain.take() { - let _ = tx.send(()); - } - // restart io processing - continue; - } - Ok(Async::NotReady) => return Err(PipelineState::Response(self)), - Err(err) => { - if let IOState::Actor(mut ctx) = - mem::replace(&mut self.iostate, IOState::Done) - { - ctx.disconnected(); - info.context = Some(ctx); - } - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - } - } - break; - } - - // response is completed - match self.iostate { - IOState::Done => { - match io.write_eof() { - Ok(_) => (), - Err(err) => { - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - } - self.resp.as_mut().unwrap().set_response_size(io.written()); - Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )) - } - _ => Err(PipelineState::Response(self)), - } - } -} - -/// Middlewares start executor -struct FinishingMiddlewares { - resp: Option, - fut: Option>>, - _s: PhantomData, - _h: PhantomData, -} - -impl FinishingMiddlewares { - #[inline] - fn init( - info: &mut PipelineInfo, mws: &[Box>], resp: HttpResponse, - ) -> PipelineState { - if info.count == 0 { - resp.release(); - Completed::init(info) - } else { - let mut state = FinishingMiddlewares { - resp: Some(resp), - fut: None, - _s: PhantomData, - _h: PhantomData, - }; - if let Some(st) = state.poll(info, mws) { - st - } else { - PipelineState::Finishing(state) - } - } - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - loop { - // poll latest fut - let not_ready = if let Some(ref mut fut) = self.fut { - match fut.poll() { - Ok(Async::NotReady) => true, - Ok(Async::Ready(())) => false, - Err(err) => { - error!("Middleware finish error: {}", err); - false - } - } - } else { - false - }; - if not_ready { - return None; - } - self.fut = None; - if info.count == 0 { - self.resp.take().unwrap().release(); - return Some(Completed::init(info)); - } - - info.count -= 1; - let state = - mws[info.count as usize].finish(&info.req, self.resp.as_ref().unwrap()); - match state { - Finished::Done => { - if info.count == 0 { - self.resp.take().unwrap().release(); - return Some(Completed::init(info)); - } - } - Finished::Future(fut) => { - self.fut = Some(fut); - } - } - } - } -} - -#[derive(Debug)] -struct Completed(PhantomData, PhantomData); - -impl Completed { - #[inline] - fn init(info: &mut PipelineInfo) -> PipelineState { - if let Some(ref err) = info.error { - error!("Error occurred during request handling: {}", err); - } - - if info.context.is_none() { - PipelineState::None - } else { - match info.poll_context() { - Ok(Async::NotReady) => { - PipelineState::Completed(Completed(PhantomData, PhantomData)) - } - Ok(Async::Ready(())) => PipelineState::None, - Err(_) => PipelineState::Error, - } - } - } - - #[inline] - fn poll(&mut self, info: &mut PipelineInfo) -> Option> { - match info.poll_context() { - Ok(Async::NotReady) => None, - Ok(Async::Ready(())) => Some(PipelineState::None), - Err(_) => Some(PipelineState::Error), - } - } -} diff --git a/src/pred.rs b/src/pred.rs deleted file mode 100644 index 99d6e608b..000000000 --- a/src/pred.rs +++ /dev/null @@ -1,328 +0,0 @@ -//! Route match predicates -#![allow(non_snake_case)] -use std::marker::PhantomData; - -use http; -use http::{header, HttpTryFrom}; -use server::message::Request; - -/// Trait defines resource route predicate. -/// Predicate can modify request object. It is also possible to -/// to store extra attributes on request by using `Extensions` container, -/// Extensions container available via `HttpRequest::extensions()` method. -pub trait Predicate { - /// Check if request matches predicate - fn check(&self, &Request, &S) -> bool; -} - -/// Return predicate that matches if any of supplied predicate matches. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{pred, App, HttpResponse}; -/// -/// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter(pred::Any(pred::Get()).or(pred::Post())) -/// .f(|r| HttpResponse::MethodNotAllowed()) -/// }); -/// } -/// ``` -pub fn Any + 'static>(pred: P) -> AnyPredicate { - AnyPredicate(vec![Box::new(pred)]) -} - -/// Matches if any of supplied predicate matches. -pub struct AnyPredicate(Vec>>); - -impl AnyPredicate { - /// Add new predicate to list of predicates to check - pub fn or + 'static>(mut self, pred: P) -> Self { - self.0.push(Box::new(pred)); - self - } -} - -impl Predicate for AnyPredicate { - fn check(&self, req: &Request, state: &S) -> bool { - for p in &self.0 { - if p.check(req, state) { - return true; - } - } - false - } -} - -/// Return predicate that matches if all of supplied predicate matches. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{pred, App, HttpResponse}; -/// -/// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter( -/// pred::All(pred::Get()) -/// .and(pred::Header("content-type", "text/plain")), -/// ) -/// .f(|_| HttpResponse::MethodNotAllowed()) -/// }); -/// } -/// ``` -pub fn All + 'static>(pred: P) -> AllPredicate { - AllPredicate(vec![Box::new(pred)]) -} - -/// Matches if all of supplied predicate matches. -pub struct AllPredicate(Vec>>); - -impl AllPredicate { - /// Add new predicate to list of predicates to check - pub fn and + 'static>(mut self, pred: P) -> Self { - self.0.push(Box::new(pred)); - self - } -} - -impl Predicate for AllPredicate { - fn check(&self, req: &Request, state: &S) -> bool { - for p in &self.0 { - if !p.check(req, state) { - return false; - } - } - true - } -} - -/// Return predicate that matches if supplied predicate does not match. -pub fn Not + 'static>(pred: P) -> NotPredicate { - NotPredicate(Box::new(pred)) -} - -#[doc(hidden)] -pub struct NotPredicate(Box>); - -impl Predicate for NotPredicate { - fn check(&self, req: &Request, state: &S) -> bool { - !self.0.check(req, state) - } -} - -/// Http method predicate -#[doc(hidden)] -pub struct MethodPredicate(http::Method, PhantomData); - -impl Predicate for MethodPredicate { - fn check(&self, req: &Request, _: &S) -> bool { - *req.method() == self.0 - } -} - -/// Predicate to match *GET* http method -pub fn Get() -> MethodPredicate { - MethodPredicate(http::Method::GET, PhantomData) -} - -/// Predicate to match *POST* http method -pub fn Post() -> MethodPredicate { - MethodPredicate(http::Method::POST, PhantomData) -} - -/// Predicate to match *PUT* http method -pub fn Put() -> MethodPredicate { - MethodPredicate(http::Method::PUT, PhantomData) -} - -/// Predicate to match *DELETE* http method -pub fn Delete() -> MethodPredicate { - MethodPredicate(http::Method::DELETE, PhantomData) -} - -/// Predicate to match *HEAD* http method -pub fn Head() -> MethodPredicate { - MethodPredicate(http::Method::HEAD, PhantomData) -} - -/// Predicate to match *OPTIONS* http method -pub fn Options() -> MethodPredicate { - MethodPredicate(http::Method::OPTIONS, PhantomData) -} - -/// Predicate to match *CONNECT* http method -pub fn Connect() -> MethodPredicate { - MethodPredicate(http::Method::CONNECT, PhantomData) -} - -/// Predicate to match *PATCH* http method -pub fn Patch() -> MethodPredicate { - MethodPredicate(http::Method::PATCH, PhantomData) -} - -/// Predicate to match *TRACE* http method -pub fn Trace() -> MethodPredicate { - MethodPredicate(http::Method::TRACE, PhantomData) -} - -/// Predicate to match specified http method -pub fn Method(method: http::Method) -> MethodPredicate { - MethodPredicate(method, PhantomData) -} - -/// Return predicate that matches if request contains specified header and -/// value. -pub fn Header( - name: &'static str, value: &'static str, -) -> HeaderPredicate { - HeaderPredicate( - header::HeaderName::try_from(name).unwrap(), - header::HeaderValue::from_static(value), - PhantomData, - ) -} - -#[doc(hidden)] -pub struct HeaderPredicate(header::HeaderName, header::HeaderValue, PhantomData); - -impl Predicate for HeaderPredicate { - fn check(&self, req: &Request, _: &S) -> bool { - if let Some(val) = req.headers().get(&self.0) { - return val == self.1; - } - false - } -} - -/// Return predicate that matches if request contains specified Host name. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{pred, App, HttpResponse}; -/// -/// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter(pred::Host("www.rust-lang.org")) -/// .f(|_| HttpResponse::MethodNotAllowed()) -/// }); -/// } -/// ``` -pub fn Host>(host: H) -> HostPredicate { - HostPredicate(host.as_ref().to_string(), None, PhantomData) -} - -#[doc(hidden)] -pub struct HostPredicate(String, Option, PhantomData); - -impl HostPredicate { - /// Set reuest scheme to match - pub fn scheme>(&mut self, scheme: H) { - self.1 = Some(scheme.as_ref().to_string()) - } -} - -impl Predicate for HostPredicate { - fn check(&self, req: &Request, _: &S) -> bool { - let info = req.connection_info(); - if let Some(ref scheme) = self.1 { - self.0 == info.host() && scheme == info.scheme() - } else { - self.0 == info.host() - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use http::{header, Method}; - use test::TestRequest; - - #[test] - fn test_header() { - let req = TestRequest::with_header( - header::TRANSFER_ENCODING, - header::HeaderValue::from_static("chunked"), - ).finish(); - - let pred = Header("transfer-encoding", "chunked"); - assert!(pred.check(&req, req.state())); - - let pred = Header("transfer-encoding", "other"); - assert!(!pred.check(&req, req.state())); - - let pred = Header("content-type", "other"); - assert!(!pred.check(&req, req.state())); - } - - #[test] - fn test_host() { - let req = TestRequest::default() - .header( - header::HOST, - header::HeaderValue::from_static("www.rust-lang.org"), - ).finish(); - - let pred = Host("www.rust-lang.org"); - assert!(pred.check(&req, req.state())); - - let pred = Host("localhost"); - assert!(!pred.check(&req, req.state())); - } - - #[test] - fn test_methods() { - let req = TestRequest::default().finish(); - let req2 = TestRequest::default().method(Method::POST).finish(); - - assert!(Get().check(&req, req.state())); - assert!(!Get().check(&req2, req2.state())); - assert!(Post().check(&req2, req2.state())); - assert!(!Post().check(&req, req.state())); - - let r = TestRequest::default().method(Method::PUT).finish(); - assert!(Put().check(&r, r.state())); - assert!(!Put().check(&req, req.state())); - - let r = TestRequest::default().method(Method::DELETE).finish(); - assert!(Delete().check(&r, r.state())); - assert!(!Delete().check(&req, req.state())); - - let r = TestRequest::default().method(Method::HEAD).finish(); - assert!(Head().check(&r, r.state())); - assert!(!Head().check(&req, req.state())); - - let r = TestRequest::default().method(Method::OPTIONS).finish(); - assert!(Options().check(&r, r.state())); - assert!(!Options().check(&req, req.state())); - - let r = TestRequest::default().method(Method::CONNECT).finish(); - assert!(Connect().check(&r, r.state())); - assert!(!Connect().check(&req, req.state())); - - let r = TestRequest::default().method(Method::PATCH).finish(); - assert!(Patch().check(&r, r.state())); - assert!(!Patch().check(&req, req.state())); - - let r = TestRequest::default().method(Method::TRACE).finish(); - assert!(Trace().check(&r, r.state())); - assert!(!Trace().check(&req, req.state())); - } - - #[test] - fn test_preds() { - let r = TestRequest::default().method(Method::TRACE).finish(); - - assert!(Not(Get()).check(&r, r.state())); - assert!(!Not(Trace()).check(&r, r.state())); - - assert!(All(Trace()).and(Trace()).check(&r, r.state())); - assert!(!All(Get()).and(Trace()).check(&r, r.state())); - - assert!(Any(Get()).or(Trace()).check(&r, r.state())); - assert!(!Any(Get()).or(Get()).check(&r, r.state())); - } -} diff --git a/src/resource.rs b/src/resource.rs deleted file mode 100644 index d884dd447..000000000 --- a/src/resource.rs +++ /dev/null @@ -1,324 +0,0 @@ -use std::ops::Deref; -use std::rc::Rc; - -use futures::Future; -use http::Method; -use smallvec::SmallVec; - -use error::Error; -use handler::{AsyncResult, FromRequest, Handler, Responder}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::Middleware; -use pred; -use route::Route; -use router::ResourceDef; -use with::WithFactory; - -#[derive(Copy, Clone)] -pub(crate) struct RouteId(usize); - -/// *Resource* is an entry in route table which corresponds to requested URL. -/// -/// Resource in turn has at least one route. -/// Route consists of an object that implements `Handler` trait (handler) -/// and list of predicates (objects that implement `Predicate` trait). -/// Route uses builder-like pattern for configuration. -/// During request handling, resource object iterate through all routes -/// and check all predicates for specific route, if request matches all -/// predicates route route considered matched and route handler get called. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{App, HttpResponse, http}; -/// -/// fn main() { -/// let app = App::new() -/// .resource( -/// "/", |r| r.method(http::Method::GET).f(|r| HttpResponse::Ok())) -/// .finish(); -/// } -pub struct Resource { - rdef: ResourceDef, - routes: SmallVec<[Route; 3]>, - middlewares: Rc>>>, -} - -impl Resource { - /// Create new resource with specified resource definition - pub fn new(rdef: ResourceDef) -> Self { - Resource { - rdef, - routes: SmallVec::new(), - middlewares: Rc::new(Vec::new()), - } - } - - /// Name of the resource - pub(crate) fn get_name(&self) -> &str { - self.rdef.name() - } - - /// Set resource name - pub fn name(&mut self, name: &str) { - self.rdef.set_name(name); - } - - /// Resource definition - pub fn rdef(&self) -> &ResourceDef { - &self.rdef - } -} - -impl Resource { - /// Register a new route and return mutable reference to *Route* object. - /// *Route* is used for route configuration, i.e. adding predicates, - /// setting up handler. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::*; - /// - /// fn main() { - /// let app = App::new() - /// .resource("/", |r| { - /// r.route() - /// .filter(pred::Any(pred::Get()).or(pred::Put())) - /// .filter(pred::Header("Content-Type", "text/plain")) - /// .f(|r| HttpResponse::Ok()) - /// }) - /// .finish(); - /// } - /// ``` - pub fn route(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap() - } - - /// Register a new `GET` route. - pub fn get(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Get()) - } - - /// Register a new `POST` route. - pub fn post(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Post()) - } - - /// Register a new `PUT` route. - pub fn put(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Put()) - } - - /// Register a new `DELETE` route. - pub fn delete(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Delete()) - } - - /// Register a new `HEAD` route. - pub fn head(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Head()) - } - - /// Register a new route and add method check to route. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::*; - /// fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// - /// App::new().resource("/", |r| r.method(http::Method::GET).f(index)); - /// ``` - /// - /// This is shortcut for: - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// # fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// App::new().resource("/", |r| r.route().filter(pred::Get()).f(index)); - /// ``` - pub fn method(&mut self, method: Method) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Method(method)) - } - - /// Register a new route and add handler object. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::*; - /// fn handler(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// - /// App::new().resource("/", |r| r.h(handler)); - /// ``` - /// - /// This is shortcut for: - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// # fn handler(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// App::new().resource("/", |r| r.route().h(handler)); - /// ``` - pub fn h>(&mut self, handler: H) { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().h(handler) - } - - /// Register a new route and add handler function. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::*; - /// fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// - /// App::new().resource("/", |r| r.f(index)); - /// ``` - /// - /// This is shortcut for: - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// # fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// App::new().resource("/", |r| r.route().f(index)); - /// ``` - pub fn f(&mut self, handler: F) - where - F: Fn(&HttpRequest) -> R + 'static, - R: Responder + 'static, - { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().f(handler) - } - - /// Register a new route and add handler. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::*; - /// fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } - /// - /// App::new().resource("/", |r| r.with(index)); - /// ``` - /// - /// This is shortcut for: - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } - /// App::new().resource("/", |r| r.route().with(index)); - /// ``` - pub fn with(&mut self, handler: F) - where - F: WithFactory, - R: Responder + 'static, - T: FromRequest + 'static, - { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().with(handler); - } - - /// Register a new route and add async handler. - /// - /// ```rust - /// # extern crate actix_web; - /// # extern crate futures; - /// use actix_web::*; - /// use futures::future::Future; - /// - /// fn index(req: HttpRequest) -> Box> { - /// unimplemented!() - /// } - /// - /// App::new().resource("/", |r| r.with_async(index)); - /// ``` - /// - /// This is shortcut for: - /// - /// ```rust - /// # extern crate actix_web; - /// # extern crate futures; - /// # use actix_web::*; - /// # use futures::future::Future; - /// # fn index(req: HttpRequest) -> Box> { - /// # unimplemented!() - /// # } - /// App::new().resource("/", |r| r.route().with_async(index)); - /// ``` - pub fn with_async(&mut self, handler: F) - where - F: Fn(T) -> R + 'static, - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, - { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().with_async(handler); - } - - /// Register a resource middleware - /// - /// This is similar to `App's` middlewares, but - /// middlewares get invoked on resource level. - /// - /// *Note* `Middleware::finish()` fires right after response get - /// prepared. It does not wait until body get sent to peer. - pub fn middleware>(&mut self, mw: M) { - Rc::get_mut(&mut self.middlewares) - .unwrap() - .push(Box::new(mw)); - } - - #[inline] - pub(crate) fn get_route_id(&self, req: &HttpRequest) -> Option { - for idx in 0..self.routes.len() { - if (&self.routes[idx]).check(req) { - return Some(RouteId(idx)); - } - } - None - } - - #[inline] - pub(crate) fn handle( - &self, id: RouteId, req: &HttpRequest, - ) -> AsyncResult { - if self.middlewares.is_empty() { - (&self.routes[id.0]).handle(req) - } else { - (&self.routes[id.0]).compose(req.clone(), Rc::clone(&self.middlewares)) - } - } -} - -/// Default resource -pub struct DefaultResource(Rc>); - -impl Deref for DefaultResource { - type Target = Resource; - - fn deref(&self) -> &Resource { - self.0.as_ref() - } -} - -impl Clone for DefaultResource { - fn clone(&self) -> Self { - DefaultResource(self.0.clone()) - } -} - -impl From> for DefaultResource { - fn from(res: Resource) -> Self { - DefaultResource(Rc::new(res)) - } -} diff --git a/src/route.rs b/src/route.rs deleted file mode 100644 index e4a7a9572..000000000 --- a/src/route.rs +++ /dev/null @@ -1,666 +0,0 @@ -use std::marker::PhantomData; -use std::rc::Rc; - -use futures::{Async, Future, Poll}; - -use error::Error; -use handler::{ - AsyncHandler, AsyncResult, AsyncResultItem, FromRequest, Handler, Responder, - RouteHandler, WrapHandler, -}; -use http::StatusCode; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{ - Finished as MiddlewareFinished, Middleware, Response as MiddlewareResponse, - Started as MiddlewareStarted, -}; -use pred::Predicate; -use with::{WithAsyncFactory, WithFactory}; - -/// Resource route definition -/// -/// Route uses builder-like pattern for configuration. -/// If handler is not explicitly set, default *404 Not Found* handler is used. -pub struct Route { - preds: Vec>>, - handler: InnerHandler, -} - -impl Default for Route { - fn default() -> Route { - Route { - preds: Vec::new(), - handler: InnerHandler::new(|_: &_| HttpResponse::new(StatusCode::NOT_FOUND)), - } - } -} - -impl Route { - #[inline] - pub(crate) fn check(&self, req: &HttpRequest) -> bool { - let state = req.state(); - for pred in &self.preds { - if !pred.check(req, state) { - return false; - } - } - true - } - - #[inline] - pub(crate) fn handle(&self, req: &HttpRequest) -> AsyncResult { - self.handler.handle(req) - } - - #[inline] - pub(crate) fn compose( - &self, req: HttpRequest, mws: Rc>>>, - ) -> AsyncResult { - AsyncResult::async(Box::new(Compose::new(req, mws, self.handler.clone()))) - } - - /// Add match predicate to route. - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// # fn main() { - /// App::new().resource("/path", |r| { - /// r.route() - /// .filter(pred::Get()) - /// .filter(pred::Header("content-type", "text/plain")) - /// .f(|req| HttpResponse::Ok()) - /// }) - /// # .finish(); - /// # } - /// ``` - pub fn filter + 'static>(&mut self, p: T) -> &mut Self { - self.preds.push(Box::new(p)); - self - } - - /// Set handler object. Usually call to this method is last call - /// during route configuration, so it does not return reference to self. - pub fn h>(&mut self, handler: H) { - self.handler = InnerHandler::new(handler); - } - - /// Set handler function. Usually call to this method is last call - /// during route configuration, so it does not return reference to self. - pub fn f(&mut self, handler: F) - where - F: Fn(&HttpRequest) -> R + 'static, - R: Responder + 'static, - { - self.handler = InnerHandler::new(handler); - } - - /// Set async handler function. - pub fn a(&mut self, handler: H) - where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - { - self.handler = InnerHandler::async(handler); - } - - /// Set handler function, use request extractor for parameters. - /// - /// ```rust - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; - /// #[macro_use] extern crate serde_derive; - /// use actix_web::{http, App, Path, Result}; - /// - /// #[derive(Deserialize)] - /// struct Info { - /// username: String, - /// } - /// - /// /// extract path info using serde - /// fn index(info: Path) -> Result { - /// Ok(format!("Welcome {}!", info.username)) - /// } - /// - /// fn main() { - /// let app = App::new().resource( - /// "/{username}/index.html", // <- define path parameters - /// |r| r.method(http::Method::GET).with(index), - /// ); // <- use `with` extractor - /// } - /// ``` - /// - /// It is possible to use multiple extractors for one handler function. - /// - /// ```rust - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; - /// #[macro_use] extern crate serde_derive; - /// # use std::collections::HashMap; - /// use actix_web::{http, App, Json, Path, Query, Result}; - /// - /// #[derive(Deserialize)] - /// struct Info { - /// username: String, - /// } - /// - /// /// extract path info using serde - /// fn index( - /// path: Path, query: Query>, body: Json, - /// ) -> Result { - /// Ok(format!("Welcome {}!", path.username)) - /// } - /// - /// fn main() { - /// let app = App::new().resource( - /// "/{username}/index.html", // <- define path parameters - /// |r| r.method(http::Method::GET).with(index), - /// ); // <- use `with` extractor - /// } - /// ``` - pub fn with(&mut self, handler: F) - where - F: WithFactory + 'static, - R: Responder + 'static, - T: FromRequest + 'static, - { - self.h(handler.create()); - } - - /// Set handler function. Same as `.with()` but it allows to configure - /// extractor. Configuration closure accepts config objects as tuple. - /// - /// ```rust - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; - /// #[macro_use] extern crate serde_derive; - /// use actix_web::{http, App, Path, Result}; - /// - /// /// extract text data from request - /// fn index(body: String) -> Result { - /// Ok(format!("Body {}!", body)) - /// } - /// - /// fn main() { - /// let app = App::new().resource("/index.html", |r| { - /// r.method(http::Method::GET) - /// .with_config(index, |cfg| { // <- register handler - /// cfg.0.limit(4096); // <- limit size of the payload - /// }) - /// }); - /// } - /// ``` - pub fn with_config(&mut self, handler: F, cfg_f: C) - where - F: WithFactory, - R: Responder + 'static, - T: FromRequest + 'static, - C: FnOnce(&mut T::Config), - { - let mut cfg = ::default(); - cfg_f(&mut cfg); - self.h(handler.create_with_config(cfg)); - } - - /// Set async handler function, use request extractor for parameters. - /// Also this method needs to be used if your handler function returns - /// `impl Future<>` - /// - /// ```rust - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; - /// #[macro_use] extern crate serde_derive; - /// use actix_web::{http, App, Error, Path}; - /// use futures::Future; - /// - /// #[derive(Deserialize)] - /// struct Info { - /// username: String, - /// } - /// - /// /// extract path info using serde - /// fn index(info: Path) -> Box> { - /// unimplemented!() - /// } - /// - /// fn main() { - /// let app = App::new().resource( - /// "/{username}/index.html", // <- define path parameters - /// |r| r.method(http::Method::GET).with_async(index), - /// ); // <- use `with` extractor - /// } - /// ``` - pub fn with_async(&mut self, handler: F) - where - F: WithAsyncFactory, - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, - { - self.h(handler.create()); - } - - /// Set async handler function, use request extractor for parameters. - /// This method allows to configure extractor. Configuration closure - /// accepts config objects as tuple. - /// - /// ```rust - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; - /// #[macro_use] extern crate serde_derive; - /// use actix_web::{http, App, Error, Form}; - /// use futures::Future; - /// - /// #[derive(Deserialize)] - /// struct Info { - /// username: String, - /// } - /// - /// /// extract path info using serde - /// fn index(info: Form) -> Box> { - /// unimplemented!() - /// } - /// - /// fn main() { - /// let app = App::new().resource( - /// "/{username}/index.html", // <- define path parameters - /// |r| r.method(http::Method::GET) - /// .with_async_config(index, |cfg| { - /// cfg.0.limit(4096); - /// }), - /// ); // <- use `with` extractor - /// } - /// ``` - pub fn with_async_config(&mut self, handler: F, cfg: C) - where - F: WithAsyncFactory, - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, - C: FnOnce(&mut T::Config), - { - let mut extractor_cfg = ::default(); - cfg(&mut extractor_cfg); - self.h(handler.create_with_config(extractor_cfg)); - } -} - -/// `RouteHandler` wrapper. This struct is required because it needs to be -/// shared for resource level middlewares. -struct InnerHandler(Rc>>); - -impl InnerHandler { - #[inline] - fn new>(h: H) -> Self { - InnerHandler(Rc::new(Box::new(WrapHandler::new(h)))) - } - - #[inline] - fn async(h: H) -> Self - where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - { - InnerHandler(Rc::new(Box::new(AsyncHandler::new(h)))) - } - - #[inline] - pub fn handle(&self, req: &HttpRequest) -> AsyncResult { - self.0.handle(req) - } -} - -impl Clone for InnerHandler { - #[inline] - fn clone(&self) -> Self { - InnerHandler(Rc::clone(&self.0)) - } -} - -/// Compose resource level middlewares with route handler. -struct Compose { - info: ComposeInfo, - state: ComposeState, -} - -struct ComposeInfo { - count: usize, - req: HttpRequest, - mws: Rc>>>, - handler: InnerHandler, -} - -enum ComposeState { - Starting(StartMiddlewares), - Handler(WaitingResponse), - RunMiddlewares(RunMiddlewares), - Finishing(FinishingMiddlewares), - Completed(Response), -} - -impl ComposeState { - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - match *self { - ComposeState::Starting(ref mut state) => state.poll(info), - ComposeState::Handler(ref mut state) => state.poll(info), - ComposeState::RunMiddlewares(ref mut state) => state.poll(info), - ComposeState::Finishing(ref mut state) => state.poll(info), - ComposeState::Completed(_) => None, - } - } -} - -impl Compose { - fn new( - req: HttpRequest, mws: Rc>>>, handler: InnerHandler, - ) -> Self { - let mut info = ComposeInfo { - count: 0, - req, - mws, - handler, - }; - let state = StartMiddlewares::init(&mut info); - - Compose { state, info } - } -} - -impl Future for Compose { - type Item = HttpResponse; - type Error = Error; - - fn poll(&mut self) -> Poll { - loop { - if let ComposeState::Completed(ref mut resp) = self.state { - let resp = resp.resp.take().unwrap(); - return Ok(Async::Ready(resp)); - } - if let Some(state) = self.state.poll(&mut self.info) { - self.state = state; - } else { - return Ok(Async::NotReady); - } - } - } -} - -/// Middlewares start executor -struct StartMiddlewares { - fut: Option, - _s: PhantomData, -} - -type Fut = Box, Error = Error>>; - -impl StartMiddlewares { - fn init(info: &mut ComposeInfo) -> ComposeState { - let len = info.mws.len(); - - loop { - if info.count == len { - let reply = info.handler.handle(&info.req); - return WaitingResponse::init(info, reply); - } else { - let result = info.mws[info.count].start(&info.req); - match result { - Ok(MiddlewareStarted::Done) => info.count += 1, - Ok(MiddlewareStarted::Response(resp)) => { - return RunMiddlewares::init(info, resp); - } - Ok(MiddlewareStarted::Future(fut)) => { - return ComposeState::Starting(StartMiddlewares { - fut: Some(fut), - _s: PhantomData, - }); - } - Err(err) => { - return RunMiddlewares::init(info, err.into()); - } - } - } - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - let len = info.mws.len(); - - 'outer: loop { - match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => { - return None; - } - Ok(Async::Ready(resp)) => { - info.count += 1; - if let Some(resp) = resp { - return Some(RunMiddlewares::init(info, resp)); - } - loop { - if info.count == len { - let reply = info.handler.handle(&info.req); - return Some(WaitingResponse::init(info, reply)); - } else { - let result = info.mws[info.count].start(&info.req); - match result { - Ok(MiddlewareStarted::Done) => info.count += 1, - Ok(MiddlewareStarted::Response(resp)) => { - return Some(RunMiddlewares::init(info, resp)); - } - Ok(MiddlewareStarted::Future(fut)) => { - self.fut = Some(fut); - continue 'outer; - } - Err(err) => { - return Some(RunMiddlewares::init(info, err.into())); - } - } - } - } - } - Err(err) => { - return Some(RunMiddlewares::init(info, err.into())); - } - } - } - } -} - -type HandlerFuture = Future; - -// waiting for response -struct WaitingResponse { - fut: Box, - _s: PhantomData, -} - -impl WaitingResponse { - #[inline] - fn init( - info: &mut ComposeInfo, reply: AsyncResult, - ) -> ComposeState { - match reply.into() { - AsyncResultItem::Ok(resp) => RunMiddlewares::init(info, resp), - AsyncResultItem::Err(err) => RunMiddlewares::init(info, err.into()), - AsyncResultItem::Future(fut) => ComposeState::Handler(WaitingResponse { - fut, - _s: PhantomData, - }), - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - match self.fut.poll() { - Ok(Async::NotReady) => None, - Ok(Async::Ready(resp)) => Some(RunMiddlewares::init(info, resp)), - Err(err) => Some(RunMiddlewares::init(info, err.into())), - } - } -} - -/// Middlewares response executor -struct RunMiddlewares { - curr: usize, - fut: Option>>, - _s: PhantomData, -} - -impl RunMiddlewares { - fn init(info: &mut ComposeInfo, mut resp: HttpResponse) -> ComposeState { - let mut curr = 0; - let len = info.mws.len(); - - loop { - let state = info.mws[curr].response(&info.req, resp); - resp = match state { - Err(err) => { - info.count = curr + 1; - return FinishingMiddlewares::init(info, err.into()); - } - Ok(MiddlewareResponse::Done(r)) => { - curr += 1; - if curr == len { - return FinishingMiddlewares::init(info, r); - } else { - r - } - } - Ok(MiddlewareResponse::Future(fut)) => { - return ComposeState::RunMiddlewares(RunMiddlewares { - curr, - fut: Some(fut), - _s: PhantomData, - }); - } - }; - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - let len = info.mws.len(); - - loop { - // poll latest fut - let mut resp = match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => return None, - Ok(Async::Ready(resp)) => { - self.curr += 1; - resp - } - Err(err) => return Some(FinishingMiddlewares::init(info, err.into())), - }; - - loop { - if self.curr == len { - return Some(FinishingMiddlewares::init(info, resp)); - } else { - let state = info.mws[self.curr].response(&info.req, resp); - match state { - Err(err) => { - return Some(FinishingMiddlewares::init(info, err.into())) - } - Ok(MiddlewareResponse::Done(r)) => { - self.curr += 1; - resp = r - } - Ok(MiddlewareResponse::Future(fut)) => { - self.fut = Some(fut); - break; - } - } - } - } - } - } -} - -/// Middlewares start executor -struct FinishingMiddlewares { - resp: Option, - fut: Option>>, - _s: PhantomData, -} - -impl FinishingMiddlewares { - fn init(info: &mut ComposeInfo, resp: HttpResponse) -> ComposeState { - if info.count == 0 { - Response::init(resp) - } else { - let mut state = FinishingMiddlewares { - resp: Some(resp), - fut: None, - _s: PhantomData, - }; - if let Some(st) = state.poll(info) { - st - } else { - ComposeState::Finishing(state) - } - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - loop { - // poll latest fut - let not_ready = if let Some(ref mut fut) = self.fut { - match fut.poll() { - Ok(Async::NotReady) => true, - Ok(Async::Ready(())) => false, - Err(err) => { - error!("Middleware finish error: {}", err); - false - } - } - } else { - false - }; - if not_ready { - return None; - } - self.fut = None; - if info.count == 0 { - return Some(Response::init(self.resp.take().unwrap())); - } - - info.count -= 1; - - let state = info.mws[info.count as usize] - .finish(&info.req, self.resp.as_ref().unwrap()); - match state { - MiddlewareFinished::Done => { - if info.count == 0 { - return Some(Response::init(self.resp.take().unwrap())); - } - } - MiddlewareFinished::Future(fut) => { - self.fut = Some(fut); - } - } - } - } -} - -struct Response { - resp: Option, - _s: PhantomData, -} - -impl Response { - fn init(resp: HttpResponse) -> ComposeState { - ComposeState::Completed(Response { - resp: Some(resp), - _s: PhantomData, - }) - } -} diff --git a/src/router.rs b/src/router.rs deleted file mode 100644 index aa15e46d2..000000000 --- a/src/router.rs +++ /dev/null @@ -1,1247 +0,0 @@ -use std::cell::RefCell; -use std::cmp::min; -use std::collections::HashMap; -use std::hash::{Hash, Hasher}; -use std::rc::Rc; - -use regex::{escape, Regex}; -use url::Url; - -use error::UrlGenerationError; -use handler::{AsyncResult, FromRequest, Responder, RouteHandler}; -use http::{Method, StatusCode}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use param::{ParamItem, Params}; -use pred::Predicate; -use resource::{DefaultResource, Resource}; -use scope::Scope; -use server::Request; -use with::WithFactory; - -#[derive(Debug, Copy, Clone, PartialEq)] -pub(crate) enum ResourceId { - Default, - Normal(u16), -} - -enum ResourcePattern { - Resource(ResourceDef), - Handler(ResourceDef, Option>>>), - Scope(ResourceDef, Vec>>), -} - -enum ResourceItem { - Resource(Resource), - Handler(Box>), - Scope(Scope), -} - -/// Interface for application router. -pub struct Router { - rmap: Rc, - patterns: Vec>, - resources: Vec>, - default: Option>, -} - -/// Information about current resource -#[derive(Clone)] -pub struct ResourceInfo { - rmap: Rc, - resource: ResourceId, - params: Params, - prefix: u16, -} - -impl ResourceInfo { - /// Name os the resource - #[inline] - pub fn name(&self) -> &str { - if let ResourceId::Normal(idx) = self.resource { - self.rmap.patterns[idx as usize].0.name() - } else { - "" - } - } - - /// This method returns reference to matched `ResourceDef` object. - #[inline] - pub fn rdef(&self) -> Option<&ResourceDef> { - if let ResourceId::Normal(idx) = self.resource { - Some(&self.rmap.patterns[idx as usize].0) - } else { - None - } - } - - pub(crate) fn set_prefix(&mut self, prefix: u16) { - self.prefix = prefix; - } - - /// Get a reference to the Params object. - /// - /// Params is a container for url parameters. - /// A variable segment is specified in the form `{identifier}`, - /// where the identifier can be used later in a request handler to - /// access the matched value for that segment. - #[inline] - pub fn match_info(&self) -> &Params { - &self.params - } - - #[inline] - pub(crate) fn merge(&mut self, info: &ResourceInfo) { - let mut p = info.params.clone(); - p.set_tail(self.params.tail); - for item in &self.params.segments { - p.add(item.0.clone(), item.1.clone()); - } - - self.prefix = info.params.tail; - self.params = p; - } - - /// Generate url for named resource - /// - /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. - /// url_for) for detailed information. - pub fn url_for( - &self, req: &Request, name: &str, elements: U, - ) -> Result - where - U: IntoIterator, - I: AsRef, - { - let mut path = String::new(); - let mut elements = elements.into_iter(); - - if self - .rmap - .patterns_for(name, &mut path, &mut elements)? - .is_some() - { - if path.starts_with('/') { - let conn = req.connection_info(); - Ok(Url::parse(&format!( - "{}://{}{}", - conn.scheme(), - conn.host(), - path - ))?) - } else { - Ok(Url::parse(&path)?) - } - } else { - Err(UrlGenerationError::ResourceNotFound) - } - } - - /// Check if application contains matching resource. - /// - /// This method does not take `prefix` into account. - /// For example if prefix is `/test` and router contains route `/name`, - /// following path would be recognizable `/test/name` but `has_resource()` call - /// would return `false`. - pub fn has_resource(&self, path: &str) -> bool { - self.rmap.has_resource(path) - } - - /// Check if application contains matching resource. - /// - /// This method does take `prefix` into account - /// but behaves like `has_route` in case `prefix` is not set in the router. - /// - /// For example if prefix is `/test` and router contains route `/name`, the - /// following path would be recognizable `/test/name` and `has_prefixed_route()` call - /// would return `true`. - /// It will not match against prefix in case it's not given. For example for `/name` - /// with a `/test` prefix would return `false` - pub fn has_prefixed_resource(&self, path: &str) -> bool { - let prefix = self.prefix as usize; - if prefix >= path.len() { - return false; - } - self.rmap.has_resource(&path[prefix..]) - } -} - -pub(crate) struct ResourceMap { - root: ResourceDef, - parent: RefCell>>, - named: HashMap, - patterns: Vec<(ResourceDef, Option>)>, - nested: Vec>, -} - -impl ResourceMap { - fn has_resource(&self, path: &str) -> bool { - let path = if path.is_empty() { "/" } else { path }; - - for (pattern, rmap) in &self.patterns { - if let Some(ref rmap) = rmap { - if let Some(plen) = pattern.is_prefix_match(path) { - return rmap.has_resource(&path[plen..]); - } - } else if pattern.is_match(path) { - return true; - } - } - false - } - - fn patterns_for( - &self, name: &str, path: &mut String, elements: &mut U, - ) -> Result, UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if self.pattern_for(name, path, elements)?.is_some() { - Ok(Some(())) - } else { - self.parent_pattern_for(name, path, elements) - } - } - - fn pattern_for( - &self, name: &str, path: &mut String, elements: &mut U, - ) -> Result, UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if let Some(pattern) = self.named.get(name) { - self.fill_root(path, elements)?; - pattern.resource_path(path, elements)?; - Ok(Some(())) - } else { - for rmap in &self.nested { - if rmap.pattern_for(name, path, elements)?.is_some() { - return Ok(Some(())); - } - } - Ok(None) - } - } - - fn fill_root( - &self, path: &mut String, elements: &mut U, - ) -> Result<(), UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if let Some(ref parent) = *self.parent.borrow() { - parent.fill_root(path, elements)?; - } - self.root.resource_path(path, elements) - } - - fn parent_pattern_for( - &self, name: &str, path: &mut String, elements: &mut U, - ) -> Result, UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if let Some(ref parent) = *self.parent.borrow() { - if let Some(pattern) = parent.named.get(name) { - self.fill_root(path, elements)?; - pattern.resource_path(path, elements)?; - Ok(Some(())) - } else { - parent.parent_pattern_for(name, path, elements) - } - } else { - Ok(None) - } - } -} - -impl Default for Router { - fn default() -> Self { - Router::new(ResourceDef::new("")) - } -} - -impl Router { - pub(crate) fn new(root: ResourceDef) -> Self { - Router { - rmap: Rc::new(ResourceMap { - root, - parent: RefCell::new(None), - named: HashMap::new(), - patterns: Vec::new(), - nested: Vec::new(), - }), - resources: Vec::new(), - patterns: Vec::new(), - default: None, - } - } - - #[inline] - pub(crate) fn route_info_params(&self, idx: u16, params: Params) -> ResourceInfo { - ResourceInfo { - params, - prefix: 0, - rmap: self.rmap.clone(), - resource: ResourceId::Normal(idx), - } - } - - #[cfg(test)] - pub(crate) fn default_route_info(&self) -> ResourceInfo { - ResourceInfo { - params: Params::new(), - rmap: self.rmap.clone(), - resource: ResourceId::Default, - prefix: 0, - } - } - - pub(crate) fn set_prefix(&mut self, path: &str) { - Rc::get_mut(&mut self.rmap).unwrap().root = ResourceDef::new(path); - } - - pub(crate) fn register_resource(&mut self, resource: Resource) { - { - let rmap = Rc::get_mut(&mut self.rmap).unwrap(); - - let name = resource.get_name(); - if !name.is_empty() { - assert!( - !rmap.named.contains_key(name), - "Named resource {:?} is registered.", - name - ); - rmap.named.insert(name.to_owned(), resource.rdef().clone()); - } - rmap.patterns.push((resource.rdef().clone(), None)); - } - self.patterns - .push(ResourcePattern::Resource(resource.rdef().clone())); - self.resources.push(ResourceItem::Resource(resource)); - } - - pub(crate) fn register_scope(&mut self, mut scope: Scope) { - Rc::get_mut(&mut self.rmap) - .unwrap() - .patterns - .push((scope.rdef().clone(), Some(scope.router().rmap.clone()))); - Rc::get_mut(&mut self.rmap) - .unwrap() - .nested - .push(scope.router().rmap.clone()); - - let filters = scope.take_filters(); - self.patterns - .push(ResourcePattern::Scope(scope.rdef().clone(), filters)); - self.resources.push(ResourceItem::Scope(scope)); - } - - pub(crate) fn register_handler( - &mut self, path: &str, hnd: Box>, - filters: Option>>>, - ) { - let rdef = ResourceDef::prefix(path); - Rc::get_mut(&mut self.rmap) - .unwrap() - .patterns - .push((rdef.clone(), None)); - self.resources.push(ResourceItem::Handler(hnd)); - self.patterns.push(ResourcePattern::Handler(rdef, filters)); - } - - pub(crate) fn has_default_resource(&self) -> bool { - self.default.is_some() - } - - pub(crate) fn register_default_resource(&mut self, resource: DefaultResource) { - self.default = Some(resource); - } - - pub(crate) fn finish(&mut self) { - for resource in &mut self.resources { - match resource { - ResourceItem::Resource(_) => (), - ResourceItem::Scope(scope) => { - if !scope.has_default_resource() { - if let Some(ref default) = self.default { - scope.default_resource(default.clone()); - } - } - *scope.router().rmap.parent.borrow_mut() = Some(self.rmap.clone()); - scope.finish(); - } - ResourceItem::Handler(hnd) => { - if !hnd.has_default_resource() { - if let Some(ref default) = self.default { - hnd.default_resource(default.clone()); - } - } - hnd.finish() - } - } - } - } - - pub(crate) fn register_external(&mut self, name: &str, rdef: ResourceDef) { - let rmap = Rc::get_mut(&mut self.rmap).unwrap(); - assert!( - !rmap.named.contains_key(name), - "Named resource {:?} is registered.", - name - ); - rmap.named.insert(name.to_owned(), rdef); - } - - pub(crate) fn register_route(&mut self, path: &str, method: Method, f: F) - where - F: WithFactory, - R: Responder + 'static, - T: FromRequest + 'static, - { - let out = { - // get resource handler - let mut iterator = self.resources.iter_mut(); - - loop { - if let Some(ref mut resource) = iterator.next() { - if let ResourceItem::Resource(ref mut resource) = resource { - if resource.rdef().pattern() == path { - resource.method(method).with(f); - break None; - } - } - } else { - let mut resource = Resource::new(ResourceDef::new(path)); - resource.method(method).with(f); - break Some(resource); - } - } - }; - if let Some(out) = out { - self.register_resource(out); - } - } - - /// Handle request - pub fn handle(&self, req: &HttpRequest) -> AsyncResult { - let resource = match req.resource().resource { - ResourceId::Normal(idx) => &self.resources[idx as usize], - ResourceId::Default => { - if let Some(ref default) = self.default { - if let Some(id) = default.get_route_id(req) { - return default.handle(id, req); - } - } - return AsyncResult::ok(HttpResponse::new(StatusCode::NOT_FOUND)); - } - }; - match resource { - ResourceItem::Resource(ref resource) => { - if let Some(id) = resource.get_route_id(req) { - return resource.handle(id, req); - } - - if let Some(ref default) = self.default { - if let Some(id) = default.get_route_id(req) { - return default.handle(id, req); - } - } - } - ResourceItem::Handler(hnd) => return hnd.handle(req), - ResourceItem::Scope(hnd) => return hnd.handle(req), - } - AsyncResult::ok(HttpResponse::new(StatusCode::NOT_FOUND)) - } - - /// Query for matched resource - pub fn recognize(&self, req: &Request, state: &S, tail: usize) -> ResourceInfo { - if tail <= req.path().len() { - 'outer: for (idx, resource) in self.patterns.iter().enumerate() { - match resource { - ResourcePattern::Resource(rdef) => { - if let Some(params) = rdef.match_with_params(req, tail) { - return self.route_info_params(idx as u16, params); - } - } - ResourcePattern::Handler(rdef, filters) => { - if let Some(params) = rdef.match_prefix_with_params(req, tail) { - if let Some(ref filters) = filters { - for filter in filters { - if !filter.check(req, state) { - continue 'outer; - } - } - } - return self.route_info_params(idx as u16, params); - } - } - ResourcePattern::Scope(rdef, filters) => { - if let Some(params) = rdef.match_prefix_with_params(req, tail) { - for filter in filters { - if !filter.check(req, state) { - continue 'outer; - } - } - return self.route_info_params(idx as u16, params); - } - } - } - } - } - ResourceInfo { - prefix: tail as u16, - params: Params::new(), - rmap: self.rmap.clone(), - resource: ResourceId::Default, - } - } -} - -#[derive(Debug, Clone, PartialEq)] -enum PatternElement { - Str(String), - Var(String), -} - -#[derive(Clone, Debug)] -enum PatternType { - Static(String), - Prefix(String), - Dynamic(Regex, Vec>, usize), -} - -#[derive(Debug, Copy, Clone, PartialEq)] -/// Resource type -pub enum ResourceType { - /// Normal resource - Normal, - /// Resource for application default handler - Default, - /// External resource - External, - /// Unknown resource type - Unset, -} - -/// Resource type describes an entry in resources table -#[derive(Clone, Debug)] -pub struct ResourceDef { - tp: PatternType, - rtp: ResourceType, - name: String, - pattern: String, - elements: Vec, -} - -impl ResourceDef { - /// Parse path pattern and create new `ResourceDef` instance. - /// - /// Panics if path pattern is wrong. - pub fn new(path: &str) -> Self { - ResourceDef::with_prefix(path, false, !path.is_empty()) - } - - /// Parse path pattern and create new `ResourceDef` instance. - /// - /// Use `prefix` type instead of `static`. - /// - /// Panics if path regex pattern is wrong. - pub fn prefix(path: &str) -> Self { - ResourceDef::with_prefix(path, true, !path.is_empty()) - } - - /// Construct external resource def - /// - /// Panics if path pattern is wrong. - pub fn external(path: &str) -> Self { - let mut resource = ResourceDef::with_prefix(path, false, false); - resource.rtp = ResourceType::External; - resource - } - - /// Parse path pattern and create new `ResourceDef` instance with custom prefix - pub fn with_prefix(path: &str, for_prefix: bool, slash: bool) -> Self { - let mut path = path.to_owned(); - if slash && !path.starts_with('/') { - path.insert(0, '/'); - } - let (pattern, elements, is_dynamic, len) = ResourceDef::parse(&path, for_prefix); - - let tp = if is_dynamic { - let re = match Regex::new(&pattern) { - Ok(re) => re, - Err(err) => panic!("Wrong path pattern: \"{}\" {}", path, err), - }; - // actix creates one router per thread - let names = re - .capture_names() - .filter_map(|name| name.map(|name| Rc::new(name.to_owned()))) - .collect(); - PatternType::Dynamic(re, names, len) - } else if for_prefix { - PatternType::Prefix(pattern.clone()) - } else { - PatternType::Static(pattern.clone()) - }; - - ResourceDef { - tp, - elements, - name: "".to_string(), - rtp: ResourceType::Normal, - pattern: path.to_owned(), - } - } - - /// Resource type - pub fn rtype(&self) -> ResourceType { - self.rtp - } - - /// Resource name - pub fn name(&self) -> &str { - &self.name - } - - /// Resource name - pub(crate) fn set_name(&mut self, name: &str) { - self.name = name.to_owned(); - } - - /// Path pattern of the resource - pub fn pattern(&self) -> &str { - &self.pattern - } - - /// Is this path a match against this resource? - pub fn is_match(&self, path: &str) -> bool { - match self.tp { - PatternType::Static(ref s) => s == path, - PatternType::Dynamic(ref re, _, _) => re.is_match(path), - PatternType::Prefix(ref s) => path.starts_with(s), - } - } - - fn is_prefix_match(&self, path: &str) -> Option { - let plen = path.len(); - let path = if path.is_empty() { "/" } else { path }; - - match self.tp { - PatternType::Static(ref s) => if s == path { - Some(plen) - } else { - None - }, - PatternType::Dynamic(ref re, _, len) => { - if let Some(captures) = re.captures(path) { - let mut pos = 0; - let mut passed = false; - for capture in captures.iter() { - if let Some(ref m) = capture { - if !passed { - passed = true; - continue; - } - - pos = m.end(); - } - } - Some(plen + pos + len) - } else { - None - } - } - PatternType::Prefix(ref s) => { - let len = if path == s { - s.len() - } else if path.starts_with(s) - && (s.ends_with('/') || path.split_at(s.len()).1.starts_with('/')) - { - if s.ends_with('/') { - s.len() - 1 - } else { - s.len() - } - } else { - return None; - }; - Some(min(plen, len)) - } - } - } - - /// Are the given path and parameters a match against this resource? - pub fn match_with_params(&self, req: &Request, plen: usize) -> Option { - let path = &req.path()[plen..]; - - match self.tp { - PatternType::Static(ref s) => if s != path { - None - } else { - Some(Params::with_url(req.url())) - }, - PatternType::Dynamic(ref re, ref names, _) => { - if let Some(captures) = re.captures(path) { - let mut params = Params::with_url(req.url()); - let mut idx = 0; - let mut passed = false; - for capture in captures.iter() { - if let Some(ref m) = capture { - if !passed { - passed = true; - continue; - } - params.add( - names[idx].clone(), - ParamItem::UrlSegment( - (plen + m.start()) as u16, - (plen + m.end()) as u16, - ), - ); - idx += 1; - } - } - params.set_tail(req.path().len() as u16); - Some(params) - } else { - None - } - } - PatternType::Prefix(ref s) => if !path.starts_with(s) { - None - } else { - Some(Params::with_url(req.url())) - }, - } - } - - /// Is the given path a prefix match and do the parameters match against this resource? - pub fn match_prefix_with_params( - &self, req: &Request, plen: usize, - ) -> Option { - let path = &req.path()[plen..]; - let path = if path.is_empty() { "/" } else { path }; - - match self.tp { - PatternType::Static(ref s) => if s == path { - let mut params = Params::with_url(req.url()); - params.set_tail(req.path().len() as u16); - Some(params) - } else { - None - }, - PatternType::Dynamic(ref re, ref names, len) => { - if let Some(captures) = re.captures(path) { - let mut params = Params::with_url(req.url()); - let mut pos = 0; - let mut passed = false; - let mut idx = 0; - for capture in captures.iter() { - if let Some(ref m) = capture { - if !passed { - passed = true; - continue; - } - - params.add( - names[idx].clone(), - ParamItem::UrlSegment( - (plen + m.start()) as u16, - (plen + m.end()) as u16, - ), - ); - idx += 1; - pos = m.end(); - } - } - params.set_tail((plen + pos + len) as u16); - Some(params) - } else { - None - } - } - PatternType::Prefix(ref s) => { - let len = if path == s { - s.len() - } else if path.starts_with(s) - && (s.ends_with('/') || path.split_at(s.len()).1.starts_with('/')) - { - if s.ends_with('/') { - s.len() - 1 - } else { - s.len() - } - } else { - return None; - }; - let mut params = Params::with_url(req.url()); - params.set_tail(min(req.path().len(), plen + len) as u16); - Some(params) - } - } - } - - /// Build resource path. - pub fn resource_path( - &self, path: &mut String, elements: &mut U, - ) -> Result<(), UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - match self.tp { - PatternType::Prefix(ref p) => path.push_str(p), - PatternType::Static(ref p) => path.push_str(p), - PatternType::Dynamic(..) => { - for el in &self.elements { - match *el { - PatternElement::Str(ref s) => path.push_str(s), - PatternElement::Var(_) => { - if let Some(val) = elements.next() { - path.push_str(val.as_ref()) - } else { - return Err(UrlGenerationError::NotEnoughElements); - } - } - } - } - } - }; - Ok(()) - } - - fn parse_param(pattern: &str) -> (PatternElement, String, &str) { - const DEFAULT_PATTERN: &str = "[^/]+"; - let mut params_nesting = 0usize; - let close_idx = pattern - .find(|c| match c { - '{' => { - params_nesting += 1; - false - } - '}' => { - params_nesting -= 1; - params_nesting == 0 - } - _ => false, - }).expect("malformed param"); - let (mut param, rem) = pattern.split_at(close_idx + 1); - param = ¶m[1..param.len() - 1]; // Remove outer brackets - let (name, pattern) = match param.find(':') { - Some(idx) => { - let (name, pattern) = param.split_at(idx); - (name, &pattern[1..]) - } - None => (param, DEFAULT_PATTERN), - }; - ( - PatternElement::Var(name.to_string()), - format!(r"(?P<{}>{})", &name, &pattern), - rem, - ) - } - - fn parse( - mut pattern: &str, for_prefix: bool, - ) -> (String, Vec, bool, usize) { - if pattern.find('{').is_none() { - return ( - String::from(pattern), - vec![PatternElement::Str(String::from(pattern))], - false, - pattern.chars().count(), - ); - }; - - let mut elems = Vec::new(); - let mut re = String::from("^"); - - while let Some(idx) = pattern.find('{') { - let (prefix, rem) = pattern.split_at(idx); - elems.push(PatternElement::Str(String::from(prefix))); - re.push_str(&escape(prefix)); - let (param_pattern, re_part, rem) = Self::parse_param(rem); - elems.push(param_pattern); - re.push_str(&re_part); - pattern = rem; - } - - elems.push(PatternElement::Str(String::from(pattern))); - re.push_str(&escape(pattern)); - - if !for_prefix { - re.push_str("$"); - } - - (re, elems, true, pattern.chars().count()) - } -} - -impl PartialEq for ResourceDef { - fn eq(&self, other: &ResourceDef) -> bool { - self.pattern == other.pattern - } -} - -impl Eq for ResourceDef {} - -impl Hash for ResourceDef { - fn hash(&self, state: &mut H) { - self.pattern.hash(state); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use test::TestRequest; - - #[test] - fn test_recognizer10() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/name"))); - router.register_resource(Resource::new(ResourceDef::new("/name/{val}"))); - router.register_resource(Resource::new(ResourceDef::new( - "/name/{val}/index.html", - ))); - router.register_resource(Resource::new(ResourceDef::new("/file/{file}.{ext}"))); - router.register_resource(Resource::new(ResourceDef::new( - "/v{val}/{val2}/index.html", - ))); - router.register_resource(Resource::new(ResourceDef::new("/v/{tail:.*}"))); - router.register_resource(Resource::new(ResourceDef::new("/test2/{test}.html"))); - router.register_resource(Resource::new(ResourceDef::new("/{test}/index.html"))); - - let req = TestRequest::with_uri("/name").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(0)); - assert!(info.match_info().is_empty()); - - let req = TestRequest::with_uri("/name/value").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(1)); - assert_eq!(info.match_info().get("val").unwrap(), "value"); - assert_eq!(&info.match_info()["val"], "value"); - - let req = TestRequest::with_uri("/name/value2/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(2)); - assert_eq!(info.match_info().get("val").unwrap(), "value2"); - - let req = TestRequest::with_uri("/file/file.gz").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(3)); - assert_eq!(info.match_info().get("file").unwrap(), "file"); - assert_eq!(info.match_info().get("ext").unwrap(), "gz"); - - let req = TestRequest::with_uri("/vtest/ttt/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(4)); - assert_eq!(info.match_info().get("val").unwrap(), "test"); - assert_eq!(info.match_info().get("val2").unwrap(), "ttt"); - - let req = TestRequest::with_uri("/v/blah-blah/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(5)); - assert_eq!( - info.match_info().get("tail").unwrap(), - "blah-blah/index.html" - ); - - let req = TestRequest::with_uri("/test2/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(6)); - assert_eq!(info.match_info().get("test").unwrap(), "index"); - - let req = TestRequest::with_uri("/bbb/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(7)); - assert_eq!(info.match_info().get("test").unwrap(), "bbb"); - } - - #[test] - fn test_recognizer_2() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/index.json"))); - router.register_resource(Resource::new(ResourceDef::new("/{source}.json"))); - - let req = TestRequest::with_uri("/index.json").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(0)); - - let req = TestRequest::with_uri("/test.json").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(1)); - } - - #[test] - fn test_recognizer_with_prefix() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/name"))); - router.register_resource(Resource::new(ResourceDef::new("/name/{val}"))); - - let req = TestRequest::with_uri("/name").finish(); - let info = router.recognize(&req, &(), 5); - assert_eq!(info.resource, ResourceId::Default); - - let req = TestRequest::with_uri("/test/name").finish(); - let info = router.recognize(&req, &(), 5); - assert_eq!(info.resource, ResourceId::Normal(0)); - - let req = TestRequest::with_uri("/test/name/value").finish(); - let info = router.recognize(&req, &(), 5); - assert_eq!(info.resource, ResourceId::Normal(1)); - assert_eq!(info.match_info().get("val").unwrap(), "value"); - assert_eq!(&info.match_info()["val"], "value"); - - // same patterns - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/name"))); - router.register_resource(Resource::new(ResourceDef::new("/name/{val}"))); - - let req = TestRequest::with_uri("/name").finish(); - let info = router.recognize(&req, &(), 6); - assert_eq!(info.resource, ResourceId::Default); - - let req = TestRequest::with_uri("/test2/name").finish(); - let info = router.recognize(&req, &(), 6); - assert_eq!(info.resource, ResourceId::Normal(0)); - - let req = TestRequest::with_uri("/test2/name-test").finish(); - let info = router.recognize(&req, &(), 6); - assert_eq!(info.resource, ResourceId::Default); - - let req = TestRequest::with_uri("/test2/name/ttt").finish(); - let info = router.recognize(&req, &(), 6); - assert_eq!(info.resource, ResourceId::Normal(1)); - assert_eq!(&info.match_info()["val"], "ttt"); - } - - #[test] - fn test_parse_static() { - let re = ResourceDef::new("/"); - assert!(re.is_match("/")); - assert!(!re.is_match("/a")); - - let re = ResourceDef::new("/name"); - assert!(re.is_match("/name")); - assert!(!re.is_match("/name1")); - assert!(!re.is_match("/name/")); - assert!(!re.is_match("/name~")); - - let re = ResourceDef::new("/name/"); - assert!(re.is_match("/name/")); - assert!(!re.is_match("/name")); - assert!(!re.is_match("/name/gs")); - - let re = ResourceDef::new("/user/profile"); - assert!(re.is_match("/user/profile")); - assert!(!re.is_match("/user/profile/profile")); - } - - #[test] - fn test_parse_param() { - let re = ResourceDef::new("/user/{id}"); - assert!(re.is_match("/user/profile")); - assert!(re.is_match("/user/2345")); - assert!(!re.is_match("/user/2345/")); - assert!(!re.is_match("/user/2345/sdg")); - - let req = TestRequest::with_uri("/user/profile").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(info.get("id").unwrap(), "profile"); - - let req = TestRequest::with_uri("/user/1245125").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(info.get("id").unwrap(), "1245125"); - - let re = ResourceDef::new("/v{version}/resource/{id}"); - assert!(re.is_match("/v1/resource/320120")); - assert!(!re.is_match("/v/resource/1")); - assert!(!re.is_match("/resource")); - - let req = TestRequest::with_uri("/v151/resource/adahg32").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(info.get("version").unwrap(), "151"); - assert_eq!(info.get("id").unwrap(), "adahg32"); - - let re = ResourceDef::new("/{id:[[:digit:]]{6}}"); - assert!(re.is_match("/012345")); - assert!(!re.is_match("/012")); - assert!(!re.is_match("/01234567")); - assert!(!re.is_match("/XXXXXX")); - - let req = TestRequest::with_uri("/012345").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(info.get("id").unwrap(), "012345"); - } - - #[test] - fn test_resource_prefix() { - let re = ResourceDef::prefix("/name"); - assert!(re.is_match("/name")); - assert!(re.is_match("/name/")); - assert!(re.is_match("/name/test/test")); - assert!(re.is_match("/name1")); - assert!(re.is_match("/name~")); - - let re = ResourceDef::prefix("/name/"); - assert!(re.is_match("/name/")); - assert!(re.is_match("/name/gs")); - assert!(!re.is_match("/name")); - } - - #[test] - fn test_reousrce_prefix_dynamic() { - let re = ResourceDef::prefix("/{name}/"); - assert!(re.is_match("/name/")); - assert!(re.is_match("/name/gs")); - assert!(!re.is_match("/name")); - - let req = TestRequest::with_uri("/test2/").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(&info["name"], "test2"); - assert_eq!(&info[0], "test2"); - - let req = TestRequest::with_uri("/test2/subpath1/subpath2/index.html").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(&info["name"], "test2"); - assert_eq!(&info[0], "test2"); - } - - #[test] - fn test_request_resource() { - let mut router = Router::<()>::default(); - let mut resource = Resource::new(ResourceDef::new("/index.json")); - resource.name("r1"); - router.register_resource(resource); - let mut resource = Resource::new(ResourceDef::new("/test.json")); - resource.name("r2"); - router.register_resource(resource); - - let req = TestRequest::with_uri("/index.json").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(0)); - - assert_eq!(info.name(), "r1"); - - let req = TestRequest::with_uri("/test.json").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(1)); - assert_eq!(info.name(), "r2"); - } - - #[test] - fn test_has_resource() { - let mut router = Router::<()>::default(); - let scope = Scope::new("/test").resource("/name", |_| "done"); - router.register_scope(scope); - - { - let info = router.default_route_info(); - assert!(!info.has_resource("/test")); - assert!(info.has_resource("/test/name")); - } - - let scope = - Scope::new("/test2").nested("/test10", |s| s.resource("/name", |_| "done")); - router.register_scope(scope); - - let info = router.default_route_info(); - assert!(info.has_resource("/test2/test10/name")); - } - - #[test] - fn test_url_for() { - let mut router = Router::<()>::new(ResourceDef::prefix("")); - - let mut resource = Resource::new(ResourceDef::new("/tttt")); - resource.name("r0"); - router.register_resource(resource); - - let scope = Scope::new("/test").resource("/name", |r| { - r.name("r1"); - }); - router.register_scope(scope); - - let scope = Scope::new("/test2") - .nested("/test10", |s| s.resource("/name", |r| r.name("r2"))); - router.register_scope(scope); - router.finish(); - - let req = TestRequest::with_uri("/test").request(); - { - let info = router.default_route_info(); - - let res = info - .url_for(&req, "r0", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/tttt"); - - let res = info - .url_for(&req, "r1", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/test/name"); - - let res = info - .url_for(&req, "r2", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/test2/test10/name"); - } - - let req = TestRequest::with_uri("/test/name").request(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(1)); - - let res = info - .url_for(&req, "r0", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/tttt"); - - let res = info - .url_for(&req, "r1", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/test/name"); - - let res = info - .url_for(&req, "r2", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/test2/test10/name"); - } - - #[test] - fn test_url_for_dynamic() { - let mut router = Router::<()>::new(ResourceDef::prefix("")); - - let mut resource = Resource::new(ResourceDef::new("/{name}/test/index.{ext}")); - resource.name("r0"); - router.register_resource(resource); - - let scope = Scope::new("/{name1}").nested("/{name2}", |s| { - s.resource("/{name3}/test/index.{ext}", |r| r.name("r2")) - }); - router.register_scope(scope); - router.finish(); - - let req = TestRequest::with_uri("/test").request(); - { - let info = router.default_route_info(); - - let res = info.url_for(&req, "r0", vec!["sec1", "html"]).unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/sec1/test/index.html"); - - let res = info - .url_for(&req, "r2", vec!["sec1", "sec2", "sec3", "html"]) - .unwrap(); - assert_eq!( - res.as_str(), - "http://localhost:8080/sec1/sec2/sec3/test/index.html" - ); - } - } -} diff --git a/src/scope.rs b/src/scope.rs deleted file mode 100644 index 43789d427..000000000 --- a/src/scope.rs +++ /dev/null @@ -1,1236 +0,0 @@ -use std::marker::PhantomData; -use std::mem; -use std::rc::Rc; - -use futures::{Async, Future, Poll}; - -use error::Error; -use handler::{ - AsyncResult, AsyncResultItem, FromRequest, Handler, Responder, RouteHandler, - WrapHandler, -}; -use http::Method; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{ - Finished as MiddlewareFinished, Middleware, Response as MiddlewareResponse, - Started as MiddlewareStarted, -}; -use pred::Predicate; -use resource::{DefaultResource, Resource}; -use router::{ResourceDef, Router}; -use server::Request; -use with::WithFactory; - -/// Resources scope -/// -/// Scope is a set of resources with common root path. -/// Scopes collect multiple paths under a common path prefix. -/// Scope path can contain variable path segments as resources. -/// Scope prefix is always complete path segment, i.e `/app` would -/// be converted to a `/app/` and it would not match `/app` path. -/// -/// You can get variable path segments from `HttpRequest::match_info()`. -/// `Path` extractor also is able to extract scope level variable segments. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{http, App, HttpRequest, HttpResponse}; -/// -/// fn main() { -/// let app = App::new().scope("/{project_id}/", |scope| { -/// scope -/// .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) -/// .resource("/path2", |r| r.f(|_| HttpResponse::Ok())) -/// .resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed())) -/// }); -/// } -/// ``` -/// -/// In the above example three routes get registered: -/// * /{project_id}/path1 - reponds to all http method -/// * /{project_id}/path2 - `GET` requests -/// * /{project_id}/path3 - `HEAD` requests -/// -pub struct Scope { - rdef: ResourceDef, - router: Rc>, - filters: Vec>>, - middlewares: Rc>>>, -} - -#[cfg_attr( - feature = "cargo-clippy", - allow(clippy::new_without_default_derive) -)] -impl Scope { - /// Create a new scope - pub fn new(path: &str) -> Scope { - let rdef = ResourceDef::prefix(path); - Scope { - rdef: rdef.clone(), - router: Rc::new(Router::new(rdef)), - filters: Vec::new(), - middlewares: Rc::new(Vec::new()), - } - } - - #[inline] - pub(crate) fn rdef(&self) -> &ResourceDef { - &self.rdef - } - - pub(crate) fn router(&self) -> &Router { - self.router.as_ref() - } - - #[inline] - pub(crate) fn take_filters(&mut self) -> Vec>> { - mem::replace(&mut self.filters, Vec::new()) - } - - /// Add match predicate to scope. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, pred, App, HttpRequest, HttpResponse, Path}; - /// - /// fn index(data: Path<(String, String)>) -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope - /// .filter(pred::Header("content-type", "text/plain")) - /// .route("/test1", http::Method::GET, index) - /// .route("/test2", http::Method::POST, |_: HttpRequest| { - /// HttpResponse::MethodNotAllowed() - /// }) - /// }); - /// } - /// ``` - pub fn filter + 'static>(mut self, p: T) -> Self { - self.filters.push(Box::new(p)); - self - } - - /// Create nested scope with new state. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest}; - /// - /// struct AppState; - /// - /// fn index(req: &HttpRequest) -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope.with_state("/state2", AppState, |scope| { - /// scope.resource("/test1", |r| r.f(index)) - /// }) - /// }); - /// } - /// ``` - pub fn with_state(mut self, path: &str, state: T, f: F) -> Scope - where - F: FnOnce(Scope) -> Scope, - { - let rdef = ResourceDef::prefix(path); - let scope = Scope { - rdef: rdef.clone(), - filters: Vec::new(), - router: Rc::new(Router::new(rdef)), - middlewares: Rc::new(Vec::new()), - }; - let mut scope = f(scope); - - let state = Rc::new(state); - let filters: Vec>> = vec![Box::new(FiltersWrapper { - state: Rc::clone(&state), - filters: scope.take_filters(), - })]; - let handler = Box::new(Wrapper { scope, state }); - - Rc::get_mut(&mut self.router).unwrap().register_handler( - path, - handler, - Some(filters), - ); - - self - } - - /// Create nested scope. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest}; - /// - /// struct AppState; - /// - /// fn index(req: &HttpRequest) -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::with_state(AppState).scope("/app", |scope| { - /// scope.nested("/v1", |scope| scope.resource("/test1", |r| r.f(index))) - /// }); - /// } - /// ``` - pub fn nested(mut self, path: &str, f: F) -> Scope - where - F: FnOnce(Scope) -> Scope, - { - let rdef = ResourceDef::prefix(&insert_slash(path)); - let scope = Scope { - rdef: rdef.clone(), - filters: Vec::new(), - router: Rc::new(Router::new(rdef)), - middlewares: Rc::new(Vec::new()), - }; - Rc::get_mut(&mut self.router) - .unwrap() - .register_scope(f(scope)); - - self - } - - /// Configure route for a specific path. - /// - /// This is a simplified version of the `Scope::resource()` method. - /// Handler functions need to accept one request extractor - /// argument. - /// - /// This method could be called multiple times, in that case - /// multiple routes would be registered for same resource path. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse, Path}; - /// - /// fn index(data: Path<(String, String)>) -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope.route("/test1", http::Method::GET, index).route( - /// "/test2", - /// http::Method::POST, - /// |_: HttpRequest| HttpResponse::MethodNotAllowed(), - /// ) - /// }); - /// } - /// ``` - pub fn route(mut self, path: &str, method: Method, f: F) -> Scope - where - F: WithFactory, - R: Responder + 'static, - T: FromRequest + 'static, - { - Rc::get_mut(&mut self.router).unwrap().register_route( - &insert_slash(path), - method, - f, - ); - self - } - - /// Configure resource for a specific path. - /// - /// This method is similar to an `App::resource()` method. - /// Resources may have variable path segments. Resource path uses scope - /// path as a path prefix. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::*; - /// - /// fn main() { - /// let app = App::new().scope("/api", |scope| { - /// scope.resource("/users/{userid}/{friend}", |r| { - /// r.get().f(|_| HttpResponse::Ok()); - /// r.head().f(|_| HttpResponse::MethodNotAllowed()); - /// r.route() - /// .filter(pred::Any(pred::Get()).or(pred::Put())) - /// .filter(pred::Header("Content-Type", "text/plain")) - /// .f(|_| HttpResponse::Ok()) - /// }) - /// }); - /// } - /// ``` - pub fn resource(mut self, path: &str, f: F) -> Scope - where - F: FnOnce(&mut Resource) -> R + 'static, - { - // add resource - let mut resource = Resource::new(ResourceDef::new(&insert_slash(path))); - f(&mut resource); - - Rc::get_mut(&mut self.router) - .unwrap() - .register_resource(resource); - self - } - - /// Default resource to be used if no matching route could be found. - pub fn default_resource(mut self, f: F) -> Scope - where - F: FnOnce(&mut Resource) -> R + 'static, - { - // create and configure default resource - let mut resource = Resource::new(ResourceDef::new("")); - f(&mut resource); - - Rc::get_mut(&mut self.router) - .expect("Multiple copies of scope router") - .register_default_resource(resource.into()); - - self - } - - /// Configure handler for specific path prefix. - /// - /// A path prefix consists of valid path segments, i.e for the - /// prefix `/app` any request with the paths `/app`, `/app/` or - /// `/app/test` would match, but the path `/application` would - /// not. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().scope("/scope-prefix", |scope| { - /// scope.handler("/app", |req: &HttpRequest| match *req.method() { - /// http::Method::GET => HttpResponse::Ok(), - /// http::Method::POST => HttpResponse::MethodNotAllowed(), - /// _ => HttpResponse::NotFound(), - /// }) - /// }); - /// } - /// ``` - pub fn handler>(mut self, path: &str, handler: H) -> Scope { - let path = insert_slash(path.trim().trim_right_matches('/')); - Rc::get_mut(&mut self.router) - .expect("Multiple copies of scope router") - .register_handler(&path, Box::new(WrapHandler::new(handler)), None); - self - } - - /// Register a scope middleware - /// - /// This is similar to `App's` middlewares, but - /// middlewares get invoked on scope level. - /// - /// *Note* `Middleware::finish()` fires right after response get - /// prepared. It does not wait until body get sent to the peer. - pub fn middleware>(mut self, mw: M) -> Scope { - Rc::get_mut(&mut self.middlewares) - .expect("Can not use after configuration") - .push(Box::new(mw)); - self - } -} - -fn insert_slash(path: &str) -> String { - let mut path = path.to_owned(); - if !path.is_empty() && !path.starts_with('/') { - path.insert(0, '/'); - }; - path -} - -impl RouteHandler for Scope { - fn handle(&self, req: &HttpRequest) -> AsyncResult { - let tail = req.match_info().tail as usize; - - // recognize resources - let info = self.router.recognize(req, req.state(), tail); - let req2 = req.with_route_info(info); - if self.middlewares.is_empty() { - self.router.handle(&req2) - } else { - AsyncResult::async(Box::new(Compose::new( - req2, - Rc::clone(&self.router), - Rc::clone(&self.middlewares), - ))) - } - } - - fn has_default_resource(&self) -> bool { - self.router.has_default_resource() - } - - fn default_resource(&mut self, default: DefaultResource) { - Rc::get_mut(&mut self.router) - .expect("Can not use after configuration") - .register_default_resource(default); - } - - fn finish(&mut self) { - Rc::get_mut(&mut self.router) - .expect("Can not use after configuration") - .finish(); - } -} - -struct Wrapper { - state: Rc, - scope: Scope, -} - -impl RouteHandler for Wrapper { - fn handle(&self, req: &HttpRequest) -> AsyncResult { - let req = req.with_state(Rc::clone(&self.state)); - self.scope.handle(&req) - } -} - -struct FiltersWrapper { - state: Rc, - filters: Vec>>, -} - -impl Predicate for FiltersWrapper { - fn check(&self, req: &Request, _: &S2) -> bool { - for filter in &self.filters { - if !filter.check(&req, &self.state) { - return false; - } - } - true - } -} - -/// Compose resource level middlewares with route handler. -struct Compose { - info: ComposeInfo, - state: ComposeState, -} - -struct ComposeInfo { - count: usize, - req: HttpRequest, - router: Rc>, - mws: Rc>>>, -} - -enum ComposeState { - Starting(StartMiddlewares), - Handler(WaitingResponse), - RunMiddlewares(RunMiddlewares), - Finishing(FinishingMiddlewares), - Completed(Response), -} - -impl ComposeState { - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - match *self { - ComposeState::Starting(ref mut state) => state.poll(info), - ComposeState::Handler(ref mut state) => state.poll(info), - ComposeState::RunMiddlewares(ref mut state) => state.poll(info), - ComposeState::Finishing(ref mut state) => state.poll(info), - ComposeState::Completed(_) => None, - } - } -} - -impl Compose { - fn new( - req: HttpRequest, router: Rc>, mws: Rc>>>, - ) -> Self { - let mut info = ComposeInfo { - mws, - req, - router, - count: 0, - }; - let state = StartMiddlewares::init(&mut info); - - Compose { state, info } - } -} - -impl Future for Compose { - type Item = HttpResponse; - type Error = Error; - - fn poll(&mut self) -> Poll { - loop { - if let ComposeState::Completed(ref mut resp) = self.state { - let resp = resp.resp.take().unwrap(); - return Ok(Async::Ready(resp)); - } - if let Some(state) = self.state.poll(&mut self.info) { - self.state = state; - } else { - return Ok(Async::NotReady); - } - } - } -} - -/// Middlewares start executor -struct StartMiddlewares { - fut: Option, - _s: PhantomData, -} - -type Fut = Box, Error = Error>>; - -impl StartMiddlewares { - fn init(info: &mut ComposeInfo) -> ComposeState { - let len = info.mws.len(); - - loop { - if info.count == len { - let reply = info.router.handle(&info.req); - return WaitingResponse::init(info, reply); - } else { - let result = info.mws[info.count].start(&info.req); - match result { - Ok(MiddlewareStarted::Done) => info.count += 1, - Ok(MiddlewareStarted::Response(resp)) => { - return RunMiddlewares::init(info, resp); - } - Ok(MiddlewareStarted::Future(fut)) => { - return ComposeState::Starting(StartMiddlewares { - fut: Some(fut), - _s: PhantomData, - }); - } - Err(err) => { - return RunMiddlewares::init(info, err.into()); - } - } - } - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - let len = info.mws.len(); - - 'outer: loop { - match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => { - return None; - } - Ok(Async::Ready(resp)) => { - info.count += 1; - - if let Some(resp) = resp { - return Some(RunMiddlewares::init(info, resp)); - } - loop { - if info.count == len { - let reply = info.router.handle(&info.req); - return Some(WaitingResponse::init(info, reply)); - } else { - let result = info.mws[info.count].start(&info.req); - match result { - Ok(MiddlewareStarted::Done) => info.count += 1, - Ok(MiddlewareStarted::Response(resp)) => { - return Some(RunMiddlewares::init(info, resp)); - } - Ok(MiddlewareStarted::Future(fut)) => { - self.fut = Some(fut); - continue 'outer; - } - Err(err) => { - return Some(RunMiddlewares::init(info, err.into())); - } - } - } - } - } - Err(err) => { - return Some(RunMiddlewares::init(info, err.into())); - } - } - } - } -} - -// waiting for response -struct WaitingResponse { - fut: Box>, - _s: PhantomData, -} - -impl WaitingResponse { - #[inline] - fn init( - info: &mut ComposeInfo, reply: AsyncResult, - ) -> ComposeState { - match reply.into() { - AsyncResultItem::Ok(resp) => RunMiddlewares::init(info, resp), - AsyncResultItem::Err(err) => RunMiddlewares::init(info, err.into()), - AsyncResultItem::Future(fut) => ComposeState::Handler(WaitingResponse { - fut, - _s: PhantomData, - }), - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - match self.fut.poll() { - Ok(Async::NotReady) => None, - Ok(Async::Ready(resp)) => Some(RunMiddlewares::init(info, resp)), - Err(err) => Some(RunMiddlewares::init(info, err.into())), - } - } -} - -/// Middlewares response executor -struct RunMiddlewares { - curr: usize, - fut: Option>>, - _s: PhantomData, -} - -impl RunMiddlewares { - fn init(info: &mut ComposeInfo, mut resp: HttpResponse) -> ComposeState { - let mut curr = 0; - let len = info.mws.len(); - - loop { - let state = info.mws[curr].response(&info.req, resp); - resp = match state { - Err(err) => { - info.count = curr + 1; - return FinishingMiddlewares::init(info, err.into()); - } - Ok(MiddlewareResponse::Done(r)) => { - curr += 1; - if curr == len { - return FinishingMiddlewares::init(info, r); - } else { - r - } - } - Ok(MiddlewareResponse::Future(fut)) => { - return ComposeState::RunMiddlewares(RunMiddlewares { - curr, - fut: Some(fut), - _s: PhantomData, - }); - } - }; - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - let len = info.mws.len(); - - loop { - // poll latest fut - let mut resp = match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => return None, - Ok(Async::Ready(resp)) => { - self.curr += 1; - resp - } - Err(err) => return Some(FinishingMiddlewares::init(info, err.into())), - }; - - loop { - if self.curr == len { - return Some(FinishingMiddlewares::init(info, resp)); - } else { - let state = info.mws[self.curr].response(&info.req, resp); - match state { - Err(err) => { - return Some(FinishingMiddlewares::init(info, err.into())) - } - Ok(MiddlewareResponse::Done(r)) => { - self.curr += 1; - resp = r - } - Ok(MiddlewareResponse::Future(fut)) => { - self.fut = Some(fut); - break; - } - } - } - } - } - } -} - -/// Middlewares start executor -struct FinishingMiddlewares { - resp: Option, - fut: Option>>, - _s: PhantomData, -} - -impl FinishingMiddlewares { - fn init(info: &mut ComposeInfo, resp: HttpResponse) -> ComposeState { - if info.count == 0 { - Response::init(resp) - } else { - let mut state = FinishingMiddlewares { - resp: Some(resp), - fut: None, - _s: PhantomData, - }; - if let Some(st) = state.poll(info) { - st - } else { - ComposeState::Finishing(state) - } - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - loop { - // poll latest fut - let not_ready = if let Some(ref mut fut) = self.fut { - match fut.poll() { - Ok(Async::NotReady) => true, - Ok(Async::Ready(())) => false, - Err(err) => { - error!("Middleware finish error: {}", err); - false - } - } - } else { - false - }; - if not_ready { - return None; - } - self.fut = None; - if info.count == 0 { - return Some(Response::init(self.resp.take().unwrap())); - } - - info.count -= 1; - let state = info.mws[info.count as usize] - .finish(&info.req, self.resp.as_ref().unwrap()); - match state { - MiddlewareFinished::Done => { - if info.count == 0 { - return Some(Response::init(self.resp.take().unwrap())); - } - } - MiddlewareFinished::Future(fut) => { - self.fut = Some(fut); - } - } - } - } -} - -struct Response { - resp: Option, - _s: PhantomData, -} - -impl Response { - fn init(resp: HttpResponse) -> ComposeState { - ComposeState::Completed(Response { - resp: Some(resp), - _s: PhantomData, - }) - } -} - -#[cfg(test)] -mod tests { - use bytes::Bytes; - - use application::App; - use body::Body; - use http::{Method, StatusCode}; - use httprequest::HttpRequest; - use httpresponse::HttpResponse; - use pred; - use test::TestRequest; - - #[test] - fn test_scope() { - let app = App::new() - .scope("/app", |scope| { - scope.resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - }).finish(); - - let req = TestRequest::with_uri("/app/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_scope_root() { - let app = App::new() - .scope("/app", |scope| { - scope - .resource("", |r| r.f(|_| HttpResponse::Ok())) - .resource("/", |r| r.f(|_| HttpResponse::Created())) - }).finish(); - - let req = TestRequest::with_uri("/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_scope_root2() { - let app = App::new() - .scope("/app/", |scope| { - scope.resource("", |r| r.f(|_| HttpResponse::Ok())) - }).finish(); - - let req = TestRequest::with_uri("/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_scope_root3() { - let app = App::new() - .scope("/app/", |scope| { - scope.resource("/", |r| r.f(|_| HttpResponse::Ok())) - }).finish(); - - let req = TestRequest::with_uri("/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_route() { - let app = App::new() - .scope("app", |scope| { - scope - .route("/path1", Method::GET, |_: HttpRequest<_>| { - HttpResponse::Ok() - }).route("/path1", Method::DELETE, |_: HttpRequest<_>| { - HttpResponse::Ok() - }) - }).finish(); - - let req = TestRequest::with_uri("/app/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::DELETE) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_route_without_leading_slash() { - let app = App::new() - .scope("app", |scope| { - scope - .route("path1", Method::GET, |_: HttpRequest<_>| HttpResponse::Ok()) - .route("path1", Method::DELETE, |_: HttpRequest<_>| { - HttpResponse::Ok() - }) - }).finish(); - - let req = TestRequest::with_uri("/app/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::DELETE) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_filter() { - let app = App::new() - .scope("/app", |scope| { - scope - .filter(pred::Get()) - .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - }).finish(); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::GET) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_scope_variable_segment() { - let app = App::new() - .scope("/ab-{project}", |scope| { - scope.resource("/path1", |r| { - r.f(|r| { - HttpResponse::Ok() - .body(format!("project: {}", &r.match_info()["project"])) - }) - }) - }).finish(); - - let req = TestRequest::with_uri("/ab-project1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - match resp.as_msg().body() { - &Body::Binary(ref b) => { - let bytes: Bytes = b.clone().into(); - assert_eq!(bytes, Bytes::from_static(b"project: project1")); - } - _ => panic!(), - } - - let req = TestRequest::with_uri("/aa-project1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_with_state() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1", State, |scope| { - scope.resource("/path1", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_scope_with_state_root() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1", State, |scope| { - scope - .resource("", |r| r.f(|_| HttpResponse::Ok())) - .resource("/", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/t1/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_scope_with_state_root2() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1/", State, |scope| { - scope.resource("", |r| r.f(|_| HttpResponse::Ok())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/t1/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_scope_with_state_root3() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1/", State, |scope| { - scope.resource("/", |r| r.f(|_| HttpResponse::Ok())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/t1/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_with_state_filter() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1", State, |scope| { - scope - .filter(pred::Get()) - .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/t1/path1") - .method(Method::GET) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_nested_scope() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope.resource("/path1", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_nested_scope_no_slash() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("t1", |scope| { - scope.resource("/path1", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_nested_scope_root() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope - .resource("", |r| r.f(|_| HttpResponse::Ok())) - .resource("/", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/t1/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_nested_scope_filter() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope - .filter(pred::Get()) - .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/t1/path1") - .method(Method::GET) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_nested_scope_with_variable_segment() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/{project_id}", |scope| { - scope.resource("/path1", |r| { - r.f(|r| { - HttpResponse::Created().body(format!( - "project: {}", - &r.match_info()["project_id"] - )) - }) - }) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/project_1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - - match resp.as_msg().body() { - &Body::Binary(ref b) => { - let bytes: Bytes = b.clone().into(); - assert_eq!(bytes, Bytes::from_static(b"project: project_1")); - } - _ => panic!(), - } - } - - #[test] - fn test_nested2_scope_with_variable_segment() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/{project}", |scope| { - scope.nested("/{id}", |scope| { - scope.resource("/path1", |r| { - r.f(|r| { - HttpResponse::Created().body(format!( - "project: {} - {}", - &r.match_info()["project"], - &r.match_info()["id"], - )) - }) - }) - }) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/test/1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - - match resp.as_msg().body() { - &Body::Binary(ref b) => { - let bytes: Bytes = b.clone().into(); - assert_eq!(bytes, Bytes::from_static(b"project: test - 1")); - } - _ => panic!(), - } - - let req = TestRequest::with_uri("/app/test/1/path2").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_default_resource() { - let app = App::new() - .scope("/app", |scope| { - scope - .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - .default_resource(|r| r.f(|_| HttpResponse::BadRequest())) - }).finish(); - - let req = TestRequest::with_uri("/app/path2").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::BAD_REQUEST); - - let req = TestRequest::with_uri("/path2").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_default_resource_propagation() { - let app = App::new() - .scope("/app1", |scope| { - scope.default_resource(|r| r.f(|_| HttpResponse::BadRequest())) - }).scope("/app2", |scope| scope) - .default_resource(|r| r.f(|_| HttpResponse::MethodNotAllowed())) - .finish(); - - let req = TestRequest::with_uri("/non-exist").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::METHOD_NOT_ALLOWED); - - let req = TestRequest::with_uri("/app1/non-exist").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::BAD_REQUEST); - - let req = TestRequest::with_uri("/app2/non-exist").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::METHOD_NOT_ALLOWED); - } - - #[test] - fn test_handler() { - let app = App::new() - .scope("/scope", |scope| { - scope.handler("/test", |_: &_| HttpResponse::Ok()) - }).finish(); - - let req = TestRequest::with_uri("/scope/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/scope/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/scope/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/scope/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/scope/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } -} diff --git a/src/server/acceptor.rs b/src/server/acceptor.rs deleted file mode 100644 index 2e1b1f283..000000000 --- a/src/server/acceptor.rs +++ /dev/null @@ -1,396 +0,0 @@ -use std::time::Duration; -use std::{fmt, net}; - -use actix_net::server::ServerMessage; -use actix_net::service::{NewService, Service}; -use futures::future::{err, ok, Either, FutureResult}; -use futures::{Async, Future, Poll}; -use tokio_reactor::Handle; -use tokio_tcp::TcpStream; -use tokio_timer::{sleep, Delay}; - -use super::channel::HttpProtocol; -use super::error::AcceptorError; -use super::handler::HttpHandler; -use super::settings::ServiceConfig; -use super::IoStream; - -/// This trait indicates types that can create acceptor service for http server. -pub trait AcceptorServiceFactory: Send + Clone + 'static { - type Io: IoStream + Send; - type NewService: NewService; - - fn create(&self) -> Self::NewService; -} - -impl AcceptorServiceFactory for F -where - F: Fn() -> T + Send + Clone + 'static, - T::Response: IoStream + Send, - T: NewService, - T::InitError: fmt::Debug, -{ - type Io = T::Response; - type NewService = T; - - fn create(&self) -> T { - (self)() - } -} - -#[derive(Clone)] -/// Default acceptor service convert `TcpStream` to a `tokio_tcp::TcpStream` -pub(crate) struct DefaultAcceptor; - -impl AcceptorServiceFactory for DefaultAcceptor { - type Io = TcpStream; - type NewService = DefaultAcceptor; - - fn create(&self) -> Self::NewService { - DefaultAcceptor - } -} - -impl NewService for DefaultAcceptor { - type Request = TcpStream; - type Response = TcpStream; - type Error = (); - type InitError = (); - type Service = DefaultAcceptor; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(DefaultAcceptor) - } -} - -impl Service for DefaultAcceptor { - type Request = TcpStream; - type Response = TcpStream; - type Error = (); - type Future = FutureResult; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - ok(req) - } -} - -pub(crate) struct TcpAcceptor { - inner: T, -} - -impl TcpAcceptor -where - T: NewService>, - T::InitError: fmt::Debug, -{ - pub(crate) fn new(inner: T) -> Self { - TcpAcceptor { inner } - } -} - -impl NewService for TcpAcceptor -where - T: NewService>, - T::InitError: fmt::Debug, -{ - type Request = net::TcpStream; - type Response = T::Response; - type Error = AcceptorError; - type InitError = T::InitError; - type Service = TcpAcceptorService; - type Future = TcpAcceptorResponse; - - fn new_service(&self) -> Self::Future { - TcpAcceptorResponse { - fut: self.inner.new_service(), - } - } -} - -pub(crate) struct TcpAcceptorResponse -where - T: NewService, - T::InitError: fmt::Debug, -{ - fut: T::Future, -} - -impl Future for TcpAcceptorResponse -where - T: NewService, - T::InitError: fmt::Debug, -{ - type Item = TcpAcceptorService; - type Error = T::InitError; - - fn poll(&mut self) -> Poll { - match self.fut.poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(service)) => { - Ok(Async::Ready(TcpAcceptorService { inner: service })) - } - Err(e) => { - error!("Can not create accetor service: {:?}", e); - Err(e) - } - } - } -} - -pub(crate) struct TcpAcceptorService { - inner: T, -} - -impl Service for TcpAcceptorService -where - T: Service>, -{ - type Request = net::TcpStream; - type Response = T::Response; - type Error = AcceptorError; - type Future = Either>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.inner.poll_ready() - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - let stream = TcpStream::from_std(req, &Handle::default()).map_err(|e| { - error!("Can not convert to an async tcp stream: {}", e); - AcceptorError::Io(e) - }); - - match stream { - Ok(stream) => Either::A(self.inner.call(stream)), - Err(e) => Either::B(err(e)), - } - } -} - -#[doc(hidden)] -/// Acceptor timeout middleware -/// -/// Applies timeout to request prcoessing. -pub struct AcceptorTimeout { - inner: T, - timeout: Duration, -} - -impl AcceptorTimeout { - /// Create new `AcceptorTimeout` instance. timeout is in milliseconds. - pub fn new(timeout: u64, inner: T) -> Self { - Self { - inner, - timeout: Duration::from_millis(timeout), - } - } -} - -impl NewService for AcceptorTimeout { - type Request = T::Request; - type Response = T::Response; - type Error = AcceptorError; - type InitError = T::InitError; - type Service = AcceptorTimeoutService; - type Future = AcceptorTimeoutFut; - - fn new_service(&self) -> Self::Future { - AcceptorTimeoutFut { - fut: self.inner.new_service(), - timeout: self.timeout, - } - } -} - -#[doc(hidden)] -pub struct AcceptorTimeoutFut { - fut: T::Future, - timeout: Duration, -} - -impl Future for AcceptorTimeoutFut { - type Item = AcceptorTimeoutService; - type Error = T::InitError; - - fn poll(&mut self) -> Poll { - let inner = try_ready!(self.fut.poll()); - Ok(Async::Ready(AcceptorTimeoutService { - inner, - timeout: self.timeout, - })) - } -} - -#[doc(hidden)] -/// Acceptor timeout service -/// -/// Applies timeout to request prcoessing. -pub struct AcceptorTimeoutService { - inner: T, - timeout: Duration, -} - -impl Service for AcceptorTimeoutService { - type Request = T::Request; - type Response = T::Response; - type Error = AcceptorError; - type Future = AcceptorTimeoutResponse; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.inner.poll_ready().map_err(AcceptorError::Service) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - AcceptorTimeoutResponse { - fut: self.inner.call(req), - sleep: sleep(self.timeout), - } - } -} - -#[doc(hidden)] -pub struct AcceptorTimeoutResponse { - fut: T::Future, - sleep: Delay, -} - -impl Future for AcceptorTimeoutResponse { - type Item = T::Response; - type Error = AcceptorError; - - fn poll(&mut self) -> Poll { - match self.fut.poll().map_err(AcceptorError::Service)? { - Async::NotReady => match self.sleep.poll() { - Err(_) => Err(AcceptorError::Timeout), - Ok(Async::Ready(_)) => Err(AcceptorError::Timeout), - Ok(Async::NotReady) => Ok(Async::NotReady), - }, - Async::Ready(resp) => Ok(Async::Ready(resp)), - } - } -} - -pub(crate) struct ServerMessageAcceptor { - inner: T, - settings: ServiceConfig, -} - -impl ServerMessageAcceptor -where - H: HttpHandler, - T: NewService, -{ - pub(crate) fn new(settings: ServiceConfig, inner: T) -> Self { - ServerMessageAcceptor { inner, settings } - } -} - -impl NewService for ServerMessageAcceptor -where - H: HttpHandler, - T: NewService, -{ - type Request = ServerMessage; - type Response = (); - type Error = T::Error; - type InitError = T::InitError; - type Service = ServerMessageAcceptorService; - type Future = ServerMessageAcceptorResponse; - - fn new_service(&self) -> Self::Future { - ServerMessageAcceptorResponse { - fut: self.inner.new_service(), - settings: self.settings.clone(), - } - } -} - -pub(crate) struct ServerMessageAcceptorResponse -where - H: HttpHandler, - T: NewService, -{ - fut: T::Future, - settings: ServiceConfig, -} - -impl Future for ServerMessageAcceptorResponse -where - H: HttpHandler, - T: NewService, -{ - type Item = ServerMessageAcceptorService; - type Error = T::InitError; - - fn poll(&mut self) -> Poll { - match self.fut.poll()? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(service) => Ok(Async::Ready(ServerMessageAcceptorService { - inner: service, - settings: self.settings.clone(), - })), - } - } -} - -pub(crate) struct ServerMessageAcceptorService { - inner: T, - settings: ServiceConfig, -} - -impl Service for ServerMessageAcceptorService -where - H: HttpHandler, - T: Service, -{ - type Request = ServerMessage; - type Response = (); - type Error = T::Error; - type Future = - Either, FutureResult<(), Self::Error>>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.inner.poll_ready() - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - match req { - ServerMessage::Connect(stream) => { - Either::A(ServerMessageAcceptorServiceFut { - fut: self.inner.call(stream), - }) - } - ServerMessage::Shutdown(_) => Either::B(ok(())), - ServerMessage::ForceShutdown => { - self.settings - .head() - .traverse(|proto: &mut HttpProtocol| proto.shutdown()); - Either::B(ok(())) - } - } - } -} - -pub(crate) struct ServerMessageAcceptorServiceFut { - fut: T::Future, -} - -impl Future for ServerMessageAcceptorServiceFut -where - T: Service, -{ - type Item = (); - type Error = T::Error; - - fn poll(&mut self) -> Poll { - match self.fut.poll()? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(_) => Ok(Async::Ready(())), - } - } -} diff --git a/src/server/builder.rs b/src/server/builder.rs deleted file mode 100644 index ec6ce9923..000000000 --- a/src/server/builder.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::{fmt, net}; - -use actix_net::either::Either; -use actix_net::server::{Server, ServiceFactory}; -use actix_net::service::{NewService, NewServiceExt}; - -use super::acceptor::{ - AcceptorServiceFactory, AcceptorTimeout, ServerMessageAcceptor, TcpAcceptor, -}; -use super::error::AcceptorError; -use super::handler::IntoHttpHandler; -use super::service::HttpService; -use super::settings::{ServerSettings, ServiceConfig}; -use super::KeepAlive; - -pub(crate) trait ServiceProvider { - fn register( - &self, server: Server, lst: net::TcpListener, host: String, - addr: net::SocketAddr, keep_alive: KeepAlive, secure: bool, client_timeout: u64, - client_shutdown: u64, - ) -> Server; -} - -/// Utility type that builds complete http pipeline -pub(crate) struct HttpServiceBuilder -where - F: Fn() -> H + Send + Clone, -{ - factory: F, - acceptor: A, -} - -impl HttpServiceBuilder -where - F: Fn() -> H + Send + Clone + 'static, - H: IntoHttpHandler, - A: AcceptorServiceFactory, - ::InitError: fmt::Debug, -{ - /// Create http service builder - pub fn new(factory: F, acceptor: A) -> Self { - Self { factory, acceptor } - } - - fn finish( - &self, host: String, addr: net::SocketAddr, keep_alive: KeepAlive, secure: bool, - client_timeout: u64, client_shutdown: u64, - ) -> impl ServiceFactory { - let factory = self.factory.clone(); - let acceptor = self.acceptor.clone(); - move || { - let app = (factory)().into_handler(); - let settings = ServiceConfig::new( - app, - keep_alive, - client_timeout, - client_shutdown, - ServerSettings::new(addr, &host, false), - ); - - if secure { - Either::B(ServerMessageAcceptor::new( - settings.clone(), - TcpAcceptor::new(AcceptorTimeout::new( - client_timeout, - acceptor.create(), - )).map_err(|_| ()) - .map_init_err(|_| ()) - .and_then( - HttpService::new(settings) - .map_init_err(|_| ()) - .map_err(|_| ()), - ), - )) - } else { - Either::A(ServerMessageAcceptor::new( - settings.clone(), - TcpAcceptor::new(acceptor.create().map_err(AcceptorError::Service)) - .map_err(|_| ()) - .map_init_err(|_| ()) - .and_then( - HttpService::new(settings) - .map_init_err(|_| ()) - .map_err(|_| ()), - ), - )) - } - } - } -} - -impl ServiceProvider for HttpServiceBuilder -where - F: Fn() -> H + Send + Clone + 'static, - A: AcceptorServiceFactory, - ::InitError: fmt::Debug, - H: IntoHttpHandler, -{ - fn register( - &self, server: Server, lst: net::TcpListener, host: String, - addr: net::SocketAddr, keep_alive: KeepAlive, secure: bool, client_timeout: u64, - client_shutdown: u64, - ) -> Server { - server.listen2( - "actix-web", - lst, - self.finish( - host, - addr, - keep_alive, - secure, - client_timeout, - client_shutdown, - ), - ) - } -} diff --git a/src/server/channel.rs b/src/server/channel.rs deleted file mode 100644 index b1fef964e..000000000 --- a/src/server/channel.rs +++ /dev/null @@ -1,436 +0,0 @@ -use std::net::Shutdown; -use std::{io, mem, time}; - -use bytes::{Buf, BufMut, BytesMut}; -use futures::{Async, Future, Poll}; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_timer::Delay; - -use super::error::HttpDispatchError; -use super::settings::ServiceConfig; -use super::{h1, h2, HttpHandler, IoStream}; -use error::Error; -use http::StatusCode; - -const HTTP2_PREFACE: [u8; 14] = *b"PRI * HTTP/2.0"; - -pub(crate) enum HttpProtocol { - H1(h1::Http1Dispatcher), - H2(h2::Http2), - Unknown(ServiceConfig, T, BytesMut), - None, -} - -impl HttpProtocol { - pub(crate) fn shutdown(&mut self) { - match self { - HttpProtocol::H1(ref mut h1) => { - let io = h1.io(); - let _ = IoStream::set_linger(io, Some(time::Duration::new(0, 0))); - let _ = IoStream::shutdown(io, Shutdown::Both); - } - HttpProtocol::H2(ref mut h2) => h2.shutdown(), - HttpProtocol::Unknown(_, io, _) => { - let _ = IoStream::set_linger(io, Some(time::Duration::new(0, 0))); - let _ = IoStream::shutdown(io, Shutdown::Both); - } - HttpProtocol::None => (), - } - } -} - -enum ProtocolKind { - Http1, - Http2, -} - -#[doc(hidden)] -pub struct HttpChannel -where - T: IoStream, - H: HttpHandler + 'static, -{ - node: Node>, - node_reg: bool, - ka_timeout: Option, -} - -impl HttpChannel -where - T: IoStream, - H: HttpHandler + 'static, -{ - pub(crate) fn new(settings: ServiceConfig, io: T) -> HttpChannel { - let ka_timeout = settings.client_timer(); - - HttpChannel { - ka_timeout, - node_reg: false, - node: Node::new(HttpProtocol::Unknown( - settings, - io, - BytesMut::with_capacity(8192), - )), - } - } -} - -impl Drop for HttpChannel -where - T: IoStream, - H: HttpHandler + 'static, -{ - fn drop(&mut self) { - self.node.remove(); - } -} - -impl Future for HttpChannel -where - T: IoStream, - H: HttpHandler + 'static, -{ - type Item = (); - type Error = HttpDispatchError; - - fn poll(&mut self) -> Poll { - // keep-alive timer - if self.ka_timeout.is_some() { - match self.ka_timeout.as_mut().unwrap().poll() { - Ok(Async::Ready(_)) => { - trace!("Slow request timed out, close connection"); - let proto = mem::replace(self.node.get_mut(), HttpProtocol::None); - if let HttpProtocol::Unknown(settings, io, buf) = proto { - *self.node.get_mut() = - HttpProtocol::H1(h1::Http1Dispatcher::for_error( - settings, - io, - StatusCode::REQUEST_TIMEOUT, - self.ka_timeout.take(), - buf, - )); - return self.poll(); - } - return Ok(Async::Ready(())); - } - Ok(Async::NotReady) => (), - Err(_) => panic!("Something is really wrong"), - } - } - - if !self.node_reg { - self.node_reg = true; - let settings = match self.node.get_mut() { - HttpProtocol::H1(ref mut h1) => h1.settings().clone(), - HttpProtocol::H2(ref mut h2) => h2.settings().clone(), - HttpProtocol::Unknown(ref mut settings, _, _) => settings.clone(), - HttpProtocol::None => unreachable!(), - }; - settings.head().insert(&mut self.node); - } - - let mut is_eof = false; - let kind = match self.node.get_mut() { - HttpProtocol::H1(ref mut h1) => return h1.poll(), - HttpProtocol::H2(ref mut h2) => return h2.poll(), - HttpProtocol::Unknown(_, ref mut io, ref mut buf) => { - let mut err = None; - let mut disconnect = false; - match io.read_available(buf) { - Ok(Async::Ready((read_some, stream_closed))) => { - is_eof = stream_closed; - // Only disconnect if no data was read. - if is_eof && !read_some { - disconnect = true; - } - } - Err(e) => { - err = Some(e.into()); - } - _ => (), - } - if disconnect { - debug!("Ignored premature client disconnection"); - return Ok(Async::Ready(())); - } else if let Some(e) = err { - return Err(e); - } - - if buf.len() >= 14 { - if buf[..14] == HTTP2_PREFACE[..] { - ProtocolKind::Http2 - } else { - ProtocolKind::Http1 - } - } else { - return Ok(Async::NotReady); - } - } - HttpProtocol::None => unreachable!(), - }; - - // upgrade to specific http protocol - let proto = mem::replace(self.node.get_mut(), HttpProtocol::None); - if let HttpProtocol::Unknown(settings, io, buf) = proto { - match kind { - ProtocolKind::Http1 => { - *self.node.get_mut() = HttpProtocol::H1(h1::Http1Dispatcher::new( - settings, - io, - buf, - is_eof, - self.ka_timeout.take(), - )); - return self.poll(); - } - ProtocolKind::Http2 => { - *self.node.get_mut() = HttpProtocol::H2(h2::Http2::new( - settings, - io, - buf.freeze(), - self.ka_timeout.take(), - )); - return self.poll(); - } - } - } - unreachable!() - } -} - -#[doc(hidden)] -pub struct H1Channel -where - T: IoStream, - H: HttpHandler + 'static, -{ - node: Node>, - node_reg: bool, -} - -impl H1Channel -where - T: IoStream, - H: HttpHandler + 'static, -{ - pub(crate) fn new(settings: ServiceConfig, io: T) -> H1Channel { - H1Channel { - node_reg: false, - node: Node::new(HttpProtocol::H1(h1::Http1Dispatcher::new( - settings, - io, - BytesMut::with_capacity(8192), - false, - None, - ))), - } - } -} - -impl Drop for H1Channel -where - T: IoStream, - H: HttpHandler + 'static, -{ - fn drop(&mut self) { - self.node.remove(); - } -} - -impl Future for H1Channel -where - T: IoStream, - H: HttpHandler + 'static, -{ - type Item = (); - type Error = HttpDispatchError; - - fn poll(&mut self) -> Poll { - if !self.node_reg { - self.node_reg = true; - let settings = match self.node.get_mut() { - HttpProtocol::H1(ref mut h1) => h1.settings().clone(), - _ => unreachable!(), - }; - settings.head().insert(&mut self.node); - } - - match self.node.get_mut() { - HttpProtocol::H1(ref mut h1) => h1.poll(), - _ => unreachable!(), - } - } -} - -pub(crate) struct Node { - next: Option<*mut Node>, - prev: Option<*mut Node>, - element: T, -} - -impl Node { - fn new(element: T) -> Self { - Node { - element, - next: None, - prev: None, - } - } - - fn get_mut(&mut self) -> &mut T { - &mut self.element - } - - fn insert(&mut self, next_el: &mut Node) { - let next: *mut Node = next_el as *const _ as *mut _; - - if let Some(next2) = self.next { - unsafe { - let n = next2.as_mut().unwrap(); - n.prev = Some(next); - } - next_el.next = Some(next2 as *mut _); - } - self.next = Some(next); - - unsafe { - let next: &mut Node = &mut *next; - next.prev = Some(self as *mut _); - } - } - - fn remove(&mut self) { - let next = self.next.take(); - let prev = self.prev.take(); - - if let Some(prev) = prev { - unsafe { - prev.as_mut().unwrap().next = next; - } - } - if let Some(next) = next { - unsafe { - next.as_mut().unwrap().prev = prev; - } - } - } -} - -impl Node<()> { - pub(crate) fn head() -> Self { - Node { - next: None, - prev: None, - element: (), - } - } - - pub(crate) fn traverse)>(&self, f: F) - where - T: IoStream, - H: HttpHandler + 'static, - { - if let Some(n) = self.next.as_ref() { - unsafe { - let mut next: &mut Node> = - &mut *(n.as_ref().unwrap() as *const _ as *mut _); - loop { - f(&mut next.element); - - next = if let Some(n) = next.next.as_ref() { - &mut **n - } else { - return; - } - } - } - } - } -} - -/// Wrapper for `AsyncRead + AsyncWrite` types -pub(crate) struct WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - io: T, -} - -impl WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - pub fn new(io: T) -> Self { - WrapperStream { io } - } -} - -impl IoStream for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn shutdown(&mut self, _: Shutdown) -> io::Result<()> { - Ok(()) - } - #[inline] - fn set_nodelay(&mut self, _: bool) -> io::Result<()> { - Ok(()) - } - #[inline] - fn set_linger(&mut self, _: Option) -> io::Result<()> { - Ok(()) - } - #[inline] - fn set_keepalive(&mut self, _: Option) -> io::Result<()> { - Ok(()) - } -} - -impl io::Read for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.io.read(buf) - } -} - -impl io::Write for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn write(&mut self, buf: &[u8]) -> io::Result { - self.io.write(buf) - } - #[inline] - fn flush(&mut self) -> io::Result<()> { - self.io.flush() - } -} - -impl AsyncRead for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn read_buf(&mut self, buf: &mut B) -> Poll { - self.io.read_buf(buf) - } -} - -impl AsyncWrite for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn shutdown(&mut self) -> Poll<(), io::Error> { - self.io.shutdown() - } - #[inline] - fn write_buf(&mut self, buf: &mut B) -> Poll { - self.io.write_buf(buf) - } -} diff --git a/src/server/error.rs b/src/server/error.rs index 3ae9a107b..d9e1239e1 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -4,7 +4,6 @@ use std::io; use futures::{Async, Poll}; use http2; -use super::{helpers, HttpHandlerTask, Writer}; use error::{Error, ParseError}; use http::{StatusCode, Version}; @@ -89,31 +88,3 @@ impl From for HttpDispatchError { HttpDispatchError::Http2(err) } } - -pub(crate) struct ServerError(Version, StatusCode); - -impl ServerError { - pub fn err(ver: Version, status: StatusCode) -> Box { - Box::new(ServerError(ver, status)) - } -} - -impl HttpHandlerTask for ServerError { - fn poll_io(&mut self, io: &mut Writer) -> Poll { - { - let bytes = io.buffer(); - // Buffer should have sufficient capacity for status line - // and extra space - bytes.reserve(helpers::STATUS_LINE_BUF_SIZE + 1); - helpers::write_status_line(self.0, self.1.as_u16(), bytes); - } - // Convert Status Code to Reason. - let reason = self.1.canonical_reason().unwrap_or(""); - io.buffer().extend_from_slice(reason.as_bytes()); - // No response body. - io.buffer().extend_from_slice(b"\r\ncontent-length: 0\r\n"); - // date header - io.set_date(); - Ok(Async::Ready(true)) - } -} diff --git a/src/server/h1writer.rs b/src/server/h1writer.rs deleted file mode 100644 index c27a4c44a..000000000 --- a/src/server/h1writer.rs +++ /dev/null @@ -1,356 +0,0 @@ -// #![cfg_attr(feature = "cargo-clippy", allow(redundant_field_names))] - -use std::io::{self, Write}; - -use bytes::{BufMut, BytesMut}; -use futures::{Async, Poll}; -use tokio_io::AsyncWrite; - -use super::helpers; -use super::output::{Output, ResponseInfo, ResponseLength}; -use super::settings::ServiceConfig; -use super::Request; -use super::{Writer, WriterState, MAX_WRITE_BUFFER_SIZE}; -use body::{Binary, Body}; -use header::ContentEncoding; -use http::header::{ - HeaderValue, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, -}; -use http::{Method, Version}; -use httpresponse::HttpResponse; - -const AVERAGE_HEADER_SIZE: usize = 30; // totally scientific - -bitflags! { - struct Flags: u8 { - const STARTED = 0b0000_0001; - const UPGRADE = 0b0000_0010; - const KEEPALIVE = 0b0000_0100; - const DISCONNECTED = 0b0000_1000; - } -} - -pub(crate) struct H1Writer { - flags: Flags, - stream: T, - written: u64, - headers_size: u32, - buffer: Output, - buffer_capacity: usize, - settings: ServiceConfig, -} - -impl H1Writer { - pub fn new(stream: T, settings: ServiceConfig) -> H1Writer { - H1Writer { - flags: Flags::KEEPALIVE, - written: 0, - headers_size: 0, - buffer: Output::Buffer(settings.get_bytes()), - buffer_capacity: 0, - stream, - settings, - } - } - - pub fn get_mut(&mut self) -> &mut T { - &mut self.stream - } - - pub fn reset(&mut self) { - self.written = 0; - self.flags = Flags::KEEPALIVE; - } - - pub fn flushed(&mut self) -> bool { - self.buffer.is_empty() - } - - pub fn disconnected(&mut self) { - self.flags.insert(Flags::DISCONNECTED); - } - - pub fn upgrade(&self) -> bool { - self.flags.contains(Flags::UPGRADE) - } - - pub fn keepalive(&self) -> bool { - self.flags.contains(Flags::KEEPALIVE) && !self.flags.contains(Flags::UPGRADE) - } - - fn write_data(stream: &mut T, data: &[u8]) -> io::Result { - let mut written = 0; - while written < data.len() { - match stream.write(&data[written..]) { - Ok(0) => { - return Err(io::Error::new(io::ErrorKind::WriteZero, "")); - } - Ok(n) => { - written += n; - } - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { - return Ok(written) - } - Err(err) => return Err(err), - } - } - Ok(written) - } -} - -impl Drop for H1Writer { - fn drop(&mut self) { - if let Some(bytes) = self.buffer.take_option() { - self.settings.release_bytes(bytes); - } - } -} - -impl Writer for H1Writer { - #[inline] - fn written(&self) -> u64 { - self.written - } - - #[inline] - fn set_date(&mut self) { - self.settings.set_date(self.buffer.as_mut(), true) - } - - #[inline] - fn buffer(&mut self) -> &mut BytesMut { - self.buffer.as_mut() - } - - fn start( - &mut self, req: &Request, msg: &mut HttpResponse, encoding: ContentEncoding, - ) -> io::Result { - // prepare task - let mut info = ResponseInfo::new(req.inner.method == Method::HEAD); - self.buffer.for_server(&mut info, &req.inner, msg, encoding); - if msg.keep_alive().unwrap_or_else(|| req.keep_alive()) { - self.flags = Flags::STARTED | Flags::KEEPALIVE; - } else { - self.flags = Flags::STARTED; - } - - // Connection upgrade - let version = msg.version().unwrap_or_else(|| req.inner.version); - if msg.upgrade() { - self.flags.insert(Flags::UPGRADE); - msg.headers_mut() - .insert(CONNECTION, HeaderValue::from_static("upgrade")); - } - // keep-alive - else if self.flags.contains(Flags::KEEPALIVE) { - if version < Version::HTTP_11 { - msg.headers_mut() - .insert(CONNECTION, HeaderValue::from_static("keep-alive")); - } - } else if version >= Version::HTTP_11 { - msg.headers_mut() - .insert(CONNECTION, HeaderValue::from_static("close")); - } - let body = msg.replace_body(Body::Empty); - - // render message - { - // output buffer - let mut buffer = self.buffer.as_mut(); - - let reason = msg.reason().as_bytes(); - if let Body::Binary(ref bytes) = body { - buffer.reserve( - 256 + msg.headers().len() * AVERAGE_HEADER_SIZE - + bytes.len() - + reason.len(), - ); - } else { - buffer.reserve( - 256 + msg.headers().len() * AVERAGE_HEADER_SIZE + reason.len(), - ); - } - - // status line - helpers::write_status_line(version, msg.status().as_u16(), &mut buffer); - buffer.extend_from_slice(reason); - - // content length - match info.length { - ResponseLength::Chunked => { - buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") - } - ResponseLength::Zero => { - buffer.extend_from_slice(b"\r\ncontent-length: 0\r\n") - } - ResponseLength::Length(len) => { - helpers::write_content_length(len, &mut buffer) - } - ResponseLength::Length64(len) => { - buffer.extend_from_slice(b"\r\ncontent-length: "); - write!(buffer.writer(), "{}", len)?; - buffer.extend_from_slice(b"\r\n"); - } - ResponseLength::None => buffer.extend_from_slice(b"\r\n"), - } - if let Some(ce) = info.content_encoding { - buffer.extend_from_slice(b"content-encoding: "); - buffer.extend_from_slice(ce.as_ref()); - buffer.extend_from_slice(b"\r\n"); - } - - // write headers - let mut pos = 0; - let mut has_date = false; - let mut remaining = buffer.remaining_mut(); - let mut buf = unsafe { &mut *(buffer.bytes_mut() as *mut [u8]) }; - for (key, value) in msg.headers() { - match *key { - TRANSFER_ENCODING => continue, - CONTENT_ENCODING => if encoding != ContentEncoding::Identity { - continue; - }, - CONTENT_LENGTH => match info.length { - ResponseLength::None => (), - _ => continue, - }, - DATE => { - has_date = true; - } - _ => (), - } - - let v = value.as_ref(); - let k = key.as_str().as_bytes(); - let len = k.len() + v.len() + 4; - if len > remaining { - unsafe { - buffer.advance_mut(pos); - } - pos = 0; - buffer.reserve(len); - remaining = buffer.remaining_mut(); - unsafe { - buf = &mut *(buffer.bytes_mut() as *mut _); - } - } - - buf[pos..pos + k.len()].copy_from_slice(k); - pos += k.len(); - buf[pos..pos + 2].copy_from_slice(b": "); - pos += 2; - buf[pos..pos + v.len()].copy_from_slice(v); - pos += v.len(); - buf[pos..pos + 2].copy_from_slice(b"\r\n"); - pos += 2; - remaining -= len; - } - unsafe { - buffer.advance_mut(pos); - } - - // optimized date header, set_date writes \r\n - if !has_date { - self.settings.set_date(&mut buffer, true); - } else { - // msg eof - buffer.extend_from_slice(b"\r\n"); - } - self.headers_size = buffer.len() as u32; - } - - if let Body::Binary(bytes) = body { - self.written = bytes.len() as u64; - self.buffer.write(bytes.as_ref())?; - } else { - // capacity, makes sense only for streaming or actor - self.buffer_capacity = msg.write_buffer_capacity(); - - msg.replace_body(body); - } - Ok(WriterState::Done) - } - - fn write(&mut self, payload: &Binary) -> io::Result { - self.written += payload.len() as u64; - if !self.flags.contains(Flags::DISCONNECTED) { - if self.flags.contains(Flags::STARTED) { - // shortcut for upgraded connection - if self.flags.contains(Flags::UPGRADE) { - if self.buffer.is_empty() { - let pl: &[u8] = payload.as_ref(); - let n = match Self::write_data(&mut self.stream, pl) { - Err(err) => { - self.disconnected(); - return Err(err); - } - Ok(val) => val, - }; - if n < pl.len() { - self.buffer.write(&pl[n..])?; - return Ok(WriterState::Done); - } - } else { - self.buffer.write(payload.as_ref())?; - } - } else { - // TODO: add warning, write after EOF - self.buffer.write(payload.as_ref())?; - } - } else { - // could be response to EXCEPT header - self.buffer.write(payload.as_ref())?; - } - } - - if self.buffer.len() > self.buffer_capacity { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - fn write_eof(&mut self) -> io::Result { - if !self.buffer.write_eof()? { - Err(io::Error::new( - io::ErrorKind::Other, - "Last payload item, but eof is not reached", - )) - } else if self.buffer.len() > MAX_WRITE_BUFFER_SIZE { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - #[inline] - fn poll_completed(&mut self, shutdown: bool) -> Poll<(), io::Error> { - if self.flags.contains(Flags::DISCONNECTED) { - return Err(io::Error::new(io::ErrorKind::Other, "disconnected")); - } - - if !self.buffer.is_empty() { - let written = { - match Self::write_data(&mut self.stream, self.buffer.as_ref().as_ref()) { - Err(err) => { - self.disconnected(); - return Err(err); - } - Ok(val) => val, - } - }; - let _ = self.buffer.split_to(written); - if shutdown && !self.buffer.is_empty() - || (self.buffer.len() > self.buffer_capacity) - { - return Ok(Async::NotReady); - } - } - if shutdown { - self.stream.poll_flush()?; - self.stream.shutdown() - } else { - Ok(self.stream.poll_flush()?) - } - } -} diff --git a/src/server/h2.rs b/src/server/h2.rs deleted file mode 100644 index 27ae47851..000000000 --- a/src/server/h2.rs +++ /dev/null @@ -1,453 +0,0 @@ -use std::collections::VecDeque; -use std::io::{Read, Write}; -use std::net::SocketAddr; -use std::rc::Rc; -use std::time::Instant; -use std::{cmp, io, mem}; - -use bytes::{Buf, Bytes}; -use futures::{Async, Future, Poll, Stream}; -use http2::server::{self, Connection, Handshake, SendResponse}; -use http2::{Reason, RecvStream}; -use modhttp::request::Parts; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_timer::Delay; - -use error::{Error, PayloadError}; -use extensions::Extensions; -use http::{StatusCode, Version}; -use payload::{Payload, PayloadStatus, PayloadWriter}; -use uri::Url; - -use super::error::{HttpDispatchError, ServerError}; -use super::h2writer::H2Writer; -use super::input::PayloadType; -use super::settings::ServiceConfig; -use super::{HttpHandler, HttpHandlerTask, IoStream, Writer}; - -bitflags! { - struct Flags: u8 { - const DISCONNECTED = 0b0000_0010; - } -} - -/// HTTP/2 Transport -pub(crate) struct Http2 -where - T: AsyncRead + AsyncWrite + 'static, - H: HttpHandler + 'static, -{ - flags: Flags, - settings: ServiceConfig, - addr: Option, - state: State>, - tasks: VecDeque>, - keepalive_timer: Option, - extensions: Option>, -} - -enum State { - Handshake(Handshake), - Connection(Connection), - Empty, -} - -impl Http2 -where - T: IoStream + 'static, - H: HttpHandler + 'static, -{ - pub fn new( - settings: ServiceConfig, io: T, buf: Bytes, keepalive_timer: Option, - ) -> Self { - let addr = io.peer_addr(); - let extensions = io.extensions(); - Http2 { - flags: Flags::empty(), - tasks: VecDeque::new(), - state: State::Handshake(server::handshake(IoWrapper { - unread: if buf.is_empty() { None } else { Some(buf) }, - inner: io, - })), - addr, - settings, - extensions, - keepalive_timer, - } - } - - pub(crate) fn shutdown(&mut self) { - self.state = State::Empty; - self.tasks.clear(); - self.keepalive_timer.take(); - } - - pub fn settings(&self) -> &ServiceConfig { - &self.settings - } - - pub fn poll(&mut self) -> Poll<(), HttpDispatchError> { - // server - if let State::Connection(ref mut conn) = self.state { - // keep-alive timer - if let Some(ref mut timeout) = self.keepalive_timer { - match timeout.poll() { - Ok(Async::Ready(_)) => { - trace!("Keep-alive timeout, close connection"); - return Ok(Async::Ready(())); - } - Ok(Async::NotReady) => (), - Err(_) => unreachable!(), - } - } - - loop { - let mut not_ready = true; - let disconnected = self.flags.contains(Flags::DISCONNECTED); - - // check in-flight connections - for item in &mut self.tasks { - // read payload - if !disconnected { - item.poll_payload(); - } - - if !item.flags.contains(EntryFlags::EOF) { - if disconnected { - item.flags.insert(EntryFlags::EOF); - } else { - let retry = item.payload.need_read() == PayloadStatus::Read; - loop { - match item.task.poll_io(&mut item.stream) { - Ok(Async::Ready(ready)) => { - if ready { - item.flags.insert( - EntryFlags::EOF | EntryFlags::FINISHED, - ); - } else { - item.flags.insert(EntryFlags::EOF); - } - not_ready = false; - } - Ok(Async::NotReady) => { - if item.payload.need_read() - == PayloadStatus::Read - && !retry - { - continue; - } - } - Err(err) => { - error!("Unhandled error: {}", err); - item.flags.insert( - EntryFlags::EOF - | EntryFlags::ERROR - | EntryFlags::WRITE_DONE, - ); - item.stream.reset(Reason::INTERNAL_ERROR); - } - } - break; - } - } - } - - if item.flags.contains(EntryFlags::EOF) - && !item.flags.contains(EntryFlags::FINISHED) - { - match item.task.poll_completed() { - Ok(Async::NotReady) => (), - Ok(Async::Ready(_)) => { - item.flags.insert( - EntryFlags::FINISHED | EntryFlags::WRITE_DONE, - ); - } - Err(err) => { - item.flags.insert( - EntryFlags::ERROR - | EntryFlags::WRITE_DONE - | EntryFlags::FINISHED, - ); - error!("Unhandled error: {}", err); - } - } - } - - if item.flags.contains(EntryFlags::FINISHED) - && !item.flags.contains(EntryFlags::WRITE_DONE) - && !disconnected - { - match item.stream.poll_completed(false) { - Ok(Async::NotReady) => (), - Ok(Async::Ready(_)) => { - not_ready = false; - item.flags.insert(EntryFlags::WRITE_DONE); - } - Err(_) => { - item.flags.insert(EntryFlags::ERROR); - } - } - } - } - - // cleanup finished tasks - while !self.tasks.is_empty() { - if self.tasks[0].flags.contains(EntryFlags::FINISHED) - && self.tasks[0].flags.contains(EntryFlags::WRITE_DONE) - || self.tasks[0].flags.contains(EntryFlags::ERROR) - { - self.tasks.pop_front(); - } else { - break; - } - } - - // get request - if !self.flags.contains(Flags::DISCONNECTED) { - match conn.poll() { - Ok(Async::Ready(None)) => { - not_ready = false; - self.flags.insert(Flags::DISCONNECTED); - for entry in &mut self.tasks { - entry.task.disconnected() - } - } - Ok(Async::Ready(Some((req, resp)))) => { - not_ready = false; - let (parts, body) = req.into_parts(); - - // stop keepalive timer - self.keepalive_timer.take(); - - self.tasks.push_back(Entry::new( - parts, - body, - resp, - self.addr, - self.settings.clone(), - self.extensions.clone(), - )); - } - Ok(Async::NotReady) => { - // start keep-alive timer - if self.tasks.is_empty() { - if self.settings.keep_alive_enabled() { - if self.keepalive_timer.is_none() { - if let Some(ka) = self.settings.keep_alive() { - trace!("Start keep-alive timer"); - let mut timeout = - Delay::new(Instant::now() + ka); - // register timeout - let _ = timeout.poll(); - self.keepalive_timer = Some(timeout); - } - } - } else { - // keep-alive disable, drop connection - return conn.poll_close().map_err(|e| e.into()); - } - } else { - // keep-alive unset, rely on operating system - return Ok(Async::NotReady); - } - } - Err(err) => { - trace!("Connection error: {}", err); - self.flags.insert(Flags::DISCONNECTED); - for entry in &mut self.tasks { - entry.task.disconnected() - } - self.keepalive_timer.take(); - } - } - } - - if not_ready { - if self.tasks.is_empty() && self.flags.contains(Flags::DISCONNECTED) - { - return conn.poll_close().map_err(|e| e.into()); - } else { - return Ok(Async::NotReady); - } - } - } - } - - // handshake - self.state = if let State::Handshake(ref mut handshake) = self.state { - match handshake.poll() { - Ok(Async::Ready(conn)) => State::Connection(conn), - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(err) => { - trace!("Error handling connection: {}", err); - return Err(err.into()); - } - } - } else { - mem::replace(&mut self.state, State::Empty) - }; - - self.poll() - } -} - -bitflags! { - struct EntryFlags: u8 { - const EOF = 0b0000_0001; - const REOF = 0b0000_0010; - const ERROR = 0b0000_0100; - const FINISHED = 0b0000_1000; - const WRITE_DONE = 0b0001_0000; - } -} - -enum EntryPipe { - Task(H::Task), - Error(Box), -} - -impl EntryPipe { - fn disconnected(&mut self) { - match *self { - EntryPipe::Task(ref mut task) => task.disconnected(), - EntryPipe::Error(ref mut task) => task.disconnected(), - } - } - fn poll_io(&mut self, io: &mut Writer) -> Poll { - match *self { - EntryPipe::Task(ref mut task) => task.poll_io(io), - EntryPipe::Error(ref mut task) => task.poll_io(io), - } - } - fn poll_completed(&mut self) -> Poll<(), Error> { - match *self { - EntryPipe::Task(ref mut task) => task.poll_completed(), - EntryPipe::Error(ref mut task) => task.poll_completed(), - } - } -} - -struct Entry { - task: EntryPipe, - payload: PayloadType, - recv: RecvStream, - stream: H2Writer, - flags: EntryFlags, -} - -impl Entry { - fn new( - parts: Parts, recv: RecvStream, resp: SendResponse, - addr: Option, settings: ServiceConfig, - extensions: Option>, - ) -> Entry - where - H: HttpHandler + 'static, - { - // Payload and Content-Encoding - let (psender, payload) = Payload::new(false); - - let mut msg = settings.get_request(); - { - let inner = msg.inner_mut(); - inner.url = Url::new(parts.uri); - inner.method = parts.method; - inner.version = parts.version; - inner.headers = parts.headers; - inner.stream_extensions = extensions; - *inner.payload.borrow_mut() = Some(payload); - inner.addr = addr; - } - - // Payload sender - let psender = PayloadType::new(msg.headers(), psender); - - // start request processing - let task = match settings.handler().handle(msg) { - Ok(task) => EntryPipe::Task(task), - Err(_) => EntryPipe::Error(ServerError::err( - Version::HTTP_2, - StatusCode::NOT_FOUND, - )), - }; - - Entry { - task, - recv, - payload: psender, - stream: H2Writer::new(resp, settings), - flags: EntryFlags::empty(), - } - } - - fn poll_payload(&mut self) { - while !self.flags.contains(EntryFlags::REOF) - && self.payload.need_read() == PayloadStatus::Read - { - match self.recv.poll() { - Ok(Async::Ready(Some(chunk))) => { - let l = chunk.len(); - self.payload.feed_data(chunk); - if let Err(err) = self.recv.release_capacity().release_capacity(l) { - self.payload.set_error(PayloadError::Http2(err)); - break; - } - } - Ok(Async::Ready(None)) => { - self.flags.insert(EntryFlags::REOF); - self.payload.feed_eof(); - } - Ok(Async::NotReady) => break, - Err(err) => { - self.payload.set_error(PayloadError::Http2(err)); - break; - } - } - } - } -} - -struct IoWrapper { - unread: Option, - inner: T, -} - -impl Read for IoWrapper { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - if let Some(mut bytes) = self.unread.take() { - let size = cmp::min(buf.len(), bytes.len()); - buf[..size].copy_from_slice(&bytes[..size]); - if bytes.len() > size { - bytes.split_to(size); - self.unread = Some(bytes); - } - Ok(size) - } else { - self.inner.read(buf) - } - } -} - -impl Write for IoWrapper { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.inner.write(buf) - } - fn flush(&mut self) -> io::Result<()> { - self.inner.flush() - } -} - -impl AsyncRead for IoWrapper { - unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { - self.inner.prepare_uninitialized_buffer(buf) - } -} - -impl AsyncWrite for IoWrapper { - fn shutdown(&mut self) -> Poll<(), io::Error> { - self.inner.shutdown() - } - fn write_buf(&mut self, buf: &mut B) -> Poll { - self.inner.write_buf(buf) - } -} diff --git a/src/server/h2writer.rs b/src/server/h2writer.rs deleted file mode 100644 index 51d4dce6f..000000000 --- a/src/server/h2writer.rs +++ /dev/null @@ -1,259 +0,0 @@ -#![cfg_attr( - feature = "cargo-clippy", - allow(clippy::redundant_field_names) -)] - -use std::{cmp, io}; - -use bytes::{Bytes, BytesMut}; -use futures::{Async, Poll}; -use http2::server::SendResponse; -use http2::{Reason, SendStream}; -use modhttp::Response; - -use super::helpers; -use super::message::Request; -use super::output::{Output, ResponseInfo, ResponseLength}; -use super::settings::ServiceConfig; -use super::{Writer, WriterState, MAX_WRITE_BUFFER_SIZE}; -use body::{Binary, Body}; -use header::ContentEncoding; -use http::header::{ - HeaderValue, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, -}; -use http::{HttpTryFrom, Method, Version}; -use httpresponse::HttpResponse; - -const CHUNK_SIZE: usize = 16_384; - -bitflags! { - struct Flags: u8 { - const STARTED = 0b0000_0001; - const DISCONNECTED = 0b0000_0010; - const EOF = 0b0000_0100; - const RESERVED = 0b0000_1000; - } -} - -pub(crate) struct H2Writer { - respond: SendResponse, - stream: Option>, - flags: Flags, - written: u64, - buffer: Output, - buffer_capacity: usize, - settings: ServiceConfig, -} - -impl H2Writer { - pub fn new(respond: SendResponse, settings: ServiceConfig) -> H2Writer { - H2Writer { - stream: None, - flags: Flags::empty(), - written: 0, - buffer: Output::Buffer(settings.get_bytes()), - buffer_capacity: 0, - respond, - settings, - } - } - - pub fn reset(&mut self, reason: Reason) { - if let Some(mut stream) = self.stream.take() { - stream.send_reset(reason) - } - } -} - -impl Drop for H2Writer { - fn drop(&mut self) { - self.settings.release_bytes(self.buffer.take()); - } -} - -impl Writer for H2Writer { - fn written(&self) -> u64 { - self.written - } - - #[inline] - fn set_date(&mut self) { - self.settings.set_date(self.buffer.as_mut(), true) - } - - #[inline] - fn buffer(&mut self) -> &mut BytesMut { - self.buffer.as_mut() - } - - fn start( - &mut self, req: &Request, msg: &mut HttpResponse, encoding: ContentEncoding, - ) -> io::Result { - // prepare response - self.flags.insert(Flags::STARTED); - let mut info = ResponseInfo::new(req.inner.method == Method::HEAD); - self.buffer.for_server(&mut info, &req.inner, msg, encoding); - - let mut has_date = false; - let mut resp = Response::new(()); - *resp.status_mut() = msg.status(); - *resp.version_mut() = Version::HTTP_2; - for (key, value) in msg.headers().iter() { - match *key { - // http2 specific - CONNECTION | TRANSFER_ENCODING => continue, - CONTENT_ENCODING => if encoding != ContentEncoding::Identity { - continue; - }, - CONTENT_LENGTH => match info.length { - ResponseLength::None => (), - _ => continue, - }, - DATE => has_date = true, - _ => (), - } - resp.headers_mut().append(key, value.clone()); - } - - // set date header - if !has_date { - let mut bytes = BytesMut::with_capacity(29); - self.settings.set_date(&mut bytes, false); - resp.headers_mut() - .insert(DATE, HeaderValue::try_from(bytes.freeze()).unwrap()); - } - - // content length - match info.length { - ResponseLength::Zero => { - resp.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::from_static("0")); - self.flags.insert(Flags::EOF); - } - ResponseLength::Length(len) => { - let mut val = BytesMut::new(); - helpers::convert_usize(len, &mut val); - let l = val.len(); - resp.headers_mut().insert( - CONTENT_LENGTH, - HeaderValue::try_from(val.split_to(l - 2).freeze()).unwrap(), - ); - } - ResponseLength::Length64(len) => { - let l = format!("{}", len); - resp.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::try_from(l.as_str()).unwrap()); - } - _ => (), - } - if let Some(ce) = info.content_encoding { - resp.headers_mut() - .insert(CONTENT_ENCODING, HeaderValue::try_from(ce).unwrap()); - } - - trace!("Response: {:?}", resp); - - match self - .respond - .send_response(resp, self.flags.contains(Flags::EOF)) - { - Ok(stream) => self.stream = Some(stream), - Err(_) => return Err(io::Error::new(io::ErrorKind::Other, "err")), - } - - let body = msg.replace_body(Body::Empty); - if let Body::Binary(bytes) = body { - if bytes.is_empty() { - Ok(WriterState::Done) - } else { - self.flags.insert(Flags::EOF); - self.buffer.write(bytes.as_ref())?; - if let Some(ref mut stream) = self.stream { - self.flags.insert(Flags::RESERVED); - stream.reserve_capacity(cmp::min(self.buffer.len(), CHUNK_SIZE)); - } - Ok(WriterState::Pause) - } - } else { - msg.replace_body(body); - self.buffer_capacity = msg.write_buffer_capacity(); - Ok(WriterState::Done) - } - } - - fn write(&mut self, payload: &Binary) -> io::Result { - if !self.flags.contains(Flags::DISCONNECTED) { - if self.flags.contains(Flags::STARTED) { - // TODO: add warning, write after EOF - self.buffer.write(payload.as_ref())?; - } else { - // might be response for EXCEPT - error!("Not supported"); - } - } - - if self.buffer.len() > self.buffer_capacity { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - fn write_eof(&mut self) -> io::Result { - self.flags.insert(Flags::EOF); - if !self.buffer.write_eof()? { - Err(io::Error::new( - io::ErrorKind::Other, - "Last payload item, but eof is not reached", - )) - } else if self.buffer.len() > MAX_WRITE_BUFFER_SIZE { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - fn poll_completed(&mut self, _shutdown: bool) -> Poll<(), io::Error> { - if !self.flags.contains(Flags::STARTED) { - return Ok(Async::NotReady); - } - - if let Some(ref mut stream) = self.stream { - // reserve capacity - if !self.flags.contains(Flags::RESERVED) && !self.buffer.is_empty() { - self.flags.insert(Flags::RESERVED); - stream.reserve_capacity(cmp::min(self.buffer.len(), CHUNK_SIZE)); - } - - loop { - match stream.poll_capacity() { - Ok(Async::NotReady) => return Ok(Async::NotReady), - Ok(Async::Ready(None)) => return Ok(Async::Ready(())), - Ok(Async::Ready(Some(cap))) => { - let len = self.buffer.len(); - let bytes = self.buffer.split_to(cmp::min(cap, len)); - let eof = - self.buffer.is_empty() && self.flags.contains(Flags::EOF); - self.written += bytes.len() as u64; - - if let Err(e) = stream.send_data(bytes.freeze(), eof) { - return Err(io::Error::new(io::ErrorKind::Other, e)); - } else if !self.buffer.is_empty() { - let cap = cmp::min(self.buffer.len(), CHUNK_SIZE); - stream.reserve_capacity(cap); - } else { - if eof { - stream.reserve_capacity(0); - continue; - } - self.flags.remove(Flags::RESERVED); - return Ok(Async::Ready(())); - } - } - Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)), - } - } - } - Ok(Async::Ready(())) - } -} diff --git a/src/server/handler.rs b/src/server/handler.rs deleted file mode 100644 index 33e50ac34..000000000 --- a/src/server/handler.rs +++ /dev/null @@ -1,208 +0,0 @@ -use futures::{Async, Future, Poll}; - -use super::message::Request; -use super::Writer; -use error::Error; - -/// Low level http request handler -#[allow(unused_variables)] -pub trait HttpHandler: 'static { - /// Request handling task - type Task: HttpHandlerTask; - - /// Handle request - fn handle(&self, req: Request) -> Result; -} - -impl HttpHandler for Box>> { - type Task = Box; - - fn handle(&self, req: Request) -> Result, Request> { - self.as_ref().handle(req) - } -} - -/// Low level http request handler -pub trait HttpHandlerTask { - /// Poll task, this method is used before or after *io* object is available - fn poll_completed(&mut self) -> Poll<(), Error> { - Ok(Async::Ready(())) - } - - /// Poll task when *io* object is available - fn poll_io(&mut self, io: &mut Writer) -> Poll; - - /// Connection is disconnected - fn disconnected(&mut self) {} -} - -impl HttpHandlerTask for Box { - fn poll_io(&mut self, io: &mut Writer) -> Poll { - self.as_mut().poll_io(io) - } -} - -pub(super) struct HttpHandlerTaskFut { - task: T, -} - -impl HttpHandlerTaskFut { - pub(crate) fn new(task: T) -> Self { - Self { task } - } -} - -impl Future for HttpHandlerTaskFut { - type Item = (); - type Error = (); - - fn poll(&mut self) -> Poll<(), ()> { - self.task.poll_completed().map_err(|_| ()) - } -} - -/// Conversion helper trait -pub trait IntoHttpHandler { - /// The associated type which is result of conversion. - type Handler: HttpHandler; - - /// Convert into `HttpHandler` object. - fn into_handler(self) -> Self::Handler; -} - -impl IntoHttpHandler for T { - type Handler = T; - - fn into_handler(self) -> Self::Handler { - self - } -} - -impl IntoHttpHandler for Vec { - type Handler = VecHttpHandler; - - fn into_handler(self) -> Self::Handler { - VecHttpHandler(self.into_iter().map(|item| item.into_handler()).collect()) - } -} - -#[doc(hidden)] -pub struct VecHttpHandler(Vec); - -impl HttpHandler for VecHttpHandler { - type Task = H::Task; - - fn handle(&self, mut req: Request) -> Result { - for h in &self.0 { - req = match h.handle(req) { - Ok(task) => return Ok(task), - Err(e) => e, - }; - } - Err(req) - } -} - -macro_rules! http_handler ({$EN:ident, $(($n:tt, $T:ident)),+} => { - impl<$($T: HttpHandler,)+> HttpHandler for ($($T,)+) { - type Task = $EN<$($T,)+>; - - fn handle(&self, mut req: Request) -> Result { - $( - req = match self.$n.handle(req) { - Ok(task) => return Ok($EN::$T(task)), - Err(e) => e, - }; - )+ - Err(req) - } - } - - #[doc(hidden)] - pub enum $EN<$($T: HttpHandler,)+> { - $($T ($T::Task),)+ - } - - impl<$($T: HttpHandler,)+> HttpHandlerTask for $EN<$($T,)+> - { - fn poll_completed(&mut self) -> Poll<(), Error> { - match self { - $($EN :: $T(ref mut task) => task.poll_completed(),)+ - } - } - - fn poll_io(&mut self, io: &mut Writer) -> Poll { - match self { - $($EN::$T(ref mut task) => task.poll_io(io),)+ - } - } - - /// Connection is disconnected - fn disconnected(&mut self) { - match self { - $($EN::$T(ref mut task) => task.disconnected(),)+ - } - } - } -}); - -http_handler!(HttpHandlerTask1, (0, A)); -http_handler!(HttpHandlerTask2, (0, A), (1, B)); -http_handler!(HttpHandlerTask3, (0, A), (1, B), (2, C)); -http_handler!(HttpHandlerTask4, (0, A), (1, B), (2, C), (3, D)); -http_handler!(HttpHandlerTask5, (0, A), (1, B), (2, C), (3, D), (4, E)); -http_handler!( - HttpHandlerTask6, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F) -); -http_handler!( - HttpHandlerTask7, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G) -); -http_handler!( - HttpHandlerTask8, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H) -); -http_handler!( - HttpHandlerTask9, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H), - (8, I) -); -http_handler!( - HttpHandlerTask10, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H), - (8, I), - (9, J) -); diff --git a/src/server/http.rs b/src/server/http.rs deleted file mode 100644 index 6a7790c13..000000000 --- a/src/server/http.rs +++ /dev/null @@ -1,584 +0,0 @@ -use std::{fmt, io, mem, net}; - -use actix::{Addr, System}; -use actix_net::server::Server; -use actix_net::service::NewService; -use actix_net::ssl; - -use net2::TcpBuilder; -use num_cpus; - -#[cfg(feature = "tls")] -use native_tls::TlsAcceptor; - -#[cfg(any(feature = "alpn", feature = "ssl"))] -use openssl::ssl::SslAcceptorBuilder; - -#[cfg(feature = "rust-tls")] -use rustls::ServerConfig; - -use super::acceptor::{AcceptorServiceFactory, DefaultAcceptor}; -use super::builder::{HttpServiceBuilder, ServiceProvider}; -use super::{IntoHttpHandler, KeepAlive}; - -struct Socket { - scheme: &'static str, - lst: net::TcpListener, - addr: net::SocketAddr, - handler: Box, -} - -/// An HTTP Server -/// -/// By default it serves HTTP2 when HTTPs is enabled, -/// in order to change it, use `ServerFlags` that can be provided -/// to acceptor service. -pub struct HttpServer -where - H: IntoHttpHandler + 'static, - F: Fn() -> H + Send + Clone, -{ - pub(super) factory: F, - pub(super) host: Option, - pub(super) keep_alive: KeepAlive, - pub(super) client_timeout: u64, - pub(super) client_shutdown: u64, - backlog: i32, - threads: usize, - exit: bool, - shutdown_timeout: u16, - no_http2: bool, - no_signals: bool, - maxconn: usize, - maxconnrate: usize, - sockets: Vec, -} - -impl HttpServer -where - H: IntoHttpHandler + 'static, - F: Fn() -> H + Send + Clone + 'static, -{ - /// Create new http server with application factory - pub fn new(factory: F) -> HttpServer { - HttpServer { - factory, - threads: num_cpus::get(), - host: None, - backlog: 2048, - keep_alive: KeepAlive::Timeout(5), - shutdown_timeout: 30, - exit: false, - no_http2: false, - no_signals: false, - maxconn: 25_600, - maxconnrate: 256, - client_timeout: 5000, - client_shutdown: 5000, - sockets: Vec::new(), - } - } - - /// Set number of workers to start. - /// - /// By default http server uses number of available logical cpu as threads - /// count. - pub fn workers(mut self, num: usize) -> Self { - self.threads = num; - self - } - - /// Set the maximum number of pending connections. - /// - /// This refers to the number of clients that can be waiting to be served. - /// Exceeding this number results in the client getting an error when - /// attempting to connect. It should only affect servers under significant - /// load. - /// - /// Generally set in the 64-2048 range. Default value is 2048. - /// - /// This method should be called before `bind()` method call. - pub fn backlog(mut self, num: i32) -> Self { - self.backlog = num; - self - } - - /// Sets the maximum per-worker number of concurrent connections. - /// - /// All socket listeners will stop accepting connections when this limit is reached - /// for each worker. - /// - /// By default max connections is set to a 25k. - pub fn maxconn(mut self, num: usize) -> Self { - self.maxconn = num; - self - } - - /// Sets the maximum per-worker concurrent connection establish process. - /// - /// All listeners will stop accepting connections when this limit is reached. It - /// can be used to limit the global SSL CPU usage. - /// - /// By default max connections is set to a 256. - pub fn maxconnrate(mut self, num: usize) -> Self { - self.maxconnrate = num; - self - } - - /// Set server keep-alive setting. - /// - /// By default keep alive is set to a 5 seconds. - pub fn keep_alive>(mut self, val: T) -> Self { - self.keep_alive = val.into(); - self - } - - /// Set server client timeout in milliseconds for first request. - /// - /// Defines a timeout for reading client request header. If a client does not transmit - /// the entire set headers within this time, the request is terminated with - /// the 408 (Request Time-out) error. - /// - /// To disable timeout set value to 0. - /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_timeout(mut self, val: u64) -> Self { - self.client_timeout = val; - self - } - - /// Set server connection shutdown timeout in milliseconds. - /// - /// Defines a timeout for shutdown connection. If a shutdown procedure does not complete - /// within this time, the request is dropped. - /// - /// To disable timeout set value to 0. - /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_shutdown(mut self, val: u64) -> Self { - self.client_shutdown = val; - self - } - - /// Set server host name. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - pub fn server_hostname(mut self, val: String) -> Self { - self.host = Some(val); - self - } - - /// Stop actix system. - /// - /// `SystemExit` message stops currently running system. - pub fn system_exit(mut self) -> Self { - self.exit = true; - self - } - - /// Disable signal handling - pub fn disable_signals(mut self) -> Self { - self.no_signals = true; - self - } - - /// Timeout for graceful workers shutdown. - /// - /// After receiving a stop signal, workers have this much time to finish - /// serving requests. Workers still alive after the timeout are force - /// dropped. - /// - /// By default shutdown timeout sets to 30 seconds. - pub fn shutdown_timeout(mut self, sec: u16) -> Self { - self.shutdown_timeout = sec; - self - } - - /// Disable `HTTP/2` support - // #[doc(hidden)] - // #[deprecated( - // since = "0.7.4", - // note = "please use acceptor service with proper ServerFlags parama" - // )] - pub fn no_http2(mut self) -> Self { - self.no_http2 = true; - self - } - - /// Get addresses of bound sockets. - pub fn addrs(&self) -> Vec { - self.sockets.iter().map(|s| s.addr).collect() - } - - /// Get addresses of bound sockets and the scheme for it. - /// - /// This is useful when the server is bound from different sources - /// with some sockets listening on http and some listening on https - /// and the user should be presented with an enumeration of which - /// socket requires which protocol. - pub fn addrs_with_scheme(&self) -> Vec<(net::SocketAddr, &str)> { - self.sockets.iter().map(|s| (s.addr, s.scheme)).collect() - } - - /// Use listener for accepting incoming connection requests - /// - /// HttpServer does not change any configuration for TcpListener, - /// it needs to be configured before passing it to listen() method. - pub fn listen(mut self, lst: net::TcpListener) -> Self { - let addr = lst.local_addr().unwrap(); - self.sockets.push(Socket { - lst, - addr, - scheme: "http", - handler: Box::new(HttpServiceBuilder::new( - self.factory.clone(), - DefaultAcceptor, - )), - }); - - self - } - - #[doc(hidden)] - /// Use listener for accepting incoming connection requests - pub fn listen_with(mut self, lst: net::TcpListener, acceptor: A) -> Self - where - A: AcceptorServiceFactory, - ::InitError: fmt::Debug, - { - let addr = lst.local_addr().unwrap(); - self.sockets.push(Socket { - lst, - addr, - scheme: "https", - handler: Box::new(HttpServiceBuilder::new(self.factory.clone(), acceptor)), - }); - - self - } - - #[cfg(feature = "tls")] - /// Use listener for accepting incoming tls connection requests - /// - /// HttpServer does not change any configuration for TcpListener, - /// it needs to be configured before passing it to listen() method. - pub fn listen_tls(self, lst: net::TcpListener, acceptor: TlsAcceptor) -> Self { - use actix_net::service::NewServiceExt; - - self.listen_with(lst, move || { - ssl::NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) - }) - } - - #[cfg(any(feature = "alpn", feature = "ssl"))] - /// Use listener for accepting incoming tls connection requests - /// - /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn listen_ssl( - self, lst: net::TcpListener, builder: SslAcceptorBuilder, - ) -> io::Result { - use super::{openssl_acceptor_with_flags, ServerFlags}; - use actix_net::service::NewServiceExt; - - let flags = if self.no_http2 { - ServerFlags::HTTP1 - } else { - ServerFlags::HTTP1 | ServerFlags::HTTP2 - }; - - let acceptor = openssl_acceptor_with_flags(builder, flags)?; - Ok(self.listen_with(lst, move || { - ssl::OpensslAcceptor::new(acceptor.clone()).map_err(|_| ()) - })) - } - - #[cfg(feature = "rust-tls")] - /// Use listener for accepting incoming tls connection requests - /// - /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn listen_rustls(self, lst: net::TcpListener, config: ServerConfig) -> Self { - use super::{RustlsAcceptor, ServerFlags}; - use actix_net::service::NewServiceExt; - - // alpn support - let flags = if self.no_http2 { - ServerFlags::HTTP1 - } else { - ServerFlags::HTTP1 | ServerFlags::HTTP2 - }; - - self.listen_with(lst, move || { - RustlsAcceptor::with_flags(config.clone(), flags).map_err(|_| ()) - }) - } - - /// The socket address to bind - /// - /// To bind multiple addresses this method can be called multiple times. - pub fn bind(mut self, addr: S) -> io::Result { - let sockets = self.bind2(addr)?; - - for lst in sockets { - self = self.listen(lst); - } - - Ok(self) - } - - /// Start listening for incoming connections with supplied acceptor. - #[doc(hidden)] - #[cfg_attr( - feature = "cargo-clippy", - allow(clippy::needless_pass_by_value) - )] - pub fn bind_with(mut self, addr: S, acceptor: A) -> io::Result - where - S: net::ToSocketAddrs, - A: AcceptorServiceFactory, - ::InitError: fmt::Debug, - { - let sockets = self.bind2(addr)?; - - for lst in sockets { - let addr = lst.local_addr().unwrap(); - self.sockets.push(Socket { - lst, - addr, - scheme: "https", - handler: Box::new(HttpServiceBuilder::new( - self.factory.clone(), - acceptor.clone(), - )), - }); - } - - Ok(self) - } - - fn bind2( - &self, addr: S, - ) -> io::Result> { - let mut err = None; - let mut succ = false; - let mut sockets = Vec::new(); - for addr in addr.to_socket_addrs()? { - match create_tcp_listener(addr, self.backlog) { - Ok(lst) => { - succ = true; - sockets.push(lst); - } - Err(e) => err = Some(e), - } - } - - if !succ { - if let Some(e) = err.take() { - Err(e) - } else { - Err(io::Error::new( - io::ErrorKind::Other, - "Can not bind to address.", - )) - } - } else { - Ok(sockets) - } - } - - #[cfg(feature = "tls")] - /// The ssl socket address to bind - /// - /// To bind multiple addresses this method can be called multiple times. - pub fn bind_tls( - self, addr: S, acceptor: TlsAcceptor, - ) -> io::Result { - use actix_net::service::NewServiceExt; - use actix_net::ssl::NativeTlsAcceptor; - - self.bind_with(addr, move || { - NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) - }) - } - - #[cfg(any(feature = "alpn", feature = "ssl"))] - /// Start listening for incoming tls connections. - /// - /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn bind_ssl(self, addr: S, builder: SslAcceptorBuilder) -> io::Result - where - S: net::ToSocketAddrs, - { - use super::{openssl_acceptor_with_flags, ServerFlags}; - use actix_net::service::NewServiceExt; - - // alpn support - let flags = if self.no_http2 { - ServerFlags::HTTP1 - } else { - ServerFlags::HTTP1 | ServerFlags::HTTP2 - }; - - let acceptor = openssl_acceptor_with_flags(builder, flags)?; - self.bind_with(addr, move || { - ssl::OpensslAcceptor::new(acceptor.clone()).map_err(|_| ()) - }) - } - - #[cfg(feature = "rust-tls")] - /// Start listening for incoming tls connections. - /// - /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn bind_rustls( - self, addr: S, builder: ServerConfig, - ) -> io::Result { - use super::{RustlsAcceptor, ServerFlags}; - use actix_net::service::NewServiceExt; - - // alpn support - let flags = if self.no_http2 { - ServerFlags::HTTP1 - } else { - ServerFlags::HTTP1 | ServerFlags::HTTP2 - }; - - self.bind_with(addr, move || { - RustlsAcceptor::with_flags(builder.clone(), flags).map_err(|_| ()) - }) - } -} - -impl H + Send + Clone> HttpServer { - /// Start listening for incoming connections. - /// - /// This method starts number of http workers in separate threads. - /// For each address this method starts separate thread which does - /// `accept()` in a loop. - /// - /// This methods panics if no socket addresses get bound. - /// - /// This method requires to run within properly configured `Actix` system. - /// - /// ```rust - /// extern crate actix_web; - /// use actix_web::{actix, server, App, HttpResponse}; - /// - /// fn main() { - /// let sys = actix::System::new("example"); // <- create Actix system - /// - /// server::new(|| App::new().resource("/", |r| r.h(|_: &_| HttpResponse::Ok()))) - /// .bind("127.0.0.1:0") - /// .expect("Can not bind to 127.0.0.1:0") - /// .start(); - /// # actix::System::current().stop(); - /// sys.run(); // <- Run actix system, this method starts all async processes - /// } - /// ``` - pub fn start(mut self) -> Addr { - ssl::max_concurrent_ssl_connect(self.maxconnrate); - - let mut srv = Server::new() - .workers(self.threads) - .maxconn(self.maxconn) - .shutdown_timeout(self.shutdown_timeout); - - srv = if self.exit { srv.system_exit() } else { srv }; - srv = if self.no_signals { - srv.disable_signals() - } else { - srv - }; - - let sockets = mem::replace(&mut self.sockets, Vec::new()); - - for socket in sockets { - let host = self - .host - .as_ref() - .map(|h| h.to_owned()) - .unwrap_or_else(|| format!("{}", socket.addr)); - let (secure, client_shutdown) = if socket.scheme == "https" { - (true, self.client_shutdown) - } else { - (false, 0) - }; - srv = socket.handler.register( - srv, - socket.lst, - host, - socket.addr, - self.keep_alive, - secure, - self.client_timeout, - client_shutdown, - ); - } - srv.start() - } - - /// Spawn new thread and start listening for incoming connections. - /// - /// This method spawns new thread and starts new actix system. Other than - /// that it is similar to `start()` method. This method blocks. - /// - /// This methods panics if no socket addresses get bound. - /// - /// ```rust,ignore - /// # extern crate futures; - /// # extern crate actix_web; - /// # use futures::Future; - /// use actix_web::*; - /// - /// fn main() { - /// HttpServer::new(|| App::new().resource("/", |r| r.h(|_| HttpResponse::Ok()))) - /// .bind("127.0.0.1:0") - /// .expect("Can not bind to 127.0.0.1:0") - /// .run(); - /// } - /// ``` - pub fn run(self) { - let sys = System::new("http-server"); - self.start(); - sys.run(); - } - - /// Register current http server as actix-net's server service - pub fn register(self, mut srv: Server) -> Server { - for socket in self.sockets { - let host = self - .host - .as_ref() - .map(|h| h.to_owned()) - .unwrap_or_else(|| format!("{}", socket.addr)); - let (secure, client_shutdown) = if socket.scheme == "https" { - (true, self.client_shutdown) - } else { - (false, 0) - }; - srv = socket.handler.register( - srv, - socket.lst, - host, - socket.addr, - self.keep_alive, - secure, - self.client_timeout, - client_shutdown, - ); - } - srv - } -} - -fn create_tcp_listener( - addr: net::SocketAddr, backlog: i32, -) -> io::Result { - let builder = match addr { - net::SocketAddr::V4(_) => TcpBuilder::new_v4()?, - net::SocketAddr::V6(_) => TcpBuilder::new_v6()?, - }; - builder.reuse_address(true)?; - builder.bind(addr)?; - Ok(builder.listen(backlog)?) -} diff --git a/src/server/incoming.rs b/src/server/incoming.rs deleted file mode 100644 index b13bba2a7..000000000 --- a/src/server/incoming.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Support for `Stream`, deprecated! -use std::{io, net}; - -use actix::{Actor, Arbiter, AsyncContext, Context, Handler, Message}; -use futures::{Future, Stream}; -use tokio_io::{AsyncRead, AsyncWrite}; - -use super::channel::{HttpChannel, WrapperStream}; -use super::handler::{HttpHandler, IntoHttpHandler}; -use super::http::HttpServer; -use super::settings::{ServerSettings, ServiceConfig}; - -impl Message for WrapperStream { - type Result = (); -} - -impl HttpServer -where - H: IntoHttpHandler, - F: Fn() -> H + Send + Clone, -{ - #[doc(hidden)] - #[deprecated(since = "0.7.8")] - /// Start listening for incoming connections from a stream. - /// - /// This method uses only one thread for handling incoming connections. - pub fn start_incoming(self, stream: S, secure: bool) - where - S: Stream + 'static, - T: AsyncRead + AsyncWrite + 'static, - { - // set server settings - let addr: net::SocketAddr = "127.0.0.1:8080".parse().unwrap(); - let apps = (self.factory)().into_handler(); - let settings = ServiceConfig::new( - apps, - self.keep_alive, - self.client_timeout, - self.client_shutdown, - ServerSettings::new(addr, "127.0.0.1:8080", secure), - ); - - // start server - HttpIncoming::create(move |ctx| { - ctx.add_message_stream(stream.map_err(|_| ()).map(WrapperStream::new)); - HttpIncoming { settings } - }); - } -} - -struct HttpIncoming { - settings: ServiceConfig, -} - -impl Actor for HttpIncoming { - type Context = Context; -} - -impl Handler> for HttpIncoming -where - T: AsyncRead + AsyncWrite, - H: HttpHandler, -{ - type Result = (); - - fn handle(&mut self, msg: WrapperStream, _: &mut Context) -> Self::Result { - Arbiter::spawn(HttpChannel::new(self.settings.clone(), msg).map_err(|_| ())); - } -} diff --git a/src/server/mod.rs b/src/server/mod.rs index ce3f2fbf6..7d64a6e20 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -117,33 +117,20 @@ use tokio_tcp::TcpStream; pub use actix_net::server::{PauseServer, ResumeServer, StopServer}; -pub(crate) mod acceptor; -pub(crate) mod builder; -mod channel; mod error; -pub(crate) mod h1; +// pub(crate) mod h1; #[doc(hidden)] pub mod h1codec; #[doc(hidden)] pub mod h1decoder; -mod h1writer; -mod h2; -mod h2writer; -mod handler; pub(crate) mod helpers; -mod http; -pub(crate) mod incoming; pub(crate) mod input; pub(crate) mod message; pub(crate) mod output; -pub(crate) mod service; +// pub(crate) mod service; pub(crate) mod settings; -mod ssl; -pub use self::handler::*; -pub use self::http::HttpServer; pub use self::message::Request; -pub use self::ssl::*; pub use self::error::{AcceptorError, HttpDispatchError}; pub use self::settings::ServerSettings; @@ -151,14 +138,11 @@ pub use self::settings::ServerSettings; #[doc(hidden)] pub mod h1disp; -#[doc(hidden)] -pub use self::acceptor::AcceptorTimeout; - #[doc(hidden)] pub use self::settings::{ServiceConfig, ServiceConfigBuilder}; -#[doc(hidden)] -pub use self::service::{H1Service, HttpService, StreamConfiguration}; +//#[doc(hidden)] +//pub use self::service::{H1Service, HttpService, StreamConfiguration}; #[doc(hidden)] pub use self::helpers::write_content_length; @@ -174,53 +158,11 @@ pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; const LW_BUFFER_SIZE: usize = 4096; const HW_BUFFER_SIZE: usize = 32_768; -/// Create new http server with application factory. -/// -/// This is shortcut for `server::HttpServer::new()` method. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{actix, server, App, HttpResponse}; -/// -/// fn main() { -/// let sys = actix::System::new("example"); // <- create Actix system -/// -/// server::new( -/// || App::new() -/// .resource("/", |r| r.f(|_| HttpResponse::Ok()))) -/// .bind("127.0.0.1:59090").unwrap() -/// .start(); -/// -/// # actix::System::current().stop(); -/// sys.run(); -/// } -/// ``` -pub fn new(factory: F) -> HttpServer -where - F: Fn() -> H + Send + Clone + 'static, - H: IntoHttpHandler + 'static, -{ - HttpServer::new(factory) -} - -#[doc(hidden)] -bitflags! { - ///Flags that can be used to configure HTTP Server. - pub struct ServerFlags: u8 { - ///Use HTTP1 protocol - const HTTP1 = 0b0000_0001; - ///Use HTTP2 protocol - const HTTP2 = 0b0000_0010; - } -} - #[derive(Debug, PartialEq, Clone, Copy)] /// Server keep-alive setting pub enum KeepAlive { /// Keep alive in seconds Timeout(usize), - /// Use `SO_KEEPALIVE` socket option, value in seconds - Tcp(usize), /// Relay on OS to shutdown tcp connection Os, /// Disabled @@ -243,41 +185,9 @@ impl From> for KeepAlive { } } -#[doc(hidden)] -#[derive(Debug)] -pub enum WriterState { - Done, - Pause, -} - -#[doc(hidden)] -/// Stream writer -pub trait Writer { - /// number of bytes written to the stream - fn written(&self) -> u64; - - #[doc(hidden)] - fn set_date(&mut self); - - #[doc(hidden)] - fn buffer(&mut self) -> &mut BytesMut; - - fn start( - &mut self, req: &Request, resp: &mut HttpResponse, encoding: ContentEncoding, - ) -> io::Result; - - fn write(&mut self, payload: &Binary) -> io::Result; - - fn write_eof(&mut self) -> io::Result; - - fn poll_completed(&mut self, shutdown: bool) -> Poll<(), io::Error>; -} - #[doc(hidden)] /// Low-level io stream operations pub trait IoStream: AsyncRead + AsyncWrite + 'static { - fn shutdown(&mut self, how: Shutdown) -> io::Result<()>; - /// Returns the socket address of the remote peer of this TCP connection. fn peer_addr(&self) -> Option { None @@ -289,54 +199,10 @@ pub trait IoStream: AsyncRead + AsyncWrite + 'static { fn set_linger(&mut self, dur: Option) -> io::Result<()>; fn set_keepalive(&mut self, dur: Option) -> io::Result<()>; - - fn read_available(&mut self, buf: &mut BytesMut) -> Poll<(bool, bool), io::Error> { - let mut read_some = false; - loop { - if buf.remaining_mut() < LW_BUFFER_SIZE { - buf.reserve(HW_BUFFER_SIZE); - } - - let read = unsafe { self.read(buf.bytes_mut()) }; - match read { - Ok(n) => { - if n == 0 { - return Ok(Async::Ready((read_some, true))); - } else { - read_some = true; - unsafe { - buf.advance_mut(n); - } - } - } - Err(e) => { - return if e.kind() == io::ErrorKind::WouldBlock { - if read_some { - Ok(Async::Ready((read_some, false))) - } else { - Ok(Async::NotReady) - } - } else { - Err(e) - }; - } - } - } - } - - /// Extra io stream extensions - fn extensions(&self) -> Option> { - None - } } #[cfg(all(unix, feature = "uds"))] impl IoStream for ::tokio_uds::UnixStream { - #[inline] - fn shutdown(&mut self, how: Shutdown) -> io::Result<()> { - ::tokio_uds::UnixStream::shutdown(self, how) - } - #[inline] fn set_nodelay(&mut self, _nodelay: bool) -> io::Result<()> { Ok(()) @@ -354,11 +220,6 @@ impl IoStream for ::tokio_uds::UnixStream { } impl IoStream for TcpStream { - #[inline] - fn shutdown(&mut self, how: Shutdown) -> io::Result<()> { - TcpStream::shutdown(self, how) - } - #[inline] fn peer_addr(&self) -> Option { TcpStream::peer_addr(self).ok() diff --git a/src/server/output.rs b/src/server/output.rs index 1da7e9025..143ba4029 100644 --- a/src/server/output.rs +++ b/src/server/output.rs @@ -254,7 +254,7 @@ impl Output { } return; } - Body::Streaming(_) | Body::Actor(_) => { + Body::Streaming(_) => { if resp.upgrade() { if version == Version::HTTP_2 { error!("Connection upgrade is forbidden for HTTP/2"); diff --git a/src/server/settings.rs b/src/server/settings.rs index 9df1e457f..f21283590 100644 --- a/src/server/settings.rs +++ b/src/server/settings.rs @@ -15,7 +15,6 @@ use time; use tokio_current_thread::spawn; use tokio_timer::{sleep, Delay}; -use super::channel::Node; use super::message::{Request, RequestPool}; use super::KeepAlive; use body::Body; @@ -128,35 +127,33 @@ impl ServerSettings { const DATE_VALUE_LENGTH: usize = 29; /// Http service configuration -pub struct ServiceConfig(Rc>); +pub struct ServiceConfig(Rc); -struct Inner { - handler: H, +struct Inner { keep_alive: Option, client_timeout: u64, client_shutdown: u64, ka_enabled: bool, bytes: Rc, messages: &'static RequestPool, - node: RefCell>, date: UnsafeCell<(bool, Date)>, } -impl Clone for ServiceConfig { +impl Clone for ServiceConfig { fn clone(&self) -> Self { ServiceConfig(self.0.clone()) } } -impl ServiceConfig { +impl ServiceConfig { /// Create instance of `ServiceConfig` pub(crate) fn new( - handler: H, keep_alive: KeepAlive, client_timeout: u64, client_shutdown: u64, + keep_alive: KeepAlive, client_timeout: u64, client_shutdown: u64, settings: ServerSettings, - ) -> ServiceConfig { + ) -> ServiceConfig { let (keep_alive, ka_enabled) = match keep_alive { KeepAlive::Timeout(val) => (val as u64, true), - KeepAlive::Os | KeepAlive::Tcp(_) => (0, true), + KeepAlive::Os => (0, true), KeepAlive::Disabled => (0, false), }; let keep_alive = if ka_enabled && keep_alive > 0 { @@ -166,29 +163,19 @@ impl ServiceConfig { }; ServiceConfig(Rc::new(Inner { - handler, keep_alive, ka_enabled, client_timeout, client_shutdown, bytes: Rc::new(SharedBytesPool::new()), messages: RequestPool::pool(settings), - node: RefCell::new(Node::head()), date: UnsafeCell::new((false, Date::new())), })) } /// Create worker settings builder. - pub fn build(handler: H) -> ServiceConfigBuilder { - ServiceConfigBuilder::new(handler) - } - - pub(crate) fn head(&self) -> RefMut> { - self.0.node.borrow_mut() - } - - pub(crate) fn handler(&self) -> &H { - &self.0.handler + pub fn build() -> ServiceConfigBuilder { + ServiceConfigBuilder::new() } #[inline] @@ -226,7 +213,7 @@ impl ServiceConfig { } } -impl ServiceConfig { +impl ServiceConfig { #[inline] /// Client timeout for first request. pub fn client_timer(&self) -> Option { @@ -329,8 +316,7 @@ impl ServiceConfig { /// /// This type can be used to construct an instance of `ServiceConfig` through a /// builder-like pattern. -pub struct ServiceConfigBuilder { - handler: H, +pub struct ServiceConfigBuilder { keep_alive: KeepAlive, client_timeout: u64, client_shutdown: u64, @@ -339,11 +325,10 @@ pub struct ServiceConfigBuilder { secure: bool, } -impl ServiceConfigBuilder { +impl ServiceConfigBuilder { /// Create instance of `ServiceConfigBuilder` - pub fn new(handler: H) -> ServiceConfigBuilder { + pub fn new() -> ServiceConfigBuilder { ServiceConfigBuilder { - handler, keep_alive: KeepAlive::Timeout(5), client_timeout: 5000, client_shutdown: 5000, @@ -426,12 +411,11 @@ impl ServiceConfigBuilder { } /// Finish service configuration and create `ServiceConfig` object. - pub fn finish(self) -> ServiceConfig { + pub fn finish(self) -> ServiceConfig { let settings = ServerSettings::new(self.addr, &self.host, self.secure); let client_shutdown = if self.secure { self.client_shutdown } else { 0 }; ServiceConfig::new( - self.handler, self.keep_alive, self.client_timeout, client_shutdown, diff --git a/src/server/ssl/mod.rs b/src/server/ssl/mod.rs deleted file mode 100644 index c09573fe3..000000000 --- a/src/server/ssl/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[cfg(any(feature = "alpn", feature = "ssl"))] -mod openssl; -#[cfg(any(feature = "alpn", feature = "ssl"))] -pub use self::openssl::{openssl_acceptor_with_flags, OpensslAcceptor}; - -#[cfg(feature = "tls")] -mod nativetls; - -#[cfg(feature = "rust-tls")] -mod rustls; -#[cfg(feature = "rust-tls")] -pub use self::rustls::RustlsAcceptor; diff --git a/src/server/ssl/nativetls.rs b/src/server/ssl/nativetls.rs deleted file mode 100644 index a9797ffb3..000000000 --- a/src/server/ssl/nativetls.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::net::{Shutdown, SocketAddr}; -use std::{io, time}; - -use actix_net::ssl::TlsStream; - -use server::IoStream; - -impl IoStream for TlsStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = self.get_mut().shutdown(); - Ok(()) - } - - #[inline] - fn peer_addr(&self) -> Option { - self.get_ref().get_ref().peer_addr() - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().get_mut().set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_keepalive(dur) - } -} diff --git a/src/server/ssl/openssl.rs b/src/server/ssl/openssl.rs deleted file mode 100644 index 9d370f8be..000000000 --- a/src/server/ssl/openssl.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::net::{Shutdown, SocketAddr}; -use std::{io, time}; - -use actix_net::ssl; -use openssl::ssl::{AlpnError, SslAcceptor, SslAcceptorBuilder}; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_openssl::SslStream; - -use server::{IoStream, ServerFlags}; - -/// Support `SSL` connections via openssl package -/// -/// `ssl` feature enables `OpensslAcceptor` type -pub struct OpensslAcceptor { - _t: ssl::OpensslAcceptor, -} - -impl OpensslAcceptor { - /// Create `OpensslAcceptor` with enabled `HTTP/2` and `HTTP1.1` support. - pub fn new(builder: SslAcceptorBuilder) -> io::Result> { - OpensslAcceptor::with_flags(builder, ServerFlags::HTTP1 | ServerFlags::HTTP2) - } - - /// Create `OpensslAcceptor` with custom server flags. - pub fn with_flags( - builder: SslAcceptorBuilder, flags: ServerFlags, - ) -> io::Result> { - let acceptor = openssl_acceptor_with_flags(builder, flags)?; - - Ok(ssl::OpensslAcceptor::new(acceptor)) - } -} - -/// Configure `SslAcceptorBuilder` with custom server flags. -pub fn openssl_acceptor_with_flags( - mut builder: SslAcceptorBuilder, flags: ServerFlags, -) -> io::Result { - let mut protos = Vec::new(); - if flags.contains(ServerFlags::HTTP1) { - protos.extend(b"\x08http/1.1"); - } - if flags.contains(ServerFlags::HTTP2) { - protos.extend(b"\x02h2"); - builder.set_alpn_select_callback(|_, protos| { - const H2: &[u8] = b"\x02h2"; - if protos.windows(3).any(|window| window == H2) { - Ok(b"h2") - } else { - Err(AlpnError::NOACK) - } - }); - } - - if !protos.is_empty() { - builder.set_alpn_protos(&protos)?; - } - - Ok(builder.build()) -} - -impl IoStream for SslStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = self.get_mut().shutdown(); - Ok(()) - } - - #[inline] - fn peer_addr(&self) -> Option { - self.get_ref().get_ref().peer_addr() - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().get_mut().set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_keepalive(dur) - } -} diff --git a/src/server/ssl/rustls.rs b/src/server/ssl/rustls.rs deleted file mode 100644 index a53a53a98..000000000 --- a/src/server/ssl/rustls.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::net::{Shutdown, SocketAddr}; -use std::{io, time}; - -use actix_net::ssl; //::RustlsAcceptor; -use rustls::{ClientSession, ServerConfig, ServerSession}; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_rustls::TlsStream; - -use server::{IoStream, ServerFlags}; - -/// Support `SSL` connections via rustls package -/// -/// `rust-tls` feature enables `RustlsAcceptor` type -pub struct RustlsAcceptor { - _t: ssl::RustlsAcceptor, -} - -impl RustlsAcceptor { - /// Create `RustlsAcceptor` with custom server flags. - pub fn with_flags( - mut config: ServerConfig, flags: ServerFlags, - ) -> ssl::RustlsAcceptor { - let mut protos = Vec::new(); - if flags.contains(ServerFlags::HTTP2) { - protos.push("h2".to_string()); - } - if flags.contains(ServerFlags::HTTP1) { - protos.push("http/1.1".to_string()); - } - if !protos.is_empty() { - config.set_protocols(&protos); - } - - ssl::RustlsAcceptor::new(config) - } -} - -impl IoStream for TlsStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = ::shutdown(self); - Ok(()) - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().0.set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().0.set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().0.set_keepalive(dur) - } -} - -impl IoStream for TlsStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = ::shutdown(self); - Ok(()) - } - - #[inline] - fn peer_addr(&self) -> Option { - self.get_ref().0.peer_addr() - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().0.set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().0.set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().0.set_keepalive(dur) - } -} diff --git a/src/with.rs b/src/with.rs deleted file mode 100644 index c6d54dee8..000000000 --- a/src/with.rs +++ /dev/null @@ -1,383 +0,0 @@ -use futures::{Async, Future, Poll}; -use std::marker::PhantomData; -use std::rc::Rc; - -use error::Error; -use handler::{AsyncResult, AsyncResultItem, FromRequest, Handler, Responder}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; - -trait FnWith: 'static { - fn call_with(self: &Self, T) -> R; -} - -impl R + 'static> FnWith for F { - fn call_with(self: &Self, arg: T) -> R { - (*self)(arg) - } -} - -#[doc(hidden)] -pub trait WithFactory: 'static -where - T: FromRequest, - R: Responder, -{ - fn create(self) -> With; - - fn create_with_config(self, T::Config) -> With; -} - -#[doc(hidden)] -pub trait WithAsyncFactory: 'static -where - T: FromRequest, - R: Future, - I: Responder, - E: Into, -{ - fn create(self) -> WithAsync; - - fn create_with_config(self, T::Config) -> WithAsync; -} - -#[doc(hidden)] -pub struct With -where - T: FromRequest, - S: 'static, -{ - hnd: Rc>, - cfg: Rc, - _s: PhantomData, -} - -impl With -where - T: FromRequest, - S: 'static, -{ - pub fn new R + 'static>(f: F, cfg: T::Config) -> Self { - With { - cfg: Rc::new(cfg), - hnd: Rc::new(f), - _s: PhantomData, - } - } -} - -impl Handler for With -where - R: Responder + 'static, - T: FromRequest + 'static, - S: 'static, -{ - type Result = AsyncResult; - - fn handle(&self, req: &HttpRequest) -> Self::Result { - let mut fut = WithHandlerFut { - req: req.clone(), - started: false, - hnd: Rc::clone(&self.hnd), - cfg: self.cfg.clone(), - fut1: None, - fut2: None, - }; - - match fut.poll() { - Ok(Async::Ready(resp)) => AsyncResult::ok(resp), - Ok(Async::NotReady) => AsyncResult::async(Box::new(fut)), - Err(e) => AsyncResult::err(e), - } - } -} - -struct WithHandlerFut -where - R: Responder, - T: FromRequest + 'static, - S: 'static, -{ - started: bool, - hnd: Rc>, - cfg: Rc, - req: HttpRequest, - fut1: Option>>, - fut2: Option>>, -} - -impl Future for WithHandlerFut -where - R: Responder + 'static, - T: FromRequest + 'static, - S: 'static, -{ - type Item = HttpResponse; - type Error = Error; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut2 { - return fut.poll(); - } - - let item = if !self.started { - self.started = true; - let reply = T::from_request(&self.req, self.cfg.as_ref()).into(); - match reply.into() { - AsyncResultItem::Err(err) => return Err(err), - AsyncResultItem::Ok(msg) => msg, - AsyncResultItem::Future(fut) => { - self.fut1 = Some(fut); - return self.poll(); - } - } - } else { - match self.fut1.as_mut().unwrap().poll()? { - Async::Ready(item) => item, - Async::NotReady => return Ok(Async::NotReady), - } - }; - - let item = match self.hnd.as_ref().call_with(item).respond_to(&self.req) { - Ok(item) => item.into(), - Err(e) => return Err(e.into()), - }; - - match item.into() { - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Ok(resp) => Ok(Async::Ready(resp)), - AsyncResultItem::Future(fut) => { - self.fut2 = Some(fut); - self.poll() - } - } - } -} - -#[doc(hidden)] -pub struct WithAsync -where - R: Future, - I: Responder, - E: Into, - T: FromRequest, - S: 'static, -{ - hnd: Rc>, - cfg: Rc, - _s: PhantomData, -} - -impl WithAsync -where - R: Future, - I: Responder, - E: Into, - T: FromRequest, - S: 'static, -{ - pub fn new R + 'static>(f: F, cfg: T::Config) -> Self { - WithAsync { - cfg: Rc::new(cfg), - hnd: Rc::new(f), - _s: PhantomData, - } - } -} - -impl Handler for WithAsync -where - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, - S: 'static, -{ - type Result = AsyncResult; - - fn handle(&self, req: &HttpRequest) -> Self::Result { - let mut fut = WithAsyncHandlerFut { - req: req.clone(), - started: false, - hnd: Rc::clone(&self.hnd), - cfg: Rc::clone(&self.cfg), - fut1: None, - fut2: None, - fut3: None, - }; - - match fut.poll() { - Ok(Async::Ready(resp)) => AsyncResult::ok(resp), - Ok(Async::NotReady) => AsyncResult::async(Box::new(fut)), - Err(e) => AsyncResult::err(e), - } - } -} - -struct WithAsyncHandlerFut -where - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, - S: 'static, -{ - started: bool, - hnd: Rc>, - cfg: Rc, - req: HttpRequest, - fut1: Option>>, - fut2: Option, - fut3: Option>>, -} - -impl Future for WithAsyncHandlerFut -where - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, - S: 'static, -{ - type Item = HttpResponse; - type Error = Error; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut3 { - return fut.poll(); - } - - if self.fut2.is_some() { - return match self.fut2.as_mut().unwrap().poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(r)) => match r.respond_to(&self.req) { - Ok(r) => match r.into().into() { - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Ok(resp) => Ok(Async::Ready(resp)), - AsyncResultItem::Future(fut) => { - self.fut3 = Some(fut); - self.poll() - } - }, - Err(e) => Err(e.into()), - }, - Err(e) => Err(e.into()), - }; - } - - let item = if !self.started { - self.started = true; - let reply = T::from_request(&self.req, self.cfg.as_ref()).into(); - match reply.into() { - AsyncResultItem::Err(err) => return Err(err), - AsyncResultItem::Ok(msg) => msg, - AsyncResultItem::Future(fut) => { - self.fut1 = Some(fut); - return self.poll(); - } - } - } else { - match self.fut1.as_mut().unwrap().poll()? { - Async::Ready(item) => item, - Async::NotReady => return Ok(Async::NotReady), - } - }; - - self.fut2 = Some(self.hnd.as_ref().call_with(item)); - self.poll() - } -} - -macro_rules! with_factory_tuple ({$(($n:tt, $T:ident)),+} => { - impl<$($T,)+ State, Func, Res> WithFactory<($($T,)+), State, Res> for Func - where Func: Fn($($T,)+) -> Res + 'static, - $($T: FromRequest + 'static,)+ - Res: Responder + 'static, - State: 'static, - { - fn create(self) -> With<($($T,)+), State, Res> { - With::new(move |($($n,)+)| (self)($($n,)+), ($($T::Config::default(),)+)) - } - - fn create_with_config(self, cfg: ($($T::Config,)+)) -> With<($($T,)+), State, Res> { - With::new(move |($($n,)+)| (self)($($n,)+), cfg) - } - } -}); - -macro_rules! with_async_factory_tuple ({$(($n:tt, $T:ident)),+} => { - impl<$($T,)+ State, Func, Res, Item, Err> WithAsyncFactory<($($T,)+), State, Res, Item, Err> for Func - where Func: Fn($($T,)+) -> Res + 'static, - $($T: FromRequest + 'static,)+ - Res: Future, - Item: Responder + 'static, - Err: Into, - State: 'static, - { - fn create(self) -> WithAsync<($($T,)+), State, Res, Item, Err> { - WithAsync::new(move |($($n,)+)| (self)($($n,)+), ($($T::Config::default(),)+)) - } - - fn create_with_config(self, cfg: ($($T::Config,)+)) -> WithAsync<($($T,)+), State, Res, Item, Err> { - WithAsync::new(move |($($n,)+)| (self)($($n,)+), cfg) - } - } -}); - -with_factory_tuple!((a, A)); -with_factory_tuple!((a, A), (b, B)); -with_factory_tuple!((a, A), (b, B), (c, C)); -with_factory_tuple!((a, A), (b, B), (c, C), (d, D)); -with_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E)); -with_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E), (f, F)); -with_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E), (f, F), (g, G)); -with_factory_tuple!( - (a, A), - (b, B), - (c, C), - (d, D), - (e, E), - (f, F), - (g, G), - (h, H) -); -with_factory_tuple!( - (a, A), - (b, B), - (c, C), - (d, D), - (e, E), - (f, F), - (g, G), - (h, H), - (i, I) -); - -with_async_factory_tuple!((a, A)); -with_async_factory_tuple!((a, A), (b, B)); -with_async_factory_tuple!((a, A), (b, B), (c, C)); -with_async_factory_tuple!((a, A), (b, B), (c, C), (d, D)); -with_async_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E)); -with_async_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E), (f, F)); -with_async_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E), (f, F), (g, G)); -with_async_factory_tuple!( - (a, A), - (b, B), - (c, C), - (d, D), - (e, E), - (f, F), - (g, G), - (h, H) -); -with_async_factory_tuple!( - (a, A), - (b, B), - (c, C), - (d, D), - (e, E), - (f, F), - (g, G), - (h, H), - (i, I) -); diff --git a/tests/test_client.rs b/tests/test_client.rs deleted file mode 100644 index 8c5d5819d..000000000 --- a/tests/test_client.rs +++ /dev/null @@ -1,508 +0,0 @@ -#![allow(deprecated)] -extern crate actix; -extern crate actix_web; -extern crate bytes; -extern crate flate2; -extern crate futures; -extern crate rand; -#[cfg(all(unix, feature = "uds"))] -extern crate tokio_uds; - -use std::io::{Read, Write}; -use std::{net, thread}; - -use bytes::Bytes; -use flate2::read::GzDecoder; -use futures::stream::once; -use futures::Future; -use rand::Rng; - -use actix_web::*; - -const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World"; - -#[test] -fn test_simple() { - let mut srv = - test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); - - let request = srv.get().header("x-test", "111").finish().unwrap(); - let repr = format!("{:?}", request); - assert!(repr.contains("ClientRequest")); - assert!(repr.contains("x-test")); - - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); - - let request = srv.post().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_connection_close() { - let mut srv = - test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); - - let request = srv.get().header("Connection", "close").finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_with_query_parameter() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| match req.query().get("qp") { - Some(_) => HttpResponse::Ok().finish(), - None => HttpResponse::BadRequest().finish(), - }) - }); - - let request = srv.get().uri(srv.url("/?qp=5").as_str()).finish().unwrap(); - - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_no_decompress() { - let mut srv = - test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); - - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - - let mut e = GzDecoder::new(&bytes[..]); - let mut dec = Vec::new(); - e.read_to_end(&mut dec).unwrap(); - assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); - - // POST - let request = srv.post().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - - let bytes = srv.execute(response.body()).unwrap(); - let mut e = GzDecoder::new(&bytes[..]); - let mut dec = Vec::new(); - e.read_to_end(&mut dec).unwrap(); - assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_client_gzip_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Deflate) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Gzip) - .body(STR) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_client_gzip_encoding_large() { - let data = STR.repeat(10); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Deflate) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Gzip) - .body(data.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_client_gzip_encoding_large_random() { - let data = rand::thread_rng() - .gen_ascii_chars() - .take(100_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Deflate) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Gzip) - .body(data.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(all(unix, feature = "uds"))] -#[test] -fn test_compatible_with_unix_socket_stream() { - let (stream, _) = tokio_uds::UnixStream::pair().unwrap(); - let _ = client::Connection::from_stream(stream); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_client_brotli_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .client(http::Method::POST, "/") - .content_encoding(http::ContentEncoding::Br) - .body(STR) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_client_brotli_encoding_large_random() { - let data = rand::thread_rng() - .gen_ascii_chars() - .take(70_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(move |bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .client(http::Method::POST, "/") - .content_encoding(http::ContentEncoding::Br) - .body(data.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_client_deflate_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Br) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Deflate) - .body(STR) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_client_deflate_encoding_large_random() { - let data = rand::thread_rng() - .gen_ascii_chars() - .take(70_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Br) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Deflate) - .body(data.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_client_streaming_explicit() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .map_err(Error::from) - .and_then(|body| { - Ok(HttpResponse::Ok() - .chunked() - .content_encoding(http::ContentEncoding::Identity) - .body(body)) - }).responder() - }) - }); - - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - - let request = srv.get().body(Body::Streaming(Box::new(body))).unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_body_streaming_implicit() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(Body::Streaming(Box::new(body))) - }) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_client_cookie_handling() { - use actix_web::http::Cookie; - fn err() -> Error { - use std::io::{Error as IoError, ErrorKind}; - // stub some generic error - Error::from(IoError::from(ErrorKind::NotFound)) - } - let cookie1 = Cookie::build("cookie1", "value1").finish(); - let cookie2 = Cookie::build("cookie2", "value2") - .domain("www.example.org") - .path("/") - .secure(true) - .http_only(true) - .finish(); - // Q: are all these clones really necessary? A: Yes, possibly - let cookie1b = cookie1.clone(); - let cookie2b = cookie2.clone(); - let mut srv = test::TestServer::new(move |app| { - let cookie1 = cookie1b.clone(); - let cookie2 = cookie2b.clone(); - app.handler(move |req: &HttpRequest| { - // Check cookies were sent correctly - req.cookie("cookie1") - .ok_or_else(err) - .and_then(|c1| { - if c1.value() == "value1" { - Ok(()) - } else { - Err(err()) - } - }).and_then(|()| req.cookie("cookie2").ok_or_else(err)) - .and_then(|c2| { - if c2.value() == "value2" { - Ok(()) - } else { - Err(err()) - } - }) - // Send some cookies back - .map(|_| { - HttpResponse::Ok() - .cookie(cookie1.clone()) - .cookie(cookie2.clone()) - .finish() - }) - }) - }); - - let request = srv - .get() - .cookie(cookie1.clone()) - .cookie(cookie2.clone()) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - let c1 = response.cookie("cookie1").expect("Missing cookie1"); - assert_eq!(c1, cookie1); - let c2 = response.cookie("cookie2").expect("Missing cookie2"); - assert_eq!(c2, cookie2); -} - -#[test] -fn test_default_headers() { - let srv = test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); - - let request = srv.get().finish().unwrap(); - let repr = format!("{:?}", request); - assert!(repr.contains("\"accept-encoding\": \"gzip, deflate\"")); - assert!(repr.contains(concat!( - "\"user-agent\": \"actix-web/", - env!("CARGO_PKG_VERSION"), - "\"" - ))); - - let request_override = srv - .get() - .header("User-Agent", "test") - .header("Accept-Encoding", "over_test") - .finish() - .unwrap(); - let repr_override = format!("{:?}", request_override); - assert!(repr_override.contains("\"user-agent\": \"test\"")); - assert!(repr_override.contains("\"accept-encoding\": \"over_test\"")); - assert!(!repr_override.contains("\"accept-encoding\": \"gzip, deflate\"")); - assert!(!repr_override.contains(concat!( - "\"user-agent\": \"Actix-web/", - env!("CARGO_PKG_VERSION"), - "\"" - ))); -} - -#[test] -fn client_read_until_eof() { - let addr = test::TestServer::unused_addr(); - - thread::spawn(move || { - let lst = net::TcpListener::bind(addr).unwrap(); - - for stream in lst.incoming() { - let mut stream = stream.unwrap(); - let mut b = [0; 1000]; - let _ = stream.read(&mut b).unwrap(); - let _ = stream - .write_all(b"HTTP/1.1 200 OK\r\nconnection: close\r\n\r\nwelcome!"); - } - }); - - let mut sys = actix::System::new("test"); - - // client request - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = sys.block_on(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"welcome!")); -} diff --git a/tests/test_custom_pipeline.rs b/tests/test_custom_pipeline.rs deleted file mode 100644 index 6b5df00e3..000000000 --- a/tests/test_custom_pipeline.rs +++ /dev/null @@ -1,81 +0,0 @@ -extern crate actix; -extern crate actix_net; -extern crate actix_web; - -use std::{thread, time}; - -use actix::System; -use actix_net::server::Server; -use actix_net::service::NewServiceExt; -use actix_web::server::{HttpService, KeepAlive, ServiceConfig, StreamConfiguration}; -use actix_web::{client, http, test, App, HttpRequest}; - -#[test] -fn test_custom_pipeline() { - let addr = test::TestServer::unused_addr(); - - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - let app = App::new() - .route("/", http::Method::GET, |_: HttpRequest| "OK") - .finish(); - let settings = ServiceConfig::build(app) - .keep_alive(KeepAlive::Disabled) - .client_timeout(1000) - .client_shutdown(1000) - .server_hostname("localhost") - .server_address(addr) - .finish(); - - StreamConfiguration::new() - .nodelay(true) - .tcp_keepalive(Some(time::Duration::from_secs(10))) - .and_then(HttpService::new(settings)) - }).unwrap() - .run(); - }); - - let mut sys = System::new("test"); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } -} - -#[test] -fn test_h1() { - use actix_web::server::H1Service; - - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - let app = App::new() - .route("/", http::Method::GET, |_: HttpRequest| "OK") - .finish(); - let settings = ServiceConfig::build(app) - .keep_alive(KeepAlive::Disabled) - .client_timeout(1000) - .client_shutdown(1000) - .server_hostname("localhost") - .server_address(addr) - .finish(); - - H1Service::new(settings) - }).unwrap() - .run(); - }); - - let mut sys = System::new("test"); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } -} diff --git a/tests/test_h1v2.rs b/tests/test_h1v2.rs index 7e8e9a42c..77b6d202f 100644 --- a/tests/test_h1v2.rs +++ b/tests/test_h1v2.rs @@ -1,4 +1,5 @@ extern crate actix; +extern crate actix_http; extern crate actix_net; extern crate actix_web; extern crate futures; @@ -8,12 +9,12 @@ use std::thread; use actix::System; use actix_net::server::Server; use actix_net::service::{IntoNewService, IntoService}; +use actix_web::{client, test}; use futures::future; -use actix_web::server::h1disp::Http1Dispatcher; -use actix_web::server::KeepAlive; -use actix_web::server::ServiceConfig; -use actix_web::{client, test, App, Error, HttpRequest, HttpResponse}; +use actix_http::server::h1disp::Http1Dispatcher; +use actix_http::server::{KeepAlive, ServiceConfig}; +use actix_http::{Error, HttpResponse}; #[test] fn test_h1_v2() { @@ -21,10 +22,7 @@ fn test_h1_v2() { thread::spawn(move || { Server::new() .bind("test", addr, move || { - let app = App::new() - .route("/", http::Method::GET, |_: HttpRequest| "OK") - .finish(); - let settings = ServiceConfig::build(app) + let settings = ServiceConfig::build() .keep_alive(KeepAlive::Disabled) .client_timeout(1000) .client_shutdown(1000) diff --git a/tests/test_handlers.rs b/tests/test_handlers.rs deleted file mode 100644 index 3ea709c92..000000000 --- a/tests/test_handlers.rs +++ /dev/null @@ -1,677 +0,0 @@ -extern crate actix; -extern crate actix_web; -extern crate bytes; -extern crate futures; -extern crate h2; -extern crate http; -extern crate tokio_timer; -#[macro_use] -extern crate serde_derive; -extern crate serde_json; - -use std::io; -use std::time::{Duration, Instant}; - -use actix_web::*; -use bytes::Bytes; -use futures::Future; -use http::StatusCode; -use serde_json::Value; -use tokio_timer::Delay; - -#[derive(Deserialize)] -struct PParam { - username: String, -} - -#[test] -fn test_path_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.with(|p: Path| format!("Welcome {}!", p.username)) - }); - }); - - // client request - let request = srv.get().uri(srv.url("/test/index.html")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test!")); -} - -#[test] -fn test_async_handler() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|p: Path| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| Ok(format!("Welcome {}!", p.username))) - .responder() - }) - }); - }); - - // client request - let request = srv.get().uri(srv.url("/test/index.html")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test!")); -} - -#[test] -fn test_query_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/index.html", |r| { - r.with(|p: Query| format!("Welcome {}!", p.username)) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/index.html?username=test")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test!")); - - // client request - let request = srv.get().uri(srv.url("/index.html")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[derive(Deserialize, Debug)] -pub enum ResponseType { - Token, - Code, -} - -#[derive(Debug, Deserialize)] -pub struct AuthRequest { - id: u64, - response_type: ResponseType, -} - -#[test] -fn test_query_enum_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/index.html", |r| { - r.with(|p: Query| format!("{:?}", p.into_inner())) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/index.html?id=64&response_type=Code")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!( - bytes, - Bytes::from_static(b"AuthRequest { id: 64, response_type: Code }") - ); - - let request = srv - .get() - .uri(srv.url("/index.html?id=64&response_type=Co")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - - let request = srv - .get() - .uri(srv.url("/index.html?response_type=Code")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_async_extractor_async() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|data: Json| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| Ok(format!("{}", data.0))) - .responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"{\"test\":1}")); -} - -#[derive(Deserialize, Serialize)] -struct FormData { - username: String, -} - -#[test] -fn test_form_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route() - .with(|form: Form| format!("{}", form.username)) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .form(FormData { - username: "test".to_string(), - }).unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"test")); -} - -#[test] -fn test_form_extractor2() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with_config( - |form: Form| format!("{}", form.username), - |cfg| { - cfg.0.error_handler(|err, _| { - error::InternalError::from_response( - err, - HttpResponse::Conflict().finish(), - ).into() - }); - }, - ); - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .header("content-type", "application/x-www-form-urlencoded") - .body("918237129hdk:D:D:D:D:D:DjASHDKJhaswkjeq") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_client_error()); -} - -#[test] -fn test_path_and_query_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|(p, q): (Path, Query)| { - format!("Welcome {} - {}!", p.username, q.username) - }) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html?username=test2")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - test2!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_path_and_query_extractor2() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route() - .with(|(_r, p, q): (HttpRequest, Path, Query)| { - format!("Welcome {} - {}!", p.username, q.username) - }) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html?username=test2")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - test2!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_path_and_query_extractor2_async() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with( - |(p, _q, data): (Path, Query, Json)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }, - ) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); -} - -#[test] -fn test_path_and_query_extractor3_async() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|(p, data): (Path, Json)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_path_and_query_extractor4_async() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|(data, p): (Json, Path)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_path_and_query_extractor2_async2() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with( - |(p, data, _q): (Path, Json, Query)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }, - ) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_path_and_query_extractor2_async3() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route() - .with(|data: Json, p: Path, _: Query| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_path_and_query_extractor2_async4() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route() - .with(|data: (Json, Path, Query)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", data.1.username, (data.0).0)) - }).responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_scope_and_path_extractor() { - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/sc", |scope| { - scope.resource("/{num}/index.html", |r| { - r.route() - .with(|p: Path<(usize,)>| format!("Welcome {}!", p.0)) - }) - }) - }); - - // client request - let request = srv - .get() - .uri(srv.url("/sc/10/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome 10!")); - - // client request - let request = srv - .get() - .uri(srv.url("/sc/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); -} - -#[test] -fn test_nested_scope_and_path_extractor() { - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/sc", |scope| { - scope.nested("/{num}", |scope| { - scope.resource("/{num}/index.html", |r| { - r.route().with(|p: Path<(usize, usize)>| { - format!("Welcome {} {}!", p.0, p.1) - }) - }) - }) - }) - }); - - // client request - let request = srv - .get() - .uri(srv.url("/sc/10/12/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome 10 12!")); - - // client request - let request = srv - .get() - .uri(srv.url("/sc/10/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); -} - -#[cfg(actix_impl_trait)] -fn test_impl_trait( - data: (Json, Path, Query), -) -> impl Future { - Delay::new(Instant::now() + Duration::from_millis(10)) - .map_err(|_| io::Error::new(io::ErrorKind::Other, "timeout")) - .and_then(move |_| Ok(format!("Welcome {} - {}!", data.1.username, (data.0).0))) -} - -#[cfg(actix_impl_trait)] -fn test_impl_trait_err( - _data: (Json, Path, Query), -) -> impl Future { - Delay::new(Instant::now() + Duration::from_millis(10)) - .map_err(|_| io::Error::new(io::ErrorKind::Other, "timeout")) - .and_then(move |_| Err(io::Error::new(io::ErrorKind::Other, "other"))) -} - -#[cfg(actix_impl_trait)] -#[test] -fn test_path_and_query_extractor2_async4_impl_trait() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with_async(test_impl_trait) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[cfg(actix_impl_trait)] -#[test] -fn test_path_and_query_extractor2_async4_impl_trait_err() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with_async(test_impl_trait_err) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); -} - -#[test] -fn test_non_ascii_route() { - let mut srv = test::TestServer::new(|app| { - app.resource("/中文/index.html", |r| r.f(|_| "success")); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/中文/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"success")); -} - -#[test] -fn test_unsafe_path_route() { - let mut srv = test::TestServer::new(|app| { - app.resource("/test/{url}", |r| { - r.f(|r| format!("success: {}", &r.match_info()["url"])) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/test/http%3A%2F%2Fexample.com")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!( - bytes, - Bytes::from_static(b"success: http:%2F%2Fexample.com") - ); -} diff --git a/tests/test_middleware.rs b/tests/test_middleware.rs deleted file mode 100644 index 6cb6ee363..000000000 --- a/tests/test_middleware.rs +++ /dev/null @@ -1,1055 +0,0 @@ -extern crate actix; -extern crate actix_web; -extern crate futures; -extern crate tokio_timer; - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::thread; -use std::time::{Duration, Instant}; - -use actix_web::error::{Error, ErrorInternalServerError}; -use actix_web::*; -use futures::{future, Future}; -use tokio_timer::Delay; - -struct MiddlewareTest { - start: Arc, - response: Arc, - finish: Arc, -} - -impl middleware::Middleware for MiddlewareTest { - fn start(&self, _: &HttpRequest) -> Result { - self.start - .store(self.start.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - Ok(middleware::Started::Done) - } - - fn response( - &self, _: &HttpRequest, resp: HttpResponse, - ) -> Result { - self.response - .store(self.response.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - Ok(middleware::Response::Done(resp)) - } - - fn finish(&self, _: &HttpRequest, _: &HttpResponse) -> middleware::Finished { - self.finish - .store(self.finish.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - middleware::Finished::Done - } -} - -#[test] -fn test_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_resource_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_scope_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_middleware_async_handler() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new() - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/", |r| { - r.route().a(|_| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(|_| Ok(HttpResponse::Ok())) - }) - }) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_async_handler() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", |r| { - r.middleware(mw); - r.route().a(|_| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(|_| Ok(HttpResponse::Ok())) - }) - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_async_handler() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| { - r.route().a(|_| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(|_| Ok(HttpResponse::Ok())) - }) - }) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -fn index_test_middleware_async_error(_: &HttpRequest) -> FutureResponse { - future::result(Err(error::ErrorBadRequest("TEST"))).responder() -} - -#[test] -fn test_middleware_async_error() { - let req = Arc::new(AtomicUsize::new(0)); - let resp = Arc::new(AtomicUsize::new(0)); - let fin = Arc::new(AtomicUsize::new(0)); - - let act_req = Arc::clone(&req); - let act_resp = Arc::clone(&resp); - let act_fin = Arc::clone(&fin); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_req), - response: Arc::clone(&act_resp), - finish: Arc::clone(&act_fin), - }).handler(index_test_middleware_async_error) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), http::StatusCode::BAD_REQUEST); - - assert_eq!(req.load(Ordering::Relaxed), 1); - assert_eq!(resp.load(Ordering::Relaxed), 1); - assert_eq!(fin.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_async_error() { - let req = Arc::new(AtomicUsize::new(0)); - let resp = Arc::new(AtomicUsize::new(0)); - let fin = Arc::new(AtomicUsize::new(0)); - - let act_req = Arc::clone(&req); - let act_resp = Arc::clone(&resp); - let act_fin = Arc::clone(&fin); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_req), - response: Arc::clone(&act_resp), - finish: Arc::clone(&act_fin), - }).resource("/test", |r| r.f(index_test_middleware_async_error)) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), http::StatusCode::BAD_REQUEST); - - assert_eq!(req.load(Ordering::Relaxed), 1); - assert_eq!(resp.load(Ordering::Relaxed), 1); - assert_eq!(fin.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_async_error() { - let req = Arc::new(AtomicUsize::new(0)); - let resp = Arc::new(AtomicUsize::new(0)); - let fin = Arc::new(AtomicUsize::new(0)); - - let act_req = Arc::clone(&req); - let act_resp = Arc::clone(&resp); - let act_fin = Arc::clone(&fin); - - let mut srv = test::TestServer::with_factory(move || { - let mw = MiddlewareAsyncTest { - start: Arc::clone(&act_req), - response: Arc::clone(&act_resp), - finish: Arc::clone(&act_fin), - }; - - App::new().resource("/test", move |r| { - r.middleware(mw); - r.f(index_test_middleware_async_error); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), http::StatusCode::BAD_REQUEST); - - assert_eq!(req.load(Ordering::Relaxed), 1); - assert_eq!(resp.load(Ordering::Relaxed), 1); - assert_eq!(fin.load(Ordering::Relaxed), 1); -} - -struct MiddlewareAsyncTest { - start: Arc, - response: Arc, - finish: Arc, -} - -impl middleware::Middleware for MiddlewareAsyncTest { - fn start(&self, _: &HttpRequest) -> Result { - let to = Delay::new(Instant::now() + Duration::from_millis(10)); - - let start = Arc::clone(&self.start); - Ok(middleware::Started::Future(Box::new( - to.from_err().and_then(move |_| { - start.fetch_add(1, Ordering::Relaxed); - Ok(None) - }), - ))) - } - - fn response( - &self, _: &HttpRequest, resp: HttpResponse, - ) -> Result { - let to = Delay::new(Instant::now() + Duration::from_millis(10)); - - let response = Arc::clone(&self.response); - Ok(middleware::Response::Future(Box::new( - to.from_err().and_then(move |_| { - response.fetch_add(1, Ordering::Relaxed); - Ok(resp) - }), - ))) - } - - fn finish(&self, _: &HttpRequest, _: &HttpResponse) -> middleware::Finished { - let to = Delay::new(Instant::now() + Duration::from_millis(10)); - - let finish = Arc::clone(&self.finish); - middleware::Finished::Future(Box::new(to.from_err().and_then(move |_| { - finish.fetch_add(1, Ordering::Relaxed); - Ok(()) - }))) - } -} - -#[test] -fn test_async_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_async_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new() - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(50)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_sync_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new() - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(50)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_scope_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_async_scope_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_async_scope_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_resource_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - - thread::sleep(Duration::from_millis(40)); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_async_resource_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - let mw2 = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw1); - r.middleware(mw2); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(40)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_sync_resource_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - let mw2 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw1); - r.middleware(mw2); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(40)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -struct MiddlewareWithErr; - -impl middleware::Middleware for MiddlewareWithErr { - fn start(&self, _: &HttpRequest) -> Result { - Err(ErrorInternalServerError("middleware error")) - } -} - -struct MiddlewareAsyncWithErr; - -impl middleware::Middleware for MiddlewareAsyncWithErr { - fn start(&self, _: &HttpRequest) -> Result { - Ok(middleware::Started::Future(Box::new(future::err( - ErrorInternalServerError("middleware error"), - )))) - } -} - -#[test] -fn test_middleware_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new() - .middleware(mw1) - .middleware(MiddlewareWithErr) - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_middleware_async_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new() - .middleware(mw1) - .middleware(MiddlewareAsyncWithErr) - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().scope("/scope", |scope| { - scope - .middleware(mw1) - .middleware(MiddlewareWithErr) - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_async_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().scope("/scope", |scope| { - scope - .middleware(mw1) - .middleware(MiddlewareAsyncWithErr) - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw1); - r.middleware(MiddlewareWithErr); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_async_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw1); - r.middleware(MiddlewareAsyncWithErr); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[cfg(feature = "session")] -#[test] -fn test_session_storage_middleware() { - use actix_web::middleware::session::{ - CookieSessionBackend, RequestSession, SessionStorage, - }; - - const SIMPLE_NAME: &'static str = "simple"; - const SIMPLE_PAYLOAD: &'static str = "kantan"; - const COMPLEX_NAME: &'static str = "test"; - const COMPLEX_PAYLOAD: &'static str = "url=https://test.com&generate_204"; - //TODO: investigate how to handle below input - //const COMPLEX_PAYLOAD: &'static str = "FJc%26continue_url%3Dhttp%253A%252F%252Fconnectivitycheck.gstatic.com%252Fgenerate_204"; - - let mut srv = test::TestServer::with_factory(move || { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/index", move |r| { - r.f(|req| { - let res = req.session().set(COMPLEX_NAME, COMPLEX_PAYLOAD); - assert!(res.is_ok()); - let value = req.session().get::(COMPLEX_NAME); - assert!(value.is_ok()); - let value = value.unwrap(); - assert!(value.is_some()); - assert_eq!(value.unwrap(), COMPLEX_PAYLOAD); - - let res = req.session().set(SIMPLE_NAME, SIMPLE_PAYLOAD); - assert!(res.is_ok()); - let value = req.session().get::(SIMPLE_NAME); - assert!(value.is_ok()); - let value = value.unwrap(); - assert!(value.is_some()); - assert_eq!(value.unwrap(), SIMPLE_PAYLOAD); - - HttpResponse::Ok() - }) - }).resource("/expect_cookie", move |r| { - r.f(|req| { - let _cookies = req.cookies().expect("To get cookies"); - - let value = req.session().get::(SIMPLE_NAME); - assert!(value.is_ok()); - let value = value.unwrap(); - assert!(value.is_some()); - assert_eq!(value.unwrap(), SIMPLE_PAYLOAD); - - let value = req.session().get::(COMPLEX_NAME); - assert!(value.is_ok()); - let value = value.unwrap(); - assert!(value.is_some()); - assert_eq!(value.unwrap(), COMPLEX_PAYLOAD); - - HttpResponse::Ok() - }) - }) - }); - - let request = srv.get().uri(srv.url("/index")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - - assert!(response.headers().contains_key("set-cookie")); - let set_cookie = response.headers().get("set-cookie"); - assert!(set_cookie.is_some()); - let set_cookie = set_cookie.unwrap().to_str().expect("Convert to str"); - - let request = srv - .get() - .uri(srv.url("/expect_cookie")) - .header("cookie", set_cookie.split(';').next().unwrap()) - .finish() - .unwrap(); - - srv.execute(request.send()).unwrap(); -} diff --git a/tests/test_server.rs b/tests/test_server.rs deleted file mode 100644 index 477d3e64b..000000000 --- a/tests/test_server.rs +++ /dev/null @@ -1,1357 +0,0 @@ -extern crate actix; -extern crate actix_net; -extern crate actix_web; -#[cfg(feature = "brotli")] -extern crate brotli2; -extern crate bytes; -extern crate flate2; -extern crate futures; -extern crate h2; -extern crate http as modhttp; -extern crate rand; -extern crate tokio; -extern crate tokio_current_thread; -extern crate tokio_current_thread as current_thread; -extern crate tokio_reactor; -extern crate tokio_tcp; - -#[cfg(feature = "tls")] -extern crate native_tls; -#[cfg(feature = "ssl")] -extern crate openssl; -#[cfg(feature = "rust-tls")] -extern crate rustls; - -use std::io::{Read, Write}; -use std::sync::Arc; -use std::{thread, time}; - -#[cfg(feature = "brotli")] -use brotli2::write::{BrotliDecoder, BrotliEncoder}; -use bytes::{Bytes, BytesMut}; -use flate2::read::GzDecoder; -use flate2::write::{GzEncoder, ZlibDecoder, ZlibEncoder}; -use flate2::Compression; -use futures::stream::once; -use futures::{Future, Stream}; -use h2::client as h2client; -use modhttp::Request; -use rand::distributions::Alphanumeric; -use rand::Rng; -use tokio::runtime::current_thread::Runtime; -use tokio_current_thread::spawn; -use tokio_tcp::TcpStream; - -use actix_web::*; - -const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World"; - -#[test] -#[cfg(unix)] -fn test_start() { - use actix::System; - use std::sync::mpsc; - - let _ = test::TestServer::unused_addr(); - let (tx, rx) = mpsc::channel(); - - thread::spawn(|| { - System::run(move || { - let srv = server::new(|| { - vec![App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - })] - }); - - let srv = srv.bind("127.0.0.1:0").unwrap(); - let addr = srv.addrs()[0]; - let srv_addr = srv.start(); - let _ = tx.send((addr, srv_addr, System::current())); - }); - }); - let (addr, srv_addr, sys) = rx.recv().unwrap(); - System::set_current(sys.clone()); - - let mut rt = Runtime::new().unwrap(); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } - - // pause - let _ = srv_addr.send(server::PauseServer).wait(); - thread::sleep(time::Duration::from_millis(200)); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .timeout(time::Duration::from_millis(200)) - .finish() - .unwrap(); - assert!(rt.block_on(req.send()).is_err()); - } - - // resume - let _ = srv_addr.send(server::ResumeServer).wait(); - thread::sleep(time::Duration::from_millis(200)); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } - - let _ = sys.stop(); -} - -#[test] -#[cfg(unix)] -fn test_shutdown() { - use actix::System; - use std::net; - use std::sync::mpsc; - - let _ = test::TestServer::unused_addr(); - let (tx, rx) = mpsc::channel(); - - thread::spawn(|| { - System::run(move || { - let srv = server::new(|| { - vec![App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - })] - }); - - let srv = srv.bind("127.0.0.1:0").unwrap(); - let addr = srv.addrs()[0]; - let srv_addr = srv.shutdown_timeout(1).start(); - let _ = tx.send((addr, srv_addr, System::current())); - }); - }); - let (addr, srv_addr, sys) = rx.recv().unwrap(); - System::set_current(sys.clone()); - - let mut rt = Runtime::new().unwrap(); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()).unwrap(); - srv_addr.do_send(server::StopServer { graceful: true }); - assert!(response.status().is_success()); - } - - thread::sleep(time::Duration::from_millis(1000)); - assert!(net::TcpStream::connect(addr).is_err()); - - let _ = sys.stop(); -} - -#[test] -#[cfg(unix)] -fn test_panic() { - use actix::System; - use std::sync::mpsc; - - let _ = test::TestServer::unused_addr(); - let (tx, rx) = mpsc::channel(); - - thread::spawn(|| { - System::run(move || { - let srv = server::new(|| { - App::new() - .resource("/panic", |r| { - r.method(http::Method::GET).f(|_| -> &'static str { - panic!("error"); - }); - }).resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - }) - }).workers(1); - - let srv = srv.bind("127.0.0.1:0").unwrap(); - let addr = srv.addrs()[0]; - srv.start(); - let _ = tx.send((addr, System::current())); - }); - }); - let (addr, sys) = rx.recv().unwrap(); - System::set_current(sys.clone()); - - let mut rt = Runtime::new().unwrap(); - { - let req = client::ClientRequest::get(format!("http://{}/panic", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()); - assert!(response.is_err()); - } - - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()); - assert!(response.is_err()); - } - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } - - let _ = sys.stop(); -} - -#[test] -fn test_simple() { - let mut srv = test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok())); - let req = srv.get().finish().unwrap(); - let response = srv.execute(req.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_headers() { - let data = STR.repeat(10); - let srv_data = Arc::new(data.clone()); - let mut srv = test::TestServer::new(move |app| { - let data = srv_data.clone(); - app.handler(move |_| { - let mut builder = HttpResponse::Ok(); - for idx in 0..90 { - builder.header( - format!("X-TEST-{}", idx).as_str(), - "TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", - ); - } - builder.body(data.as_ref()) - }) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_body() { - let mut srv = - test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_body_gzip() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(STR) - }) - }); - - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - - // decode - let mut e = GzDecoder::new(&bytes[..]); - let mut dec = Vec::new(); - e.read_to_end(&mut dec).unwrap(); - assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_body_gzip_large() { - let data = STR.repeat(10); - let srv_data = Arc::new(data.clone()); - - let mut srv = test::TestServer::new(move |app| { - let data = srv_data.clone(); - app.handler(move |_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(data.as_ref()) - }) - }); - - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - - // decode - let mut e = GzDecoder::new(&bytes[..]); - let mut dec = Vec::new(); - e.read_to_end(&mut dec).unwrap(); - assert_eq!(Bytes::from(dec), Bytes::from(data)); -} - -#[test] -fn test_body_gzip_large_random() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(70_000) - .collect::(); - let srv_data = Arc::new(data.clone()); - - let mut srv = test::TestServer::new(move |app| { - let data = srv_data.clone(); - app.handler(move |_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(data.as_ref()) - }) - }); - - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - - // decode - let mut e = GzDecoder::new(&bytes[..]); - let mut dec = Vec::new(); - e.read_to_end(&mut dec).unwrap(); - assert_eq!(dec.len(), data.len()); - assert_eq!(Bytes::from(dec), Bytes::from(data)); -} - -#[test] -fn test_body_chunked_implicit() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(Body::Streaming(Box::new(body))) - }) - }); - - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - - // decode - let mut e = GzDecoder::new(&bytes[..]); - let mut dec = Vec::new(); - e.read_to_end(&mut dec).unwrap(); - assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_body_br_streaming() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Br) - .body(Body::Streaming(Box::new(body))) - }) - }); - - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - - // decode br - let mut e = BrotliDecoder::new(Vec::with_capacity(2048)); - e.write_all(bytes.as_ref()).unwrap(); - let dec = e.finish().unwrap(); - assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_head_empty() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| HttpResponse::Ok().content_length(STR.len() as u64).finish()) - }); - - let request = srv.head().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - { - let len = response - .headers() - .get(http::header::CONTENT_LENGTH) - .unwrap(); - assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); - } - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert!(bytes.is_empty()); -} - -#[test] -fn test_head_binary() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .content_length(100) - .body(STR) - }) - }); - - let request = srv.head().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - { - let len = response - .headers() - .get(http::header::CONTENT_LENGTH) - .unwrap(); - assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); - } - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert!(bytes.is_empty()); -} - -#[test] -fn test_head_binary2() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(STR) - }) - }); - - let request = srv.head().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - { - let len = response - .headers() - .get(http::header::CONTENT_LENGTH) - .unwrap(); - assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); - } -} - -#[test] -fn test_body_length() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .content_length(STR.len() as u64) - .content_encoding(http::ContentEncoding::Identity) - .body(Body::Streaming(Box::new(body))) - }) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_body_chunked_explicit() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .chunked() - .content_encoding(http::ContentEncoding::Gzip) - .body(Body::Streaming(Box::new(body))) - }) - }); - - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - - // decode - let mut e = GzDecoder::new(&bytes[..]); - let mut dec = Vec::new(); - e.read_to_end(&mut dec).unwrap(); - assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_body_identity() { - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(STR.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - let enc2 = enc.clone(); - - let mut srv = test::TestServer::new(move |app| { - let enc3 = enc2.clone(); - app.handler(move |_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .header(http::header::CONTENT_ENCODING, "deflate") - .body(enc3.clone()) - }) - }); - - // client request - let request = srv - .get() - .header("accept-encoding", "deflate") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - - // decode deflate - assert_eq!(bytes, Bytes::from(STR)); -} - -#[test] -fn test_body_deflate() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Deflate) - .body(STR) - }) - }); - - // client request - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - - // decode deflate - let mut e = ZlibDecoder::new(Vec::new()); - e.write_all(bytes.as_ref()).unwrap(); - let dec = e.finish().unwrap(); - assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_body_brotli() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Br) - .body(STR) - }) - }); - - // client request - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - - // decode brotli - let mut e = BrotliDecoder::new(Vec::with_capacity(2048)); - e.write_all(bytes.as_ref()).unwrap(); - let dec = e.finish().unwrap(); - assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_gzip_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - // client request - let mut e = GzEncoder::new(Vec::new(), Compression::default()); - e.write_all(STR.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "gzip") - .body(enc.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_gzip_encoding_large() { - let data = STR.repeat(10); - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - // client request - let mut e = GzEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "gzip") - .body(enc.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_reading_gzip_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(60_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - // client request - let mut e = GzEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "gzip") - .body(enc.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_reading_deflate_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(STR.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "deflate") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_reading_deflate_encoding_large() { - let data = STR.repeat(10); - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "deflate") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_reading_deflate_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(160_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "deflate") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_brotli_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = BrotliEncoder::new(Vec::new(), 5); - e.write_all(STR.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "br") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_brotli_encoding_large() { - let data = STR.repeat(10); - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = BrotliEncoder::new(Vec::new(), 5); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "br") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(all(feature = "brotli", feature = "ssl"))] -#[test] -fn test_brotli_encoding_large_ssl() { - use actix::{Actor, System}; - use openssl::ssl::{ - SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode, - }; - // load ssl keys - let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); - builder - .set_private_key_file("tests/key.pem", SslFiletype::PEM) - .unwrap(); - builder - .set_certificate_chain_file("tests/cert.pem") - .unwrap(); - - let data = STR.repeat(10); - let srv = test::TestServer::build().ssl(builder).start(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - let mut rt = System::new("test"); - - // client connector - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - let conn = client::ClientConnector::with_connector(builder.build()).start(); - - // body - let mut e = BrotliEncoder::new(Vec::new(), 5); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = client::ClientRequest::build() - .uri(srv.url("/")) - .method(http::Method::POST) - .header(http::header::CONTENT_ENCODING, "br") - .with_connector(conn) - .body(enc) - .unwrap(); - let response = rt.block_on(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = rt.block_on(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(all(feature = "rust-tls", feature = "ssl"))] -#[test] -fn test_reading_deflate_encoding_large_random_ssl() { - use actix::{Actor, System}; - use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; - use rustls::internal::pemfile::{certs, rsa_private_keys}; - use rustls::{NoClientAuth, ServerConfig}; - use std::fs::File; - use std::io::BufReader; - - // load ssl keys - let mut config = ServerConfig::new(NoClientAuth::new()); - let cert_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); - let key_file = &mut BufReader::new(File::open("tests/key.pem").unwrap()); - let cert_chain = certs(cert_file).unwrap(); - let mut keys = rsa_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); - - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(160_000) - .collect::(); - - let srv = test::TestServer::build().rustls(config).start(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut rt = System::new("test"); - - // client connector - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - let conn = client::ClientConnector::with_connector(builder.build()).start(); - - // encode data - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = client::ClientRequest::build() - .uri(srv.url("/")) - .method(http::Method::POST) - .header(http::header::CONTENT_ENCODING, "deflate") - .with_connector(conn) - .body(enc) - .unwrap(); - let response = rt.block_on(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = rt.block_on(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(all(feature = "tls", feature = "ssl"))] -#[test] -fn test_reading_deflate_encoding_large_random_tls() { - use native_tls::{Identity, TlsAcceptor}; - use openssl::ssl::{ - SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode, - }; - use std::fs::File; - use std::sync::mpsc; - - use actix::{Actor, System}; - let (tx, rx) = mpsc::channel(); - - // load ssl keys - let mut file = File::open("tests/identity.pfx").unwrap(); - let mut identity = vec![]; - file.read_to_end(&mut identity).unwrap(); - let identity = Identity::from_pkcs12(&identity, "1").unwrap(); - let acceptor = TlsAcceptor::new(identity).unwrap(); - - // load ssl keys - let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); - builder - .set_private_key_file("tests/key.pem", SslFiletype::PEM) - .unwrap(); - builder - .set_certificate_chain_file("tests/cert.pem") - .unwrap(); - - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(160_000) - .collect::(); - - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - System::run(move || { - server::new(|| { - App::new().handler("/", |req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }).bind_tls(addr, acceptor) - .unwrap() - .start(); - let _ = tx.send(System::current()); - }); - }); - let sys = rx.recv().unwrap(); - - let mut rt = System::new("test"); - - // client connector - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - let conn = client::ClientConnector::with_connector(builder.build()).start(); - - // encode data - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = client::ClientRequest::build() - .uri(format!("https://{}/", addr)) - .method(http::Method::POST) - .header(http::header::CONTENT_ENCODING, "deflate") - .with_connector(conn) - .body(enc) - .unwrap(); - let response = rt.block_on(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = rt.block_on(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); - - let _ = sys.stop(); -} - -#[test] -fn test_h2() { - let srv = test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); - let addr = srv.addr(); - thread::sleep(time::Duration::from_millis(500)); - - let mut core = Runtime::new().unwrap(); - let tcp = TcpStream::connect(&addr); - - let tcp = tcp - .then(|res| h2client::handshake(res.unwrap())) - .then(move |res| { - let (mut client, h2) = res.unwrap(); - - let request = Request::builder() - .uri(format!("https://{}/", addr).as_str()) - .body(()) - .unwrap(); - let (response, _) = client.send_request(request, false).unwrap(); - - // Spawn a task to run the conn... - spawn(h2.map_err(|e| println!("GOT ERR={:?}", e))); - - response.and_then(|response| { - assert_eq!(response.status(), http::StatusCode::OK); - - let (_, body) = response.into_parts(); - - body.fold(BytesMut::new(), |mut b, c| -> Result<_, h2::Error> { - b.extend(c); - Ok(b) - }) - }) - }); - let _res = core.block_on(tcp); - // assert_eq!(_res.unwrap(), Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_application() { - let mut srv = test::TestServer::with_factory(|| { - App::new().resource("/", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_default_404_handler_response() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .prefix("/app") - .resource("", |r| r.f(|_| HttpResponse::Ok())) - .resource("/", |r| r.f(|_| HttpResponse::Ok())) - }); - let addr = srv.addr(); - - let mut buf = [0; 24]; - let request = TcpStream::connect(&addr) - .and_then(|sock| { - tokio::io::write_all(sock, "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n") - .and_then(|(sock, _)| tokio::io::read_exact(sock, &mut buf)) - .and_then(|(_, buf)| Ok(buf)) - }).map_err(|e| panic!("{:?}", e)); - let response = srv.execute(request).unwrap(); - let rep = String::from_utf8_lossy(&response[..]); - assert!(rep.contains("HTTP/1.1 404 Not Found")); -} - -#[test] -fn test_server_cookies() { - use actix_web::http; - - let mut srv = test::TestServer::with_factory(|| { - App::new().resource("/", |r| { - r.f(|_| { - HttpResponse::Ok() - .cookie( - http::CookieBuilder::new("first", "first_value") - .http_only(true) - .finish(), - ).cookie(http::Cookie::new("second", "first_value")) - .cookie(http::Cookie::new("second", "second_value")) - .finish() - }) - }) - }); - - let first_cookie = http::CookieBuilder::new("first", "first_value") - .http_only(true) - .finish(); - let second_cookie = http::Cookie::new("second", "second_value"); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - let cookies = response.cookies().expect("To have cookies"); - assert_eq!(cookies.len(), 2); - if cookies[0] == first_cookie { - assert_eq!(cookies[1], second_cookie); - } else { - assert_eq!(cookies[0], second_cookie); - assert_eq!(cookies[1], first_cookie); - } - - let first_cookie = first_cookie.to_string(); - let second_cookie = second_cookie.to_string(); - //Check that we have exactly two instances of raw cookie headers - let cookies = response - .headers() - .get_all(http::header::SET_COOKIE) - .iter() - .map(|header| header.to_str().expect("To str").to_string()) - .collect::>(); - assert_eq!(cookies.len(), 2); - if cookies[0] == first_cookie { - assert_eq!(cookies[1], second_cookie); - } else { - assert_eq!(cookies[0], second_cookie); - assert_eq!(cookies[1], first_cookie); - } -} - -#[test] -fn test_slow_request() { - use actix::System; - use std::net; - use std::sync::mpsc; - let (tx, rx) = mpsc::channel(); - - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - System::run(move || { - let srv = server::new(|| { - vec![App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - })] - }); - - let srv = srv.bind(addr).unwrap(); - srv.client_timeout(200).start(); - let _ = tx.send(System::current()); - }); - }); - let sys = rx.recv().unwrap(); - - thread::sleep(time::Duration::from_millis(200)); - - let mut stream = net::TcpStream::connect(addr).unwrap(); - let mut data = String::new(); - let _ = stream.read_to_string(&mut data); - assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); - - let mut stream = net::TcpStream::connect(addr).unwrap(); - let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n"); - let mut data = String::new(); - let _ = stream.read_to_string(&mut data); - assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); - - sys.stop(); -} - -#[test] -fn test_malformed_request() { - use actix::System; - use std::net; - use std::sync::mpsc; - let (tx, rx) = mpsc::channel(); - - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - System::run(move || { - let srv = server::new(|| { - App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - }) - }); - - let _ = srv.bind(addr).unwrap().start(); - let _ = tx.send(System::current()); - }); - }); - let sys = rx.recv().unwrap(); - thread::sleep(time::Duration::from_millis(200)); - - let mut stream = net::TcpStream::connect(addr).unwrap(); - let _ = stream.write_all(b"GET /test/tests/test HTTP1.1\r\n"); - let mut data = String::new(); - let _ = stream.read_to_string(&mut data); - assert!(data.starts_with("HTTP/1.1 400 Bad Request")); - - sys.stop(); -} - -#[test] -fn test_app_404() { - let mut srv = test::TestServer::with_factory(|| { - App::new().prefix("/prefix").resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - }) - }); - - let request = srv.client(http::Method::GET, "/prefix/").finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - let request = srv.client(http::Method::GET, "/").finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), http::StatusCode::NOT_FOUND); -} - -#[test] -#[cfg(feature = "ssl")] -fn test_ssl_handshake_timeout() { - use actix::System; - use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; - use std::net; - use std::sync::mpsc; - - let (tx, rx) = mpsc::channel(); - let addr = test::TestServer::unused_addr(); - - // load ssl keys - let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); - builder - .set_private_key_file("tests/key.pem", SslFiletype::PEM) - .unwrap(); - builder - .set_certificate_chain_file("tests/cert.pem") - .unwrap(); - - thread::spawn(move || { - System::run(move || { - let srv = server::new(|| { - App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - }) - }); - - srv.bind_ssl(addr, builder) - .unwrap() - .workers(1) - .client_timeout(200) - .start(); - let _ = tx.send(System::current()); - }); - }); - let sys = rx.recv().unwrap(); - - let mut stream = net::TcpStream::connect(addr).unwrap(); - let mut data = String::new(); - let _ = stream.read_to_string(&mut data); - assert!(data.is_empty()); - - let _ = sys.stop(); -} diff --git a/tests/test_ws.rs b/tests/test_ws.rs deleted file mode 100644 index 5a0ce204f..000000000 --- a/tests/test_ws.rs +++ /dev/null @@ -1,394 +0,0 @@ -extern crate actix; -extern crate actix_web; -extern crate bytes; -extern crate futures; -extern crate http; -extern crate rand; - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::{thread, time}; - -use bytes::Bytes; -use futures::Stream; -use rand::distributions::Alphanumeric; -use rand::Rng; - -#[cfg(feature = "ssl")] -extern crate openssl; -#[cfg(feature = "rust-tls")] -extern crate rustls; - -use actix::prelude::*; -use actix_web::*; - -struct Ws; - -impl Actor for Ws { - type Context = ws::WebsocketContext; -} - -impl StreamHandler for Ws { - fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { - match msg { - ws::Message::Ping(msg) => ctx.pong(&msg), - ws::Message::Text(text) => ctx.text(text), - ws::Message::Binary(bin) => ctx.binary(bin), - ws::Message::Close(reason) => ctx.close(reason), - _ => (), - } - } -} - -#[test] -fn test_simple() { - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (reader, mut writer) = srv.ws().unwrap(); - - writer.text("text"); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Text("text".to_owned()))); - - writer.binary(b"text".as_ref()); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(ws::Message::Binary(Bytes::from_static(b"text").into())) - ); - - writer.ping("ping"); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Pong("ping".to_owned()))); - - writer.close(Some(ws::CloseCode::Normal.into())); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(ws::Message::Close(Some(ws::CloseCode::Normal.into()))) - ); -} - -// websocket resource helper function -fn start_ws_resource(req: &HttpRequest) -> Result { - ws::start(req, Ws) -} - -#[test] -fn test_simple_path() { - const PATH: &str = "/v1/ws/"; - - // Create a websocket at a specific path. - let mut srv = test::TestServer::new(|app| { - app.resource(PATH, |r| r.route().f(start_ws_resource)); - }); - // fetch the sockets for the resource at a given path. - let (reader, mut writer) = srv.ws_at(PATH).unwrap(); - - writer.text("text"); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Text("text".to_owned()))); - - writer.binary(b"text".as_ref()); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(ws::Message::Binary(Bytes::from_static(b"text").into())) - ); - - writer.ping("ping"); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Pong("ping".to_owned()))); - - writer.close(Some(ws::CloseCode::Normal.into())); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(ws::Message::Close(Some(ws::CloseCode::Normal.into()))) - ); -} - -#[test] -fn test_empty_close_code() { - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (reader, mut writer) = srv.ws().unwrap(); - - writer.close(None); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Close(None))); -} - -#[test] -fn test_close_description() { - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (reader, mut writer) = srv.ws().unwrap(); - - let close_reason: ws::CloseReason = - (ws::CloseCode::Normal, "close description").into(); - writer.close(Some(close_reason.clone())); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Close(Some(close_reason)))); -} - -#[test] -fn test_large_text() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(65_536) - .collect::(); - - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (mut reader, mut writer) = srv.ws().unwrap(); - - for _ in 0..100 { - writer.text(data.clone()); - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, Some(ws::Message::Text(data.clone()))); - } -} - -#[test] -fn test_large_bin() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(65_536) - .collect::(); - - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (mut reader, mut writer) = srv.ws().unwrap(); - - for _ in 0..100 { - writer.binary(data.clone()); - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, Some(ws::Message::Binary(Binary::from(data.clone())))); - } -} - -#[test] -fn test_client_frame_size() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(131_072) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req| -> Result { - let mut resp = ws::handshake(req)?; - let stream = ws::WsStream::new(req.payload()).max_size(131_072); - - let body = ws::WebsocketContext::create(req.clone(), Ws, stream); - Ok(resp.body(body)) - }) - }); - let (reader, mut writer) = srv.ws().unwrap(); - - writer.binary(data.clone()); - match srv.execute(reader.into_future()).err().unwrap().0 { - ws::ProtocolError::Overflow => (), - _ => panic!(), - } -} - -struct Ws2 { - count: usize, - bin: bool, -} - -impl Actor for Ws2 { - type Context = ws::WebsocketContext; - - fn started(&mut self, ctx: &mut Self::Context) { - self.send(ctx); - } -} - -impl Ws2 { - fn send(&mut self, ctx: &mut ws::WebsocketContext) { - if self.bin { - ctx.binary(Vec::from("0".repeat(65_536))); - } else { - ctx.text("0".repeat(65_536)); - } - ctx.drain() - .and_then(|_, act, ctx| { - act.count += 1; - if act.count != 10_000 { - act.send(ctx); - } - actix::fut::ok(()) - }).wait(ctx); - } -} - -impl StreamHandler for Ws2 { - fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { - match msg { - ws::Message::Ping(msg) => ctx.pong(&msg), - ws::Message::Text(text) => ctx.text(text), - ws::Message::Binary(bin) => ctx.binary(bin), - ws::Message::Close(reason) => ctx.close(reason), - _ => (), - } - } -} - -#[test] -fn test_server_send_text() { - let data = Some(ws::Message::Text("0".repeat(65_536))); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req| { - ws::start( - req, - Ws2 { - count: 0, - bin: false, - }, - ) - }) - }); - let (mut reader, _writer) = srv.ws().unwrap(); - - for _ in 0..10_000 { - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, data); - } -} - -#[test] -fn test_server_send_bin() { - let data = Some(ws::Message::Binary(Binary::from("0".repeat(65_536)))); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req| { - ws::start( - req, - Ws2 { - count: 0, - bin: true, - }, - ) - }) - }); - let (mut reader, _writer) = srv.ws().unwrap(); - - for _ in 0..10_000 { - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, data); - } -} - -#[test] -#[cfg(feature = "ssl")] -fn test_ws_server_ssl() { - use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; - - // load ssl keys - let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); - builder - .set_private_key_file("tests/key.pem", SslFiletype::PEM) - .unwrap(); - builder - .set_certificate_chain_file("tests/cert.pem") - .unwrap(); - - let mut srv = test::TestServer::build().ssl(builder).start(|app| { - app.handler(|req| { - ws::start( - req, - Ws2 { - count: 0, - bin: false, - }, - ) - }) - }); - let (mut reader, _writer) = srv.ws().unwrap(); - - let data = Some(ws::Message::Text("0".repeat(65_536))); - for _ in 0..10_000 { - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, data); - } -} - -#[test] -#[cfg(feature = "rust-tls")] -fn test_ws_server_rust_tls() { - use rustls::internal::pemfile::{certs, rsa_private_keys}; - use rustls::{NoClientAuth, ServerConfig}; - use std::fs::File; - use std::io::BufReader; - - // load ssl keys - let mut config = ServerConfig::new(NoClientAuth::new()); - let cert_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); - let key_file = &mut BufReader::new(File::open("tests/key.pem").unwrap()); - let cert_chain = certs(cert_file).unwrap(); - let mut keys = rsa_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); - - let mut srv = test::TestServer::build().rustls(config).start(|app| { - app.handler(|req| { - ws::start( - req, - Ws2 { - count: 0, - bin: false, - }, - ) - }) - }); - - let (mut reader, _writer) = srv.ws().unwrap(); - - let data = Some(ws::Message::Text("0".repeat(65_536))); - for _ in 0..10_000 { - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, data); - } -} - -struct WsStopped(Arc); - -impl Actor for WsStopped { - type Context = ws::WebsocketContext; - - fn stopped(&mut self, _: &mut Self::Context) { - self.0.fetch_add(1, Ordering::Relaxed); - } -} - -impl StreamHandler for WsStopped { - fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { - match msg { - ws::Message::Text(text) => ctx.text(text), - _ => (), - } - } -} - -#[test] -fn test_ws_stopped() { - let num = Arc::new(AtomicUsize::new(0)); - let num2 = num.clone(); - - let mut srv = test::TestServer::new(move |app| { - let num3 = num2.clone(); - app.handler(move |req| ws::start(req, WsStopped(num3.clone()))) - }); - { - let (reader, mut writer) = srv.ws().unwrap(); - writer.text("text"); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Text("text".to_owned()))); - } - thread::sleep(time::Duration::from_millis(1000)); - - assert_eq!(num.load(Ordering::Relaxed), 1); -} From b15b2dda22dda5d13c58c457ec3ada773c03d5b8 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 4 Oct 2018 17:34:57 -0700 Subject: [PATCH 003/427] remove ServerSettings --- Cargo.toml | 10 +- src/error.rs | 4 - src/info.rs | 220 ----------------------------------------- src/lib.rs | 12 --- src/server/error.rs | 17 ---- src/server/message.rs | 46 ++------- src/server/mod.rs | 7 +- src/server/settings.rs | 119 +--------------------- 8 files changed, 10 insertions(+), 425 deletions(-) delete mode 100644 src/info.rs diff --git a/Cargo.toml b/Cargo.toml index 258301daa..60f14485f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,14 +66,11 @@ actix-net = { git="https://github.com/actix/actix-net.git" } base64 = "0.9" bitflags = "1.0" -h2 = "0.1" -htmlescape = "0.3" http = "^0.1.8" httparse = "1.3" log = "0.4" mime = "0.3" mime_guess = "2.0.0-alpha" -num_cpus = "1.0" percent-encoding = "1.0" rand = "0.5" regex = "1.0" @@ -85,8 +82,6 @@ time = "0.1" encoding = "0.2" language-tags = "0.2" lazy_static = "1.0" -lazycell = "1.0.0" -parking_lot = "0.6" serde_urlencoded = "^0.5.3" url = { version="1.7", features=["query_encoding"] } cookie = { version="0.11", features=["percent-encode"] } @@ -96,14 +91,10 @@ flate2 = { version="^1.0.2", optional = true, default-features = false } failure = "^0.1.2" # io -mio = "^0.6.13" net2 = "0.2" bytes = "0.4" byteorder = "1.2" futures = "0.1" -futures-cpupool = "0.1" -slab = "0.4" -tokio = "0.1" tokio-codec = "0.1" tokio-io = "0.1" tokio-tcp = "0.1" @@ -132,6 +123,7 @@ tokio-uds = { version="0.2", optional = true } actix-web = "0.7" env_logger = "0.5" serde_derive = "1.0" +tokio = "0.1" [build-dependencies] version_check = "0.1" diff --git a/src/error.rs b/src/error.rs index 724803805..ff2388de8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,7 +11,6 @@ use failure::{self, Backtrace, Fail}; use futures::Canceled; use http::uri::InvalidUri; use http::{header, Error as HttpError, StatusCode}; -use http2::Error as Http2Error; use httparse; use serde::de::value::Error as DeError; use serde_json::error::Error as JsonError; @@ -350,9 +349,6 @@ pub enum PayloadError { /// Io error #[fail(display = "{}", _0)] Io(#[cause] IoError), - /// Http2 error - #[fail(display = "{}", _0)] - Http2(#[cause] Http2Error), } impl From for PayloadError { diff --git a/src/info.rs b/src/info.rs deleted file mode 100644 index 5a2f21805..000000000 --- a/src/info.rs +++ /dev/null @@ -1,220 +0,0 @@ -use http::header::{self, HeaderName}; -use server::Request; - -const X_FORWARDED_FOR: &[u8] = b"x-forwarded-for"; -const X_FORWARDED_HOST: &[u8] = b"x-forwarded-host"; -const X_FORWARDED_PROTO: &[u8] = b"x-forwarded-proto"; - -/// `HttpRequest` connection information -#[derive(Clone, Default)] -pub struct ConnectionInfo { - scheme: String, - host: String, - remote: Option, - peer: Option, -} - -impl ConnectionInfo { - /// Create *ConnectionInfo* instance for a request. - #[cfg_attr( - feature = "cargo-clippy", - allow(clippy::cyclomatic_complexity) - )] - pub fn update(&mut self, req: &Request) { - let mut host = None; - let mut scheme = None; - let mut remote = None; - let mut peer = None; - - // load forwarded header - for hdr in req.headers().get_all(header::FORWARDED) { - if let Ok(val) = hdr.to_str() { - for pair in val.split(';') { - for el in pair.split(',') { - let mut items = el.trim().splitn(2, '='); - if let Some(name) = items.next() { - if let Some(val) = items.next() { - match &name.to_lowercase() as &str { - "for" => if remote.is_none() { - remote = Some(val.trim()); - }, - "proto" => if scheme.is_none() { - scheme = Some(val.trim()); - }, - "host" => if host.is_none() { - host = Some(val.trim()); - }, - _ => (), - } - } - } - } - } - } - } - - // scheme - if scheme.is_none() { - if let Some(h) = req - .headers() - .get(HeaderName::from_lowercase(X_FORWARDED_PROTO).unwrap()) - { - if let Ok(h) = h.to_str() { - scheme = h.split(',').next().map(|v| v.trim()); - } - } - if scheme.is_none() { - scheme = req.uri().scheme_part().map(|a| a.as_str()); - if scheme.is_none() && req.server_settings().secure() { - scheme = Some("https") - } - } - } - - // host - if host.is_none() { - if let Some(h) = req - .headers() - .get(HeaderName::from_lowercase(X_FORWARDED_HOST).unwrap()) - { - if let Ok(h) = h.to_str() { - host = h.split(',').next().map(|v| v.trim()); - } - } - if host.is_none() { - if let Some(h) = req.headers().get(header::HOST) { - host = h.to_str().ok(); - } - if host.is_none() { - host = req.uri().authority_part().map(|a| a.as_str()); - if host.is_none() { - host = Some(req.server_settings().host()); - } - } - } - } - - // remote addr - if remote.is_none() { - if let Some(h) = req - .headers() - .get(HeaderName::from_lowercase(X_FORWARDED_FOR).unwrap()) - { - if let Ok(h) = h.to_str() { - remote = h.split(',').next().map(|v| v.trim()); - } - } - if remote.is_none() { - // get peeraddr from socketaddr - peer = req.peer_addr().map(|addr| format!("{}", addr)); - } - } - - self.scheme = scheme.unwrap_or("http").to_owned(); - self.host = host.unwrap_or("localhost").to_owned(); - self.remote = remote.map(|s| s.to_owned()); - self.peer = peer; - } - - /// Scheme of the request. - /// - /// Scheme is resolved through the following headers, in this order: - /// - /// - Forwarded - /// - X-Forwarded-Proto - /// - Uri - #[inline] - pub fn scheme(&self) -> &str { - &self.scheme - } - - /// Hostname of the request. - /// - /// Hostname is resolved through the following headers, in this order: - /// - /// - Forwarded - /// - X-Forwarded-Host - /// - Host - /// - Uri - /// - Server hostname - pub fn host(&self) -> &str { - &self.host - } - - /// Remote IP of client initiated HTTP request. - /// - /// The IP is resolved through the following headers, in this order: - /// - /// - Forwarded - /// - X-Forwarded-For - /// - peer name of opened socket - #[inline] - pub fn remote(&self) -> Option<&str> { - if let Some(ref r) = self.remote { - Some(r) - } else if let Some(ref peer) = self.peer { - Some(peer) - } else { - None - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use test::TestRequest; - - #[test] - fn test_forwarded() { - let req = TestRequest::default().request(); - let mut info = ConnectionInfo::default(); - info.update(&req); - assert_eq!(info.scheme(), "http"); - assert_eq!(info.host(), "localhost:8080"); - - let req = TestRequest::default() - .header( - header::FORWARDED, - "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org", - ).request(); - - let mut info = ConnectionInfo::default(); - info.update(&req); - assert_eq!(info.scheme(), "https"); - assert_eq!(info.host(), "rust-lang.org"); - assert_eq!(info.remote(), Some("192.0.2.60")); - - let req = TestRequest::default() - .header(header::HOST, "rust-lang.org") - .request(); - - let mut info = ConnectionInfo::default(); - info.update(&req); - assert_eq!(info.scheme(), "http"); - assert_eq!(info.host(), "rust-lang.org"); - assert_eq!(info.remote(), None); - - let req = TestRequest::default() - .header(X_FORWARDED_FOR, "192.0.2.60") - .request(); - let mut info = ConnectionInfo::default(); - info.update(&req); - assert_eq!(info.remote(), Some("192.0.2.60")); - - let req = TestRequest::default() - .header(X_FORWARDED_HOST, "192.0.2.60") - .request(); - let mut info = ConnectionInfo::default(); - info.update(&req); - assert_eq!(info.host(), "192.0.2.60"); - assert_eq!(info.remote(), None); - - let req = TestRequest::default() - .header(X_FORWARDED_PROTO, "https") - .request(); - let mut info = ConnectionInfo::default(); - info.update(&req); - assert_eq!(info.scheme(), "https"); - } -} diff --git a/src/lib.rs b/src/lib.rs index 6df1a770e..efd566187 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,20 +99,13 @@ extern crate lazy_static; #[macro_use] extern crate futures; extern crate cookie; -extern crate futures_cpupool; -extern crate htmlescape; extern crate http as modhttp; extern crate httparse; extern crate language_tags; -extern crate lazycell; extern crate mime; extern crate mime_guess; -extern crate mio; extern crate net2; -extern crate parking_lot; extern crate rand; -extern crate slab; -extern crate tokio; extern crate tokio_codec; extern crate tokio_current_thread; extern crate tokio_io; @@ -129,8 +122,6 @@ extern crate brotli2; extern crate encoding; #[cfg(feature = "flate2")] extern crate flate2; -extern crate h2 as http2; -extern crate num_cpus; extern crate serde_urlencoded; #[macro_use] extern crate percent_encoding; @@ -148,9 +139,7 @@ mod extensions; mod header; mod httpcodes; mod httpmessage; -//mod httprequest; mod httpresponse; -mod info; mod json; mod payload; mod uri; @@ -182,7 +171,6 @@ pub mod dev { pub use body::BodyStream; pub use httpmessage::{MessageBody, Readlines, UrlEncoded}; pub use httpresponse::HttpResponseBuilder; - pub use info::ConnectionInfo; pub use json::JsonBody; pub use payload::{Payload, PayloadBuffer}; } diff --git a/src/server/error.rs b/src/server/error.rs index d9e1239e1..7d5c67d1e 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -2,7 +2,6 @@ use std::fmt::{Debug, Display}; use std::io; use futures::{Async, Poll}; -use http2; use error::{Error, ParseError}; use http::{StatusCode, Version}; @@ -44,10 +43,6 @@ pub enum HttpDispatchError { // #[fail(display = "Connection shutdown timeout")] ShutdownTimeout, - /// HTTP2 error - // #[fail(display = "HTTP2 error: {}", _0)] - Http2(http2::Error), - /// Payload is not consumed // #[fail(display = "Task is completed but request's payload is not consumed")] PayloadIsNotConsumed, @@ -65,12 +60,6 @@ pub enum HttpDispatchError { Unknown, } -// impl From for HttpDispatchError { -// fn from(err: E) -> Self { -// HttpDispatchError::App(err) -// } -// } - impl From for HttpDispatchError { fn from(err: ParseError) -> Self { HttpDispatchError::Parse(err) @@ -82,9 +71,3 @@ impl From for HttpDispatchError { HttpDispatchError::Io(err) } } - -impl From for HttpDispatchError { - fn from(err: http2::Error) -> Self { - HttpDispatchError::Http2(err) - } -} diff --git a/src/server/message.rs b/src/server/message.rs index 74ec5f17c..c39302bab 100644 --- a/src/server/message.rs +++ b/src/server/message.rs @@ -8,9 +8,7 @@ use http::{header, HeaderMap, Method, Uri, Version}; use extensions::Extensions; use httpmessage::HttpMessage; -use info::ConnectionInfo; use payload::Payload; -use server::ServerSettings; use uri::Url as InnerUrl; bitflags! { @@ -33,9 +31,7 @@ pub(crate) struct InnerRequest { pub(crate) headers: HeaderMap, pub(crate) extensions: RefCell, pub(crate) addr: Option, - pub(crate) info: RefCell, pub(crate) payload: RefCell>, - pub(crate) settings: ServerSettings, pub(crate) stream_extensions: Option>, pool: &'static RequestPool, } @@ -70,18 +66,16 @@ impl HttpMessage for Request { impl Request { /// Create new RequestContext instance - pub(crate) fn new(pool: &'static RequestPool, settings: ServerSettings) -> Request { + pub(crate) fn new(pool: &'static RequestPool) -> Request { Request { inner: Rc::new(InnerRequest { pool, - settings, method: Method::GET, url: InnerUrl::default(), version: Version::HTTP_11, headers: HeaderMap::with_capacity(16), flags: Cell::new(MessageFlags::empty()), addr: None, - info: RefCell::new(ConnectionInfo::default()), payload: RefCell::new(None), extensions: RefCell::new(Extensions::new()), stream_extensions: None, @@ -144,9 +138,6 @@ impl Request { /// /// Peer address is actual socket address, if proxy is used in front of /// actix http server, then peer address would be address of this proxy. - /// - /// To get client connection information `connection_info()` method should - /// be used. pub fn peer_addr(&self) -> Option { self.inner().addr } @@ -179,31 +170,12 @@ impl Request { self.inner().method == Method::CONNECT } - /// Get *ConnectionInfo* for the correct request. - pub fn connection_info(&self) -> Ref { - if self.inner().flags.get().contains(MessageFlags::CONN_INFO) { - self.inner().info.borrow() - } else { - let mut flags = self.inner().flags.get(); - flags.insert(MessageFlags::CONN_INFO); - self.inner().flags.set(flags); - self.inner().info.borrow_mut().update(self); - self.inner().info.borrow() - } - } - /// Io stream extensions #[inline] pub fn stream_extensions(&self) -> Option<&Extensions> { self.inner().stream_extensions.as_ref().map(|e| e.as_ref()) } - /// Server settings - #[inline] - pub fn server_settings(&self) -> &ServerSettings { - &self.inner().settings - } - pub(crate) fn clone(&self) -> Self { Request { inner: self.inner.clone(), @@ -241,24 +213,18 @@ impl fmt::Debug for Request { } } -pub struct RequestPool(RefCell>>, RefCell); +pub struct RequestPool(RefCell>>); thread_local!(static POOL: &'static RequestPool = RequestPool::create()); impl RequestPool { fn create() -> &'static RequestPool { - let pool = RequestPool( - RefCell::new(VecDeque::with_capacity(128)), - RefCell::new(ServerSettings::default()), - ); + let pool = RequestPool(RefCell::new(VecDeque::with_capacity(128))); Box::leak(Box::new(pool)) } - pub(crate) fn pool(settings: ServerSettings) -> &'static RequestPool { - POOL.with(|p| { - *p.1.borrow_mut() = settings; - *p - }) + pub(crate) fn pool() -> &'static RequestPool { + POOL.with(|p| *p) } #[inline] @@ -266,7 +232,7 @@ impl RequestPool { if let Some(msg) = pool.0.borrow_mut().pop_front() { Request { inner: msg } } else { - Request::new(pool, pool.1.borrow().clone()) + Request::new(pool) } } diff --git a/src/server/mod.rs b/src/server/mod.rs index 7d64a6e20..be172e646 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -130,10 +130,8 @@ pub(crate) mod output; // pub(crate) mod service; pub(crate) mod settings; -pub use self::message::Request; - pub use self::error::{AcceptorError, HttpDispatchError}; -pub use self::settings::ServerSettings; +pub use self::message::Request; #[doc(hidden)] pub mod h1disp; @@ -141,9 +139,6 @@ pub mod h1disp; #[doc(hidden)] pub use self::settings::{ServiceConfig, ServiceConfigBuilder}; -//#[doc(hidden)] -//pub use self::service::{H1Service, HttpService, StreamConfiguration}; - #[doc(hidden)] pub use self::helpers::write_content_length; diff --git a/src/server/settings.rs b/src/server/settings.rs index f21283590..b8b7e51f0 100644 --- a/src/server/settings.rs +++ b/src/server/settings.rs @@ -7,10 +7,7 @@ use std::{env, fmt, net}; use bytes::BytesMut; use futures::{future, Future}; -use futures_cpupool::CpuPool; use http::StatusCode; -use lazycell::LazyCell; -use parking_lot::Mutex; use time; use tokio_current_thread::spawn; use tokio_timer::{sleep, Delay}; @@ -20,109 +17,6 @@ use super::KeepAlive; use body::Body; use httpresponse::{HttpResponse, HttpResponseBuilder, HttpResponsePool}; -/// Env variable for default cpu pool size -const ENV_CPU_POOL_VAR: &str = "ACTIX_CPU_POOL"; - -lazy_static! { - pub(crate) static ref DEFAULT_CPUPOOL: Mutex = { - let default = match env::var(ENV_CPU_POOL_VAR) { - Ok(val) => { - if let Ok(val) = val.parse() { - val - } else { - error!("Can not parse ACTIX_CPU_POOL value"); - 20 - } - } - Err(_) => 20, - }; - Mutex::new(CpuPool::new(default)) - }; -} - -/// Various server settings -pub struct ServerSettings { - addr: net::SocketAddr, - secure: bool, - host: String, - cpu_pool: LazyCell, - responses: &'static HttpResponsePool, -} - -impl Clone for ServerSettings { - fn clone(&self) -> Self { - ServerSettings { - addr: self.addr, - secure: self.secure, - host: self.host.clone(), - cpu_pool: LazyCell::new(), - responses: HttpResponsePool::get_pool(), - } - } -} - -impl Default for ServerSettings { - fn default() -> Self { - ServerSettings { - addr: "127.0.0.1:8080".parse().unwrap(), - secure: false, - host: "localhost:8080".to_owned(), - responses: HttpResponsePool::get_pool(), - cpu_pool: LazyCell::new(), - } - } -} - -impl ServerSettings { - /// Crate server settings instance - pub(crate) fn new( - addr: net::SocketAddr, host: &str, secure: bool, - ) -> ServerSettings { - let host = host.to_owned(); - let cpu_pool = LazyCell::new(); - let responses = HttpResponsePool::get_pool(); - ServerSettings { - addr, - secure, - host, - cpu_pool, - responses, - } - } - - /// Returns the socket address of the local half of this TCP connection - pub fn local_addr(&self) -> net::SocketAddr { - self.addr - } - - /// Returns true if connection is secure(https) - pub fn secure(&self) -> bool { - self.secure - } - - /// Returns host header value - pub fn host(&self) -> &str { - &self.host - } - - /// Returns default `CpuPool` for server - pub fn cpu_pool(&self) -> &CpuPool { - self.cpu_pool.borrow_with(|| DEFAULT_CPUPOOL.lock().clone()) - } - - #[inline] - pub(crate) fn get_response(&self, status: StatusCode, body: Body) -> HttpResponse { - HttpResponsePool::get_response(&self.responses, status, body) - } - - #[inline] - pub(crate) fn get_response_builder( - &self, status: StatusCode, - ) -> HttpResponseBuilder { - HttpResponsePool::get_builder(&self.responses, status) - } -} - // "Sun, 06 Nov 1994 08:49:37 GMT".len() const DATE_VALUE_LENGTH: usize = 29; @@ -149,7 +43,6 @@ impl ServiceConfig { /// Create instance of `ServiceConfig` pub(crate) fn new( keep_alive: KeepAlive, client_timeout: u64, client_shutdown: u64, - settings: ServerSettings, ) -> ServiceConfig { let (keep_alive, ka_enabled) = match keep_alive { KeepAlive::Timeout(val) => (val as u64, true), @@ -168,7 +61,7 @@ impl ServiceConfig { client_timeout, client_shutdown, bytes: Rc::new(SharedBytesPool::new()), - messages: RequestPool::pool(settings), + messages: RequestPool::pool(), date: UnsafeCell::new((false, Date::new())), })) } @@ -211,9 +104,7 @@ impl ServiceConfig { // Unsafe: WorkerSetting is !Sync and !Send unsafe { (*self.0.date.get()).0 = false }; } -} -impl ServiceConfig { #[inline] /// Client timeout for first request. pub fn client_timer(&self) -> Option { @@ -412,15 +303,9 @@ impl ServiceConfigBuilder { /// Finish service configuration and create `ServiceConfig` object. pub fn finish(self) -> ServiceConfig { - let settings = ServerSettings::new(self.addr, &self.host, self.secure); let client_shutdown = if self.secure { self.client_shutdown } else { 0 }; - ServiceConfig::new( - self.keep_alive, - self.client_timeout, - client_shutdown, - settings, - ) + ServiceConfig::new(self.keep_alive, self.client_timeout, client_shutdown) } } From 4ca711909b07b882f57fc76c395da37378bee649 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 4 Oct 2018 20:02:10 -0700 Subject: [PATCH 004/427] refactor types --- src/{server/settings.rs => config.rs} | 50 +- src/error.rs | 136 +-- src/{server/h1codec.rs => h1/codec.rs} | 32 +- src/{server/h1decoder.rs => h1/decoder.rs} | 16 +- src/{server/h1disp.rs => h1/dispatcher.rs} | 80 +- src/h1/mod.rs | 9 + src/h1/service.rs | 125 ++ src/{server => }/helpers.rs | 0 src/lib.rs | 9 +- src/{server/message.rs => request.rs} | 34 +- src/server/error.rs | 73 -- src/server/h1.rs | 1289 -------------------- src/server/mod.rs | 21 +- src/server/output.rs | 2 +- tests/test_h1v2.rs | 20 +- 15 files changed, 273 insertions(+), 1623 deletions(-) rename src/{server/settings.rs => config.rs} (89%) rename src/{server/h1codec.rs => h1/codec.rs} (93%) rename src/{server/h1decoder.rs => h1/decoder.rs} (97%) rename src/{server/h1disp.rs => h1/dispatcher.rs} (86%) create mode 100644 src/h1/mod.rs create mode 100644 src/h1/service.rs rename src/{server => }/helpers.rs (100%) rename src/{server/message.rs => request.rs} (87%) delete mode 100644 src/server/error.rs delete mode 100644 src/server/h1.rs diff --git a/src/server/settings.rs b/src/config.rs similarity index 89% rename from src/server/settings.rs rename to src/config.rs index b8b7e51f0..508cd5dde 100644 --- a/src/server/settings.rs +++ b/src/config.rs @@ -12,10 +12,10 @@ use time; use tokio_current_thread::spawn; use tokio_timer::{sleep, Delay}; -use super::message::{Request, RequestPool}; -use super::KeepAlive; use body::Body; use httpresponse::{HttpResponse, HttpResponseBuilder, HttpResponsePool}; +use request::{Request, RequestPool}; +use server::KeepAlive; // "Sun, 06 Nov 1994 08:49:37 GMT".len() const DATE_VALUE_LENGTH: usize = 29; @@ -28,8 +28,6 @@ struct Inner { client_timeout: u64, client_shutdown: u64, ka_enabled: bool, - bytes: Rc, - messages: &'static RequestPool, date: UnsafeCell<(bool, Date)>, } @@ -60,8 +58,6 @@ impl ServiceConfig { ka_enabled, client_timeout, client_shutdown, - bytes: Rc::new(SharedBytesPool::new()), - messages: RequestPool::pool(), date: UnsafeCell::new((false, Date::new())), })) } @@ -83,23 +79,6 @@ impl ServiceConfig { self.0.ka_enabled } - pub(crate) fn get_bytes(&self) -> BytesMut { - self.0.bytes.get_bytes() - } - - pub(crate) fn release_bytes(&self, bytes: BytesMut) { - self.0.bytes.release_bytes(bytes) - } - - pub(crate) fn get_request(&self) -> Request { - RequestPool::get(self.0.messages) - } - - #[doc(hidden)] - pub fn request_pool(&self) -> &'static RequestPool { - self.0.messages - } - fn update_date(&self) { // Unsafe: WorkerSetting is !Sync and !Send unsafe { (*self.0.date.get()).0 = false }; @@ -341,31 +320,6 @@ impl fmt::Write for Date { } } -#[derive(Debug)] -pub(crate) struct SharedBytesPool(RefCell>); - -impl SharedBytesPool { - pub fn new() -> SharedBytesPool { - SharedBytesPool(RefCell::new(VecDeque::with_capacity(128))) - } - - pub fn get_bytes(&self) -> BytesMut { - if let Some(bytes) = self.0.borrow_mut().pop_front() { - bytes - } else { - BytesMut::new() - } - } - - pub fn release_bytes(&self, mut bytes: BytesMut) { - let v = &mut self.0.borrow_mut(); - if v.len() < 128 { - bytes.clear(); - v.push_front(bytes); - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/error.rs b/src/error.rs index ff2388de8..e39dea9b2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -377,62 +377,56 @@ impl ResponseError for cookie::ParseError { } } -/// A set of errors that can occur during parsing multipart streams -#[derive(Fail, Debug)] -pub enum MultipartError { - /// Content-Type header is not found - #[fail(display = "No Content-type header found")] - NoContentType, - /// Can not parse Content-Type header - #[fail(display = "Can not parse Content-Type header")] - ParseContentType, - /// Multipart boundary is not found - #[fail(display = "Multipart boundary is not found")] - Boundary, - /// Multipart stream is incomplete - #[fail(display = "Multipart stream is incomplete")] - Incomplete, - /// Error during field parsing - #[fail(display = "{}", _0)] - Parse(#[cause] ParseError), - /// Payload error - #[fail(display = "{}", _0)] - Payload(#[cause] PayloadError), +#[derive(Debug)] +/// A set of errors that can occur during dispatching http requests +pub enum DispatchError { + /// Service error + // #[fail(display = "Application specific error: {}", _0)] + Service(E), + + /// An `io::Error` that occurred while trying to read or write to a network + /// stream. + // #[fail(display = "IO error: {}", _0)] + Io(io::Error), + + /// Http request parse error. + // #[fail(display = "Parse error: {}", _0)] + Parse(ParseError), + + /// The first request did not complete within the specified timeout. + // #[fail(display = "The first request did not complete within the specified timeout")] + SlowRequestTimeout, + + /// Shutdown timeout + // #[fail(display = "Connection shutdown timeout")] + ShutdownTimeout, + + /// Payload is not consumed + // #[fail(display = "Task is completed but request's payload is not consumed")] + PayloadIsNotConsumed, + + /// Malformed request + // #[fail(display = "Malformed request")] + MalformedRequest, + + /// Internal error + // #[fail(display = "Internal error")] + InternalError, + + /// Unknown error + // #[fail(display = "Unknown error")] + Unknown, } -impl From for MultipartError { - fn from(err: ParseError) -> MultipartError { - MultipartError::Parse(err) +impl From for DispatchError { + fn from(err: ParseError) -> Self { + DispatchError::Parse(err) } } -impl From for MultipartError { - fn from(err: PayloadError) -> MultipartError { - MultipartError::Payload(err) - } -} - -/// Return `BadRequest` for `MultipartError` -impl ResponseError for MultipartError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// Error during handling `Expect` header -#[derive(Fail, PartialEq, Debug)] -pub enum ExpectError { - /// Expect header value can not be converted to utf8 - #[fail(display = "Expect header value can not be converted to utf8")] - Encoding, - /// Unknown expect value - #[fail(display = "Unknown expect value")] - UnknownExpect, -} - -impl ResponseError for ExpectError { - fn error_response(&self) -> HttpResponse { - HttpResponse::with_body(StatusCode::EXPECTATION_FAILED, "Unknown Expect") +impl From for DispatchError { + fn from(err: io::Error) -> Self { + DispatchError::Io(err) } } @@ -565,28 +559,6 @@ impl From for ReadlinesError { } } -/// Errors which can occur when attempting to interpret a segment string as a -/// valid path segment. -#[derive(Fail, Debug, PartialEq)] -pub enum UriSegmentError { - /// The segment started with the wrapped invalid character. - #[fail(display = "The segment started with the wrapped invalid character")] - BadStart(char), - /// The segment contained the wrapped invalid character. - #[fail(display = "The segment contained the wrapped invalid character")] - BadChar(char), - /// The segment ended with the wrapped invalid character. - #[fail(display = "The segment ended with the wrapped invalid character")] - BadEnd(char), -} - -/// Return `BadRequest` for `UriSegmentError` -impl ResponseError for UriSegmentError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - /// Errors which can occur when attempting to generate resource uri. #[derive(Fail, Debug, PartialEq)] pub enum UrlGenerationError { @@ -610,24 +582,6 @@ impl From for UrlGenerationError { } } -/// Errors which can occur when serving static files. -#[derive(Fail, Debug, PartialEq)] -pub enum StaticFileError { - /// Path is not a directory - #[fail(display = "Path is not a directory. Unable to serve static files")] - IsNotDirectory, - /// Cannot render directory - #[fail(display = "Unable to render directory without index file")] - IsDirectory, -} - -/// Return `NotFound` for `StaticFileError` -impl ResponseError for StaticFileError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::NOT_FOUND) - } -} - /// Helper type that can wrap any error and generate custom response. /// /// In following example any `io::Error` will be converted into "BAD REQUEST" diff --git a/src/server/h1codec.rs b/src/h1/codec.rs similarity index 93% rename from src/server/h1codec.rs rename to src/h1/codec.rs index ea56110d3..011883575 100644 --- a/src/server/h1codec.rs +++ b/src/h1/codec.rs @@ -4,37 +4,45 @@ use std::io::{self, Write}; use bytes::{BufMut, Bytes, BytesMut}; use tokio_codec::{Decoder, Encoder}; -use super::h1decoder::{H1Decoder, Message}; -use super::helpers; -use super::message::RequestPool; -use super::output::{ResponseInfo, ResponseLength}; +use super::decoder::H1Decoder; +pub use super::decoder::InMessage; use body::Body; use error::ParseError; +use helpers; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::Version; use httpresponse::HttpResponse; +use request::RequestPool; +use server::output::{ResponseInfo, ResponseLength}; -pub(crate) enum OutMessage { +pub enum OutMessage { Response(HttpResponse), Payload(Bytes), } -pub(crate) struct H1Codec { +/// HTTP/1 Codec +pub struct Codec { decoder: H1Decoder, encoder: H1Writer, } -impl H1Codec { - pub fn new(pool: &'static RequestPool) -> Self { - H1Codec { +impl Codec { + /// Create HTTP/1 codec + pub fn new() -> Self { + Codec::with_pool(RequestPool::pool()) + } + + /// Create HTTP/1 codec with request's pool + pub(crate) fn with_pool(pool: &'static RequestPool) -> Self { + Codec { decoder: H1Decoder::new(pool), encoder: H1Writer::new(), } } } -impl Decoder for H1Codec { - type Item = Message; +impl Decoder for Codec { + type Item = InMessage; type Error = ParseError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { @@ -42,7 +50,7 @@ impl Decoder for H1Codec { } } -impl Encoder for H1Codec { +impl Encoder for Codec { type Item = OutMessage; type Error = io::Error; diff --git a/src/server/h1decoder.rs b/src/h1/decoder.rs similarity index 97% rename from src/server/h1decoder.rs rename to src/h1/decoder.rs index c6f0974aa..47cc5fdf1 100644 --- a/src/server/h1decoder.rs +++ b/src/h1/decoder.rs @@ -4,10 +4,10 @@ use bytes::{Bytes, BytesMut}; use futures::{Async, Poll}; use httparse; -use super::message::{MessageFlags, Request, RequestPool}; use error::ParseError; use http::header::{HeaderName, HeaderValue}; use http::{header, HttpTryFrom, Method, Uri, Version}; +use request::{MessageFlags, Request, RequestPool}; use uri::Url; const MAX_BUFFER_SIZE: usize = 131_072; @@ -19,7 +19,7 @@ pub(crate) struct H1Decoder { } #[derive(Debug)] -pub enum Message { +pub enum InMessage { Message(Request), MessageWithPayload(Request), Chunk(Bytes), @@ -34,14 +34,16 @@ impl H1Decoder { } } - pub fn decode(&mut self, src: &mut BytesMut) -> Result, ParseError> { + pub fn decode( + &mut self, src: &mut BytesMut, + ) -> Result, ParseError> { // read payload if self.decoder.is_some() { match self.decoder.as_mut().unwrap().decode(src)? { - Async::Ready(Some(bytes)) => return Ok(Some(Message::Chunk(bytes))), + Async::Ready(Some(bytes)) => return Ok(Some(InMessage::Chunk(bytes))), Async::Ready(None) => { self.decoder.take(); - return Ok(Some(Message::Eof)); + return Ok(Some(InMessage::Eof)); } Async::NotReady => return Ok(None), } @@ -51,9 +53,9 @@ impl H1Decoder { Async::Ready((msg, decoder)) => { self.decoder = decoder; if self.decoder.is_some() { - Ok(Some(Message::MessageWithPayload(msg))) + Ok(Some(InMessage::MessageWithPayload(msg))) } else { - Ok(Some(Message::Message(msg))) + Ok(Some(InMessage::Message(msg))) } } Async::NotReady => { diff --git a/src/server/h1disp.rs b/src/h1/dispatcher.rs similarity index 86% rename from src/server/h1disp.rs rename to src/h1/dispatcher.rs index b1c2c8a21..6e5672d35 100644 --- a/src/server/h1disp.rs +++ b/src/h1/dispatcher.rs @@ -9,21 +9,20 @@ use actix_net::service::Service; use futures::{Async, AsyncSink, Future, Poll, Sink, Stream}; use tokio_codec::Framed; // use tokio_current_thread::spawn; -use tokio_io::AsyncWrite; +use tokio_io::{AsyncRead, AsyncWrite}; // use tokio_timer::Delay; use error::{ParseError, PayloadError}; use payload::{Payload, PayloadStatus, PayloadWriter}; use body::Body; +use error::DispatchError; use httpresponse::HttpResponse; -use super::error::HttpDispatchError; -use super::h1codec::{H1Codec, OutMessage}; -use super::h1decoder::Message; -use super::input::PayloadType; -use super::message::{Request, RequestPool}; -use super::IoStream; +use request::{Request, RequestPool}; +use server::input::PayloadType; + +use super::codec::{Codec, InMessage, OutMessage}; const MAX_PIPELINED_MESSAGES: usize = 16; @@ -41,15 +40,14 @@ bitflags! { } /// Dispatcher for HTTP/1.1 protocol -pub struct Http1Dispatcher +pub struct Dispatcher where S::Error: Debug + Display, { service: S, flags: Flags, - addr: Option, - framed: Framed, - error: Option>, + framed: Framed, + error: Option>, state: State, payload: Option, @@ -74,26 +72,24 @@ impl State { } } -impl Http1Dispatcher +impl Dispatcher where - T: IoStream, + T: AsyncRead + AsyncWrite, S: Service, S::Error: Debug + Display, { - pub fn new(stream: T, pool: &'static RequestPool, service: S) -> Self { - let addr = stream.peer_addr(); + /// Create http/1 dispatcher. + pub fn new(stream: T, service: S) -> Self { let flags = Flags::FLUSHED; - let codec = H1Codec::new(pool); - let framed = Framed::new(stream, codec); + let framed = Framed::new(stream, Codec::new()); - Http1Dispatcher { + Dispatcher { payload: None, state: State::None, error: None, messages: VecDeque::new(), service, flags, - addr, framed, } } @@ -141,7 +137,7 @@ where } /// Flush stream - fn poll_flush(&mut self) -> Poll<(), HttpDispatchError> { + fn poll_flush(&mut self) -> Poll<(), DispatchError> { if self.flags.contains(Flags::STARTED) && !self.flags.contains(Flags::FLUSHED) { match self.framed.poll_complete() { Ok(Async::NotReady) => Ok(Async::NotReady), @@ -153,7 +149,7 @@ where Ok(Async::Ready(_)) => { // if payload is not consumed we can not use connection if self.payload.is_some() && self.state.is_empty() { - return Err(HttpDispatchError::PayloadIsNotConsumed); + return Err(DispatchError::PayloadIsNotConsumed); } self.flags.insert(Flags::FLUSHED); Ok(Async::Ready(())) @@ -164,7 +160,7 @@ where } } - pub(self) fn poll_handler(&mut self) -> Result<(), HttpDispatchError> { + pub(self) fn poll_handler(&mut self) -> Result<(), DispatchError> { self.poll_io()?; let mut retry = self.can_read(); @@ -185,7 +181,7 @@ where } } Ok(Async::NotReady) => Some(Ok(State::Response(task))), - Err(err) => Some(Err(HttpDispatchError::App(err))), + Err(err) => Some(Err(DispatchError::Service(err))), } } else { None @@ -207,7 +203,7 @@ where Err(err) => { // it is not possible to recover from error // during pipe handling, so just drop connection - Some(Err(HttpDispatchError::App(err))) + Some(Err(DispatchError::Service(err))) } } } @@ -222,7 +218,7 @@ where *item = Some(msg); return Ok(()); } - Err(err) => Some(Err(HttpDispatchError::Io(err))), + Err(err) => Some(Err(DispatchError::Io(err))), } } State::SendResponseWithPayload(ref mut item) => { @@ -236,7 +232,7 @@ where *item = Some((msg, body)); return Ok(()); } - Err(err) => Some(Err(HttpDispatchError::Io(err))), + Err(err) => Some(Err(DispatchError::Io(err))), } } }; @@ -263,14 +259,11 @@ where Ok(()) } - fn one_message(&mut self, msg: Message) -> Result<(), HttpDispatchError> { + fn one_message(&mut self, msg: InMessage) -> Result<(), DispatchError> { self.flags.insert(Flags::STARTED); match msg { - Message::Message(mut msg) => { - // set remote addr - msg.inner_mut().addr = self.addr; - + InMessage::Message(msg) => { // handle request early if self.state.is_empty() { let mut task = self.service.call(msg); @@ -287,17 +280,14 @@ where Err(err) => { error!("Unhandled application error: {}", err); self.client_disconnected(false); - return Err(HttpDispatchError::App(err)); + return Err(DispatchError::Service(err)); } } } else { self.messages.push_back(msg); } } - Message::MessageWithPayload(mut msg) => { - // set remote addr - msg.inner_mut().addr = self.addr; - + InMessage::MessageWithPayload(msg) => { // payload let (ps, pl) = Payload::new(false); *msg.inner.payload.borrow_mut() = Some(pl); @@ -305,24 +295,24 @@ where self.messages.push_back(msg); } - Message::Chunk(chunk) => { + InMessage::Chunk(chunk) => { if let Some(ref mut payload) = self.payload { payload.feed_data(chunk); } else { error!("Internal server error: unexpected payload chunk"); self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); // self.push_response_entry(StatusCode::INTERNAL_SERVER_ERROR); - self.error = Some(HttpDispatchError::InternalError); + self.error = Some(DispatchError::InternalError); } } - Message::Eof => { + InMessage::Eof => { if let Some(mut payload) = self.payload.take() { payload.feed_eof(); } else { error!("Internal server error: unexpected eof"); self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); // self.push_response_entry(StatusCode::INTERNAL_SERVER_ERROR); - self.error = Some(HttpDispatchError::InternalError); + self.error = Some(DispatchError::InternalError); } } } @@ -330,7 +320,7 @@ where Ok(()) } - pub(self) fn poll_io(&mut self) -> Result> { + pub(self) fn poll_io(&mut self) -> Result> { let mut updated = false; if self.messages.len() < MAX_PIPELINED_MESSAGES { @@ -359,7 +349,7 @@ where // Malformed requests should be responded with 400 // self.push_response_entry(StatusCode::BAD_REQUEST); self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); - self.error = Some(HttpDispatchError::MalformedRequest); + self.error = Some(DispatchError::MalformedRequest); break; } } @@ -370,14 +360,14 @@ where } } -impl Future for Http1Dispatcher +impl Future for Dispatcher where - T: IoStream, + T: AsyncRead + AsyncWrite, S: Service, S::Error: Debug + Display, { type Item = (); - type Error = HttpDispatchError; + type Error = DispatchError; #[inline] fn poll(&mut self) -> Poll<(), Self::Error> { diff --git a/src/h1/mod.rs b/src/h1/mod.rs new file mode 100644 index 000000000..245f2fc23 --- /dev/null +++ b/src/h1/mod.rs @@ -0,0 +1,9 @@ +//! HTTP/1 implementation +mod codec; +mod decoder; +mod dispatcher; +mod service; + +pub use self::codec::Codec; +pub use self::dispatcher::Dispatcher; +pub use self::service::{H1Service, H1ServiceHandler}; diff --git a/src/h1/service.rs b/src/h1/service.rs new file mode 100644 index 000000000..3017a3ef7 --- /dev/null +++ b/src/h1/service.rs @@ -0,0 +1,125 @@ +use std::fmt::{Debug, Display}; +use std::marker::PhantomData; +use std::time::Duration; + +use actix_net::service::{IntoNewService, NewService, Service}; +use futures::future::{ok, FutureResult}; +use futures::{Async, Future, Poll}; +use tokio_io::{AsyncRead, AsyncWrite}; + +use config::ServiceConfig; +use error::DispatchError; +use httpresponse::HttpResponse; +use request::Request; + +use super::dispatcher::Dispatcher; + +/// `NewService` implementation for HTTP1 transport +pub struct H1Service { + srv: S, + cfg: ServiceConfig, + _t: PhantomData, +} + +impl H1Service +where + S: NewService, +{ + /// Create new `HttpService` instance. + pub fn new>(cfg: ServiceConfig, service: F) -> Self { + H1Service { + cfg, + srv: service.into_new_service(), + _t: PhantomData, + } + } +} + +impl NewService for H1Service +where + T: AsyncRead + AsyncWrite, + S: NewService + Clone, + S::Service: Clone, + S::Error: Debug + Display, +{ + type Request = T; + type Response = (); + type Error = DispatchError; + type InitError = S::InitError; + type Service = H1ServiceHandler; + type Future = H1ServiceResponse; + + fn new_service(&self) -> Self::Future { + H1ServiceResponse { + fut: self.srv.new_service(), + cfg: Some(self.cfg.clone()), + _t: PhantomData, + } + } +} + +pub struct H1ServiceResponse { + fut: S::Future, + cfg: Option, + _t: PhantomData, +} + +impl Future for H1ServiceResponse +where + T: AsyncRead + AsyncWrite, + S: NewService, + S::Service: Clone, + S::Error: Debug + Display, +{ + type Item = H1ServiceHandler; + type Error = S::InitError; + + fn poll(&mut self) -> Poll { + let service = try_ready!(self.fut.poll()); + Ok(Async::Ready(H1ServiceHandler::new( + self.cfg.take().unwrap(), + service, + ))) + } +} + +/// `Service` implementation for HTTP1 transport +pub struct H1ServiceHandler { + srv: S, + cfg: ServiceConfig, + _t: PhantomData, +} + +impl H1ServiceHandler +where + S: Service + Clone, + S::Error: Debug + Display, +{ + fn new(cfg: ServiceConfig, srv: S) -> H1ServiceHandler { + H1ServiceHandler { + srv, + cfg, + _t: PhantomData, + } + } +} + +impl Service for H1ServiceHandler +where + T: AsyncRead + AsyncWrite, + S: Service + Clone, + S::Error: Debug + Display, +{ + type Request = T; + type Response = (); + type Error = DispatchError; + type Future = Dispatcher; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.srv.poll_ready().map_err(|e| DispatchError::Service(e)) + } + + fn call(&mut self, req: Self::Request) -> Self::Future { + Dispatcher::new(req, self.srv.clone()) + } +} diff --git a/src/server/helpers.rs b/src/helpers.rs similarity index 100% rename from src/server/helpers.rs rename to src/helpers.rs diff --git a/src/lib.rs b/src/lib.rs index efd566187..ec86f0320 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -135,6 +135,7 @@ extern crate actix_net; extern crate serde_derive; mod body; +mod config; mod extensions; mod header; mod httpcodes; @@ -142,9 +143,12 @@ mod httpmessage; mod httpresponse; mod json; mod payload; +mod request; mod uri; pub mod error; +pub mod h1; +pub(crate) mod helpers; pub mod server; //pub mod test; //pub mod ws; @@ -152,10 +156,11 @@ pub use body::{Binary, Body}; pub use error::{Error, ResponseError, Result}; pub use extensions::Extensions; pub use httpmessage::HttpMessage; -//pub use httprequest::HttpRequest; pub use httpresponse::HttpResponse; pub use json::Json; -pub use server::Request; +pub use request::Request; + +pub use self::config::{ServiceConfig, ServiceConfigBuilder}; pub mod dev { //! The `actix-web` prelude for library developers diff --git a/src/server/message.rs b/src/request.rs similarity index 87% rename from src/server/message.rs rename to src/request.rs index c39302bab..a75fda3a0 100644 --- a/src/server/message.rs +++ b/src/request.rs @@ -30,7 +30,6 @@ pub(crate) struct InnerRequest { pub(crate) flags: Cell, pub(crate) headers: HeaderMap, pub(crate) extensions: RefCell, - pub(crate) addr: Option, pub(crate) payload: RefCell>, pub(crate) stream_extensions: Option>, pool: &'static RequestPool, @@ -65,8 +64,13 @@ impl HttpMessage for Request { } impl Request { - /// Create new RequestContext instance - pub(crate) fn new(pool: &'static RequestPool) -> Request { + /// Create new Request instance + pub fn new() -> Request { + Request::with_pool(RequestPool::pool()) + } + + /// Create new Request instance with pool + pub(crate) fn with_pool(pool: &'static RequestPool) -> Request { Request { inner: Rc::new(InnerRequest { pool, @@ -75,7 +79,6 @@ impl Request { version: Version::HTTP_11, headers: HeaderMap::with_capacity(16), flags: Cell::new(MessageFlags::empty()), - addr: None, payload: RefCell::new(None), extensions: RefCell::new(Extensions::new()), stream_extensions: None, @@ -134,14 +137,6 @@ impl Request { &mut self.inner_mut().headers } - /// Peer socket address - /// - /// Peer address is actual socket address, if proxy is used in front of - /// actix http server, then peer address would be address of this proxy. - pub fn peer_addr(&self) -> Option { - self.inner().addr - } - /// Checks if a connection should be kept alive. #[inline] pub fn keep_alive(&self) -> bool { @@ -170,12 +165,6 @@ impl Request { self.inner().method == Method::CONNECT } - /// Io stream extensions - #[inline] - pub fn stream_extensions(&self) -> Option<&Extensions> { - self.inner().stream_extensions.as_ref().map(|e| e.as_ref()) - } - pub(crate) fn clone(&self) -> Self { Request { inner: self.inner.clone(), @@ -213,7 +202,8 @@ impl fmt::Debug for Request { } } -pub struct RequestPool(RefCell>>); +/// Request's objects pool +pub(crate) struct RequestPool(RefCell>>); thread_local!(static POOL: &'static RequestPool = RequestPool::create()); @@ -223,16 +213,18 @@ impl RequestPool { Box::leak(Box::new(pool)) } - pub(crate) fn pool() -> &'static RequestPool { + /// Get default request's pool + pub fn pool() -> &'static RequestPool { POOL.with(|p| *p) } + /// Get Request object #[inline] pub fn get(pool: &'static RequestPool) -> Request { if let Some(msg) = pool.0.borrow_mut().pop_front() { Request { inner: msg } } else { - Request::new(pool) + Request::with_pool(pool) } } diff --git a/src/server/error.rs b/src/server/error.rs deleted file mode 100644 index 7d5c67d1e..000000000 --- a/src/server/error.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::fmt::{Debug, Display}; -use std::io; - -use futures::{Async, Poll}; - -use error::{Error, ParseError}; -use http::{StatusCode, Version}; - -/// Errors produced by `AcceptorError` service. -#[derive(Debug)] -pub enum AcceptorError { - /// The inner service error - Service(T), - - /// Io specific error - Io(io::Error), - - /// The request did not complete within the specified timeout. - Timeout, -} - -#[derive(Debug)] -/// A set of errors that can occur during dispatching http requests -pub enum HttpDispatchError { - /// Application error - // #[fail(display = "Application specific error: {}", _0)] - App(E), - - /// An `io::Error` that occurred while trying to read or write to a network - /// stream. - // #[fail(display = "IO error: {}", _0)] - Io(io::Error), - - /// Http request parse error. - // #[fail(display = "Parse error: {}", _0)] - Parse(ParseError), - - /// The first request did not complete within the specified timeout. - // #[fail(display = "The first request did not complete within the specified timeout")] - SlowRequestTimeout, - - /// Shutdown timeout - // #[fail(display = "Connection shutdown timeout")] - ShutdownTimeout, - - /// Payload is not consumed - // #[fail(display = "Task is completed but request's payload is not consumed")] - PayloadIsNotConsumed, - - /// Malformed request - // #[fail(display = "Malformed request")] - MalformedRequest, - - /// Internal error - // #[fail(display = "Internal error")] - InternalError, - - /// Unknown error - // #[fail(display = "Unknown error")] - Unknown, -} - -impl From for HttpDispatchError { - fn from(err: ParseError) -> Self { - HttpDispatchError::Parse(err) - } -} - -impl From for HttpDispatchError { - fn from(err: io::Error) -> Self { - HttpDispatchError::Io(err) - } -} diff --git a/src/server/h1.rs b/src/server/h1.rs deleted file mode 100644 index e2b4bf45e..000000000 --- a/src/server/h1.rs +++ /dev/null @@ -1,1289 +0,0 @@ -use std::collections::VecDeque; -use std::net::{Shutdown, SocketAddr}; -use std::time::{Duration, Instant}; - -use bytes::BytesMut; -use futures::{Async, Future, Poll}; -use tokio_current_thread::spawn; -use tokio_timer::Delay; - -use error::{Error, ParseError, PayloadError}; -use http::{StatusCode, Version}; -use payload::{Payload, PayloadStatus, PayloadWriter}; - -use super::error::{HttpDispatchError, ServerError}; -use super::h1decoder::{H1Decoder, Message}; -use super::h1writer::H1Writer; -use super::handler::{HttpHandler, HttpHandlerTask, HttpHandlerTaskFut}; -use super::input::PayloadType; -use super::message::Request; -use super::settings::ServiceConfig; -use super::{IoStream, Writer}; - -const MAX_PIPELINED_MESSAGES: usize = 16; - -bitflags! { - pub struct Flags: u8 { - const STARTED = 0b0000_0001; - const KEEPALIVE_ENABLED = 0b0000_0010; - const KEEPALIVE = 0b0000_0100; - const SHUTDOWN = 0b0000_1000; - const READ_DISCONNECTED = 0b0001_0000; - const WRITE_DISCONNECTED = 0b0010_0000; - const POLLED = 0b0100_0000; - const FLUSHED = 0b1000_0000; - } -} - -/// Dispatcher for HTTP/1.1 protocol -pub struct Http1Dispatcher { - flags: Flags, - settings: ServiceConfig, - addr: Option, - stream: H1Writer, - decoder: H1Decoder, - payload: Option, - buf: BytesMut, - tasks: VecDeque>, - error: Option>, - ka_expire: Instant, - ka_timer: Option, -} - -enum Entry { - Task(H::Task), - Error(Box), -} - -impl Entry { - fn into_task(self) -> H::Task { - match self { - Entry::Task(task) => task, - Entry::Error(_) => panic!(), - } - } - fn disconnected(&mut self) { - match *self { - Entry::Task(ref mut task) => task.disconnected(), - Entry::Error(ref mut task) => task.disconnected(), - } - } - fn poll_io(&mut self, io: &mut Writer) -> Poll { - match *self { - Entry::Task(ref mut task) => task.poll_io(io), - Entry::Error(ref mut task) => task.poll_io(io), - } - } - fn poll_completed(&mut self) -> Poll<(), Error> { - match *self { - Entry::Task(ref mut task) => task.poll_completed(), - Entry::Error(ref mut task) => task.poll_completed(), - } - } -} - -impl Http1Dispatcher -where - T: IoStream, - H: HttpHandler + 'static, -{ - pub fn new( - settings: ServiceConfig, stream: T, buf: BytesMut, is_eof: bool, - keepalive_timer: Option, - ) -> Self { - let addr = stream.peer_addr(); - let (ka_expire, ka_timer) = if let Some(delay) = keepalive_timer { - (delay.deadline(), Some(delay)) - } else if let Some(delay) = settings.keep_alive_timer() { - (delay.deadline(), Some(delay)) - } else { - (settings.now(), None) - }; - - let flags = if is_eof { - Flags::READ_DISCONNECTED | Flags::FLUSHED - } else if settings.keep_alive_enabled() { - Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED | Flags::FLUSHED - } else { - Flags::empty() - }; - - Http1Dispatcher { - stream: H1Writer::new(stream, settings.clone()), - decoder: H1Decoder::new(settings.request_pool()), - payload: None, - tasks: VecDeque::new(), - error: None, - flags, - addr, - buf, - settings, - ka_timer, - ka_expire, - } - } - - pub(crate) fn for_error( - settings: ServiceConfig, stream: T, status: StatusCode, - mut keepalive_timer: Option, buf: BytesMut, - ) -> Self { - if let Some(deadline) = settings.client_timer_expire() { - let _ = keepalive_timer.as_mut().map(|delay| delay.reset(deadline)); - } - - let mut disp = Http1Dispatcher { - flags: Flags::STARTED | Flags::READ_DISCONNECTED | Flags::FLUSHED, - stream: H1Writer::new(stream, settings.clone()), - decoder: H1Decoder::new(settings.request_pool()), - payload: None, - tasks: VecDeque::new(), - error: None, - addr: None, - ka_timer: keepalive_timer, - ka_expire: settings.now(), - buf, - settings, - }; - disp.push_response_entry(status); - disp - } - - #[inline] - pub fn settings(&self) -> &ServiceConfig { - &self.settings - } - - #[inline] - pub(crate) fn io(&mut self) -> &mut T { - self.stream.get_mut() - } - - #[inline] - fn can_read(&self) -> bool { - if self.flags.contains(Flags::READ_DISCONNECTED) { - return false; - } - - if let Some(ref info) = self.payload { - info.need_read() == PayloadStatus::Read - } else { - true - } - } - - // if checked is set to true, delay disconnect until all tasks have finished. - fn client_disconnected(&mut self, checked: bool) { - self.flags.insert(Flags::READ_DISCONNECTED); - if let Some(mut payload) = self.payload.take() { - payload.set_error(PayloadError::Incomplete); - } - - if !checked || self.tasks.is_empty() { - self.flags - .insert(Flags::WRITE_DISCONNECTED | Flags::FLUSHED); - self.stream.disconnected(); - - // notify all tasks - for mut task in self.tasks.drain(..) { - task.disconnected(); - match task.poll_completed() { - Ok(Async::NotReady) => { - // spawn not completed task, it does not require access to io - // at this point - spawn(HttpHandlerTaskFut::new(task.into_task())); - } - Ok(Async::Ready(_)) => (), - Err(err) => { - error!("Unhandled application error: {}", err); - } - } - } - } - } - - #[inline] - pub fn poll(&mut self) -> Poll<(), HttpDispatchError> { - // check connection keep-alive - self.poll_keep_alive()?; - - // shutdown - if self.flags.contains(Flags::SHUTDOWN) { - if self.flags.contains(Flags::WRITE_DISCONNECTED) { - return Ok(Async::Ready(())); - } - return self.poll_flush(true); - } - - // process incoming requests - if !self.flags.contains(Flags::WRITE_DISCONNECTED) { - self.poll_handler()?; - - // flush stream - self.poll_flush(false)?; - - // deal with keep-alive and stream eof (client-side write shutdown) - if self.tasks.is_empty() && self.flags.contains(Flags::FLUSHED) { - // handle stream eof - if self - .flags - .intersects(Flags::READ_DISCONNECTED | Flags::WRITE_DISCONNECTED) - { - return Ok(Async::Ready(())); - } - // no keep-alive - if self.flags.contains(Flags::STARTED) - && (!self.flags.contains(Flags::KEEPALIVE_ENABLED) - || !self.flags.contains(Flags::KEEPALIVE)) - { - self.flags.insert(Flags::SHUTDOWN); - return self.poll(); - } - } - Ok(Async::NotReady) - } else if let Some(err) = self.error.take() { - Err(err) - } else { - Ok(Async::Ready(())) - } - } - - /// Flush stream - fn poll_flush(&mut self, shutdown: bool) -> Poll<(), HttpDispatchError> { - if shutdown || self.flags.contains(Flags::STARTED) { - match self.stream.poll_completed(shutdown) { - Ok(Async::NotReady) => { - // mark stream - if !self.stream.flushed() { - self.flags.remove(Flags::FLUSHED); - } - Ok(Async::NotReady) - } - Err(err) => { - debug!("Error sending data: {}", err); - self.client_disconnected(false); - Err(err.into()) - } - Ok(Async::Ready(_)) => { - // if payload is not consumed we can not use connection - if self.payload.is_some() && self.tasks.is_empty() { - return Err(HttpDispatchError::PayloadIsNotConsumed); - } - self.flags.insert(Flags::FLUSHED); - Ok(Async::Ready(())) - } - } - } else { - Ok(Async::Ready(())) - } - } - - /// keep-alive timer. returns `true` is keep-alive, otherwise drop - fn poll_keep_alive(&mut self) -> Result<(), HttpDispatchError> { - if let Some(ref mut timer) = self.ka_timer { - match timer.poll() { - Ok(Async::Ready(_)) => { - if timer.deadline() >= self.ka_expire { - // check for any outstanding request handling - if self.tasks.is_empty() { - // if we get timer during shutdown, just drop connection - if self.flags.contains(Flags::SHUTDOWN) { - let io = self.stream.get_mut(); - let _ = IoStream::set_linger( - io, - Some(Duration::from_secs(0)), - ); - let _ = IoStream::shutdown(io, Shutdown::Both); - return Err(HttpDispatchError::ShutdownTimeout); - } else if !self.flags.contains(Flags::STARTED) { - // timeout on first request (slow request) return 408 - trace!("Slow request timeout"); - self.flags - .insert(Flags::STARTED | Flags::READ_DISCONNECTED); - self.tasks.push_back(Entry::Error(ServerError::err( - Version::HTTP_11, - StatusCode::REQUEST_TIMEOUT, - ))); - } else { - trace!("Keep-alive timeout, close connection"); - self.flags.insert(Flags::SHUTDOWN); - - // start shutdown timer - if let Some(deadline) = - self.settings.client_shutdown_timer() - { - timer.reset(deadline) - } else { - return Ok(()); - } - } - } else if let Some(deadline) = self.settings.keep_alive_expire() - { - timer.reset(deadline) - } - } else { - timer.reset(self.ka_expire) - } - } - Ok(Async::NotReady) => (), - Err(e) => { - error!("Timer error {:?}", e); - return Err(HttpDispatchError::Unknown); - } - } - } - - Ok(()) - } - - #[inline] - /// read data from the stream - pub(self) fn poll_io(&mut self) -> Result> { - if !self.flags.contains(Flags::POLLED) { - self.flags.insert(Flags::POLLED); - if !self.buf.is_empty() { - let updated = self.parse()?; - return Ok(updated); - } - } - - // read io from socket - let mut updated = false; - if self.can_read() && self.tasks.len() < MAX_PIPELINED_MESSAGES { - match self.stream.get_mut().read_available(&mut self.buf) { - Ok(Async::Ready((read_some, disconnected))) => { - if read_some && self.parse()? { - updated = true; - } - if disconnected { - self.client_disconnected(true); - } - } - Ok(Async::NotReady) => (), - Err(err) => { - self.client_disconnected(false); - return Err(err.into()); - } - } - } - Ok(updated) - } - - pub(self) fn poll_handler(&mut self) -> Result<(), HttpDispatchError> { - self.poll_io()?; - let mut retry = self.can_read(); - - // process first pipelined response, only first task can do io operation in http/1 - while !self.tasks.is_empty() { - match self.tasks[0].poll_io(&mut self.stream) { - Ok(Async::Ready(ready)) => { - // override keep-alive state - if self.stream.keepalive() { - self.flags.insert(Flags::KEEPALIVE); - } else { - self.flags.remove(Flags::KEEPALIVE); - } - // prepare stream for next response - self.stream.reset(); - - let task = self.tasks.pop_front().unwrap(); - if !ready { - // task is done with io operations but still needs to do more work - spawn(HttpHandlerTaskFut::new(task.into_task())); - } - } - Ok(Async::NotReady) => { - // check if we need timer - if self.ka_timer.is_some() && self.stream.upgrade() { - self.ka_timer.take(); - } - - // if read-backpressure is enabled and we consumed some data. - // we may read more dataand retry - if !retry && self.can_read() && self.poll_io()? { - retry = self.can_read(); - continue; - } - break; - } - Err(err) => { - error!("Unhandled error1: {}", err); - // it is not possible to recover from error - // during pipe handling, so just drop connection - self.client_disconnected(false); - return Err(HttpDispatchError::App(err)); - } - } - } - - // check in-flight messages. all tasks must be alive, - // they need to produce response. if app returned error - // and we can not continue processing incoming requests. - let mut idx = 1; - while idx < self.tasks.len() { - let stop = match self.tasks[idx].poll_completed() { - Ok(Async::NotReady) => false, - Ok(Async::Ready(_)) => true, - Err(err) => { - self.error = Some(HttpDispatchError::App(err)); - true - } - }; - if stop { - // error in task handling or task is completed, - // so no response for this task which means we can not read more requests - // because pipeline sequence is broken. - // but we can safely complete existing tasks - self.flags.insert(Flags::READ_DISCONNECTED); - - for mut task in self.tasks.drain(idx..) { - task.disconnected(); - match task.poll_completed() { - Ok(Async::NotReady) => { - // spawn not completed task, it does not require access to io - // at this point - spawn(HttpHandlerTaskFut::new(task.into_task())); - } - Ok(Async::Ready(_)) => (), - Err(err) => { - error!("Unhandled application error: {}", err); - } - } - } - break; - } else { - idx += 1; - } - } - - Ok(()) - } - - fn push_response_entry(&mut self, status: StatusCode) { - self.tasks - .push_back(Entry::Error(ServerError::err(Version::HTTP_11, status))); - } - - fn handle_message( - &mut self, mut msg: Request, payload: bool, - ) -> Result<(), HttpDispatchError> { - self.flags.insert(Flags::STARTED); - - if payload { - let (ps, pl) = Payload::new(false); - *msg.inner.payload.borrow_mut() = Some(pl); - self.payload = Some(PayloadType::new(&msg.inner.headers, ps)); - } - - // stream extensions - msg.inner_mut().stream_extensions = self.stream.get_mut().extensions(); - - // set remote addr - msg.inner_mut().addr = self.addr; - - // search handler for request - match self.settings.handler().handle(msg) { - Ok(mut task) => { - if self.tasks.is_empty() { - match task.poll_io(&mut self.stream) { - Ok(Async::Ready(ready)) => { - // override keep-alive state - if self.stream.keepalive() { - self.flags.insert(Flags::KEEPALIVE); - } else { - self.flags.remove(Flags::KEEPALIVE); - } - // prepare stream for next response - self.stream.reset(); - - if !ready { - // task is done with io operations - // but still needs to do more work - spawn(HttpHandlerTaskFut::new(task)); - } - } - Ok(Async::NotReady) => (), - Err(err) => { - error!("Unhandled error: {}", err); - self.client_disconnected(false); - return Err(HttpDispatchError::App(err)); - } - } - } else { - self.tasks.push_back(Entry::Task(task)); - } - } - Err(_) => { - // handler is not found - self.push_response_entry(StatusCode::NOT_FOUND); - } - } - Ok(()) - } - - pub(self) fn parse(&mut self) -> Result> { - let mut updated = false; - - 'outer: loop { - match self.decoder.decode(&mut self.buf) { - Ok(Some(Message::Message(msg))) => { - updated = true; - self.handle_message(msg, false)?; - } - Ok(Some(Message::MessageWithPayload(msg))) => { - updated = true; - self.handle_message(msg, true)?; - } - Ok(Some(Message::Chunk(chunk))) => { - updated = true; - if let Some(ref mut payload) = self.payload { - payload.feed_data(chunk); - } else { - error!("Internal server error: unexpected payload chunk"); - self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); - self.push_response_entry(StatusCode::INTERNAL_SERVER_ERROR); - self.error = Some(HttpDispatchError::InternalError); - break; - } - } - Ok(Some(Message::Eof)) => { - updated = true; - if let Some(mut payload) = self.payload.take() { - payload.feed_eof(); - } else { - error!("Internal server error: unexpected eof"); - self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); - self.push_response_entry(StatusCode::INTERNAL_SERVER_ERROR); - self.error = Some(HttpDispatchError::InternalError); - break; - } - } - Ok(None) => { - if self.flags.contains(Flags::READ_DISCONNECTED) { - self.client_disconnected(true); - } - break; - } - Err(e) => { - if let Some(mut payload) = self.payload.take() { - let e = match e { - ParseError::Io(e) => PayloadError::Io(e), - _ => PayloadError::EncodingCorrupted, - }; - payload.set_error(e); - } - - // Malformed requests should be responded with 400 - self.push_response_entry(StatusCode::BAD_REQUEST); - self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); - self.error = Some(HttpDispatchError::MalformedRequest); - break; - } - } - } - - if self.ka_timer.is_some() && updated { - if let Some(expire) = self.settings.keep_alive_expire() { - self.ka_expire = expire; - } - } - Ok(updated) - } -} - -#[cfg(test)] -mod tests { - use std::net::Shutdown; - use std::{cmp, io, time}; - - use actix::System; - use bytes::{Buf, Bytes, BytesMut}; - use futures::future; - use http::{Method, Version}; - use tokio_io::{AsyncRead, AsyncWrite}; - - use super::*; - use application::{App, HttpApplication}; - use error::ParseError; - use httpmessage::HttpMessage; - use server::h1decoder::Message; - use server::handler::IntoHttpHandler; - use server::settings::{ServerSettings, ServiceConfig}; - use server::{KeepAlive, Request}; - - fn wrk_settings() -> ServiceConfig { - ServiceConfig::::new( - App::new().into_handler(), - KeepAlive::Os, - 5000, - 2000, - ServerSettings::default(), - ) - } - - impl Message { - fn message(self) -> Request { - match self { - Message::Message(msg) => msg, - Message::MessageWithPayload(msg) => msg, - _ => panic!("error"), - } - } - fn is_payload(&self) -> bool { - match *self { - Message::MessageWithPayload(_) => true, - _ => panic!("error"), - } - } - fn chunk(self) -> Bytes { - match self { - Message::Chunk(chunk) => chunk, - _ => panic!("error"), - } - } - fn eof(&self) -> bool { - match *self { - Message::Eof => true, - _ => false, - } - } - } - - macro_rules! parse_ready { - ($e:expr) => {{ - let settings = wrk_settings(); - match H1Decoder::new(settings.request_pool()).decode($e) { - Ok(Some(msg)) => msg.message(), - Ok(_) => unreachable!("Eof during parsing http request"), - Err(err) => unreachable!("Error during parsing http request: {:?}", err), - } - }}; - } - - macro_rules! expect_parse_err { - ($e:expr) => {{ - let settings = wrk_settings(); - - match H1Decoder::new(settings.request_pool()).decode($e) { - Err(err) => match err { - ParseError::Io(_) => unreachable!("Parse error expected"), - _ => (), - }, - _ => unreachable!("Error expected"), - } - }}; - } - - struct Buffer { - buf: Bytes, - err: Option, - } - - impl Buffer { - fn new(data: &'static str) -> Buffer { - Buffer { - buf: Bytes::from(data), - err: None, - } - } - } - - impl AsyncRead for Buffer {} - impl io::Read for Buffer { - fn read(&mut self, dst: &mut [u8]) -> Result { - if self.buf.is_empty() { - if self.err.is_some() { - Err(self.err.take().unwrap()) - } else { - Err(io::Error::new(io::ErrorKind::WouldBlock, "")) - } - } else { - let size = cmp::min(self.buf.len(), dst.len()); - let b = self.buf.split_to(size); - dst[..size].copy_from_slice(&b); - Ok(size) - } - } - } - - impl IoStream for Buffer { - fn shutdown(&mut self, _: Shutdown) -> io::Result<()> { - Ok(()) - } - fn set_nodelay(&mut self, _: bool) -> io::Result<()> { - Ok(()) - } - fn set_linger(&mut self, _: Option) -> io::Result<()> { - Ok(()) - } - fn set_keepalive(&mut self, _: Option) -> io::Result<()> { - Ok(()) - } - } - impl io::Write for Buffer { - fn write(&mut self, buf: &[u8]) -> io::Result { - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } - } - impl AsyncWrite for Buffer { - fn shutdown(&mut self) -> Poll<(), io::Error> { - Ok(Async::Ready(())) - } - fn write_buf(&mut self, _: &mut B) -> Poll { - Ok(Async::NotReady) - } - } - - #[test] - fn test_req_parse_err() { - let mut sys = System::new("test"); - let _ = sys.block_on(future::lazy(|| { - let buf = Buffer::new("GET /test HTTP/1\r\n\r\n"); - let readbuf = BytesMut::new(); - let settings = wrk_settings(); - - let mut h1 = - Http1Dispatcher::new(settings.clone(), buf, readbuf, false, None); - assert!(h1.poll_io().is_ok()); - assert!(h1.poll_io().is_ok()); - assert!(h1.flags.contains(Flags::READ_DISCONNECTED)); - assert_eq!(h1.tasks.len(), 1); - future::ok::<_, ()>(()) - })); - } - - #[test] - fn test_parse() { - let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(settings.request_pool()); - match reader.decode(&mut buf) { - Ok(Some(msg)) => { - let req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_partial() { - let mut buf = BytesMut::from("PUT /test HTTP/1"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(settings.request_pool()); - match reader.decode(&mut buf) { - Ok(None) => (), - _ => unreachable!("Error"), - } - - buf.extend(b".1\r\n\r\n"); - match reader.decode(&mut buf) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::PUT); - assert_eq!(req.path(), "/test"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_post() { - let mut buf = BytesMut::from("POST /test2 HTTP/1.0\r\n\r\n"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(settings.request_pool()); - match reader.decode(&mut buf) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_10); - assert_eq!(*req.method(), Method::POST); - assert_eq!(req.path(), "/test2"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_body() { - let mut buf = - BytesMut::from("GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(settings.request_pool()); - match reader.decode(&mut buf) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - assert_eq!( - reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), - b"body" - ); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_body_crlf() { - let mut buf = - BytesMut::from("\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(settings.request_pool()); - match reader.decode(&mut buf) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - assert_eq!( - reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), - b"body" - ); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_partial_eof() { - let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); - let settings = wrk_settings(); - let mut reader = H1Decoder::new(settings.request_pool()); - assert!(reader.decode(&mut buf).unwrap().is_none()); - - buf.extend(b"\r\n"); - match reader.decode(&mut buf) { - Ok(Some(msg)) => { - let req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_headers_split_field() { - let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(settings.request_pool()); - assert!{ reader.decode(&mut buf).unwrap().is_none() } - - buf.extend(b"t"); - assert!{ reader.decode(&mut buf).unwrap().is_none() } - - buf.extend(b"es"); - assert!{ reader.decode(&mut buf).unwrap().is_none() } - - buf.extend(b"t: value\r\n\r\n"); - match reader.decode(&mut buf) { - Ok(Some(msg)) => { - let req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - assert_eq!(req.headers().get("test").unwrap().as_bytes(), b"value"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_headers_multi_value() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - Set-Cookie: c1=cookie1\r\n\ - Set-Cookie: c2=cookie2\r\n\r\n", - ); - let settings = wrk_settings(); - let mut reader = H1Decoder::new(settings.request_pool()); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - let req = msg.message(); - - let val: Vec<_> = req - .headers() - .get_all("Set-Cookie") - .iter() - .map(|v| v.to_str().unwrap().to_owned()) - .collect(); - assert_eq!(val[0], "c1=cookie1"); - assert_eq!(val[1], "c2=cookie2"); - } - - #[test] - fn test_conn_default_1_0() { - let mut buf = BytesMut::from("GET /test HTTP/1.0\r\n\r\n"); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - } - - #[test] - fn test_conn_default_1_1() { - let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); - let req = parse_ready!(&mut buf); - - assert!(req.keep_alive()); - } - - #[test] - fn test_conn_close() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - connection: close\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - } - - #[test] - fn test_conn_close_1_0() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.0\r\n\ - connection: close\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - } - - #[test] - fn test_conn_keep_alive_1_0() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.0\r\n\ - connection: keep-alive\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.keep_alive()); - } - - #[test] - fn test_conn_keep_alive_1_1() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - connection: keep-alive\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.keep_alive()); - } - - #[test] - fn test_conn_other_1_0() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.0\r\n\ - connection: other\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - } - - #[test] - fn test_conn_other_1_1() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - connection: other\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.keep_alive()); - } - - #[test] - fn test_conn_upgrade() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - upgrade: websockets\r\n\ - connection: upgrade\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.upgrade()); - } - - #[test] - fn test_conn_upgrade_connect_method() { - let mut buf = BytesMut::from( - "CONNECT /test HTTP/1.1\r\n\ - content-type: text/plain\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.upgrade()); - } - - #[test] - fn test_request_chunked() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - if let Ok(val) = req.chunked() { - assert!(val); - } else { - unreachable!("Error"); - } - - // type in chunked - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chnked\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - if let Ok(val) = req.chunked() { - assert!(!val); - } else { - unreachable!("Error"); - } - } - - #[test] - fn test_headers_content_length_err_1() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - content-length: line\r\n\r\n", - ); - - expect_parse_err!(&mut buf) - } - - #[test] - fn test_headers_content_length_err_2() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - content-length: -1\r\n\r\n", - ); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_invalid_header() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - test line\r\n\r\n", - ); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_invalid_name() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - test[]: line\r\n\r\n", - ); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_http_request_bad_status_line() { - let mut buf = BytesMut::from("getpath \r\n\r\n"); - expect_parse_err!(&mut buf); - } - - #[test] - fn test_http_request_upgrade() { - let settings = wrk_settings(); - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - connection: upgrade\r\n\ - upgrade: websocket\r\n\r\n\ - some raw data", - ); - let mut reader = H1Decoder::new(settings.request_pool()); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); - assert!(!req.keep_alive()); - assert!(req.upgrade()); - assert_eq!( - reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), - b"some raw data" - ); - } - - #[test] - fn test_http_request_parser_utf8() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - x-test: теÑÑ‚\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert_eq!( - req.headers().get("x-test").unwrap().as_bytes(), - "теÑÑ‚".as_bytes() - ); - } - - #[test] - fn test_http_request_parser_two_slashes() { - let mut buf = BytesMut::from("GET //path HTTP/1.1\r\n\r\n"); - let req = parse_ready!(&mut buf); - - assert_eq!(req.path(), "//path"); - } - - #[test] - fn test_http_request_parser_bad_method() { - let mut buf = BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n"); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_http_request_parser_bad_version() { - let mut buf = BytesMut::from("GET //get HT/11\r\n\r\n"); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_http_request_chunked_payload() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let settings = wrk_settings(); - let mut reader = H1Decoder::new(settings.request_pool()); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); - assert!(req.chunked().unwrap()); - - buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); - assert_eq!( - reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), - b"data" - ); - assert_eq!( - reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), - b"line" - ); - assert!(reader.decode(&mut buf).unwrap().unwrap().eof()); - } - - #[test] - fn test_http_request_chunked_payload_and_next_message() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let settings = wrk_settings(); - let mut reader = H1Decoder::new(settings.request_pool()); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); - assert!(req.chunked().unwrap()); - - buf.extend( - b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\ - POST /test2 HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n" - .iter(), - ); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"data"); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"line"); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert!(msg.eof()); - - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert!(msg.is_payload()); - let req2 = msg.message(); - assert!(req2.chunked().unwrap()); - assert_eq!(*req2.method(), Method::POST); - assert!(req2.chunked().unwrap()); - } - - #[test] - fn test_http_request_chunked_payload_chunks() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(settings.request_pool()); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); - assert!(req.chunked().unwrap()); - - buf.extend(b"4\r\n1111\r\n"); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"1111"); - - buf.extend(b"4\r\ndata\r"); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"data"); - - buf.extend(b"\n4"); - assert!(reader.decode(&mut buf).unwrap().is_none()); - - buf.extend(b"\r"); - assert!(reader.decode(&mut buf).unwrap().is_none()); - buf.extend(b"\n"); - assert!(reader.decode(&mut buf).unwrap().is_none()); - - buf.extend(b"li"); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"li"); - - //trailers - //buf.feed_data("test: test\r\n"); - //not_ready!(reader.parse(&mut buf, &mut readbuf)); - - buf.extend(b"ne\r\n0\r\n"); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"ne"); - assert!(reader.decode(&mut buf).unwrap().is_none()); - - buf.extend(b"\r\n"); - assert!(reader.decode(&mut buf).unwrap().unwrap().eof()); - } - - #[test] - fn test_parse_chunked_payload_chunk_extension() { - let mut buf = BytesMut::from( - &"GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n"[..], - ); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(settings.request_pool()); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert!(msg.is_payload()); - assert!(msg.message().chunked().unwrap()); - - buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n") - let chunk = reader.decode(&mut buf).unwrap().unwrap().chunk(); - assert_eq!(chunk, Bytes::from_static(b"data")); - let chunk = reader.decode(&mut buf).unwrap().unwrap().chunk(); - assert_eq!(chunk, Bytes::from_static(b"line")); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert!(msg.eof()); - } -} diff --git a/src/server/mod.rs b/src/server/mod.rs index be172e646..068094c2a 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -117,30 +117,11 @@ use tokio_tcp::TcpStream; pub use actix_net::server::{PauseServer, ResumeServer, StopServer}; -mod error; -// pub(crate) mod h1; -#[doc(hidden)] -pub mod h1codec; -#[doc(hidden)] -pub mod h1decoder; -pub(crate) mod helpers; pub(crate) mod input; -pub(crate) mod message; pub(crate) mod output; -// pub(crate) mod service; -pub(crate) mod settings; - -pub use self::error::{AcceptorError, HttpDispatchError}; -pub use self::message::Request; #[doc(hidden)] -pub mod h1disp; - -#[doc(hidden)] -pub use self::settings::{ServiceConfig, ServiceConfigBuilder}; - -#[doc(hidden)] -pub use self::helpers::write_content_length; +pub use super::helpers::write_content_length; use body::Binary; use extensions::Extensions; diff --git a/src/server/output.rs b/src/server/output.rs index 143ba4029..f20bd3266 100644 --- a/src/server/output.rs +++ b/src/server/output.rs @@ -13,10 +13,10 @@ use flate2::Compression; use http::header::{HeaderValue, ACCEPT_ENCODING, CONTENT_LENGTH}; use http::{StatusCode, Version}; -use super::message::InnerRequest; use body::{Binary, Body}; use header::ContentEncoding; use httpresponse::HttpResponse; +use request::InnerRequest; // #[derive(Debug)] // pub(crate) struct RequestInfo { diff --git a/tests/test_h1v2.rs b/tests/test_h1v2.rs index 77b6d202f..e32481bc2 100644 --- a/tests/test_h1v2.rs +++ b/tests/test_h1v2.rs @@ -12,9 +12,8 @@ use actix_net::service::{IntoNewService, IntoService}; use actix_web::{client, test}; use futures::future; -use actix_http::server::h1disp::Http1Dispatcher; -use actix_http::server::{KeepAlive, ServiceConfig}; -use actix_http::{Error, HttpResponse}; +use actix_http::server::KeepAlive; +use actix_http::{h1, Error, HttpResponse, ServiceConfig}; #[test] fn test_h1_v2() { @@ -30,17 +29,10 @@ fn test_h1_v2() { .server_address(addr) .finish(); - (move |io| { - let pool = settings.request_pool(); - Http1Dispatcher::new( - io, - pool, - (|req| { - println!("REQ: {:?}", req); - future::ok::<_, Error>(HttpResponse::Ok().finish()) - }).into_service(), - ) - }).into_new_service() + h1::H1Service::new(settings, |req| { + println!("REQ: {:?}", req); + future::ok::<_, Error>(HttpResponse::Ok().finish()) + }) }).unwrap() .run(); }); From 829dbae609205d9245c927c6234d346957a1f94f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 4 Oct 2018 21:14:18 -0700 Subject: [PATCH 005/427] cleanups and tests --- Cargo.toml | 2 +- src/body.rs | 1 - src/config.rs | 17 +- src/error.rs | 38 +- src/h1/codec.rs | 5 +- src/h1/decoder.rs | 666 ++++++++++++++++++++- src/h1/dispatcher.rs | 14 +- src/h1/mod.rs | 2 +- src/h1/service.rs | 4 +- src/header/common/accept.rs | 18 +- src/header/common/accept_charset.rs | 18 +- src/header/common/accept_language.rs | 12 +- src/header/common/allow.rs | 19 +- src/header/common/cache_control.rs | 10 +- src/header/common/content_disposition.rs | 9 +- src/header/common/content_language.rs | 12 +- src/header/common/content_type.rs | 10 +- src/header/common/date.rs | 4 +- src/header/common/etag.rs | 8 +- src/header/common/expires.rs | 4 +- src/header/common/if_match.rs | 8 +- src/header/common/if_modified_since.rs | 4 +- src/header/common/if_none_match.rs | 8 +- src/header/common/if_range.rs | 14 +- src/header/common/if_unmodified_since.rs | 4 +- src/header/common/last_modified.rs | 4 +- src/httpmessage.rs | 6 +- src/httpresponse.rs | 121 ++-- src/json.rs | 47 +- src/lib.rs | 29 +- src/request.rs | 25 +- src/server/mod.rs | 17 +- src/server/output.rs | 1 + src/test.rs | 712 +++++++---------------- tests/test_h1v2.rs | 1 - 35 files changed, 1063 insertions(+), 811 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 60f14485f..86012175f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,7 @@ bytes = "0.4" byteorder = "1.2" futures = "0.1" tokio-codec = "0.1" +tokio = "0.1" tokio-io = "0.1" tokio-tcp = "0.1" tokio-timer = "0.2" @@ -123,7 +124,6 @@ tokio-uds = { version="0.2", optional = true } actix-web = "0.7" env_logger = "0.5" serde_derive = "1.0" -tokio = "0.1" [build-dependencies] version_check = "0.1" diff --git a/src/body.rs b/src/body.rs index e689b704c..db06bef22 100644 --- a/src/body.rs +++ b/src/body.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use std::{fmt, mem}; use error::Error; -use httpresponse::HttpResponse; /// Type represent streaming body pub type BodyStream = Box>; diff --git a/src/config.rs b/src/config.rs index 508cd5dde..543e78acd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,20 +1,15 @@ -use std::cell::{RefCell, RefMut, UnsafeCell}; -use std::collections::VecDeque; +use std::cell::UnsafeCell; use std::fmt::Write; use std::rc::Rc; use std::time::{Duration, Instant}; -use std::{env, fmt, net}; +use std::{fmt, net}; use bytes::BytesMut; use futures::{future, Future}; -use http::StatusCode; use time; use tokio_current_thread::spawn; use tokio_timer::{sleep, Delay}; -use body::Body; -use httpresponse::{HttpResponse, HttpResponseBuilder, HttpResponsePool}; -use request::{Request, RequestPool}; use server::KeepAlive; // "Sun, 06 Nov 1994 08:49:37 GMT".len() @@ -336,13 +331,7 @@ mod tests { let mut rt = current_thread::Runtime::new().unwrap(); let _ = rt.block_on(future::lazy(|| { - let settings = ServiceConfig::<()>::new( - (), - KeepAlive::Os, - 0, - 0, - ServerSettings::default(), - ); + let settings = ServiceConfig::new(KeepAlive::Os, 0, 0); let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); settings.set_date(&mut buf1, true); let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); diff --git a/src/error.rs b/src/error.rs index e39dea9b2..21aabac49 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,7 +28,7 @@ use httpresponse::{HttpResponse, HttpResponseParts}; /// for actix web operations /// /// This typedef is generally used to avoid writing out -/// `actix_web::error::Error` directly and is otherwise a direct mapping to +/// `actix_http::error::Error` directly and is otherwise a direct mapping to /// `Result`. pub type Result = result::Result; @@ -589,13 +589,12 @@ impl From for UrlGenerationError { /// default. /// /// ```rust -/// # extern crate actix_web; -/// # use actix_web::*; -/// use actix_web::fs::NamedFile; +/// # extern crate actix_http; +/// # use std::io; +/// # use actix_http::*; /// -/// fn index(req: HttpRequest) -> Result { -/// let f = NamedFile::open("test.txt").map_err(error::ErrorBadRequest)?; -/// Ok(f) +/// fn index(req: Request) -> Result<&'static str> { +/// Err(error::ErrorBadRequest(io::Error::new(io::ErrorKind::Other, "error"))) /// } /// # fn main() {} /// ``` @@ -837,14 +836,6 @@ mod tests { use std::error::Error as StdError; use std::io; - #[test] - #[cfg(actix_nightly)] - fn test_nightly() { - let resp: HttpResponse = - IoError::new(io::ErrorKind::Other, "test").error_response(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - #[test] fn test_into_response() { let resp: HttpResponse = ParseError::Incomplete.error_response(); @@ -853,9 +844,6 @@ mod tests { let resp: HttpResponse = CookieParseError::EmptyName.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: HttpResponse = MultipartError::Boundary.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into(); let resp: HttpResponse = err.error_response(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); @@ -899,14 +887,6 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } - #[test] - fn test_expect_error() { - let resp: HttpResponse = ExpectError::Encoding.error_response(); - assert_eq!(resp.status(), StatusCode::EXPECTATION_FAILED); - let resp: HttpResponse = ExpectError::UnknownExpect.error_response(); - assert_eq!(resp.status(), StatusCode::EXPECTATION_FAILED); - } - macro_rules! from { ($from:expr => $error:pat) => { match ParseError::from($from) { @@ -963,10 +943,8 @@ mod tests { #[test] fn test_internal_error() { - let err = InternalError::from_response( - ExpectError::Encoding, - HttpResponse::Ok().into(), - ); + let err = + InternalError::from_response(ParseError::Method, HttpResponse::Ok().into()); let resp: HttpResponse = err.error_response(); assert_eq!(resp.status(), StatusCode::OK); } diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 011883575..f1b526d52 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -15,8 +15,11 @@ use httpresponse::HttpResponse; use request::RequestPool; use server::output::{ResponseInfo, ResponseLength}; +/// Http response pub enum OutMessage { + /// Http response message Response(HttpResponse), + /// Payload chunk Payload(Bytes), } @@ -35,7 +38,7 @@ impl Codec { /// Create HTTP/1 codec with request's pool pub(crate) fn with_pool(pool: &'static RequestPool) -> Self { Codec { - decoder: H1Decoder::new(pool), + decoder: H1Decoder::with_pool(pool), encoder: H1Writer::new(), } } diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 47cc5fdf1..66f24628c 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -18,16 +18,26 @@ pub(crate) struct H1Decoder { pool: &'static RequestPool, } +/// Incoming http/1 request #[derive(Debug)] pub enum InMessage { + /// Request Message(Request), + /// Request with payload MessageWithPayload(Request), + /// Payload chunk Chunk(Bytes), + /// End of payload Eof, } impl H1Decoder { - pub fn new(pool: &'static RequestPool) -> H1Decoder { + #[cfg(test)] + pub fn new() -> H1Decoder { + H1Decoder::with_pool(RequestPool::pool()) + } + + pub fn with_pool(pool: &'static RequestPool) -> H1Decoder { H1Decoder { pool, decoder: None, @@ -497,3 +507,657 @@ impl ChunkedState { } } } + +#[cfg(test)] +mod tests { + use std::net::Shutdown; + use std::{cmp, io, time}; + + use actix::System; + use bytes::{Buf, Bytes, BytesMut}; + use futures::{future, future::ok}; + use http::{Method, Version}; + use tokio_io::{AsyncRead, AsyncWrite}; + + use super::*; + use error::ParseError; + use h1::{Dispatcher, InMessage}; + use httpmessage::HttpMessage; + use request::Request; + use server::KeepAlive; + + impl InMessage { + fn message(self) -> Request { + match self { + InMessage::Message(msg) => msg, + InMessage::MessageWithPayload(msg) => msg, + _ => panic!("error"), + } + } + fn is_payload(&self) -> bool { + match *self { + InMessage::MessageWithPayload(_) => true, + _ => panic!("error"), + } + } + fn chunk(self) -> Bytes { + match self { + InMessage::Chunk(chunk) => chunk, + _ => panic!("error"), + } + } + fn eof(&self) -> bool { + match *self { + InMessage::Eof => true, + _ => false, + } + } + } + + macro_rules! parse_ready { + ($e:expr) => {{ + match H1Decoder::new().decode($e) { + Ok(Some(msg)) => msg.message(), + Ok(_) => unreachable!("Eof during parsing http request"), + Err(err) => unreachable!("Error during parsing http request: {:?}", err), + } + }}; + } + + macro_rules! expect_parse_err { + ($e:expr) => {{ + match H1Decoder::new().decode($e) { + Err(err) => match err { + ParseError::Io(_) => unreachable!("Parse error expected"), + _ => (), + }, + _ => unreachable!("Error expected"), + } + }}; + } + + struct Buffer { + buf: Bytes, + err: Option, + } + + impl Buffer { + fn new(data: &'static str) -> Buffer { + Buffer { + buf: Bytes::from(data), + err: None, + } + } + } + + impl AsyncRead for Buffer {} + impl io::Read for Buffer { + fn read(&mut self, dst: &mut [u8]) -> Result { + if self.buf.is_empty() { + if self.err.is_some() { + Err(self.err.take().unwrap()) + } else { + Err(io::Error::new(io::ErrorKind::WouldBlock, "")) + } + } else { + let size = cmp::min(self.buf.len(), dst.len()); + let b = self.buf.split_to(size); + dst[..size].copy_from_slice(&b); + Ok(size) + } + } + } + + impl io::Write for Buffer { + fn write(&mut self, buf: &[u8]) -> io::Result { + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + impl AsyncWrite for Buffer { + fn shutdown(&mut self) -> Poll<(), io::Error> { + Ok(Async::Ready(())) + } + fn write_buf(&mut self, _: &mut B) -> Poll { + Ok(Async::NotReady) + } + } + + // #[test] + // fn test_req_parse_err() { + // let mut sys = System::new("test"); + // let _ = sys.block_on(future::lazy(|| { + // let buf = Buffer::new("GET /test HTTP/1\r\n\r\n"); + // let readbuf = BytesMut::new(); + + // let mut h1 = Dispatcher::new(buf, |req| ok(HttpResponse::Ok().finish())); + // assert!(h1.poll_io().is_ok()); + // assert!(h1.poll_io().is_ok()); + // assert!(h1.flags.contains(Flags::READ_DISCONNECTED)); + // assert_eq!(h1.tasks.len(), 1); + // future::ok::<_, ()>(()) + // })); + // } + + #[test] + fn test_parse() { + let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); + + let mut reader = H1Decoder::new(); + match reader.decode(&mut buf) { + Ok(Some(msg)) => { + let req = msg.message(); + assert_eq!(req.version(), Version::HTTP_11); + assert_eq!(*req.method(), Method::GET); + assert_eq!(req.path(), "/test"); + } + Ok(_) | Err(_) => unreachable!("Error during parsing http request"), + } + } + + #[test] + fn test_parse_partial() { + let mut buf = BytesMut::from("PUT /test HTTP/1"); + + let mut reader = H1Decoder::new(); + match reader.decode(&mut buf) { + Ok(None) => (), + _ => unreachable!("Error"), + } + + buf.extend(b".1\r\n\r\n"); + match reader.decode(&mut buf) { + Ok(Some(msg)) => { + let mut req = msg.message(); + assert_eq!(req.version(), Version::HTTP_11); + assert_eq!(*req.method(), Method::PUT); + assert_eq!(req.path(), "/test"); + } + Ok(_) | Err(_) => unreachable!("Error during parsing http request"), + } + } + + #[test] + fn test_parse_post() { + let mut buf = BytesMut::from("POST /test2 HTTP/1.0\r\n\r\n"); + + let mut reader = H1Decoder::new(); + match reader.decode(&mut buf) { + Ok(Some(msg)) => { + let mut req = msg.message(); + assert_eq!(req.version(), Version::HTTP_10); + assert_eq!(*req.method(), Method::POST); + assert_eq!(req.path(), "/test2"); + } + Ok(_) | Err(_) => unreachable!("Error during parsing http request"), + } + } + + #[test] + fn test_parse_body() { + let mut buf = + BytesMut::from("GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); + + let mut reader = H1Decoder::new(); + match reader.decode(&mut buf) { + Ok(Some(msg)) => { + let mut req = msg.message(); + assert_eq!(req.version(), Version::HTTP_11); + assert_eq!(*req.method(), Method::GET); + assert_eq!(req.path(), "/test"); + assert_eq!( + reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + b"body" + ); + } + Ok(_) | Err(_) => unreachable!("Error during parsing http request"), + } + } + + #[test] + fn test_parse_body_crlf() { + let mut buf = + BytesMut::from("\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); + + let mut reader = H1Decoder::new(); + match reader.decode(&mut buf) { + Ok(Some(msg)) => { + let mut req = msg.message(); + assert_eq!(req.version(), Version::HTTP_11); + assert_eq!(*req.method(), Method::GET); + assert_eq!(req.path(), "/test"); + assert_eq!( + reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + b"body" + ); + } + Ok(_) | Err(_) => unreachable!("Error during parsing http request"), + } + } + + #[test] + fn test_parse_partial_eof() { + let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); + let mut reader = H1Decoder::new(); + assert!(reader.decode(&mut buf).unwrap().is_none()); + + buf.extend(b"\r\n"); + match reader.decode(&mut buf) { + Ok(Some(msg)) => { + let req = msg.message(); + assert_eq!(req.version(), Version::HTTP_11); + assert_eq!(*req.method(), Method::GET); + assert_eq!(req.path(), "/test"); + } + Ok(_) | Err(_) => unreachable!("Error during parsing http request"), + } + } + + #[test] + fn test_headers_split_field() { + let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); + + let mut reader = H1Decoder::new(); + assert!{ reader.decode(&mut buf).unwrap().is_none() } + + buf.extend(b"t"); + assert!{ reader.decode(&mut buf).unwrap().is_none() } + + buf.extend(b"es"); + assert!{ reader.decode(&mut buf).unwrap().is_none() } + + buf.extend(b"t: value\r\n\r\n"); + match reader.decode(&mut buf) { + Ok(Some(msg)) => { + let req = msg.message(); + assert_eq!(req.version(), Version::HTTP_11); + assert_eq!(*req.method(), Method::GET); + assert_eq!(req.path(), "/test"); + assert_eq!(req.headers().get("test").unwrap().as_bytes(), b"value"); + } + Ok(_) | Err(_) => unreachable!("Error during parsing http request"), + } + } + + #[test] + fn test_headers_multi_value() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + Set-Cookie: c1=cookie1\r\n\ + Set-Cookie: c2=cookie2\r\n\r\n", + ); + let mut reader = H1Decoder::new(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); + let req = msg.message(); + + let val: Vec<_> = req + .headers() + .get_all("Set-Cookie") + .iter() + .map(|v| v.to_str().unwrap().to_owned()) + .collect(); + assert_eq!(val[0], "c1=cookie1"); + assert_eq!(val[1], "c2=cookie2"); + } + + #[test] + fn test_conn_default_1_0() { + let mut buf = BytesMut::from("GET /test HTTP/1.0\r\n\r\n"); + let req = parse_ready!(&mut buf); + + assert!(!req.keep_alive()); + } + + #[test] + fn test_conn_default_1_1() { + let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); + let req = parse_ready!(&mut buf); + + assert!(req.keep_alive()); + } + + #[test] + fn test_conn_close() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + connection: close\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + assert!(!req.keep_alive()); + } + + #[test] + fn test_conn_close_1_0() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.0\r\n\ + connection: close\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + assert!(!req.keep_alive()); + } + + #[test] + fn test_conn_keep_alive_1_0() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.0\r\n\ + connection: keep-alive\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + assert!(req.keep_alive()); + } + + #[test] + fn test_conn_keep_alive_1_1() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + connection: keep-alive\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + assert!(req.keep_alive()); + } + + #[test] + fn test_conn_other_1_0() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.0\r\n\ + connection: other\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + assert!(!req.keep_alive()); + } + + #[test] + fn test_conn_other_1_1() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + connection: other\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + assert!(req.keep_alive()); + } + + #[test] + fn test_conn_upgrade() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + upgrade: websockets\r\n\ + connection: upgrade\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + assert!(req.upgrade()); + } + + #[test] + fn test_conn_upgrade_connect_method() { + let mut buf = BytesMut::from( + "CONNECT /test HTTP/1.1\r\n\ + content-type: text/plain\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + assert!(req.upgrade()); + } + + #[test] + fn test_request_chunked() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + if let Ok(val) = req.chunked() { + assert!(val); + } else { + unreachable!("Error"); + } + + // type in chunked + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chnked\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + if let Ok(val) = req.chunked() { + assert!(!val); + } else { + unreachable!("Error"); + } + } + + #[test] + fn test_headers_content_length_err_1() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + content-length: line\r\n\r\n", + ); + + expect_parse_err!(&mut buf) + } + + #[test] + fn test_headers_content_length_err_2() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + content-length: -1\r\n\r\n", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn test_invalid_header() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + test line\r\n\r\n", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn test_invalid_name() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + test[]: line\r\n\r\n", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn test_http_request_bad_status_line() { + let mut buf = BytesMut::from("getpath \r\n\r\n"); + expect_parse_err!(&mut buf); + } + + #[test] + fn test_http_request_upgrade() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + connection: upgrade\r\n\ + upgrade: websocket\r\n\r\n\ + some raw data", + ); + let mut reader = H1Decoder::new(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); + assert!(msg.is_payload()); + let req = msg.message(); + assert!(!req.keep_alive()); + assert!(req.upgrade()); + assert_eq!( + reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + b"some raw data" + ); + } + + #[test] + fn test_http_request_parser_utf8() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + x-test: теÑÑ‚\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + assert_eq!( + req.headers().get("x-test").unwrap().as_bytes(), + "теÑÑ‚".as_bytes() + ); + } + + #[test] + fn test_http_request_parser_two_slashes() { + let mut buf = BytesMut::from("GET //path HTTP/1.1\r\n\r\n"); + let req = parse_ready!(&mut buf); + + assert_eq!(req.path(), "//path"); + } + + #[test] + fn test_http_request_parser_bad_method() { + let mut buf = BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n"); + + expect_parse_err!(&mut buf); + } + + #[test] + fn test_http_request_parser_bad_version() { + let mut buf = BytesMut::from("GET //get HT/11\r\n\r\n"); + + expect_parse_err!(&mut buf); + } + + #[test] + fn test_http_request_chunked_payload() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + let mut reader = H1Decoder::new(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); + assert!(msg.is_payload()); + let req = msg.message(); + assert!(req.chunked().unwrap()); + + buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); + assert_eq!( + reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + b"data" + ); + assert_eq!( + reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + b"line" + ); + assert!(reader.decode(&mut buf).unwrap().unwrap().eof()); + } + + #[test] + fn test_http_request_chunked_payload_and_next_message() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + let mut reader = H1Decoder::new(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); + assert!(msg.is_payload()); + let req = msg.message(); + assert!(req.chunked().unwrap()); + + buf.extend( + b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\ + POST /test2 HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n" + .iter(), + ); + let msg = reader.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"data"); + let msg = reader.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"line"); + let msg = reader.decode(&mut buf).unwrap().unwrap(); + assert!(msg.eof()); + + let msg = reader.decode(&mut buf).unwrap().unwrap(); + assert!(msg.is_payload()); + let req2 = msg.message(); + assert!(req2.chunked().unwrap()); + assert_eq!(*req2.method(), Method::POST); + assert!(req2.chunked().unwrap()); + } + + #[test] + fn test_http_request_chunked_payload_chunks() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + + let mut reader = H1Decoder::new(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); + assert!(msg.is_payload()); + let req = msg.message(); + assert!(req.chunked().unwrap()); + + buf.extend(b"4\r\n1111\r\n"); + let msg = reader.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"1111"); + + buf.extend(b"4\r\ndata\r"); + let msg = reader.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"data"); + + buf.extend(b"\n4"); + assert!(reader.decode(&mut buf).unwrap().is_none()); + + buf.extend(b"\r"); + assert!(reader.decode(&mut buf).unwrap().is_none()); + buf.extend(b"\n"); + assert!(reader.decode(&mut buf).unwrap().is_none()); + + buf.extend(b"li"); + let msg = reader.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"li"); + + //trailers + //buf.feed_data("test: test\r\n"); + //not_ready!(reader.parse(&mut buf, &mut readbuf)); + + buf.extend(b"ne\r\n0\r\n"); + let msg = reader.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"ne"); + assert!(reader.decode(&mut buf).unwrap().is_none()); + + buf.extend(b"\r\n"); + assert!(reader.decode(&mut buf).unwrap().unwrap().eof()); + } + + #[test] + fn test_parse_chunked_payload_chunk_extension() { + let mut buf = BytesMut::from( + &"GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n"[..], + ); + + let mut reader = H1Decoder::new(); + let msg = reader.decode(&mut buf).unwrap().unwrap(); + assert!(msg.is_payload()); + assert!(msg.message().chunked().unwrap()); + + buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n") + let chunk = reader.decode(&mut buf).unwrap().unwrap().chunk(); + assert_eq!(chunk, Bytes::from_static(b"data")); + let chunk = reader.decode(&mut buf).unwrap().unwrap().chunk(); + assert_eq!(chunk, Bytes::from_static(b"line")); + let msg = reader.decode(&mut buf).unwrap().unwrap(); + assert!(msg.eof()); + } +} diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 6e5672d35..eda8ebf00 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -1,7 +1,6 @@ // #![allow(unused_imports, unused_variables, dead_code)] use std::collections::VecDeque; use std::fmt::{Debug, Display}; -use std::net::SocketAddr; // use std::time::{Duration, Instant}; use actix_net::service::Service; @@ -16,10 +15,10 @@ use error::{ParseError, PayloadError}; use payload::{Payload, PayloadStatus, PayloadWriter}; use body::Body; +use config::ServiceConfig; use error::DispatchError; use httpresponse::HttpResponse; - -use request::{Request, RequestPool}; +use request::Request; use server::input::PayloadType; use super::codec::{Codec, InMessage, OutMessage}; @@ -52,6 +51,8 @@ where state: State, payload: Option, messages: VecDeque, + + config: ServiceConfig, } enum State { @@ -79,7 +80,7 @@ where S::Error: Debug + Display, { /// Create http/1 dispatcher. - pub fn new(stream: T, service: S) -> Self { + pub fn new(stream: T, config: ServiceConfig, service: S) -> Self { let flags = Flags::FLUSHED; let framed = Framed::new(stream, Codec::new()); @@ -91,6 +92,7 @@ where service, flags, framed, + config, } } @@ -108,7 +110,7 @@ where } // if checked is set to true, delay disconnect until all tasks have finished. - fn client_disconnected(&mut self, checked: bool) { + fn client_disconnected(&mut self, _checked: bool) { self.flags.insert(Flags::READ_DISCONNECTED); if let Some(mut payload) = self.payload.take() { payload.set_error(PayloadError::Incomplete); @@ -187,7 +189,7 @@ where None }; }, - State::Payload(ref mut body) => unimplemented!(), + State::Payload(ref mut _body) => unimplemented!(), State::Response(ref mut fut) => { match fut.poll() { Ok(Async::Ready(res)) => { diff --git a/src/h1/mod.rs b/src/h1/mod.rs index 245f2fc23..1a2bb0183 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -4,6 +4,6 @@ mod decoder; mod dispatcher; mod service; -pub use self::codec::Codec; +pub use self::codec::{Codec, InMessage, OutMessage}; pub use self::dispatcher::Dispatcher; pub use self::service::{H1Service, H1ServiceHandler}; diff --git a/src/h1/service.rs b/src/h1/service.rs index 3017a3ef7..436e77a55 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -1,9 +1,7 @@ use std::fmt::{Debug, Display}; use std::marker::PhantomData; -use std::time::Duration; use actix_net::service::{IntoNewService, NewService, Service}; -use futures::future::{ok, FutureResult}; use futures::{Async, Future, Poll}; use tokio_io::{AsyncRead, AsyncWrite}; @@ -120,6 +118,6 @@ where } fn call(&mut self, req: Self::Request) -> Self::Future { - Dispatcher::new(req, self.srv.clone()) + Dispatcher::new(req, self.cfg.clone(), self.srv.clone()) } } diff --git a/src/header/common/accept.rs b/src/header/common/accept.rs index d736e53af..1ba321ce8 100644 --- a/src/header/common/accept.rs +++ b/src/header/common/accept.rs @@ -30,10 +30,10 @@ header! { /// /// # Examples /// ```rust - /// # extern crate actix_web; + /// # extern crate actix_http; /// extern crate mime; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, qitem}; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::{Accept, qitem}; /// /// # fn main() { /// let mut builder = HttpResponse::Ok(); @@ -47,10 +47,10 @@ header! { /// ``` /// /// ```rust - /// # extern crate actix_web; + /// # extern crate actix_http; /// extern crate mime; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, qitem}; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::{Accept, qitem}; /// /// # fn main() { /// let mut builder = HttpResponse::Ok(); @@ -64,10 +64,10 @@ header! { /// ``` /// /// ```rust - /// # extern crate actix_web; + /// # extern crate actix_http; /// extern crate mime; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, QualityItem, q, qitem}; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::{Accept, QualityItem, q, qitem}; /// /// # fn main() { /// let mut builder = HttpResponse::Ok(); diff --git a/src/header/common/accept_charset.rs b/src/header/common/accept_charset.rs index 674415fba..49a7237aa 100644 --- a/src/header/common/accept_charset.rs +++ b/src/header/common/accept_charset.rs @@ -22,9 +22,9 @@ header! { /// /// # Examples /// ```rust - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptCharset, Charset, qitem}; + /// # extern crate actix_http; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::{AcceptCharset, Charset, qitem}; /// /// # fn main() { /// let mut builder = HttpResponse::Ok(); @@ -34,9 +34,9 @@ header! { /// # } /// ``` /// ```rust - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptCharset, Charset, q, QualityItem}; + /// # extern crate actix_http; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::{AcceptCharset, Charset, q, QualityItem}; /// /// # fn main() { /// let mut builder = HttpResponse::Ok(); @@ -49,9 +49,9 @@ header! { /// # } /// ``` /// ```rust - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptCharset, Charset, qitem}; + /// # extern crate actix_http; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::{AcceptCharset, Charset, qitem}; /// /// # fn main() { /// let mut builder = HttpResponse::Ok(); diff --git a/src/header/common/accept_language.rs b/src/header/common/accept_language.rs index 12593e1ac..25fd97df4 100644 --- a/src/header/common/accept_language.rs +++ b/src/header/common/accept_language.rs @@ -23,10 +23,10 @@ header! { /// # Examples /// /// ```rust - /// # extern crate actix_web; + /// # extern crate actix_http; /// # extern crate language_tags; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptLanguage, LanguageTag, qitem}; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::{AcceptLanguage, LanguageTag, qitem}; /// /// # fn main() { /// let mut builder = HttpResponse::Ok(); @@ -42,10 +42,10 @@ header! { /// ``` /// /// ```rust - /// # extern crate actix_web; + /// # extern crate actix_http; /// # #[macro_use] extern crate language_tags; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptLanguage, QualityItem, q, qitem}; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::{AcceptLanguage, QualityItem, q, qitem}; /// # /// # fn main() { /// let mut builder = HttpResponse::Ok(); diff --git a/src/header/common/allow.rs b/src/header/common/allow.rs index 5046290de..089c823d0 100644 --- a/src/header/common/allow.rs +++ b/src/header/common/allow.rs @@ -1,5 +1,5 @@ -use http::Method; use http::header; +use http::Method; header! { /// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1) @@ -23,11 +23,10 @@ header! { /// # Examples /// /// ```rust - /// # extern crate http; - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::Allow; - /// use http::Method; + /// # extern crate actix_http; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::Allow; + /// use actix_http::http::Method; /// /// # fn main() { /// let mut builder = HttpResponse::Ok(); @@ -38,11 +37,9 @@ header! { /// ``` /// /// ```rust - /// # extern crate http; - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::Allow; - /// use http::Method; + /// # extern crate actix_http; + /// use actix_http::HttpResponse; + /// use actix_http::http::{Method, header::Allow}; /// /// # fn main() { /// let mut builder = HttpResponse::Ok(); diff --git a/src/header/common/cache_control.rs b/src/header/common/cache_control.rs index adc60e4a5..4379b6f7a 100644 --- a/src/header/common/cache_control.rs +++ b/src/header/common/cache_control.rs @@ -1,5 +1,5 @@ -use header::{Header, IntoHeaderValue, Writer}; use header::{fmt_comma_delimited, from_comma_delimited}; +use header::{Header, IntoHeaderValue, Writer}; use http::header; use std::fmt::{self, Write}; use std::str::FromStr; @@ -26,16 +26,16 @@ use std::str::FromStr; /// /// # Examples /// ```rust -/// use actix_web::HttpResponse; -/// use actix_web::http::header::{CacheControl, CacheDirective}; +/// use actix_http::HttpResponse; +/// use actix_http::http::header::{CacheControl, CacheDirective}; /// /// let mut builder = HttpResponse::Ok(); /// builder.set(CacheControl(vec![CacheDirective::MaxAge(86400u32)])); /// ``` /// /// ```rust -/// use actix_web::HttpResponse; -/// use actix_web::http::header::{CacheControl, CacheDirective}; +/// use actix_http::HttpResponse; +/// use actix_http::http::header::{CacheControl, CacheDirective}; /// /// let mut builder = HttpResponse::Ok(); /// builder.set(CacheControl(vec![ diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs index 5e8cbd67a..0efc4fb0b 100644 --- a/src/header/common/content_disposition.rs +++ b/src/header/common/content_disposition.rs @@ -64,7 +64,7 @@ impl<'a> From<&'a str> for DispositionType { /// /// # Examples /// ``` -/// use actix_web::http::header::DispositionParam; +/// use actix_http::http::header::DispositionParam; /// /// let param = DispositionParam::Filename(String::from("sample.txt")); /// assert!(param.is_filename()); @@ -226,7 +226,7 @@ impl DispositionParam { /// # Example /// /// ``` -/// use actix_web::http::header::{ +/// use actix_http::http::header::{ /// Charset, ContentDisposition, DispositionParam, DispositionType, /// ExtendedValue, /// }; @@ -327,7 +327,8 @@ impl ContentDisposition { left = &left[end.ok_or(::error::ParseError::Header)? + 1..]; left = split_once(left, ';').1.trim_left(); // In fact, it should not be Err if the above code is correct. - String::from_utf8(quoted_string).map_err(|_| ::error::ParseError::Header)? + String::from_utf8(quoted_string) + .map_err(|_| ::error::ParseError::Header)? } else { // token: won't contains semicolon according to RFC 2616 Section 2.2 let (token, new_left) = split_once_and_trim(left, ';'); @@ -874,7 +875,7 @@ mod tests { "attachment; filename=\"carriage\\\rreturn.png\"", display_rendered );*/ - // No way to create a HeaderValue containing a carriage return. + // No way to create a HeaderValue containing a carriage return. let a: ContentDisposition = ContentDisposition { disposition: DispositionType::Inline, diff --git a/src/header/common/content_language.rs b/src/header/common/content_language.rs index e12d34d0d..c1f87d513 100644 --- a/src/header/common/content_language.rs +++ b/src/header/common/content_language.rs @@ -24,10 +24,10 @@ header! { /// # Examples /// /// ```rust - /// # extern crate actix_web; + /// # extern crate actix_http; /// # #[macro_use] extern crate language_tags; - /// use actix_web::HttpResponse; - /// # use actix_web::http::header::{ContentLanguage, qitem}; + /// use actix_http::HttpResponse; + /// # use actix_http::http::header::{ContentLanguage, qitem}; /// # /// # fn main() { /// let mut builder = HttpResponse::Ok(); @@ -40,10 +40,10 @@ header! { /// ``` /// /// ```rust - /// # extern crate actix_web; + /// # extern crate actix_http; /// # #[macro_use] extern crate language_tags; - /// use actix_web::HttpResponse; - /// # use actix_web::http::header::{ContentLanguage, qitem}; + /// use actix_http::HttpResponse; + /// # use actix_http::http::header::{ContentLanguage, qitem}; /// # /// # fn main() { /// diff --git a/src/header/common/content_type.rs b/src/header/common/content_type.rs index 08900e1cc..3286d4cae 100644 --- a/src/header/common/content_type.rs +++ b/src/header/common/content_type.rs @@ -31,8 +31,8 @@ header! { /// # Examples /// /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::ContentType; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::ContentType; /// /// # fn main() { /// let mut builder = HttpResponse::Ok(); @@ -44,10 +44,10 @@ header! { /// /// ```rust /// # extern crate mime; - /// # extern crate actix_web; + /// # extern crate actix_http; /// use mime::TEXT_HTML; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::ContentType; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::ContentType; /// /// # fn main() { /// let mut builder = HttpResponse::Ok(); diff --git a/src/header/common/date.rs b/src/header/common/date.rs index 88a47bc3f..9ce2bd65f 100644 --- a/src/header/common/date.rs +++ b/src/header/common/date.rs @@ -20,8 +20,8 @@ header! { /// # Example /// /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::Date; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::Date; /// use std::time::SystemTime; /// /// let mut builder = HttpResponse::Ok(); diff --git a/src/header/common/etag.rs b/src/header/common/etag.rs index 39dd908c1..ea4be2a77 100644 --- a/src/header/common/etag.rs +++ b/src/header/common/etag.rs @@ -28,16 +28,16 @@ header! { /// # Examples /// /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{ETag, EntityTag}; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::{ETag, EntityTag}; /// /// let mut builder = HttpResponse::Ok(); /// builder.set(ETag(EntityTag::new(false, "xyzzy".to_owned()))); /// ``` /// /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{ETag, EntityTag}; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::{ETag, EntityTag}; /// /// let mut builder = HttpResponse::Ok(); /// builder.set(ETag(EntityTag::new(true, "xyzzy".to_owned()))); diff --git a/src/header/common/expires.rs b/src/header/common/expires.rs index 4ec66b880..bdd25fdb9 100644 --- a/src/header/common/expires.rs +++ b/src/header/common/expires.rs @@ -22,8 +22,8 @@ header! { /// # Example /// /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::Expires; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::Expires; /// use std::time::{SystemTime, Duration}; /// /// let mut builder = HttpResponse::Ok(); diff --git a/src/header/common/if_match.rs b/src/header/common/if_match.rs index 20a2b1e6b..5f7976a4a 100644 --- a/src/header/common/if_match.rs +++ b/src/header/common/if_match.rs @@ -30,16 +30,16 @@ header! { /// # Examples /// /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfMatch; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::IfMatch; /// /// let mut builder = HttpResponse::Ok(); /// builder.set(IfMatch::Any); /// ``` /// /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{IfMatch, EntityTag}; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::{IfMatch, EntityTag}; /// /// let mut builder = HttpResponse::Ok(); /// builder.set( diff --git a/src/header/common/if_modified_since.rs b/src/header/common/if_modified_since.rs index 1914d34d3..41d6fba27 100644 --- a/src/header/common/if_modified_since.rs +++ b/src/header/common/if_modified_since.rs @@ -22,8 +22,8 @@ header! { /// # Example /// /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfModifiedSince; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::IfModifiedSince; /// use std::time::{SystemTime, Duration}; /// /// let mut builder = HttpResponse::Ok(); diff --git a/src/header/common/if_none_match.rs b/src/header/common/if_none_match.rs index 124f4b8e0..8b3905bab 100644 --- a/src/header/common/if_none_match.rs +++ b/src/header/common/if_none_match.rs @@ -32,16 +32,16 @@ header! { /// # Examples /// /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfNoneMatch; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::IfNoneMatch; /// /// let mut builder = HttpResponse::Ok(); /// builder.set(IfNoneMatch::Any); /// ``` /// /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{IfNoneMatch, EntityTag}; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::{IfNoneMatch, EntityTag}; /// /// let mut builder = HttpResponse::Ok(); /// builder.set( diff --git a/src/header/common/if_range.rs b/src/header/common/if_range.rs index dd95b7ba8..8cbb8c897 100644 --- a/src/header/common/if_range.rs +++ b/src/header/common/if_range.rs @@ -1,7 +1,9 @@ use error::ParseError; use header::from_one_raw_str; -use header::{EntityTag, Header, HeaderName, HeaderValue, HttpDate, IntoHeaderValue, - InvalidHeaderValueBytes, Writer}; +use header::{ + EntityTag, Header, HeaderName, HeaderValue, HttpDate, IntoHeaderValue, + InvalidHeaderValueBytes, Writer, +}; use http::header; use httpmessage::HttpMessage; use std::fmt::{self, Display, Write}; @@ -35,8 +37,8 @@ use std::fmt::{self, Display, Write}; /// # Examples /// /// ```rust -/// use actix_web::HttpResponse; -/// use actix_web::http::header::{EntityTag, IfRange}; +/// use actix_http::HttpResponse; +/// use actix_http::http::header::{EntityTag, IfRange}; /// /// let mut builder = HttpResponse::Ok(); /// builder.set(IfRange::EntityTag(EntityTag::new( @@ -46,8 +48,8 @@ use std::fmt::{self, Display, Write}; /// ``` /// /// ```rust -/// use actix_web::HttpResponse; -/// use actix_web::http::header::IfRange; +/// use actix_http::HttpResponse; +/// use actix_http::http::header::IfRange; /// use std::time::{Duration, SystemTime}; /// /// let mut builder = HttpResponse::Ok(); diff --git a/src/header/common/if_unmodified_since.rs b/src/header/common/if_unmodified_since.rs index f87e760c0..02f9252e2 100644 --- a/src/header/common/if_unmodified_since.rs +++ b/src/header/common/if_unmodified_since.rs @@ -23,8 +23,8 @@ header! { /// # Example /// /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfUnmodifiedSince; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::IfUnmodifiedSince; /// use std::time::{SystemTime, Duration}; /// /// let mut builder = HttpResponse::Ok(); diff --git a/src/header/common/last_modified.rs b/src/header/common/last_modified.rs index aba828883..608f43138 100644 --- a/src/header/common/last_modified.rs +++ b/src/header/common/last_modified.rs @@ -22,8 +22,8 @@ header! { /// # Example /// /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::LastModified; + /// use actix_http::HttpResponse; + /// use actix_http::http::header::LastModified; /// use std::time::{SystemTime, Duration}; /// /// let mut builder = HttpResponse::Ok(); diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 8c972bd13..531aa1a72 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -106,7 +106,7 @@ pub trait HttpMessage: Sized { /// /// ## Server example /// - /// ```rust + /// ```rust,ignore /// # extern crate bytes; /// # extern crate actix_web; /// # extern crate futures; @@ -143,7 +143,7 @@ pub trait HttpMessage: Sized { /// /// ## Server example /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// # extern crate futures; /// # use futures::Future; @@ -176,7 +176,7 @@ pub trait HttpMessage: Sized { /// /// ## Server example /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// # extern crate futures; /// # #[macro_use] extern crate serde_derive; diff --git a/src/httpresponse.rs b/src/httpresponse.rs index 73de380ad..cdcdea94a 100644 --- a/src/httpresponse.rs +++ b/src/httpresponse.rs @@ -15,7 +15,7 @@ use serde_json; use body::Body; use error::Error; use header::{ContentEncoding, Header, IntoHeaderValue}; -use httpmessage::HttpMessage; +// use httpmessage::HttpMessage; // use httprequest::HttpRequest; /// max write buffer size 64k @@ -366,7 +366,7 @@ impl HttpResponseBuilder { /// Set a header. /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// use actix_web::{http, HttpRequest, HttpResponse, Result}; /// @@ -394,7 +394,7 @@ impl HttpResponseBuilder { /// Set a header. /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// use actix_web::{http, HttpRequest, HttpResponse}; /// @@ -516,7 +516,7 @@ impl HttpResponseBuilder { /// Set a cookie /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// use actix_web::{http, HttpRequest, HttpResponse, Result}; /// @@ -546,7 +546,7 @@ impl HttpResponseBuilder { /// Remove cookie /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// use actix_web::{http, HttpRequest, HttpResponse, Result}; /// @@ -956,38 +956,38 @@ mod tests { assert!(dbg.contains("HttpResponse")); } - #[test] - fn test_response_cookies() { - let req = TestRequest::default() - .header(COOKIE, "cookie1=value1") - .header(COOKIE, "cookie2=value2") - .finish(); - let cookies = req.cookies().unwrap(); + // #[test] + // fn test_response_cookies() { + // let req = TestRequest::default() + // .header(COOKIE, "cookie1=value1") + // .header(COOKIE, "cookie2=value2") + // .finish(); + // let cookies = req.cookies().unwrap(); - let resp = HttpResponse::Ok() - .cookie( - http::Cookie::build("name", "value") - .domain("www.rust-lang.org") - .path("/test") - .http_only(true) - .max_age(Duration::days(1)) - .finish(), - ).del_cookie(&cookies[0]) - .finish(); + // let resp = HttpResponse::Ok() + // .cookie( + // http::Cookie::build("name", "value") + // .domain("www.rust-lang.org") + // .path("/test") + // .http_only(true) + // .max_age(Duration::days(1)) + // .finish(), + // ).del_cookie(&cookies[0]) + // .finish(); - let mut val: Vec<_> = resp - .headers() - .get_all("Set-Cookie") - .iter() - .map(|v| v.to_str().unwrap().to_owned()) - .collect(); - val.sort(); - assert!(val[0].starts_with("cookie1=; Max-Age=0;")); - assert_eq!( - val[1], - "name=value; HttpOnly; Path=/test; Domain=www.rust-lang.org; Max-Age=86400" - ); - } + // let mut val: Vec<_> = resp + // .headers() + // .get_all("Set-Cookie") + // .iter() + // .map(|v| v.to_str().unwrap().to_owned()) + // .collect(); + // val.sort(); + // assert!(val[0].starts_with("cookie1=; Max-Age=0;")); + // assert_eq!( + // val[1], + // "name=value; HttpOnly; Path=/test; Domain=www.rust-lang.org; Max-Age=86400" + // ); + // } #[test] fn test_update_response_cookies() { @@ -1131,15 +1131,6 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().bin_ref(), &Binary::from("test")); - let resp: HttpResponse = "test".respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from("test")); - let resp: HttpResponse = b"test".as_ref().into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( @@ -1149,15 +1140,6 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().bin_ref(), &Binary::from(b"test".as_ref())); - let resp: HttpResponse = b"test".as_ref().respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(b"test".as_ref())); - let resp: HttpResponse = "test".to_owned().into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( @@ -1167,15 +1149,6 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().bin_ref(), &Binary::from("test".to_owned())); - let resp: HttpResponse = "test".to_owned().respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from("test".to_owned())); - let resp: HttpResponse = (&"test".to_owned()).into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( @@ -1185,15 +1158,6 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().bin_ref(), &Binary::from(&"test".to_owned())); - let resp: HttpResponse = (&"test".to_owned()).respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(&"test".to_owned())); - let b = Bytes::from_static(b"test"); let resp: HttpResponse = b.into(); assert_eq!(resp.status(), StatusCode::OK); @@ -1208,19 +1172,6 @@ mod tests { ); let b = Bytes::from_static(b"test"); - let resp: HttpResponse = b.respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.body().bin_ref(), - &Binary::from(Bytes::from_static(b"test")) - ); - - let b = BytesMut::from("test"); let resp: HttpResponse = b.into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( @@ -1231,7 +1182,7 @@ mod tests { assert_eq!(resp.body().bin_ref(), &Binary::from(BytesMut::from("test"))); let b = BytesMut::from("test"); - let resp: HttpResponse = b.respond_to(&req).ok().unwrap(); + let resp: HttpResponse = b.into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), diff --git a/src/json.rs b/src/json.rs index 04dd369eb..5c64b9bdd 100644 --- a/src/json.rs +++ b/src/json.rs @@ -3,18 +3,13 @@ use futures::{Future, Poll, Stream}; use http::header::CONTENT_LENGTH; use std::fmt; use std::ops::{Deref, DerefMut}; -use std::rc::Rc; use mime; use serde::de::DeserializeOwned; -use serde::Serialize; use serde_json; -use error::{Error, JsonPayloadError}; -use http::StatusCode; +use error::JsonPayloadError; use httpmessage::HttpMessage; -// use httprequest::HttpRequest; -use httpresponse::HttpResponse; /// Json helper /// @@ -30,7 +25,7 @@ use httpresponse::HttpResponse; /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// #[macro_use] extern crate serde_derive; /// use actix_web::{App, Json, Result, http}; @@ -57,7 +52,7 @@ use httpresponse::HttpResponse; /// to serialize into *JSON*. The type `T` must implement the `Serialize` /// trait from *serde*. /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// # #[macro_use] extern crate serde_derive; /// # use actix_web::*; @@ -124,7 +119,7 @@ where /// /// # Server example /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// # extern crate futures; /// # #[macro_use] extern crate serde_derive; @@ -243,9 +238,7 @@ mod tests { use futures::Async; use http::header; - use handler::Handler; use test::TestRequest; - use with::With; impl PartialEq for JsonPayloadError { fn eq(&self, other: &JsonPayloadError) -> bool { @@ -268,18 +261,6 @@ mod tests { name: String, } - #[test] - fn test_json() { - let json = Json(MyObject { - name: "test".to_owned(), - }); - let resp = json.respond_to(&TestRequest::default().finish()).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "application/json" - ); - } - #[test] fn test_json_body() { let req = TestRequest::default().finish(); @@ -323,24 +304,4 @@ mod tests { }) ); } - - #[test] - fn test_with_json() { - let mut cfg = JsonConfig::default(); - cfg.limit(4096); - let handler = With::new(|data: Json| data, cfg); - - let req = TestRequest::default().finish(); - assert!(handler.handle(&req).as_err().is_some()); - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - ).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()) - } } diff --git a/src/lib.rs b/src/lib.rs index ec86f0320..b9f90c7a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ //! Actix web is a small, pragmatic, and extremely fast web framework //! for Rust. //! -//! ```rust +//! ```rust,ignore //! use actix_web::{server, App, Path, Responder}; //! # use std::thread; //! @@ -78,10 +78,11 @@ //! `gzip`, `deflate` compression. //! #![cfg_attr(actix_nightly, feature(tool_lints))] -#![warn(missing_docs)] -#![allow(unused_imports, unused_variables, dead_code)] +// #![warn(missing_docs)] +// #![allow(unused_imports, unused_variables, dead_code)] extern crate actix; +extern crate actix_net; #[macro_use] extern crate log; extern crate base64; @@ -98,7 +99,12 @@ extern crate failure; extern crate lazy_static; #[macro_use] extern crate futures; +#[cfg(feature = "brotli")] +extern crate brotli2; extern crate cookie; +extern crate encoding; +#[cfg(feature = "flate2")] +extern crate flate2; extern crate http as modhttp; extern crate httparse; extern crate language_tags; @@ -106,6 +112,8 @@ extern crate mime; extern crate mime_guess; extern crate net2; extern crate rand; +extern crate serde; +extern crate serde_urlencoded; extern crate tokio_codec; extern crate tokio_current_thread; extern crate tokio_io; @@ -116,19 +124,10 @@ extern crate tokio_timer; extern crate tokio_uds; extern crate url; #[macro_use] -extern crate serde; -#[cfg(feature = "brotli")] -extern crate brotli2; -extern crate encoding; -#[cfg(feature = "flate2")] -extern crate flate2; -extern crate serde_urlencoded; -#[macro_use] extern crate percent_encoding; extern crate serde_json; extern crate smallvec; - -extern crate actix_net; +extern crate tokio; #[cfg(test)] #[macro_use] @@ -150,7 +149,7 @@ pub mod error; pub mod h1; pub(crate) mod helpers; pub mod server; -//pub mod test; +pub mod test; //pub mod ws; pub use body::{Binary, Body}; pub use error::{Error, ResponseError, Result}; @@ -170,7 +169,7 @@ pub mod dev { //! //! ``` //! # #![allow(unused_imports)] - //! use actix_web::dev::*; + //! use actix_http::dev::*; //! ``` pub use body::BodyStream; diff --git a/src/request.rs b/src/request.rs index a75fda3a0..82d8c22fa 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,7 +1,6 @@ use std::cell::{Cell, Ref, RefCell, RefMut}; use std::collections::VecDeque; use std::fmt; -use std::net::SocketAddr; use std::rc::Rc; use http::{header, HeaderMap, Method, Uri, Version}; @@ -31,7 +30,6 @@ pub(crate) struct InnerRequest { pub(crate) headers: HeaderMap, pub(crate) extensions: RefCell, pub(crate) payload: RefCell>, - pub(crate) stream_extensions: Option>, pool: &'static RequestPool, } @@ -81,7 +79,6 @@ impl Request { flags: Cell::new(MessageFlags::empty()), payload: RefCell::new(None), extensions: RefCell::new(Extensions::new()), - stream_extensions: None, }), } } @@ -170,15 +167,13 @@ impl Request { inner: self.inner.clone(), } } +} - pub(crate) fn release(self) { - let mut inner = self.inner; - if let Some(r) = Rc::get_mut(&mut inner) { - r.reset(); - } else { - return; +impl Drop for Request { + fn drop(&mut self) { + if Rc::strong_count(&self.inner) == 1 { + self.inner.pool.release(self.inner.clone()); } - inner.pool.release(inner); } } @@ -221,11 +216,13 @@ impl RequestPool { /// Get Request object #[inline] pub fn get(pool: &'static RequestPool) -> Request { - if let Some(msg) = pool.0.borrow_mut().pop_front() { - Request { inner: msg } - } else { - Request::with_pool(pool) + if let Some(mut msg) = pool.0.borrow_mut().pop_front() { + if let Some(r) = Rc::get_mut(&mut msg) { + r.reset(); + } + return Request { inner: msg }; } + Request::with_pool(pool) } #[inline] diff --git a/src/server/mod.rs b/src/server/mod.rs index 068094c2a..0abd7c216 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -106,12 +106,9 @@ //! let _ = sys.run(); //!} //! ``` -use std::net::{Shutdown, SocketAddr}; -use std::rc::Rc; +use std::net::SocketAddr; use std::{io, time}; -use bytes::{BufMut, BytesMut}; -use futures::{Async, Poll}; use tokio_io::{AsyncRead, AsyncWrite}; use tokio_tcp::TcpStream; @@ -123,16 +120,8 @@ pub(crate) mod output; #[doc(hidden)] pub use super::helpers::write_content_length; -use body::Binary; -use extensions::Extensions; -use header::ContentEncoding; -use httpresponse::HttpResponse; - -/// max buffer size 64k -pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; - -const LW_BUFFER_SIZE: usize = 4096; -const HW_BUFFER_SIZE: usize = 32_768; +// /// max buffer size 64k +// pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; #[derive(Debug, PartialEq, Clone, Copy)] /// Server keep-alive setting diff --git a/src/server/output.rs b/src/server/output.rs index f20bd3266..cfc85e4bc 100644 --- a/src/server/output.rs +++ b/src/server/output.rs @@ -1,3 +1,4 @@ +#![allow(unused_imports, unused_variables, dead_code)] use std::fmt::Write as FmtWrite; use std::io::Write; use std::str::FromStr; diff --git a/src/test.rs b/src/test.rs index d0cfb255a..3c48df643 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,10 +1,8 @@ //! Various helpers for Actix applications to use during testing. -use std::rc::Rc; +use std::net; use std::str::FromStr; -use std::sync::mpsc; -use std::{net, thread}; -use actix_inner::{Actor, Addr, System}; +use actix::System; use cookie::Cookie; use futures::Future; @@ -13,28 +11,12 @@ use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; use net2::TcpBuilder; use tokio::runtime::current_thread::Runtime; -#[cfg(any(feature = "alpn", feature = "ssl"))] -use openssl::ssl::SslAcceptorBuilder; -#[cfg(feature = "rust-tls")] -use rustls::ServerConfig; - -use application::{App, HttpApplication}; use body::Binary; -use client::{ClientConnector, ClientRequest, ClientRequestBuilder}; -use error::Error; -use handler::{AsyncResult, AsyncResultItem, Handler, Responder}; use header::{Header, IntoHeaderValue}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::Middleware; -use param::Params; use payload::Payload; -use resource::Resource; -use router::Router; -use server::message::{Request, RequestPool}; -use server::{HttpServer, IntoHttpHandler, ServerSettings}; +use request::Request; use uri::Url as InnerUrl; -use ws; +// use ws; /// The `TestServer` type. /// @@ -63,9 +45,8 @@ use ws; /// ``` pub struct TestServer { addr: net::SocketAddr, - ssl: bool, - conn: Addr, rt: Runtime, + ssl: bool, } impl TestServer { @@ -73,92 +54,11 @@ impl TestServer { /// /// This method accepts configuration method. You can add /// middlewares or set handlers for test application. - pub fn new(config: F) -> Self + pub fn new(_config: F) -> Self where - F: Clone + Send + 'static + Fn(&mut TestApp<()>), + F: Fn() + Clone + Send + 'static, { - TestServerBuilder::new(|| ()).start(config) - } - - /// Create test server builder - pub fn build() -> TestServerBuilder<(), impl Fn() -> () + Clone + Send + 'static> { - TestServerBuilder::new(|| ()) - } - - /// Create test server builder with specific state factory - /// - /// This method can be used for constructing application state. - /// Also it can be used for external dependency initialization, - /// like creating sync actors for diesel integration. - pub fn build_with_state(state: F) -> TestServerBuilder - where - F: Fn() -> S + Clone + Send + 'static, - S: 'static, - { - TestServerBuilder::new(state) - } - - /// Start new test server with application factory - pub fn with_factory(factory: F) -> Self - where - F: Fn() -> H + Send + Clone + 'static, - H: IntoHttpHandler + 'static, - { - let (tx, rx) = mpsc::channel(); - - // run server in separate thread - thread::spawn(move || { - let sys = System::new("actix-test-server"); - let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); - let local_addr = tcp.local_addr().unwrap(); - - let _ = HttpServer::new(factory) - .disable_signals() - .listen(tcp) - .keep_alive(5) - .start(); - - tx.send((System::current(), local_addr, TestServer::get_conn())) - .unwrap(); - sys.run(); - }); - - let (system, addr, conn) = rx.recv().unwrap(); - System::set_current(system); - TestServer { - addr, - conn, - ssl: false, - rt: Runtime::new().unwrap(), - } - } - - fn get_conn() -> Addr { - #[cfg(any(feature = "alpn", feature = "ssl"))] - { - use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; - - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - ClientConnector::with_connector(builder.build()).start() - } - #[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "ssl")) - ))] - { - use rustls::ClientConfig; - use std::fs::File; - use std::io::BufReader; - let mut config = ClientConfig::new(); - let pem_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); - config.root_store.add_pem_file(pem_file).unwrap(); - ClientConnector::with_connector(config).start() - } - #[cfg(not(any(feature = "alpn", feature = "ssl", feature = "rust-tls")))] - { - ClientConnector::default().start() - } + unimplemented!() } /// Get firat available unused address @@ -208,45 +108,45 @@ impl TestServer { self.rt.block_on(fut) } - /// Connect to websocket server at a given path - pub fn ws_at( - &mut self, path: &str, - ) -> Result<(ws::ClientReader, ws::ClientWriter), ws::ClientError> { - let url = self.url(path); - self.rt - .block_on(ws::Client::with_connector(url, self.conn.clone()).connect()) - } + // /// Connect to websocket server at a given path + // pub fn ws_at( + // &mut self, path: &str, + // ) -> Result<(ws::ClientReader, ws::ClientWriter), ws::ClientError> { + // let url = self.url(path); + // self.rt + // .block_on(ws::Client::with_connector(url, self.conn.clone()).connect()) + // } - /// Connect to a websocket server - pub fn ws( - &mut self, - ) -> Result<(ws::ClientReader, ws::ClientWriter), ws::ClientError> { - self.ws_at("/") - } + // /// Connect to a websocket server + // pub fn ws( + // &mut self, + // ) -> Result<(ws::ClientReader, ws::ClientWriter), ws::ClientError> { + // self.ws_at("/") + // } - /// Create `GET` request - pub fn get(&self) -> ClientRequestBuilder { - ClientRequest::get(self.url("/").as_str()) - } + // /// Create `GET` request + // pub fn get(&self) -> ClientRequestBuilder { + // ClientRequest::get(self.url("/").as_str()) + // } - /// Create `POST` request - pub fn post(&self) -> ClientRequestBuilder { - ClientRequest::post(self.url("/").as_str()) - } + // /// Create `POST` request + // pub fn post(&self) -> ClientRequestBuilder { + // ClientRequest::post(self.url("/").as_str()) + // } - /// Create `HEAD` request - pub fn head(&self) -> ClientRequestBuilder { - ClientRequest::head(self.url("/").as_str()) - } + // /// Create `HEAD` request + // pub fn head(&self) -> ClientRequestBuilder { + // ClientRequest::head(self.url("/").as_str()) + // } - /// Connect to test http server - pub fn client(&self, meth: Method, path: &str) -> ClientRequestBuilder { - ClientRequest::build() - .method(meth) - .uri(self.url(path).as_str()) - .with_connector(self.conn.clone()) - .take() - } + // /// Connect to test http server + // pub fn client(&self, meth: Method, path: &str) -> ClientRequestBuilder { + // ClientRequest::build() + // .method(meth) + // .uri(self.url(path).as_str()) + // .with_connector(self.conn.clone()) + // .take() + // } } impl Drop for TestServer { @@ -255,183 +155,98 @@ impl Drop for TestServer { } } -/// An `TestServer` builder -/// -/// This type can be used to construct an instance of `TestServer` through a -/// builder-like pattern. -pub struct TestServerBuilder -where - F: Fn() -> S + Send + Clone + 'static, -{ - state: F, - #[cfg(any(feature = "alpn", feature = "ssl"))] - ssl: Option, - #[cfg(feature = "rust-tls")] - rust_ssl: Option, -} +// /// An `TestServer` builder +// /// +// /// This type can be used to construct an instance of `TestServer` through a +// /// builder-like pattern. +// pub struct TestServerBuilder +// where +// F: Fn() -> S + Send + Clone + 'static, +// { +// state: F, +// } -impl TestServerBuilder -where - F: Fn() -> S + Send + Clone + 'static, -{ - /// Create a new test server - pub fn new(state: F) -> TestServerBuilder { - TestServerBuilder { - state, - #[cfg(any(feature = "alpn", feature = "ssl"))] - ssl: None, - #[cfg(feature = "rust-tls")] - rust_ssl: None, - } - } +// impl TestServerBuilder +// where +// F: Fn() -> S + Send + Clone + 'static, +// { +// /// Create a new test server +// pub fn new(state: F) -> TestServerBuilder { +// TestServerBuilder { state } +// } - #[cfg(any(feature = "alpn", feature = "ssl"))] - /// Create ssl server - pub fn ssl(mut self, ssl: SslAcceptorBuilder) -> Self { - self.ssl = Some(ssl); - self - } +// #[allow(unused_mut)] +// /// Configure test application and run test server +// pub fn start(mut self, config: C) -> TestServer +// where +// C: Fn(&mut TestApp) + Clone + Send + 'static, +// { +// let (tx, rx) = mpsc::channel(); - #[cfg(feature = "rust-tls")] - /// Create rust tls server - pub fn rustls(mut self, ssl: ServerConfig) -> Self { - self.rust_ssl = Some(ssl); - self - } +// let mut has_ssl = false; - #[allow(unused_mut)] - /// Configure test application and run test server - pub fn start(mut self, config: C) -> TestServer - where - C: Fn(&mut TestApp) + Clone + Send + 'static, - { - let (tx, rx) = mpsc::channel(); +// #[cfg(any(feature = "alpn", feature = "ssl"))] +// { +// has_ssl = has_ssl || self.ssl.is_some(); +// } - let mut has_ssl = false; +// #[cfg(feature = "rust-tls")] +// { +// has_ssl = has_ssl || self.rust_ssl.is_some(); +// } - #[cfg(any(feature = "alpn", feature = "ssl"))] - { - has_ssl = has_ssl || self.ssl.is_some(); - } +// // run server in separate thread +// thread::spawn(move || { +// let addr = TestServer::unused_addr(); - #[cfg(feature = "rust-tls")] - { - has_ssl = has_ssl || self.rust_ssl.is_some(); - } +// let sys = System::new("actix-test-server"); +// let state = self.state; +// let mut srv = HttpServer::new(move || { +// let mut app = TestApp::new(state()); +// config(&mut app); +// app +// }).workers(1) +// .keep_alive(5) +// .disable_signals(); - // run server in separate thread - thread::spawn(move || { - let addr = TestServer::unused_addr(); +// tx.send((System::current(), addr, TestServer::get_conn())) +// .unwrap(); - let sys = System::new("actix-test-server"); - let state = self.state; - let mut srv = HttpServer::new(move || { - let mut app = TestApp::new(state()); - config(&mut app); - app - }).workers(1) - .keep_alive(5) - .disable_signals(); +// #[cfg(any(feature = "alpn", feature = "ssl"))] +// { +// let ssl = self.ssl.take(); +// if let Some(ssl) = ssl { +// let tcp = net::TcpListener::bind(addr).unwrap(); +// srv = srv.listen_ssl(tcp, ssl).unwrap(); +// } +// } +// #[cfg(feature = "rust-tls")] +// { +// let ssl = self.rust_ssl.take(); +// if let Some(ssl) = ssl { +// let tcp = net::TcpListener::bind(addr).unwrap(); +// srv = srv.listen_rustls(tcp, ssl); +// } +// } +// if !has_ssl { +// let tcp = net::TcpListener::bind(addr).unwrap(); +// srv = srv.listen(tcp); +// } +// srv.start(); - tx.send((System::current(), addr, TestServer::get_conn())) - .unwrap(); +// sys.run(); +// }); - #[cfg(any(feature = "alpn", feature = "ssl"))] - { - let ssl = self.ssl.take(); - if let Some(ssl) = ssl { - let tcp = net::TcpListener::bind(addr).unwrap(); - srv = srv.listen_ssl(tcp, ssl).unwrap(); - } - } - #[cfg(feature = "rust-tls")] - { - let ssl = self.rust_ssl.take(); - if let Some(ssl) = ssl { - let tcp = net::TcpListener::bind(addr).unwrap(); - srv = srv.listen_rustls(tcp, ssl); - } - } - if !has_ssl { - let tcp = net::TcpListener::bind(addr).unwrap(); - srv = srv.listen(tcp); - } - srv.start(); - - sys.run(); - }); - - let (system, addr, conn) = rx.recv().unwrap(); - System::set_current(system); - TestServer { - addr, - conn, - ssl: has_ssl, - rt: Runtime::new().unwrap(), - } - } -} - -/// Test application helper for testing request handlers. -pub struct TestApp { - app: Option>, -} - -impl TestApp { - fn new(state: S) -> TestApp { - let app = App::with_state(state); - TestApp { app: Some(app) } - } - - /// Register handler for "/" - pub fn handler(&mut self, handler: F) - where - F: Fn(&HttpRequest) -> R + 'static, - R: Responder + 'static, - { - self.app = Some(self.app.take().unwrap().resource("/", |r| r.f(handler))); - } - - /// Register middleware - pub fn middleware(&mut self, mw: T) -> &mut TestApp - where - T: Middleware + 'static, - { - self.app = Some(self.app.take().unwrap().middleware(mw)); - self - } - - /// Register resource. This method is similar - /// to `App::resource()` method. - pub fn resource(&mut self, path: &str, f: F) -> &mut TestApp - where - F: FnOnce(&mut Resource) -> R + 'static, - { - self.app = Some(self.app.take().unwrap().resource(path, f)); - self - } -} - -impl IntoHttpHandler for TestApp { - type Handler = HttpApplication; - - fn into_handler(mut self) -> HttpApplication { - self.app.take().unwrap().into_handler() - } -} - -#[doc(hidden)] -impl Iterator for TestApp { - type Item = HttpApplication; - - fn next(&mut self) -> Option { - if let Some(mut app) = self.app.take() { - Some(app.finish()) - } else { - None - } - } -} +// let (system, addr, conn) = rx.recv().unwrap(); +// System::set_current(system); +// TestServer { +// addr, +// conn, +// ssl: has_ssl, +// rt: Runtime::new().unwrap(), +// } +// } +// } /// Test `HttpRequest` builder /// @@ -460,70 +275,49 @@ impl Iterator for TestApp { /// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); /// } /// ``` -pub struct TestRequest { - state: S, +pub struct TestRequest { version: Version, method: Method, uri: Uri, headers: HeaderMap, - params: Params, - cookies: Option>>, + _cookies: Option>>, payload: Option, prefix: u16, } -impl Default for TestRequest<()> { - fn default() -> TestRequest<()> { +impl Default for TestRequest { + fn default() -> TestRequest { TestRequest { - state: (), method: Method::GET, uri: Uri::from_str("/").unwrap(), version: Version::HTTP_11, headers: HeaderMap::new(), - params: Params::new(), - cookies: None, + _cookies: None, payload: None, prefix: 0, } } } -impl TestRequest<()> { +impl TestRequest { /// Create TestRequest and set request uri - pub fn with_uri(path: &str) -> TestRequest<()> { + pub fn with_uri(path: &str) -> TestRequest { TestRequest::default().uri(path) } /// Create TestRequest and set header - pub fn with_hdr(hdr: H) -> TestRequest<()> { + pub fn with_hdr(hdr: H) -> TestRequest { TestRequest::default().set(hdr) } /// Create TestRequest and set header - pub fn with_header(key: K, value: V) -> TestRequest<()> + pub fn with_header(key: K, value: V) -> TestRequest where HeaderName: HttpTryFrom, V: IntoHeaderValue, { TestRequest::default().header(key, value) } -} - -impl TestRequest { - /// Start HttpRequest build process with application state - pub fn with_state(state: S) -> TestRequest { - TestRequest { - state, - method: Method::GET, - uri: Uri::from_str("/").unwrap(), - version: Version::HTTP_11, - headers: HeaderMap::new(), - params: Params::new(), - cookies: None, - payload: None, - prefix: 0, - } - } /// Set HTTP version of this request pub fn version(mut self, ver: Version) -> Self { @@ -567,12 +361,6 @@ impl TestRequest { panic!("Can not create header"); } - /// Set request path pattern parameter - pub fn param(mut self, name: &'static str, value: &'static str) -> Self { - self.params.add_static(name, value); - self - } - /// Set request payload pub fn set_payload>(mut self, data: B) -> Self { let mut data = data.into(); @@ -588,23 +376,19 @@ impl TestRequest { self } - /// Complete request creation and generate `HttpRequest` instance - pub fn finish(self) -> HttpRequest { + /// Complete request creation and generate `Request` instance + pub fn finish(self) -> Request { let TestRequest { - state, method, uri, version, headers, - mut params, - cookies, + _cookies: _, payload, - prefix, + prefix: _, } = self; - let router = Router::<()>::default(); - let pool = RequestPool::pool(ServerSettings::default()); - let mut req = RequestPool::get(pool); + let mut req = Request::new(); { let inner = req.inner_mut(); inner.method = method; @@ -613,156 +397,94 @@ impl TestRequest { inner.headers = headers; *inner.payload.borrow_mut() = payload; } - params.set_url(req.url().clone()); - let mut info = router.route_info_params(0, params); - info.set_prefix(prefix); - - let mut req = HttpRequest::new(req, Rc::new(state), info); - req.set_cookies(cookies); + // req.set_cookies(cookies); req } - #[cfg(test)] - /// Complete request creation and generate `HttpRequest` instance - pub(crate) fn finish_with_router(self, router: Router) -> HttpRequest { - let TestRequest { - state, - method, - uri, - version, - headers, - mut params, - cookies, - payload, - prefix, - } = self; + // /// This method generates `HttpRequest` instance and runs handler + // /// with generated request. + // pub fn run>(self, h: &H) -> Result { + // let req = self.finish(); + // let resp = h.handle(&req); - let pool = RequestPool::pool(ServerSettings::default()); - let mut req = RequestPool::get(pool); - { - let inner = req.inner_mut(); - inner.method = method; - inner.url = InnerUrl::new(uri); - inner.version = version; - inner.headers = headers; - *inner.payload.borrow_mut() = payload; - } - params.set_url(req.url().clone()); - let mut info = router.route_info_params(0, params); - info.set_prefix(prefix); - let mut req = HttpRequest::new(req, Rc::new(state), info); - req.set_cookies(cookies); - req - } + // match resp.respond_to(&req) { + // Ok(resp) => match resp.into().into() { + // AsyncResultItem::Ok(resp) => Ok(resp), + // AsyncResultItem::Err(err) => Err(err), + // AsyncResultItem::Future(fut) => { + // let mut sys = System::new("test"); + // sys.block_on(fut) + // } + // }, + // Err(err) => Err(err.into()), + // } + // } - /// Complete request creation and generate server `Request` instance - pub fn request(self) -> Request { - let TestRequest { - method, - uri, - version, - headers, - payload, - .. - } = self; + // /// This method generates `HttpRequest` instance and runs handler + // /// with generated request. + // /// + // /// This method panics is handler returns actor. + // pub fn run_async(self, h: H) -> Result + // where + // H: Fn(HttpRequest) -> F + 'static, + // F: Future + 'static, + // R: Responder + 'static, + // E: Into + 'static, + // { + // let req = self.finish(); + // let fut = h(req.clone()); - let pool = RequestPool::pool(ServerSettings::default()); - let mut req = RequestPool::get(pool); - { - let inner = req.inner_mut(); - inner.method = method; - inner.url = InnerUrl::new(uri); - inner.version = version; - inner.headers = headers; - *inner.payload.borrow_mut() = payload; - } - req - } + // let mut sys = System::new("test"); + // match sys.block_on(fut) { + // Ok(r) => match r.respond_to(&req) { + // Ok(reply) => match reply.into().into() { + // AsyncResultItem::Ok(resp) => Ok(resp), + // _ => panic!("Nested async replies are not supported"), + // }, + // Err(e) => Err(e), + // }, + // Err(err) => Err(err), + // } + // } - /// This method generates `HttpRequest` instance and runs handler - /// with generated request. - pub fn run>(self, h: &H) -> Result { - let req = self.finish(); - let resp = h.handle(&req); + // /// This method generates `HttpRequest` instance and executes handler + // pub fn run_async_result(self, f: F) -> Result + // where + // F: FnOnce(&HttpRequest) -> R, + // R: Into>, + // { + // let req = self.finish(); + // let res = f(&req); - match resp.respond_to(&req) { - Ok(resp) => match resp.into().into() { - AsyncResultItem::Ok(resp) => Ok(resp), - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Future(fut) => { - let mut sys = System::new("test"); - sys.block_on(fut) - } - }, - Err(err) => Err(err.into()), - } - } + // match res.into().into() { + // AsyncResultItem::Ok(resp) => Ok(resp), + // AsyncResultItem::Err(err) => Err(err), + // AsyncResultItem::Future(fut) => { + // let mut sys = System::new("test"); + // sys.block_on(fut) + // } + // } + // } - /// This method generates `HttpRequest` instance and runs handler - /// with generated request. - /// - /// This method panics is handler returns actor. - pub fn run_async(self, h: H) -> Result - where - H: Fn(HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - { - let req = self.finish(); - let fut = h(req.clone()); + // /// This method generates `HttpRequest` instance and executes handler + // pub fn execute(self, f: F) -> Result + // where + // F: FnOnce(&HttpRequest) -> R, + // R: Responder + 'static, + // { + // let req = self.finish(); + // let resp = f(&req); - let mut sys = System::new("test"); - match sys.block_on(fut) { - Ok(r) => match r.respond_to(&req) { - Ok(reply) => match reply.into().into() { - AsyncResultItem::Ok(resp) => Ok(resp), - _ => panic!("Nested async replies are not supported"), - }, - Err(e) => Err(e), - }, - Err(err) => Err(err), - } - } - - /// This method generates `HttpRequest` instance and executes handler - pub fn run_async_result(self, f: F) -> Result - where - F: FnOnce(&HttpRequest) -> R, - R: Into>, - { - let req = self.finish(); - let res = f(&req); - - match res.into().into() { - AsyncResultItem::Ok(resp) => Ok(resp), - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Future(fut) => { - let mut sys = System::new("test"); - sys.block_on(fut) - } - } - } - - /// This method generates `HttpRequest` instance and executes handler - pub fn execute(self, f: F) -> Result - where - F: FnOnce(&HttpRequest) -> R, - R: Responder + 'static, - { - let req = self.finish(); - let resp = f(&req); - - match resp.respond_to(&req) { - Ok(resp) => match resp.into().into() { - AsyncResultItem::Ok(resp) => Ok(resp), - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Future(fut) => { - let mut sys = System::new("test"); - sys.block_on(fut) - } - }, - Err(err) => Err(err.into()), - } - } + // match resp.respond_to(&req) { + // Ok(resp) => match resp.into().into() { + // AsyncResultItem::Ok(resp) => Ok(resp), + // AsyncResultItem::Err(err) => Err(err), + // AsyncResultItem::Future(fut) => { + // let mut sys = System::new("test"); + // sys.block_on(fut) + // } + // }, + // Err(err) => Err(err.into()), + // } + // } } diff --git a/tests/test_h1v2.rs b/tests/test_h1v2.rs index e32481bc2..d06777b75 100644 --- a/tests/test_h1v2.rs +++ b/tests/test_h1v2.rs @@ -8,7 +8,6 @@ use std::thread; use actix::System; use actix_net::server::Server; -use actix_net::service::{IntoNewService, IntoService}; use actix_web::{client, test}; use futures::future; From 99a915e66870bc144aaab16eaf404c1f250f9288 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 4 Oct 2018 21:15:24 -0700 Subject: [PATCH 006/427] disable gh-pages update --- .travis.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 62867e030..aa8a44f25 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,12 +43,12 @@ script: fi # Upload docs -after_success: - - | - if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then - cargo doc --features "ssl,tls,rust-tls,session" --no-deps && - echo "" > target/doc/index.html && - git clone https://github.com/davisp/ghp-import.git && - ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc && - echo "Uploaded documentation" - fi +#after_success: +# - | +# if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then +# cargo doc --features "ssl,tls,rust-tls,session" --no-deps && +# echo "" > target/doc/index.html && +# git clone https://github.com/davisp/ghp-import.git && +# ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc && +# echo "Uploaded documentation" +# fi From df50e636f19ee864e15ba38e404845f8fb88b9e2 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 4 Oct 2018 21:18:36 -0700 Subject: [PATCH 007/427] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b092a1723..74024eb5a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Actix http [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# Actix http [![Build Status](https://travis-ci.org/fafhrd91/actix-http.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Actix http From e78014c65a0971c87e7913c8bf6b8aa46edf1ebd Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 4 Oct 2018 21:19:43 -0700 Subject: [PATCH 008/427] fix travis link in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 74024eb5a..7ddd532e8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Actix http [![Build Status](https://travis-ci.org/fafhrd91/actix-http.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# Actix http [![Build Status](https://travis-ci.org/fafhrd91/actix-http.svg?branch=master)](https://travis-ci.org/fafhrd91/actix-http) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Actix http From 7fdc18f9b9e278e6b64c14ddf3bff1a1f96e3c94 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 4 Oct 2018 23:39:11 -0700 Subject: [PATCH 009/427] calculate response parameters --- src/config.rs | 36 +-- src/error.rs | 4 +- src/h1/codec.rs | 36 ++- src/h1/dispatcher.rs | 89 +++++++- src/lib.rs | 2 +- src/server/output.rs | 530 +++++-------------------------------------- 6 files changed, 194 insertions(+), 503 deletions(-) diff --git a/src/config.rs b/src/config.rs index 543e78acd..36b949c33 100644 --- a/src/config.rs +++ b/src/config.rs @@ -21,7 +21,7 @@ pub struct ServiceConfig(Rc); struct Inner { keep_alive: Option, client_timeout: u64, - client_shutdown: u64, + client_disconnect: u64, ka_enabled: bool, date: UnsafeCell<(bool, Date)>, } @@ -35,7 +35,7 @@ impl Clone for ServiceConfig { impl ServiceConfig { /// Create instance of `ServiceConfig` pub(crate) fn new( - keep_alive: KeepAlive, client_timeout: u64, client_shutdown: u64, + keep_alive: KeepAlive, client_timeout: u64, client_disconnect: u64, ) -> ServiceConfig { let (keep_alive, ka_enabled) = match keep_alive { KeepAlive::Timeout(val) => (val as u64, true), @@ -52,7 +52,7 @@ impl ServiceConfig { keep_alive, ka_enabled, client_timeout, - client_shutdown, + client_disconnect, date: UnsafeCell::new((false, Date::new())), })) } @@ -100,9 +100,9 @@ impl ServiceConfig { } } - /// Client shutdown timer - pub fn client_shutdown_timer(&self) -> Option { - let delay = self.0.client_shutdown; + /// Client disconnect timer + pub fn client_disconnect_timer(&self) -> Option { + let delay = self.0.client_disconnect; if delay != 0 { Some(self.now() + Duration::from_millis(delay)) } else { @@ -184,7 +184,7 @@ impl ServiceConfig { pub struct ServiceConfigBuilder { keep_alive: KeepAlive, client_timeout: u64, - client_shutdown: u64, + client_disconnect: u64, host: String, addr: net::SocketAddr, secure: bool, @@ -196,7 +196,7 @@ impl ServiceConfigBuilder { ServiceConfigBuilder { keep_alive: KeepAlive::Timeout(5), client_timeout: 5000, - client_shutdown: 5000, + client_disconnect: 0, secure: false, host: "localhost".to_owned(), addr: "127.0.0.1:8080".parse().unwrap(), @@ -204,10 +204,14 @@ impl ServiceConfigBuilder { } /// Enable secure flag for current server. + /// This flags also enables `client disconnect timeout`. /// /// By default this flag is set to false. pub fn secure(mut self) -> Self { self.secure = true; + if self.client_disconnect == 0 { + self.client_disconnect = 3000; + } self } @@ -233,16 +237,16 @@ impl ServiceConfigBuilder { self } - /// Set server connection shutdown timeout in milliseconds. + /// Set server connection disconnect timeout in milliseconds. /// - /// Defines a timeout for shutdown connection. If a shutdown procedure does not complete - /// within this time, the request is dropped. This timeout affects only secure connections. + /// Defines a timeout for disconnect connection. If a disconnect procedure does not complete + /// within this time, the request get dropped. This timeout affects secure connections. /// /// To disable timeout set value to 0. /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_shutdown(mut self, val: u64) -> Self { - self.client_shutdown = val; + /// By default disconnect timeout is set to 3000 milliseconds. + pub fn client_disconnect(mut self, val: u64) -> Self { + self.client_disconnect = val; self } @@ -277,9 +281,7 @@ impl ServiceConfigBuilder { /// Finish service configuration and create `ServiceConfig` object. pub fn finish(self) -> ServiceConfig { - let client_shutdown = if self.secure { self.client_shutdown } else { 0 }; - - ServiceConfig::new(self.keep_alive, self.client_timeout, client_shutdown) + ServiceConfig::new(self.keep_alive, self.client_timeout, self.client_disconnect) } } diff --git a/src/error.rs b/src/error.rs index 21aabac49..fb5df2328 100644 --- a/src/error.rs +++ b/src/error.rs @@ -397,9 +397,9 @@ pub enum DispatchError { // #[fail(display = "The first request did not complete within the specified timeout")] SlowRequestTimeout, - /// Shutdown timeout + /// Disconnect timeout. Makes sense for ssl streams. // #[fail(display = "Connection shutdown timeout")] - ShutdownTimeout, + DisconnectTimeout, /// Payload is not consumed // #[fail(display = "Task is completed but request's payload is not consumed")] diff --git a/src/h1/codec.rs b/src/h1/codec.rs index f1b526d52..ac54194ab 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -10,7 +10,7 @@ use body::Body; use error::ParseError; use helpers; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; -use http::Version; +use http::{Method, Version}; use httpresponse::HttpResponse; use request::RequestPool; use server::output::{ResponseInfo, ResponseLength}; @@ -27,6 +27,8 @@ pub enum OutMessage { pub struct Codec { decoder: H1Decoder, encoder: H1Writer, + head: bool, + version: Version, } impl Codec { @@ -40,6 +42,8 @@ impl Codec { Codec { decoder: H1Decoder::with_pool(pool), encoder: H1Writer::new(), + head: false, + version: Version::HTTP_11, } } } @@ -49,7 +53,17 @@ impl Decoder for Codec { type Error = ParseError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - self.decoder.decode(src) + let res = self.decoder.decode(src); + + match res { + Ok(Some(InMessage::Message(ref req))) + | Ok(Some(InMessage::MessageWithPayload(ref req))) => { + self.head = req.inner.method == Method::HEAD; + self.version = req.inner.version; + } + _ => (), + } + res } } @@ -62,7 +76,7 @@ impl Encoder for Codec { ) -> Result<(), Self::Error> { match item { OutMessage::Response(res) => { - self.encoder.encode(res, dst)?; + self.encoder.encode(res, dst, self.head, self.version)?; } OutMessage::Payload(bytes) => { dst.extend_from_slice(&bytes); @@ -87,6 +101,7 @@ struct H1Writer { flags: Flags, written: u64, headers_size: u32, + info: ResponseInfo, } impl H1Writer { @@ -95,6 +110,7 @@ impl H1Writer { flags: Flags::empty(), written: 0, headers_size: 0, + info: ResponseInfo::default(), } } @@ -116,10 +132,11 @@ impl H1Writer { } fn encode( - &mut self, mut msg: HttpResponse, buffer: &mut BytesMut, + &mut self, mut msg: HttpResponse, buffer: &mut BytesMut, head: bool, + version: Version, ) -> io::Result<()> { // prepare task - let info = ResponseInfo::new(false); // req.inner.method == Method::HEAD); + self.info.update(&mut msg, head, version); //if msg.keep_alive().unwrap_or_else(|| req.keep_alive()) { //self.flags = Flags::STARTED | Flags::KEEPALIVE; @@ -166,7 +183,7 @@ impl H1Writer { buffer.extend_from_slice(reason); // content length - match info.length { + match self.info.length { ResponseLength::Chunked => { buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") } @@ -183,11 +200,6 @@ impl H1Writer { } ResponseLength::None => buffer.extend_from_slice(b"\r\n"), } - if let Some(ce) = info.content_encoding { - buffer.extend_from_slice(b"content-encoding: "); - buffer.extend_from_slice(ce.as_ref()); - buffer.extend_from_slice(b"\r\n"); - } // write headers let mut pos = 0; @@ -197,7 +209,7 @@ impl H1Writer { for (key, value) in msg.headers() { match *key { TRANSFER_ENCODING => continue, - CONTENT_LENGTH => match info.length { + CONTENT_LENGTH => match self.info.length { ResponseLength::None => (), _ => continue, }, diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index eda8ebf00..f777648ec 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -1,7 +1,7 @@ // #![allow(unused_imports, unused_variables, dead_code)] use std::collections::VecDeque; use std::fmt::{Debug, Display}; -// use std::time::{Duration, Instant}; +use std::time::Instant; use actix_net::service::Service; @@ -9,7 +9,7 @@ use futures::{Async, AsyncSink, Future, Poll, Sink, Stream}; use tokio_codec::Framed; // use tokio_current_thread::spawn; use tokio_io::{AsyncRead, AsyncWrite}; -// use tokio_timer::Delay; +use tokio_timer::Delay; use error::{ParseError, PayloadError}; use payload::{Payload, PayloadStatus, PayloadWriter}; @@ -47,12 +47,14 @@ where flags: Flags, framed: Framed, error: Option>, + config: ServiceConfig, state: State, payload: Option, messages: VecDeque, - config: ServiceConfig, + ka_expire: Instant, + ka_timer: Option, } enum State { @@ -81,9 +83,28 @@ where { /// Create http/1 dispatcher. pub fn new(stream: T, config: ServiceConfig, service: S) -> Self { - let flags = Flags::FLUSHED; + Dispatcher::with_timeout(stream, config, None, service) + } + + /// Create http/1 dispatcher with slow request timeout. + pub fn with_timeout( + stream: T, config: ServiceConfig, timeout: Option, service: S, + ) -> Self { + let flags = if config.keep_alive_enabled() { + Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED | Flags::FLUSHED + } else { + Flags::FLUSHED + }; let framed = Framed::new(stream, Codec::new()); + let (ka_expire, ka_timer) = if let Some(delay) = timeout { + (delay.deadline(), Some(delay)) + } else if let Some(delay) = config.keep_alive_timer() { + (delay.deadline(), Some(delay)) + } else { + (config.now(), None) + }; + Dispatcher { payload: None, state: State::None, @@ -93,6 +114,8 @@ where flags, framed, config, + ka_expire, + ka_timer, } } @@ -358,8 +381,64 @@ where } } + if self.ka_timer.is_some() && updated { + if let Some(expire) = self.config.keep_alive_expire() { + self.ka_expire = expire; + } + } Ok(updated) } + + /// keep-alive timer + fn poll_keepalive(&mut self) -> Result<(), DispatchError> { + if let Some(ref mut timer) = self.ka_timer { + match timer.poll() { + Ok(Async::Ready(_)) => { + if timer.deadline() >= self.ka_expire { + // check for any outstanding request handling + if self.state.is_empty() && self.messages.is_empty() { + // if we get timer during shutdown, just drop connection + if self.flags.contains(Flags::SHUTDOWN) { + return Err(DispatchError::DisconnectTimeout); + } else if !self.flags.contains(Flags::STARTED) { + // timeout on first request (slow request) return 408 + trace!("Slow request timeout"); + self.flags + .insert(Flags::STARTED | Flags::READ_DISCONNECTED); + self.state = + State::SendResponse(Some(OutMessage::Response( + HttpResponse::RequestTimeout().finish(), + ))); + } else { + trace!("Keep-alive timeout, close connection"); + self.flags.insert(Flags::SHUTDOWN); + + // start shutdown timer + if let Some(deadline) = + self.config.client_disconnect_timer() + { + timer.reset(deadline) + } else { + return Ok(()); + } + } + } else if let Some(deadline) = self.config.keep_alive_expire() { + timer.reset(deadline) + } + } else { + timer.reset(self.ka_expire) + } + } + Ok(Async::NotReady) => (), + Err(e) => { + error!("Timer error {:?}", e); + return Err(DispatchError::Unknown); + } + } + } + + Ok(()) + } } impl Future for Dispatcher @@ -373,6 +452,8 @@ where #[inline] fn poll(&mut self) -> Poll<(), Self::Error> { + self.poll_keepalive()?; + // shutdown if self.flags.contains(Flags::SHUTDOWN) { if self.flags.contains(Flags::WRITE_DISCONNECTED) { diff --git a/src/lib.rs b/src/lib.rs index b9f90c7a2..6215bc4fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,7 +79,7 @@ //! #![cfg_attr(actix_nightly, feature(tool_lints))] // #![warn(missing_docs)] -// #![allow(unused_imports, unused_variables, dead_code)] +#![allow(dead_code)] extern crate actix; extern crate actix_net; diff --git a/src/server/output.rs b/src/server/output.rs index cfc85e4bc..5fc6fc839 100644 --- a/src/server/output.rs +++ b/src/server/output.rs @@ -4,26 +4,15 @@ use std::io::Write; use std::str::FromStr; use std::{cmp, fmt, io, mem}; -#[cfg(feature = "brotli")] -use brotli2::write::BrotliEncoder; -use bytes::BytesMut; -#[cfg(feature = "flate2")] -use flate2::write::{GzEncoder, ZlibEncoder}; -#[cfg(feature = "flate2")] -use flate2::Compression; +use bytes::{Bytes, BytesMut}; use http::header::{HeaderValue, ACCEPT_ENCODING, CONTENT_LENGTH}; use http::{StatusCode, Version}; use body::{Binary, Body}; use header::ContentEncoding; +use http::Method; use httpresponse::HttpResponse; -use request::InnerRequest; - -// #[derive(Debug)] -// pub(crate) struct RequestInfo { -// pub version: Version, -// pub accept_encoding: Option, -// } +use request::Request; #[derive(Debug)] pub(crate) enum ResponseLength { @@ -38,285 +27,91 @@ pub(crate) enum ResponseLength { pub(crate) struct ResponseInfo { head: bool, pub length: ResponseLength, - pub content_encoding: Option<&'static str>, + pub te: TransferEncoding, +} + +impl Default for ResponseInfo { + fn default() -> Self { + ResponseInfo { + head: false, + length: ResponseLength::None, + te: TransferEncoding::empty(), + } + } } impl ResponseInfo { - pub fn new(head: bool) -> Self { - ResponseInfo { - head, - length: ResponseLength::None, - content_encoding: None, - } - } -} + pub fn update(&mut self, resp: &mut HttpResponse, head: bool, version: Version) { + self.head = head; -#[derive(Debug)] -pub(crate) enum Output { - Empty(BytesMut), - Buffer(BytesMut), - Encoder(ContentEncoder), - TE(TransferEncoding), - Done, -} - -impl Output { - pub fn take(&mut self) -> BytesMut { - match mem::replace(self, Output::Done) { - Output::Empty(bytes) => bytes, - Output::Buffer(bytes) => bytes, - Output::Encoder(mut enc) => enc.take_buf(), - Output::TE(mut te) => te.take(), - Output::Done => panic!(), - } - } - - pub fn take_option(&mut self) -> Option { - match mem::replace(self, Output::Done) { - Output::Empty(bytes) => Some(bytes), - Output::Buffer(bytes) => Some(bytes), - Output::Encoder(mut enc) => Some(enc.take_buf()), - Output::TE(mut te) => Some(te.take()), - Output::Done => None, - } - } - - pub fn as_ref(&mut self) -> &BytesMut { - match self { - Output::Empty(ref mut bytes) => bytes, - Output::Buffer(ref mut bytes) => bytes, - Output::Encoder(ref mut enc) => enc.buf_ref(), - Output::TE(ref mut te) => te.buf_ref(), - Output::Done => panic!(), - } - } - pub fn as_mut(&mut self) -> &mut BytesMut { - match self { - Output::Empty(ref mut bytes) => bytes, - Output::Buffer(ref mut bytes) => bytes, - Output::Encoder(ref mut enc) => enc.buf_mut(), - Output::TE(ref mut te) => te.buf_mut(), - Output::Done => panic!(), - } - } - pub fn split_to(&mut self, cap: usize) -> BytesMut { - match self { - Output::Empty(ref mut bytes) => bytes.split_to(cap), - Output::Buffer(ref mut bytes) => bytes.split_to(cap), - Output::Encoder(ref mut enc) => enc.buf_mut().split_to(cap), - Output::TE(ref mut te) => te.buf_mut().split_to(cap), - Output::Done => BytesMut::new(), - } - } - - pub fn len(&self) -> usize { - match self { - Output::Empty(ref bytes) => bytes.len(), - Output::Buffer(ref bytes) => bytes.len(), - Output::Encoder(ref enc) => enc.len(), - Output::TE(ref te) => te.len(), - Output::Done => 0, - } - } - - pub fn is_empty(&self) -> bool { - match self { - Output::Empty(ref bytes) => bytes.is_empty(), - Output::Buffer(ref bytes) => bytes.is_empty(), - Output::Encoder(ref enc) => enc.is_empty(), - Output::TE(ref te) => te.is_empty(), - Output::Done => true, - } - } - - pub fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { - match self { - Output::Buffer(ref mut bytes) => { - bytes.extend_from_slice(data); - Ok(()) - } - Output::Encoder(ref mut enc) => enc.write(data), - Output::TE(ref mut te) => te.encode(data).map(|_| ()), - Output::Empty(_) | Output::Done => Ok(()), - } - } - - pub fn write_eof(&mut self) -> Result { - match self { - Output::Buffer(_) => Ok(true), - Output::Encoder(ref mut enc) => enc.write_eof(), - Output::TE(ref mut te) => Ok(te.encode_eof()), - Output::Empty(_) | Output::Done => Ok(true), - } - } - - pub(crate) fn for_server( - &mut self, info: &mut ResponseInfo, req: &InnerRequest, resp: &mut HttpResponse, - response_encoding: ContentEncoding, - ) { - let buf = self.take(); - let version = resp.version().unwrap_or_else(|| req.version); + let version = resp.version().unwrap_or_else(|| version); let mut len = 0; let has_body = match resp.body() { Body::Empty => false, Body::Binary(ref bin) => { len = bin.len(); - !(response_encoding == ContentEncoding::Auto && len < 96) + true } _ => true, }; - // Enable content encoding only if response does not contain Content-Encoding - // header - #[cfg(any(feature = "brotli", feature = "flate2"))] - let mut encoding = if has_body { - let encoding = match response_encoding { - ContentEncoding::Auto => { - // negotiate content-encoding - if let Some(val) = req.headers.get(ACCEPT_ENCODING) { - if let Ok(enc) = val.to_str() { - AcceptEncoding::parse(enc) - } else { - ContentEncoding::Identity - } - } else { - ContentEncoding::Identity - } - } - encoding => encoding, - }; - if encoding.is_compression() { - info.content_encoding = Some(encoding.as_str()); - } - encoding - } else { - ContentEncoding::Identity + let has_body = match resp.body() { + Body::Empty => false, + _ => true, }; - #[cfg(not(any(feature = "brotli", feature = "flate2")))] - let mut encoding = ContentEncoding::Identity; let transfer = match resp.body() { Body::Empty => { - if !info.head { - info.length = match resp.status() { + if !self.head { + self.length = match resp.status() { StatusCode::NO_CONTENT | StatusCode::CONTINUE | StatusCode::SWITCHING_PROTOCOLS | StatusCode::PROCESSING => ResponseLength::None, _ => ResponseLength::Zero, }; + } else { + self.length = ResponseLength::Zero; } - *self = Output::Empty(buf); - return; + TransferEncoding::empty() } Body::Binary(_) => { - #[cfg(any(feature = "brotli", feature = "flate2"))] - { - if !(encoding == ContentEncoding::Identity - || encoding == ContentEncoding::Auto) - { - let mut tmp = BytesMut::new(); - let mut transfer = TransferEncoding::eof(tmp); - let mut enc = match encoding { - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => ContentEncoder::Deflate( - ZlibEncoder::new(transfer, Compression::fast()), - ), - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => ContentEncoder::Gzip( - GzEncoder::new(transfer, Compression::fast()), - ), - #[cfg(feature = "brotli")] - ContentEncoding::Br => { - ContentEncoder::Br(BrotliEncoder::new(transfer, 3)) - } - ContentEncoding::Identity | ContentEncoding::Auto => { - unreachable!() - } - }; - - let bin = resp.replace_body(Body::Empty).binary(); - - // TODO return error! - let _ = enc.write(bin.as_ref()); - let _ = enc.write_eof(); - let body = enc.buf_mut().take(); - len = body.len(); - resp.replace_body(Binary::from(body)); - } - } - - info.length = ResponseLength::Length(len); - if info.head { - *self = Output::Empty(buf); - } else { - *self = Output::Buffer(buf); - } - return; + self.length = ResponseLength::Length(len); + TransferEncoding::length(len as u64) } Body::Streaming(_) => { if resp.upgrade() { - if version == Version::HTTP_2 { - error!("Connection upgrade is forbidden for HTTP/2"); - } - if encoding != ContentEncoding::Identity { - encoding = ContentEncoding::Identity; - info.content_encoding.take(); - } - TransferEncoding::eof(buf) + self.length = ResponseLength::None; + TransferEncoding::eof() } else { - if !(encoding == ContentEncoding::Identity - || encoding == ContentEncoding::Auto) - { - resp.headers_mut().remove(CONTENT_LENGTH); - } - Output::streaming_encoding(info, buf, version, resp) + self.streaming_encoding(version, resp) } } }; // check for head response - if info.head { + if self.head { resp.set_body(Body::Empty); - *self = Output::Empty(transfer.buf.unwrap()); - return; + } else { + self.te = transfer; } - - let enc = match encoding { - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => { - ContentEncoder::Deflate(ZlibEncoder::new(transfer, Compression::fast())) - } - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => { - ContentEncoder::Gzip(GzEncoder::new(transfer, Compression::fast())) - } - #[cfg(feature = "brotli")] - ContentEncoding::Br => ContentEncoder::Br(BrotliEncoder::new(transfer, 3)), - ContentEncoding::Identity | ContentEncoding::Auto => { - *self = Output::TE(transfer); - return; - } - }; - *self = Output::Encoder(enc); } fn streaming_encoding( - info: &mut ResponseInfo, buf: BytesMut, version: Version, - resp: &mut HttpResponse, + &mut self, version: Version, resp: &mut HttpResponse, ) -> TransferEncoding { match resp.chunked() { Some(true) => { // Enable transfer encoding if version == Version::HTTP_2 { - info.length = ResponseLength::None; - TransferEncoding::eof(buf) + self.length = ResponseLength::None; + TransferEncoding::eof() } else { - info.length = ResponseLength::Chunked; - TransferEncoding::chunked(buf) + self.length = ResponseLength::Chunked; + TransferEncoding::chunked() } } - Some(false) => TransferEncoding::eof(buf), + Some(false) => TransferEncoding::eof(), None => { // if Content-Length is specified, then use it as length hint let (len, chunked) = @@ -339,21 +134,21 @@ impl Output { if !chunked { if let Some(len) = len { - info.length = ResponseLength::Length64(len); - TransferEncoding::length(len, buf) + self.length = ResponseLength::Length64(len); + TransferEncoding::length(len) } else { - TransferEncoding::eof(buf) + TransferEncoding::eof() } } else { // Enable transfer encoding match version { Version::HTTP_11 => { - info.length = ResponseLength::Chunked; - TransferEncoding::chunked(buf) + self.length = ResponseLength::Chunked; + TransferEncoding::chunked() } _ => { - info.length = ResponseLength::None; - TransferEncoding::eof(buf) + self.length = ResponseLength::None; + TransferEncoding::eof() } } } @@ -362,178 +157,9 @@ impl Output { } } -pub(crate) enum ContentEncoder { - #[cfg(feature = "flate2")] - Deflate(ZlibEncoder), - #[cfg(feature = "flate2")] - Gzip(GzEncoder), - #[cfg(feature = "brotli")] - Br(BrotliEncoder), - Identity(TransferEncoding), -} - -impl fmt::Debug for ContentEncoder { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(_) => writeln!(f, "ContentEncoder(Brotli)"), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(_) => writeln!(f, "ContentEncoder(Deflate)"), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(_) => writeln!(f, "ContentEncoder(Gzip)"), - ContentEncoder::Identity(_) => writeln!(f, "ContentEncoder(Identity)"), - } - } -} - -impl ContentEncoder { - #[inline] - pub fn len(&self) -> usize { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref encoder) => encoder.get_ref().len(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref encoder) => encoder.get_ref().len(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref encoder) => encoder.get_ref().len(), - ContentEncoder::Identity(ref encoder) => encoder.len(), - } - } - - #[inline] - pub fn is_empty(&self) -> bool { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref encoder) => encoder.get_ref().is_empty(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref encoder) => encoder.get_ref().is_empty(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref encoder) => encoder.get_ref().is_empty(), - ContentEncoder::Identity(ref encoder) => encoder.is_empty(), - } - } - - #[inline] - pub(crate) fn take_buf(&mut self) -> BytesMut { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), - ContentEncoder::Identity(ref mut encoder) => encoder.take(), - } - } - - #[inline] - pub(crate) fn buf_mut(&mut self) -> &mut BytesMut { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref mut encoder) => encoder.get_mut().buf_mut(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().buf_mut(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().buf_mut(), - ContentEncoder::Identity(ref mut encoder) => encoder.buf_mut(), - } - } - - #[inline] - pub(crate) fn buf_ref(&mut self) -> &BytesMut { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref mut encoder) => encoder.get_mut().buf_ref(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().buf_ref(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().buf_ref(), - ContentEncoder::Identity(ref mut encoder) => encoder.buf_ref(), - } - } - - #[cfg_attr(feature = "cargo-clippy", allow(clippy::inline_always))] - #[inline(always)] - pub fn write_eof(&mut self) -> Result { - let encoder = - mem::replace(self, ContentEncoder::Identity(TransferEncoding::empty())); - - match encoder { - #[cfg(feature = "brotli")] - ContentEncoder::Br(encoder) => match encoder.finish() { - Ok(mut writer) => { - writer.encode_eof(); - *self = ContentEncoder::Identity(writer); - Ok(true) - } - Err(err) => Err(err), - }, - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(encoder) => match encoder.finish() { - Ok(mut writer) => { - writer.encode_eof(); - *self = ContentEncoder::Identity(writer); - Ok(true) - } - Err(err) => Err(err), - }, - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(encoder) => match encoder.finish() { - Ok(mut writer) => { - writer.encode_eof(); - *self = ContentEncoder::Identity(writer); - Ok(true) - } - Err(err) => Err(err), - }, - ContentEncoder::Identity(mut writer) => { - let res = writer.encode_eof(); - *self = ContentEncoder::Identity(writer); - Ok(res) - } - } - } - - #[cfg_attr(feature = "cargo-clippy", allow(clippy::inline_always))] - #[inline(always)] - pub fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { - Ok(_) => Ok(()), - Err(err) => { - trace!("Error decoding br encoding: {}", err); - Err(err) - } - }, - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { - Ok(_) => Ok(()), - Err(err) => { - trace!("Error decoding gzip encoding: {}", err); - Err(err) - } - }, - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { - Ok(_) => Ok(()), - Err(err) => { - trace!("Error decoding deflate encoding: {}", err); - Err(err) - } - }, - ContentEncoder::Identity(ref mut encoder) => { - encoder.encode(data)?; - Ok(()) - } - } - } -} - /// Encoders to handle different Transfer-Encodings. #[derive(Debug)] pub(crate) struct TransferEncoding { - buf: Option, kind: TransferEncodingKind, } @@ -552,65 +178,41 @@ enum TransferEncodingKind { } impl TransferEncoding { - fn take(&mut self) -> BytesMut { - self.buf.take().unwrap() - } - - fn buf_ref(&mut self) -> &BytesMut { - self.buf.as_ref().unwrap() - } - - fn len(&self) -> usize { - self.buf.as_ref().unwrap().len() - } - - fn is_empty(&self) -> bool { - self.buf.as_ref().unwrap().is_empty() - } - - fn buf_mut(&mut self) -> &mut BytesMut { - self.buf.as_mut().unwrap() - } - #[inline] pub fn empty() -> TransferEncoding { TransferEncoding { - buf: None, kind: TransferEncodingKind::Eof, } } #[inline] - pub fn eof(buf: BytesMut) -> TransferEncoding { + pub fn eof() -> TransferEncoding { TransferEncoding { - buf: Some(buf), kind: TransferEncodingKind::Eof, } } #[inline] - pub fn chunked(buf: BytesMut) -> TransferEncoding { + pub fn chunked() -> TransferEncoding { TransferEncoding { - buf: Some(buf), kind: TransferEncodingKind::Chunked(false), } } #[inline] - pub fn length(len: u64, buf: BytesMut) -> TransferEncoding { + pub fn length(len: u64) -> TransferEncoding { TransferEncoding { - buf: Some(buf), kind: TransferEncodingKind::Length(len), } } /// Encode message. Return `EOF` state of encoder #[inline] - pub fn encode(&mut self, msg: &[u8]) -> io::Result { + pub fn encode(&mut self, msg: &[u8], buf: &mut BytesMut) -> io::Result { match self.kind { TransferEncodingKind::Eof => { let eof = msg.is_empty(); - self.buf.as_mut().unwrap().extend_from_slice(msg); + buf.extend_from_slice(msg); Ok(eof) } TransferEncodingKind::Chunked(ref mut eof) => { @@ -620,17 +222,14 @@ impl TransferEncoding { if msg.is_empty() { *eof = true; - self.buf.as_mut().unwrap().extend_from_slice(b"0\r\n\r\n"); + buf.extend_from_slice(b"0\r\n\r\n"); } else { - let mut buf = BytesMut::new(); - writeln!(&mut buf, "{:X}\r", msg.len()) + writeln!(buf.as_mut(), "{:X}\r", msg.len()) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - let b = self.buf.as_mut().unwrap(); - b.reserve(buf.len() + msg.len() + 2); - b.extend_from_slice(buf.as_ref()); - b.extend_from_slice(msg); - b.extend_from_slice(b"\r\n"); + buf.reserve(msg.len() + 2); + buf.extend_from_slice(msg); + buf.extend_from_slice(b"\r\n"); } Ok(*eof) } @@ -641,10 +240,7 @@ impl TransferEncoding { } let len = cmp::min(*remaining, msg.len() as u64); - self.buf - .as_mut() - .unwrap() - .extend_from_slice(&msg[..len as usize]); + buf.extend_from_slice(&msg[..len as usize]); *remaining -= len as u64; Ok(*remaining == 0) @@ -657,14 +253,14 @@ impl TransferEncoding { /// Encode eof. Return `EOF` state of encoder #[inline] - pub fn encode_eof(&mut self) -> bool { + pub fn encode_eof(&mut self, buf: &mut BytesMut) -> bool { match self.kind { TransferEncodingKind::Eof => true, TransferEncodingKind::Length(rem) => rem == 0, TransferEncodingKind::Chunked(ref mut eof) => { if !*eof { *eof = true; - self.buf.as_mut().unwrap().extend_from_slice(b"0\r\n\r\n"); + buf.extend_from_slice(b"0\r\n\r\n"); } true } @@ -675,9 +271,9 @@ impl TransferEncoding { impl io::Write for TransferEncoding { #[inline] fn write(&mut self, buf: &[u8]) -> io::Result { - if self.buf.is_some() { - self.encode(buf)?; - } + // if self.buf.is_some() { + // self.encode(buf)?; + // } Ok(buf.len()) } From caa5a54b8f965484d2fb1fb5584f9a6047dc57b8 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 4 Oct 2018 23:46:43 -0700 Subject: [PATCH 010/427] fix test and remove unused code --- src/h1/decoder.rs | 8 +- src/httprequest.rs | 545 ------------------------------------------- src/server/output.rs | 89 +------ 3 files changed, 12 insertions(+), 630 deletions(-) delete mode 100644 src/httprequest.rs diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 66f24628c..90946b453 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -510,21 +510,17 @@ impl ChunkedState { #[cfg(test)] mod tests { - use std::net::Shutdown; - use std::{cmp, io, time}; + use std::{cmp, io}; - use actix::System; use bytes::{Buf, Bytes, BytesMut}; - use futures::{future, future::ok}; use http::{Method, Version}; use tokio_io::{AsyncRead, AsyncWrite}; use super::*; use error::ParseError; - use h1::{Dispatcher, InMessage}; + use h1::InMessage; use httpmessage::HttpMessage; use request::Request; - use server::KeepAlive; impl InMessage { fn message(self) -> Request { diff --git a/src/httprequest.rs b/src/httprequest.rs deleted file mode 100644 index d8c49496a..000000000 --- a/src/httprequest.rs +++ /dev/null @@ -1,545 +0,0 @@ -//! HTTP Request message related code. -use std::cell::{Ref, RefMut}; -use std::collections::HashMap; -use std::net::SocketAddr; -use std::ops::Deref; -use std::rc::Rc; -use std::{fmt, str}; - -use cookie::Cookie; -use futures_cpupool::CpuPool; -use http::{header, HeaderMap, Method, StatusCode, Uri, Version}; -use url::{form_urlencoded, Url}; - -use body::Body; -use error::{CookieParseError, UrlGenerationError}; -use extensions::Extensions; -use handler::FromRequest; -use httpmessage::HttpMessage; -use httpresponse::{HttpResponse, HttpResponseBuilder}; -use info::ConnectionInfo; -use param::Params; -use payload::Payload; -use router::ResourceInfo; -use server::Request; - -struct Query(HashMap); -struct Cookies(Vec>); - -/// An HTTP Request -pub struct HttpRequest { - req: Option, - state: Rc, - resource: ResourceInfo, -} - -impl HttpMessage for HttpRequest { - type Stream = Payload; - - #[inline] - fn headers(&self) -> &HeaderMap { - self.request().headers() - } - - #[inline] - fn payload(&self) -> Payload { - if let Some(payload) = self.request().inner.payload.borrow_mut().take() { - payload - } else { - Payload::empty() - } - } -} - -impl Deref for HttpRequest { - type Target = Request; - - fn deref(&self) -> &Request { - self.request() - } -} - -impl HttpRequest { - #[inline] - pub(crate) fn new( - req: Request, state: Rc, resource: ResourceInfo, - ) -> HttpRequest { - HttpRequest { - state, - resource, - req: Some(req), - } - } - - #[inline] - /// Construct new http request with state. - pub(crate) fn with_state(&self, state: Rc) -> HttpRequest { - HttpRequest { - state, - req: self.req.as_ref().map(|r| r.clone()), - resource: self.resource.clone(), - } - } - - /// Construct new http request with empty state. - pub fn drop_state(&self) -> HttpRequest { - HttpRequest { - state: Rc::new(()), - req: self.req.as_ref().map(|r| r.clone()), - resource: self.resource.clone(), - } - } - - #[inline] - /// Construct new http request with new RouteInfo. - pub(crate) fn with_route_info(&self, mut resource: ResourceInfo) -> HttpRequest { - resource.merge(&self.resource); - - HttpRequest { - resource, - req: self.req.as_ref().map(|r| r.clone()), - state: self.state.clone(), - } - } - - /// Shared application state - #[inline] - pub fn state(&self) -> &S { - &self.state - } - - #[inline] - /// Server request - pub fn request(&self) -> &Request { - self.req.as_ref().unwrap() - } - - /// Request extensions - #[inline] - pub fn extensions(&self) -> Ref { - self.request().extensions() - } - - /// Mutable reference to a the request's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut { - self.request().extensions_mut() - } - - /// Default `CpuPool` - #[inline] - #[doc(hidden)] - pub fn cpu_pool(&self) -> &CpuPool { - self.request().server_settings().cpu_pool() - } - - #[inline] - /// Create http response - pub fn response(&self, status: StatusCode, body: Body) -> HttpResponse { - self.request().server_settings().get_response(status, body) - } - - #[inline] - /// Create http response builder - pub fn build_response(&self, status: StatusCode) -> HttpResponseBuilder { - self.request() - .server_settings() - .get_response_builder(status) - } - - /// Read the Request Uri. - #[inline] - pub fn uri(&self) -> &Uri { - self.request().inner.url.uri() - } - - /// Read the Request method. - #[inline] - pub fn method(&self) -> &Method { - &self.request().inner.method - } - - /// Read the Request Version. - #[inline] - pub fn version(&self) -> Version { - self.request().inner.version - } - - /// The target path of this Request. - #[inline] - pub fn path(&self) -> &str { - self.request().inner.url.path() - } - - /// Get *ConnectionInfo* for the correct request. - #[inline] - pub fn connection_info(&self) -> Ref { - self.request().connection_info() - } - - /// Generate url for named resource - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::{App, HttpRequest, HttpResponse, http}; - /// # - /// fn index(req: HttpRequest) -> HttpResponse { - /// let url = req.url_for("foo", &["1", "2", "3"]); // <- generate url for "foo" resource - /// HttpResponse::Ok().into() - /// } - /// - /// fn main() { - /// let app = App::new() - /// .resource("/test/{one}/{two}/{three}", |r| { - /// r.name("foo"); // <- set resource name, then it could be used in `url_for` - /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); - /// }) - /// .finish(); - /// } - /// ``` - pub fn url_for( - &self, name: &str, elements: U, - ) -> Result - where - U: IntoIterator, - I: AsRef, - { - self.resource.url_for(&self, name, elements) - } - - /// Generate url for named resource - /// - /// This method is similar to `HttpRequest::url_for()` but it can be used - /// for urls that do not contain variable parts. - pub fn url_for_static(&self, name: &str) -> Result { - const NO_PARAMS: [&str; 0] = []; - self.url_for(name, &NO_PARAMS) - } - - /// This method returns reference to current `RouteInfo` object. - #[inline] - pub fn resource(&self) -> &ResourceInfo { - &self.resource - } - - /// Peer socket address - /// - /// Peer address is actual socket address, if proxy is used in front of - /// actix http server, then peer address would be address of this proxy. - /// - /// To get client connection information `connection_info()` method should - /// be used. - #[inline] - pub fn peer_addr(&self) -> Option { - self.request().inner.addr - } - - /// url query parameters. - pub fn query(&self) -> Ref> { - if self.extensions().get::().is_none() { - let mut query = HashMap::new(); - for (key, val) in form_urlencoded::parse(self.query_string().as_ref()) { - query.insert(key.as_ref().to_string(), val.to_string()); - } - self.extensions_mut().insert(Query(query)); - } - Ref::map(self.extensions(), |ext| &ext.get::().unwrap().0) - } - - /// The query string in the URL. - /// - /// E.g., id=10 - #[inline] - pub fn query_string(&self) -> &str { - if let Some(query) = self.uri().query().as_ref() { - query - } else { - "" - } - } - - /// Load request cookies. - #[inline] - pub fn cookies(&self) -> Result>>, CookieParseError> { - if self.extensions().get::().is_none() { - let mut cookies = Vec::new(); - for hdr in self.request().inner.headers.get_all(header::COOKIE) { - let s = - str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; - for cookie_str in s.split(';').map(|s| s.trim()) { - if !cookie_str.is_empty() { - cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned()); - } - } - } - self.extensions_mut().insert(Cookies(cookies)); - } - Ok(Ref::map(self.extensions(), |ext| { - &ext.get::().unwrap().0 - })) - } - - /// Return request cookie. - #[inline] - pub fn cookie(&self, name: &str) -> Option> { - if let Ok(cookies) = self.cookies() { - for cookie in cookies.iter() { - if cookie.name() == name { - return Some(cookie.to_owned()); - } - } - } - None - } - - pub(crate) fn set_cookies(&mut self, cookies: Option>>) { - if let Some(cookies) = cookies { - self.extensions_mut().insert(Cookies(cookies)); - } - } - - /// Get a reference to the Params object. - /// - /// Params is a container for url parameters. - /// A variable segment is specified in the form `{identifier}`, - /// where the identifier can be used later in a request handler to - /// access the matched value for that segment. - #[inline] - pub fn match_info(&self) -> &Params { - &self.resource.match_info() - } - - /// Check if request requires connection upgrade - pub(crate) fn upgrade(&self) -> bool { - self.request().upgrade() - } - - /// Set read buffer capacity - /// - /// Default buffer capacity is 32Kb. - pub fn set_read_buffer_capacity(&mut self, cap: usize) { - if let Some(payload) = self.request().inner.payload.borrow_mut().as_mut() { - payload.set_read_buffer_capacity(cap) - } - } -} - -impl Drop for HttpRequest { - fn drop(&mut self) { - if let Some(req) = self.req.take() { - req.release(); - } - } -} - -impl Clone for HttpRequest { - fn clone(&self) -> HttpRequest { - HttpRequest { - req: self.req.as_ref().map(|r| r.clone()), - state: self.state.clone(), - resource: self.resource.clone(), - } - } -} - -impl FromRequest for HttpRequest { - type Config = (); - type Result = Self; - - #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { - req.clone() - } -} - -impl fmt::Debug for HttpRequest { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!( - f, - "\nHttpRequest {:?} {}:{}", - self.version(), - self.method(), - self.path() - )?; - if !self.query_string().is_empty() { - writeln!(f, " query: ?{:?}", self.query_string())?; - } - if !self.match_info().is_empty() { - writeln!(f, " params: {:?}", self.match_info())?; - } - writeln!(f, " headers:")?; - for (key, val) in self.headers().iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use resource::Resource; - use router::{ResourceDef, Router}; - use test::TestRequest; - - #[test] - fn test_debug() { - let req = TestRequest::with_header("content-type", "text/plain").finish(); - let dbg = format!("{:?}", req); - assert!(dbg.contains("HttpRequest")); - } - - #[test] - fn test_no_request_cookies() { - let req = TestRequest::default().finish(); - assert!(req.cookies().unwrap().is_empty()); - } - - #[test] - fn test_request_cookies() { - let req = TestRequest::default() - .header(header::COOKIE, "cookie1=value1") - .header(header::COOKIE, "cookie2=value2") - .finish(); - { - let cookies = req.cookies().unwrap(); - assert_eq!(cookies.len(), 2); - assert_eq!(cookies[0].name(), "cookie1"); - assert_eq!(cookies[0].value(), "value1"); - assert_eq!(cookies[1].name(), "cookie2"); - assert_eq!(cookies[1].value(), "value2"); - } - - let cookie = req.cookie("cookie1"); - assert!(cookie.is_some()); - let cookie = cookie.unwrap(); - assert_eq!(cookie.name(), "cookie1"); - assert_eq!(cookie.value(), "value1"); - - let cookie = req.cookie("cookie-unknown"); - assert!(cookie.is_none()); - } - - #[test] - fn test_request_query() { - let req = TestRequest::with_uri("/?id=test").finish(); - assert_eq!(req.query_string(), "id=test"); - let query = req.query(); - assert_eq!(&query["id"], "test"); - } - - #[test] - fn test_request_match_info() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{key}/"))); - - let req = TestRequest::with_uri("/value/?id=test").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.match_info().get("key"), Some("value")); - } - - #[test] - fn test_url_for() { - let mut router = Router::<()>::default(); - let mut resource = Resource::new(ResourceDef::new("/user/{name}.{ext}")); - resource.name("index"); - router.register_resource(resource); - - let info = router.default_route_info(); - assert!(!info.has_prefixed_resource("/use/")); - assert!(info.has_resource("/user/test.html")); - assert!(info.has_prefixed_resource("/user/test.html")); - assert!(!info.has_resource("/test/unknown")); - assert!(!info.has_prefixed_resource("/test/unknown")); - - let req = TestRequest::with_header(header::HOST, "www.rust-lang.org") - .finish_with_router(router); - - assert_eq!( - req.url_for("unknown", &["test"]), - Err(UrlGenerationError::ResourceNotFound) - ); - assert_eq!( - req.url_for("index", &["test"]), - Err(UrlGenerationError::NotEnoughElements) - ); - let url = req.url_for("index", &["test", "html"]); - assert_eq!( - url.ok().unwrap().as_str(), - "http://www.rust-lang.org/user/test.html" - ); - } - - #[test] - fn test_url_for_with_prefix() { - let mut resource = Resource::new(ResourceDef::new("/user/{name}.html")); - resource.name("index"); - let mut router = Router::<()>::default(); - router.set_prefix("/prefix"); - router.register_resource(resource); - - let mut info = router.default_route_info(); - info.set_prefix(7); - assert!(!info.has_prefixed_resource("/use/")); - assert!(info.has_resource("/user/test.html")); - assert!(!info.has_prefixed_resource("/user/test.html")); - assert!(!info.has_resource("/prefix/user/test.html")); - assert!(info.has_prefixed_resource("/prefix/user/test.html")); - - let req = TestRequest::with_uri("/prefix/test") - .prefix(7) - .header(header::HOST, "www.rust-lang.org") - .finish_with_router(router); - let url = req.url_for("index", &["test"]); - assert_eq!( - url.ok().unwrap().as_str(), - "http://www.rust-lang.org/prefix/user/test.html" - ); - } - - #[test] - fn test_url_for_static() { - let mut resource = Resource::new(ResourceDef::new("/index.html")); - resource.name("index"); - let mut router = Router::<()>::default(); - router.set_prefix("/prefix"); - router.register_resource(resource); - - let mut info = router.default_route_info(); - info.set_prefix(7); - assert!(info.has_resource("/index.html")); - assert!(!info.has_prefixed_resource("/index.html")); - assert!(!info.has_resource("/prefix/index.html")); - assert!(info.has_prefixed_resource("/prefix/index.html")); - - let req = TestRequest::with_uri("/prefix/test") - .prefix(7) - .header(header::HOST, "www.rust-lang.org") - .finish_with_router(router); - let url = req.url_for_static("index"); - assert_eq!( - url.ok().unwrap().as_str(), - "http://www.rust-lang.org/prefix/index.html" - ); - } - - #[test] - fn test_url_for_external() { - let mut router = Router::<()>::default(); - router.register_external( - "youtube", - ResourceDef::external("https://youtube.com/watch/{video_id}"), - ); - - let info = router.default_route_info(); - assert!(!info.has_resource("https://youtube.com/watch/unknown")); - assert!(!info.has_prefixed_resource("https://youtube.com/watch/unknown")); - - let req = TestRequest::default().finish_with_router(router); - let url = req.url_for("youtube", &["oHg5SJYRHA0"]); - assert_eq!( - url.ok().unwrap().as_str(), - "https://youtube.com/watch/oHg5SJYRHA0" - ); - } -} diff --git a/src/server/output.rs b/src/server/output.rs index 5fc6fc839..fc6886840 100644 --- a/src/server/output.rs +++ b/src/server/output.rs @@ -224,7 +224,7 @@ impl TransferEncoding { *eof = true; buf.extend_from_slice(b"0\r\n\r\n"); } else { - writeln!(buf.as_mut(), "{:X}\r", msg.len()) + writeln!(Writer(buf), "{:X}\r", msg.len()) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; buf.reserve(msg.len() + 2); @@ -268,87 +268,18 @@ impl TransferEncoding { } } -impl io::Write for TransferEncoding { - #[inline] +struct Writer<'a>(pub &'a mut BytesMut); + +impl<'a> io::Write for Writer<'a> { fn write(&mut self, buf: &[u8]) -> io::Result { - // if self.buf.is_some() { - // self.encode(buf)?; - // } + self.0.extend_from_slice(buf); Ok(buf.len()) } - - #[inline] fn flush(&mut self) -> io::Result<()> { Ok(()) } } -struct AcceptEncoding { - encoding: ContentEncoding, - quality: f64, -} - -impl Eq for AcceptEncoding {} - -impl Ord for AcceptEncoding { - fn cmp(&self, other: &AcceptEncoding) -> cmp::Ordering { - if self.quality > other.quality { - cmp::Ordering::Less - } else if self.quality < other.quality { - cmp::Ordering::Greater - } else { - cmp::Ordering::Equal - } - } -} - -impl PartialOrd for AcceptEncoding { - fn partial_cmp(&self, other: &AcceptEncoding) -> Option { - Some(self.cmp(other)) - } -} - -impl PartialEq for AcceptEncoding { - fn eq(&self, other: &AcceptEncoding) -> bool { - self.quality == other.quality - } -} - -impl AcceptEncoding { - fn new(tag: &str) -> Option { - let parts: Vec<&str> = tag.split(';').collect(); - let encoding = match parts.len() { - 0 => return None, - _ => ContentEncoding::from(parts[0]), - }; - let quality = match parts.len() { - 1 => encoding.quality(), - _ => match f64::from_str(parts[1]) { - Ok(q) => q, - Err(_) => 0.0, - }, - }; - Some(AcceptEncoding { encoding, quality }) - } - - /// Parse a raw Accept-Encoding header value into an ordered list. - pub fn parse(raw: &str) -> ContentEncoding { - let mut encodings: Vec<_> = raw - .replace(' ', "") - .split(',') - .map(|l| AcceptEncoding::new(l)) - .collect(); - encodings.sort(); - - for enc in encodings { - if let Some(enc) = enc { - return enc.encoding; - } - } - ContentEncoding::Identity - } -} - #[cfg(test)] mod tests { use super::*; @@ -356,14 +287,14 @@ mod tests { #[test] fn test_chunked_te() { - let bytes = BytesMut::new(); - let mut enc = TransferEncoding::chunked(bytes); + let mut bytes = BytesMut::new(); + let mut enc = TransferEncoding::chunked(); { - assert!(!enc.encode(b"test").ok().unwrap()); - assert!(enc.encode(b"").ok().unwrap()); + assert!(!enc.encode(b"test", &mut bytes).ok().unwrap()); + assert!(enc.encode(b"", &mut bytes).ok().unwrap()); } assert_eq!( - enc.buf_mut().take().freeze(), + bytes.take().freeze(), Bytes::from_static(b"4\r\ntest\r\n0\r\n\r\n") ); } From c99f9eaa63b87670fcf6cb5f15f859ed761bfdb8 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 5 Oct 2018 05:59:02 -0700 Subject: [PATCH 011/427] Update test_h1v2.rs --- tests/test_h1v2.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_h1v2.rs b/tests/test_h1v2.rs index d06777b75..77a6ecae5 100644 --- a/tests/test_h1v2.rs +++ b/tests/test_h1v2.rs @@ -23,7 +23,7 @@ fn test_h1_v2() { let settings = ServiceConfig::build() .keep_alive(KeepAlive::Disabled) .client_timeout(1000) - .client_shutdown(1000) + .client_disconnect(1000) .server_hostname("localhost") .server_address(addr) .finish(); From c24a8f4c2d121a02e828be52712b9d4b43004f29 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 5 Oct 2018 07:02:09 -0700 Subject: [PATCH 012/427] remove high level apis --- Cargo.toml | 17 +- src/h1/codec.rs | 2 +- src/h1/dispatcher.rs | 7 +- src/h1/mod.rs | 1 + src/{server/output.rs => h1/response.rs} | 0 src/header.rs | 265 +++++++ src/header/common/accept.rs | 159 ---- src/header/common/accept_charset.rs | 69 -- src/header/common/accept_encoding.rs | 72 -- src/header/common/accept_language.rs | 75 -- src/header/common/allow.rs | 82 -- src/header/common/cache_control.rs | 254 ------- src/header/common/content_disposition.rs | 915 ----------------------- src/header/common/content_language.rs | 65 -- src/header/common/content_range.rs | 210 ------ src/header/common/content_type.rs | 122 --- src/header/common/date.rs | 42 -- src/header/common/etag.rs | 96 --- src/header/common/expires.rs | 39 - src/header/common/if_match.rs | 70 -- src/header/common/if_modified_since.rs | 39 - src/header/common/if_none_match.rs | 92 --- src/header/common/if_range.rs | 117 --- src/header/common/if_unmodified_since.rs | 40 - src/header/common/last_modified.rs | 38 - src/header/common/mod.rs | 350 --------- src/header/common/range.rs | 434 ----------- src/header/mod.rs | 471 ------------ src/header/shared/charset.rs | 152 ---- src/header/shared/encoding.rs | 59 -- src/header/shared/entity.rs | 266 ------- src/header/shared/httpdate.rs | 119 --- src/header/shared/mod.rs | 14 - src/header/shared/quality_item.rs | 294 -------- src/httpresponse.rs | 4 +- src/lib.rs | 13 +- src/server/input.rs | 288 ------- src/server/mod.rs | 3 - src/server/service.rs | 273 ------- src/ws/context.rs | 334 --------- 40 files changed, 273 insertions(+), 5689 deletions(-) rename src/{server/output.rs => h1/response.rs} (100%) create mode 100644 src/header.rs delete mode 100644 src/header/common/accept.rs delete mode 100644 src/header/common/accept_charset.rs delete mode 100644 src/header/common/accept_encoding.rs delete mode 100644 src/header/common/accept_language.rs delete mode 100644 src/header/common/allow.rs delete mode 100644 src/header/common/cache_control.rs delete mode 100644 src/header/common/content_disposition.rs delete mode 100644 src/header/common/content_language.rs delete mode 100644 src/header/common/content_range.rs delete mode 100644 src/header/common/content_type.rs delete mode 100644 src/header/common/date.rs delete mode 100644 src/header/common/etag.rs delete mode 100644 src/header/common/expires.rs delete mode 100644 src/header/common/if_match.rs delete mode 100644 src/header/common/if_modified_since.rs delete mode 100644 src/header/common/if_none_match.rs delete mode 100644 src/header/common/if_range.rs delete mode 100644 src/header/common/if_unmodified_since.rs delete mode 100644 src/header/common/last_modified.rs delete mode 100644 src/header/common/mod.rs delete mode 100644 src/header/common/range.rs delete mode 100644 src/header/mod.rs delete mode 100644 src/header/shared/charset.rs delete mode 100644 src/header/shared/encoding.rs delete mode 100644 src/header/shared/entity.rs delete mode 100644 src/header/shared/httpdate.rs delete mode 100644 src/header/shared/mod.rs delete mode 100644 src/header/shared/quality_item.rs delete mode 100644 src/server/input.rs delete mode 100644 src/server/service.rs delete mode 100644 src/ws/context.rs diff --git a/Cargo.toml b/Cargo.toml index 86012175f..870f772e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ name = "actix_http" path = "src/lib.rs" [features] -default = ["session", "brotli", "flate2-c", "cell"] +default = ["session", "cell"] # tls tls = ["native-tls", "tokio-tls", "actix-net/tls"] @@ -48,15 +48,6 @@ uds = ["tokio-uds"] # sessions feature, session require "ring" crate and c compiler session = ["cookie/secure"] -# brotli encoding, requires c compiler -brotli = ["brotli2"] - -# miniz-sys backend for flate2 crate -flate2-c = ["flate2/miniz-sys"] - -# rust backend for flate2 crate -flate2-rust = ["flate2/rust_backend"] - cell = ["actix-net/cell"] [dependencies] @@ -70,23 +61,17 @@ http = "^0.1.8" httparse = "1.3" log = "0.4" mime = "0.3" -mime_guess = "2.0.0-alpha" percent-encoding = "1.0" rand = "0.5" -regex = "1.0" serde = "1.0" serde_json = "1.0" sha1 = "0.6" -smallvec = "0.6" time = "0.1" encoding = "0.2" -language-tags = "0.2" lazy_static = "1.0" serde_urlencoded = "^0.5.3" url = { version="1.7", features=["query_encoding"] } cookie = { version="0.11", features=["percent-encode"] } -brotli2 = { version="^0.3.2", optional = true } -flate2 = { version="^1.0.2", optional = true, default-features = false } failure = "^0.1.2" diff --git a/src/h1/codec.rs b/src/h1/codec.rs index ac54194ab..dd6b26bab 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -6,6 +6,7 @@ use tokio_codec::{Decoder, Encoder}; use super::decoder::H1Decoder; pub use super::decoder::InMessage; +use super::response::{ResponseInfo, ResponseLength}; use body::Body; use error::ParseError; use helpers; @@ -13,7 +14,6 @@ use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCOD use http::{Method, Version}; use httpresponse::HttpResponse; use request::RequestPool; -use server::output::{ResponseInfo, ResponseLength}; /// Http response pub enum OutMessage { diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index f777648ec..2b524556e 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -12,14 +12,13 @@ use tokio_io::{AsyncRead, AsyncWrite}; use tokio_timer::Delay; use error::{ParseError, PayloadError}; -use payload::{Payload, PayloadStatus, PayloadWriter}; +use payload::{Payload, PayloadSender, PayloadStatus, PayloadWriter}; use body::Body; use config::ServiceConfig; use error::DispatchError; use httpresponse::HttpResponse; use request::Request; -use server::input::PayloadType; use super::codec::{Codec, InMessage, OutMessage}; @@ -50,7 +49,7 @@ where config: ServiceConfig, state: State, - payload: Option, + payload: Option, messages: VecDeque, ka_expire: Instant, @@ -316,7 +315,7 @@ where // payload let (ps, pl) = Payload::new(false); *msg.inner.payload.borrow_mut() = Some(pl); - self.payload = Some(PayloadType::new(&msg.inner.headers, ps)); + self.payload = Some(ps); self.messages.push_back(msg); } diff --git a/src/h1/mod.rs b/src/h1/mod.rs index 1a2bb0183..1653227be 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -2,6 +2,7 @@ mod codec; mod decoder; mod dispatcher; +mod response; mod service; pub use self::codec::{Codec, InMessage, OutMessage}; diff --git a/src/server/output.rs b/src/h1/response.rs similarity index 100% rename from src/server/output.rs rename to src/h1/response.rs diff --git a/src/header.rs b/src/header.rs new file mode 100644 index 000000000..7b32d6c24 --- /dev/null +++ b/src/header.rs @@ -0,0 +1,265 @@ +//! Various http headers + +use std::fmt; +use std::str::FromStr; + +use bytes::{Bytes, BytesMut}; +use mime::Mime; +use modhttp::header::GetAll; +use modhttp::Error as HttpError; +use percent_encoding; + +pub use modhttp::header::*; + +use error::ParseError; +use httpmessage::HttpMessage; + +#[doc(hidden)] +/// A trait for any object that will represent a header field and value. +pub trait Header +where + Self: IntoHeaderValue, +{ + /// Returns the name of the header field + fn name() -> HeaderName; + + /// Parse a header + fn parse(msg: &T) -> Result; +} + +#[doc(hidden)] +/// A trait for any object that can be Converted to a `HeaderValue` +pub trait IntoHeaderValue: Sized { + /// The type returned in the event of a conversion error. + type Error: Into; + + /// Try to convert value to a Header value. + fn try_into(self) -> Result; +} + +impl IntoHeaderValue for HeaderValue { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + Ok(self) + } +} + +impl<'a> IntoHeaderValue for &'a str { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + self.parse() + } +} + +impl<'a> IntoHeaderValue for &'a [u8] { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::from_bytes(self) + } +} + +impl IntoHeaderValue for Bytes { + type Error = InvalidHeaderValueBytes; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::from_shared(self) + } +} + +impl IntoHeaderValue for Vec { + type Error = InvalidHeaderValueBytes; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::from_shared(Bytes::from(self)) + } +} + +impl IntoHeaderValue for String { + type Error = InvalidHeaderValueBytes; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::from_shared(Bytes::from(self)) + } +} + +impl IntoHeaderValue for Mime { + type Error = InvalidHeaderValueBytes; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::from_shared(Bytes::from(format!("{}", self))) + } +} + +/// Represents supported types of content encodings +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum ContentEncoding { + /// Automatically select encoding based on encoding negotiation + Auto, + /// A format using the Brotli algorithm + Br, + /// A format using the zlib structure with deflate algorithm + Deflate, + /// Gzip algorithm + Gzip, + /// Indicates the identity function (i.e. no compression, nor modification) + Identity, +} + +impl ContentEncoding { + #[inline] + /// Is the content compressed? + pub fn is_compression(self) -> bool { + match self { + ContentEncoding::Identity | ContentEncoding::Auto => false, + _ => true, + } + } + + #[inline] + /// Convert content encoding to string + pub fn as_str(self) -> &'static str { + match self { + ContentEncoding::Br => "br", + ContentEncoding::Gzip => "gzip", + ContentEncoding::Deflate => "deflate", + ContentEncoding::Identity | ContentEncoding::Auto => "identity", + } + } + + #[inline] + /// default quality value + pub fn quality(self) -> f64 { + match self { + ContentEncoding::Br => 1.1, + ContentEncoding::Gzip => 1.0, + ContentEncoding::Deflate => 0.9, + ContentEncoding::Identity | ContentEncoding::Auto => 0.1, + } + } +} + +// TODO: remove memory allocation +impl<'a> From<&'a str> for ContentEncoding { + fn from(s: &'a str) -> ContentEncoding { + match AsRef::::as_ref(&s.trim().to_lowercase()) { + "br" => ContentEncoding::Br, + "gzip" => ContentEncoding::Gzip, + "deflate" => ContentEncoding::Deflate, + _ => ContentEncoding::Identity, + } + } +} + +#[doc(hidden)] +pub(crate) struct Writer { + buf: BytesMut, +} + +impl Writer { + fn new() -> Writer { + Writer { + buf: BytesMut::new(), + } + } + fn take(&mut self) -> Bytes { + self.buf.take().freeze() + } +} + +impl fmt::Write for Writer { + #[inline] + fn write_str(&mut self, s: &str) -> fmt::Result { + self.buf.extend_from_slice(s.as_bytes()); + Ok(()) + } + + #[inline] + fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result { + fmt::write(self, args) + } +} + +#[inline] +#[doc(hidden)] +/// Reads a comma-delimited raw header into a Vec. +pub fn from_comma_delimited( + all: GetAll, +) -> Result, ParseError> { + let mut result = Vec::new(); + for h in all { + let s = h.to_str().map_err(|_| ParseError::Header)?; + result.extend( + s.split(',') + .filter_map(|x| match x.trim() { + "" => None, + y => Some(y), + }).filter_map(|x| x.trim().parse().ok()), + ) + } + Ok(result) +} + +#[inline] +#[doc(hidden)] +/// Reads a single string when parsing a header. +pub fn from_one_raw_str(val: Option<&HeaderValue>) -> Result { + if let Some(line) = val { + let line = line.to_str().map_err(|_| ParseError::Header)?; + if !line.is_empty() { + return T::from_str(line).or(Err(ParseError::Header)); + } + } + Err(ParseError::Header) +} + +#[inline] +#[doc(hidden)] +/// Format an array into a comma-delimited string. +pub fn fmt_comma_delimited(f: &mut fmt::Formatter, parts: &[T]) -> fmt::Result +where + T: fmt::Display, +{ + let mut iter = parts.iter(); + if let Some(part) = iter.next() { + fmt::Display::fmt(part, f)?; + } + for part in iter { + f.write_str(", ")?; + fmt::Display::fmt(part, f)?; + } + Ok(()) +} + +/// Percent encode a sequence of bytes with a character set defined in +/// [https://tools.ietf.org/html/rfc5987#section-3.2][url] +/// +/// [url]: https://tools.ietf.org/html/rfc5987#section-3.2 +pub fn http_percent_encode(f: &mut fmt::Formatter, bytes: &[u8]) -> fmt::Result { + let encoded = + percent_encoding::percent_encode(bytes, self::percent_encoding_http::HTTP_VALUE); + fmt::Display::fmt(&encoded, f) +} +mod percent_encoding_http { + use percent_encoding; + + // internal module because macro is hard-coded to make a public item + // but we don't want to public export this item + define_encode_set! { + // This encode set is used for HTTP header values and is defined at + // https://tools.ietf.org/html/rfc5987#section-3.2 + pub HTTP_VALUE = [percent_encoding::SIMPLE_ENCODE_SET] | { + ' ', '"', '%', '\'', '(', ')', '*', ',', '/', ':', ';', '<', '-', '>', '?', + '[', '\\', ']', '{', '}' + } + } +} diff --git a/src/header/common/accept.rs b/src/header/common/accept.rs deleted file mode 100644 index 1ba321ce8..000000000 --- a/src/header/common/accept.rs +++ /dev/null @@ -1,159 +0,0 @@ -use header::{qitem, QualityItem}; -use http::header as http; -use mime::{self, Mime}; - -header! { - /// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2) - /// - /// The `Accept` header field can be used by user agents to specify - /// response media types that are acceptable. Accept header fields can - /// be used to indicate that the request is specifically limited to a - /// small set of desired types, as in the case of a request for an - /// in-line image - /// - /// # ABNF - /// - /// ```text - /// Accept = #( media-range [ accept-params ] ) - /// - /// media-range = ( "*/*" - /// / ( type "/" "*" ) - /// / ( type "/" subtype ) - /// ) *( OWS ";" OWS parameter ) - /// accept-params = weight *( accept-ext ) - /// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ] - /// ``` - /// - /// # Example values - /// * `audio/*; q=0.2, audio/basic` - /// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c` - /// - /// # Examples - /// ```rust - /// # extern crate actix_http; - /// extern crate mime; - /// use actix_http::HttpResponse; - /// use actix_http::http::header::{Accept, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// - /// builder.set( - /// Accept(vec![ - /// qitem(mime::TEXT_HTML), - /// ]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate actix_http; - /// extern crate mime; - /// use actix_http::HttpResponse; - /// use actix_http::http::header::{Accept, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// - /// builder.set( - /// Accept(vec![ - /// qitem(mime::APPLICATION_JSON), - /// ]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate actix_http; - /// extern crate mime; - /// use actix_http::HttpResponse; - /// use actix_http::http::header::{Accept, QualityItem, q, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// - /// builder.set( - /// Accept(vec![ - /// qitem(mime::TEXT_HTML), - /// qitem("application/xhtml+xml".parse().unwrap()), - /// QualityItem::new( - /// mime::TEXT_XML, - /// q(900) - /// ), - /// qitem("image/webp".parse().unwrap()), - /// QualityItem::new( - /// mime::STAR_STAR, - /// q(800) - /// ), - /// ]) - /// ); - /// # } - /// ``` - (Accept, http::ACCEPT) => (QualityItem)+ - - test_accept { - // Tests from the RFC - test_header!( - test1, - vec![b"audio/*; q=0.2, audio/basic"], - Some(HeaderField(vec![ - QualityItem::new("audio/*".parse().unwrap(), q(200)), - qitem("audio/basic".parse().unwrap()), - ]))); - test_header!( - test2, - vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], - Some(HeaderField(vec![ - QualityItem::new(TEXT_PLAIN, q(500)), - qitem(TEXT_HTML), - QualityItem::new( - "text/x-dvi".parse().unwrap(), - q(800)), - qitem("text/x-c".parse().unwrap()), - ]))); - // Custom tests - test_header!( - test3, - vec![b"text/plain; charset=utf-8"], - Some(Accept(vec![ - qitem(TEXT_PLAIN_UTF_8), - ]))); - test_header!( - test4, - vec![b"text/plain; charset=utf-8; q=0.5"], - Some(Accept(vec![ - QualityItem::new(TEXT_PLAIN_UTF_8, - q(500)), - ]))); - - #[test] - fn test_fuzzing1() { - use test::TestRequest; - let req = TestRequest::with_header(super::http::ACCEPT, "chunk#;e").finish(); - let header = Accept::parse(&req); - assert!(header.is_ok()); - } - } -} - -impl Accept { - /// A constructor to easily create `Accept: */*`. - pub fn star() -> Accept { - Accept(vec![qitem(mime::STAR_STAR)]) - } - - /// A constructor to easily create `Accept: application/json`. - pub fn json() -> Accept { - Accept(vec![qitem(mime::APPLICATION_JSON)]) - } - - /// A constructor to easily create `Accept: text/*`. - pub fn text() -> Accept { - Accept(vec![qitem(mime::TEXT_STAR)]) - } - - /// A constructor to easily create `Accept: image/*`. - pub fn image() -> Accept { - Accept(vec![qitem(mime::IMAGE_STAR)]) - } -} diff --git a/src/header/common/accept_charset.rs b/src/header/common/accept_charset.rs deleted file mode 100644 index 49a7237aa..000000000 --- a/src/header/common/accept_charset.rs +++ /dev/null @@ -1,69 +0,0 @@ -use header::{Charset, QualityItem, ACCEPT_CHARSET}; - -header! { - /// `Accept-Charset` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.3) - /// - /// The `Accept-Charset` header field can be sent by a user agent to - /// indicate what charsets are acceptable in textual response content. - /// This field allows user agents capable of understanding more - /// comprehensive or special-purpose charsets to signal that capability - /// to an origin server that is capable of representing information in - /// those charsets. - /// - /// # ABNF - /// - /// ```text - /// Accept-Charset = 1#( ( charset / "*" ) [ weight ] ) - /// ``` - /// - /// # Example values - /// * `iso-8859-5, unicode-1-1;q=0.8` - /// - /// # Examples - /// ```rust - /// # extern crate actix_http; - /// use actix_http::HttpResponse; - /// use actix_http::http::header::{AcceptCharset, Charset, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// AcceptCharset(vec![qitem(Charset::Us_Ascii)]) - /// ); - /// # } - /// ``` - /// ```rust - /// # extern crate actix_http; - /// use actix_http::HttpResponse; - /// use actix_http::http::header::{AcceptCharset, Charset, q, QualityItem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// AcceptCharset(vec![ - /// QualityItem::new(Charset::Us_Ascii, q(900)), - /// QualityItem::new(Charset::Iso_8859_10, q(200)), - /// ]) - /// ); - /// # } - /// ``` - /// ```rust - /// # extern crate actix_http; - /// use actix_http::HttpResponse; - /// use actix_http::http::header::{AcceptCharset, Charset, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))]) - /// ); - /// # } - /// ``` - (AcceptCharset, ACCEPT_CHARSET) => (QualityItem)+ - - test_accept_charset { - /// Test case from RFC - test_header!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); - } -} diff --git a/src/header/common/accept_encoding.rs b/src/header/common/accept_encoding.rs deleted file mode 100644 index c90f529bc..000000000 --- a/src/header/common/accept_encoding.rs +++ /dev/null @@ -1,72 +0,0 @@ -use header::{Encoding, QualityItem}; - -header! { - /// `Accept-Encoding` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.4) - /// - /// The `Accept-Encoding` header field can be used by user agents to - /// indicate what response content-codings are - /// acceptable in the response. An `identity` token is used as a synonym - /// for "no encoding" in order to communicate when no encoding is - /// preferred. - /// - /// # ABNF - /// - /// ```text - /// Accept-Encoding = #( codings [ weight ] ) - /// codings = content-coding / "identity" / "*" - /// ``` - /// - /// # Example values - /// * `compress, gzip` - /// * `` - /// * `*` - /// * `compress;q=0.5, gzip;q=1` - /// * `gzip;q=1.0, identity; q=0.5, *;q=0` - /// - /// # Examples - /// ``` - /// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem}; - /// - /// let mut headers = Headers::new(); - /// headers.set( - /// AcceptEncoding(vec![qitem(Encoding::Chunked)]) - /// ); - /// ``` - /// ``` - /// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem}; - /// - /// let mut headers = Headers::new(); - /// headers.set( - /// AcceptEncoding(vec![ - /// qitem(Encoding::Chunked), - /// qitem(Encoding::Gzip), - /// qitem(Encoding::Deflate), - /// ]) - /// ); - /// ``` - /// ``` - /// use hyper::header::{Headers, AcceptEncoding, Encoding, QualityItem, q, qitem}; - /// - /// let mut headers = Headers::new(); - /// headers.set( - /// AcceptEncoding(vec![ - /// qitem(Encoding::Chunked), - /// QualityItem::new(Encoding::Gzip, q(600)), - /// QualityItem::new(Encoding::EncodingExt("*".to_owned()), q(0)), - /// ]) - /// ); - /// ``` - (AcceptEncoding, "Accept-Encoding") => (QualityItem)* - - test_accept_encoding { - // From the RFC - test_header!(test1, vec![b"compress, gzip"]); - test_header!(test2, vec![b""], Some(AcceptEncoding(vec![]))); - test_header!(test3, vec![b"*"]); - // Note: Removed quality 1 from gzip - test_header!(test4, vec![b"compress;q=0.5, gzip"]); - // Note: Removed quality 1 from gzip - test_header!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); - } -} diff --git a/src/header/common/accept_language.rs b/src/header/common/accept_language.rs deleted file mode 100644 index 25fd97df4..000000000 --- a/src/header/common/accept_language.rs +++ /dev/null @@ -1,75 +0,0 @@ -use header::{QualityItem, ACCEPT_LANGUAGE}; -use language_tags::LanguageTag; - -header! { - /// `Accept-Language` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.5) - /// - /// The `Accept-Language` header field can be used by user agents to - /// indicate the set of natural languages that are preferred in the - /// response. - /// - /// # ABNF - /// - /// ```text - /// Accept-Language = 1#( language-range [ weight ] ) - /// language-range = - /// ``` - /// - /// # Example values - /// * `da, en-gb;q=0.8, en;q=0.7` - /// * `en-us;q=1.0, en;q=0.5, fr` - /// - /// # Examples - /// - /// ```rust - /// # extern crate actix_http; - /// # extern crate language_tags; - /// use actix_http::HttpResponse; - /// use actix_http::http::header::{AcceptLanguage, LanguageTag, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// let mut langtag: LanguageTag = Default::default(); - /// langtag.language = Some("en".to_owned()); - /// langtag.region = Some("US".to_owned()); - /// builder.set( - /// AcceptLanguage(vec![ - /// qitem(langtag), - /// ]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate actix_http; - /// # #[macro_use] extern crate language_tags; - /// use actix_http::HttpResponse; - /// use actix_http::http::header::{AcceptLanguage, QualityItem, q, qitem}; - /// # - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// AcceptLanguage(vec![ - /// qitem(langtag!(da)), - /// QualityItem::new(langtag!(en;;;GB), q(800)), - /// QualityItem::new(langtag!(en), q(700)), - /// ]) - /// ); - /// # } - /// ``` - (AcceptLanguage, ACCEPT_LANGUAGE) => (QualityItem)+ - - test_accept_language { - // From the RFC - test_header!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]); - // Own test - test_header!( - test2, vec![b"en-US, en; q=0.5, fr"], - Some(AcceptLanguage(vec![ - qitem("en-US".parse().unwrap()), - QualityItem::new("en".parse().unwrap(), q(500)), - qitem("fr".parse().unwrap()), - ]))); - } -} diff --git a/src/header/common/allow.rs b/src/header/common/allow.rs deleted file mode 100644 index 089c823d0..000000000 --- a/src/header/common/allow.rs +++ /dev/null @@ -1,82 +0,0 @@ -use http::header; -use http::Method; - -header! { - /// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1) - /// - /// The `Allow` header field lists the set of methods advertised as - /// supported by the target resource. The purpose of this field is - /// strictly to inform the recipient of valid request methods associated - /// with the resource. - /// - /// # ABNF - /// - /// ```text - /// Allow = #method - /// ``` - /// - /// # Example values - /// * `GET, HEAD, PUT` - /// * `OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH, fOObAr` - /// * `` - /// - /// # Examples - /// - /// ```rust - /// # extern crate actix_http; - /// use actix_http::HttpResponse; - /// use actix_http::http::header::Allow; - /// use actix_http::http::Method; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// Allow(vec![Method::GET]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate actix_http; - /// use actix_http::HttpResponse; - /// use actix_http::http::{Method, header::Allow}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// Allow(vec![ - /// Method::GET, - /// Method::POST, - /// Method::PATCH, - /// ]) - /// ); - /// # } - /// ``` - (Allow, header::ALLOW) => (Method)* - - test_allow { - // From the RFC - test_header!( - test1, - vec![b"GET, HEAD, PUT"], - Some(HeaderField(vec![Method::GET, Method::HEAD, Method::PUT]))); - // Own tests - test_header!( - test2, - vec![b"OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH"], - Some(HeaderField(vec![ - Method::OPTIONS, - Method::GET, - Method::PUT, - Method::POST, - Method::DELETE, - Method::HEAD, - Method::TRACE, - Method::CONNECT, - Method::PATCH]))); - test_header!( - test3, - vec![b""], - Some(HeaderField(Vec::::new()))); - } -} diff --git a/src/header/common/cache_control.rs b/src/header/common/cache_control.rs deleted file mode 100644 index 4379b6f7a..000000000 --- a/src/header/common/cache_control.rs +++ /dev/null @@ -1,254 +0,0 @@ -use header::{fmt_comma_delimited, from_comma_delimited}; -use header::{Header, IntoHeaderValue, Writer}; -use http::header; -use std::fmt::{self, Write}; -use std::str::FromStr; - -/// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2) -/// -/// The `Cache-Control` header field is used to specify directives for -/// caches along the request/response chain. Such cache directives are -/// unidirectional in that the presence of a directive in a request does -/// not imply that the same directive is to be given in the response. -/// -/// # ABNF -/// -/// ```text -/// Cache-Control = 1#cache-directive -/// cache-directive = token [ "=" ( token / quoted-string ) ] -/// ``` -/// -/// # Example values -/// -/// * `no-cache` -/// * `private, community="UCI"` -/// * `max-age=30` -/// -/// # Examples -/// ```rust -/// use actix_http::HttpResponse; -/// use actix_http::http::header::{CacheControl, CacheDirective}; -/// -/// let mut builder = HttpResponse::Ok(); -/// builder.set(CacheControl(vec![CacheDirective::MaxAge(86400u32)])); -/// ``` -/// -/// ```rust -/// use actix_http::HttpResponse; -/// use actix_http::http::header::{CacheControl, CacheDirective}; -/// -/// let mut builder = HttpResponse::Ok(); -/// builder.set(CacheControl(vec![ -/// CacheDirective::NoCache, -/// CacheDirective::Private, -/// CacheDirective::MaxAge(360u32), -/// CacheDirective::Extension("foo".to_owned(), Some("bar".to_owned())), -/// ])); -/// ``` -#[derive(PartialEq, Clone, Debug)] -pub struct CacheControl(pub Vec); - -__hyper__deref!(CacheControl => Vec); - -//TODO: this could just be the header! macro -impl Header for CacheControl { - fn name() -> header::HeaderName { - header::CACHE_CONTROL - } - - #[inline] - fn parse(msg: &T) -> Result - where - T: ::HttpMessage, - { - let directives = from_comma_delimited(msg.headers().get_all(Self::name()))?; - if !directives.is_empty() { - Ok(CacheControl(directives)) - } else { - Err(::error::ParseError::Header) - } - } -} - -impl fmt::Display for CacheControl { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt_comma_delimited(f, &self[..]) - } -} - -impl IntoHeaderValue for CacheControl { - type Error = header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut writer = Writer::new(); - let _ = write!(&mut writer, "{}", self); - header::HeaderValue::from_shared(writer.take()) - } -} - -/// `CacheControl` contains a list of these directives. -#[derive(PartialEq, Clone, Debug)] -pub enum CacheDirective { - /// "no-cache" - NoCache, - /// "no-store" - NoStore, - /// "no-transform" - NoTransform, - /// "only-if-cached" - OnlyIfCached, - - // request directives - /// "max-age=delta" - MaxAge(u32), - /// "max-stale=delta" - MaxStale(u32), - /// "min-fresh=delta" - MinFresh(u32), - - // response directives - /// "must-revalidate" - MustRevalidate, - /// "public" - Public, - /// "private" - Private, - /// "proxy-revalidate" - ProxyRevalidate, - /// "s-maxage=delta" - SMaxAge(u32), - - /// Extension directives. Optionally include an argument. - Extension(String, Option), -} - -impl fmt::Display for CacheDirective { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::CacheDirective::*; - fmt::Display::fmt( - match *self { - NoCache => "no-cache", - NoStore => "no-store", - NoTransform => "no-transform", - OnlyIfCached => "only-if-cached", - - MaxAge(secs) => return write!(f, "max-age={}", secs), - MaxStale(secs) => return write!(f, "max-stale={}", secs), - MinFresh(secs) => return write!(f, "min-fresh={}", secs), - - MustRevalidate => "must-revalidate", - Public => "public", - Private => "private", - ProxyRevalidate => "proxy-revalidate", - SMaxAge(secs) => return write!(f, "s-maxage={}", secs), - - Extension(ref name, None) => &name[..], - Extension(ref name, Some(ref arg)) => { - return write!(f, "{}={}", name, arg) - } - }, - f, - ) - } -} - -impl FromStr for CacheDirective { - type Err = Option<::Err>; - fn from_str(s: &str) -> Result::Err>> { - use self::CacheDirective::*; - match s { - "no-cache" => Ok(NoCache), - "no-store" => Ok(NoStore), - "no-transform" => Ok(NoTransform), - "only-if-cached" => Ok(OnlyIfCached), - "must-revalidate" => Ok(MustRevalidate), - "public" => Ok(Public), - "private" => Ok(Private), - "proxy-revalidate" => Ok(ProxyRevalidate), - "" => Err(None), - _ => match s.find('=') { - Some(idx) if idx + 1 < s.len() => { - match (&s[..idx], (&s[idx + 1..]).trim_matches('"')) { - ("max-age", secs) => secs.parse().map(MaxAge).map_err(Some), - ("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some), - ("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some), - ("s-maxage", secs) => secs.parse().map(SMaxAge).map_err(Some), - (left, right) => { - Ok(Extension(left.to_owned(), Some(right.to_owned()))) - } - } - } - Some(_) => Err(None), - None => Ok(Extension(s.to_owned(), None)), - }, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use header::Header; - use test::TestRequest; - - #[test] - fn test_parse_multiple_headers() { - let req = TestRequest::with_header(header::CACHE_CONTROL, "no-cache, private") - .finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::NoCache, - CacheDirective::Private, - ])) - ) - } - - #[test] - fn test_parse_argument() { - let req = - TestRequest::with_header(header::CACHE_CONTROL, "max-age=100, private") - .finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::MaxAge(100), - CacheDirective::Private, - ])) - ) - } - - #[test] - fn test_parse_quote_form() { - let req = - TestRequest::with_header(header::CACHE_CONTROL, "max-age=\"200\"").finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![CacheDirective::MaxAge(200)])) - ) - } - - #[test] - fn test_parse_extension() { - let req = - TestRequest::with_header(header::CACHE_CONTROL, "foo, bar=baz").finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::Extension("foo".to_owned(), None), - CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned())), - ])) - ) - } - - #[test] - fn test_parse_bad_syntax() { - let req = TestRequest::with_header(header::CACHE_CONTROL, "foo=").finish(); - let cache: Result = Header::parse(&req); - assert_eq!(cache.ok(), None) - } -} diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs deleted file mode 100644 index 0efc4fb0b..000000000 --- a/src/header/common/content_disposition.rs +++ /dev/null @@ -1,915 +0,0 @@ -// # References -// -// "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt -// "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt -// "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt -// Browser conformance tests at: http://greenbytes.de/tech/tc2231/ -// IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml - -use header; -use header::ExtendedValue; -use header::{Header, IntoHeaderValue, Writer}; -use regex::Regex; - -use std::fmt::{self, Write}; - -/// Split at the index of the first `needle` if it exists or at the end. -fn split_once(haystack: &str, needle: char) -> (&str, &str) { - haystack.find(needle).map_or_else( - || (haystack, ""), - |sc| { - let (first, last) = haystack.split_at(sc); - (first, last.split_at(1).1) - }, - ) -} - -/// Split at the index of the first `needle` if it exists or at the end, trim the right of the -/// first part and the left of the last part. -fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) { - let (first, last) = split_once(haystack, needle); - (first.trim_right(), last.trim_left()) -} - -/// The implied disposition of the content of the HTTP body. -#[derive(Clone, Debug, PartialEq)] -pub enum DispositionType { - /// Inline implies default processing - Inline, - /// Attachment implies that the recipient should prompt the user to save the response locally, - /// rather than process it normally (as per its media type). - Attachment, - /// Used in *multipart/form-data* as defined in - /// [RFC7578](https://tools.ietf.org/html/rfc7578) to carry the field name and the file name. - FormData, - /// Extension type. Should be handled by recipients the same way as Attachment - Ext(String), -} - -impl<'a> From<&'a str> for DispositionType { - fn from(origin: &'a str) -> DispositionType { - if origin.eq_ignore_ascii_case("inline") { - DispositionType::Inline - } else if origin.eq_ignore_ascii_case("attachment") { - DispositionType::Attachment - } else if origin.eq_ignore_ascii_case("form-data") { - DispositionType::FormData - } else { - DispositionType::Ext(origin.to_owned()) - } - } -} - -/// Parameter in [`ContentDisposition`]. -/// -/// # Examples -/// ``` -/// use actix_http::http::header::DispositionParam; -/// -/// let param = DispositionParam::Filename(String::from("sample.txt")); -/// assert!(param.is_filename()); -/// assert_eq!(param.as_filename().unwrap(), "sample.txt"); -/// ``` -#[derive(Clone, Debug, PartialEq)] -pub enum DispositionParam { - /// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from - /// the form. - Name(String), - /// A plain file name. - Filename(String), - /// An extended file name. It must not exist for `ContentType::Formdata` according to - /// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2). - FilenameExt(ExtendedValue), - /// An unrecognized regular parameter as defined in - /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *reg-parameter*, in - /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *token "=" value*. Recipients should - /// ignore unrecognizable parameters. - Unknown(String, String), - /// An unrecognized extended paramater as defined in - /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *ext-parameter*, in - /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *ext-token "=" ext-value*. The single - /// trailling asterisk is not included. Recipients should ignore unrecognizable parameters. - UnknownExt(String, ExtendedValue), -} - -impl DispositionParam { - /// Returns `true` if the paramater is [`Name`](DispositionParam::Name). - #[inline] - pub fn is_name(&self) -> bool { - self.as_name().is_some() - } - - /// Returns `true` if the paramater is [`Filename`](DispositionParam::Filename). - #[inline] - pub fn is_filename(&self) -> bool { - self.as_filename().is_some() - } - - /// Returns `true` if the paramater is [`FilenameExt`](DispositionParam::FilenameExt). - #[inline] - pub fn is_filename_ext(&self) -> bool { - self.as_filename_ext().is_some() - } - - /// Returns `true` if the paramater is [`Unknown`](DispositionParam::Unknown) and the `name` - #[inline] - /// matches. - pub fn is_unknown>(&self, name: T) -> bool { - self.as_unknown(name).is_some() - } - - /// Returns `true` if the paramater is [`UnknownExt`](DispositionParam::UnknownExt) and the - /// `name` matches. - #[inline] - pub fn is_unknown_ext>(&self, name: T) -> bool { - self.as_unknown_ext(name).is_some() - } - - /// Returns the name if applicable. - #[inline] - pub fn as_name(&self) -> Option<&str> { - match self { - DispositionParam::Name(ref name) => Some(name.as_str()), - _ => None, - } - } - - /// Returns the filename if applicable. - #[inline] - pub fn as_filename(&self) -> Option<&str> { - match self { - DispositionParam::Filename(ref filename) => Some(filename.as_str()), - _ => None, - } - } - - /// Returns the filename* if applicable. - #[inline] - pub fn as_filename_ext(&self) -> Option<&ExtendedValue> { - match self { - DispositionParam::FilenameExt(ref value) => Some(value), - _ => None, - } - } - - /// Returns the value of the unrecognized regular parameter if it is - /// [`Unknown`](DispositionParam::Unknown) and the `name` matches. - #[inline] - pub fn as_unknown>(&self, name: T) -> Option<&str> { - match self { - DispositionParam::Unknown(ref ext_name, ref value) - if ext_name.eq_ignore_ascii_case(name.as_ref()) => - { - Some(value.as_str()) - } - _ => None, - } - } - - /// Returns the value of the unrecognized extended parameter if it is - /// [`Unknown`](DispositionParam::Unknown) and the `name` matches. - #[inline] - pub fn as_unknown_ext>(&self, name: T) -> Option<&ExtendedValue> { - match self { - DispositionParam::UnknownExt(ref ext_name, ref value) - if ext_name.eq_ignore_ascii_case(name.as_ref()) => - { - Some(value) - } - _ => None, - } - } -} - -/// A *Content-Disposition* header. It is compatible to be used either as -/// [a response header for the main body](https://mdn.io/Content-Disposition#As_a_response_header_for_the_main_body) -/// as (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266), or as -/// [a header for a multipart body](https://mdn.io/Content-Disposition#As_a_header_for_a_multipart_body) -/// as (re)defined in [RFC7587](https://tools.ietf.org/html/rfc7578). -/// -/// In a regular HTTP response, the *Content-Disposition* response header is a header indicating if -/// the content is expected to be displayed *inline* in the browser, that is, as a Web page or as -/// part of a Web page, or as an attachment, that is downloaded and saved locally, and also can be -/// used to attach additional metadata, such as the filename to use when saving the response payload -/// locally. -/// -/// In a *multipart/form-data* body, the HTTP *Content-Disposition* general header is a header that -/// can be used on the subpart of a multipart body to give information about the field it applies to. -/// The subpart is delimited by the boundary defined in the *Content-Type* header. Used on the body -/// itself, *Content-Disposition* has no effect. -/// -/// # ABNF - -/// ```text -/// content-disposition = "Content-Disposition" ":" -/// disposition-type *( ";" disposition-parm ) -/// -/// disposition-type = "inline" | "attachment" | disp-ext-type -/// ; case-insensitive -/// -/// disp-ext-type = token -/// -/// disposition-parm = filename-parm | disp-ext-parm -/// -/// filename-parm = "filename" "=" value -/// | "filename*" "=" ext-value -/// -/// disp-ext-parm = token "=" value -/// | ext-token "=" ext-value -/// -/// ext-token = -/// ``` -/// -/// **Note**: filename* [must not](https://tools.ietf.org/html/rfc7578#section-4.2) be used within -/// *multipart/form-data*. -/// -/// # Example -/// -/// ``` -/// use actix_http::http::header::{ -/// Charset, ContentDisposition, DispositionParam, DispositionType, -/// ExtendedValue, -/// }; -/// -/// let cd1 = ContentDisposition { -/// disposition: DispositionType::Attachment, -/// parameters: vec![DispositionParam::FilenameExt(ExtendedValue { -/// charset: Charset::Iso_8859_1, // The character set for the bytes of the filename -/// language_tag: None, // The optional language tag (see `language-tag` crate) -/// value: b"\xa9 Copyright 1989.txt".to_vec(), // the actual bytes of the filename -/// })], -/// }; -/// assert!(cd1.is_attachment()); -/// assert!(cd1.get_filename_ext().is_some()); -/// -/// let cd2 = ContentDisposition { -/// disposition: DispositionType::FormData, -/// parameters: vec![ -/// DispositionParam::Name(String::from("file")), -/// DispositionParam::Filename(String::from("bill.odt")), -/// ], -/// }; -/// assert_eq!(cd2.get_name(), Some("file")); // field name -/// assert_eq!(cd2.get_filename(), Some("bill.odt")); -/// ``` -/// -/// # WARN -/// If "filename" parameter is supplied, do not use the file name blindly, check and possibly -/// change to match local file system conventions if applicable, and do not use directory path -/// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3) -/// . -#[derive(Clone, Debug, PartialEq)] -pub struct ContentDisposition { - /// The disposition type - pub disposition: DispositionType, - /// Disposition parameters - pub parameters: Vec, -} - -impl ContentDisposition { - /// Parse a raw Content-Disposition header value. - pub fn from_raw(hv: &header::HeaderValue) -> Result { - // `header::from_one_raw_str` invokes `hv.to_str` which assumes `hv` contains only visible - // ASCII characters. So `hv.as_bytes` is necessary here. - let hv = String::from_utf8(hv.as_bytes().to_vec()) - .map_err(|_| ::error::ParseError::Header)?; - let (disp_type, mut left) = split_once_and_trim(hv.as_str().trim(), ';'); - if disp_type.is_empty() { - return Err(::error::ParseError::Header); - } - let mut cd = ContentDisposition { - disposition: disp_type.into(), - parameters: Vec::new(), - }; - - while !left.is_empty() { - let (param_name, new_left) = split_once_and_trim(left, '='); - if param_name.is_empty() || param_name == "*" || new_left.is_empty() { - return Err(::error::ParseError::Header); - } - left = new_left; - if param_name.ends_with('*') { - // extended parameters - let param_name = ¶m_name[..param_name.len() - 1]; // trim asterisk - let (ext_value, new_left) = split_once_and_trim(left, ';'); - left = new_left; - let ext_value = header::parse_extended_value(ext_value)?; - - let param = if param_name.eq_ignore_ascii_case("filename") { - DispositionParam::FilenameExt(ext_value) - } else { - DispositionParam::UnknownExt(param_name.to_owned(), ext_value) - }; - cd.parameters.push(param); - } else { - // regular parameters - let value = if left.starts_with('\"') { - // quoted-string: defined in RFC6266 -> RFC2616 Section 3.6 - let mut escaping = false; - let mut quoted_string = vec![]; - let mut end = None; - // search for closing quote - for (i, &c) in left.as_bytes().iter().skip(1).enumerate() { - if escaping { - escaping = false; - quoted_string.push(c); - } else if c == 0x5c { - // backslash - escaping = true; - } else if c == 0x22 { - // double quote - end = Some(i + 1); // cuz skipped 1 for the leading quote - break; - } else { - quoted_string.push(c); - } - } - left = &left[end.ok_or(::error::ParseError::Header)? + 1..]; - left = split_once(left, ';').1.trim_left(); - // In fact, it should not be Err if the above code is correct. - String::from_utf8(quoted_string) - .map_err(|_| ::error::ParseError::Header)? - } else { - // token: won't contains semicolon according to RFC 2616 Section 2.2 - let (token, new_left) = split_once_and_trim(left, ';'); - left = new_left; - token.to_owned() - }; - if value.is_empty() { - return Err(::error::ParseError::Header); - } - - let param = if param_name.eq_ignore_ascii_case("name") { - DispositionParam::Name(value) - } else if param_name.eq_ignore_ascii_case("filename") { - DispositionParam::Filename(value) - } else { - DispositionParam::Unknown(param_name.to_owned(), value) - }; - cd.parameters.push(param); - } - } - - Ok(cd) - } - - /// Returns `true` if it is [`Inline`](DispositionType::Inline). - pub fn is_inline(&self) -> bool { - match self.disposition { - DispositionType::Inline => true, - _ => false, - } - } - - /// Returns `true` if it is [`Attachment`](DispositionType::Attachment). - pub fn is_attachment(&self) -> bool { - match self.disposition { - DispositionType::Attachment => true, - _ => false, - } - } - - /// Returns `true` if it is [`FormData`](DispositionType::FormData). - pub fn is_form_data(&self) -> bool { - match self.disposition { - DispositionType::FormData => true, - _ => false, - } - } - - /// Returns `true` if it is [`Ext`](DispositionType::Ext) and the `disp_type` matches. - pub fn is_ext>(&self, disp_type: T) -> bool { - match self.disposition { - DispositionType::Ext(ref t) - if t.eq_ignore_ascii_case(disp_type.as_ref()) => - { - true - } - _ => false, - } - } - - /// Return the value of *name* if exists. - pub fn get_name(&self) -> Option<&str> { - self.parameters.iter().filter_map(|p| p.as_name()).nth(0) - } - - /// Return the value of *filename* if exists. - pub fn get_filename(&self) -> Option<&str> { - self.parameters - .iter() - .filter_map(|p| p.as_filename()) - .nth(0) - } - - /// Return the value of *filename\** if exists. - pub fn get_filename_ext(&self) -> Option<&ExtendedValue> { - self.parameters - .iter() - .filter_map(|p| p.as_filename_ext()) - .nth(0) - } - - /// Return the value of the parameter which the `name` matches. - pub fn get_unknown>(&self, name: T) -> Option<&str> { - let name = name.as_ref(); - self.parameters - .iter() - .filter_map(|p| p.as_unknown(name)) - .nth(0) - } - - /// Return the value of the extended parameter which the `name` matches. - pub fn get_unknown_ext>(&self, name: T) -> Option<&ExtendedValue> { - let name = name.as_ref(); - self.parameters - .iter() - .filter_map(|p| p.as_unknown_ext(name)) - .nth(0) - } -} - -impl IntoHeaderValue for ContentDisposition { - type Error = header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut writer = Writer::new(); - let _ = write!(&mut writer, "{}", self); - header::HeaderValue::from_shared(writer.take()) - } -} - -impl Header for ContentDisposition { - fn name() -> header::HeaderName { - header::CONTENT_DISPOSITION - } - - fn parse(msg: &T) -> Result { - if let Some(h) = msg.headers().get(Self::name()) { - Self::from_raw(&h) - } else { - Err(::error::ParseError::Header) - } - } -} - -impl fmt::Display for DispositionType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - DispositionType::Inline => write!(f, "inline"), - DispositionType::Attachment => write!(f, "attachment"), - DispositionType::FormData => write!(f, "form-data"), - DispositionType::Ext(ref s) => write!(f, "{}", s), - } - } -} - -impl fmt::Display for DispositionParam { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // All ASCII control charaters (0-30, 127) excepting horizontal tab, double quote, and - // backslash should be escaped in quoted-string (i.e. "foobar"). - // Ref: RFC6266 S4.1 -> RFC2616 S2.2; RFC 7578 S4.2 -> RFC2183 S2 -> ... . - lazy_static! { - static ref RE: Regex = Regex::new("[\x01-\x08\x10\x1F\x7F\"\\\\]").unwrap(); - } - match self { - DispositionParam::Name(ref value) => write!(f, "name={}", value), - DispositionParam::Filename(ref value) => { - write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref()) - } - DispositionParam::Unknown(ref name, ref value) => write!( - f, - "{}=\"{}\"", - name, - &RE.replace_all(value, "\\$0").as_ref() - ), - DispositionParam::FilenameExt(ref ext_value) => { - write!(f, "filename*={}", ext_value) - } - DispositionParam::UnknownExt(ref name, ref ext_value) => { - write!(f, "{}*={}", name, ext_value) - } - } - } -} - -impl fmt::Display for ContentDisposition { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.disposition)?; - self.parameters - .iter() - .map(|param| write!(f, "; {}", param)) - .collect() - } -} - -#[cfg(test)] -mod tests { - use super::{ContentDisposition, DispositionParam, DispositionType}; - use header::shared::Charset; - use header::{ExtendedValue, HeaderValue}; - #[test] - fn test_from_raw_basic() { - assert!(ContentDisposition::from_raw(&HeaderValue::from_static("")).is_err()); - - let a = HeaderValue::from_static( - "form-data; dummy=3; name=upload; filename=\"sample.png\"", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), - DispositionParam::Name("upload".to_owned()), - DispositionParam::Filename("sample.png".to_owned()), - ], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static("attachment; filename=\"image.jpg\""); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::Filename("image.jpg".to_owned())], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static("inline; filename=image.jpg"); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![DispositionParam::Filename("image.jpg".to_owned())], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static( - "attachment; creation-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::Unknown( - String::from("creation-date"), - "Wed, 12 Feb 1997 16:29:51 -0500".to_owned(), - )], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_extended() { - let a = HeaderValue::from_static( - "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::FilenameExt(ExtendedValue { - charset: Charset::Ext(String::from("UTF-8")), - language_tag: None, - value: vec![ - 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, - b'r', b'a', b't', b'e', b's', - ], - })], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static( - "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::FilenameExt(ExtendedValue { - charset: Charset::Ext(String::from("UTF-8")), - language_tag: None, - value: vec![ - 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, - b'r', b'a', b't', b'e', b's', - ], - })], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_extra_whitespace() { - let a = HeaderValue::from_static( - "form-data ; du-mmy= 3 ; name =upload ; filename = \"sample.png\" ; ", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("du-mmy".to_owned(), "3".to_owned()), - DispositionParam::Name("upload".to_owned()), - DispositionParam::Filename("sample.png".to_owned()), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_unordered() { - let a = HeaderValue::from_static( - "form-data; dummy=3; filename=\"sample.png\" ; name=upload;", - // Actually, a trailling semolocon is not compliant. But it is fine to accept. - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), - DispositionParam::Filename("sample.png".to_owned()), - DispositionParam::Name("upload".to_owned()), - ], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_str( - "attachment; filename*=iso-8859-1''foo-%E4.html; filename=\"foo-ä.html\"", - ).unwrap(); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![ - DispositionParam::FilenameExt(ExtendedValue { - charset: Charset::Iso_8859_1, - language_tag: None, - value: b"foo-\xe4.html".to_vec(), - }), - DispositionParam::Filename("foo-ä.html".to_owned()), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_only_disp() { - let a = ContentDisposition::from_raw(&HeaderValue::from_static("attachment")) - .unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![], - }; - assert_eq!(a, b); - - let a = - ContentDisposition::from_raw(&HeaderValue::from_static("inline ;")).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![], - }; - assert_eq!(a, b); - - let a = ContentDisposition::from_raw(&HeaderValue::from_static( - "unknown-disp-param", - )).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Ext(String::from("unknown-disp-param")), - parameters: vec![], - }; - assert_eq!(a, b); - } - - #[test] - fn from_raw_with_mixed_case() { - let a = HeaderValue::from_str( - "InLInE; fIlenAME*=iso-8859-1''foo-%E4.html; filEName=\"foo-ä.html\"", - ).unwrap(); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![ - DispositionParam::FilenameExt(ExtendedValue { - charset: Charset::Iso_8859_1, - language_tag: None, - value: b"foo-\xe4.html".to_vec(), - }), - DispositionParam::Filename("foo-ä.html".to_owned()), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn from_raw_with_unicode() { - /* RFC7578 Section 4.2: - Some commonly deployed systems use multipart/form-data with file names directly encoded - including octets outside the US-ASCII range. The encoding used for the file names is - typically UTF-8, although HTML forms will use the charset associated with the form. - - Mainstream browsers like Firefox (gecko) and Chrome use UTF-8 directly as above. - (And now, only UTF-8 is handled by this implementation.) - */ - let a = - HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"") - .unwrap(); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Name(String::from("upload")), - DispositionParam::Filename(String::from("文件.webp")), - ], - }; - assert_eq!(a, b); - - let a = - HeaderValue::from_str("form-data; name=upload; filename=\"余固知謇謇之為患兮,å¿è€Œä¸èƒ½èˆä¹Ÿ.pptx\"").unwrap(); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Name(String::from("upload")), - DispositionParam::Filename(String::from( - "余固知謇謇之為患兮,å¿è€Œä¸èƒ½èˆä¹Ÿ.pptx", - )), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_escape() { - let a = HeaderValue::from_static( - "form-data; dummy=3; name=upload; filename=\"s\\amp\\\"le.png\"", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), - DispositionParam::Name("upload".to_owned()), - DispositionParam::Filename( - ['s', 'a', 'm', 'p', '\"', 'l', 'e', '.', 'p', 'n', 'g'] - .iter() - .collect(), - ), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_semicolon() { - let a = - HeaderValue::from_static("form-data; filename=\"A semicolon here;.pdf\""); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![DispositionParam::Filename(String::from( - "A semicolon here;.pdf", - ))], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_uncessary_percent_decode() { - let a = HeaderValue::from_static( - "form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"", // Should not be decoded! - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Name("photo".to_owned()), - DispositionParam::Filename(String::from("%74%65%73%74%2e%70%6e%67")), - ], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static( - "form-data; name=photo; filename=\"%74%65%73%74.png\"", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Name("photo".to_owned()), - DispositionParam::Filename(String::from("%74%65%73%74.png")), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_param_value_missing() { - let a = HeaderValue::from_static("form-data; name=upload ; filename="); - assert!(ContentDisposition::from_raw(&a).is_err()); - - let a = HeaderValue::from_static("attachment; dummy=; filename=invoice.pdf"); - assert!(ContentDisposition::from_raw(&a).is_err()); - - let a = HeaderValue::from_static("inline; filename= "); - assert!(ContentDisposition::from_raw(&a).is_err()); - } - - #[test] - fn test_from_raw_param_name_missing() { - let a = HeaderValue::from_static("inline; =\"test.txt\""); - assert!(ContentDisposition::from_raw(&a).is_err()); - - let a = HeaderValue::from_static("inline; =diary.odt"); - assert!(ContentDisposition::from_raw(&a).is_err()); - - let a = HeaderValue::from_static("inline; ="); - assert!(ContentDisposition::from_raw(&a).is_err()); - } - - #[test] - fn test_display_extended() { - let as_string = - "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates"; - let a = HeaderValue::from_static(as_string); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!(as_string, display_rendered); - - let a = HeaderValue::from_static("attachment; filename=colourful.csv"); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!( - "attachment; filename=\"colourful.csv\"".to_owned(), - display_rendered - ); - } - - #[test] - fn test_display_quote() { - let as_string = "form-data; name=upload; filename=\"Quote\\\"here.png\""; - as_string - .find(['\\', '\"'].iter().collect::().as_str()) - .unwrap(); // ensure `\"` is there - let a = HeaderValue::from_static(as_string); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!(as_string, display_rendered); - } - - #[test] - fn test_display_space_tab() { - let as_string = "form-data; name=upload; filename=\"Space here.png\""; - let a = HeaderValue::from_static(as_string); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!(as_string, display_rendered); - - let a: ContentDisposition = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![DispositionParam::Filename(String::from("Tab\there.png"))], - }; - let display_rendered = format!("{}", a); - assert_eq!("inline; filename=\"Tab\x09here.png\"", display_rendered); - } - - #[test] - fn test_display_control_characters() { - /* let a = "attachment; filename=\"carriage\rreturn.png\""; - let a = HeaderValue::from_static(a); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!( - "attachment; filename=\"carriage\\\rreturn.png\"", - display_rendered - );*/ - // No way to create a HeaderValue containing a carriage return. - - let a: ContentDisposition = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![DispositionParam::Filename(String::from("bell\x07.png"))], - }; - let display_rendered = format!("{}", a); - assert_eq!("inline; filename=\"bell\\\x07.png\"", display_rendered); - } - - #[test] - fn test_param_methods() { - let param = DispositionParam::Filename(String::from("sample.txt")); - assert!(param.is_filename()); - assert_eq!(param.as_filename().unwrap(), "sample.txt"); - - let param = DispositionParam::Unknown(String::from("foo"), String::from("bar")); - assert!(param.is_unknown("foo")); - assert_eq!(param.as_unknown("fOo"), Some("bar")); - } - - #[test] - fn test_disposition_methods() { - let cd = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), - DispositionParam::Name("upload".to_owned()), - DispositionParam::Filename("sample.png".to_owned()), - ], - }; - assert_eq!(cd.get_name(), Some("upload")); - assert_eq!(cd.get_unknown("dummy"), Some("3")); - assert_eq!(cd.get_filename(), Some("sample.png")); - assert_eq!(cd.get_unknown_ext("dummy"), None); - assert_eq!(cd.get_unknown("duMMy"), Some("3")); - } -} diff --git a/src/header/common/content_language.rs b/src/header/common/content_language.rs deleted file mode 100644 index c1f87d513..000000000 --- a/src/header/common/content_language.rs +++ /dev/null @@ -1,65 +0,0 @@ -use header::{QualityItem, CONTENT_LANGUAGE}; -use language_tags::LanguageTag; - -header! { - /// `Content-Language` header, defined in - /// [RFC7231](https://tools.ietf.org/html/rfc7231#section-3.1.3.2) - /// - /// The `Content-Language` header field describes the natural language(s) - /// of the intended audience for the representation. Note that this - /// might not be equivalent to all the languages used within the - /// representation. - /// - /// # ABNF - /// - /// ```text - /// Content-Language = 1#language-tag - /// ``` - /// - /// # Example values - /// - /// * `da` - /// * `mi, en` - /// - /// # Examples - /// - /// ```rust - /// # extern crate actix_http; - /// # #[macro_use] extern crate language_tags; - /// use actix_http::HttpResponse; - /// # use actix_http::http::header::{ContentLanguage, qitem}; - /// # - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// ContentLanguage(vec![ - /// qitem(langtag!(en)), - /// ]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate actix_http; - /// # #[macro_use] extern crate language_tags; - /// use actix_http::HttpResponse; - /// # use actix_http::http::header::{ContentLanguage, qitem}; - /// # - /// # fn main() { - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// ContentLanguage(vec![ - /// qitem(langtag!(da)), - /// qitem(langtag!(en;;;GB)), - /// ]) - /// ); - /// # } - /// ``` - (ContentLanguage, CONTENT_LANGUAGE) => (QualityItem)+ - - test_content_language { - test_header!(test1, vec![b"da"]); - test_header!(test2, vec![b"mi, en"]); - } -} diff --git a/src/header/common/content_range.rs b/src/header/common/content_range.rs deleted file mode 100644 index 999307e2a..000000000 --- a/src/header/common/content_range.rs +++ /dev/null @@ -1,210 +0,0 @@ -use error::ParseError; -use header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, Writer, - CONTENT_RANGE}; -use std::fmt::{self, Display, Write}; -use std::str::FromStr; - -header! { - /// `Content-Range` header, defined in - /// [RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2) - (ContentRange, CONTENT_RANGE) => [ContentRangeSpec] - - test_content_range { - test_header!(test_bytes, - vec![b"bytes 0-499/500"], - Some(ContentRange(ContentRangeSpec::Bytes { - range: Some((0, 499)), - instance_length: Some(500) - }))); - - test_header!(test_bytes_unknown_len, - vec![b"bytes 0-499/*"], - Some(ContentRange(ContentRangeSpec::Bytes { - range: Some((0, 499)), - instance_length: None - }))); - - test_header!(test_bytes_unknown_range, - vec![b"bytes */500"], - Some(ContentRange(ContentRangeSpec::Bytes { - range: None, - instance_length: Some(500) - }))); - - test_header!(test_unregistered, - vec![b"seconds 1-2"], - Some(ContentRange(ContentRangeSpec::Unregistered { - unit: "seconds".to_owned(), - resp: "1-2".to_owned() - }))); - - test_header!(test_no_len, - vec![b"bytes 0-499"], - None::); - - test_header!(test_only_unit, - vec![b"bytes"], - None::); - - test_header!(test_end_less_than_start, - vec![b"bytes 499-0/500"], - None::); - - test_header!(test_blank, - vec![b""], - None::); - - test_header!(test_bytes_many_spaces, - vec![b"bytes 1-2/500 3"], - None::); - - test_header!(test_bytes_many_slashes, - vec![b"bytes 1-2/500/600"], - None::); - - test_header!(test_bytes_many_dashes, - vec![b"bytes 1-2-3/500"], - None::); - - } -} - -/// Content-Range, described in [RFC7233](https://tools.ietf.org/html/rfc7233#section-4.2) -/// -/// # ABNF -/// -/// ```text -/// Content-Range = byte-content-range -/// / other-content-range -/// -/// byte-content-range = bytes-unit SP -/// ( byte-range-resp / unsatisfied-range ) -/// -/// byte-range-resp = byte-range "/" ( complete-length / "*" ) -/// byte-range = first-byte-pos "-" last-byte-pos -/// unsatisfied-range = "*/" complete-length -/// -/// complete-length = 1*DIGIT -/// -/// other-content-range = other-range-unit SP other-range-resp -/// other-range-resp = *CHAR -/// ``` -#[derive(PartialEq, Clone, Debug)] -pub enum ContentRangeSpec { - /// Byte range - Bytes { - /// First and last bytes of the range, omitted if request could not be - /// satisfied - range: Option<(u64, u64)>, - - /// Total length of the instance, can be omitted if unknown - instance_length: Option, - }, - - /// Custom range, with unit not registered at IANA - Unregistered { - /// other-range-unit - unit: String, - - /// other-range-resp - resp: String, - }, -} - -fn split_in_two(s: &str, separator: char) -> Option<(&str, &str)> { - let mut iter = s.splitn(2, separator); - match (iter.next(), iter.next()) { - (Some(a), Some(b)) => Some((a, b)), - _ => None, - } -} - -impl FromStr for ContentRangeSpec { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - let res = match split_in_two(s, ' ') { - Some(("bytes", resp)) => { - let (range, instance_length) = - split_in_two(resp, '/').ok_or(ParseError::Header)?; - - let instance_length = if instance_length == "*" { - None - } else { - Some(instance_length - .parse() - .map_err(|_| ParseError::Header)?) - }; - - let range = if range == "*" { - None - } else { - let (first_byte, last_byte) = - split_in_two(range, '-').ok_or(ParseError::Header)?; - let first_byte = first_byte.parse().map_err(|_| ParseError::Header)?; - let last_byte = last_byte.parse().map_err(|_| ParseError::Header)?; - if last_byte < first_byte { - return Err(ParseError::Header); - } - Some((first_byte, last_byte)) - }; - - ContentRangeSpec::Bytes { - range, - instance_length, - } - } - Some((unit, resp)) => ContentRangeSpec::Unregistered { - unit: unit.to_owned(), - resp: resp.to_owned(), - }, - _ => return Err(ParseError::Header), - }; - Ok(res) - } -} - -impl Display for ContentRangeSpec { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ContentRangeSpec::Bytes { - range, - instance_length, - } => { - f.write_str("bytes ")?; - match range { - Some((first_byte, last_byte)) => { - write!(f, "{}-{}", first_byte, last_byte)?; - } - None => { - f.write_str("*")?; - } - }; - f.write_str("/")?; - if let Some(v) = instance_length { - write!(f, "{}", v) - } else { - f.write_str("*") - } - } - ContentRangeSpec::Unregistered { - ref unit, - ref resp, - } => { - f.write_str(unit)?; - f.write_str(" ")?; - f.write_str(resp) - } - } - } -} - -impl IntoHeaderValue for ContentRangeSpec { - type Error = InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut writer = Writer::new(); - let _ = write!(&mut writer, "{}", self); - HeaderValue::from_shared(writer.take()) - } -} diff --git a/src/header/common/content_type.rs b/src/header/common/content_type.rs deleted file mode 100644 index 3286d4cae..000000000 --- a/src/header/common/content_type.rs +++ /dev/null @@ -1,122 +0,0 @@ -use header::CONTENT_TYPE; -use mime::{self, Mime}; - -header! { - /// `Content-Type` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-3.1.1.5) - /// - /// The `Content-Type` header field indicates the media type of the - /// associated representation: either the representation enclosed in the - /// message payload or the selected representation, as determined by the - /// message semantics. The indicated media type defines both the data - /// format and how that data is intended to be processed by a recipient, - /// within the scope of the received message semantics, after any content - /// codings indicated by Content-Encoding are decoded. - /// - /// Although the `mime` crate allows the mime options to be any slice, this crate - /// forces the use of Vec. This is to make sure the same header can't have more than 1 type. If - /// this is an issue, it's possible to implement `Header` on a custom struct. - /// - /// # ABNF - /// - /// ```text - /// Content-Type = media-type - /// ``` - /// - /// # Example values - /// - /// * `text/html; charset=utf-8` - /// * `application/json` - /// - /// # Examples - /// - /// ```rust - /// use actix_http::HttpResponse; - /// use actix_http::http::header::ContentType; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// ContentType::json() - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate mime; - /// # extern crate actix_http; - /// use mime::TEXT_HTML; - /// use actix_http::HttpResponse; - /// use actix_http::http::header::ContentType; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// ContentType(TEXT_HTML) - /// ); - /// # } - /// ``` - (ContentType, CONTENT_TYPE) => [Mime] - - test_content_type { - test_header!( - test1, - vec![b"text/html"], - Some(HeaderField(TEXT_HTML))); - } -} - -impl ContentType { - /// A constructor to easily create a `Content-Type: application/json` - /// header. - #[inline] - pub fn json() -> ContentType { - ContentType(mime::APPLICATION_JSON) - } - - /// A constructor to easily create a `Content-Type: text/plain; - /// charset=utf-8` header. - #[inline] - pub fn plaintext() -> ContentType { - ContentType(mime::TEXT_PLAIN_UTF_8) - } - - /// A constructor to easily create a `Content-Type: text/html` header. - #[inline] - pub fn html() -> ContentType { - ContentType(mime::TEXT_HTML) - } - - /// A constructor to easily create a `Content-Type: text/xml` header. - #[inline] - pub fn xml() -> ContentType { - ContentType(mime::TEXT_XML) - } - - /// A constructor to easily create a `Content-Type: - /// application/www-form-url-encoded` header. - #[inline] - pub fn form_url_encoded() -> ContentType { - ContentType(mime::APPLICATION_WWW_FORM_URLENCODED) - } - /// A constructor to easily create a `Content-Type: image/jpeg` header. - #[inline] - pub fn jpeg() -> ContentType { - ContentType(mime::IMAGE_JPEG) - } - - /// A constructor to easily create a `Content-Type: image/png` header. - #[inline] - pub fn png() -> ContentType { - ContentType(mime::IMAGE_PNG) - } - - /// A constructor to easily create a `Content-Type: - /// application/octet-stream` header. - #[inline] - pub fn octet_stream() -> ContentType { - ContentType(mime::APPLICATION_OCTET_STREAM) - } -} - -impl Eq for ContentType {} diff --git a/src/header/common/date.rs b/src/header/common/date.rs deleted file mode 100644 index 9ce2bd65f..000000000 --- a/src/header/common/date.rs +++ /dev/null @@ -1,42 +0,0 @@ -use header::{HttpDate, DATE}; -use std::time::SystemTime; - -header! { - /// `Date` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.1.1.2) - /// - /// The `Date` header field represents the date and time at which the - /// message was originated. - /// - /// # ABNF - /// - /// ```text - /// Date = HTTP-date - /// ``` - /// - /// # Example values - /// - /// * `Tue, 15 Nov 1994 08:12:31 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_http::HttpResponse; - /// use actix_http::http::header::Date; - /// use std::time::SystemTime; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(Date(SystemTime::now().into())); - /// ``` - (Date, DATE) => [HttpDate] - - test_date { - test_header!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]); - } -} - -impl Date { - /// Create a date instance set to the current system time - pub fn now() -> Date { - Date(SystemTime::now().into()) - } -} diff --git a/src/header/common/etag.rs b/src/header/common/etag.rs deleted file mode 100644 index ea4be2a77..000000000 --- a/src/header/common/etag.rs +++ /dev/null @@ -1,96 +0,0 @@ -use header::{EntityTag, ETAG}; - -header! { - /// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3) - /// - /// The `ETag` header field in a response provides the current entity-tag - /// for the selected representation, as determined at the conclusion of - /// handling the request. An entity-tag is an opaque validator for - /// differentiating between multiple representations of the same - /// resource, regardless of whether those multiple representations are - /// due to resource state changes over time, content negotiation - /// resulting in multiple representations being valid at the same time, - /// or both. An entity-tag consists of an opaque quoted string, possibly - /// prefixed by a weakness indicator. - /// - /// # ABNF - /// - /// ```text - /// ETag = entity-tag - /// ``` - /// - /// # Example values - /// - /// * `"xyzzy"` - /// * `W/"xyzzy"` - /// * `""` - /// - /// # Examples - /// - /// ```rust - /// use actix_http::HttpResponse; - /// use actix_http::http::header::{ETag, EntityTag}; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(ETag(EntityTag::new(false, "xyzzy".to_owned()))); - /// ``` - /// - /// ```rust - /// use actix_http::HttpResponse; - /// use actix_http::http::header::{ETag, EntityTag}; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(ETag(EntityTag::new(true, "xyzzy".to_owned()))); - /// ``` - (ETag, ETAG) => [EntityTag] - - test_etag { - // From the RFC - test_header!(test1, - vec![b"\"xyzzy\""], - Some(ETag(EntityTag::new(false, "xyzzy".to_owned())))); - test_header!(test2, - vec![b"W/\"xyzzy\""], - Some(ETag(EntityTag::new(true, "xyzzy".to_owned())))); - test_header!(test3, - vec![b"\"\""], - Some(ETag(EntityTag::new(false, "".to_owned())))); - // Own tests - test_header!(test4, - vec![b"\"foobar\""], - Some(ETag(EntityTag::new(false, "foobar".to_owned())))); - test_header!(test5, - vec![b"\"\""], - Some(ETag(EntityTag::new(false, "".to_owned())))); - test_header!(test6, - vec![b"W/\"weak-etag\""], - Some(ETag(EntityTag::new(true, "weak-etag".to_owned())))); - test_header!(test7, - vec![b"W/\"\x65\x62\""], - Some(ETag(EntityTag::new(true, "\u{0065}\u{0062}".to_owned())))); - test_header!(test8, - vec![b"W/\"\""], - Some(ETag(EntityTag::new(true, "".to_owned())))); - test_header!(test9, - vec![b"no-dquotes"], - None::); - test_header!(test10, - vec![b"w/\"the-first-w-is-case-sensitive\""], - None::); - test_header!(test11, - vec![b""], - None::); - test_header!(test12, - vec![b"\"unmatched-dquotes1"], - None::); - test_header!(test13, - vec![b"unmatched-dquotes2\""], - None::); - test_header!(test14, - vec![b"matched-\"dquotes\""], - None::); - test_header!(test15, - vec![b"\""], - None::); - } -} diff --git a/src/header/common/expires.rs b/src/header/common/expires.rs deleted file mode 100644 index bdd25fdb9..000000000 --- a/src/header/common/expires.rs +++ /dev/null @@ -1,39 +0,0 @@ -use header::{HttpDate, EXPIRES}; - -header! { - /// `Expires` header, defined in [RFC7234](http://tools.ietf.org/html/rfc7234#section-5.3) - /// - /// The `Expires` header field gives the date/time after which the - /// response is considered stale. - /// - /// The presence of an Expires field does not imply that the original - /// resource will change or cease to exist at, before, or after that - /// time. - /// - /// # ABNF - /// - /// ```text - /// Expires = HTTP-date - /// ``` - /// - /// # Example values - /// * `Thu, 01 Dec 1994 16:00:00 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_http::HttpResponse; - /// use actix_http::http::header::Expires; - /// use std::time::{SystemTime, Duration}; - /// - /// let mut builder = HttpResponse::Ok(); - /// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24); - /// builder.set(Expires(expiration.into())); - /// ``` - (Expires, EXPIRES) => [HttpDate] - - test_expires { - // Test case from RFC - test_header!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]); - } -} diff --git a/src/header/common/if_match.rs b/src/header/common/if_match.rs deleted file mode 100644 index 5f7976a4a..000000000 --- a/src/header/common/if_match.rs +++ /dev/null @@ -1,70 +0,0 @@ -use header::{EntityTag, IF_MATCH}; - -header! { - /// `If-Match` header, defined in - /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.1) - /// - /// The `If-Match` header field makes the request method conditional on - /// the recipient origin server either having at least one current - /// representation of the target resource, when the field-value is "*", - /// or having a current representation of the target resource that has an - /// entity-tag matching a member of the list of entity-tags provided in - /// the field-value. - /// - /// An origin server MUST use the strong comparison function when - /// comparing entity-tags for `If-Match`, since the client - /// intends this precondition to prevent the method from being applied if - /// there have been any changes to the representation data. - /// - /// # ABNF - /// - /// ```text - /// If-Match = "*" / 1#entity-tag - /// ``` - /// - /// # Example values - /// - /// * `"xyzzy"` - /// * "xyzzy", "r2d2xxxx", "c3piozzzz" - /// - /// # Examples - /// - /// ```rust - /// use actix_http::HttpResponse; - /// use actix_http::http::header::IfMatch; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(IfMatch::Any); - /// ``` - /// - /// ```rust - /// use actix_http::HttpResponse; - /// use actix_http::http::header::{IfMatch, EntityTag}; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// IfMatch::Items(vec![ - /// EntityTag::new(false, "xyzzy".to_owned()), - /// EntityTag::new(false, "foobar".to_owned()), - /// EntityTag::new(false, "bazquux".to_owned()), - /// ]) - /// ); - /// ``` - (IfMatch, IF_MATCH) => {Any / (EntityTag)+} - - test_if_match { - test_header!( - test1, - vec![b"\"xyzzy\""], - Some(HeaderField::Items( - vec![EntityTag::new(false, "xyzzy".to_owned())]))); - test_header!( - test2, - vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""], - Some(HeaderField::Items( - vec![EntityTag::new(false, "xyzzy".to_owned()), - EntityTag::new(false, "r2d2xxxx".to_owned()), - EntityTag::new(false, "c3piozzzz".to_owned())]))); - test_header!(test3, vec![b"*"], Some(IfMatch::Any)); - } -} diff --git a/src/header/common/if_modified_since.rs b/src/header/common/if_modified_since.rs deleted file mode 100644 index 41d6fba27..000000000 --- a/src/header/common/if_modified_since.rs +++ /dev/null @@ -1,39 +0,0 @@ -use header::{HttpDate, IF_MODIFIED_SINCE}; - -header! { - /// `If-Modified-Since` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.3) - /// - /// The `If-Modified-Since` header field makes a GET or HEAD request - /// method conditional on the selected representation's modification date - /// being more recent than the date provided in the field-value. - /// Transfer of the selected representation's data is avoided if that - /// data has not changed. - /// - /// # ABNF - /// - /// ```text - /// If-Unmodified-Since = HTTP-date - /// ``` - /// - /// # Example values - /// * `Sat, 29 Oct 1994 19:43:31 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_http::HttpResponse; - /// use actix_http::http::header::IfModifiedSince; - /// use std::time::{SystemTime, Duration}; - /// - /// let mut builder = HttpResponse::Ok(); - /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); - /// builder.set(IfModifiedSince(modified.into())); - /// ``` - (IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate] - - test_if_modified_since { - // Test case from RFC - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); - } -} diff --git a/src/header/common/if_none_match.rs b/src/header/common/if_none_match.rs deleted file mode 100644 index 8b3905bab..000000000 --- a/src/header/common/if_none_match.rs +++ /dev/null @@ -1,92 +0,0 @@ -use header::{EntityTag, IF_NONE_MATCH}; - -header! { - /// `If-None-Match` header, defined in - /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.2) - /// - /// The `If-None-Match` header field makes the request method conditional - /// on a recipient cache or origin server either not having any current - /// representation of the target resource, when the field-value is "*", - /// or having a selected representation with an entity-tag that does not - /// match any of those listed in the field-value. - /// - /// A recipient MUST use the weak comparison function when comparing - /// entity-tags for If-None-Match (Section 2.3.2), since weak entity-tags - /// can be used for cache validation even if there have been changes to - /// the representation data. - /// - /// # ABNF - /// - /// ```text - /// If-None-Match = "*" / 1#entity-tag - /// ``` - /// - /// # Example values - /// - /// * `"xyzzy"` - /// * `W/"xyzzy"` - /// * `"xyzzy", "r2d2xxxx", "c3piozzzz"` - /// * `W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"` - /// * `*` - /// - /// # Examples - /// - /// ```rust - /// use actix_http::HttpResponse; - /// use actix_http::http::header::IfNoneMatch; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(IfNoneMatch::Any); - /// ``` - /// - /// ```rust - /// use actix_http::HttpResponse; - /// use actix_http::http::header::{IfNoneMatch, EntityTag}; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// IfNoneMatch::Items(vec![ - /// EntityTag::new(false, "xyzzy".to_owned()), - /// EntityTag::new(false, "foobar".to_owned()), - /// EntityTag::new(false, "bazquux".to_owned()), - /// ]) - /// ); - /// ``` - (IfNoneMatch, IF_NONE_MATCH) => {Any / (EntityTag)+} - - test_if_none_match { - test_header!(test1, vec![b"\"xyzzy\""]); - test_header!(test2, vec![b"W/\"xyzzy\""]); - test_header!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); - test_header!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]); - test_header!(test5, vec![b"*"]); - } -} - -#[cfg(test)] -mod tests { - use super::IfNoneMatch; - use header::{EntityTag, Header, IF_NONE_MATCH}; - use test::TestRequest; - - #[test] - fn test_if_none_match() { - let mut if_none_match: Result; - - let req = TestRequest::with_header(IF_NONE_MATCH, "*").finish(); - if_none_match = Header::parse(&req); - assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any)); - - let req = - TestRequest::with_header(IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..]) - .finish(); - - if_none_match = Header::parse(&req); - let mut entities: Vec = Vec::new(); - let foobar_etag = EntityTag::new(false, "foobar".to_owned()); - let weak_etag = EntityTag::new(true, "weak-etag".to_owned()); - entities.push(foobar_etag); - entities.push(weak_etag); - assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities))); - } -} diff --git a/src/header/common/if_range.rs b/src/header/common/if_range.rs deleted file mode 100644 index 8cbb8c897..000000000 --- a/src/header/common/if_range.rs +++ /dev/null @@ -1,117 +0,0 @@ -use error::ParseError; -use header::from_one_raw_str; -use header::{ - EntityTag, Header, HeaderName, HeaderValue, HttpDate, IntoHeaderValue, - InvalidHeaderValueBytes, Writer, -}; -use http::header; -use httpmessage::HttpMessage; -use std::fmt::{self, Display, Write}; - -/// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2) -/// -/// If a client has a partial copy of a representation and wishes to have -/// an up-to-date copy of the entire representation, it could use the -/// Range header field with a conditional GET (using either or both of -/// If-Unmodified-Since and If-Match.) However, if the precondition -/// fails because the representation has been modified, the client would -/// then have to make a second request to obtain the entire current -/// representation. -/// -/// The `If-Range` header field allows a client to \"short-circuit\" the -/// second request. Informally, its meaning is as follows: if the -/// representation is unchanged, send me the part(s) that I am requesting -/// in Range; otherwise, send me the entire representation. -/// -/// # ABNF -/// -/// ```text -/// If-Range = entity-tag / HTTP-date -/// ``` -/// -/// # Example values -/// -/// * `Sat, 29 Oct 1994 19:43:31 GMT` -/// * `\"xyzzy\"` -/// -/// # Examples -/// -/// ```rust -/// use actix_http::HttpResponse; -/// use actix_http::http::header::{EntityTag, IfRange}; -/// -/// let mut builder = HttpResponse::Ok(); -/// builder.set(IfRange::EntityTag(EntityTag::new( -/// false, -/// "xyzzy".to_owned(), -/// ))); -/// ``` -/// -/// ```rust -/// use actix_http::HttpResponse; -/// use actix_http::http::header::IfRange; -/// use std::time::{Duration, SystemTime}; -/// -/// let mut builder = HttpResponse::Ok(); -/// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24); -/// builder.set(IfRange::Date(fetched.into())); -/// ``` -#[derive(Clone, Debug, PartialEq)] -pub enum IfRange { - /// The entity-tag the client has of the resource - EntityTag(EntityTag), - /// The date when the client retrieved the resource - Date(HttpDate), -} - -impl Header for IfRange { - fn name() -> HeaderName { - header::IF_RANGE - } - #[inline] - fn parse(msg: &T) -> Result - where - T: HttpMessage, - { - let etag: Result = - from_one_raw_str(msg.headers().get(header::IF_RANGE)); - if let Ok(etag) = etag { - return Ok(IfRange::EntityTag(etag)); - } - let date: Result = - from_one_raw_str(msg.headers().get(header::IF_RANGE)); - if let Ok(date) = date { - return Ok(IfRange::Date(date)); - } - Err(ParseError::Header) - } -} - -impl Display for IfRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - IfRange::EntityTag(ref x) => Display::fmt(x, f), - IfRange::Date(ref x) => Display::fmt(x, f), - } - } -} - -impl IntoHeaderValue for IfRange { - type Error = InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut writer = Writer::new(); - let _ = write!(&mut writer, "{}", self); - HeaderValue::from_shared(writer.take()) - } -} - -#[cfg(test)] -mod test_if_range { - use super::IfRange as HeaderField; - use header::*; - use std::str; - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); - test_header!(test2, vec![b"\"xyzzy\""]); - test_header!(test3, vec![b"this-is-invalid"], None::); -} diff --git a/src/header/common/if_unmodified_since.rs b/src/header/common/if_unmodified_since.rs deleted file mode 100644 index 02f9252e2..000000000 --- a/src/header/common/if_unmodified_since.rs +++ /dev/null @@ -1,40 +0,0 @@ -use header::{HttpDate, IF_UNMODIFIED_SINCE}; - -header! { - /// `If-Unmodified-Since` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.4) - /// - /// The `If-Unmodified-Since` header field makes the request method - /// conditional on the selected representation's last modification date - /// being earlier than or equal to the date provided in the field-value. - /// This field accomplishes the same purpose as If-Match for cases where - /// the user agent does not have an entity-tag for the representation. - /// - /// # ABNF - /// - /// ```text - /// If-Unmodified-Since = HTTP-date - /// ``` - /// - /// # Example values - /// - /// * `Sat, 29 Oct 1994 19:43:31 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_http::HttpResponse; - /// use actix_http::http::header::IfUnmodifiedSince; - /// use std::time::{SystemTime, Duration}; - /// - /// let mut builder = HttpResponse::Ok(); - /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); - /// builder.set(IfUnmodifiedSince(modified.into())); - /// ``` - (IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate] - - test_if_unmodified_since { - // Test case from RFC - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); - } -} diff --git a/src/header/common/last_modified.rs b/src/header/common/last_modified.rs deleted file mode 100644 index 608f43138..000000000 --- a/src/header/common/last_modified.rs +++ /dev/null @@ -1,38 +0,0 @@ -use header::{HttpDate, LAST_MODIFIED}; - -header! { - /// `Last-Modified` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2) - /// - /// The `Last-Modified` header field in a response provides a timestamp - /// indicating the date and time at which the origin server believes the - /// selected representation was last modified, as determined at the - /// conclusion of handling the request. - /// - /// # ABNF - /// - /// ```text - /// Expires = HTTP-date - /// ``` - /// - /// # Example values - /// - /// * `Sat, 29 Oct 1994 19:43:31 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_http::HttpResponse; - /// use actix_http::http::header::LastModified; - /// use std::time::{SystemTime, Duration}; - /// - /// let mut builder = HttpResponse::Ok(); - /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); - /// builder.set(LastModified(modified.into())); - /// ``` - (LastModified, LAST_MODIFIED) => [HttpDate] - - test_last_modified { - // Test case from RFC - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);} -} diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs deleted file mode 100644 index e6185b5a7..000000000 --- a/src/header/common/mod.rs +++ /dev/null @@ -1,350 +0,0 @@ -//! A Collection of Header implementations for common HTTP Headers. -//! -//! ## Mime -//! -//! Several header fields use MIME values for their contents. Keeping with the -//! strongly-typed theme, the [mime](https://docs.rs/mime) crate -//! is used, such as `ContentType(pub Mime)`. -#![cfg_attr(rustfmt, rustfmt_skip)] - -pub use self::accept_charset::AcceptCharset; -//pub use self::accept_encoding::AcceptEncoding; -pub use self::accept_language::AcceptLanguage; -pub use self::accept::Accept; -pub use self::allow::Allow; -pub use self::cache_control::{CacheControl, CacheDirective}; -pub use self::content_disposition::{ContentDisposition, DispositionType, DispositionParam}; -pub use self::content_language::ContentLanguage; -pub use self::content_range::{ContentRange, ContentRangeSpec}; -pub use self::content_type::ContentType; -pub use self::date::Date; -pub use self::etag::ETag; -pub use self::expires::Expires; -pub use self::if_match::IfMatch; -pub use self::if_modified_since::IfModifiedSince; -pub use self::if_none_match::IfNoneMatch; -pub use self::if_range::IfRange; -pub use self::if_unmodified_since::IfUnmodifiedSince; -pub use self::last_modified::LastModified; -//pub use self::range::{Range, ByteRangeSpec}; - -#[doc(hidden)] -#[macro_export] -macro_rules! __hyper__deref { - ($from:ty => $to:ty) => { - impl ::std::ops::Deref for $from { - type Target = $to; - - #[inline] - fn deref(&self) -> &$to { - &self.0 - } - } - - impl ::std::ops::DerefMut for $from { - #[inline] - fn deref_mut(&mut self) -> &mut $to { - &mut self.0 - } - } - } -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __hyper__tm { - ($id:ident, $tm:ident{$($tf:item)*}) => { - #[allow(unused_imports)] - #[cfg(test)] - mod $tm{ - use std::str; - use http::Method; - use $crate::header::*; - use $crate::mime::*; - use super::$id as HeaderField; - $($tf)* - } - - } -} - -#[doc(hidden)] -#[macro_export] -macro_rules! test_header { - ($id:ident, $raw:expr) => { - #[test] - fn $id() { - use test; - let raw = $raw; - let a: Vec> = raw.iter().map(|x| x.to_vec()).collect(); - let mut req = test::TestRequest::default(); - for item in a { - req = req.header(HeaderField::name(), item); - } - let req = req.finish(); - let value = HeaderField::parse(&req); - let result = format!("{}", value.unwrap()); - let expected = String::from_utf8(raw[0].to_vec()).unwrap(); - let result_cmp: Vec = result - .to_ascii_lowercase() - .split(' ') - .map(|x| x.to_owned()) - .collect(); - let expected_cmp: Vec = expected - .to_ascii_lowercase() - .split(' ') - .map(|x| x.to_owned()) - .collect(); - assert_eq!(result_cmp.concat(), expected_cmp.concat()); - } - }; - ($id:ident, $raw:expr, $typed:expr) => { - #[test] - fn $id() { - use $crate::test; - let a: Vec> = $raw.iter().map(|x| x.to_vec()).collect(); - let mut req = test::TestRequest::default(); - for item in a { - req = req.header(HeaderField::name(), item); - } - let req = req.finish(); - let val = HeaderField::parse(&req); - let typed: Option = $typed; - // Test parsing - assert_eq!(val.ok(), typed); - // Test formatting - if typed.is_some() { - let raw = &($raw)[..]; - let mut iter = raw.iter().map(|b|str::from_utf8(&b[..]).unwrap()); - let mut joined = String::new(); - joined.push_str(iter.next().unwrap()); - for s in iter { - joined.push_str(", "); - joined.push_str(s); - } - assert_eq!(format!("{}", typed.unwrap()), joined); - } - } - } -} - -#[macro_export] -macro_rules! header { - // $a:meta: Attributes associated with the header item (usually docs) - // $id:ident: Identifier of the header - // $n:expr: Lowercase name of the header - // $nn:expr: Nice name of the header - - // List header, zero or more items - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)*) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub struct $id(pub Vec<$item>); - __hyper__deref!($id => Vec<$item>); - impl $crate::header::Header for $id { - #[inline] - fn name() -> $crate::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::header::from_comma_delimited( - msg.headers().get_all(Self::name())).map($id) - } - } - impl ::std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - $crate::header::fmt_comma_delimited(f, &self.0[..]) - } - } - impl $crate::header::IntoHeaderValue for $id { - type Error = $crate::header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> { - use std::fmt::Write; - let mut writer = $crate::header::Writer::new(); - let _ = write!(&mut writer, "{}", self); - $crate::header::HeaderValue::from_shared(writer.take()) - } - } - }; - // List header, one or more items - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)+) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub struct $id(pub Vec<$item>); - __hyper__deref!($id => Vec<$item>); - impl $crate::header::Header for $id { - #[inline] - fn name() -> $crate::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::header::from_comma_delimited( - msg.headers().get_all(Self::name())).map($id) - } - } - impl ::std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - $crate::header::fmt_comma_delimited(f, &self.0[..]) - } - } - impl $crate::header::IntoHeaderValue for $id { - type Error = $crate::header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> { - use std::fmt::Write; - let mut writer = $crate::header::Writer::new(); - let _ = write!(&mut writer, "{}", self); - $crate::header::HeaderValue::from_shared(writer.take()) - } - } - }; - // Single value header - ($(#[$a:meta])*($id:ident, $name:expr) => [$value:ty]) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub struct $id(pub $value); - __hyper__deref!($id => $value); - impl $crate::header::Header for $id { - #[inline] - fn name() -> $crate::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::header::from_one_raw_str( - msg.headers().get(Self::name())).map($id) - } - } - impl ::std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - ::std::fmt::Display::fmt(&self.0, f) - } - } - impl $crate::header::IntoHeaderValue for $id { - type Error = $crate::header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> { - self.0.try_into() - } - } - }; - // List header, one or more items with "*" option - ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub enum $id { - /// Any value is a match - Any, - /// Only the listed items are a match - Items(Vec<$item>), - } - impl $crate::header::Header for $id { - #[inline] - fn name() -> $crate::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - let any = msg.headers().get(Self::name()).and_then(|hdr| { - hdr.to_str().ok().and_then(|hdr| Some(hdr.trim() == "*"))}); - - if let Some(true) = any { - Ok($id::Any) - } else { - Ok($id::Items( - $crate::header::from_comma_delimited( - msg.headers().get_all(Self::name()))?)) - } - } - } - impl ::std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - match *self { - $id::Any => f.write_str("*"), - $id::Items(ref fields) => $crate::header::fmt_comma_delimited( - f, &fields[..]) - } - } - } - impl $crate::header::IntoHeaderValue for $id { - type Error = $crate::header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> { - use std::fmt::Write; - let mut writer = $crate::header::Writer::new(); - let _ = write!(&mut writer, "{}", self); - $crate::header::HeaderValue::from_shared(writer.take()) - } - } - }; - - // optional test module - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* - ($id, $name) => ($item)* - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; - ($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* - ($id, $n) => ($item)+ - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; - ($(#[$a:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* ($id, $name) => [$item] - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; - ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* - ($id, $name) => {Any / ($item)+} - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; -} - - -mod accept_charset; -//mod accept_encoding; -mod accept_language; -mod accept; -mod allow; -mod cache_control; -mod content_disposition; -mod content_language; -mod content_range; -mod content_type; -mod date; -mod etag; -mod expires; -mod if_match; -mod if_modified_since; -mod if_none_match; -mod if_range; -mod if_unmodified_since; -mod last_modified; -//mod range; diff --git a/src/header/common/range.rs b/src/header/common/range.rs deleted file mode 100644 index 71718fc7a..000000000 --- a/src/header/common/range.rs +++ /dev/null @@ -1,434 +0,0 @@ -use std::fmt::{self, Display}; -use std::str::FromStr; - -use header::parsing::from_one_raw_str; -use header::{Header, Raw}; - -/// `Range` header, defined in [RFC7233](https://tools.ietf.org/html/rfc7233#section-3.1) -/// -/// The "Range" header field on a GET request modifies the method -/// semantics to request transfer of only one or more subranges of the -/// selected representation data, rather than the entire selected -/// representation data. -/// -/// # ABNF -/// -/// ```text -/// Range = byte-ranges-specifier / other-ranges-specifier -/// other-ranges-specifier = other-range-unit "=" other-range-set -/// other-range-set = 1*VCHAR -/// -/// bytes-unit = "bytes" -/// -/// byte-ranges-specifier = bytes-unit "=" byte-range-set -/// byte-range-set = 1#(byte-range-spec / suffix-byte-range-spec) -/// byte-range-spec = first-byte-pos "-" [last-byte-pos] -/// first-byte-pos = 1*DIGIT -/// last-byte-pos = 1*DIGIT -/// ``` -/// -/// # Example values -/// -/// * `bytes=1000-` -/// * `bytes=-2000` -/// * `bytes=0-1,30-40` -/// * `bytes=0-10,20-90,-100` -/// * `custom_unit=0-123` -/// * `custom_unit=xxx-yyy` -/// -/// # Examples -/// -/// ``` -/// use hyper::header::{Headers, Range, ByteRangeSpec}; -/// -/// let mut headers = Headers::new(); -/// headers.set(Range::Bytes( -/// vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::AllFrom(200)] -/// )); -/// -/// headers.clear(); -/// headers.set(Range::Unregistered("letters".to_owned(), "a-f".to_owned())); -/// ``` -/// -/// ``` -/// use hyper::header::{Headers, Range}; -/// -/// let mut headers = Headers::new(); -/// headers.set(Range::bytes(1, 100)); -/// -/// headers.clear(); -/// headers.set(Range::bytes_multi(vec![(1, 100), (200, 300)])); -/// ``` -#[derive(PartialEq, Clone, Debug)] -pub enum Range { - /// Byte range - Bytes(Vec), - /// Custom range, with unit not registered at IANA - /// (`other-range-unit`: String , `other-range-set`: String) - Unregistered(String, String), -} - -/// Each `Range::Bytes` header can contain one or more `ByteRangeSpecs`. -/// Each `ByteRangeSpec` defines a range of bytes to fetch -#[derive(PartialEq, Clone, Debug)] -pub enum ByteRangeSpec { - /// Get all bytes between x and y ("x-y") - FromTo(u64, u64), - /// Get all bytes starting from x ("x-") - AllFrom(u64), - /// Get last x bytes ("-x") - Last(u64), -} - -impl ByteRangeSpec { - /// Given the full length of the entity, attempt to normalize the byte range - /// into an satisfiable end-inclusive (from, to) range. - /// - /// The resulting range is guaranteed to be a satisfiable range within the - /// bounds of `0 <= from <= to < full_length`. - /// - /// If the byte range is deemed unsatisfiable, `None` is returned. - /// An unsatisfiable range is generally cause for a server to either reject - /// the client request with a `416 Range Not Satisfiable` status code, or to - /// simply ignore the range header and serve the full entity using a `200 - /// OK` status code. - /// - /// This function closely follows [RFC 7233][1] section 2.1. - /// As such, it considers ranges to be satisfiable if they meet the - /// following conditions: - /// - /// > If a valid byte-range-set includes at least one byte-range-spec with - /// a first-byte-pos that is less than the current length of the - /// representation, or at least one suffix-byte-range-spec with a - /// non-zero suffix-length, then the byte-range-set is satisfiable. - /// Otherwise, the byte-range-set is unsatisfiable. - /// - /// The function also computes remainder ranges based on the RFC: - /// - /// > If the last-byte-pos value is - /// absent, or if the value is greater than or equal to the current - /// length of the representation data, the byte range is interpreted as - /// the remainder of the representation (i.e., the server replaces the - /// value of last-byte-pos with a value that is one less than the current - /// length of the selected representation). - /// - /// [1]: https://tools.ietf.org/html/rfc7233 - pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> { - // If the full length is zero, there is no satisfiable end-inclusive range. - if full_length == 0 { - return None; - } - match self { - &ByteRangeSpec::FromTo(from, to) => { - if from < full_length && from <= to { - Some((from, ::std::cmp::min(to, full_length - 1))) - } else { - None - } - } - &ByteRangeSpec::AllFrom(from) => { - if from < full_length { - Some((from, full_length - 1)) - } else { - None - } - } - &ByteRangeSpec::Last(last) => { - if last > 0 { - // From the RFC: If the selected representation is shorter - // than the specified suffix-length, - // the entire representation is used. - if last > full_length { - Some((0, full_length - 1)) - } else { - Some((full_length - last, full_length - 1)) - } - } else { - None - } - } - } - } -} - -impl Range { - /// Get the most common byte range header ("bytes=from-to") - pub fn bytes(from: u64, to: u64) -> Range { - Range::Bytes(vec![ByteRangeSpec::FromTo(from, to)]) - } - - /// Get byte range header with multiple subranges - /// ("bytes=from1-to1,from2-to2,fromX-toX") - pub fn bytes_multi(ranges: Vec<(u64, u64)>) -> Range { - Range::Bytes( - ranges - .iter() - .map(|r| ByteRangeSpec::FromTo(r.0, r.1)) - .collect(), - ) - } -} - -impl fmt::Display for ByteRangeSpec { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ByteRangeSpec::FromTo(from, to) => write!(f, "{}-{}", from, to), - ByteRangeSpec::Last(pos) => write!(f, "-{}", pos), - ByteRangeSpec::AllFrom(pos) => write!(f, "{}-", pos), - } - } -} - -impl fmt::Display for Range { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Range::Bytes(ref ranges) => { - try!(write!(f, "bytes=")); - - for (i, range) in ranges.iter().enumerate() { - if i != 0 { - try!(f.write_str(",")); - } - try!(Display::fmt(range, f)); - } - Ok(()) - } - Range::Unregistered(ref unit, ref range_str) => { - write!(f, "{}={}", unit, range_str) - } - } - } -} - -impl FromStr for Range { - type Err = ::Error; - - fn from_str(s: &str) -> ::Result { - let mut iter = s.splitn(2, '='); - - match (iter.next(), iter.next()) { - (Some("bytes"), Some(ranges)) => { - let ranges = from_comma_delimited(ranges); - if ranges.is_empty() { - return Err(::Error::Header); - } - Ok(Range::Bytes(ranges)) - } - (Some(unit), Some(range_str)) if unit != "" && range_str != "" => Ok( - Range::Unregistered(unit.to_owned(), range_str.to_owned()), - ), - _ => Err(::Error::Header), - } - } -} - -impl FromStr for ByteRangeSpec { - type Err = ::Error; - - fn from_str(s: &str) -> ::Result { - let mut parts = s.splitn(2, '-'); - - match (parts.next(), parts.next()) { - (Some(""), Some(end)) => end.parse() - .or(Err(::Error::Header)) - .map(ByteRangeSpec::Last), - (Some(start), Some("")) => start - .parse() - .or(Err(::Error::Header)) - .map(ByteRangeSpec::AllFrom), - (Some(start), Some(end)) => match (start.parse(), end.parse()) { - (Ok(start), Ok(end)) if start <= end => { - Ok(ByteRangeSpec::FromTo(start, end)) - } - _ => Err(::Error::Header), - }, - _ => Err(::Error::Header), - } - } -} - -fn from_comma_delimited(s: &str) -> Vec { - s.split(',') - .filter_map(|x| match x.trim() { - "" => None, - y => Some(y), - }) - .filter_map(|x| x.parse().ok()) - .collect() -} - -impl Header for Range { - fn header_name() -> &'static str { - static NAME: &'static str = "Range"; - NAME - } - - fn parse_header(raw: &Raw) -> ::Result { - from_one_raw_str(raw) - } - - fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result { - f.fmt_line(self) - } -} - -#[test] -fn test_parse_bytes_range_valid() { - let r: Range = Header::parse_header(&"bytes=1-100".into()).unwrap(); - let r2: Range = Header::parse_header(&"bytes=1-100,-".into()).unwrap(); - let r3 = Range::bytes(1, 100); - assert_eq!(r, r2); - assert_eq!(r2, r3); - - let r: Range = Header::parse_header(&"bytes=1-100,200-".into()).unwrap(); - let r2: Range = - Header::parse_header(&"bytes= 1-100 , 101-xxx, 200- ".into()).unwrap(); - let r3 = Range::Bytes(vec![ - ByteRangeSpec::FromTo(1, 100), - ByteRangeSpec::AllFrom(200), - ]); - assert_eq!(r, r2); - assert_eq!(r2, r3); - - let r: Range = Header::parse_header(&"bytes=1-100,-100".into()).unwrap(); - let r2: Range = Header::parse_header(&"bytes=1-100, ,,-100".into()).unwrap(); - let r3 = Range::Bytes(vec![ - ByteRangeSpec::FromTo(1, 100), - ByteRangeSpec::Last(100), - ]); - assert_eq!(r, r2); - assert_eq!(r2, r3); - - let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); - assert_eq!(r, r2); -} - -#[test] -fn test_parse_unregistered_range_valid() { - let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); - assert_eq!(r, r2); - - let r: Range = Header::parse_header(&"custom=abcd".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "abcd".to_owned()); - assert_eq!(r, r2); - - let r: Range = Header::parse_header(&"custom=xxx-yyy".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "xxx-yyy".to_owned()); - assert_eq!(r, r2); -} - -#[test] -fn test_parse_invalid() { - let r: ::Result = Header::parse_header(&"bytes=1-a,-".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"bytes=1-2-3".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"abc".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"bytes=1-100=".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"bytes=".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"custom=".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"=1-100".into()); - assert_eq!(r.ok(), None); -} - -#[test] -fn test_fmt() { - use header::Headers; - - let mut headers = Headers::new(); - - headers.set(Range::Bytes(vec![ - ByteRangeSpec::FromTo(0, 1000), - ByteRangeSpec::AllFrom(2000), - ])); - assert_eq!(&headers.to_string(), "Range: bytes=0-1000,2000-\r\n"); - - headers.clear(); - headers.set(Range::Bytes(vec![])); - - assert_eq!(&headers.to_string(), "Range: bytes=\r\n"); - - headers.clear(); - headers.set(Range::Unregistered( - "custom".to_owned(), - "1-xxx".to_owned(), - )); - - assert_eq!(&headers.to_string(), "Range: custom=1-xxx\r\n"); -} - -#[test] -fn test_byte_range_spec_to_satisfiable_range() { - assert_eq!( - Some((0, 0)), - ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(3) - ); - assert_eq!( - Some((1, 2)), - ByteRangeSpec::FromTo(1, 2).to_satisfiable_range(3) - ); - assert_eq!( - Some((1, 2)), - ByteRangeSpec::FromTo(1, 5).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::FromTo(3, 3).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::FromTo(2, 1).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(0) - ); - - assert_eq!( - Some((0, 2)), - ByteRangeSpec::AllFrom(0).to_satisfiable_range(3) - ); - assert_eq!( - Some((2, 2)), - ByteRangeSpec::AllFrom(2).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::AllFrom(3).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::AllFrom(5).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::AllFrom(0).to_satisfiable_range(0) - ); - - assert_eq!( - Some((1, 2)), - ByteRangeSpec::Last(2).to_satisfiable_range(3) - ); - assert_eq!( - Some((2, 2)), - ByteRangeSpec::Last(1).to_satisfiable_range(3) - ); - assert_eq!( - Some((0, 2)), - ByteRangeSpec::Last(5).to_satisfiable_range(3) - ); - assert_eq!(None, ByteRangeSpec::Last(0).to_satisfiable_range(3)); - assert_eq!(None, ByteRangeSpec::Last(2).to_satisfiable_range(0)); -} diff --git a/src/header/mod.rs b/src/header/mod.rs deleted file mode 100644 index 74e4b03e5..000000000 --- a/src/header/mod.rs +++ /dev/null @@ -1,471 +0,0 @@ -//! Various http headers -// This is mostly copy of [hyper](https://github.com/hyperium/hyper/tree/master/src/header) - -use std::fmt; -use std::str::FromStr; - -use bytes::{Bytes, BytesMut}; -use mime::Mime; -use modhttp::header::GetAll; -use modhttp::Error as HttpError; -use percent_encoding; - -pub use modhttp::header::*; - -use error::ParseError; -use httpmessage::HttpMessage; - -mod common; -mod shared; -#[doc(hidden)] -pub use self::common::*; -#[doc(hidden)] -pub use self::shared::*; - -#[doc(hidden)] -/// A trait for any object that will represent a header field and value. -pub trait Header -where - Self: IntoHeaderValue, -{ - /// Returns the name of the header field - fn name() -> HeaderName; - - /// Parse a header - fn parse(msg: &T) -> Result; -} - -#[doc(hidden)] -/// A trait for any object that can be Converted to a `HeaderValue` -pub trait IntoHeaderValue: Sized { - /// The type returned in the event of a conversion error. - type Error: Into; - - /// Try to convert value to a Header value. - fn try_into(self) -> Result; -} - -impl IntoHeaderValue for HeaderValue { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - Ok(self) - } -} - -impl<'a> IntoHeaderValue for &'a str { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - self.parse() - } -} - -impl<'a> IntoHeaderValue for &'a [u8] { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_bytes(self) - } -} - -impl IntoHeaderValue for Bytes { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(self) - } -} - -impl IntoHeaderValue for Vec { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(Bytes::from(self)) - } -} - -impl IntoHeaderValue for String { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(Bytes::from(self)) - } -} - -impl IntoHeaderValue for Mime { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(Bytes::from(format!("{}", self))) - } -} - -/// Represents supported types of content encodings -#[derive(Copy, Clone, PartialEq, Debug)] -pub enum ContentEncoding { - /// Automatically select encoding based on encoding negotiation - Auto, - /// A format using the Brotli algorithm - #[cfg(feature = "brotli")] - Br, - /// A format using the zlib structure with deflate algorithm - #[cfg(feature = "flate2")] - Deflate, - /// Gzip algorithm - #[cfg(feature = "flate2")] - Gzip, - /// Indicates the identity function (i.e. no compression, nor modification) - Identity, -} - -impl ContentEncoding { - #[inline] - /// Is the content compressed? - pub fn is_compression(self) -> bool { - match self { - ContentEncoding::Identity | ContentEncoding::Auto => false, - _ => true, - } - } - - #[inline] - /// Convert content encoding to string - pub fn as_str(self) -> &'static str { - match self { - #[cfg(feature = "brotli")] - ContentEncoding::Br => "br", - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => "gzip", - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => "deflate", - ContentEncoding::Identity | ContentEncoding::Auto => "identity", - } - } - - #[inline] - /// default quality value - pub fn quality(self) -> f64 { - match self { - #[cfg(feature = "brotli")] - ContentEncoding::Br => 1.1, - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => 1.0, - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => 0.9, - ContentEncoding::Identity | ContentEncoding::Auto => 0.1, - } - } -} - -// TODO: remove memory allocation -impl<'a> From<&'a str> for ContentEncoding { - fn from(s: &'a str) -> ContentEncoding { - match AsRef::::as_ref(&s.trim().to_lowercase()) { - #[cfg(feature = "brotli")] - "br" => ContentEncoding::Br, - #[cfg(feature = "flate2")] - "gzip" => ContentEncoding::Gzip, - #[cfg(feature = "flate2")] - "deflate" => ContentEncoding::Deflate, - _ => ContentEncoding::Identity, - } - } -} - -#[doc(hidden)] -pub(crate) struct Writer { - buf: BytesMut, -} - -impl Writer { - fn new() -> Writer { - Writer { - buf: BytesMut::new(), - } - } - fn take(&mut self) -> Bytes { - self.buf.take().freeze() - } -} - -impl fmt::Write for Writer { - #[inline] - fn write_str(&mut self, s: &str) -> fmt::Result { - self.buf.extend_from_slice(s.as_bytes()); - Ok(()) - } - - #[inline] - fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result { - fmt::write(self, args) - } -} - -#[inline] -#[doc(hidden)] -/// Reads a comma-delimited raw header into a Vec. -pub fn from_comma_delimited( - all: GetAll, -) -> Result, ParseError> { - let mut result = Vec::new(); - for h in all { - let s = h.to_str().map_err(|_| ParseError::Header)?; - result.extend( - s.split(',') - .filter_map(|x| match x.trim() { - "" => None, - y => Some(y), - }).filter_map(|x| x.trim().parse().ok()), - ) - } - Ok(result) -} - -#[inline] -#[doc(hidden)] -/// Reads a single string when parsing a header. -pub fn from_one_raw_str(val: Option<&HeaderValue>) -> Result { - if let Some(line) = val { - let line = line.to_str().map_err(|_| ParseError::Header)?; - if !line.is_empty() { - return T::from_str(line).or(Err(ParseError::Header)); - } - } - Err(ParseError::Header) -} - -#[inline] -#[doc(hidden)] -/// Format an array into a comma-delimited string. -pub fn fmt_comma_delimited(f: &mut fmt::Formatter, parts: &[T]) -> fmt::Result -where - T: fmt::Display, -{ - let mut iter = parts.iter(); - if let Some(part) = iter.next() { - fmt::Display::fmt(part, f)?; - } - for part in iter { - f.write_str(", ")?; - fmt::Display::fmt(part, f)?; - } - Ok(()) -} - -// From hyper v0.11.27 src/header/parsing.rs - -/// The value part of an extended parameter consisting of three parts: -/// the REQUIRED character set name (`charset`), the OPTIONAL language information (`language_tag`), -/// and a character sequence representing the actual value (`value`), separated by single quote -/// characters. It is defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). -#[derive(Clone, Debug, PartialEq)] -pub struct ExtendedValue { - /// The character set that is used to encode the `value` to a string. - pub charset: Charset, - /// The human language details of the `value`, if available. - pub language_tag: Option, - /// The parameter value, as expressed in octets. - pub value: Vec, -} - -/// Parses extended header parameter values (`ext-value`), as defined in -/// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). -/// -/// Extended values are denoted by parameter names that end with `*`. -/// -/// ## ABNF -/// -/// ```text -/// ext-value = charset "'" [ language ] "'" value-chars -/// ; like RFC 2231's -/// ; (see [RFC2231], Section 7) -/// -/// charset = "UTF-8" / "ISO-8859-1" / mime-charset -/// -/// mime-charset = 1*mime-charsetc -/// mime-charsetc = ALPHA / DIGIT -/// / "!" / "#" / "$" / "%" / "&" -/// / "+" / "-" / "^" / "_" / "`" -/// / "{" / "}" / "~" -/// ; as in Section 2.3 of [RFC2978] -/// ; except that the single quote is not included -/// ; SHOULD be registered in the IANA charset registry -/// -/// language = -/// -/// value-chars = *( pct-encoded / attr-char ) -/// -/// pct-encoded = "%" HEXDIG HEXDIG -/// ; see [RFC3986], Section 2.1 -/// -/// attr-char = ALPHA / DIGIT -/// / "!" / "#" / "$" / "&" / "+" / "-" / "." -/// / "^" / "_" / "`" / "|" / "~" -/// ; token except ( "*" / "'" / "%" ) -/// ``` -pub fn parse_extended_value(val: &str) -> Result { - // Break into three pieces separated by the single-quote character - let mut parts = val.splitn(3, '\''); - - // Interpret the first piece as a Charset - let charset: Charset = match parts.next() { - None => return Err(::error::ParseError::Header), - Some(n) => FromStr::from_str(n).map_err(|_| ::error::ParseError::Header)?, - }; - - // Interpret the second piece as a language tag - let language_tag: Option = match parts.next() { - None => return Err(::error::ParseError::Header), - Some("") => None, - Some(s) => match s.parse() { - Ok(lt) => Some(lt), - Err(_) => return Err(::error::ParseError::Header), - }, - }; - - // Interpret the third piece as a sequence of value characters - let value: Vec = match parts.next() { - None => return Err(::error::ParseError::Header), - Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(), - }; - - Ok(ExtendedValue { - value, - charset, - language_tag, - }) -} - -impl fmt::Display for ExtendedValue { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let encoded_value = percent_encoding::percent_encode( - &self.value[..], - self::percent_encoding_http::HTTP_VALUE, - ); - if let Some(ref lang) = self.language_tag { - write!(f, "{}'{}'{}", self.charset, lang, encoded_value) - } else { - write!(f, "{}''{}", self.charset, encoded_value) - } - } -} - -/// Percent encode a sequence of bytes with a character set defined in -/// [https://tools.ietf.org/html/rfc5987#section-3.2][url] -/// -/// [url]: https://tools.ietf.org/html/rfc5987#section-3.2 -pub fn http_percent_encode(f: &mut fmt::Formatter, bytes: &[u8]) -> fmt::Result { - let encoded = - percent_encoding::percent_encode(bytes, self::percent_encoding_http::HTTP_VALUE); - fmt::Display::fmt(&encoded, f) -} -mod percent_encoding_http { - use percent_encoding; - - // internal module because macro is hard-coded to make a public item - // but we don't want to public export this item - define_encode_set! { - // This encode set is used for HTTP header values and is defined at - // https://tools.ietf.org/html/rfc5987#section-3.2 - pub HTTP_VALUE = [percent_encoding::SIMPLE_ENCODE_SET] | { - ' ', '"', '%', '\'', '(', ')', '*', ',', '/', ':', ';', '<', '-', '>', '?', - '[', '\\', ']', '{', '}' - } - } -} - -#[cfg(test)] -mod tests { - use super::{parse_extended_value, ExtendedValue}; - use header::shared::Charset; - use language_tags::LanguageTag; - - #[test] - fn test_parse_extended_value_with_encoding_and_language_tag() { - let expected_language_tag = "en".parse::().unwrap(); - // RFC 5987, Section 3.2.2 - // Extended notation, using the Unicode character U+00A3 (POUND SIGN) - let result = parse_extended_value("iso-8859-1'en'%A3%20rates"); - assert!(result.is_ok()); - let extended_value = result.unwrap(); - assert_eq!(Charset::Iso_8859_1, extended_value.charset); - assert!(extended_value.language_tag.is_some()); - assert_eq!(expected_language_tag, extended_value.language_tag.unwrap()); - assert_eq!( - vec![163, b' ', b'r', b'a', b't', b'e', b's'], - extended_value.value - ); - } - - #[test] - fn test_parse_extended_value_with_encoding() { - // RFC 5987, Section 3.2.2 - // Extended notation, using the Unicode characters U+00A3 (POUND SIGN) - // and U+20AC (EURO SIGN) - let result = parse_extended_value("UTF-8''%c2%a3%20and%20%e2%82%ac%20rates"); - assert!(result.is_ok()); - let extended_value = result.unwrap(); - assert_eq!(Charset::Ext("UTF-8".to_string()), extended_value.charset); - assert!(extended_value.language_tag.is_none()); - assert_eq!( - vec![ - 194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', - b't', b'e', b's', - ], - extended_value.value - ); - } - - #[test] - fn test_parse_extended_value_missing_language_tag_and_encoding() { - // From: https://greenbytes.de/tech/tc2231/#attwithfn2231quot2 - let result = parse_extended_value("foo%20bar.html"); - assert!(result.is_err()); - } - - #[test] - fn test_parse_extended_value_partially_formatted() { - let result = parse_extended_value("UTF-8'missing third part"); - assert!(result.is_err()); - } - - #[test] - fn test_parse_extended_value_partially_formatted_blank() { - let result = parse_extended_value("blank second part'"); - assert!(result.is_err()); - } - - #[test] - fn test_fmt_extended_value_with_encoding_and_language_tag() { - let extended_value = ExtendedValue { - charset: Charset::Iso_8859_1, - language_tag: Some("en".parse().expect("Could not parse language tag")), - value: vec![163, b' ', b'r', b'a', b't', b'e', b's'], - }; - assert_eq!("ISO-8859-1'en'%A3%20rates", format!("{}", extended_value)); - } - - #[test] - fn test_fmt_extended_value_with_encoding() { - let extended_value = ExtendedValue { - charset: Charset::Ext("UTF-8".to_string()), - language_tag: None, - value: vec![ - 194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', - b't', b'e', b's', - ], - }; - assert_eq!( - "UTF-8''%C2%A3%20and%20%E2%82%AC%20rates", - format!("{}", extended_value) - ); - } -} diff --git a/src/header/shared/charset.rs b/src/header/shared/charset.rs deleted file mode 100644 index b679971b0..000000000 --- a/src/header/shared/charset.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::fmt::{self, Display}; -use std::str::FromStr; - -use self::Charset::*; - -/// A Mime charset. -/// -/// The string representation is normalized to upper case. -/// -/// See [http://www.iana.org/assignments/character-sets/character-sets.xhtml][url]. -/// -/// [url]: http://www.iana.org/assignments/character-sets/character-sets.xhtml -#[derive(Clone, Debug, PartialEq)] -#[allow(non_camel_case_types)] -pub enum Charset { - /// US ASCII - Us_Ascii, - /// ISO-8859-1 - Iso_8859_1, - /// ISO-8859-2 - Iso_8859_2, - /// ISO-8859-3 - Iso_8859_3, - /// ISO-8859-4 - Iso_8859_4, - /// ISO-8859-5 - Iso_8859_5, - /// ISO-8859-6 - Iso_8859_6, - /// ISO-8859-7 - Iso_8859_7, - /// ISO-8859-8 - Iso_8859_8, - /// ISO-8859-9 - Iso_8859_9, - /// ISO-8859-10 - Iso_8859_10, - /// Shift_JIS - Shift_Jis, - /// EUC-JP - Euc_Jp, - /// ISO-2022-KR - Iso_2022_Kr, - /// EUC-KR - Euc_Kr, - /// ISO-2022-JP - Iso_2022_Jp, - /// ISO-2022-JP-2 - Iso_2022_Jp_2, - /// ISO-8859-6-E - Iso_8859_6_E, - /// ISO-8859-6-I - Iso_8859_6_I, - /// ISO-8859-8-E - Iso_8859_8_E, - /// ISO-8859-8-I - Iso_8859_8_I, - /// GB2312 - Gb2312, - /// Big5 - Big5, - /// KOI8-R - Koi8_R, - /// An arbitrary charset specified as a string - Ext(String), -} - -impl Charset { - fn label(&self) -> &str { - match *self { - Us_Ascii => "US-ASCII", - Iso_8859_1 => "ISO-8859-1", - Iso_8859_2 => "ISO-8859-2", - Iso_8859_3 => "ISO-8859-3", - Iso_8859_4 => "ISO-8859-4", - Iso_8859_5 => "ISO-8859-5", - Iso_8859_6 => "ISO-8859-6", - Iso_8859_7 => "ISO-8859-7", - Iso_8859_8 => "ISO-8859-8", - Iso_8859_9 => "ISO-8859-9", - Iso_8859_10 => "ISO-8859-10", - Shift_Jis => "Shift-JIS", - Euc_Jp => "EUC-JP", - Iso_2022_Kr => "ISO-2022-KR", - Euc_Kr => "EUC-KR", - Iso_2022_Jp => "ISO-2022-JP", - Iso_2022_Jp_2 => "ISO-2022-JP-2", - Iso_8859_6_E => "ISO-8859-6-E", - Iso_8859_6_I => "ISO-8859-6-I", - Iso_8859_8_E => "ISO-8859-8-E", - Iso_8859_8_I => "ISO-8859-8-I", - Gb2312 => "GB2312", - Big5 => "big5", - Koi8_R => "KOI8-R", - Ext(ref s) => s, - } - } -} - -impl Display for Charset { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(self.label()) - } -} - -impl FromStr for Charset { - type Err = ::Error; - fn from_str(s: &str) -> ::Result { - Ok(match s.to_ascii_uppercase().as_ref() { - "US-ASCII" => Us_Ascii, - "ISO-8859-1" => Iso_8859_1, - "ISO-8859-2" => Iso_8859_2, - "ISO-8859-3" => Iso_8859_3, - "ISO-8859-4" => Iso_8859_4, - "ISO-8859-5" => Iso_8859_5, - "ISO-8859-6" => Iso_8859_6, - "ISO-8859-7" => Iso_8859_7, - "ISO-8859-8" => Iso_8859_8, - "ISO-8859-9" => Iso_8859_9, - "ISO-8859-10" => Iso_8859_10, - "SHIFT-JIS" => Shift_Jis, - "EUC-JP" => Euc_Jp, - "ISO-2022-KR" => Iso_2022_Kr, - "EUC-KR" => Euc_Kr, - "ISO-2022-JP" => Iso_2022_Jp, - "ISO-2022-JP-2" => Iso_2022_Jp_2, - "ISO-8859-6-E" => Iso_8859_6_E, - "ISO-8859-6-I" => Iso_8859_6_I, - "ISO-8859-8-E" => Iso_8859_8_E, - "ISO-8859-8-I" => Iso_8859_8_I, - "GB2312" => Gb2312, - "big5" => Big5, - "KOI8-R" => Koi8_R, - s => Ext(s.to_owned()), - }) - } -} - -#[test] -fn test_parse() { - assert_eq!(Us_Ascii, "us-ascii".parse().unwrap()); - assert_eq!(Us_Ascii, "US-Ascii".parse().unwrap()); - assert_eq!(Us_Ascii, "US-ASCII".parse().unwrap()); - assert_eq!(Shift_Jis, "Shift-JIS".parse().unwrap()); - assert_eq!(Ext("ABCD".to_owned()), "abcd".parse().unwrap()); -} - -#[test] -fn test_display() { - assert_eq!("US-ASCII", format!("{}", Us_Ascii)); - assert_eq!("ABCD", format!("{}", Ext("ABCD".to_owned()))); -} diff --git a/src/header/shared/encoding.rs b/src/header/shared/encoding.rs deleted file mode 100644 index 64027d8a5..000000000 --- a/src/header/shared/encoding.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::fmt; -use std::str; - -pub use self::Encoding::{ - Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, -}; - -/// A value to represent an encoding used in `Transfer-Encoding` -/// or `Accept-Encoding` header. -#[derive(Clone, PartialEq, Debug)] -pub enum Encoding { - /// The `chunked` encoding. - Chunked, - /// The `br` encoding. - Brotli, - /// The `gzip` encoding. - Gzip, - /// The `deflate` encoding. - Deflate, - /// The `compress` encoding. - Compress, - /// The `identity` encoding. - Identity, - /// The `trailers` encoding. - Trailers, - /// Some other encoding that is less common, can be any String. - EncodingExt(String), -} - -impl fmt::Display for Encoding { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(match *self { - Chunked => "chunked", - Brotli => "br", - Gzip => "gzip", - Deflate => "deflate", - Compress => "compress", - Identity => "identity", - Trailers => "trailers", - EncodingExt(ref s) => s.as_ref(), - }) - } -} - -impl str::FromStr for Encoding { - type Err = ::error::ParseError; - fn from_str(s: &str) -> Result { - match s { - "chunked" => Ok(Chunked), - "br" => Ok(Brotli), - "deflate" => Ok(Deflate), - "gzip" => Ok(Gzip), - "compress" => Ok(Compress), - "identity" => Ok(Identity), - "trailers" => Ok(Trailers), - _ => Ok(EncodingExt(s.to_owned())), - } - } -} diff --git a/src/header/shared/entity.rs b/src/header/shared/entity.rs deleted file mode 100644 index 0d3b0a4ef..000000000 --- a/src/header/shared/entity.rs +++ /dev/null @@ -1,266 +0,0 @@ -use header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, Writer}; -use std::fmt::{self, Display, Write}; -use std::str::FromStr; - -/// check that each char in the slice is either: -/// 1. `%x21`, or -/// 2. in the range `%x23` to `%x7E`, or -/// 3. above `%x80` -fn check_slice_validity(slice: &str) -> bool { - slice - .bytes() - .all(|c| c == b'\x21' || (c >= b'\x23' && c <= b'\x7e') | (c >= b'\x80')) -} - -/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3) -/// -/// An entity tag consists of a string enclosed by two literal double quotes. -/// Preceding the first double quote is an optional weakness indicator, -/// which always looks like `W/`. Examples for valid tags are `"xyzzy"` and -/// `W/"xyzzy"`. -/// -/// # ABNF -/// -/// ```text -/// entity-tag = [ weak ] opaque-tag -/// weak = %x57.2F ; "W/", case-sensitive -/// opaque-tag = DQUOTE *etagc DQUOTE -/// etagc = %x21 / %x23-7E / obs-text -/// ; VCHAR except double quotes, plus obs-text -/// ``` -/// -/// # Comparison -/// To check if two entity tags are equivalent in an application always use the -/// `strong_eq` or `weak_eq` methods based on the context of the Tag. Only use -/// `==` to check if two tags are identical. -/// -/// The example below shows the results for a set of entity-tag pairs and -/// both the weak and strong comparison function results: -/// -/// | `ETag 1`| `ETag 2`| Strong Comparison | Weak Comparison | -/// |---------|---------|-------------------|-----------------| -/// | `W/"1"` | `W/"1"` | no match | match | -/// | `W/"1"` | `W/"2"` | no match | no match | -/// | `W/"1"` | `"1"` | no match | match | -/// | `"1"` | `"1"` | match | match | -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct EntityTag { - /// Weakness indicator for the tag - pub weak: bool, - /// The opaque string in between the DQUOTEs - tag: String, -} - -impl EntityTag { - /// Constructs a new EntityTag. - /// # Panics - /// If the tag contains invalid characters. - pub fn new(weak: bool, tag: String) -> EntityTag { - assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); - EntityTag { weak, tag } - } - - /// Constructs a new weak EntityTag. - /// # Panics - /// If the tag contains invalid characters. - pub fn weak(tag: String) -> EntityTag { - EntityTag::new(true, tag) - } - - /// Constructs a new strong EntityTag. - /// # Panics - /// If the tag contains invalid characters. - pub fn strong(tag: String) -> EntityTag { - EntityTag::new(false, tag) - } - - /// Get the tag. - pub fn tag(&self) -> &str { - self.tag.as_ref() - } - - /// Set the tag. - /// # Panics - /// If the tag contains invalid characters. - pub fn set_tag(&mut self, tag: String) { - assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); - self.tag = tag - } - - /// For strong comparison two entity-tags are equivalent if both are not - /// weak and their opaque-tags match character-by-character. - pub fn strong_eq(&self, other: &EntityTag) -> bool { - !self.weak && !other.weak && self.tag == other.tag - } - - /// For weak comparison two entity-tags are equivalent if their - /// opaque-tags match character-by-character, regardless of either or - /// both being tagged as "weak". - pub fn weak_eq(&self, other: &EntityTag) -> bool { - self.tag == other.tag - } - - /// The inverse of `EntityTag.strong_eq()`. - pub fn strong_ne(&self, other: &EntityTag) -> bool { - !self.strong_eq(other) - } - - /// The inverse of `EntityTag.weak_eq()`. - pub fn weak_ne(&self, other: &EntityTag) -> bool { - !self.weak_eq(other) - } -} - -impl Display for EntityTag { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.weak { - write!(f, "W/\"{}\"", self.tag) - } else { - write!(f, "\"{}\"", self.tag) - } - } -} - -impl FromStr for EntityTag { - type Err = ::error::ParseError; - - fn from_str(s: &str) -> Result { - let length: usize = s.len(); - let slice = &s[..]; - // Early exits if it doesn't terminate in a DQUOTE. - if !slice.ends_with('"') || slice.len() < 2 { - return Err(::error::ParseError::Header); - } - // The etag is weak if its first char is not a DQUOTE. - if slice.len() >= 2 - && slice.starts_with('"') - && check_slice_validity(&slice[1..length - 1]) - { - // No need to check if the last char is a DQUOTE, - // we already did that above. - return Ok(EntityTag { - weak: false, - tag: slice[1..length - 1].to_owned(), - }); - } else if slice.len() >= 4 - && slice.starts_with("W/\"") - && check_slice_validity(&slice[3..length - 1]) - { - return Ok(EntityTag { - weak: true, - tag: slice[3..length - 1].to_owned(), - }); - } - Err(::error::ParseError::Header) - } -} - -impl IntoHeaderValue for EntityTag { - type Error = InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut wrt = Writer::new(); - write!(wrt, "{}", self).unwrap(); - HeaderValue::from_shared(wrt.take()) - } -} - -#[cfg(test)] -mod tests { - use super::EntityTag; - - #[test] - fn test_etag_parse_success() { - // Expected success - assert_eq!( - "\"foobar\"".parse::().unwrap(), - EntityTag::strong("foobar".to_owned()) - ); - assert_eq!( - "\"\"".parse::().unwrap(), - EntityTag::strong("".to_owned()) - ); - assert_eq!( - "W/\"weaktag\"".parse::().unwrap(), - EntityTag::weak("weaktag".to_owned()) - ); - assert_eq!( - "W/\"\x65\x62\"".parse::().unwrap(), - EntityTag::weak("\x65\x62".to_owned()) - ); - assert_eq!( - "W/\"\"".parse::().unwrap(), - EntityTag::weak("".to_owned()) - ); - } - - #[test] - fn test_etag_parse_failures() { - // Expected failures - assert!("no-dquotes".parse::().is_err()); - assert!( - "w/\"the-first-w-is-case-sensitive\"" - .parse::() - .is_err() - ); - assert!("".parse::().is_err()); - assert!("\"unmatched-dquotes1".parse::().is_err()); - assert!("unmatched-dquotes2\"".parse::().is_err()); - assert!("matched-\"dquotes\"".parse::().is_err()); - } - - #[test] - fn test_etag_fmt() { - assert_eq!( - format!("{}", EntityTag::strong("foobar".to_owned())), - "\"foobar\"" - ); - assert_eq!(format!("{}", EntityTag::strong("".to_owned())), "\"\""); - assert_eq!( - format!("{}", EntityTag::weak("weak-etag".to_owned())), - "W/\"weak-etag\"" - ); - assert_eq!( - format!("{}", EntityTag::weak("\u{0065}".to_owned())), - "W/\"\x65\"" - ); - assert_eq!(format!("{}", EntityTag::weak("".to_owned())), "W/\"\""); - } - - #[test] - fn test_cmp() { - // | ETag 1 | ETag 2 | Strong Comparison | Weak Comparison | - // |---------|---------|-------------------|-----------------| - // | `W/"1"` | `W/"1"` | no match | match | - // | `W/"1"` | `W/"2"` | no match | no match | - // | `W/"1"` | `"1"` | no match | match | - // | `"1"` | `"1"` | match | match | - let mut etag1 = EntityTag::weak("1".to_owned()); - let mut etag2 = EntityTag::weak("1".to_owned()); - assert!(!etag1.strong_eq(&etag2)); - assert!(etag1.weak_eq(&etag2)); - assert!(etag1.strong_ne(&etag2)); - assert!(!etag1.weak_ne(&etag2)); - - etag1 = EntityTag::weak("1".to_owned()); - etag2 = EntityTag::weak("2".to_owned()); - assert!(!etag1.strong_eq(&etag2)); - assert!(!etag1.weak_eq(&etag2)); - assert!(etag1.strong_ne(&etag2)); - assert!(etag1.weak_ne(&etag2)); - - etag1 = EntityTag::weak("1".to_owned()); - etag2 = EntityTag::strong("1".to_owned()); - assert!(!etag1.strong_eq(&etag2)); - assert!(etag1.weak_eq(&etag2)); - assert!(etag1.strong_ne(&etag2)); - assert!(!etag1.weak_ne(&etag2)); - - etag1 = EntityTag::strong("1".to_owned()); - etag2 = EntityTag::strong("1".to_owned()); - assert!(etag1.strong_eq(&etag2)); - assert!(etag1.weak_eq(&etag2)); - assert!(!etag1.strong_ne(&etag2)); - assert!(!etag1.weak_ne(&etag2)); - } -} diff --git a/src/header/shared/httpdate.rs b/src/header/shared/httpdate.rs deleted file mode 100644 index 7fd26b121..000000000 --- a/src/header/shared/httpdate.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::fmt::{self, Display}; -use std::io::Write; -use std::str::FromStr; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use bytes::{BufMut, BytesMut}; -use http::header::{HeaderValue, InvalidHeaderValueBytes}; -use time; - -use error::ParseError; -use header::IntoHeaderValue; - -/// A timestamp with HTTP formatting and parsing -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct HttpDate(time::Tm); - -impl FromStr for HttpDate { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - match time::strptime(s, "%a, %d %b %Y %T %Z") - .or_else(|_| time::strptime(s, "%A, %d-%b-%y %T %Z")) - .or_else(|_| time::strptime(s, "%c")) - { - Ok(t) => Ok(HttpDate(t)), - Err(_) => Err(ParseError::Header), - } - } -} - -impl Display for HttpDate { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0.to_utc().rfc822(), f) - } -} - -impl From for HttpDate { - fn from(tm: time::Tm) -> HttpDate { - HttpDate(tm) - } -} - -impl From for HttpDate { - fn from(sys: SystemTime) -> HttpDate { - let tmspec = match sys.duration_since(UNIX_EPOCH) { - Ok(dur) => { - time::Timespec::new(dur.as_secs() as i64, dur.subsec_nanos() as i32) - } - Err(err) => { - let neg = err.duration(); - time::Timespec::new( - -(neg.as_secs() as i64), - -(neg.subsec_nanos() as i32), - ) - } - }; - HttpDate(time::at_utc(tmspec)) - } -} - -impl IntoHeaderValue for HttpDate { - type Error = InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut wrt = BytesMut::with_capacity(29).writer(); - write!(wrt, "{}", self.0.rfc822()).unwrap(); - HeaderValue::from_shared(wrt.get_mut().take().freeze()) - } -} - -impl From for SystemTime { - fn from(date: HttpDate) -> SystemTime { - let spec = date.0.to_timespec(); - if spec.sec >= 0 { - UNIX_EPOCH + Duration::new(spec.sec as u64, spec.nsec as u32) - } else { - UNIX_EPOCH - Duration::new(spec.sec as u64, spec.nsec as u32) - } - } -} - -#[cfg(test)] -mod tests { - use super::HttpDate; - use time::Tm; - - const NOV_07: HttpDate = HttpDate(Tm { - tm_nsec: 0, - tm_sec: 37, - tm_min: 48, - tm_hour: 8, - tm_mday: 7, - tm_mon: 10, - tm_year: 94, - tm_wday: 0, - tm_isdst: 0, - tm_yday: 0, - tm_utcoff: 0, - }); - - #[test] - fn test_date() { - assert_eq!( - "Sun, 07 Nov 1994 08:48:37 GMT".parse::().unwrap(), - NOV_07 - ); - assert_eq!( - "Sunday, 07-Nov-94 08:48:37 GMT" - .parse::() - .unwrap(), - NOV_07 - ); - assert_eq!( - "Sun Nov 7 08:48:37 1994".parse::().unwrap(), - NOV_07 - ); - assert!("this-is-no-date".parse::().is_err()); - } -} diff --git a/src/header/shared/mod.rs b/src/header/shared/mod.rs deleted file mode 100644 index f2bc91634..000000000 --- a/src/header/shared/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Copied for `hyper::header::shared`; - -pub use self::charset::Charset; -pub use self::encoding::Encoding; -pub use self::entity::EntityTag; -pub use self::httpdate::HttpDate; -pub use self::quality_item::{q, qitem, Quality, QualityItem}; -pub use language_tags::LanguageTag; - -mod charset; -mod encoding; -mod entity; -mod httpdate; -mod quality_item; diff --git a/src/header/shared/quality_item.rs b/src/header/shared/quality_item.rs deleted file mode 100644 index 80bd7e1c2..000000000 --- a/src/header/shared/quality_item.rs +++ /dev/null @@ -1,294 +0,0 @@ -use std::cmp; -use std::default::Default; -use std::fmt; -use std::str; - -use self::internal::IntoQuality; - -/// Represents a quality used in quality values. -/// -/// Can be created with the `q` function. -/// -/// # Implementation notes -/// -/// The quality value is defined as a number between 0 and 1 with three decimal -/// places. This means there are 1001 possible values. Since floating point -/// numbers are not exact and the smallest floating point data type (`f32`) -/// consumes four bytes, hyper uses an `u16` value to store the -/// quality internally. For performance reasons you may set quality directly to -/// a value between 0 and 1000 e.g. `Quality(532)` matches the quality -/// `q=0.532`. -/// -/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1) -/// gives more information on quality values in HTTP header fields. -#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct Quality(u16); - -impl Default for Quality { - fn default() -> Quality { - Quality(1000) - } -} - -/// Represents an item with a quality value as defined in -/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.1). -#[derive(Clone, PartialEq, Debug)] -pub struct QualityItem { - /// The actual contents of the field. - pub item: T, - /// The quality (client or server preference) for the value. - pub quality: Quality, -} - -impl QualityItem { - /// Creates a new `QualityItem` from an item and a quality. - /// The item can be of any type. - /// The quality should be a value in the range [0, 1]. - pub fn new(item: T, quality: Quality) -> QualityItem { - QualityItem { item, quality } - } -} - -impl cmp::PartialOrd for QualityItem { - fn partial_cmp(&self, other: &QualityItem) -> Option { - self.quality.partial_cmp(&other.quality) - } -} - -impl fmt::Display for QualityItem { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.item, f)?; - match self.quality.0 { - 1000 => Ok(()), - 0 => f.write_str("; q=0"), - x => write!(f, "; q=0.{}", format!("{:03}", x).trim_right_matches('0')), - } - } -} - -impl str::FromStr for QualityItem { - type Err = ::error::ParseError; - - fn from_str(s: &str) -> Result, ::error::ParseError> { - if !s.is_ascii() { - return Err(::error::ParseError::Header); - } - // Set defaults used if parsing fails. - let mut raw_item = s; - let mut quality = 1f32; - - let parts: Vec<&str> = s.rsplitn(2, ';').map(|x| x.trim()).collect(); - if parts.len() == 2 { - if parts[0].len() < 2 { - return Err(::error::ParseError::Header); - } - let start = &parts[0][0..2]; - if start == "q=" || start == "Q=" { - let q_part = &parts[0][2..parts[0].len()]; - if q_part.len() > 5 { - return Err(::error::ParseError::Header); - } - match q_part.parse::() { - Ok(q_value) => { - if 0f32 <= q_value && q_value <= 1f32 { - quality = q_value; - raw_item = parts[1]; - } else { - return Err(::error::ParseError::Header); - } - } - Err(_) => return Err(::error::ParseError::Header), - } - } - } - match raw_item.parse::() { - // we already checked above that the quality is within range - Ok(item) => Ok(QualityItem::new(item, from_f32(quality))), - Err(_) => Err(::error::ParseError::Header), - } - } -} - -#[inline] -fn from_f32(f: f32) -> Quality { - // this function is only used internally. A check that `f` is within range - // should be done before calling this method. Just in case, this - // debug_assert should catch if we were forgetful - debug_assert!( - f >= 0f32 && f <= 1f32, - "q value must be between 0.0 and 1.0" - ); - Quality((f * 1000f32) as u16) -} - -/// Convenience function to wrap a value in a `QualityItem` -/// Sets `q` to the default 1.0 -pub fn qitem(item: T) -> QualityItem { - QualityItem::new(item, Default::default()) -} - -/// Convenience function to create a `Quality` from a float or integer. -/// -/// Implemented for `u16` and `f32`. Panics if value is out of range. -pub fn q(val: T) -> Quality { - val.into_quality() -} - -mod internal { - use super::Quality; - - // TryFrom is probably better, but it's not stable. For now, we want to - // keep the functionality of the `q` function, while allowing it to be - // generic over `f32` and `u16`. - // - // `q` would panic before, so keep that behavior. `TryFrom` can be - // introduced later for a non-panicking conversion. - - pub trait IntoQuality: Sealed + Sized { - fn into_quality(self) -> Quality; - } - - impl IntoQuality for f32 { - fn into_quality(self) -> Quality { - assert!( - self >= 0f32 && self <= 1f32, - "float must be between 0.0 and 1.0" - ); - super::from_f32(self) - } - } - - impl IntoQuality for u16 { - fn into_quality(self) -> Quality { - assert!(self <= 1000, "u16 must be between 0 and 1000"); - Quality(self) - } - } - - pub trait Sealed {} - impl Sealed for u16 {} - impl Sealed for f32 {} -} - -#[cfg(test)] -mod tests { - use super::super::encoding::*; - use super::*; - - #[test] - fn test_quality_item_fmt_q_1() { - let x = qitem(Chunked); - assert_eq!(format!("{}", x), "chunked"); - } - #[test] - fn test_quality_item_fmt_q_0001() { - let x = QualityItem::new(Chunked, Quality(1)); - assert_eq!(format!("{}", x), "chunked; q=0.001"); - } - #[test] - fn test_quality_item_fmt_q_05() { - // Custom value - let x = QualityItem { - item: EncodingExt("identity".to_owned()), - quality: Quality(500), - }; - assert_eq!(format!("{}", x), "identity; q=0.5"); - } - - #[test] - fn test_quality_item_fmt_q_0() { - // Custom value - let x = QualityItem { - item: EncodingExt("identity".to_owned()), - quality: Quality(0), - }; - assert_eq!(x.to_string(), "identity; q=0"); - } - - #[test] - fn test_quality_item_from_str1() { - let x: Result, _> = "chunked".parse(); - assert_eq!( - x.unwrap(), - QualityItem { - item: Chunked, - quality: Quality(1000), - } - ); - } - #[test] - fn test_quality_item_from_str2() { - let x: Result, _> = "chunked; q=1".parse(); - assert_eq!( - x.unwrap(), - QualityItem { - item: Chunked, - quality: Quality(1000), - } - ); - } - #[test] - fn test_quality_item_from_str3() { - let x: Result, _> = "gzip; q=0.5".parse(); - assert_eq!( - x.unwrap(), - QualityItem { - item: Gzip, - quality: Quality(500), - } - ); - } - #[test] - fn test_quality_item_from_str4() { - let x: Result, _> = "gzip; q=0.273".parse(); - assert_eq!( - x.unwrap(), - QualityItem { - item: Gzip, - quality: Quality(273), - } - ); - } - #[test] - fn test_quality_item_from_str5() { - let x: Result, _> = "gzip; q=0.2739999".parse(); - assert!(x.is_err()); - } - #[test] - fn test_quality_item_from_str6() { - let x: Result, _> = "gzip; q=2".parse(); - assert!(x.is_err()); - } - #[test] - fn test_quality_item_ordering() { - let x: QualityItem = "gzip; q=0.5".parse().ok().unwrap(); - let y: QualityItem = "gzip; q=0.273".parse().ok().unwrap(); - let comparision_result: bool = x.gt(&y); - assert!(comparision_result) - } - - #[test] - fn test_quality() { - assert_eq!(q(0.5), Quality(500)); - } - - #[test] - #[should_panic] // FIXME - 32-bit msvc unwinding broken - #[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)] - fn test_quality_invalid() { - q(-1.0); - } - - #[test] - #[should_panic] // FIXME - 32-bit msvc unwinding broken - #[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)] - fn test_quality_invalid2() { - q(2.0); - } - - #[test] - fn test_fuzzing_bugs() { - assert!("99999;".parse::>().is_err()); - assert!("\x0d;;;=\u{d6aa}==".parse::>().is_err()) - } -} diff --git a/src/httpresponse.rs b/src/httpresponse.rs index cdcdea94a..3c034fae3 100644 --- a/src/httpresponse.rs +++ b/src/httpresponse.rs @@ -15,8 +15,6 @@ use serde_json; use body::Body; use error::Error; use header::{ContentEncoding, Header, IntoHeaderValue}; -// use httpmessage::HttpMessage; -// use httprequest::HttpRequest; /// max write buffer size 64k pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; @@ -942,8 +940,8 @@ mod tests { use body::Binary; use http; use http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; - use time::Duration; + use header::ContentEncoding; use test::TestRequest; #[test] diff --git a/src/lib.rs b/src/lib.rs index 6215bc4fb..7efb4deab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,7 +88,6 @@ extern crate log; extern crate base64; extern crate byteorder; extern crate bytes; -extern crate regex; extern crate sha1; extern crate time; #[macro_use] @@ -99,21 +98,16 @@ extern crate failure; extern crate lazy_static; #[macro_use] extern crate futures; -#[cfg(feature = "brotli")] -extern crate brotli2; extern crate cookie; extern crate encoding; -#[cfg(feature = "flate2")] -extern crate flate2; extern crate http as modhttp; extern crate httparse; -extern crate language_tags; extern crate mime; -extern crate mime_guess; extern crate net2; extern crate rand; extern crate serde; extern crate serde_urlencoded; +extern crate tokio; extern crate tokio_codec; extern crate tokio_current_thread; extern crate tokio_io; @@ -126,8 +120,6 @@ extern crate url; #[macro_use] extern crate percent_encoding; extern crate serde_json; -extern crate smallvec; -extern crate tokio; #[cfg(test)] #[macro_use] @@ -193,9 +185,6 @@ pub mod http { /// Various http headers pub mod header { pub use header::*; - pub use header::{ - Charset, ContentDisposition, DispositionParam, DispositionType, LanguageTag, - }; } pub use header::ContentEncoding; pub use httpresponse::ConnectionType; diff --git a/src/server/input.rs b/src/server/input.rs deleted file mode 100644 index d23d1e991..000000000 --- a/src/server/input.rs +++ /dev/null @@ -1,288 +0,0 @@ -use std::io::{self, Write}; - -#[cfg(feature = "brotli")] -use brotli2::write::BrotliDecoder; -use bytes::{Bytes, BytesMut}; -use error::PayloadError; -#[cfg(feature = "flate2")] -use flate2::write::{GzDecoder, ZlibDecoder}; -use header::ContentEncoding; -use http::header::{HeaderMap, CONTENT_ENCODING}; -use payload::{PayloadSender, PayloadStatus, PayloadWriter}; - -pub(crate) enum PayloadType { - Sender(PayloadSender), - Encoding(Box), -} - -impl PayloadType { - #[cfg(any(feature = "brotli", feature = "flate2"))] - pub fn new(headers: &HeaderMap, sender: PayloadSender) -> PayloadType { - // check content-encoding - let enc = if let Some(enc) = headers.get(CONTENT_ENCODING) { - if let Ok(enc) = enc.to_str() { - ContentEncoding::from(enc) - } else { - ContentEncoding::Auto - } - } else { - ContentEncoding::Auto - }; - - match enc { - ContentEncoding::Auto | ContentEncoding::Identity => { - PayloadType::Sender(sender) - } - _ => PayloadType::Encoding(Box::new(EncodedPayload::new(sender, enc))), - } - } - - #[cfg(not(any(feature = "brotli", feature = "flate2")))] - pub fn new(headers: &HeaderMap, sender: PayloadSender) -> PayloadType { - PayloadType::Sender(sender) - } -} - -impl PayloadWriter for PayloadType { - #[inline] - fn set_error(&mut self, err: PayloadError) { - match *self { - PayloadType::Sender(ref mut sender) => sender.set_error(err), - PayloadType::Encoding(ref mut enc) => enc.set_error(err), - } - } - - #[inline] - fn feed_eof(&mut self) { - match *self { - PayloadType::Sender(ref mut sender) => sender.feed_eof(), - PayloadType::Encoding(ref mut enc) => enc.feed_eof(), - } - } - - #[inline] - fn feed_data(&mut self, data: Bytes) { - match *self { - PayloadType::Sender(ref mut sender) => sender.feed_data(data), - PayloadType::Encoding(ref mut enc) => enc.feed_data(data), - } - } - - #[inline] - fn need_read(&self) -> PayloadStatus { - match *self { - PayloadType::Sender(ref sender) => sender.need_read(), - PayloadType::Encoding(ref enc) => enc.need_read(), - } - } -} - -/// Payload wrapper with content decompression support -pub(crate) struct EncodedPayload { - inner: PayloadSender, - error: bool, - payload: PayloadStream, -} - -impl EncodedPayload { - pub fn new(inner: PayloadSender, enc: ContentEncoding) -> EncodedPayload { - EncodedPayload { - inner, - error: false, - payload: PayloadStream::new(enc), - } - } -} - -impl PayloadWriter for EncodedPayload { - fn set_error(&mut self, err: PayloadError) { - self.inner.set_error(err) - } - - fn feed_eof(&mut self) { - if !self.error { - match self.payload.feed_eof() { - Err(err) => { - self.error = true; - self.set_error(PayloadError::Io(err)); - } - Ok(value) => { - if let Some(b) = value { - self.inner.feed_data(b); - } - self.inner.feed_eof(); - } - } - } - } - - fn feed_data(&mut self, data: Bytes) { - if self.error { - return; - } - - match self.payload.feed_data(data) { - Ok(Some(b)) => self.inner.feed_data(b), - Ok(None) => (), - Err(e) => { - self.error = true; - self.set_error(e.into()); - } - } - } - - #[inline] - fn need_read(&self) -> PayloadStatus { - self.inner.need_read() - } -} - -pub(crate) enum Decoder { - #[cfg(feature = "flate2")] - Deflate(Box>), - #[cfg(feature = "flate2")] - Gzip(Box>), - #[cfg(feature = "brotli")] - Br(Box>), - Identity, -} - -pub(crate) struct Writer { - buf: BytesMut, -} - -impl Writer { - fn new() -> Writer { - Writer { - buf: BytesMut::with_capacity(8192), - } - } - fn take(&mut self) -> Bytes { - self.buf.take().freeze() - } -} - -impl io::Write for Writer { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.buf.extend_from_slice(buf); - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -/// Payload stream with decompression support -pub(crate) struct PayloadStream { - decoder: Decoder, -} - -impl PayloadStream { - pub fn new(enc: ContentEncoding) -> PayloadStream { - let decoder = match enc { - #[cfg(feature = "brotli")] - ContentEncoding::Br => { - Decoder::Br(Box::new(BrotliDecoder::new(Writer::new()))) - } - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => { - Decoder::Deflate(Box::new(ZlibDecoder::new(Writer::new()))) - } - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => { - Decoder::Gzip(Box::new(GzDecoder::new(Writer::new()))) - } - _ => Decoder::Identity, - }; - PayloadStream { decoder } - } -} - -impl PayloadStream { - pub fn feed_eof(&mut self) -> io::Result> { - match self.decoder { - #[cfg(feature = "brotli")] - Decoder::Br(ref mut decoder) => match decoder.finish() { - Ok(mut writer) => { - let b = writer.take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - #[cfg(feature = "flate2")] - Decoder::Gzip(ref mut decoder) => match decoder.try_finish() { - Ok(_) => { - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - #[cfg(feature = "flate2")] - Decoder::Deflate(ref mut decoder) => match decoder.try_finish() { - Ok(_) => { - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - Decoder::Identity => Ok(None), - } - } - - pub fn feed_data(&mut self, data: Bytes) -> io::Result> { - match self.decoder { - #[cfg(feature = "brotli")] - Decoder::Br(ref mut decoder) => match decoder.write_all(&data) { - Ok(_) => { - decoder.flush()?; - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - #[cfg(feature = "flate2")] - Decoder::Gzip(ref mut decoder) => match decoder.write_all(&data) { - Ok(_) => { - decoder.flush()?; - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - #[cfg(feature = "flate2")] - Decoder::Deflate(ref mut decoder) => match decoder.write_all(&data) { - Ok(_) => { - decoder.flush()?; - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - Decoder::Identity => Ok(Some(data)), - } - } -} diff --git a/src/server/mod.rs b/src/server/mod.rs index 0abd7c216..972fbf3fc 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -114,9 +114,6 @@ use tokio_tcp::TcpStream; pub use actix_net::server::{PauseServer, ResumeServer, StopServer}; -pub(crate) mod input; -pub(crate) mod output; - #[doc(hidden)] pub use super::helpers::write_content_length; diff --git a/src/server/service.rs b/src/server/service.rs deleted file mode 100644 index a55c33f72..000000000 --- a/src/server/service.rs +++ /dev/null @@ -1,273 +0,0 @@ -use std::marker::PhantomData; -use std::time::Duration; - -use actix_net::service::{NewService, Service}; -use futures::future::{ok, FutureResult}; -use futures::{Async, Poll}; - -use super::channel::{H1Channel, HttpChannel}; -use super::error::HttpDispatchError; -use super::handler::HttpHandler; -use super::settings::ServiceConfig; -use super::IoStream; -use error::Error; - -/// `NewService` implementation for HTTP1/HTTP2 transports -pub struct HttpService -where - H: HttpHandler, - Io: IoStream, -{ - settings: ServiceConfig, - _t: PhantomData, -} - -impl HttpService -where - H: HttpHandler, - Io: IoStream, -{ - /// Create new `HttpService` instance. - pub fn new(settings: ServiceConfig) -> Self { - HttpService { - settings, - _t: PhantomData, - } - } -} - -impl NewService for HttpService -where - H: HttpHandler, - Io: IoStream, -{ - type Request = Io; - type Response = (); - type Error = HttpDispatchError; - type InitError = (); - type Service = HttpServiceHandler; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(HttpServiceHandler::new(self.settings.clone())) - } -} - -pub struct HttpServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - settings: ServiceConfig, - _t: PhantomData, -} - -impl HttpServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - fn new(settings: ServiceConfig) -> HttpServiceHandler { - HttpServiceHandler { - settings, - _t: PhantomData, - } - } -} - -impl Service for HttpServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - type Request = Io; - type Response = (); - type Error = HttpDispatchError; - type Future = HttpChannel; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - HttpChannel::new(self.settings.clone(), req) - } -} - -/// `NewService` implementation for HTTP1 transport -pub struct H1Service -where - H: HttpHandler, - Io: IoStream, -{ - settings: ServiceConfig, - _t: PhantomData, -} - -impl H1Service -where - H: HttpHandler, - Io: IoStream, -{ - /// Create new `HttpService` instance. - pub fn new(settings: ServiceConfig) -> Self { - H1Service { - settings, - _t: PhantomData, - } - } -} - -impl NewService for H1Service -where - H: HttpHandler, - Io: IoStream, -{ - type Request = Io; - type Response = (); - type Error = HttpDispatchError; - type InitError = (); - type Service = H1ServiceHandler; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(H1ServiceHandler::new(self.settings.clone())) - } -} - -/// `Service` implementation for HTTP1 transport -pub struct H1ServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - settings: ServiceConfig, - _t: PhantomData, -} - -impl H1ServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - fn new(settings: ServiceConfig) -> H1ServiceHandler { - H1ServiceHandler { - settings, - _t: PhantomData, - } - } -} - -impl Service for H1ServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - type Request = Io; - type Response = (); - type Error = HttpDispatchError; - type Future = H1Channel; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - H1Channel::new(self.settings.clone(), req) - } -} - -/// `NewService` implementation for stream configuration service -/// -/// Stream configuration service allows to change some socket level -/// parameters. for example `tcp nodelay` or `tcp keep-alive`. -pub struct StreamConfiguration { - no_delay: Option, - tcp_ka: Option>, - _t: PhantomData<(T, E)>, -} - -impl Default for StreamConfiguration { - fn default() -> Self { - Self::new() - } -} - -impl StreamConfiguration { - /// Create new `StreamConfigurationService` instance. - pub fn new() -> Self { - Self { - no_delay: None, - tcp_ka: None, - _t: PhantomData, - } - } - - /// Sets the value of the `TCP_NODELAY` option on this socket. - pub fn nodelay(mut self, nodelay: bool) -> Self { - self.no_delay = Some(nodelay); - self - } - - /// Sets whether keepalive messages are enabled to be sent on this socket. - pub fn tcp_keepalive(mut self, keepalive: Option) -> Self { - self.tcp_ka = Some(keepalive); - self - } -} - -impl NewService for StreamConfiguration { - type Request = T; - type Response = T; - type Error = E; - type InitError = (); - type Service = StreamConfigurationService; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(StreamConfigurationService { - no_delay: self.no_delay, - tcp_ka: self.tcp_ka, - _t: PhantomData, - }) - } -} - -/// Stream configuration service -/// -/// Stream configuration service allows to change some socket level -/// parameters. for example `tcp nodelay` or `tcp keep-alive`. -pub struct StreamConfigurationService { - no_delay: Option, - tcp_ka: Option>, - _t: PhantomData<(T, E)>, -} - -impl Service for StreamConfigurationService -where - T: IoStream, -{ - type Request = T; - type Response = T; - type Error = E; - type Future = FutureResult; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, mut req: Self::Request) -> Self::Future { - if let Some(no_delay) = self.no_delay { - if req.set_nodelay(no_delay).is_err() { - error!("Can not set socket no-delay option"); - } - } - if let Some(keepalive) = self.tcp_ka { - if req.set_keepalive(keepalive).is_err() { - error!("Can not set socket keep-alive option"); - } - } - - ok(req) - } -} diff --git a/src/ws/context.rs b/src/ws/context.rs deleted file mode 100644 index 4db83df5c..000000000 --- a/src/ws/context.rs +++ /dev/null @@ -1,334 +0,0 @@ -extern crate actix; - -use bytes::Bytes; -use futures::sync::oneshot::{self, Sender}; -use futures::{Async, Future, Poll, Stream}; -use smallvec::SmallVec; - -use self::actix::dev::{ - AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, - ToEnvelope, -}; -use self::actix::fut::ActorFuture; -use self::actix::{ - Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, - Message as ActixMessage, SpawnHandle, -}; - -use body::{Binary, Body}; -use context::{ActorHttpContext, Drain, Frame as ContextFrame}; -use error::{Error, ErrorInternalServerError, PayloadError}; -use httprequest::HttpRequest; - -use ws::frame::{Frame, FramedMessage}; -use ws::proto::{CloseReason, OpCode}; -use ws::{Message, ProtocolError, WsStream, WsWriter}; - -/// Execution context for `WebSockets` actors -pub struct WebsocketContext -where - A: Actor>, -{ - inner: ContextParts, - stream: Option>, - request: HttpRequest, - disconnected: bool, -} - -impl ActorContext for WebsocketContext -where - A: Actor, -{ - fn stop(&mut self) { - self.inner.stop(); - } - fn terminate(&mut self) { - self.inner.terminate() - } - fn state(&self) -> ActorState { - self.inner.state() - } -} - -impl AsyncContext for WebsocketContext -where - A: Actor, -{ - fn spawn(&mut self, fut: F) -> SpawnHandle - where - F: ActorFuture + 'static, - { - self.inner.spawn(fut) - } - - fn wait(&mut self, fut: F) - where - F: ActorFuture + 'static, - { - self.inner.wait(fut) - } - - #[doc(hidden)] - #[inline] - fn waiting(&self) -> bool { - self.inner.waiting() - || self.inner.state() == ActorState::Stopping - || self.inner.state() == ActorState::Stopped - } - - fn cancel_future(&mut self, handle: SpawnHandle) -> bool { - self.inner.cancel_future(handle) - } - - #[inline] - fn address(&self) -> Addr { - self.inner.address() - } -} - -impl WebsocketContext -where - A: Actor, -{ - #[inline] - /// Create a new Websocket context from a request and an actor - pub fn create

    (req: HttpRequest, actor: A, stream: WsStream

    ) -> Body - where - A: StreamHandler, - P: Stream + 'static, - { - let mb = Mailbox::default(); - let mut ctx = WebsocketContext { - inner: ContextParts::new(mb.sender_producer()), - stream: None, - request: req, - disconnected: false, - }; - ctx.add_stream(stream); - - Body::Actor(Box::new(WebsocketContextFut::new(ctx, actor, mb))) - } - - /// Create a new Websocket context - pub fn with_factory(req: HttpRequest, f: F) -> Body - where - F: FnOnce(&mut Self) -> A + 'static, - { - let mb = Mailbox::default(); - let mut ctx = WebsocketContext { - inner: ContextParts::new(mb.sender_producer()), - stream: None, - request: req, - disconnected: false, - }; - - let act = f(&mut ctx); - Body::Actor(Box::new(WebsocketContextFut::new(ctx, act, mb))) - } -} - -impl WebsocketContext -where - A: Actor, -{ - /// Write payload - /// - /// This is a low-level function that accepts framed messages that should - /// be created using `Frame::message()`. If you want to send text or binary - /// data you should prefer the `text()` or `binary()` convenience functions - /// that handle the framing for you. - #[inline] - pub fn write_raw(&mut self, data: FramedMessage) { - if !self.disconnected { - if self.stream.is_none() { - self.stream = Some(SmallVec::new()); - } - let stream = self.stream.as_mut().unwrap(); - stream.push(ContextFrame::Chunk(Some(data.0))); - } else { - warn!("Trying to write to disconnected response"); - } - } - - /// Shared application state - #[inline] - pub fn state(&self) -> &S { - self.request.state() - } - - /// Incoming request - #[inline] - pub fn request(&mut self) -> &mut HttpRequest { - &mut self.request - } - - /// Returns drain future - pub fn drain(&mut self) -> Drain { - let (tx, rx) = oneshot::channel(); - self.add_frame(ContextFrame::Drain(tx)); - Drain::new(rx) - } - - /// Send text frame - #[inline] - pub fn text>(&mut self, text: T) { - self.write_raw(Frame::message(text.into(), OpCode::Text, true, false)); - } - - /// Send binary frame - #[inline] - pub fn binary>(&mut self, data: B) { - self.write_raw(Frame::message(data, OpCode::Binary, true, false)); - } - - /// Send ping frame - #[inline] - pub fn ping(&mut self, message: &str) { - self.write_raw(Frame::message( - Vec::from(message), - OpCode::Ping, - true, - false, - )); - } - - /// Send pong frame - #[inline] - pub fn pong(&mut self, message: &str) { - self.write_raw(Frame::message( - Vec::from(message), - OpCode::Pong, - true, - false, - )); - } - - /// Send close frame - #[inline] - pub fn close(&mut self, reason: Option) { - self.write_raw(Frame::close(reason, false)); - } - - /// Check if connection still open - #[inline] - pub fn connected(&self) -> bool { - !self.disconnected - } - - #[inline] - fn add_frame(&mut self, frame: ContextFrame) { - if self.stream.is_none() { - self.stream = Some(SmallVec::new()); - } - if let Some(s) = self.stream.as_mut() { - s.push(frame) - } - } - - /// Handle of the running future - /// - /// SpawnHandle is the handle returned by `AsyncContext::spawn()` method. - pub fn handle(&self) -> SpawnHandle { - self.inner.curr_handle() - } -} - -impl WsWriter for WebsocketContext -where - A: Actor, - S: 'static, -{ - /// Send text frame - #[inline] - fn send_text>(&mut self, text: T) { - self.text(text) - } - - /// Send binary frame - #[inline] - fn send_binary>(&mut self, data: B) { - self.binary(data) - } - - /// Send ping frame - #[inline] - fn send_ping(&mut self, message: &str) { - self.ping(message) - } - - /// Send pong frame - #[inline] - fn send_pong(&mut self, message: &str) { - self.pong(message) - } - - /// Send close frame - #[inline] - fn send_close(&mut self, reason: Option) { - self.close(reason) - } -} - -impl AsyncContextParts for WebsocketContext -where - A: Actor, -{ - fn parts(&mut self) -> &mut ContextParts { - &mut self.inner - } -} - -struct WebsocketContextFut -where - A: Actor>, -{ - fut: ContextFut>, -} - -impl WebsocketContextFut -where - A: Actor>, -{ - fn new(ctx: WebsocketContext, act: A, mailbox: Mailbox) -> Self { - let fut = ContextFut::new(ctx, act, mailbox); - WebsocketContextFut { fut } - } -} - -impl ActorHttpContext for WebsocketContextFut -where - A: Actor>, - S: 'static, -{ - #[inline] - fn disconnected(&mut self) { - self.fut.ctx().disconnected = true; - self.fut.ctx().stop(); - } - - fn poll(&mut self) -> Poll>, Error> { - if self.fut.alive() && self.fut.poll().is_err() { - return Err(ErrorInternalServerError("error")); - } - - // frames - if let Some(data) = self.fut.ctx().stream.take() { - Ok(Async::Ready(Some(data))) - } else if self.fut.alive() { - Ok(Async::NotReady) - } else { - Ok(Async::Ready(None)) - } - } -} - -impl ToEnvelope for WebsocketContext -where - A: Actor> + Handler, - M: ActixMessage + Send + 'static, - M::Result: Send, -{ - fn pack(msg: M, tx: Option>) -> Envelope { - Envelope::new(msg, tx) - } -} From fbf67544e5e4a4dcb49bff695b2282a76b94221d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 5 Oct 2018 08:00:36 -0700 Subject: [PATCH 013/427] remove unused code --- Cargo.toml | 6 +- src/config.rs | 29 ++++++- src/header.rs | 111 +----------------------- src/lib.rs | 7 +- src/server/mod.rs | 204 --------------------------------------------- tests/test_h1v2.rs | 3 +- 6 files changed, 32 insertions(+), 328 deletions(-) delete mode 100644 src/server/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 870f772e8..455b61488 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,15 +30,12 @@ path = "src/lib.rs" [features] default = ["session", "cell"] -# tls +# native-tls tls = ["native-tls", "tokio-tls", "actix-net/tls"] # openssl ssl = ["openssl", "tokio-openssl", "actix-net/ssl"] -# deprecated, use "ssl" -alpn = ["openssl", "tokio-openssl", "actix-net/ssl"] - # rustls rust-tls = ["rustls", "tokio-rustls", "webpki", "webpki-roots", "actix-net/rust-tls"] @@ -61,7 +58,6 @@ http = "^0.1.8" httparse = "1.3" log = "0.4" mime = "0.3" -percent-encoding = "1.0" rand = "0.5" serde = "1.0" serde_json = "1.0" diff --git a/src/config.rs b/src/config.rs index 36b949c33..4e85044f1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,11 +10,36 @@ use time; use tokio_current_thread::spawn; use tokio_timer::{sleep, Delay}; -use server::KeepAlive; - // "Sun, 06 Nov 1994 08:49:37 GMT".len() const DATE_VALUE_LENGTH: usize = 29; +#[derive(Debug, PartialEq, Clone, Copy)] +/// Server keep-alive setting +pub enum KeepAlive { + /// Keep alive in seconds + Timeout(usize), + /// Relay on OS to shutdown tcp connection + Os, + /// Disabled + Disabled, +} + +impl From for KeepAlive { + fn from(keepalive: usize) -> Self { + KeepAlive::Timeout(keepalive) + } +} + +impl From> for KeepAlive { + fn from(keepalive: Option) -> Self { + if let Some(keepalive) = keepalive { + KeepAlive::Timeout(keepalive) + } else { + KeepAlive::Disabled + } + } +} + /// Http service configuration pub struct ServiceConfig(Rc); diff --git a/src/header.rs b/src/header.rs index 7b32d6c24..b1ba6524a 100644 --- a/src/header.rs +++ b/src/header.rs @@ -1,13 +1,8 @@ //! Various http headers -use std::fmt; -use std::str::FromStr; - -use bytes::{Bytes, BytesMut}; +use bytes::Bytes; use mime::Mime; -use modhttp::header::GetAll; use modhttp::Error as HttpError; -use percent_encoding; pub use modhttp::header::*; @@ -159,107 +154,3 @@ impl<'a> From<&'a str> for ContentEncoding { } } } - -#[doc(hidden)] -pub(crate) struct Writer { - buf: BytesMut, -} - -impl Writer { - fn new() -> Writer { - Writer { - buf: BytesMut::new(), - } - } - fn take(&mut self) -> Bytes { - self.buf.take().freeze() - } -} - -impl fmt::Write for Writer { - #[inline] - fn write_str(&mut self, s: &str) -> fmt::Result { - self.buf.extend_from_slice(s.as_bytes()); - Ok(()) - } - - #[inline] - fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result { - fmt::write(self, args) - } -} - -#[inline] -#[doc(hidden)] -/// Reads a comma-delimited raw header into a Vec. -pub fn from_comma_delimited( - all: GetAll, -) -> Result, ParseError> { - let mut result = Vec::new(); - for h in all { - let s = h.to_str().map_err(|_| ParseError::Header)?; - result.extend( - s.split(',') - .filter_map(|x| match x.trim() { - "" => None, - y => Some(y), - }).filter_map(|x| x.trim().parse().ok()), - ) - } - Ok(result) -} - -#[inline] -#[doc(hidden)] -/// Reads a single string when parsing a header. -pub fn from_one_raw_str(val: Option<&HeaderValue>) -> Result { - if let Some(line) = val { - let line = line.to_str().map_err(|_| ParseError::Header)?; - if !line.is_empty() { - return T::from_str(line).or(Err(ParseError::Header)); - } - } - Err(ParseError::Header) -} - -#[inline] -#[doc(hidden)] -/// Format an array into a comma-delimited string. -pub fn fmt_comma_delimited(f: &mut fmt::Formatter, parts: &[T]) -> fmt::Result -where - T: fmt::Display, -{ - let mut iter = parts.iter(); - if let Some(part) = iter.next() { - fmt::Display::fmt(part, f)?; - } - for part in iter { - f.write_str(", ")?; - fmt::Display::fmt(part, f)?; - } - Ok(()) -} - -/// Percent encode a sequence of bytes with a character set defined in -/// [https://tools.ietf.org/html/rfc5987#section-3.2][url] -/// -/// [url]: https://tools.ietf.org/html/rfc5987#section-3.2 -pub fn http_percent_encode(f: &mut fmt::Formatter, bytes: &[u8]) -> fmt::Result { - let encoded = - percent_encoding::percent_encode(bytes, self::percent_encoding_http::HTTP_VALUE); - fmt::Display::fmt(&encoded, f) -} -mod percent_encoding_http { - use percent_encoding; - - // internal module because macro is hard-coded to make a public item - // but we don't want to public export this item - define_encode_set! { - // This encode set is used for HTTP header values and is defined at - // https://tools.ietf.org/html/rfc5987#section-3.2 - pub HTTP_VALUE = [percent_encoding::SIMPLE_ENCODE_SET] | { - ' ', '"', '%', '\'', '(', ')', '*', ',', '/', ':', ';', '<', '-', '>', '?', - '[', '\\', ']', '{', '}' - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 7efb4deab..accad2fbb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,6 +106,7 @@ extern crate mime; extern crate net2; extern crate rand; extern crate serde; +extern crate serde_json; extern crate serde_urlencoded; extern crate tokio; extern crate tokio_codec; @@ -117,9 +118,6 @@ extern crate tokio_timer; #[cfg(all(unix, feature = "uds"))] extern crate tokio_uds; extern crate url; -#[macro_use] -extern crate percent_encoding; -extern crate serde_json; #[cfg(test)] #[macro_use] @@ -140,7 +138,6 @@ mod uri; pub mod error; pub mod h1; pub(crate) mod helpers; -pub mod server; pub mod test; //pub mod ws; pub use body::{Binary, Body}; @@ -151,7 +148,7 @@ pub use httpresponse::HttpResponse; pub use json::Json; pub use request::Request; -pub use self::config::{ServiceConfig, ServiceConfigBuilder}; +pub use self::config::{KeepAlive, ServiceConfig, ServiceConfigBuilder}; pub mod dev { //! The `actix-web` prelude for library developers diff --git a/src/server/mod.rs b/src/server/mod.rs deleted file mode 100644 index 972fbf3fc..000000000 --- a/src/server/mod.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! Http server module -//! -//! The module contains everything necessary to setup -//! HTTP server. -//! -//! In order to start HTTP server, first you need to create and configure it -//! using factory that can be supplied to [new](fn.new.html). -//! -//! ## Factory -//! -//! Factory is a function that returns Application, describing how -//! to serve incoming HTTP requests. -//! -//! As the server uses worker pool, the factory function is restricted to trait bounds -//! `Sync + Send + 'static` so that each worker would be able to accept Application -//! without a need for synchronization. -//! -//! If you wish to share part of state among all workers you should -//! wrap it in `Arc` and potentially synchronization primitive like -//! [RwLock](https://doc.rust-lang.org/std/sync/struct.RwLock.html) -//! If the wrapped type is not thread safe. -//! -//! Note though that locking is not advisable for asynchronous programming -//! and you should minimize all locks in your request handlers -//! -//! ## HTTPS Support -//! -//! Actix-web provides support for major crates that provides TLS. -//! Each TLS implementation is provided with [AcceptorService](trait.AcceptorService.html) -//! that describes how HTTP Server accepts connections. -//! -//! For `bind` and `listen` there are corresponding `bind_with` and `listen_with` that accepts -//! these services. -//! -//! By default, acceptor would work with both HTTP2 and HTTP1 protocols. -//! But it can be controlled using [ServerFlags](struct.ServerFlags.html) which -//! can be supplied when creating `AcceptorService`. -//! -//! **NOTE:** `native-tls` doesn't support `HTTP2` yet -//! -//! ## Signal handling and shutdown -//! -//! By default HTTP Server listens for system signals -//! and, gracefully shuts down at most after 30 seconds. -//! -//! Both signal handling and shutdown timeout can be controlled -//! using corresponding methods. -//! -//! If worker, for some reason, unable to shut down within timeout -//! it is forcibly dropped. -//! -//! ## Example -//! -//! ```rust,ignore -//!extern crate actix; -//!extern crate actix_web; -//!extern crate rustls; -//! -//!use actix_web::{http, middleware, server, App, Error, HttpRequest, HttpResponse, Responder}; -//!use std::io::BufReader; -//!use rustls::internal::pemfile::{certs, rsa_private_keys}; -//!use rustls::{NoClientAuth, ServerConfig}; -//! -//!fn index(req: &HttpRequest) -> Result { -//! Ok(HttpResponse::Ok().content_type("text/plain").body("Welcome!")) -//!} -//! -//!fn load_ssl() -> ServerConfig { -//! use std::io::BufReader; -//! -//! const CERT: &'static [u8] = include_bytes!("../cert.pem"); -//! const KEY: &'static [u8] = include_bytes!("../key.pem"); -//! -//! let mut cert = BufReader::new(CERT); -//! let mut key = BufReader::new(KEY); -//! -//! let mut config = ServerConfig::new(NoClientAuth::new()); -//! let cert_chain = certs(&mut cert).unwrap(); -//! let mut keys = rsa_private_keys(&mut key).unwrap(); -//! config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); -//! -//! config -//!} -//! -//!fn main() { -//! let sys = actix::System::new("http-server"); -//! // load ssl keys -//! let config = load_ssl(); -//! -//! // Create acceptor service for only HTTP1 protocol -//! // You can use ::new(config) to leave defaults -//! let acceptor = server::RustlsAcceptor::with_flags(config, actix_web::server::ServerFlags::HTTP1); -//! -//! // create and start server at once -//! server::new(|| { -//! App::new() -//! // register simple handler, handle all methods -//! .resource("/index.html", |r| r.f(index)) -//! })) -//! }).bind_with("127.0.0.1:8080", acceptor) -//! .unwrap() -//! .start(); -//! -//! println!("Started http server: 127.0.0.1:8080"); -//! //Run system so that server would start accepting connections -//! let _ = sys.run(); -//!} -//! ``` -use std::net::SocketAddr; -use std::{io, time}; - -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_tcp::TcpStream; - -pub use actix_net::server::{PauseServer, ResumeServer, StopServer}; - -#[doc(hidden)] -pub use super::helpers::write_content_length; - -// /// max buffer size 64k -// pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; - -#[derive(Debug, PartialEq, Clone, Copy)] -/// Server keep-alive setting -pub enum KeepAlive { - /// Keep alive in seconds - Timeout(usize), - /// Relay on OS to shutdown tcp connection - Os, - /// Disabled - Disabled, -} - -impl From for KeepAlive { - fn from(keepalive: usize) -> Self { - KeepAlive::Timeout(keepalive) - } -} - -impl From> for KeepAlive { - fn from(keepalive: Option) -> Self { - if let Some(keepalive) = keepalive { - KeepAlive::Timeout(keepalive) - } else { - KeepAlive::Disabled - } - } -} - -#[doc(hidden)] -/// Low-level io stream operations -pub trait IoStream: AsyncRead + AsyncWrite + 'static { - /// Returns the socket address of the remote peer of this TCP connection. - fn peer_addr(&self) -> Option { - None - } - - /// Sets the value of the TCP_NODELAY option on this socket. - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()>; - - fn set_linger(&mut self, dur: Option) -> io::Result<()>; - - fn set_keepalive(&mut self, dur: Option) -> io::Result<()>; -} - -#[cfg(all(unix, feature = "uds"))] -impl IoStream for ::tokio_uds::UnixStream { - #[inline] - fn set_nodelay(&mut self, _nodelay: bool) -> io::Result<()> { - Ok(()) - } - - #[inline] - fn set_linger(&mut self, _dur: Option) -> io::Result<()> { - Ok(()) - } - - #[inline] - fn set_keepalive(&mut self, _nodelay: bool) -> io::Result<()> { - Ok(()) - } -} - -impl IoStream for TcpStream { - #[inline] - fn peer_addr(&self) -> Option { - TcpStream::peer_addr(self).ok() - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - TcpStream::set_nodelay(self, nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - TcpStream::set_linger(self, dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - TcpStream::set_keepalive(self, dur) - } -} diff --git a/tests/test_h1v2.rs b/tests/test_h1v2.rs index 77a6ecae5..bb9430659 100644 --- a/tests/test_h1v2.rs +++ b/tests/test_h1v2.rs @@ -11,8 +11,7 @@ use actix_net::server::Server; use actix_web::{client, test}; use futures::future; -use actix_http::server::KeepAlive; -use actix_http::{h1, Error, HttpResponse, ServiceConfig}; +use actix_http::{h1, Error, HttpResponse, KeepAlive, ServiceConfig}; #[test] fn test_h1_v2() { From 2e27d7774089e21aecaea41f59657bc5c73882e4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 5 Oct 2018 10:03:10 -0700 Subject: [PATCH 014/427] fix connection keepalive support --- src/framed/framed.rs | 283 +++++++++++++++++++++++++++++ src/framed/framed_read.rs | 216 ++++++++++++++++++++++ src/framed/framed_write.rs | 243 +++++++++++++++++++++++++ src/framed/mod.rs | 32 ++++ src/h1/codec.rs | 193 ++++++++++---------- src/h1/dispatcher.rs | 15 +- src/h1/{response.rs => encoder.rs} | 8 +- src/h1/mod.rs | 2 +- src/lib.rs | 3 + 9 files changed, 890 insertions(+), 105 deletions(-) create mode 100644 src/framed/framed.rs create mode 100644 src/framed/framed_read.rs create mode 100644 src/framed/framed_write.rs create mode 100644 src/framed/mod.rs rename src/h1/{response.rs => encoder.rs} (98%) diff --git a/src/framed/framed.rs b/src/framed/framed.rs new file mode 100644 index 000000000..f6295d98f --- /dev/null +++ b/src/framed/framed.rs @@ -0,0 +1,283 @@ +#![allow(deprecated)] + +use std::fmt; +use std::io::{self, Read, Write}; + +use bytes::BytesMut; +use futures::{Poll, Sink, StartSend, Stream}; +use tokio_codec::{Decoder, Encoder}; +use tokio_io::{AsyncRead, AsyncWrite}; + +use super::framed_read::{framed_read2, framed_read2_with_buffer, FramedRead2}; +use super::framed_write::{framed_write2, framed_write2_with_buffer, FramedWrite2}; + +/// A unified `Stream` and `Sink` interface to an underlying I/O object, using +/// the `Encoder` and `Decoder` traits to encode and decode frames. +/// +/// You can create a `Framed` instance by using the `AsyncRead::framed` adapter. +pub struct Framed { + inner: FramedRead2>>, +} + +pub struct Fuse(pub T, pub U); + +impl Framed +where + T: AsyncRead + AsyncWrite, + U: Decoder + Encoder, +{ + /// Provides a `Stream` and `Sink` interface for reading and writing to this + /// `Io` object, using `Decode` and `Encode` to read and write the raw data. + /// + /// Raw I/O objects work with byte sequences, but higher-level code usually + /// wants to batch these into meaningful chunks, called "frames". This + /// method layers framing on top of an I/O object, by using the `Codec` + /// traits to handle encoding and decoding of messages frames. Note that + /// the incoming and outgoing frame types may be distinct. + /// + /// This function returns a *single* object that is both `Stream` and + /// `Sink`; grouping this into a single object is often useful for layering + /// things like gzip or TLS, which require both read and write access to the + /// underlying object. + /// + /// If you want to work more directly with the streams and sink, consider + /// calling `split` on the `Framed` returned by this method, which will + /// break them into separate objects, allowing them to interact more easily. + pub fn new(inner: T, codec: U) -> Framed { + Framed { + inner: framed_read2(framed_write2(Fuse(inner, codec))), + } + } +} + +impl Framed { + /// Provides a `Stream` and `Sink` interface for reading and writing to this + /// `Io` object, using `Decode` and `Encode` to read and write the raw data. + /// + /// Raw I/O objects work with byte sequences, but higher-level code usually + /// wants to batch these into meaningful chunks, called "frames". This + /// method layers framing on top of an I/O object, by using the `Codec` + /// traits to handle encoding and decoding of messages frames. Note that + /// the incoming and outgoing frame types may be distinct. + /// + /// This function returns a *single* object that is both `Stream` and + /// `Sink`; grouping this into a single object is often useful for layering + /// things like gzip or TLS, which require both read and write access to the + /// underlying object. + /// + /// This objects takes a stream and a readbuffer and a writebuffer. These field + /// can be obtained from an existing `Framed` with the `into_parts` method. + /// + /// If you want to work more directly with the streams and sink, consider + /// calling `split` on the `Framed` returned by this method, which will + /// break them into separate objects, allowing them to interact more easily. + pub fn from_parts(parts: FramedParts) -> Framed { + Framed { + inner: framed_read2_with_buffer( + framed_write2_with_buffer(Fuse(parts.io, parts.codec), parts.write_buf), + parts.read_buf, + ), + } + } + + /// Returns a reference to the underlying codec. + pub fn get_codec(&self) -> &U { + &self.inner.get_ref().get_ref().1 + } + + /// Returns a mutable reference to the underlying codec. + pub fn get_codec_mut(&mut self) -> &mut U { + &mut self.inner.get_mut().get_mut().1 + } + + /// Returns a reference to the underlying I/O stream wrapped by + /// `Frame`. + /// + /// Note that care should be taken to not tamper with the underlying stream + /// of data coming in as it may corrupt the stream of frames otherwise + /// being worked with. + pub fn get_ref(&self) -> &T { + &self.inner.get_ref().get_ref().0 + } + + /// Returns a mutable reference to the underlying I/O stream wrapped by + /// `Frame`. + /// + /// Note that care should be taken to not tamper with the underlying stream + /// of data coming in as it may corrupt the stream of frames otherwise + /// being worked with. + pub fn get_mut(&mut self) -> &mut T { + &mut self.inner.get_mut().get_mut().0 + } + + /// Consumes the `Frame`, returning its underlying I/O stream. + /// + /// Note that care should be taken to not tamper with the underlying stream + /// of data coming in as it may corrupt the stream of frames otherwise + /// being worked with. + pub fn into_inner(self) -> T { + self.inner.into_inner().into_inner().0 + } + + /// Consumes the `Frame`, returning its underlying I/O stream, the buffer + /// with unprocessed data, and the codec. + /// + /// Note that care should be taken to not tamper with the underlying stream + /// of data coming in as it may corrupt the stream of frames otherwise + /// being worked with. + pub fn into_parts(self) -> FramedParts { + let (inner, read_buf) = self.inner.into_parts(); + let (inner, write_buf) = inner.into_parts(); + + FramedParts { + io: inner.0, + codec: inner.1, + read_buf: read_buf, + write_buf: write_buf, + _priv: (), + } + } +} + +impl Stream for Framed +where + T: AsyncRead, + U: Decoder, +{ + type Item = U::Item; + type Error = U::Error; + + fn poll(&mut self) -> Poll, Self::Error> { + self.inner.poll() + } +} + +impl Sink for Framed +where + T: AsyncWrite, + U: Encoder, + U::Error: From, +{ + type SinkItem = U::Item; + type SinkError = U::Error; + + fn start_send( + &mut self, item: Self::SinkItem, + ) -> StartSend { + self.inner.get_mut().start_send(item) + } + + fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { + self.inner.get_mut().poll_complete() + } + + fn close(&mut self) -> Poll<(), Self::SinkError> { + self.inner.get_mut().close() + } +} + +impl fmt::Debug for Framed +where + T: fmt::Debug, + U: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Framed") + .field("io", &self.inner.get_ref().get_ref().0) + .field("codec", &self.inner.get_ref().get_ref().1) + .finish() + } +} + +// ===== impl Fuse ===== + +impl Read for Fuse { + fn read(&mut self, dst: &mut [u8]) -> io::Result { + self.0.read(dst) + } +} + +impl AsyncRead for Fuse { + unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { + self.0.prepare_uninitialized_buffer(buf) + } +} + +impl Write for Fuse { + fn write(&mut self, src: &[u8]) -> io::Result { + self.0.write(src) + } + + fn flush(&mut self) -> io::Result<()> { + self.0.flush() + } +} + +impl AsyncWrite for Fuse { + fn shutdown(&mut self) -> Poll<(), io::Error> { + self.0.shutdown() + } +} + +impl Decoder for Fuse { + type Item = U::Item; + type Error = U::Error; + + fn decode( + &mut self, buffer: &mut BytesMut, + ) -> Result, Self::Error> { + self.1.decode(buffer) + } + + fn decode_eof( + &mut self, buffer: &mut BytesMut, + ) -> Result, Self::Error> { + self.1.decode_eof(buffer) + } +} + +impl Encoder for Fuse { + type Item = U::Item; + type Error = U::Error; + + fn encode( + &mut self, item: Self::Item, dst: &mut BytesMut, + ) -> Result<(), Self::Error> { + self.1.encode(item, dst) + } +} + +/// `FramedParts` contains an export of the data of a Framed transport. +/// It can be used to construct a new `Framed` with a different codec. +/// It contains all current buffers and the inner transport. +#[derive(Debug)] +pub struct FramedParts { + /// The inner transport used to read bytes to and write bytes to + pub io: T, + + /// The codec + pub codec: U, + + /// The buffer with read but unprocessed data. + pub read_buf: BytesMut, + + /// A buffer with unprocessed data which are not written yet. + pub write_buf: BytesMut, + + /// This private field allows us to add additional fields in the future in a + /// backwards compatible way. + _priv: (), +} + +impl FramedParts { + /// Create a new, default, `FramedParts` + pub fn new(io: T, codec: U) -> FramedParts { + FramedParts { + io, + codec, + read_buf: BytesMut::new(), + write_buf: BytesMut::new(), + _priv: (), + } + } +} diff --git a/src/framed/framed_read.rs b/src/framed/framed_read.rs new file mode 100644 index 000000000..065e29205 --- /dev/null +++ b/src/framed/framed_read.rs @@ -0,0 +1,216 @@ +use std::fmt; + +use bytes::BytesMut; +use futures::{Async, Poll, Sink, StartSend, Stream}; +use tokio_codec::Decoder; +use tokio_io::AsyncRead; + +use super::framed::Fuse; + +/// A `Stream` of messages decoded from an `AsyncRead`. +pub struct FramedRead { + inner: FramedRead2>, +} + +pub struct FramedRead2 { + inner: T, + eof: bool, + is_readable: bool, + buffer: BytesMut, +} + +const INITIAL_CAPACITY: usize = 8 * 1024; + +// ===== impl FramedRead ===== + +impl FramedRead +where + T: AsyncRead, + D: Decoder, +{ + /// Creates a new `FramedRead` with the given `decoder`. + pub fn new(inner: T, decoder: D) -> FramedRead { + FramedRead { + inner: framed_read2(Fuse(inner, decoder)), + } + } +} + +impl FramedRead { + /// Returns a reference to the underlying I/O stream wrapped by + /// `FramedRead`. + /// + /// Note that care should be taken to not tamper with the underlying stream + /// of data coming in as it may corrupt the stream of frames otherwise + /// being worked with. + pub fn get_ref(&self) -> &T { + &self.inner.inner.0 + } + + /// Returns a mutable reference to the underlying I/O stream wrapped by + /// `FramedRead`. + /// + /// Note that care should be taken to not tamper with the underlying stream + /// of data coming in as it may corrupt the stream of frames otherwise + /// being worked with. + pub fn get_mut(&mut self) -> &mut T { + &mut self.inner.inner.0 + } + + /// Consumes the `FramedRead`, returning its underlying I/O stream. + /// + /// Note that care should be taken to not tamper with the underlying stream + /// of data coming in as it may corrupt the stream of frames otherwise + /// being worked with. + pub fn into_inner(self) -> T { + self.inner.inner.0 + } + + /// Returns a reference to the underlying decoder. + pub fn decoder(&self) -> &D { + &self.inner.inner.1 + } + + /// Returns a mutable reference to the underlying decoder. + pub fn decoder_mut(&mut self) -> &mut D { + &mut self.inner.inner.1 + } +} + +impl Stream for FramedRead +where + T: AsyncRead, + D: Decoder, +{ + type Item = D::Item; + type Error = D::Error; + + fn poll(&mut self) -> Poll, Self::Error> { + self.inner.poll() + } +} + +impl Sink for FramedRead +where + T: Sink, +{ + type SinkItem = T::SinkItem; + type SinkError = T::SinkError; + + fn start_send( + &mut self, item: Self::SinkItem, + ) -> StartSend { + self.inner.inner.0.start_send(item) + } + + fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { + self.inner.inner.0.poll_complete() + } + + fn close(&mut self) -> Poll<(), Self::SinkError> { + self.inner.inner.0.close() + } +} + +impl fmt::Debug for FramedRead +where + T: fmt::Debug, + D: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("FramedRead") + .field("inner", &self.inner.inner.0) + .field("decoder", &self.inner.inner.1) + .field("eof", &self.inner.eof) + .field("is_readable", &self.inner.is_readable) + .field("buffer", &self.inner.buffer) + .finish() + } +} + +// ===== impl FramedRead2 ===== + +pub fn framed_read2(inner: T) -> FramedRead2 { + FramedRead2 { + inner: inner, + eof: false, + is_readable: false, + buffer: BytesMut::with_capacity(INITIAL_CAPACITY), + } +} + +pub fn framed_read2_with_buffer(inner: T, mut buf: BytesMut) -> FramedRead2 { + if buf.capacity() < INITIAL_CAPACITY { + let bytes_to_reserve = INITIAL_CAPACITY - buf.capacity(); + buf.reserve(bytes_to_reserve); + } + FramedRead2 { + inner: inner, + eof: false, + is_readable: buf.len() > 0, + buffer: buf, + } +} + +impl FramedRead2 { + pub fn get_ref(&self) -> &T { + &self.inner + } + + pub fn into_inner(self) -> T { + self.inner + } + + pub fn into_parts(self) -> (T, BytesMut) { + (self.inner, self.buffer) + } + + pub fn get_mut(&mut self) -> &mut T { + &mut self.inner + } +} + +impl Stream for FramedRead2 +where + T: AsyncRead + Decoder, +{ + type Item = T::Item; + type Error = T::Error; + + fn poll(&mut self) -> Poll, Self::Error> { + loop { + // Repeatedly call `decode` or `decode_eof` as long as it is + // "readable". Readable is defined as not having returned `None`. If + // the upstream has returned EOF, and the decoder is no longer + // readable, it can be assumed that the decoder will never become + // readable again, at which point the stream is terminated. + if self.is_readable { + if self.eof { + let frame = try!(self.inner.decode_eof(&mut self.buffer)); + return Ok(Async::Ready(frame)); + } + + trace!("attempting to decode a frame"); + + if let Some(frame) = try!(self.inner.decode(&mut self.buffer)) { + trace!("frame decoded from buffer"); + return Ok(Async::Ready(Some(frame))); + } + + self.is_readable = false; + } + + assert!(!self.eof); + + // Otherwise, try to read more data and try again. Make sure we've + // got room for at least one byte to read to ensure that we don't + // get a spurious 0 that looks like EOF + self.buffer.reserve(1); + if 0 == try_ready!(self.inner.read_buf(&mut self.buffer)) { + self.eof = true; + } + + self.is_readable = true; + } + } +} diff --git a/src/framed/framed_write.rs b/src/framed/framed_write.rs new file mode 100644 index 000000000..310c76307 --- /dev/null +++ b/src/framed/framed_write.rs @@ -0,0 +1,243 @@ +use std::fmt; +use std::io::{self, Read}; + +use bytes::BytesMut; +use futures::{Async, AsyncSink, Poll, Sink, StartSend, Stream}; +use tokio_codec::{Decoder, Encoder}; +use tokio_io::{AsyncRead, AsyncWrite}; + +use super::framed::Fuse; + +/// A `Sink` of frames encoded to an `AsyncWrite`. +pub struct FramedWrite { + inner: FramedWrite2>, +} + +pub struct FramedWrite2 { + inner: T, + buffer: BytesMut, +} + +const INITIAL_CAPACITY: usize = 8 * 1024; +const BACKPRESSURE_BOUNDARY: usize = INITIAL_CAPACITY; + +impl FramedWrite +where + T: AsyncWrite, + E: Encoder, +{ + /// Creates a new `FramedWrite` with the given `encoder`. + pub fn new(inner: T, encoder: E) -> FramedWrite { + FramedWrite { + inner: framed_write2(Fuse(inner, encoder)), + } + } +} + +impl FramedWrite { + /// Returns a reference to the underlying I/O stream wrapped by + /// `FramedWrite`. + /// + /// Note that care should be taken to not tamper with the underlying stream + /// of data coming in as it may corrupt the stream of frames otherwise + /// being worked with. + pub fn get_ref(&self) -> &T { + &self.inner.inner.0 + } + + /// Returns a mutable reference to the underlying I/O stream wrapped by + /// `FramedWrite`. + /// + /// Note that care should be taken to not tamper with the underlying stream + /// of data coming in as it may corrupt the stream of frames otherwise + /// being worked with. + pub fn get_mut(&mut self) -> &mut T { + &mut self.inner.inner.0 + } + + /// Consumes the `FramedWrite`, returning its underlying I/O stream. + /// + /// Note that care should be taken to not tamper with the underlying stream + /// of data coming in as it may corrupt the stream of frames otherwise + /// being worked with. + pub fn into_inner(self) -> T { + self.inner.inner.0 + } + + /// Returns a reference to the underlying decoder. + pub fn encoder(&self) -> &E { + &self.inner.inner.1 + } + + /// Returns a mutable reference to the underlying decoder. + pub fn encoder_mut(&mut self) -> &mut E { + &mut self.inner.inner.1 + } +} + +impl Sink for FramedWrite +where + T: AsyncWrite, + E: Encoder, +{ + type SinkItem = E::Item; + type SinkError = E::Error; + + fn start_send(&mut self, item: E::Item) -> StartSend { + self.inner.start_send(item) + } + + fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { + self.inner.poll_complete() + } + + fn close(&mut self) -> Poll<(), Self::SinkError> { + Ok(try!(self.inner.close())) + } +} + +impl Stream for FramedWrite +where + T: Stream, +{ + type Item = T::Item; + type Error = T::Error; + + fn poll(&mut self) -> Poll, Self::Error> { + self.inner.inner.0.poll() + } +} + +impl fmt::Debug for FramedWrite +where + T: fmt::Debug, + U: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("FramedWrite") + .field("inner", &self.inner.get_ref().0) + .field("encoder", &self.inner.get_ref().1) + .field("buffer", &self.inner.buffer) + .finish() + } +} + +// ===== impl FramedWrite2 ===== + +pub fn framed_write2(inner: T) -> FramedWrite2 { + FramedWrite2 { + inner: inner, + buffer: BytesMut::with_capacity(INITIAL_CAPACITY), + } +} + +pub fn framed_write2_with_buffer(inner: T, mut buf: BytesMut) -> FramedWrite2 { + if buf.capacity() < INITIAL_CAPACITY { + let bytes_to_reserve = INITIAL_CAPACITY - buf.capacity(); + buf.reserve(bytes_to_reserve); + } + FramedWrite2 { + inner: inner, + buffer: buf, + } +} + +impl FramedWrite2 { + pub fn get_ref(&self) -> &T { + &self.inner + } + + pub fn into_inner(self) -> T { + self.inner + } + + pub fn into_parts(self) -> (T, BytesMut) { + (self.inner, self.buffer) + } + + pub fn get_mut(&mut self) -> &mut T { + &mut self.inner + } +} + +impl Sink for FramedWrite2 +where + T: AsyncWrite + Encoder, +{ + type SinkItem = T::Item; + type SinkError = T::Error; + + fn start_send(&mut self, item: T::Item) -> StartSend { + // If the buffer is already over 8KiB, then attempt to flush it. If after flushing it's + // *still* over 8KiB, then apply backpressure (reject the send). + if self.buffer.len() >= BACKPRESSURE_BOUNDARY { + try!(self.poll_complete()); + + if self.buffer.len() >= BACKPRESSURE_BOUNDARY { + return Ok(AsyncSink::NotReady(item)); + } + } + + try!(self.inner.encode(item, &mut self.buffer)); + + Ok(AsyncSink::Ready) + } + + fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { + trace!("flushing framed transport"); + + while !self.buffer.is_empty() { + trace!("writing; remaining={}", self.buffer.len()); + + let n = try_ready!(self.inner.poll_write(&self.buffer)); + + if n == 0 { + return Err(io::Error::new( + io::ErrorKind::WriteZero, + "failed to \ + write frame to transport", + ).into()); + } + + // TODO: Add a way to `bytes` to do this w/o returning the drained + // data. + let _ = self.buffer.split_to(n); + } + + // Try flushing the underlying IO + try_ready!(self.inner.poll_flush()); + + trace!("framed transport flushed"); + return Ok(Async::Ready(())); + } + + fn close(&mut self) -> Poll<(), Self::SinkError> { + try_ready!(self.poll_complete()); + Ok(try!(self.inner.shutdown())) + } +} + +impl Decoder for FramedWrite2 { + type Item = T::Item; + type Error = T::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, T::Error> { + self.inner.decode(src) + } + + fn decode_eof(&mut self, src: &mut BytesMut) -> Result, T::Error> { + self.inner.decode_eof(src) + } +} + +impl Read for FramedWrite2 { + fn read(&mut self, dst: &mut [u8]) -> io::Result { + self.inner.read(dst) + } +} + +impl AsyncRead for FramedWrite2 { + unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { + self.inner.prepare_uninitialized_buffer(buf) + } +} diff --git a/src/framed/mod.rs b/src/framed/mod.rs new file mode 100644 index 000000000..cb0308fa0 --- /dev/null +++ b/src/framed/mod.rs @@ -0,0 +1,32 @@ +//! Utilities for encoding and decoding frames. +//! +//! Contains adapters to go from streams of bytes, [`AsyncRead`] and +//! [`AsyncWrite`], to framed streams implementing [`Sink`] and [`Stream`]. +//! Framed streams are also known as [transports]. +//! +//! [`AsyncRead`]: # +//! [`AsyncWrite`]: # +//! [`Sink`]: # +//! [`Stream`]: # +//! [transports]: # + +#![deny(missing_docs, missing_debug_implementations, warnings)] +#![doc(hidden, html_root_url = "https://docs.rs/tokio-codec/0.1.0")] + +// _tokio_codec are the items that belong in the `tokio_codec` crate. However, because we need to +// maintain backward compatibility until the next major breaking change, they are defined here. +// When the next breaking change comes, they should be moved to the `tokio_codec` crate and become +// independent. +// +// The primary reason we can't move these to `tokio-codec` now is because, again for backward +// compatibility reasons, we need to keep `Decoder` and `Encoder` in tokio_io::codec. And `Decoder` +// and `Encoder` needs to reference `Framed`. So they all still need to still be in the same +// module. + +mod framed; +mod framed_read; +mod framed_write; + +pub use self::framed::{Framed, FramedParts}; +pub use self::framed_read::FramedRead; +pub use self::framed_write::FramedWrite; diff --git a/src/h1/codec.rs b/src/h1/codec.rs index dd6b26bab..40d0a240f 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -6,7 +6,7 @@ use tokio_codec::{Decoder, Encoder}; use super::decoder::H1Decoder; pub use super::decoder::InMessage; -use super::response::{ResponseInfo, ResponseLength}; +use super::encoder::{ResponseEncoder, ResponseLength}; use body::Body; use error::ParseError; use helpers; @@ -15,6 +15,17 @@ use http::{Method, Version}; use httpresponse::HttpResponse; use request::RequestPool; +bitflags! { + struct Flags: u8 { + const HEAD = 0b0000_0001; + const UPGRADE = 0b0000_0010; + const KEEPALIVE = 0b0000_0100; + const KEEPALIVE_ENABLED = 0b0001_0000; + } +} + +const AVERAGE_HEADER_SIZE: usize = 30; + /// Http response pub enum OutMessage { /// Http response message @@ -26,91 +37,40 @@ pub enum OutMessage { /// HTTP/1 Codec pub struct Codec { decoder: H1Decoder, - encoder: H1Writer, - head: bool, version: Version, -} -impl Codec { - /// Create HTTP/1 codec - pub fn new() -> Self { - Codec::with_pool(RequestPool::pool()) - } - - /// Create HTTP/1 codec with request's pool - pub(crate) fn with_pool(pool: &'static RequestPool) -> Self { - Codec { - decoder: H1Decoder::with_pool(pool), - encoder: H1Writer::new(), - head: false, - version: Version::HTTP_11, - } - } -} - -impl Decoder for Codec { - type Item = InMessage; - type Error = ParseError; - - fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - let res = self.decoder.decode(src); - - match res { - Ok(Some(InMessage::Message(ref req))) - | Ok(Some(InMessage::MessageWithPayload(ref req))) => { - self.head = req.inner.method == Method::HEAD; - self.version = req.inner.version; - } - _ => (), - } - res - } -} - -impl Encoder for Codec { - type Item = OutMessage; - type Error = io::Error; - - fn encode( - &mut self, item: Self::Item, dst: &mut BytesMut, - ) -> Result<(), Self::Error> { - match item { - OutMessage::Response(res) => { - self.encoder.encode(res, dst, self.head, self.version)?; - } - OutMessage::Payload(bytes) => { - dst.extend_from_slice(&bytes); - } - } - Ok(()) - } -} - -bitflags! { - struct Flags: u8 { - const STARTED = 0b0000_0001; - const UPGRADE = 0b0000_0010; - const KEEPALIVE = 0b0000_0100; - const DISCONNECTED = 0b0000_1000; - } -} - -const AVERAGE_HEADER_SIZE: usize = 30; - -struct H1Writer { + // encoder part flags: Flags, written: u64, headers_size: u32, - info: ResponseInfo, + te: ResponseEncoder, } -impl H1Writer { - fn new() -> H1Writer { - H1Writer { - flags: Flags::empty(), +impl Codec { + /// Create HTTP/1 codec. + /// + /// `keepalive_enabled` how response `connection` header get generated. + pub fn new(keepalive_enabled: bool) -> Self { + Codec::with_pool(RequestPool::pool(), keepalive_enabled) + } + + /// Create HTTP/1 codec with request's pool + pub(crate) fn with_pool( + pool: &'static RequestPool, keepalive_enabled: bool, + ) -> Self { + let flags = if keepalive_enabled { + Flags::KEEPALIVE_ENABLED + } else { + Flags::empty() + }; + Codec { + decoder: H1Decoder::with_pool(pool), + version: Version::HTTP_11, + + flags, written: 0, headers_size: 0, - info: ResponseInfo::default(), + te: ResponseEncoder::default(), } } @@ -118,46 +78,42 @@ impl H1Writer { self.written } - pub fn reset(&mut self) { - self.written = 0; - self.flags = Flags::KEEPALIVE; - } - pub fn upgrade(&self) -> bool { self.flags.contains(Flags::UPGRADE) } pub fn keepalive(&self) -> bool { - self.flags.contains(Flags::KEEPALIVE) && !self.flags.contains(Flags::UPGRADE) + self.flags.contains(Flags::KEEPALIVE) } - fn encode( - &mut self, mut msg: HttpResponse, buffer: &mut BytesMut, head: bool, - version: Version, + fn encode_response( + &mut self, mut msg: HttpResponse, buffer: &mut BytesMut, ) -> io::Result<()> { - // prepare task - self.info.update(&mut msg, head, version); + // prepare transfer encoding + self.te + .update(&mut msg, self.flags.contains(Flags::HEAD), self.version); - //if msg.keep_alive().unwrap_or_else(|| req.keep_alive()) { - //self.flags = Flags::STARTED | Flags::KEEPALIVE; - //} else { - self.flags = Flags::STARTED; - //} + let ka = self.flags.contains(Flags::KEEPALIVE_ENABLED) && msg + .keep_alive() + .unwrap_or_else(|| self.flags.contains(Flags::KEEPALIVE)); // Connection upgrade - let version = msg.version().unwrap_or_else(|| Version::HTTP_11); //req.inner.version); + let version = msg.version().unwrap_or_else(|| self.version); if msg.upgrade() { self.flags.insert(Flags::UPGRADE); + self.flags.remove(Flags::KEEPALIVE); msg.headers_mut() .insert(CONNECTION, HeaderValue::from_static("upgrade")); } // keep-alive - else if self.flags.contains(Flags::KEEPALIVE) { + else if ka { + self.flags.insert(Flags::KEEPALIVE); if version < Version::HTTP_11 { msg.headers_mut() .insert(CONNECTION, HeaderValue::from_static("keep-alive")); } } else if version >= Version::HTTP_11 { + self.flags.remove(Flags::KEEPALIVE); msg.headers_mut() .insert(CONNECTION, HeaderValue::from_static("close")); } @@ -183,7 +139,7 @@ impl H1Writer { buffer.extend_from_slice(reason); // content length - match self.info.length { + match self.te.length { ResponseLength::Chunked => { buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") } @@ -209,7 +165,7 @@ impl H1Writer { for (key, value) in msg.headers() { match *key { TRANSFER_ENCODING => continue, - CONTENT_LENGTH => match self.info.length { + CONTENT_LENGTH => match self.te.length { ResponseLength::None => (), _ => continue, }, @@ -272,3 +228,46 @@ impl H1Writer { Ok(()) } } + +impl Decoder for Codec { + type Item = InMessage; + type Error = ParseError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + let res = self.decoder.decode(src); + + match res { + Ok(Some(InMessage::Message(ref req))) + | Ok(Some(InMessage::MessageWithPayload(ref req))) => { + self.flags + .set(Flags::HEAD, req.inner.method == Method::HEAD); + self.version = req.inner.version; + if self.flags.contains(Flags::KEEPALIVE_ENABLED) { + self.flags.set(Flags::KEEPALIVE, req.keep_alive()); + } + } + _ => (), + } + res + } +} + +impl Encoder for Codec { + type Item = OutMessage; + type Error = io::Error; + + fn encode( + &mut self, item: Self::Item, dst: &mut BytesMut, + ) -> Result<(), Self::Error> { + match item { + OutMessage::Response(res) => { + self.written = 0; + self.encode_response(res, dst)?; + } + OutMessage::Payload(bytes) => { + dst.extend_from_slice(&bytes); + } + } + Ok(()) + } +} diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 2b524556e..183094028 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -6,7 +6,6 @@ use std::time::Instant; use actix_net::service::Service; use futures::{Async, AsyncSink, Future, Poll, Sink, Stream}; -use tokio_codec::Framed; // use tokio_current_thread::spawn; use tokio_io::{AsyncRead, AsyncWrite}; use tokio_timer::Delay; @@ -17,6 +16,7 @@ use payload::{Payload, PayloadSender, PayloadStatus, PayloadWriter}; use body::Body; use config::ServiceConfig; use error::DispatchError; +use framed::Framed; use httpresponse::HttpResponse; use request::Request; @@ -89,12 +89,13 @@ where pub fn with_timeout( stream: T, config: ServiceConfig, timeout: Option, service: S, ) -> Self { - let flags = if config.keep_alive_enabled() { + let keepalive = config.keep_alive_enabled(); + let flags = if keepalive { Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED | Flags::FLUSHED } else { Flags::FLUSHED }; - let framed = Framed::new(stream, Codec::new()); + let framed = Framed::new(stream, Codec::new(keepalive)); let (ka_expire, ka_timer) = if let Some(delay) = timeout { (delay.deadline(), Some(delay)) @@ -235,6 +236,10 @@ where let msg = item.take().expect("SendResponse is empty"); match self.framed.start_send(msg) { Ok(AsyncSink::Ready) => { + self.flags.set( + Flags::KEEPALIVE, + self.framed.get_codec().keepalive(), + ); self.flags.remove(Flags::FLUSHED); Some(Ok(State::None)) } @@ -249,6 +254,10 @@ where let (msg, body) = item.take().expect("SendResponse is empty"); match self.framed.start_send(msg) { Ok(AsyncSink::Ready) => { + self.flags.set( + Flags::KEEPALIVE, + self.framed.get_codec().keepalive(), + ); self.flags.remove(Flags::FLUSHED); Some(Ok(State::Payload(body))) } diff --git a/src/h1/response.rs b/src/h1/encoder.rs similarity index 98% rename from src/h1/response.rs rename to src/h1/encoder.rs index fc6886840..d17587358 100644 --- a/src/h1/response.rs +++ b/src/h1/encoder.rs @@ -24,15 +24,15 @@ pub(crate) enum ResponseLength { } #[derive(Debug)] -pub(crate) struct ResponseInfo { +pub(crate) struct ResponseEncoder { head: bool, pub length: ResponseLength, pub te: TransferEncoding, } -impl Default for ResponseInfo { +impl Default for ResponseEncoder { fn default() -> Self { - ResponseInfo { + ResponseEncoder { head: false, length: ResponseLength::None, te: TransferEncoding::empty(), @@ -40,7 +40,7 @@ impl Default for ResponseInfo { } } -impl ResponseInfo { +impl ResponseEncoder { pub fn update(&mut self, resp: &mut HttpResponse, head: bool, version: Version) { self.head = head; diff --git a/src/h1/mod.rs b/src/h1/mod.rs index 1653227be..f9abfea5c 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -2,7 +2,7 @@ mod codec; mod decoder; mod dispatcher; -mod response; +mod encoder; mod service; pub use self::codec::{Codec, InMessage, OutMessage}; diff --git a/src/lib.rs b/src/lib.rs index accad2fbb..0544569af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -135,6 +135,9 @@ mod payload; mod request; mod uri; +#[doc(hidden)] +pub mod framed; + pub mod error; pub mod h1; pub(crate) mod helpers; From d53f3d718793aa9f7aed0feca1f6de74b970a4f7 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 5 Oct 2018 10:19:15 -0700 Subject: [PATCH 015/427] re-enable websockets --- src/lib.rs | 2 +- src/ws/client.rs | 602 ----------------------------------------------- src/ws/mod.rs | 71 +----- 3 files changed, 6 insertions(+), 669 deletions(-) delete mode 100644 src/ws/client.rs diff --git a/src/lib.rs b/src/lib.rs index 0544569af..ae5a9f957 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -142,7 +142,7 @@ pub mod error; pub mod h1; pub(crate) mod helpers; pub mod test; -//pub mod ws; +pub mod ws; pub use body::{Binary, Body}; pub use error::{Error, ResponseError, Result}; pub use extensions::Extensions; diff --git a/src/ws/client.rs b/src/ws/client.rs deleted file mode 100644 index 18789fef8..000000000 --- a/src/ws/client.rs +++ /dev/null @@ -1,602 +0,0 @@ -//! Http client request -use std::cell::RefCell; -use std::rc::Rc; -use std::time::Duration; -use std::{fmt, io, str}; - -use base64; -use bytes::Bytes; -use cookie::Cookie; -use futures::sync::mpsc::{unbounded, UnboundedSender}; -use futures::{Async, Future, Poll, Stream}; -use http::header::{self, HeaderName, HeaderValue}; -use http::{Error as HttpError, HttpTryFrom, StatusCode}; -use rand; -use sha1::Sha1; - -use actix::{Addr, SystemService}; - -use body::{Binary, Body}; -use error::{Error, UrlParseError}; -use header::IntoHeaderValue; -use httpmessage::HttpMessage; -use payload::PayloadBuffer; - -use client::{ - ClientConnector, ClientRequest, ClientRequestBuilder, HttpResponseParserError, - Pipeline, SendRequest, SendRequestError, -}; - -use super::frame::{Frame, FramedMessage}; -use super::proto::{CloseReason, OpCode}; -use super::{Message, ProtocolError, WsWriter}; - -/// Websocket client error -#[derive(Fail, Debug)] -pub enum ClientError { - /// Invalid url - #[fail(display = "Invalid url")] - InvalidUrl, - /// Invalid response status - #[fail(display = "Invalid response status")] - InvalidResponseStatus(StatusCode), - /// Invalid upgrade header - #[fail(display = "Invalid upgrade header")] - InvalidUpgradeHeader, - /// Invalid connection header - #[fail(display = "Invalid connection header")] - InvalidConnectionHeader(HeaderValue), - /// Missing CONNECTION header - #[fail(display = "Missing CONNECTION header")] - MissingConnectionHeader, - /// Missing SEC-WEBSOCKET-ACCEPT header - #[fail(display = "Missing SEC-WEBSOCKET-ACCEPT header")] - MissingWebSocketAcceptHeader, - /// Invalid challenge response - #[fail(display = "Invalid challenge response")] - InvalidChallengeResponse(String, HeaderValue), - /// Http parsing error - #[fail(display = "Http parsing error")] - Http(Error), - /// Url parsing error - #[fail(display = "Url parsing error")] - Url(UrlParseError), - /// Response parsing error - #[fail(display = "Response parsing error")] - ResponseParseError(HttpResponseParserError), - /// Send request error - #[fail(display = "{}", _0)] - SendRequest(SendRequestError), - /// Protocol error - #[fail(display = "{}", _0)] - Protocol(#[cause] ProtocolError), - /// IO Error - #[fail(display = "{}", _0)] - Io(io::Error), - /// "Disconnected" - #[fail(display = "Disconnected")] - Disconnected, -} - -impl From for ClientError { - fn from(err: Error) -> ClientError { - ClientError::Http(err) - } -} - -impl From for ClientError { - fn from(err: UrlParseError) -> ClientError { - ClientError::Url(err) - } -} - -impl From for ClientError { - fn from(err: SendRequestError) -> ClientError { - ClientError::SendRequest(err) - } -} - -impl From for ClientError { - fn from(err: ProtocolError) -> ClientError { - ClientError::Protocol(err) - } -} - -impl From for ClientError { - fn from(err: io::Error) -> ClientError { - ClientError::Io(err) - } -} - -impl From for ClientError { - fn from(err: HttpResponseParserError) -> ClientError { - ClientError::ResponseParseError(err) - } -} - -/// `WebSocket` client -/// -/// Example of `WebSocket` client usage is available in -/// [websocket example]( -/// https://github.com/actix/examples/blob/master/websocket/src/client.rs#L24) -pub struct Client { - request: ClientRequestBuilder, - err: Option, - http_err: Option, - origin: Option, - protocols: Option, - conn: Addr, - max_size: usize, - no_masking: bool, -} - -impl Client { - /// Create new websocket connection - pub fn new>(uri: S) -> Client { - Client::with_connector(uri, ClientConnector::from_registry()) - } - - /// Create new websocket connection with custom `ClientConnector` - pub fn with_connector>(uri: S, conn: Addr) -> Client { - let mut cl = Client { - request: ClientRequest::build(), - err: None, - http_err: None, - origin: None, - protocols: None, - max_size: 65_536, - no_masking: false, - conn, - }; - cl.request.uri(uri.as_ref()); - cl - } - - /// Set supported websocket protocols - pub fn protocols(mut self, protos: U) -> Self - where - U: IntoIterator + 'static, - V: AsRef, - { - let mut protos = protos - .into_iter() - .fold(String::new(), |acc, s| acc + s.as_ref() + ","); - protos.pop(); - self.protocols = Some(protos); - self - } - - /// Set cookie for handshake request - pub fn cookie(mut self, cookie: Cookie) -> Self { - self.request.cookie(cookie); - self - } - - /// Set request Origin - pub fn origin(mut self, origin: V) -> Self - where - HeaderValue: HttpTryFrom, - { - match HeaderValue::try_from(origin) { - Ok(value) => self.origin = Some(value), - Err(e) => self.http_err = Some(e.into()), - } - self - } - - /// Set max frame size - /// - /// By default max size is set to 64kb - pub fn max_frame_size(mut self, size: usize) -> Self { - self.max_size = size; - self - } - - /// Set write buffer capacity - /// - /// Default buffer capacity is 32kb - pub fn write_buffer_capacity(mut self, cap: usize) -> Self { - self.request.write_buffer_capacity(cap); - self - } - - /// Disable payload masking. By default ws client masks frame payload. - pub fn no_masking(mut self) -> Self { - self.no_masking = true; - self - } - - /// Set request header - pub fn header(mut self, key: K, value: V) -> Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - self.request.header(key, value); - self - } - - /// Set websocket handshake timeout - /// - /// Handshake timeout is a total time for successful handshake. - /// Default value is 5 seconds. - pub fn timeout(mut self, timeout: Duration) -> Self { - self.request.timeout(timeout); - self - } - - /// Connect to websocket server and do ws handshake - pub fn connect(&mut self) -> ClientHandshake { - if let Some(e) = self.err.take() { - ClientHandshake::error(e) - } else if let Some(e) = self.http_err.take() { - ClientHandshake::error(Error::from(e).into()) - } else { - // origin - if let Some(origin) = self.origin.take() { - self.request.set_header(header::ORIGIN, origin); - } - - self.request.upgrade(); - self.request.set_header(header::UPGRADE, "websocket"); - self.request.set_header(header::CONNECTION, "upgrade"); - self.request.set_header(header::SEC_WEBSOCKET_VERSION, "13"); - self.request.with_connector(self.conn.clone()); - - if let Some(protocols) = self.protocols.take() { - self.request - .set_header(header::SEC_WEBSOCKET_PROTOCOL, protocols.as_str()); - } - let request = match self.request.finish() { - Ok(req) => req, - Err(err) => return ClientHandshake::error(err.into()), - }; - - if request.uri().host().is_none() { - return ClientHandshake::error(ClientError::InvalidUrl); - } - if let Some(scheme) = request.uri().scheme_part() { - if scheme != "http" - && scheme != "https" - && scheme != "ws" - && scheme != "wss" - { - return ClientHandshake::error(ClientError::InvalidUrl); - } - } else { - return ClientHandshake::error(ClientError::InvalidUrl); - } - - // start handshake - ClientHandshake::new(request, self.max_size, self.no_masking) - } - } -} - -struct Inner { - tx: UnboundedSender, - rx: PayloadBuffer>, - closed: bool, -} - -/// Future that implementes client websocket handshake process. -/// -/// It resolves to a pair of `ClientReader` and `ClientWriter` that -/// can be used for reading and writing websocket frames. -pub struct ClientHandshake { - request: Option, - tx: Option>, - key: String, - error: Option, - max_size: usize, - no_masking: bool, -} - -impl ClientHandshake { - fn new( - mut request: ClientRequest, max_size: usize, no_masking: bool, - ) -> ClientHandshake { - // Generate a random key for the `Sec-WebSocket-Key` header. - // a base64-encoded (see Section 4 of [RFC4648]) value that, - // when decoded, is 16 bytes in length (RFC 6455) - let sec_key: [u8; 16] = rand::random(); - let key = base64::encode(&sec_key); - - request.headers_mut().insert( - header::SEC_WEBSOCKET_KEY, - HeaderValue::try_from(key.as_str()).unwrap(), - ); - - let (tx, rx) = unbounded(); - request.set_body(Body::Streaming(Box::new(rx.map_err(|_| { - io::Error::new(io::ErrorKind::Other, "disconnected").into() - })))); - - ClientHandshake { - key, - max_size, - no_masking, - request: Some(request.send()), - tx: Some(tx), - error: None, - } - } - - fn error(err: ClientError) -> ClientHandshake { - ClientHandshake { - key: String::new(), - request: None, - tx: None, - error: Some(err), - max_size: 0, - no_masking: false, - } - } - - /// Set handshake timeout - /// - /// Handshake timeout is a total time before handshake should be completed. - /// Default value is 5 seconds. - pub fn timeout(mut self, timeout: Duration) -> Self { - if let Some(request) = self.request.take() { - self.request = Some(request.timeout(timeout)); - } - self - } - - /// Set connection timeout - /// - /// Connection timeout includes resolving hostname and actual connection to - /// the host. - /// Default value is 1 second. - pub fn conn_timeout(mut self, timeout: Duration) -> Self { - if let Some(request) = self.request.take() { - self.request = Some(request.conn_timeout(timeout)); - } - self - } -} - -impl Future for ClientHandshake { - type Item = (ClientReader, ClientWriter); - type Error = ClientError; - - fn poll(&mut self) -> Poll { - if let Some(err) = self.error.take() { - return Err(err); - } - - let resp = match self.request.as_mut().unwrap().poll()? { - Async::Ready(response) => { - self.request.take(); - response - } - Async::NotReady => return Ok(Async::NotReady), - }; - - // verify response - if resp.status() != StatusCode::SWITCHING_PROTOCOLS { - return Err(ClientError::InvalidResponseStatus(resp.status())); - } - // Check for "UPGRADE" to websocket header - let has_hdr = if let Some(hdr) = resp.headers().get(header::UPGRADE) { - if let Ok(s) = hdr.to_str() { - s.to_lowercase().contains("websocket") - } else { - false - } - } else { - false - }; - if !has_hdr { - trace!("Invalid upgrade header"); - return Err(ClientError::InvalidUpgradeHeader); - } - // Check for "CONNECTION" header - if let Some(conn) = resp.headers().get(header::CONNECTION) { - if let Ok(s) = conn.to_str() { - if !s.to_lowercase().contains("upgrade") { - trace!("Invalid connection header: {}", s); - return Err(ClientError::InvalidConnectionHeader(conn.clone())); - } - } else { - trace!("Invalid connection header: {:?}", conn); - return Err(ClientError::InvalidConnectionHeader(conn.clone())); - } - } else { - trace!("Missing connection header"); - return Err(ClientError::MissingConnectionHeader); - } - - if let Some(key) = resp.headers().get(header::SEC_WEBSOCKET_ACCEPT) { - // field is constructed by concatenating /key/ - // with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (RFC 6455) - const WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - let mut sha1 = Sha1::new(); - sha1.update(self.key.as_ref()); - sha1.update(WS_GUID); - let encoded = base64::encode(&sha1.digest().bytes()); - if key.as_bytes() != encoded.as_bytes() { - trace!( - "Invalid challenge response: expected: {} received: {:?}", - encoded, - key - ); - return Err(ClientError::InvalidChallengeResponse(encoded, key.clone())); - } - } else { - trace!("Missing SEC-WEBSOCKET-ACCEPT header"); - return Err(ClientError::MissingWebSocketAcceptHeader); - }; - - let inner = Inner { - tx: self.tx.take().unwrap(), - rx: PayloadBuffer::new(resp.payload()), - closed: false, - }; - - let inner = Rc::new(RefCell::new(inner)); - Ok(Async::Ready(( - ClientReader { - inner: Rc::clone(&inner), - max_size: self.max_size, - no_masking: self.no_masking, - }, - ClientWriter { inner }, - ))) - } -} - -/// Websocket reader client -pub struct ClientReader { - inner: Rc>, - max_size: usize, - no_masking: bool, -} - -impl fmt::Debug for ClientReader { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "ws::ClientReader()") - } -} - -impl Stream for ClientReader { - type Item = Message; - type Error = ProtocolError; - - fn poll(&mut self) -> Poll, Self::Error> { - let max_size = self.max_size; - let no_masking = self.no_masking; - let mut inner = self.inner.borrow_mut(); - if inner.closed { - return Ok(Async::Ready(None)); - } - - // read - match Frame::parse(&mut inner.rx, no_masking, max_size) { - Ok(Async::Ready(Some(frame))) => { - let (_finished, opcode, payload) = frame.unpack(); - - match opcode { - // continuation is not supported - OpCode::Continue => { - inner.closed = true; - Err(ProtocolError::NoContinuation) - } - OpCode::Bad => { - inner.closed = true; - Err(ProtocolError::BadOpCode) - } - OpCode::Close => { - inner.closed = true; - let close_reason = Frame::parse_close_payload(&payload); - Ok(Async::Ready(Some(Message::Close(close_reason)))) - } - OpCode::Ping => Ok(Async::Ready(Some(Message::Ping( - String::from_utf8_lossy(payload.as_ref()).into(), - )))), - OpCode::Pong => Ok(Async::Ready(Some(Message::Pong( - String::from_utf8_lossy(payload.as_ref()).into(), - )))), - OpCode::Binary => Ok(Async::Ready(Some(Message::Binary(payload)))), - OpCode::Text => { - let tmp = Vec::from(payload.as_ref()); - match String::from_utf8(tmp) { - Ok(s) => Ok(Async::Ready(Some(Message::Text(s)))), - Err(_) => { - inner.closed = true; - Err(ProtocolError::BadEncoding) - } - } - } - } - } - Ok(Async::Ready(None)) => Ok(Async::Ready(None)), - Ok(Async::NotReady) => Ok(Async::NotReady), - Err(e) => { - inner.closed = true; - Err(e) - } - } - } -} - -/// Websocket writer client -pub struct ClientWriter { - inner: Rc>, -} - -impl ClientWriter { - /// Write payload - #[inline] - fn write(&mut self, mut data: FramedMessage) { - let inner = self.inner.borrow_mut(); - if !inner.closed { - let _ = inner.tx.unbounded_send(data.0.take()); - } else { - warn!("Trying to write to disconnected response"); - } - } - - /// Send text frame - #[inline] - pub fn text>(&mut self, text: T) { - self.write(Frame::message(text.into(), OpCode::Text, true, true)); - } - - /// Send binary frame - #[inline] - pub fn binary>(&mut self, data: B) { - self.write(Frame::message(data, OpCode::Binary, true, true)); - } - - /// Send ping frame - #[inline] - pub fn ping(&mut self, message: &str) { - self.write(Frame::message(Vec::from(message), OpCode::Ping, true, true)); - } - - /// Send pong frame - #[inline] - pub fn pong(&mut self, message: &str) { - self.write(Frame::message(Vec::from(message), OpCode::Pong, true, true)); - } - - /// Send close frame - #[inline] - pub fn close(&mut self, reason: Option) { - self.write(Frame::close(reason, true)); - } -} - -impl WsWriter for ClientWriter { - /// Send text frame - #[inline] - fn send_text>(&mut self, text: T) { - self.text(text) - } - - /// Send binary frame - #[inline] - fn send_binary>(&mut self, data: B) { - self.binary(data) - } - - /// Send ping frame - #[inline] - fn send_ping(&mut self, message: &str) { - self.ping(message) - } - - /// Send pong frame - #[inline] - fn send_pong(&mut self, message: &str) { - self.pong(message) - } - - /// Send close frame - #[inline] - fn send_close(&mut self, reason: Option) { - self.close(reason); - } -} diff --git a/src/ws/mod.rs b/src/ws/mod.rs index c16f8d6d2..6bb84c189 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -1,69 +1,23 @@ -//! `WebSocket` support for Actix +//! `WebSocket` support. //! //! To setup a `WebSocket`, first do web socket handshake then on success //! convert `Payload` into a `WsStream` stream and then use `WsWriter` to //! communicate with the peer. -//! -//! ## Example -//! -//! ```rust -//! # extern crate actix_web; -//! # use actix_web::actix::*; -//! # use actix_web::*; -//! use actix_web::{ws, HttpRequest, HttpResponse}; -//! -//! // do websocket handshake and start actor -//! fn ws_index(req: &HttpRequest) -> Result { -//! ws::start(req, Ws) -//! } -//! -//! struct Ws; -//! -//! impl Actor for Ws { -//! type Context = ws::WebsocketContext; -//! } -//! -//! // Handler for ws::Message messages -//! impl StreamHandler for Ws { -//! fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { -//! match msg { -//! ws::Message::Ping(msg) => ctx.pong(&msg), -//! ws::Message::Text(text) => ctx.text(text), -//! ws::Message::Binary(bin) => ctx.binary(bin), -//! _ => (), -//! } -//! } -//! } -//! # -//! # fn main() { -//! # App::new() -//! # .resource("/ws/", |r| r.f(ws_index)) // <- register websocket route -//! # .finish(); -//! # } //! ``` use bytes::Bytes; use futures::{Async, Poll, Stream}; use http::{header, Method, StatusCode}; -use super::actix::{Actor, StreamHandler}; - use body::Binary; -use error::{Error, PayloadError, ResponseError}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; +use error::{PayloadError, ResponseError}; use httpresponse::{ConnectionType, HttpResponse, HttpResponseBuilder}; use payload::PayloadBuffer; +use request::Request; -mod client; -mod context; mod frame; mod mask; mod proto; -pub use self::client::{ - Client, ClientError, ClientHandshake, ClientReader, ClientWriter, -}; -pub use self::context::WebsocketContext; pub use self::frame::{Frame, FramedMessage}; pub use self::proto::{CloseCode, CloseReason, OpCode}; @@ -156,7 +110,7 @@ impl ResponseError for HandshakeError { } /// `WebSocket` Message -#[derive(Debug, PartialEq, Message)] +#[derive(Debug, PartialEq)] pub enum Message { /// Text message Text(String), @@ -170,19 +124,6 @@ pub enum Message { Close(Option), } -/// Do websocket handshake and start actor -pub fn start(req: &HttpRequest, actor: A) -> Result -where - A: Actor> + StreamHandler, - S: 'static, -{ - let mut resp = handshake(req)?; - let stream = WsStream::new(req.payload()); - - let body = WebsocketContext::create(req.clone(), actor, stream); - Ok(resp.body(body)) -} - /// Prepare `WebSocket` handshake response. /// /// This function returns handshake `HttpResponse`, ready to send to peer. @@ -191,9 +132,7 @@ where // /// `protocols` is a sequence of known protocols. On successful handshake, // /// the returned response headers contain the first protocol in this list // /// which the server also knows. -pub fn handshake( - req: &HttpRequest, -) -> Result { +pub fn handshake(req: &Request) -> Result { // WebSocket accepts only GET if *req.method() != Method::GET { return Err(HandshakeError::GetMethodRequired); From 8c2244dd889ce4a1f2e216b379661aa3a22806e5 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 5 Oct 2018 11:04:59 -0700 Subject: [PATCH 016/427] rename HttpResponse --- README.md | 2 +- src/error.rs | 133 +++++++------- src/h1/codec.rs | 6 +- src/h1/decoder.rs | 2 +- src/h1/dispatcher.rs | 8 +- src/h1/encoder.rs | 6 +- src/h1/service.rs | 10 +- src/httpcodes.rs | 12 +- src/httpmessage.rs | 16 +- src/json.rs | 6 +- src/lib.rs | 12 +- src/payload.rs | 2 +- src/{httpresponse.rs => response.rs} | 260 +++++++++++++-------------- src/test.rs | 22 +-- src/ws/mod.rs | 52 ++---- tests/test_h1v2.rs | 4 +- 16 files changed, 266 insertions(+), 287 deletions(-) rename src/{httpresponse.rs => response.rs} (81%) diff --git a/README.md b/README.md index 7ddd532e8..b273ea8c5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Actix http [![Build Status](https://travis-ci.org/fafhrd91/actix-http.svg?branch=master)](https://travis-ci.org/fafhrd91/actix-http) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# Actix http [![Build Status](https://travis-ci.org/fafhrd91/actix-http.svg?branch=master)](https://travis-ci.org/fafhrd91/actix-http) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/fafhrd91/actix-http/branch/master/graph/badge.svg)](https://codecov.io/gh/fafhrd91/actix-http) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Actix http diff --git a/src/error.rs b/src/error.rs index fb5df2328..dc2d45b84 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,8 +21,7 @@ pub use url::ParseError as UrlParseError; // re-exports pub use cookie::ParseError as CookieParseError; -// use httprequest::HttpRequest; -use httpresponse::{HttpResponse, HttpResponseParts}; +use response::{Response, ResponseParts}; /// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html) /// for actix web operations @@ -124,13 +123,13 @@ impl InternalResponseErrorAsFail for T { } } -/// Error that can be converted to `HttpResponse` +/// Error that can be converted to `Response` pub trait ResponseError: Fail + InternalResponseErrorAsFail { /// Create response for error /// /// Internal server error is generated by default. - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR) + fn error_response(&self) -> Response { + Response::new(StatusCode::INTERNAL_SERVER_ERROR) } } @@ -155,10 +154,10 @@ impl fmt::Debug for Error { } } -/// Convert `Error` to a `HttpResponse` instance -impl From for HttpResponse { +/// Convert `Error` to a `Response` instance +impl From for Response { fn from(err: Error) -> Self { - HttpResponse::from_error(err) + Response::from_error(err) } } @@ -202,15 +201,15 @@ impl ResponseError for UrlParseError {} /// Return `BAD_REQUEST` for `de::value::Error` impl ResponseError for DeError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) + fn error_response(&self) -> Response { + Response::new(StatusCode::BAD_REQUEST) } } /// Return `BAD_REQUEST` for `Utf8Error` impl ResponseError for Utf8Error { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) + fn error_response(&self) -> Response { + Response::new(StatusCode::BAD_REQUEST) } } @@ -220,26 +219,26 @@ impl ResponseError for HttpError {} /// Return `InternalServerError` for `io::Error` impl ResponseError for io::Error { - fn error_response(&self) -> HttpResponse { + fn error_response(&self) -> Response { match self.kind() { - io::ErrorKind::NotFound => HttpResponse::new(StatusCode::NOT_FOUND), - io::ErrorKind::PermissionDenied => HttpResponse::new(StatusCode::FORBIDDEN), - _ => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR), + io::ErrorKind::NotFound => Response::new(StatusCode::NOT_FOUND), + io::ErrorKind::PermissionDenied => Response::new(StatusCode::FORBIDDEN), + _ => Response::new(StatusCode::INTERNAL_SERVER_ERROR), } } } /// `BadRequest` for `InvalidHeaderValue` impl ResponseError for header::InvalidHeaderValue { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) + fn error_response(&self) -> Response { + Response::new(StatusCode::BAD_REQUEST) } } /// `BadRequest` for `InvalidHeaderValue` impl ResponseError for header::InvalidHeaderValueBytes { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) + fn error_response(&self) -> Response { + Response::new(StatusCode::BAD_REQUEST) } } @@ -288,8 +287,8 @@ pub enum ParseError { /// Return `BadRequest` for `ParseError` impl ResponseError for ParseError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) + fn error_response(&self) -> Response { + Response::new(StatusCode::BAD_REQUEST) } } @@ -362,18 +361,18 @@ impl From for PayloadError { /// - `Overflow` returns `PayloadTooLarge` /// - Other errors returns `BadRequest` impl ResponseError for PayloadError { - fn error_response(&self) -> HttpResponse { + fn error_response(&self) -> Response { match *self { - PayloadError::Overflow => HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE), - _ => HttpResponse::new(StatusCode::BAD_REQUEST), + PayloadError::Overflow => Response::new(StatusCode::PAYLOAD_TOO_LARGE), + _ => Response::new(StatusCode::BAD_REQUEST), } } } /// Return `BadRequest` for `cookie::ParseError` impl ResponseError for cookie::ParseError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) + fn error_response(&self) -> Response { + Response::new(StatusCode::BAD_REQUEST) } } @@ -443,8 +442,8 @@ pub enum ContentTypeError { /// Return `BadRequest` for `ContentTypeError` impl ResponseError for ContentTypeError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) + fn error_response(&self) -> Response { + Response::new(StatusCode::BAD_REQUEST) } } @@ -475,15 +474,11 @@ pub enum UrlencodedError { /// Return `BadRequest` for `UrlencodedError` impl ResponseError for UrlencodedError { - fn error_response(&self) -> HttpResponse { + fn error_response(&self) -> Response { match *self { - UrlencodedError::Overflow => { - HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) - } - UrlencodedError::UnknownLength => { - HttpResponse::new(StatusCode::LENGTH_REQUIRED) - } - _ => HttpResponse::new(StatusCode::BAD_REQUEST), + UrlencodedError::Overflow => Response::new(StatusCode::PAYLOAD_TOO_LARGE), + UrlencodedError::UnknownLength => Response::new(StatusCode::LENGTH_REQUIRED), + _ => Response::new(StatusCode::BAD_REQUEST), } } } @@ -513,12 +508,10 @@ pub enum JsonPayloadError { /// Return `BadRequest` for `UrlencodedError` impl ResponseError for JsonPayloadError { - fn error_response(&self) -> HttpResponse { + fn error_response(&self) -> Response { match *self { - JsonPayloadError::Overflow => { - HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) - } - _ => HttpResponse::new(StatusCode::BAD_REQUEST), + JsonPayloadError::Overflow => Response::new(StatusCode::PAYLOAD_TOO_LARGE), + _ => Response::new(StatusCode::BAD_REQUEST), } } } @@ -606,7 +599,7 @@ pub struct InternalError { enum InternalErrorType { Status(StatusCode), - Response(Box>>), + Response(Box>>), } impl InternalError { @@ -619,8 +612,8 @@ impl InternalError { } } - /// Create `InternalError` with predefined `HttpResponse`. - pub fn from_response(cause: T, response: HttpResponse) -> Self { + /// Create `InternalError` with predefined `Response`. + pub fn from_response(cause: T, response: Response) -> Self { let resp = response.into_parts(); InternalError { cause, @@ -661,14 +654,14 @@ impl ResponseError for InternalError where T: Send + Sync + fmt::Debug + fmt::Display + 'static, { - fn error_response(&self) -> HttpResponse { + fn error_response(&self) -> Response { match self.status { - InternalErrorType::Status(st) => HttpResponse::new(st), + InternalErrorType::Status(st) => Response::new(st), InternalErrorType::Response(ref resp) => { if let Some(resp) = resp.lock().unwrap().take() { - HttpResponse::from_parts(resp) + Response::from_parts(resp) } else { - HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR) + Response::new(StatusCode::INTERNAL_SERVER_ERROR) } } } @@ -838,14 +831,14 @@ mod tests { #[test] fn test_into_response() { - let resp: HttpResponse = ParseError::Incomplete.error_response(); + let resp: Response = ParseError::Incomplete.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: HttpResponse = CookieParseError::EmptyName.error_response(); + let resp: Response = CookieParseError::EmptyName.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into(); - let resp: HttpResponse = err.error_response(); + let resp: Response = err.error_response(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } @@ -883,7 +876,7 @@ mod tests { fn test_error_http_response() { let orig = io::Error::new(io::ErrorKind::Other, "other"); let e = Error::from(orig); - let resp: HttpResponse = e.into(); + let resp: Response = e.into(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } @@ -944,8 +937,8 @@ mod tests { #[test] fn test_internal_error() { let err = - InternalError::from_response(ParseError::Method, HttpResponse::Ok().into()); - let resp: HttpResponse = err.error_response(); + InternalError::from_response(ParseError::Method, Response::Ok().into()); + let resp: Response = err.error_response(); assert_eq!(resp.status(), StatusCode::OK); } @@ -977,49 +970,49 @@ mod tests { #[test] fn test_error_helpers() { - let r: HttpResponse = ErrorBadRequest("err").into(); + let r: Response = ErrorBadRequest("err").into(); assert_eq!(r.status(), StatusCode::BAD_REQUEST); - let r: HttpResponse = ErrorUnauthorized("err").into(); + let r: Response = ErrorUnauthorized("err").into(); assert_eq!(r.status(), StatusCode::UNAUTHORIZED); - let r: HttpResponse = ErrorForbidden("err").into(); + let r: Response = ErrorForbidden("err").into(); assert_eq!(r.status(), StatusCode::FORBIDDEN); - let r: HttpResponse = ErrorNotFound("err").into(); + let r: Response = ErrorNotFound("err").into(); assert_eq!(r.status(), StatusCode::NOT_FOUND); - let r: HttpResponse = ErrorMethodNotAllowed("err").into(); + let r: Response = ErrorMethodNotAllowed("err").into(); assert_eq!(r.status(), StatusCode::METHOD_NOT_ALLOWED); - let r: HttpResponse = ErrorRequestTimeout("err").into(); + let r: Response = ErrorRequestTimeout("err").into(); assert_eq!(r.status(), StatusCode::REQUEST_TIMEOUT); - let r: HttpResponse = ErrorConflict("err").into(); + let r: Response = ErrorConflict("err").into(); assert_eq!(r.status(), StatusCode::CONFLICT); - let r: HttpResponse = ErrorGone("err").into(); + let r: Response = ErrorGone("err").into(); assert_eq!(r.status(), StatusCode::GONE); - let r: HttpResponse = ErrorPreconditionFailed("err").into(); + let r: Response = ErrorPreconditionFailed("err").into(); assert_eq!(r.status(), StatusCode::PRECONDITION_FAILED); - let r: HttpResponse = ErrorExpectationFailed("err").into(); + let r: Response = ErrorExpectationFailed("err").into(); assert_eq!(r.status(), StatusCode::EXPECTATION_FAILED); - let r: HttpResponse = ErrorInternalServerError("err").into(); + let r: Response = ErrorInternalServerError("err").into(); assert_eq!(r.status(), StatusCode::INTERNAL_SERVER_ERROR); - let r: HttpResponse = ErrorNotImplemented("err").into(); + let r: Response = ErrorNotImplemented("err").into(); assert_eq!(r.status(), StatusCode::NOT_IMPLEMENTED); - let r: HttpResponse = ErrorBadGateway("err").into(); + let r: Response = ErrorBadGateway("err").into(); assert_eq!(r.status(), StatusCode::BAD_GATEWAY); - let r: HttpResponse = ErrorServiceUnavailable("err").into(); + let r: Response = ErrorServiceUnavailable("err").into(); assert_eq!(r.status(), StatusCode::SERVICE_UNAVAILABLE); - let r: HttpResponse = ErrorGatewayTimeout("err").into(); + let r: Response = ErrorGatewayTimeout("err").into(); assert_eq!(r.status(), StatusCode::GATEWAY_TIMEOUT); } } diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 40d0a240f..a27e6472c 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -12,8 +12,8 @@ use error::ParseError; use helpers; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::{Method, Version}; -use httpresponse::HttpResponse; use request::RequestPool; +use response::Response; bitflags! { struct Flags: u8 { @@ -29,7 +29,7 @@ const AVERAGE_HEADER_SIZE: usize = 30; /// Http response pub enum OutMessage { /// Http response message - Response(HttpResponse), + Response(Response), /// Payload chunk Payload(Bytes), } @@ -87,7 +87,7 @@ impl Codec { } fn encode_response( - &mut self, mut msg: HttpResponse, buffer: &mut BytesMut, + &mut self, mut msg: Response, buffer: &mut BytesMut, ) -> io::Result<()> { // prepare transfer encoding self.te diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 90946b453..48776226b 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -628,7 +628,7 @@ mod tests { // let buf = Buffer::new("GET /test HTTP/1\r\n\r\n"); // let readbuf = BytesMut::new(); - // let mut h1 = Dispatcher::new(buf, |req| ok(HttpResponse::Ok().finish())); + // let mut h1 = Dispatcher::new(buf, |req| ok(Response::Ok().finish())); // assert!(h1.poll_io().is_ok()); // assert!(h1.poll_io().is_ok()); // assert!(h1.flags.contains(Flags::READ_DISCONNECTED)); diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 183094028..728a78b9f 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -17,8 +17,8 @@ use body::Body; use config::ServiceConfig; use error::DispatchError; use framed::Framed; -use httpresponse::HttpResponse; use request::Request; +use response::Response; use super::codec::{Codec, InMessage, OutMessage}; @@ -77,7 +77,7 @@ impl State { impl Dispatcher where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Error: Debug + Display, { /// Create http/1 dispatcher. @@ -415,7 +415,7 @@ where .insert(Flags::STARTED | Flags::READ_DISCONNECTED); self.state = State::SendResponse(Some(OutMessage::Response( - HttpResponse::RequestTimeout().finish(), + Response::RequestTimeout().finish(), ))); } else { trace!("Keep-alive timeout, close connection"); @@ -452,7 +452,7 @@ where impl Future for Dispatcher where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Error: Debug + Display, { type Item = (); diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index d17587358..1544b2404 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -11,8 +11,8 @@ use http::{StatusCode, Version}; use body::{Binary, Body}; use header::ContentEncoding; use http::Method; -use httpresponse::HttpResponse; use request::Request; +use response::Response; #[derive(Debug)] pub(crate) enum ResponseLength { @@ -41,7 +41,7 @@ impl Default for ResponseEncoder { } impl ResponseEncoder { - pub fn update(&mut self, resp: &mut HttpResponse, head: bool, version: Version) { + pub fn update(&mut self, resp: &mut Response, head: bool, version: Version) { self.head = head; let version = resp.version().unwrap_or_else(|| version); @@ -98,7 +98,7 @@ impl ResponseEncoder { } fn streaming_encoding( - &mut self, version: Version, resp: &mut HttpResponse, + &mut self, version: Version, resp: &mut Response, ) -> TransferEncoding { match resp.chunked() { Some(true) => { diff --git a/src/h1/service.rs b/src/h1/service.rs index 436e77a55..8038c9fb8 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -7,8 +7,8 @@ use tokio_io::{AsyncRead, AsyncWrite}; use config::ServiceConfig; use error::DispatchError; -use httpresponse::HttpResponse; use request::Request; +use response::Response; use super::dispatcher::Dispatcher; @@ -36,7 +36,7 @@ where impl NewService for H1Service where T: AsyncRead + AsyncWrite, - S: NewService + Clone, + S: NewService + Clone, S::Service: Clone, S::Error: Debug + Display, { @@ -65,7 +65,7 @@ pub struct H1ServiceResponse { impl Future for H1ServiceResponse where T: AsyncRead + AsyncWrite, - S: NewService, + S: NewService, S::Service: Clone, S::Error: Debug + Display, { @@ -90,7 +90,7 @@ pub struct H1ServiceHandler { impl H1ServiceHandler where - S: Service + Clone, + S: Service + Clone, S::Error: Debug + Display, { fn new(cfg: ServiceConfig, srv: S) -> H1ServiceHandler { @@ -105,7 +105,7 @@ where impl Service for H1ServiceHandler where T: AsyncRead + AsyncWrite, - S: Service + Clone, + S: Service + Clone, S::Error: Debug + Display, { type Request = T; diff --git a/src/httpcodes.rs b/src/httpcodes.rs index 41e57d1ee..7d42a1cc3 100644 --- a/src/httpcodes.rs +++ b/src/httpcodes.rs @@ -1,18 +1,18 @@ //! Basic http responses #![allow(non_upper_case_globals)] use http::StatusCode; -use httpresponse::{HttpResponse, HttpResponseBuilder}; +use response::{Response, ResponseBuilder}; macro_rules! STATIC_RESP { ($name:ident, $status:expr) => { #[allow(non_snake_case, missing_docs)] - pub fn $name() -> HttpResponseBuilder { - HttpResponse::build($status) + pub fn $name() -> ResponseBuilder { + Response::build($status) } }; } -impl HttpResponse { +impl Response { STATIC_RESP!(Ok, StatusCode::OK); STATIC_RESP!(Created, StatusCode::CREATED); STATIC_RESP!(Accepted, StatusCode::ACCEPTED); @@ -74,11 +74,11 @@ impl HttpResponse { mod tests { use body::Body; use http::StatusCode; - use httpresponse::HttpResponse; + use response::Response; #[test] fn test_build() { - let resp = HttpResponse::Ok().body(Body::Empty); + let resp = Response::Ok().body(Body::Empty); assert_eq!(resp.status(), StatusCode::OK); } } diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 531aa1a72..f68f3650b 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -112,18 +112,18 @@ pub trait HttpMessage: Sized { /// # extern crate futures; /// # #[macro_use] extern crate serde_derive; /// use actix_web::{ - /// AsyncResponder, FutureResponse, HttpMessage, HttpRequest, HttpResponse, + /// AsyncResponder, FutureResponse, HttpMessage, HttpRequest, Response, /// }; /// use bytes::Bytes; /// use futures::future::Future; /// - /// fn index(mut req: HttpRequest) -> FutureResponse { + /// fn index(mut req: HttpRequest) -> FutureResponse { /// req.body() // <- get Body future /// .limit(1024) // <- change max size of the body to a 1kb /// .from_err() /// .and_then(|bytes: Bytes| { // <- complete body /// println!("==== BODY ==== {:?}", bytes); - /// Ok(HttpResponse::Ok().into()) + /// Ok(Response::Ok().into()) /// }).responder() /// } /// # fn main() {} @@ -148,15 +148,15 @@ pub trait HttpMessage: Sized { /// # extern crate futures; /// # use futures::Future; /// # use std::collections::HashMap; - /// use actix_web::{FutureResponse, HttpMessage, HttpRequest, HttpResponse}; + /// use actix_web::{FutureResponse, HttpMessage, HttpRequest, Response}; /// - /// fn index(mut req: HttpRequest) -> FutureResponse { + /// fn index(mut req: HttpRequest) -> FutureResponse { /// Box::new( /// req.urlencoded::>() // <- get UrlEncoded future /// .from_err() /// .and_then(|params| { // <- url encoded parameters /// println!("==== BODY ==== {:?}", params); - /// Ok(HttpResponse::Ok().into()) + /// Ok(Response::Ok().into()) /// }), /// ) /// } @@ -188,12 +188,12 @@ pub trait HttpMessage: Sized { /// name: String, /// } /// - /// fn index(mut req: HttpRequest) -> Box> { + /// fn index(mut req: HttpRequest) -> Box> { /// req.json() // <- get JsonBody future /// .from_err() /// .and_then(|val: MyObj| { // <- deserialized value /// println!("==== BODY ==== {:?}", val); - /// Ok(HttpResponse::Ok().into()) + /// Ok(Response::Ok().into()) /// }).responder() /// } /// # fn main() {} diff --git a/src/json.rs b/src/json.rs index 5c64b9bdd..a52884894 100644 --- a/src/json.rs +++ b/src/json.rs @@ -123,7 +123,7 @@ where /// # extern crate actix_web; /// # extern crate futures; /// # #[macro_use] extern crate serde_derive; -/// use actix_web::{AsyncResponder, Error, HttpMessage, HttpRequest, HttpResponse}; +/// use actix_web::{AsyncResponder, Error, HttpMessage, HttpRequest, Response}; /// use futures::future::Future; /// /// #[derive(Deserialize, Debug)] @@ -131,12 +131,12 @@ where /// name: String, /// } /// -/// fn index(mut req: HttpRequest) -> Box> { +/// fn index(mut req: HttpRequest) -> Box> { /// req.json() // <- get JsonBody future /// .from_err() /// .and_then(|val: MyObj| { // <- deserialized value /// println!("==== BODY ==== {:?}", val); -/// Ok(HttpResponse::Ok().into()) +/// Ok(Response::Ok().into()) /// }).responder() /// } /// # fn main() {} diff --git a/src/lib.rs b/src/lib.rs index ae5a9f957..74e7ced78 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,8 +41,8 @@ //! represents an HTTP server instance and is used to instantiate and //! configure servers. //! -//! * [HttpRequest](struct.HttpRequest.html) and -//! [HttpResponse](struct.HttpResponse.html): These structs +//! * [Request](struct.Request.html) and +//! [Response](struct.Response.html): These structs //! represent HTTP requests and responses and expose various methods //! for inspecting, creating and otherwise utilizing them. //! @@ -129,10 +129,10 @@ mod extensions; mod header; mod httpcodes; mod httpmessage; -mod httpresponse; mod json; mod payload; mod request; +mod response; mod uri; #[doc(hidden)] @@ -147,9 +147,9 @@ pub use body::{Binary, Body}; pub use error::{Error, ResponseError, Result}; pub use extensions::Extensions; pub use httpmessage::HttpMessage; -pub use httpresponse::HttpResponse; pub use json::Json; pub use request::Request; +pub use response::Response; pub use self::config::{KeepAlive, ServiceConfig, ServiceConfigBuilder}; @@ -166,9 +166,9 @@ pub mod dev { pub use body::BodyStream; pub use httpmessage::{MessageBody, Readlines, UrlEncoded}; - pub use httpresponse::HttpResponseBuilder; pub use json::JsonBody; pub use payload::{Payload, PayloadBuffer}; + pub use response::ResponseBuilder; } pub mod http { @@ -187,5 +187,5 @@ pub mod http { pub use header::*; } pub use header::ContentEncoding; - pub use httpresponse::ConnectionType; + pub use response::ConnectionType; } diff --git a/src/payload.rs b/src/payload.rs index 2131e3c3c..3f51f6ec0 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -27,7 +27,7 @@ pub(crate) enum PayloadStatus { /// `.readany()` method. Payload stream is not thread safe. Payload does not /// notify current task when new data is available. /// -/// Payload stream can be used as `HttpResponse` body stream. +/// Payload stream can be used as `Response` body stream. #[derive(Debug)] pub struct Payload { inner: Rc>, diff --git a/src/httpresponse.rs b/src/response.rs similarity index 81% rename from src/httpresponse.rs rename to src/response.rs index 3c034fae3..d0136b408 100644 --- a/src/httpresponse.rs +++ b/src/response.rs @@ -31,54 +31,54 @@ pub enum ConnectionType { } /// An HTTP Response -pub struct HttpResponse(Box, &'static HttpResponsePool); +pub struct Response(Box, &'static ResponsePool); -impl HttpResponse { +impl Response { #[inline] - fn get_ref(&self) -> &InnerHttpResponse { + fn get_ref(&self) -> &InnerResponse { self.0.as_ref() } #[inline] - fn get_mut(&mut self) -> &mut InnerHttpResponse { + fn get_mut(&mut self) -> &mut InnerResponse { self.0.as_mut() } /// Create http response builder with specific status. #[inline] - pub fn build(status: StatusCode) -> HttpResponseBuilder { - HttpResponsePool::get(status) + pub fn build(status: StatusCode) -> ResponseBuilder { + ResponsePool::get(status) } /// Create http response builder #[inline] - pub fn build_from>(source: T) -> HttpResponseBuilder { + pub fn build_from>(source: T) -> ResponseBuilder { source.into() } /// Constructs a response #[inline] - pub fn new(status: StatusCode) -> HttpResponse { - HttpResponsePool::with_body(status, Body::Empty) + pub fn new(status: StatusCode) -> Response { + ResponsePool::with_body(status, Body::Empty) } /// Constructs a response with body #[inline] - pub fn with_body>(status: StatusCode, body: B) -> HttpResponse { - HttpResponsePool::with_body(status, body.into()) + pub fn with_body>(status: StatusCode, body: B) -> Response { + ResponsePool::with_body(status, body.into()) } /// Constructs an error response #[inline] - pub fn from_error(error: Error) -> HttpResponse { + pub fn from_error(error: Error) -> Response { let mut resp = error.as_response_error().error_response(); resp.get_mut().error = Some(error); resp } - /// Convert `HttpResponse` to a `HttpResponseBuilder` + /// Convert `Response` to a `ResponseBuilder` #[inline] - pub fn into_builder(self) -> HttpResponseBuilder { + pub fn into_builder(self) -> ResponseBuilder { // If this response has cookies, load them into a jar let mut jar: Option = None; for c in self.cookies() { @@ -91,7 +91,7 @@ impl HttpResponse { } } - HttpResponseBuilder { + ResponseBuilder { pool: self.1, response: Some(self.0), err: None, @@ -282,23 +282,23 @@ impl HttpResponse { self.1.release(self.0); } - pub(crate) fn into_parts(self) -> HttpResponseParts { + pub(crate) fn into_parts(self) -> ResponseParts { self.0.into_parts() } - pub(crate) fn from_parts(parts: HttpResponseParts) -> HttpResponse { - HttpResponse( - Box::new(InnerHttpResponse::from_parts(parts)), - HttpResponsePool::get_pool(), + pub(crate) fn from_parts(parts: ResponseParts) -> Response { + Response( + Box::new(InnerResponse::from_parts(parts)), + ResponsePool::get_pool(), ) } } -impl fmt::Debug for HttpResponse { +impl fmt::Debug for Response { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let res = writeln!( f, - "\nHttpResponse {:?} {}{}", + "\nResponse {:?} {}{}", self.get_ref().version, self.get_ref().status, self.get_ref().reason.unwrap_or("") @@ -332,16 +332,16 @@ impl<'a> Iterator for CookieIter<'a> { /// An HTTP response builder /// -/// This type can be used to construct an instance of `HttpResponse` through a +/// This type can be used to construct an instance of `Response` through a /// builder-like pattern. -pub struct HttpResponseBuilder { - pool: &'static HttpResponsePool, - response: Option>, +pub struct ResponseBuilder { + pool: &'static ResponsePool, + response: Option>, err: Option, cookies: Option, } -impl HttpResponseBuilder { +impl ResponseBuilder { /// Set HTTP status code of this response. #[inline] pub fn status(&mut self, status: StatusCode) -> &mut Self { @@ -366,10 +366,10 @@ impl HttpResponseBuilder { /// /// ```rust,ignore /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse, Result}; + /// use actix_web::{http, Request, Response, Result}; /// - /// fn index(req: HttpRequest) -> Result { - /// Ok(HttpResponse::Ok() + /// fn index(req: HttpRequest) -> Result { + /// Ok(Response::Ok() /// .set(http::header::IfModifiedSince( /// "Sun, 07 Nov 1994 08:48:37 GMT".parse()?, /// )) @@ -394,10 +394,10 @@ impl HttpResponseBuilder { /// /// ```rust,ignore /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse}; + /// use actix_web::{http, Request, Response}; /// - /// fn index(req: HttpRequest) -> HttpResponse { - /// HttpResponse::Ok() + /// fn index(req: HttpRequest) -> Response { + /// Response::Ok() /// .header("X-TEST", "value") /// .header(http::header::CONTENT_TYPE, "application/json") /// .finish() @@ -516,10 +516,10 @@ impl HttpResponseBuilder { /// /// ```rust,ignore /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse, Result}; + /// use actix_web::{http, HttpRequest, Response, Result}; /// - /// fn index(req: HttpRequest) -> HttpResponse { - /// HttpResponse::Ok() + /// fn index(req: HttpRequest) -> Response { + /// Response::Ok() /// .cookie( /// http::Cookie::build("name", "value") /// .domain("www.rust-lang.org") @@ -546,10 +546,10 @@ impl HttpResponseBuilder { /// /// ```rust,ignore /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse, Result}; + /// use actix_web::{http, HttpRequest, Response, Result}; /// - /// fn index(req: &HttpRequest) -> HttpResponse { - /// let mut builder = HttpResponse::Ok(); + /// fn index(req: &HttpRequest) -> Response { + /// let mut builder = Response::Ok(); /// /// if let Some(ref cookie) = req.cookie("name") { /// builder.del_cookie(cookie); @@ -575,7 +575,7 @@ impl HttpResponseBuilder { /// true. pub fn if_true(&mut self, value: bool, f: F) -> &mut Self where - F: FnOnce(&mut HttpResponseBuilder), + F: FnOnce(&mut ResponseBuilder), { if value { f(self); @@ -587,7 +587,7 @@ impl HttpResponseBuilder { /// Some. pub fn if_some(&mut self, value: Option, f: F) -> &mut Self where - F: FnOnce(T, &mut HttpResponseBuilder), + F: FnOnce(T, &mut ResponseBuilder), { if let Some(val) = value { f(val, self); @@ -609,10 +609,10 @@ impl HttpResponseBuilder { self } - /// Set a body and generate `HttpResponse`. + /// Set a body and generate `Response`. /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn body>(&mut self, body: B) -> HttpResponse { + /// `ResponseBuilder` can not be used after this call. + pub fn body>(&mut self, body: B) -> Response { if let Some(e) = self.err.take() { return Error::from(e).into(); } @@ -626,14 +626,14 @@ impl HttpResponseBuilder { } } response.body = body.into(); - HttpResponse(response, self.pool) + Response(response, self.pool) } #[inline] - /// Set a streaming body and generate `HttpResponse`. + /// Set a streaming body and generate `Response`. /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn streaming(&mut self, stream: S) -> HttpResponse + /// `ResponseBuilder` can not be used after this call. + pub fn streaming(&mut self, stream: S) -> Response where S: Stream + 'static, E: Into, @@ -641,17 +641,17 @@ impl HttpResponseBuilder { self.body(Body::Streaming(Box::new(stream.map_err(|e| e.into())))) } - /// Set a json body and generate `HttpResponse` + /// Set a json body and generate `Response` /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn json(&mut self, value: T) -> HttpResponse { + /// `ResponseBuilder` can not be used after this call. + pub fn json(&mut self, value: T) -> Response { self.json2(&value) } - /// Set a json body and generate `HttpResponse` + /// Set a json body and generate `Response` /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn json2(&mut self, value: &T) -> HttpResponse { + /// `ResponseBuilder` can not be used after this call. + pub fn json2(&mut self, value: &T) -> Response { match serde_json::to_string(value) { Ok(body) => { let contains = if let Some(parts) = parts(&mut self.response, &self.err) @@ -671,16 +671,16 @@ impl HttpResponseBuilder { } #[inline] - /// Set an empty body and generate `HttpResponse` + /// Set an empty body and generate `Response` /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn finish(&mut self) -> HttpResponse { + /// `ResponseBuilder` can not be used after this call. + pub fn finish(&mut self) -> Response { self.body(Body::Empty) } - /// This method construct new `HttpResponseBuilder` - pub fn take(&mut self) -> HttpResponseBuilder { - HttpResponseBuilder { + /// This method construct new `ResponseBuilder` + pub fn take(&mut self) -> ResponseBuilder { + ResponseBuilder { pool: self.pool, response: self.response.take(), err: self.err.take(), @@ -692,8 +692,8 @@ impl HttpResponseBuilder { #[inline] #[cfg_attr(feature = "cargo-clippy", allow(clippy::borrowed_box))] fn parts<'a>( - parts: &'a mut Option>, err: &Option, -) -> Option<&'a mut Box> { + parts: &'a mut Option>, err: &Option, +) -> Option<&'a mut Box> { if err.is_some() { return None; } @@ -701,7 +701,7 @@ fn parts<'a>( } /// Helper converters -impl, E: Into> From> for HttpResponse { +impl, E: Into> From> for Response { fn from(res: Result) -> Self { match res { Ok(val) => val.into(), @@ -710,62 +710,62 @@ impl, E: Into> From> for HttpResponse } } -impl From for HttpResponse { - fn from(mut builder: HttpResponseBuilder) -> Self { +impl From for Response { + fn from(mut builder: ResponseBuilder) -> Self { builder.finish() } } -impl From<&'static str> for HttpResponse { +impl From<&'static str> for Response { fn from(val: &'static str) -> Self { - HttpResponse::Ok() + Response::Ok() .content_type("text/plain; charset=utf-8") .body(val) } } -impl From<&'static [u8]> for HttpResponse { +impl From<&'static [u8]> for Response { fn from(val: &'static [u8]) -> Self { - HttpResponse::Ok() + Response::Ok() .content_type("application/octet-stream") .body(val) } } -impl From for HttpResponse { +impl From for Response { fn from(val: String) -> Self { - HttpResponse::Ok() + Response::Ok() .content_type("text/plain; charset=utf-8") .body(val) } } -impl<'a> From<&'a String> for HttpResponse { +impl<'a> From<&'a String> for Response { fn from(val: &'a String) -> Self { - HttpResponse::build(StatusCode::OK) + Response::build(StatusCode::OK) .content_type("text/plain; charset=utf-8") .body(val) } } -impl From for HttpResponse { +impl From for Response { fn from(val: Bytes) -> Self { - HttpResponse::Ok() + Response::Ok() .content_type("application/octet-stream") .body(val) } } -impl From for HttpResponse { +impl From for Response { fn from(val: BytesMut) -> Self { - HttpResponse::Ok() + Response::Ok() .content_type("application/octet-stream") .body(val) } } #[derive(Debug)] -struct InnerHttpResponse { +struct InnerResponse { version: Option, headers: HeaderMap, status: StatusCode, @@ -779,7 +779,7 @@ struct InnerHttpResponse { error: Option, } -pub(crate) struct HttpResponseParts { +pub(crate) struct ResponseParts { version: Option, headers: HeaderMap, status: StatusCode, @@ -790,10 +790,10 @@ pub(crate) struct HttpResponseParts { error: Option, } -impl InnerHttpResponse { +impl InnerResponse { #[inline] - fn new(status: StatusCode, body: Body) -> InnerHttpResponse { - InnerHttpResponse { + fn new(status: StatusCode, body: Body) -> InnerResponse { + InnerResponse { status, body, version: None, @@ -809,7 +809,7 @@ impl InnerHttpResponse { } /// This is for failure, we can not have Send + Sync on Streaming and Actor response - fn into_parts(mut self) -> HttpResponseParts { + fn into_parts(mut self) -> ResponseParts { let body = match mem::replace(&mut self.body, Body::Empty) { Body::Empty => None, Body::Binary(mut bin) => Some(bin.take()), @@ -819,7 +819,7 @@ impl InnerHttpResponse { } }; - HttpResponseParts { + ResponseParts { body, version: self.version, headers: self.headers, @@ -831,14 +831,14 @@ impl InnerHttpResponse { } } - fn from_parts(parts: HttpResponseParts) -> InnerHttpResponse { + fn from_parts(parts: ResponseParts) -> InnerResponse { let body = if let Some(ref body) = parts.body { Body::Binary(body.clone().into()) } else { Body::Empty }; - InnerHttpResponse { + InnerResponse { body, status: parts.status, version: parts.version, @@ -855,35 +855,35 @@ impl InnerHttpResponse { } /// Internal use only! -pub(crate) struct HttpResponsePool(RefCell>>); +pub(crate) struct ResponsePool(RefCell>>); -thread_local!(static POOL: &'static HttpResponsePool = HttpResponsePool::pool()); +thread_local!(static POOL: &'static ResponsePool = ResponsePool::pool()); -impl HttpResponsePool { - fn pool() -> &'static HttpResponsePool { - let pool = HttpResponsePool(RefCell::new(VecDeque::with_capacity(128))); +impl ResponsePool { + fn pool() -> &'static ResponsePool { + let pool = ResponsePool(RefCell::new(VecDeque::with_capacity(128))); Box::leak(Box::new(pool)) } - pub fn get_pool() -> &'static HttpResponsePool { + pub fn get_pool() -> &'static ResponsePool { POOL.with(|p| *p) } #[inline] pub fn get_builder( - pool: &'static HttpResponsePool, status: StatusCode, - ) -> HttpResponseBuilder { + pool: &'static ResponsePool, status: StatusCode, + ) -> ResponseBuilder { if let Some(mut msg) = pool.0.borrow_mut().pop_front() { msg.status = status; - HttpResponseBuilder { + ResponseBuilder { pool, response: Some(msg), err: None, cookies: None, } } else { - let msg = Box::new(InnerHttpResponse::new(status, Body::Empty)); - HttpResponseBuilder { + let msg = Box::new(InnerResponse::new(status, Body::Empty)); + ResponseBuilder { pool, response: Some(msg), err: None, @@ -894,30 +894,30 @@ impl HttpResponsePool { #[inline] pub fn get_response( - pool: &'static HttpResponsePool, status: StatusCode, body: Body, - ) -> HttpResponse { + pool: &'static ResponsePool, status: StatusCode, body: Body, + ) -> Response { if let Some(mut msg) = pool.0.borrow_mut().pop_front() { msg.status = status; msg.body = body; - HttpResponse(msg, pool) + Response(msg, pool) } else { - let msg = Box::new(InnerHttpResponse::new(status, body)); - HttpResponse(msg, pool) + let msg = Box::new(InnerResponse::new(status, body)); + Response(msg, pool) } } #[inline] - fn get(status: StatusCode) -> HttpResponseBuilder { - POOL.with(|pool| HttpResponsePool::get_builder(pool, status)) + fn get(status: StatusCode) -> ResponseBuilder { + POOL.with(|pool| ResponsePool::get_builder(pool, status)) } #[inline] - fn with_body(status: StatusCode, body: Body) -> HttpResponse { - POOL.with(|pool| HttpResponsePool::get_response(pool, status, body)) + fn with_body(status: StatusCode, body: Body) -> Response { + POOL.with(|pool| ResponsePool::get_response(pool, status, body)) } #[inline] - fn release(&self, mut inner: Box) { + fn release(&self, mut inner: Box) { let mut p = self.0.borrow_mut(); if p.len() < 128 { inner.headers.clear(); @@ -946,12 +946,12 @@ mod tests { #[test] fn test_debug() { - let resp = HttpResponse::Ok() + let resp = Response::Ok() .header(COOKIE, HeaderValue::from_static("cookie1=value1; ")) .header(COOKIE, HeaderValue::from_static("cookie2=value2; ")) .finish(); let dbg = format!("{:?}", resp); - assert!(dbg.contains("HttpResponse")); + assert!(dbg.contains("Response")); } // #[test] @@ -962,7 +962,7 @@ mod tests { // .finish(); // let cookies = req.cookies().unwrap(); - // let resp = HttpResponse::Ok() + // let resp = Response::Ok() // .cookie( // http::Cookie::build("name", "value") // .domain("www.rust-lang.org") @@ -989,7 +989,7 @@ mod tests { #[test] fn test_update_response_cookies() { - let mut r = HttpResponse::Ok() + let mut r = Response::Ok() .cookie(http::Cookie::new("original", "val100")) .finish(); @@ -1012,7 +1012,7 @@ mod tests { #[test] fn test_basic_builder() { - let resp = HttpResponse::Ok() + let resp = Response::Ok() .header("X-TEST", "value") .version(Version::HTTP_10) .finish(); @@ -1022,19 +1022,19 @@ mod tests { #[test] fn test_upgrade() { - let resp = HttpResponse::build(StatusCode::OK).upgrade().finish(); + let resp = Response::build(StatusCode::OK).upgrade().finish(); assert!(resp.upgrade()) } #[test] fn test_force_close() { - let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); + let resp = Response::build(StatusCode::OK).force_close().finish(); assert!(!resp.keep_alive().unwrap()) } #[test] fn test_content_type() { - let resp = HttpResponse::build(StatusCode::OK) + let resp = Response::build(StatusCode::OK) .content_type("text/plain") .body(Body::Empty); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") @@ -1042,18 +1042,18 @@ mod tests { #[test] fn test_content_encoding() { - let resp = HttpResponse::build(StatusCode::OK).finish(); + let resp = Response::build(StatusCode::OK).finish(); assert_eq!(resp.content_encoding(), None); #[cfg(feature = "brotli")] { - let resp = HttpResponse::build(StatusCode::OK) + let resp = Response::build(StatusCode::OK) .content_encoding(ContentEncoding::Br) .finish(); assert_eq!(resp.content_encoding(), Some(ContentEncoding::Br)); } - let resp = HttpResponse::build(StatusCode::OK) + let resp = Response::build(StatusCode::OK) .content_encoding(ContentEncoding::Gzip) .finish(); assert_eq!(resp.content_encoding(), Some(ContentEncoding::Gzip)); @@ -1061,7 +1061,7 @@ mod tests { #[test] fn test_json() { - let resp = HttpResponse::build(StatusCode::OK).json(vec!["v1", "v2", "v3"]); + let resp = Response::build(StatusCode::OK).json(vec!["v1", "v2", "v3"]); let ct = resp.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("application/json")); assert_eq!( @@ -1072,7 +1072,7 @@ mod tests { #[test] fn test_json_ct() { - let resp = HttpResponse::build(StatusCode::OK) + let resp = Response::build(StatusCode::OK) .header(CONTENT_TYPE, "text/json") .json(vec!["v1", "v2", "v3"]); let ct = resp.headers().get(CONTENT_TYPE).unwrap(); @@ -1085,7 +1085,7 @@ mod tests { #[test] fn test_json2() { - let resp = HttpResponse::build(StatusCode::OK).json2(&vec!["v1", "v2", "v3"]); + let resp = Response::build(StatusCode::OK).json2(&vec!["v1", "v2", "v3"]); let ct = resp.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("application/json")); assert_eq!( @@ -1096,7 +1096,7 @@ mod tests { #[test] fn test_json2_ct() { - let resp = HttpResponse::build(StatusCode::OK) + let resp = Response::build(StatusCode::OK) .header(CONTENT_TYPE, "text/json") .json2(&vec!["v1", "v2", "v3"]); let ct = resp.headers().get(CONTENT_TYPE).unwrap(); @@ -1120,7 +1120,7 @@ mod tests { fn test_into_response() { let req = TestRequest::default().finish(); - let resp: HttpResponse = "test".into(); + let resp: Response = "test".into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), @@ -1129,7 +1129,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().bin_ref(), &Binary::from("test")); - let resp: HttpResponse = b"test".as_ref().into(); + let resp: Response = b"test".as_ref().into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), @@ -1138,7 +1138,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().bin_ref(), &Binary::from(b"test".as_ref())); - let resp: HttpResponse = "test".to_owned().into(); + let resp: Response = "test".to_owned().into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), @@ -1147,7 +1147,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().bin_ref(), &Binary::from("test".to_owned())); - let resp: HttpResponse = (&"test".to_owned()).into(); + let resp: Response = (&"test".to_owned()).into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), @@ -1157,7 +1157,7 @@ mod tests { assert_eq!(resp.body().bin_ref(), &Binary::from(&"test".to_owned())); let b = Bytes::from_static(b"test"); - let resp: HttpResponse = b.into(); + let resp: Response = b.into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), @@ -1170,7 +1170,7 @@ mod tests { ); let b = Bytes::from_static(b"test"); - let resp: HttpResponse = b.into(); + let resp: Response = b.into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), @@ -1180,7 +1180,7 @@ mod tests { assert_eq!(resp.body().bin_ref(), &Binary::from(BytesMut::from("test"))); let b = BytesMut::from("test"); - let resp: HttpResponse = b.into(); + let resp: Response = b.into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), @@ -1192,7 +1192,7 @@ mod tests { #[test] fn test_into_builder() { - let mut resp: HttpResponse = "test".into(); + let mut resp: Response = "test".into(); assert_eq!(resp.status(), StatusCode::OK); resp.add_cookie(&http::Cookie::new("cookie1", "val100")) diff --git a/src/test.rs b/src/test.rs index 3c48df643..71145cee8 100644 --- a/src/test.rs +++ b/src/test.rs @@ -25,12 +25,12 @@ use uri::Url as InnerUrl; /// /// # Examples /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// # use actix_web::*; /// # -/// # fn my_handler(req: &HttpRequest) -> HttpResponse { -/// # HttpResponse::Ok().into() +/// # fn my_handler(req: &HttpRequest) -> Response { +/// # Response::Ok().into() /// # } /// # /// # fn main() { @@ -248,20 +248,20 @@ impl Drop for TestServer { // } // } -/// Test `HttpRequest` builder +/// Test `Request` builder /// -/// ```rust +/// ```rust,ignore /// # extern crate http; /// # extern crate actix_web; /// # use http::{header, StatusCode}; /// # use actix_web::*; /// use actix_web::test::TestRequest; /// -/// fn index(req: &HttpRequest) -> HttpResponse { +/// fn index(req: &HttpRequest) -> Response { /// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { -/// HttpResponse::Ok().into() +/// Response::Ok().into() /// } else { -/// HttpResponse::BadRequest().into() +/// Response::BadRequest().into() /// } /// } /// @@ -403,7 +403,7 @@ impl TestRequest { // /// This method generates `HttpRequest` instance and runs handler // /// with generated request. - // pub fn run>(self, h: &H) -> Result { + // pub fn run>(self, h: &H) -> Result { // let req = self.finish(); // let resp = h.handle(&req); @@ -424,7 +424,7 @@ impl TestRequest { // /// with generated request. // /// // /// This method panics is handler returns actor. - // pub fn run_async(self, h: H) -> Result + // pub fn run_async(self, h: H) -> Result // where // H: Fn(HttpRequest) -> F + 'static, // F: Future + 'static, @@ -467,7 +467,7 @@ impl TestRequest { // } // /// This method generates `HttpRequest` instance and executes handler - // pub fn execute(self, f: F) -> Result + // pub fn execute(self, f: F) -> Result // where // F: FnOnce(&HttpRequest) -> R, // R: Responder + 'static, diff --git a/src/ws/mod.rs b/src/ws/mod.rs index 6bb84c189..61fad543c 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -10,9 +10,9 @@ use http::{header, Method, StatusCode}; use body::Binary; use error::{PayloadError, ResponseError}; -use httpresponse::{ConnectionType, HttpResponse, HttpResponseBuilder}; use payload::PayloadBuffer; use request::Request; +use response::{ConnectionType, Response, ResponseBuilder}; mod frame; mod mask; @@ -85,26 +85,26 @@ pub enum HandshakeError { } impl ResponseError for HandshakeError { - fn error_response(&self) -> HttpResponse { + fn error_response(&self) -> Response { match *self { - HandshakeError::GetMethodRequired => HttpResponse::MethodNotAllowed() + HandshakeError::GetMethodRequired => Response::MethodNotAllowed() .header(header::ALLOW, "GET") .finish(), - HandshakeError::NoWebsocketUpgrade => HttpResponse::BadRequest() + HandshakeError::NoWebsocketUpgrade => Response::BadRequest() .reason("No WebSocket UPGRADE header found") .finish(), - HandshakeError::NoConnectionUpgrade => HttpResponse::BadRequest() + HandshakeError::NoConnectionUpgrade => Response::BadRequest() .reason("No CONNECTION upgrade") .finish(), - HandshakeError::NoVersionHeader => HttpResponse::BadRequest() + HandshakeError::NoVersionHeader => Response::BadRequest() .reason("Websocket version header is required") .finish(), - HandshakeError::UnsupportedVersion => HttpResponse::BadRequest() + HandshakeError::UnsupportedVersion => Response::BadRequest() .reason("Unsupported version") .finish(), - HandshakeError::BadWebsocketKey => HttpResponse::BadRequest() - .reason("Handshake error") - .finish(), + HandshakeError::BadWebsocketKey => { + Response::BadRequest().reason("Handshake error").finish() + } } } } @@ -126,13 +126,13 @@ pub enum Message { /// Prepare `WebSocket` handshake response. /// -/// This function returns handshake `HttpResponse`, ready to send to peer. +/// This function returns handshake `Response`, ready to send to peer. /// It does not perform any IO. /// // /// `protocols` is a sequence of known protocols. On successful handshake, // /// the returned response headers contain the first protocol in this list // /// which the server also knows. -pub fn handshake(req: &Request) -> Result { +pub fn handshake(req: &Request) -> Result { // WebSocket accepts only GET if *req.method() != Method::GET { return Err(HandshakeError::GetMethodRequired); @@ -181,7 +181,7 @@ pub fn handshake(req: &Request) -> Result { proto::hash_key(key.as_ref()) }; - Ok(HttpResponse::build(StatusCode::SWITCHING_PROTOCOLS) + Ok(Response::build(StatusCode::SWITCHING_PROTOCOLS) .connection_type(ConnectionType::Upgrade) .header(header::UPGRADE, "websocket") .header(header::TRANSFER_ENCODING, "chunked") @@ -280,20 +280,6 @@ where } } -/// Common writing methods for a websocket. -pub trait WsWriter { - /// Send a text - fn send_text>(&mut self, text: T); - /// Send a binary - fn send_binary>(&mut self, data: B); - /// Send a ping message - fn send_ping(&mut self, message: &str); - /// Send a pong message - fn send_pong(&mut self, message: &str); - /// Close the connection - fn send_close(&mut self, reason: Option); -} - #[cfg(test)] mod tests { use super::*; @@ -399,17 +385,17 @@ mod tests { #[test] fn test_wserror_http_response() { - let resp: HttpResponse = HandshakeError::GetMethodRequired.error_response(); + let resp: Response = HandshakeError::GetMethodRequired.error_response(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - let resp: HttpResponse = HandshakeError::NoWebsocketUpgrade.error_response(); + let resp: Response = HandshakeError::NoWebsocketUpgrade.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: HttpResponse = HandshakeError::NoConnectionUpgrade.error_response(); + let resp: Response = HandshakeError::NoConnectionUpgrade.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: HttpResponse = HandshakeError::NoVersionHeader.error_response(); + let resp: Response = HandshakeError::NoVersionHeader.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: HttpResponse = HandshakeError::UnsupportedVersion.error_response(); + let resp: Response = HandshakeError::UnsupportedVersion.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: HttpResponse = HandshakeError::BadWebsocketKey.error_response(); + let resp: Response = HandshakeError::BadWebsocketKey.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } diff --git a/tests/test_h1v2.rs b/tests/test_h1v2.rs index bb9430659..1866f29b4 100644 --- a/tests/test_h1v2.rs +++ b/tests/test_h1v2.rs @@ -11,7 +11,7 @@ use actix_net::server::Server; use actix_web::{client, test}; use futures::future; -use actix_http::{h1, Error, HttpResponse, KeepAlive, ServiceConfig}; +use actix_http::{h1, Error, KeepAlive, Response, ServiceConfig}; #[test] fn test_h1_v2() { @@ -29,7 +29,7 @@ fn test_h1_v2() { h1::H1Service::new(settings, |req| { println!("REQ: {:?}", req); - future::ok::<_, Error>(HttpResponse::Ok().finish()) + future::ok::<_, Error>(Response::Ok().finish()) }) }).unwrap() .run(); From 5c0a2066cc901c136062121345684332a2e98ac3 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 5 Oct 2018 12:47:22 -0700 Subject: [PATCH 017/427] refactor ws to a websocket codec --- src/body.rs | 2 - src/ws/codec.rs | 119 +++++++++++++++ src/ws/frame.rs | 382 ++++++++++++++---------------------------------- src/ws/mod.rs | 133 ++--------------- 4 files changed, 238 insertions(+), 398 deletions(-) create mode 100644 src/ws/codec.rs diff --git a/src/body.rs b/src/body.rs index db06bef22..c10b067a2 100644 --- a/src/body.rs +++ b/src/body.rs @@ -17,8 +17,6 @@ pub enum Body { /// Unspecified streaming response. Developer is responsible for setting /// right `Content-Length` or `Transfer-Encoding` headers. Streaming(BodyStream), - // /// Special body type for actor response. - // Actor(Box), } /// Represents various types of binary body. diff --git a/src/ws/codec.rs b/src/ws/codec.rs new file mode 100644 index 000000000..6e2b12090 --- /dev/null +++ b/src/ws/codec.rs @@ -0,0 +1,119 @@ +use bytes::BytesMut; +use tokio_codec::{Decoder, Encoder}; + +use super::frame::Frame; +use super::proto::{CloseReason, OpCode}; +use super::ProtocolError; +use body::Binary; + +/// `WebSocket` Message +#[derive(Debug, PartialEq)] +pub enum Message { + /// Text message + Text(String), + /// Binary message + Binary(Binary), + /// Ping message + Ping(String), + /// Pong message + Pong(String), + /// Close message with optional reason + Close(Option), +} + +/// WebSockets protocol codec +pub struct Codec { + max_size: usize, + server: bool, +} + +impl Codec { + /// Create new websocket frames decoder + pub fn new() -> Codec { + Codec { + max_size: 65_536, + server: true, + } + } + + /// Set max frame size + /// + /// By default max size is set to 64kb + pub fn max_size(mut self, size: usize) -> Self { + self.max_size = size; + self + } + + /// Set decoder to client mode. + /// + /// By default decoder works in server mode. + pub fn client_mode(mut self) -> Self { + self.server = false; + self + } +} + +impl Encoder for Codec { + type Item = Message; + type Error = ProtocolError; + + fn encode(&mut self, item: Message, dst: &mut BytesMut) -> Result<(), Self::Error> { + match item { + Message::Text(txt) => { + Frame::write_message(dst, txt, OpCode::Text, true, !self.server) + } + Message::Binary(bin) => { + Frame::write_message(dst, bin, OpCode::Binary, true, !self.server) + } + Message::Ping(txt) => { + Frame::write_message(dst, txt, OpCode::Ping, true, !self.server) + } + Message::Pong(txt) => { + Frame::write_message(dst, txt, OpCode::Pong, true, !self.server) + } + Message::Close(reason) => Frame::write_close(dst, reason, !self.server), + } + Ok(()) + } +} + +impl Decoder for Codec { + type Item = Message; + type Error = ProtocolError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + match Frame::parse(src, self.server, self.max_size) { + Ok(Some((finished, opcode, payload))) => { + // continuation is not supported + if !finished { + return Err(ProtocolError::NoContinuation); + } + + match opcode { + OpCode::Continue => Err(ProtocolError::NoContinuation), + OpCode::Bad => Err(ProtocolError::BadOpCode), + OpCode::Close => { + let close_reason = Frame::parse_close_payload(&payload); + Ok(Some(Message::Close(close_reason))) + } + OpCode::Ping => Ok(Some(Message::Ping( + String::from_utf8_lossy(payload.as_ref()).into(), + ))), + OpCode::Pong => Ok(Some(Message::Pong( + String::from_utf8_lossy(payload.as_ref()).into(), + ))), + OpCode::Binary => Ok(Some(Message::Binary(payload))), + OpCode::Text => { + let tmp = Vec::from(payload.as_ref()); + match String::from_utf8(tmp) { + Ok(s) => Ok(Some(Message::Text(s))), + Err(_) => Err(ProtocolError::BadEncoding), + } + } + } + } + Ok(None) => Ok(None), + Err(e) => Err(e), + } + } +} diff --git a/src/ws/frame.rs b/src/ws/frame.rs index d5fa98272..38bebc283 100644 --- a/src/ws/frame.rs +++ b/src/ws/frame.rs @@ -1,144 +1,29 @@ use byteorder::{ByteOrder, LittleEndian, NetworkEndian}; -use bytes::{BufMut, Bytes, BytesMut}; -use futures::{Async, Poll, Stream}; +use bytes::{BufMut, BytesMut}; use rand; -use std::fmt; use body::Binary; -use error::PayloadError; -use payload::PayloadBuffer; - use ws::mask::apply_mask; use ws::proto::{CloseCode, CloseReason, OpCode}; use ws::ProtocolError; /// A struct representing a `WebSocket` frame. #[derive(Debug)] -pub struct Frame { - finished: bool, - opcode: OpCode, - payload: Binary, -} +pub struct Frame; impl Frame { - /// Destruct frame - pub fn unpack(self) -> (bool, OpCode, Binary) { - (self.finished, self.opcode, self.payload) - } - - /// Create a new Close control frame. - #[inline] - pub fn close(reason: Option, genmask: bool) -> FramedMessage { - let payload = match reason { - None => Vec::new(), - Some(reason) => { - let mut code_bytes = [0; 2]; - NetworkEndian::write_u16(&mut code_bytes, reason.code.into()); - - let mut payload = Vec::from(&code_bytes[..]); - if let Some(description) = reason.description { - payload.extend(description.as_bytes()); - } - payload - } - }; - - Frame::message(payload, OpCode::Close, true, genmask) - } - - #[cfg_attr(feature = "cargo-clippy", allow(clippy::type_complexity))] - fn read_copy_md( - pl: &mut PayloadBuffer, server: bool, max_size: usize, - ) -> Poll)>, ProtocolError> - where - S: Stream, - { - let mut idx = 2; - let buf = match pl.copy(2)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => return Ok(Async::NotReady), - }; - let first = buf[0]; - let second = buf[1]; - let finished = first & 0x80 != 0; - - // check masking - let masked = second & 0x80 != 0; - if !masked && server { - return Err(ProtocolError::UnmaskedFrame); - } else if masked && !server { - return Err(ProtocolError::MaskedFrame); - } - - // Op code - let opcode = OpCode::from(first & 0x0F); - - if let OpCode::Bad = opcode { - return Err(ProtocolError::InvalidOpcode(first & 0x0F)); - } - - let len = second & 0x7F; - let length = if len == 126 { - let buf = match pl.copy(4)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => return Ok(Async::NotReady), - }; - let len = NetworkEndian::read_uint(&buf[idx..], 2) as usize; - idx += 2; - len - } else if len == 127 { - let buf = match pl.copy(10)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => return Ok(Async::NotReady), - }; - let len = NetworkEndian::read_uint(&buf[idx..], 8); - if len > max_size as u64 { - return Err(ProtocolError::Overflow); - } - idx += 8; - len as usize - } else { - len as usize - }; - - // check for max allowed size - if length > max_size { - return Err(ProtocolError::Overflow); - } - - let mask = if server { - let buf = match pl.copy(idx + 4)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => return Ok(Async::NotReady), - }; - - let mask: &[u8] = &buf[idx..idx + 4]; - let mask_u32 = LittleEndian::read_u32(mask); - idx += 4; - Some(mask_u32) - } else { - None - }; - - Ok(Async::Ready(Some((idx, finished, opcode, length, mask)))) - } - - fn read_chunk_md( - chunk: &[u8], server: bool, max_size: usize, - ) -> Poll<(usize, bool, OpCode, usize, Option), ProtocolError> { - let chunk_len = chunk.len(); + fn parse_metadata( + src: &[u8], server: bool, max_size: usize, + ) -> Result)>, ProtocolError> { + let chunk_len = src.len(); let mut idx = 2; if chunk_len < 2 { - return Ok(Async::NotReady); + return Ok(None); } - let first = chunk[0]; - let second = chunk[1]; + let first = src[0]; + let second = src[1]; let finished = first & 0x80 != 0; // check masking @@ -159,16 +44,16 @@ impl Frame { let len = second & 0x7F; let length = if len == 126 { if chunk_len < 4 { - return Ok(Async::NotReady); + return Ok(None); } - let len = NetworkEndian::read_uint(&chunk[idx..], 2) as usize; + let len = NetworkEndian::read_uint(&src[idx..], 2) as usize; idx += 2; len } else if len == 127 { if chunk_len < 10 { - return Ok(Async::NotReady); + return Ok(None); } - let len = NetworkEndian::read_uint(&chunk[idx..], 8); + let len = NetworkEndian::read_uint(&src[idx..], 8); if len > max_size as u64 { return Err(ProtocolError::Overflow); } @@ -185,10 +70,10 @@ impl Frame { let mask = if server { if chunk_len < idx + 4 { - return Ok(Async::NotReady); + return Ok(None); } - let mask: &[u8] = &chunk[idx..idx + 4]; + let mask: &[u8] = &src[idx..idx + 4]; let mask_u32 = LittleEndian::read_u32(mask); idx += 4; Some(mask_u32) @@ -196,56 +81,34 @@ impl Frame { None }; - Ok(Async::Ready((idx, finished, opcode, length, mask))) + Ok(Some((idx, finished, opcode, length, mask))) } /// Parse the input stream into a frame. - pub fn parse( - pl: &mut PayloadBuffer, server: bool, max_size: usize, - ) -> Poll, ProtocolError> - where - S: Stream, - { - // try to parse ws frame md from one chunk - let result = match pl.get_chunk()? { - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::Ready(Some(chunk)) => Frame::read_chunk_md(chunk, server, max_size)?, - }; + pub fn parse( + src: &mut BytesMut, server: bool, max_size: usize, + ) -> Result, ProtocolError> { + // try to parse ws frame metadata + let (idx, finished, opcode, length, mask) = + match Frame::parse_metadata(src, server, max_size)? { + None => return Ok(None), + Some(res) => res, + }; - let (idx, finished, opcode, length, mask) = match result { - // we may need to join several chunks - Async::NotReady => match Frame::read_copy_md(pl, server, max_size)? { - Async::Ready(Some(item)) => item, - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(None) => return Ok(Async::Ready(None)), - }, - Async::Ready(item) => item, - }; - - match pl.can_read(idx + length)? { - Async::Ready(Some(true)) => (), - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::Ready(Some(false)) | Async::NotReady => return Ok(Async::NotReady), + // not enough data + if src.len() < idx + length { + return Ok(None); } // remove prefix - pl.drop_bytes(idx); + src.split_to(idx); // no need for body if length == 0 { - return Ok(Async::Ready(Some(Frame { - finished, - opcode, - payload: Binary::from(""), - }))); + return Ok(Some((finished, opcode, Binary::from("")))); } - let data = match pl.read_exact(length)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => panic!(), - }; + let mut data = src.split_to(length); // control frames must have length <= 125 match opcode { @@ -254,26 +117,17 @@ impl Frame { } OpCode::Close if length > 125 => { debug!("Received close frame with payload length exceeding 125. Morphing to protocol close frame."); - return Ok(Async::Ready(Some(Frame::default()))); + return Ok(Some((true, OpCode::Close, Binary::from("")))); } _ => (), } // unmask - let data = if let Some(mask) = mask { - let mut buf = BytesMut::new(); - buf.extend_from_slice(&data); - apply_mask(&mut buf, mask); - buf.freeze() - } else { - data - }; + if let Some(mask) = mask { + apply_mask(&mut data, mask); + } - Ok(Async::Ready(Some(Frame { - finished, - opcode, - payload: data.into(), - }))) + Ok(Some((finished, opcode, data.into()))) } /// Parse the payload of a close frame. @@ -293,120 +147,101 @@ impl Frame { } /// Generate binary representation - pub fn message>( - data: B, code: OpCode, finished: bool, genmask: bool, - ) -> FramedMessage { - let payload = data.into(); - let one: u8 = if finished { - 0x80 | Into::::into(code) + pub fn write_message>( + dst: &mut BytesMut, pl: B, op: OpCode, fin: bool, mask: bool, + ) { + let payload = pl.into(); + let one: u8 = if fin { + 0x80 | Into::::into(op) } else { - code.into() + op.into() }; let payload_len = payload.len(); - let (two, p_len) = if genmask { + let (two, p_len) = if mask { (0x80, payload_len + 4) } else { (0, payload_len) }; - let mut buf = if payload_len < 126 { - let mut buf = BytesMut::with_capacity(p_len + 2); - buf.put_slice(&[one, two | payload_len as u8]); - buf + if payload_len < 126 { + dst.put_slice(&[one, two | payload_len as u8]); } else if payload_len <= 65_535 { - let mut buf = BytesMut::with_capacity(p_len + 4); - buf.put_slice(&[one, two | 126]); - buf.put_u16_be(payload_len as u16); - buf + dst.reserve(p_len + 4); + dst.put_slice(&[one, two | 126]); + dst.put_u16_be(payload_len as u16); } else { - let mut buf = BytesMut::with_capacity(p_len + 10); - buf.put_slice(&[one, two | 127]); - buf.put_u64_be(payload_len as u64); - buf + dst.reserve(p_len + 10); + dst.put_slice(&[one, two | 127]); + dst.put_u64_be(payload_len as u64); }; - let binary = if genmask { + if mask { let mask = rand::random::(); - buf.put_u32_le(mask); - buf.extend_from_slice(payload.as_ref()); - let pos = buf.len() - payload_len; - apply_mask(&mut buf[pos..], mask); - buf.into() + dst.put_u32_le(mask); + dst.extend_from_slice(payload.as_ref()); + let pos = dst.len() - payload_len; + apply_mask(&mut dst[pos..], mask); } else { - buf.put_slice(payload.as_ref()); - buf.into() - }; - - FramedMessage(binary) - } -} - -impl Default for Frame { - fn default() -> Frame { - Frame { - finished: true, - opcode: OpCode::Close, - payload: Binary::from(&b""[..]), + dst.put_slice(payload.as_ref()); } } -} -impl fmt::Display for Frame { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - " - - final: {} - opcode: {} - payload length: {} - payload: 0x{} -", - self.finished, - self.opcode, - self.payload.len(), - self.payload - .as_ref() - .iter() - .map(|byte| format!("{:x}", byte)) - .collect::() - ) + /// Create a new Close control frame. + #[inline] + pub fn write_close(dst: &mut BytesMut, reason: Option, mask: bool) { + let payload = match reason { + None => Vec::new(), + Some(reason) => { + let mut code_bytes = [0; 2]; + NetworkEndian::write_u16(&mut code_bytes, reason.code.into()); + + let mut payload = Vec::from(&code_bytes[..]); + if let Some(description) = reason.description { + payload.extend(description.as_bytes()); + } + payload + } + }; + + Frame::write_message(dst, payload, OpCode::Close, true, mask) } } -/// `WebSocket` message with framing. -#[derive(Debug)] -pub struct FramedMessage(pub(crate) Binary); - #[cfg(test)] mod tests { use super::*; - use futures::stream::once; - fn is_none(frm: &Poll, ProtocolError>) -> bool { + struct F { + finished: bool, + opcode: OpCode, + payload: Binary, + } + + fn is_none(frm: &Result, ProtocolError>) -> bool { match *frm { - Ok(Async::Ready(None)) => true, + Ok(None) => true, _ => false, } } - fn extract(frm: Poll, ProtocolError>) -> Frame { + fn extract(frm: Result, ProtocolError>) -> F { match frm { - Ok(Async::Ready(Some(frame))) => frame, + Ok(Some((finished, opcode, payload))) => F { + finished, + opcode, + payload, + }, _ => unreachable!("error"), } } #[test] fn test_parse() { - let mut buf = PayloadBuffer::new(once(Ok(BytesMut::from( - &[0b0000_0001u8, 0b0000_0001u8][..], - ).freeze()))); + let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]); assert!(is_none(&Frame::parse(&mut buf, false, 1024))); let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]); buf.extend(b"1"); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); let frame = extract(Frame::parse(&mut buf, false, 1024)); assert!(!frame.finished); @@ -416,9 +251,7 @@ mod tests { #[test] fn test_parse_length0() { - let buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0000u8][..]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - + let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0000u8][..]); let frame = extract(Frame::parse(&mut buf, false, 1024)); assert!(!frame.finished); assert_eq!(frame.opcode, OpCode::Text); @@ -427,14 +260,12 @@ mod tests { #[test] fn test_parse_length2() { - let buf = BytesMut::from(&[0b0000_0001u8, 126u8][..]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); + let mut buf = BytesMut::from(&[0b0000_0001u8, 126u8][..]); assert!(is_none(&Frame::parse(&mut buf, false, 1024))); let mut buf = BytesMut::from(&[0b0000_0001u8, 126u8][..]); buf.extend(&[0u8, 4u8][..]); buf.extend(b"1234"); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); let frame = extract(Frame::parse(&mut buf, false, 1024)); assert!(!frame.finished); @@ -444,14 +275,12 @@ mod tests { #[test] fn test_parse_length4() { - let buf = BytesMut::from(&[0b0000_0001u8, 127u8][..]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); + let mut buf = BytesMut::from(&[0b0000_0001u8, 127u8][..]); assert!(is_none(&Frame::parse(&mut buf, false, 1024))); let mut buf = BytesMut::from(&[0b0000_0001u8, 127u8][..]); buf.extend(&[0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 4u8][..]); buf.extend(b"1234"); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); let frame = extract(Frame::parse(&mut buf, false, 1024)); assert!(!frame.finished); @@ -464,7 +293,6 @@ mod tests { let mut buf = BytesMut::from(&[0b0000_0001u8, 0b1000_0001u8][..]); buf.extend(b"0001"); buf.extend(b"1"); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); assert!(Frame::parse(&mut buf, false, 1024).is_err()); @@ -478,7 +306,6 @@ mod tests { fn test_parse_frame_no_mask() { let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]); buf.extend(&[1u8]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); assert!(Frame::parse(&mut buf, true, 1024).is_err()); @@ -492,7 +319,6 @@ mod tests { fn test_parse_frame_max_size() { let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0010u8][..]); buf.extend(&[1u8, 1u8]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); assert!(Frame::parse(&mut buf, true, 1).is_err()); @@ -504,35 +330,39 @@ mod tests { #[test] fn test_ping_frame() { - let frame = Frame::message(Vec::from("data"), OpCode::Ping, true, false); + let mut buf = BytesMut::new(); + Frame::write_message(&mut buf, Vec::from("data"), OpCode::Ping, true, false); let mut v = vec![137u8, 4u8]; v.extend(b"data"); - assert_eq!(frame.0, v.into()); + assert_eq!(&buf[..], &v[..]); } #[test] fn test_pong_frame() { - let frame = Frame::message(Vec::from("data"), OpCode::Pong, true, false); + let mut buf = BytesMut::new(); + Frame::write_message(&mut buf, Vec::from("data"), OpCode::Pong, true, false); let mut v = vec![138u8, 4u8]; v.extend(b"data"); - assert_eq!(frame.0, v.into()); + assert_eq!(&buf[..], &v[..]); } #[test] fn test_close_frame() { + let mut buf = BytesMut::new(); let reason = (CloseCode::Normal, "data"); - let frame = Frame::close(Some(reason.into()), false); + Frame::write_close(&mut buf, Some(reason.into()), false); let mut v = vec![136u8, 6u8, 3u8, 232u8]; v.extend(b"data"); - assert_eq!(frame.0, v.into()); + assert_eq!(&buf[..], &v[..]); } #[test] fn test_empty_close_frame() { - let frame = Frame::close(None, false); - assert_eq!(frame.0, vec![0x88, 0x00].into()); + let mut buf = BytesMut::new(); + Frame::write_close(&mut buf, None, false); + assert_eq!(&buf[..], &vec![0x88, 0x00][..]); } } diff --git a/src/ws/mod.rs b/src/ws/mod.rs index 61fad543c..e8bf3870d 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -1,24 +1,23 @@ -//! `WebSocket` support. +//! WebSocket protocol support. //! //! To setup a `WebSocket`, first do web socket handshake then on success //! convert `Payload` into a `WsStream` stream and then use `WsWriter` to //! communicate with the peer. //! ``` -use bytes::Bytes; -use futures::{Async, Poll, Stream}; -use http::{header, Method, StatusCode}; +use std::io; -use body::Binary; -use error::{PayloadError, ResponseError}; -use payload::PayloadBuffer; +use error::ResponseError; +use http::{header, Method, StatusCode}; use request::Request; use response::{ConnectionType, Response, ResponseBuilder}; +mod codec; mod frame; mod mask; mod proto; -pub use self::frame::{Frame, FramedMessage}; +pub use self::codec::Message; +pub use self::frame::Frame; pub use self::proto::{CloseCode, CloseReason, OpCode}; /// Websocket protocol errors @@ -48,16 +47,16 @@ pub enum ProtocolError { /// Bad utf-8 encoding #[fail(display = "Bad utf-8 encoding.")] BadEncoding, - /// Payload error - #[fail(display = "Payload error: {}", _0)] - Payload(#[cause] PayloadError), + /// Io error + #[fail(display = "io error: {}", _0)] + Io(#[cause] io::Error), } impl ResponseError for ProtocolError {} -impl From for ProtocolError { - fn from(err: PayloadError) -> ProtocolError { - ProtocolError::Payload(err) +impl From for ProtocolError { + fn from(err: io::Error) -> ProtocolError { + ProtocolError::Io(err) } } @@ -109,21 +108,6 @@ impl ResponseError for HandshakeError { } } -/// `WebSocket` Message -#[derive(Debug, PartialEq)] -pub enum Message { - /// Text message - Text(String), - /// Binary message - Binary(Binary), - /// Ping message - Ping(String), - /// Pong message - Pong(String), - /// Close message with optional reason - Close(Option), -} - /// Prepare `WebSocket` handshake response. /// /// This function returns handshake `Response`, ready to send to peer. @@ -189,97 +173,6 @@ pub fn handshake(req: &Request) -> Result { .take()) } -/// Maps `Payload` stream into stream of `ws::Message` items -pub struct WsStream { - rx: PayloadBuffer, - closed: bool, - max_size: usize, -} - -impl WsStream -where - S: Stream, -{ - /// Create new websocket frames stream - pub fn new(stream: S) -> WsStream { - WsStream { - rx: PayloadBuffer::new(stream), - closed: false, - max_size: 65_536, - } - } - - /// Set max frame size - /// - /// By default max size is set to 64kb - pub fn max_size(mut self, size: usize) -> Self { - self.max_size = size; - self - } -} - -impl Stream for WsStream -where - S: Stream, -{ - type Item = Message; - type Error = ProtocolError; - - fn poll(&mut self) -> Poll, Self::Error> { - if self.closed { - return Ok(Async::Ready(None)); - } - - match Frame::parse(&mut self.rx, true, self.max_size) { - Ok(Async::Ready(Some(frame))) => { - let (finished, opcode, payload) = frame.unpack(); - - // continuation is not supported - if !finished { - self.closed = true; - return Err(ProtocolError::NoContinuation); - } - - match opcode { - OpCode::Continue => Err(ProtocolError::NoContinuation), - OpCode::Bad => { - self.closed = true; - Err(ProtocolError::BadOpCode) - } - OpCode::Close => { - self.closed = true; - let close_reason = Frame::parse_close_payload(&payload); - Ok(Async::Ready(Some(Message::Close(close_reason)))) - } - OpCode::Ping => Ok(Async::Ready(Some(Message::Ping( - String::from_utf8_lossy(payload.as_ref()).into(), - )))), - OpCode::Pong => Ok(Async::Ready(Some(Message::Pong( - String::from_utf8_lossy(payload.as_ref()).into(), - )))), - OpCode::Binary => Ok(Async::Ready(Some(Message::Binary(payload)))), - OpCode::Text => { - let tmp = Vec::from(payload.as_ref()); - match String::from_utf8(tmp) { - Ok(s) => Ok(Async::Ready(Some(Message::Text(s)))), - Err(_) => { - self.closed = true; - Err(ProtocolError::BadEncoding) - } - } - } - } - } - Ok(Async::Ready(None)) => Ok(Async::Ready(None)), - Ok(Async::NotReady) => Ok(Async::NotReady), - Err(e) => { - self.closed = true; - Err(e) - } - } - } -} - #[cfg(test)] mod tests { use super::*; From 7e135b798b1f20368b62a9d2f7f03dbbc361bffe Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 5 Oct 2018 14:30:40 -0700 Subject: [PATCH 018/427] add websocket transport and test --- src/framed/framed.rs | 283 ------------------------------------- src/framed/framed_read.rs | 216 ---------------------------- src/framed/framed_write.rs | 243 ------------------------------- src/framed/mod.rs | 32 ----- src/h1/dispatcher.rs | 4 +- src/lib.rs | 3 - src/ws/mod.rs | 4 +- src/ws/transport.rs | 50 +++++++ tests/test_ws.rs | 111 +++++++++++++++ 9 files changed, 165 insertions(+), 781 deletions(-) delete mode 100644 src/framed/framed.rs delete mode 100644 src/framed/framed_read.rs delete mode 100644 src/framed/framed_write.rs delete mode 100644 src/framed/mod.rs create mode 100644 src/ws/transport.rs create mode 100644 tests/test_ws.rs diff --git a/src/framed/framed.rs b/src/framed/framed.rs deleted file mode 100644 index f6295d98f..000000000 --- a/src/framed/framed.rs +++ /dev/null @@ -1,283 +0,0 @@ -#![allow(deprecated)] - -use std::fmt; -use std::io::{self, Read, Write}; - -use bytes::BytesMut; -use futures::{Poll, Sink, StartSend, Stream}; -use tokio_codec::{Decoder, Encoder}; -use tokio_io::{AsyncRead, AsyncWrite}; - -use super::framed_read::{framed_read2, framed_read2_with_buffer, FramedRead2}; -use super::framed_write::{framed_write2, framed_write2_with_buffer, FramedWrite2}; - -/// A unified `Stream` and `Sink` interface to an underlying I/O object, using -/// the `Encoder` and `Decoder` traits to encode and decode frames. -/// -/// You can create a `Framed` instance by using the `AsyncRead::framed` adapter. -pub struct Framed { - inner: FramedRead2>>, -} - -pub struct Fuse(pub T, pub U); - -impl Framed -where - T: AsyncRead + AsyncWrite, - U: Decoder + Encoder, -{ - /// Provides a `Stream` and `Sink` interface for reading and writing to this - /// `Io` object, using `Decode` and `Encode` to read and write the raw data. - /// - /// Raw I/O objects work with byte sequences, but higher-level code usually - /// wants to batch these into meaningful chunks, called "frames". This - /// method layers framing on top of an I/O object, by using the `Codec` - /// traits to handle encoding and decoding of messages frames. Note that - /// the incoming and outgoing frame types may be distinct. - /// - /// This function returns a *single* object that is both `Stream` and - /// `Sink`; grouping this into a single object is often useful for layering - /// things like gzip or TLS, which require both read and write access to the - /// underlying object. - /// - /// If you want to work more directly with the streams and sink, consider - /// calling `split` on the `Framed` returned by this method, which will - /// break them into separate objects, allowing them to interact more easily. - pub fn new(inner: T, codec: U) -> Framed { - Framed { - inner: framed_read2(framed_write2(Fuse(inner, codec))), - } - } -} - -impl Framed { - /// Provides a `Stream` and `Sink` interface for reading and writing to this - /// `Io` object, using `Decode` and `Encode` to read and write the raw data. - /// - /// Raw I/O objects work with byte sequences, but higher-level code usually - /// wants to batch these into meaningful chunks, called "frames". This - /// method layers framing on top of an I/O object, by using the `Codec` - /// traits to handle encoding and decoding of messages frames. Note that - /// the incoming and outgoing frame types may be distinct. - /// - /// This function returns a *single* object that is both `Stream` and - /// `Sink`; grouping this into a single object is often useful for layering - /// things like gzip or TLS, which require both read and write access to the - /// underlying object. - /// - /// This objects takes a stream and a readbuffer and a writebuffer. These field - /// can be obtained from an existing `Framed` with the `into_parts` method. - /// - /// If you want to work more directly with the streams and sink, consider - /// calling `split` on the `Framed` returned by this method, which will - /// break them into separate objects, allowing them to interact more easily. - pub fn from_parts(parts: FramedParts) -> Framed { - Framed { - inner: framed_read2_with_buffer( - framed_write2_with_buffer(Fuse(parts.io, parts.codec), parts.write_buf), - parts.read_buf, - ), - } - } - - /// Returns a reference to the underlying codec. - pub fn get_codec(&self) -> &U { - &self.inner.get_ref().get_ref().1 - } - - /// Returns a mutable reference to the underlying codec. - pub fn get_codec_mut(&mut self) -> &mut U { - &mut self.inner.get_mut().get_mut().1 - } - - /// Returns a reference to the underlying I/O stream wrapped by - /// `Frame`. - /// - /// Note that care should be taken to not tamper with the underlying stream - /// of data coming in as it may corrupt the stream of frames otherwise - /// being worked with. - pub fn get_ref(&self) -> &T { - &self.inner.get_ref().get_ref().0 - } - - /// Returns a mutable reference to the underlying I/O stream wrapped by - /// `Frame`. - /// - /// Note that care should be taken to not tamper with the underlying stream - /// of data coming in as it may corrupt the stream of frames otherwise - /// being worked with. - pub fn get_mut(&mut self) -> &mut T { - &mut self.inner.get_mut().get_mut().0 - } - - /// Consumes the `Frame`, returning its underlying I/O stream. - /// - /// Note that care should be taken to not tamper with the underlying stream - /// of data coming in as it may corrupt the stream of frames otherwise - /// being worked with. - pub fn into_inner(self) -> T { - self.inner.into_inner().into_inner().0 - } - - /// Consumes the `Frame`, returning its underlying I/O stream, the buffer - /// with unprocessed data, and the codec. - /// - /// Note that care should be taken to not tamper with the underlying stream - /// of data coming in as it may corrupt the stream of frames otherwise - /// being worked with. - pub fn into_parts(self) -> FramedParts { - let (inner, read_buf) = self.inner.into_parts(); - let (inner, write_buf) = inner.into_parts(); - - FramedParts { - io: inner.0, - codec: inner.1, - read_buf: read_buf, - write_buf: write_buf, - _priv: (), - } - } -} - -impl Stream for Framed -where - T: AsyncRead, - U: Decoder, -{ - type Item = U::Item; - type Error = U::Error; - - fn poll(&mut self) -> Poll, Self::Error> { - self.inner.poll() - } -} - -impl Sink for Framed -where - T: AsyncWrite, - U: Encoder, - U::Error: From, -{ - type SinkItem = U::Item; - type SinkError = U::Error; - - fn start_send( - &mut self, item: Self::SinkItem, - ) -> StartSend { - self.inner.get_mut().start_send(item) - } - - fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { - self.inner.get_mut().poll_complete() - } - - fn close(&mut self) -> Poll<(), Self::SinkError> { - self.inner.get_mut().close() - } -} - -impl fmt::Debug for Framed -where - T: fmt::Debug, - U: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Framed") - .field("io", &self.inner.get_ref().get_ref().0) - .field("codec", &self.inner.get_ref().get_ref().1) - .finish() - } -} - -// ===== impl Fuse ===== - -impl Read for Fuse { - fn read(&mut self, dst: &mut [u8]) -> io::Result { - self.0.read(dst) - } -} - -impl AsyncRead for Fuse { - unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { - self.0.prepare_uninitialized_buffer(buf) - } -} - -impl Write for Fuse { - fn write(&mut self, src: &[u8]) -> io::Result { - self.0.write(src) - } - - fn flush(&mut self) -> io::Result<()> { - self.0.flush() - } -} - -impl AsyncWrite for Fuse { - fn shutdown(&mut self) -> Poll<(), io::Error> { - self.0.shutdown() - } -} - -impl Decoder for Fuse { - type Item = U::Item; - type Error = U::Error; - - fn decode( - &mut self, buffer: &mut BytesMut, - ) -> Result, Self::Error> { - self.1.decode(buffer) - } - - fn decode_eof( - &mut self, buffer: &mut BytesMut, - ) -> Result, Self::Error> { - self.1.decode_eof(buffer) - } -} - -impl Encoder for Fuse { - type Item = U::Item; - type Error = U::Error; - - fn encode( - &mut self, item: Self::Item, dst: &mut BytesMut, - ) -> Result<(), Self::Error> { - self.1.encode(item, dst) - } -} - -/// `FramedParts` contains an export of the data of a Framed transport. -/// It can be used to construct a new `Framed` with a different codec. -/// It contains all current buffers and the inner transport. -#[derive(Debug)] -pub struct FramedParts { - /// The inner transport used to read bytes to and write bytes to - pub io: T, - - /// The codec - pub codec: U, - - /// The buffer with read but unprocessed data. - pub read_buf: BytesMut, - - /// A buffer with unprocessed data which are not written yet. - pub write_buf: BytesMut, - - /// This private field allows us to add additional fields in the future in a - /// backwards compatible way. - _priv: (), -} - -impl FramedParts { - /// Create a new, default, `FramedParts` - pub fn new(io: T, codec: U) -> FramedParts { - FramedParts { - io, - codec, - read_buf: BytesMut::new(), - write_buf: BytesMut::new(), - _priv: (), - } - } -} diff --git a/src/framed/framed_read.rs b/src/framed/framed_read.rs deleted file mode 100644 index 065e29205..000000000 --- a/src/framed/framed_read.rs +++ /dev/null @@ -1,216 +0,0 @@ -use std::fmt; - -use bytes::BytesMut; -use futures::{Async, Poll, Sink, StartSend, Stream}; -use tokio_codec::Decoder; -use tokio_io::AsyncRead; - -use super::framed::Fuse; - -/// A `Stream` of messages decoded from an `AsyncRead`. -pub struct FramedRead { - inner: FramedRead2>, -} - -pub struct FramedRead2 { - inner: T, - eof: bool, - is_readable: bool, - buffer: BytesMut, -} - -const INITIAL_CAPACITY: usize = 8 * 1024; - -// ===== impl FramedRead ===== - -impl FramedRead -where - T: AsyncRead, - D: Decoder, -{ - /// Creates a new `FramedRead` with the given `decoder`. - pub fn new(inner: T, decoder: D) -> FramedRead { - FramedRead { - inner: framed_read2(Fuse(inner, decoder)), - } - } -} - -impl FramedRead { - /// Returns a reference to the underlying I/O stream wrapped by - /// `FramedRead`. - /// - /// Note that care should be taken to not tamper with the underlying stream - /// of data coming in as it may corrupt the stream of frames otherwise - /// being worked with. - pub fn get_ref(&self) -> &T { - &self.inner.inner.0 - } - - /// Returns a mutable reference to the underlying I/O stream wrapped by - /// `FramedRead`. - /// - /// Note that care should be taken to not tamper with the underlying stream - /// of data coming in as it may corrupt the stream of frames otherwise - /// being worked with. - pub fn get_mut(&mut self) -> &mut T { - &mut self.inner.inner.0 - } - - /// Consumes the `FramedRead`, returning its underlying I/O stream. - /// - /// Note that care should be taken to not tamper with the underlying stream - /// of data coming in as it may corrupt the stream of frames otherwise - /// being worked with. - pub fn into_inner(self) -> T { - self.inner.inner.0 - } - - /// Returns a reference to the underlying decoder. - pub fn decoder(&self) -> &D { - &self.inner.inner.1 - } - - /// Returns a mutable reference to the underlying decoder. - pub fn decoder_mut(&mut self) -> &mut D { - &mut self.inner.inner.1 - } -} - -impl Stream for FramedRead -where - T: AsyncRead, - D: Decoder, -{ - type Item = D::Item; - type Error = D::Error; - - fn poll(&mut self) -> Poll, Self::Error> { - self.inner.poll() - } -} - -impl Sink for FramedRead -where - T: Sink, -{ - type SinkItem = T::SinkItem; - type SinkError = T::SinkError; - - fn start_send( - &mut self, item: Self::SinkItem, - ) -> StartSend { - self.inner.inner.0.start_send(item) - } - - fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { - self.inner.inner.0.poll_complete() - } - - fn close(&mut self) -> Poll<(), Self::SinkError> { - self.inner.inner.0.close() - } -} - -impl fmt::Debug for FramedRead -where - T: fmt::Debug, - D: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("FramedRead") - .field("inner", &self.inner.inner.0) - .field("decoder", &self.inner.inner.1) - .field("eof", &self.inner.eof) - .field("is_readable", &self.inner.is_readable) - .field("buffer", &self.inner.buffer) - .finish() - } -} - -// ===== impl FramedRead2 ===== - -pub fn framed_read2(inner: T) -> FramedRead2 { - FramedRead2 { - inner: inner, - eof: false, - is_readable: false, - buffer: BytesMut::with_capacity(INITIAL_CAPACITY), - } -} - -pub fn framed_read2_with_buffer(inner: T, mut buf: BytesMut) -> FramedRead2 { - if buf.capacity() < INITIAL_CAPACITY { - let bytes_to_reserve = INITIAL_CAPACITY - buf.capacity(); - buf.reserve(bytes_to_reserve); - } - FramedRead2 { - inner: inner, - eof: false, - is_readable: buf.len() > 0, - buffer: buf, - } -} - -impl FramedRead2 { - pub fn get_ref(&self) -> &T { - &self.inner - } - - pub fn into_inner(self) -> T { - self.inner - } - - pub fn into_parts(self) -> (T, BytesMut) { - (self.inner, self.buffer) - } - - pub fn get_mut(&mut self) -> &mut T { - &mut self.inner - } -} - -impl Stream for FramedRead2 -where - T: AsyncRead + Decoder, -{ - type Item = T::Item; - type Error = T::Error; - - fn poll(&mut self) -> Poll, Self::Error> { - loop { - // Repeatedly call `decode` or `decode_eof` as long as it is - // "readable". Readable is defined as not having returned `None`. If - // the upstream has returned EOF, and the decoder is no longer - // readable, it can be assumed that the decoder will never become - // readable again, at which point the stream is terminated. - if self.is_readable { - if self.eof { - let frame = try!(self.inner.decode_eof(&mut self.buffer)); - return Ok(Async::Ready(frame)); - } - - trace!("attempting to decode a frame"); - - if let Some(frame) = try!(self.inner.decode(&mut self.buffer)) { - trace!("frame decoded from buffer"); - return Ok(Async::Ready(Some(frame))); - } - - self.is_readable = false; - } - - assert!(!self.eof); - - // Otherwise, try to read more data and try again. Make sure we've - // got room for at least one byte to read to ensure that we don't - // get a spurious 0 that looks like EOF - self.buffer.reserve(1); - if 0 == try_ready!(self.inner.read_buf(&mut self.buffer)) { - self.eof = true; - } - - self.is_readable = true; - } - } -} diff --git a/src/framed/framed_write.rs b/src/framed/framed_write.rs deleted file mode 100644 index 310c76307..000000000 --- a/src/framed/framed_write.rs +++ /dev/null @@ -1,243 +0,0 @@ -use std::fmt; -use std::io::{self, Read}; - -use bytes::BytesMut; -use futures::{Async, AsyncSink, Poll, Sink, StartSend, Stream}; -use tokio_codec::{Decoder, Encoder}; -use tokio_io::{AsyncRead, AsyncWrite}; - -use super::framed::Fuse; - -/// A `Sink` of frames encoded to an `AsyncWrite`. -pub struct FramedWrite { - inner: FramedWrite2>, -} - -pub struct FramedWrite2 { - inner: T, - buffer: BytesMut, -} - -const INITIAL_CAPACITY: usize = 8 * 1024; -const BACKPRESSURE_BOUNDARY: usize = INITIAL_CAPACITY; - -impl FramedWrite -where - T: AsyncWrite, - E: Encoder, -{ - /// Creates a new `FramedWrite` with the given `encoder`. - pub fn new(inner: T, encoder: E) -> FramedWrite { - FramedWrite { - inner: framed_write2(Fuse(inner, encoder)), - } - } -} - -impl FramedWrite { - /// Returns a reference to the underlying I/O stream wrapped by - /// `FramedWrite`. - /// - /// Note that care should be taken to not tamper with the underlying stream - /// of data coming in as it may corrupt the stream of frames otherwise - /// being worked with. - pub fn get_ref(&self) -> &T { - &self.inner.inner.0 - } - - /// Returns a mutable reference to the underlying I/O stream wrapped by - /// `FramedWrite`. - /// - /// Note that care should be taken to not tamper with the underlying stream - /// of data coming in as it may corrupt the stream of frames otherwise - /// being worked with. - pub fn get_mut(&mut self) -> &mut T { - &mut self.inner.inner.0 - } - - /// Consumes the `FramedWrite`, returning its underlying I/O stream. - /// - /// Note that care should be taken to not tamper with the underlying stream - /// of data coming in as it may corrupt the stream of frames otherwise - /// being worked with. - pub fn into_inner(self) -> T { - self.inner.inner.0 - } - - /// Returns a reference to the underlying decoder. - pub fn encoder(&self) -> &E { - &self.inner.inner.1 - } - - /// Returns a mutable reference to the underlying decoder. - pub fn encoder_mut(&mut self) -> &mut E { - &mut self.inner.inner.1 - } -} - -impl Sink for FramedWrite -where - T: AsyncWrite, - E: Encoder, -{ - type SinkItem = E::Item; - type SinkError = E::Error; - - fn start_send(&mut self, item: E::Item) -> StartSend { - self.inner.start_send(item) - } - - fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { - self.inner.poll_complete() - } - - fn close(&mut self) -> Poll<(), Self::SinkError> { - Ok(try!(self.inner.close())) - } -} - -impl Stream for FramedWrite -where - T: Stream, -{ - type Item = T::Item; - type Error = T::Error; - - fn poll(&mut self) -> Poll, Self::Error> { - self.inner.inner.0.poll() - } -} - -impl fmt::Debug for FramedWrite -where - T: fmt::Debug, - U: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("FramedWrite") - .field("inner", &self.inner.get_ref().0) - .field("encoder", &self.inner.get_ref().1) - .field("buffer", &self.inner.buffer) - .finish() - } -} - -// ===== impl FramedWrite2 ===== - -pub fn framed_write2(inner: T) -> FramedWrite2 { - FramedWrite2 { - inner: inner, - buffer: BytesMut::with_capacity(INITIAL_CAPACITY), - } -} - -pub fn framed_write2_with_buffer(inner: T, mut buf: BytesMut) -> FramedWrite2 { - if buf.capacity() < INITIAL_CAPACITY { - let bytes_to_reserve = INITIAL_CAPACITY - buf.capacity(); - buf.reserve(bytes_to_reserve); - } - FramedWrite2 { - inner: inner, - buffer: buf, - } -} - -impl FramedWrite2 { - pub fn get_ref(&self) -> &T { - &self.inner - } - - pub fn into_inner(self) -> T { - self.inner - } - - pub fn into_parts(self) -> (T, BytesMut) { - (self.inner, self.buffer) - } - - pub fn get_mut(&mut self) -> &mut T { - &mut self.inner - } -} - -impl Sink for FramedWrite2 -where - T: AsyncWrite + Encoder, -{ - type SinkItem = T::Item; - type SinkError = T::Error; - - fn start_send(&mut self, item: T::Item) -> StartSend { - // If the buffer is already over 8KiB, then attempt to flush it. If after flushing it's - // *still* over 8KiB, then apply backpressure (reject the send). - if self.buffer.len() >= BACKPRESSURE_BOUNDARY { - try!(self.poll_complete()); - - if self.buffer.len() >= BACKPRESSURE_BOUNDARY { - return Ok(AsyncSink::NotReady(item)); - } - } - - try!(self.inner.encode(item, &mut self.buffer)); - - Ok(AsyncSink::Ready) - } - - fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { - trace!("flushing framed transport"); - - while !self.buffer.is_empty() { - trace!("writing; remaining={}", self.buffer.len()); - - let n = try_ready!(self.inner.poll_write(&self.buffer)); - - if n == 0 { - return Err(io::Error::new( - io::ErrorKind::WriteZero, - "failed to \ - write frame to transport", - ).into()); - } - - // TODO: Add a way to `bytes` to do this w/o returning the drained - // data. - let _ = self.buffer.split_to(n); - } - - // Try flushing the underlying IO - try_ready!(self.inner.poll_flush()); - - trace!("framed transport flushed"); - return Ok(Async::Ready(())); - } - - fn close(&mut self) -> Poll<(), Self::SinkError> { - try_ready!(self.poll_complete()); - Ok(try!(self.inner.shutdown())) - } -} - -impl Decoder for FramedWrite2 { - type Item = T::Item; - type Error = T::Error; - - fn decode(&mut self, src: &mut BytesMut) -> Result, T::Error> { - self.inner.decode(src) - } - - fn decode_eof(&mut self, src: &mut BytesMut) -> Result, T::Error> { - self.inner.decode_eof(src) - } -} - -impl Read for FramedWrite2 { - fn read(&mut self, dst: &mut [u8]) -> io::Result { - self.inner.read(dst) - } -} - -impl AsyncRead for FramedWrite2 { - unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { - self.inner.prepare_uninitialized_buffer(buf) - } -} diff --git a/src/framed/mod.rs b/src/framed/mod.rs deleted file mode 100644 index cb0308fa0..000000000 --- a/src/framed/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! Utilities for encoding and decoding frames. -//! -//! Contains adapters to go from streams of bytes, [`AsyncRead`] and -//! [`AsyncWrite`], to framed streams implementing [`Sink`] and [`Stream`]. -//! Framed streams are also known as [transports]. -//! -//! [`AsyncRead`]: # -//! [`AsyncWrite`]: # -//! [`Sink`]: # -//! [`Stream`]: # -//! [transports]: # - -#![deny(missing_docs, missing_debug_implementations, warnings)] -#![doc(hidden, html_root_url = "https://docs.rs/tokio-codec/0.1.0")] - -// _tokio_codec are the items that belong in the `tokio_codec` crate. However, because we need to -// maintain backward compatibility until the next major breaking change, they are defined here. -// When the next breaking change comes, they should be moved to the `tokio_codec` crate and become -// independent. -// -// The primary reason we can't move these to `tokio-codec` now is because, again for backward -// compatibility reasons, we need to keep `Decoder` and `Encoder` in tokio_io::codec. And `Decoder` -// and `Encoder` needs to reference `Framed`. So they all still need to still be in the same -// module. - -mod framed; -mod framed_read; -mod framed_write; - -pub use self::framed::{Framed, FramedParts}; -pub use self::framed_read::FramedRead; -pub use self::framed_write::FramedWrite; diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 728a78b9f..f20013020 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -1,12 +1,11 @@ -// #![allow(unused_imports, unused_variables, dead_code)] use std::collections::VecDeque; use std::fmt::{Debug, Display}; use std::time::Instant; +use actix_net::codec::Framed; use actix_net::service::Service; use futures::{Async, AsyncSink, Future, Poll, Sink, Stream}; -// use tokio_current_thread::spawn; use tokio_io::{AsyncRead, AsyncWrite}; use tokio_timer::Delay; @@ -16,7 +15,6 @@ use payload::{Payload, PayloadSender, PayloadStatus, PayloadWriter}; use body::Body; use config::ServiceConfig; use error::DispatchError; -use framed::Framed; use request::Request; use response::Response; diff --git a/src/lib.rs b/src/lib.rs index 74e7ced78..9acdf3cdb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -135,9 +135,6 @@ mod request; mod response; mod uri; -#[doc(hidden)] -pub mod framed; - pub mod error; pub mod h1; pub(crate) mod helpers; diff --git a/src/ws/mod.rs b/src/ws/mod.rs index e8bf3870d..bd657d944 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -15,10 +15,12 @@ mod codec; mod frame; mod mask; mod proto; +mod transport; -pub use self::codec::Message; +pub use self::codec::{Codec, Message}; pub use self::frame::Frame; pub use self::proto::{CloseCode, CloseReason, OpCode}; +pub use self::transport::Transport; /// Websocket protocol errors #[derive(Fail, Debug)] diff --git a/src/ws/transport.rs b/src/ws/transport.rs new file mode 100644 index 000000000..aabeb5d5a --- /dev/null +++ b/src/ws/transport.rs @@ -0,0 +1,50 @@ +use actix_net::codec::Framed; +use actix_net::framed::{FramedTransport, FramedTransportError}; +use actix_net::service::{IntoService, Service}; +use futures::{Future, Poll}; +use tokio_io::{AsyncRead, AsyncWrite}; + +use super::{Codec, Message}; + +pub struct Transport +where + S: Service, + T: AsyncRead + AsyncWrite, +{ + inner: FramedTransport, +} + +impl Transport +where + T: AsyncRead + AsyncWrite, + S: Service, + S::Future: 'static, + S::Error: 'static, +{ + pub fn new>(io: T, service: F) -> Self { + Transport { + inner: FramedTransport::new(Framed::new(io, Codec::new()), service), + } + } + + pub fn with>(framed: Framed, service: F) -> Self { + Transport { + inner: FramedTransport::new(framed, service), + } + } +} + +impl Future for Transport +where + T: AsyncRead + AsyncWrite, + S: Service, + S::Future: 'static, + S::Error: 'static, +{ + type Item = (); + type Error = FramedTransportError; + + fn poll(&mut self) -> Poll { + self.inner.poll() + } +} diff --git a/tests/test_ws.rs b/tests/test_ws.rs new file mode 100644 index 000000000..86a309191 --- /dev/null +++ b/tests/test_ws.rs @@ -0,0 +1,111 @@ +extern crate actix; +extern crate actix_http; +extern crate actix_net; +extern crate actix_web; +extern crate bytes; +extern crate futures; + +use std::{io, thread}; + +use actix::System; +use actix_net::codec::Framed; +use actix_net::server::Server; +use actix_net::service::IntoNewService; +use actix_web::{test, ws as web_ws}; +use bytes::Bytes; +use futures::future::{ok, Either}; +use futures::{Future, Sink, Stream}; + +use actix_http::{h1, ws, ResponseError}; + +fn ws_handler(req: ws::Message) -> impl Future { + match req { + ws::Message::Ping(msg) => ok(ws::Message::Pong(msg)), + ws::Message::Text(text) => ok(ws::Message::Text(text)), + ws::Message::Binary(bin) => ok(ws::Message::Binary(bin)), + ws::Message::Close(reason) => ok(ws::Message::Close(reason)), + _ => ok(ws::Message::Close(None)), + } +} + +#[test] +fn test_simple() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + (|io| { + // read http request + let framed = Framed::new(io, h1::Codec::new(false)); + framed + .into_future() + .map_err(|_| ()) + .and_then(|(req, framed)| { + // validate request + if let Some(h1::InMessage::MessageWithPayload(req)) = req { + match ws::handshake(&req) { + Err(e) => { + // validation failed + let resp = e.error_response(); + Either::A( + framed + .send(h1::OutMessage::Response(resp)) + .map_err(|_| ()) + .map(|_| ()), + ) + } + Ok(mut resp) => Either::B( + // send response + framed + .send(h1::OutMessage::Response( + resp.finish(), + )).map_err(|_| ()) + .and_then(|framed| { + // start websocket service + let framed = + framed.into_framed(ws::Codec::new()); + ws::Transport::with(framed, ws_handler) + .map_err(|_| ()) + }), + ), + } + } else { + panic!() + } + }) + }).into_new_service() + }).unwrap() + .run(); + }); + + let mut sys = System::new("test"); + { + let (reader, mut writer) = sys + .block_on(web_ws::Client::new(format!("http://{}/", addr)).connect()) + .unwrap(); + + writer.text("text"); + let (item, reader) = sys.block_on(reader.into_future()).unwrap(); + assert_eq!(item, Some(web_ws::Message::Text("text".to_owned()))); + + writer.binary(b"text".as_ref()); + let (item, reader) = sys.block_on(reader.into_future()).unwrap(); + assert_eq!( + item, + Some(web_ws::Message::Binary(Bytes::from_static(b"text").into())) + ); + + writer.ping("ping"); + let (item, reader) = sys.block_on(reader.into_future()).unwrap(); + assert_eq!(item, Some(web_ws::Message::Pong("ping".to_owned()))); + + writer.close(Some(web_ws::CloseCode::Normal.into())); + let (item, _) = sys.block_on(reader.into_future()).unwrap(); + assert_eq!( + item, + Some(web_ws::Message::Close(Some( + web_ws::CloseCode::Normal.into() + ))) + ); + } +} From c0699a070efc7b72f120cb380a03cf5abaaa3bd8 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 5 Oct 2018 15:39:35 -0700 Subject: [PATCH 019/427] add TakeRequest service; update ws test case --- src/h1/mod.rs | 2 +- src/h1/service.rs | 81 +++++++++++++++++++++++++++++++++++++++++++++-- src/ws/mod.rs | 1 - tests/test_ws.rs | 73 ++++++++++++++++++++---------------------- 4 files changed, 114 insertions(+), 43 deletions(-) diff --git a/src/h1/mod.rs b/src/h1/mod.rs index f9abfea5c..266ebf39c 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -7,4 +7,4 @@ mod service; pub use self::codec::{Codec, InMessage, OutMessage}; pub use self::dispatcher::Dispatcher; -pub use self::service::{H1Service, H1ServiceHandler}; +pub use self::service::{H1Service, H1ServiceHandler, TakeRequest}; diff --git a/src/h1/service.rs b/src/h1/service.rs index 8038c9fb8..02535fc8c 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -1,15 +1,17 @@ use std::fmt::{Debug, Display}; use std::marker::PhantomData; +use actix_net::codec::Framed; use actix_net::service::{IntoNewService, NewService, Service}; -use futures::{Async, Future, Poll}; +use futures::{future, Async, Future, Poll, Stream}; use tokio_io::{AsyncRead, AsyncWrite}; use config::ServiceConfig; -use error::DispatchError; +use error::{DispatchError, ParseError}; use request::Request; use response::Response; +use super::codec::{Codec, InMessage}; use super::dispatcher::Dispatcher; /// `NewService` implementation for HTTP1 transport @@ -121,3 +123,78 @@ where Dispatcher::new(req, self.cfg.clone(), self.srv.clone()) } } + +/// `NewService` that implements, read one request from framed object feature. +pub struct TakeRequest { + _t: PhantomData, +} + +impl TakeRequest { + /// Create new `TakeRequest` instance. + pub fn new() -> Self { + TakeRequest { _t: PhantomData } + } +} + +impl NewService for TakeRequest +where + T: AsyncRead + AsyncWrite, +{ + type Request = Framed; + type Response = (Option, Framed); + type Error = ParseError; + type InitError = (); + type Service = TakeRequestService; + type Future = future::FutureResult; + + fn new_service(&self) -> Self::Future { + future::ok(TakeRequestService { _t: PhantomData }) + } +} + +/// `NewService` that implements, read one request from framed object feature. +pub struct TakeRequestService { + _t: PhantomData, +} + +impl Service for TakeRequestService +where + T: AsyncRead + AsyncWrite, +{ + type Request = Framed; + type Response = (Option, Framed); + type Error = ParseError; + type Future = TakeRequestServiceResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, framed: Self::Request) -> Self::Future { + TakeRequestServiceResponse { + framed: Some(framed), + } + } +} + +pub struct TakeRequestServiceResponse +where + T: AsyncRead + AsyncWrite, +{ + framed: Option>, +} + +impl Future for TakeRequestServiceResponse +where + T: AsyncRead + AsyncWrite, +{ + type Item = (Option, Framed); + type Error = ParseError; + + fn poll(&mut self) -> Poll { + match self.framed.as_mut().unwrap().poll()? { + Async::Ready(item) => Ok(Async::Ready((item, self.framed.take().unwrap()))), + Async::NotReady => Ok(Async::NotReady), + } + } +} diff --git a/src/ws/mod.rs b/src/ws/mod.rs index bd657d944..5ebb502bb 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -3,7 +3,6 @@ //! To setup a `WebSocket`, first do web socket handshake then on success //! convert `Payload` into a `WsStream` stream and then use `WsWriter` to //! communicate with the peer. -//! ``` use std::io; use error::ResponseError; diff --git a/tests/test_ws.rs b/tests/test_ws.rs index 86a309191..a2a18ff28 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -9,8 +9,9 @@ use std::{io, thread}; use actix::System; use actix_net::codec::Framed; +use actix_net::framed::IntoFramed; use actix_net::server::Server; -use actix_net::service::IntoNewService; +use actix_net::service::NewServiceExt; use actix_web::{test, ws as web_ws}; use bytes::Bytes; use futures::future::{ok, Either}; @@ -18,7 +19,7 @@ use futures::{Future, Sink, Stream}; use actix_http::{h1, ws, ResponseError}; -fn ws_handler(req: ws::Message) -> impl Future { +fn ws_service(req: ws::Message) -> impl Future { match req { ws::Message::Ping(msg) => ok(ws::Message::Pong(msg)), ws::Message::Text(text) => ok(ws::Message::Text(text)), @@ -34,46 +35,40 @@ fn test_simple() { thread::spawn(move || { Server::new() .bind("test", addr, move || { - (|io| { - // read http request - let framed = Framed::new(io, h1::Codec::new(false)); - framed - .into_future() - .map_err(|_| ()) - .and_then(|(req, framed)| { - // validate request - if let Some(h1::InMessage::MessageWithPayload(req)) = req { - match ws::handshake(&req) { - Err(e) => { - // validation failed - let resp = e.error_response(); - Either::A( - framed - .send(h1::OutMessage::Response(resp)) - .map_err(|_| ()) - .map(|_| ()), - ) - } - Ok(mut resp) => Either::B( - // send response + IntoFramed::new(|| h1::Codec::new(false)) + .and_then(h1::TakeRequest::new().map_err(|_| ())) + .and_then(|(req, framed): (_, Framed<_, _>)| { + // validate request + if let Some(h1::InMessage::MessageWithPayload(req)) = req { + match ws::handshake(&req) { + Err(e) => { + // validation failed + let resp = e.error_response(); + Either::A( framed - .send(h1::OutMessage::Response( - resp.finish(), - )).map_err(|_| ()) - .and_then(|framed| { - // start websocket service - let framed = - framed.into_framed(ws::Codec::new()); - ws::Transport::with(framed, ws_handler) - .map_err(|_| ()) - }), - ), + .send(h1::OutMessage::Response(resp)) + .map_err(|_| ()) + .map(|_| ()), + ) } - } else { - panic!() + Ok(mut resp) => Either::B( + // send response + framed + .send(h1::OutMessage::Response(resp.finish())) + .map_err(|_| ()) + .and_then(|framed| { + // start websocket service + let framed = + framed.into_framed(ws::Codec::new()); + ws::Transport::with(framed, ws_service) + .map_err(|_| ()) + }), + ), } - }) - }).into_new_service() + } else { + panic!() + } + }) }).unwrap() .run(); }); From ee62814216fb67697f2451803f31f006085c05fc Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 6 Oct 2018 20:31:22 -0700 Subject: [PATCH 020/427] split request decoder and payload decoder --- src/h1/codec.rs | 57 ++++--- src/h1/decoder.rs | 389 +++++++++++++++++++--------------------------- 2 files changed, 199 insertions(+), 247 deletions(-) diff --git a/src/h1/codec.rs b/src/h1/codec.rs index a27e6472c..8ab8f2528 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -4,15 +4,14 @@ use std::io::{self, Write}; use bytes::{BufMut, Bytes, BytesMut}; use tokio_codec::{Decoder, Encoder}; -use super::decoder::H1Decoder; -pub use super::decoder::InMessage; +use super::decoder::{PayloadDecoder, PayloadItem, RequestDecoder}; use super::encoder::{ResponseEncoder, ResponseLength}; use body::Body; use error::ParseError; use helpers; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::{Method, Version}; -use request::RequestPool; +use request::{Request, RequestPool}; use response::Response; bitflags! { @@ -34,9 +33,23 @@ pub enum OutMessage { Payload(Bytes), } +/// Incoming http/1 request +#[derive(Debug)] +pub enum InMessage { + /// Request + Message(Request), + /// Request with payload + MessageWithPayload(Request), + /// Payload chunk + Chunk(Bytes), + /// End of payload + Eof, +} + /// HTTP/1 Codec pub struct Codec { - decoder: H1Decoder, + decoder: RequestDecoder, + payload: Option, version: Version, // encoder part @@ -64,7 +77,8 @@ impl Codec { Flags::empty() }; Codec { - decoder: H1Decoder::with_pool(pool), + decoder: RequestDecoder::with_pool(pool), + payload: None, version: Version::HTTP_11, flags, @@ -234,21 +248,28 @@ impl Decoder for Codec { type Error = ParseError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - let res = self.decoder.decode(src); - - match res { - Ok(Some(InMessage::Message(ref req))) - | Ok(Some(InMessage::MessageWithPayload(ref req))) => { - self.flags - .set(Flags::HEAD, req.inner.method == Method::HEAD); - self.version = req.inner.version; - if self.flags.contains(Flags::KEEPALIVE_ENABLED) { - self.flags.set(Flags::KEEPALIVE, req.keep_alive()); - } + if self.payload.is_some() { + Ok(match self.payload.as_mut().unwrap().decode(src)? { + Some(PayloadItem::Chunk(chunk)) => Some(InMessage::Chunk(chunk)), + Some(PayloadItem::Eof) => Some(InMessage::Eof), + None => None, + }) + } else if let Some((req, payload)) = self.decoder.decode(src)? { + self.flags + .set(Flags::HEAD, req.inner.method == Method::HEAD); + self.version = req.inner.version; + if self.flags.contains(Flags::KEEPALIVE_ENABLED) { + self.flags.set(Flags::KEEPALIVE, req.keep_alive()); } - _ => (), + self.payload = payload; + if self.payload.is_some() { + Ok(Some(InMessage::MessageWithPayload(req))) + } else { + Ok(Some(InMessage::Message(req))) + } + } else { + Ok(None) } - res } } diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 48776226b..fb29f033c 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -3,6 +3,7 @@ use std::{io, mem}; use bytes::{Bytes, BytesMut}; use futures::{Async, Poll}; use httparse; +use tokio_codec::Decoder; use error::ParseError; use http::header::{HeaderName, HeaderValue}; @@ -13,75 +14,25 @@ use uri::Url; const MAX_BUFFER_SIZE: usize = 131_072; const MAX_HEADERS: usize = 96; -pub(crate) struct H1Decoder { - decoder: Option, - pool: &'static RequestPool, +pub struct RequestDecoder(&'static RequestPool); + +impl RequestDecoder { + pub(crate) fn with_pool(pool: &'static RequestPool) -> RequestDecoder { + RequestDecoder(pool) + } } -/// Incoming http/1 request -#[derive(Debug)] -pub enum InMessage { - /// Request - Message(Request), - /// Request with payload - MessageWithPayload(Request), - /// Payload chunk - Chunk(Bytes), - /// End of payload - Eof, +impl Default for RequestDecoder { + fn default() -> RequestDecoder { + RequestDecoder::with_pool(RequestPool::pool()) + } } -impl H1Decoder { - #[cfg(test)] - pub fn new() -> H1Decoder { - H1Decoder::with_pool(RequestPool::pool()) - } +impl Decoder for RequestDecoder { + type Item = (Request, Option); + type Error = ParseError; - pub fn with_pool(pool: &'static RequestPool) -> H1Decoder { - H1Decoder { - pool, - decoder: None, - } - } - - pub fn decode( - &mut self, src: &mut BytesMut, - ) -> Result, ParseError> { - // read payload - if self.decoder.is_some() { - match self.decoder.as_mut().unwrap().decode(src)? { - Async::Ready(Some(bytes)) => return Ok(Some(InMessage::Chunk(bytes))), - Async::Ready(None) => { - self.decoder.take(); - return Ok(Some(InMessage::Eof)); - } - Async::NotReady => return Ok(None), - } - } - - match self.parse_message(src)? { - Async::Ready((msg, decoder)) => { - self.decoder = decoder; - if self.decoder.is_some() { - Ok(Some(InMessage::MessageWithPayload(msg))) - } else { - Ok(Some(InMessage::Message(msg))) - } - } - Async::NotReady => { - if src.len() >= MAX_BUFFER_SIZE { - error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); - Err(ParseError::TooLarge) - } else { - Ok(None) - } - } - } - } - - fn parse_message( - &self, buf: &mut BytesMut, - ) -> Poll<(Request, Option), ParseError> { + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { // Parse http message let mut has_upgrade = false; let mut chunked = false; @@ -98,7 +49,7 @@ impl H1Decoder { unsafe { mem::uninitialized() }; let mut req = httparse::Request::new(&mut parsed); - match req.parse(buf)? { + match req.parse(src)? { httparse::Status::Complete(len) => { let method = Method::from_bytes(req.method.unwrap().as_bytes()) .map_err(|_| ParseError::Method)?; @@ -108,18 +59,18 @@ impl H1Decoder { } else { Version::HTTP_10 }; - HeaderIndex::record(buf, req.headers, &mut headers); + HeaderIndex::record(src, req.headers, &mut headers); (len, method, path, version, req.headers.len()) } - httparse::Status::Partial => return Ok(Async::NotReady), + httparse::Status::Partial => return Ok(None), } }; - let slice = buf.split_to(len).freeze(); + let slice = src.split_to(len).freeze(); // convert headers - let mut msg = RequestPool::get(self.pool); + let mut msg = RequestPool::get(self.0); { let inner = msg.inner_mut(); inner @@ -198,18 +149,21 @@ impl H1Decoder { // https://tools.ietf.org/html/rfc7230#section-3.3.3 let decoder = if chunked { // Chunked encoding - Some(EncodingDecoder::chunked()) + Some(PayloadDecoder::chunked()) } else if let Some(len) = content_length { // Content-Length - Some(EncodingDecoder::length(len)) + Some(PayloadDecoder::length(len)) } else if has_upgrade || msg.inner.method == Method::CONNECT { // upgrade(websocket) or connect - Some(EncodingDecoder::eof()) + Some(PayloadDecoder::eof()) + } else if src.len() >= MAX_BUFFER_SIZE { + error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); + return Err(ParseError::TooLarge); } else { None }; - Ok(Async::Ready((msg, decoder))) + Ok(Some((msg, decoder))) } } @@ -235,30 +189,37 @@ impl HeaderIndex { } } +#[derive(Debug, Clone)] +/// Http payload item +pub enum PayloadItem { + Chunk(Bytes), + Eof, +} + /// Decoders to handle different Transfer-Encodings. /// /// If a message body does not include a Transfer-Encoding, it *should* /// include a Content-Length header. #[derive(Debug, Clone, PartialEq)] -pub struct EncodingDecoder { +pub struct PayloadDecoder { kind: Kind, } -impl EncodingDecoder { - pub fn length(x: u64) -> EncodingDecoder { - EncodingDecoder { +impl PayloadDecoder { + pub fn length(x: u64) -> PayloadDecoder { + PayloadDecoder { kind: Kind::Length(x), } } - pub fn chunked() -> EncodingDecoder { - EncodingDecoder { + pub fn chunked() -> PayloadDecoder { + PayloadDecoder { kind: Kind::Chunked(ChunkedState::Size, 0), } } - pub fn eof() -> EncodingDecoder { - EncodingDecoder { + pub fn eof() -> PayloadDecoder { + PayloadDecoder { kind: Kind::Eof(false), } } @@ -302,53 +263,59 @@ enum ChunkedState { End, } -impl EncodingDecoder { - pub fn decode(&mut self, body: &mut BytesMut) -> Poll, io::Error> { +impl Decoder for PayloadDecoder { + type Item = PayloadItem; + type Error = io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { match self.kind { Kind::Length(ref mut remaining) => { if *remaining == 0 { - Ok(Async::Ready(None)) + Ok(Some(PayloadItem::Eof)) } else { - if body.is_empty() { - return Ok(Async::NotReady); + if src.is_empty() { + return Ok(None); } - let len = body.len() as u64; + let len = src.len() as u64; let buf; if *remaining > len { - buf = body.take().freeze(); + buf = src.take().freeze(); *remaining -= len; } else { - buf = body.split_to(*remaining as usize).freeze(); + buf = src.split_to(*remaining as usize).freeze(); *remaining = 0; - } + }; trace!("Length read: {}", buf.len()); - Ok(Async::Ready(Some(buf))) + Ok(Some(PayloadItem::Chunk(buf))) } } Kind::Chunked(ref mut state, ref mut size) => { loop { let mut buf = None; // advances the chunked state - *state = try_ready!(state.step(body, size, &mut buf)); + *state = match state.step(src, size, &mut buf)? { + Async::NotReady => return Ok(None), + Async::Ready(state) => state, + }; if *state == ChunkedState::End { trace!("End of chunked stream"); - return Ok(Async::Ready(None)); + return Ok(Some(PayloadItem::Eof)); } if let Some(buf) = buf { - return Ok(Async::Ready(Some(buf))); + return Ok(Some(PayloadItem::Chunk(buf))); } - if body.is_empty() { - return Ok(Async::NotReady); + if src.is_empty() { + return Ok(None); } } } Kind::Eof(ref mut is_eof) => { if *is_eof { - Ok(Async::Ready(None)) - } else if !body.is_empty() { - Ok(Async::Ready(Some(body.take().freeze()))) + Ok(Some(PayloadItem::Eof)) + } else if !src.is_empty() { + Ok(Some(PayloadItem::Chunk(src.take().freeze()))) } else { - Ok(Async::NotReady) + Ok(None) } } } @@ -536,15 +503,18 @@ mod tests { _ => panic!("error"), } } + } + + impl PayloadItem { fn chunk(self) -> Bytes { match self { - InMessage::Chunk(chunk) => chunk, + PayloadItem::Chunk(chunk) => chunk, _ => panic!("error"), } } fn eof(&self) -> bool { match *self { - InMessage::Eof => true, + PayloadItem::Eof => true, _ => false, } } @@ -552,8 +522,8 @@ mod tests { macro_rules! parse_ready { ($e:expr) => {{ - match H1Decoder::new().decode($e) { - Ok(Some(msg)) => msg.message(), + match RequestDecoder::default().decode($e) { + Ok(Some((msg, _))) => msg, Ok(_) => unreachable!("Eof during parsing http request"), Err(err) => unreachable!("Error during parsing http request: {:?}", err), } @@ -562,7 +532,7 @@ mod tests { macro_rules! expect_parse_err { ($e:expr) => {{ - match H1Decoder::new().decode($e) { + match RequestDecoder::default().decode($e) { Err(err) => match err { ParseError::Io(_) => unreachable!("Parse error expected"), _ => (), @@ -641,10 +611,9 @@ mod tests { fn test_parse() { let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); - let mut reader = H1Decoder::new(); + let mut reader = RequestDecoder::default(); match reader.decode(&mut buf) { - Ok(Some(msg)) => { - let req = msg.message(); + Ok(Some((req, _))) => { assert_eq!(req.version(), Version::HTTP_11); assert_eq!(*req.method(), Method::GET); assert_eq!(req.path(), "/test"); @@ -657,38 +626,25 @@ mod tests { fn test_parse_partial() { let mut buf = BytesMut::from("PUT /test HTTP/1"); - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf) { - Ok(None) => (), - _ => unreachable!("Error"), - } + let mut reader = RequestDecoder::default(); + assert!(reader.decode(&mut buf).unwrap().is_none()); buf.extend(b".1\r\n\r\n"); - match reader.decode(&mut buf) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::PUT); - assert_eq!(req.path(), "/test"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } + let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); + assert_eq!(req.version(), Version::HTTP_11); + assert_eq!(*req.method(), Method::PUT); + assert_eq!(req.path(), "/test"); } #[test] fn test_parse_post() { let mut buf = BytesMut::from("POST /test2 HTTP/1.0\r\n\r\n"); - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_10); - assert_eq!(*req.method(), Method::POST); - assert_eq!(req.path(), "/test2"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } + let mut reader = RequestDecoder::default(); + let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); + assert_eq!(req.version(), Version::HTTP_10); + assert_eq!(*req.method(), Method::POST); + assert_eq!(req.path(), "/test2"); } #[test] @@ -696,20 +652,16 @@ mod tests { let mut buf = BytesMut::from("GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - assert_eq!( - reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), - b"body" - ); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } + let mut reader = RequestDecoder::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert_eq!(req.version(), Version::HTTP_11); + assert_eq!(*req.method(), Method::GET); + assert_eq!(req.path(), "/test"); + assert_eq!( + pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + b"body" + ); } #[test] @@ -717,45 +669,36 @@ mod tests { let mut buf = BytesMut::from("\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - assert_eq!( - reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), - b"body" - ); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } + let mut reader = RequestDecoder::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert_eq!(req.version(), Version::HTTP_11); + assert_eq!(*req.method(), Method::GET); + assert_eq!(req.path(), "/test"); + assert_eq!( + pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + b"body" + ); } #[test] fn test_parse_partial_eof() { let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); - let mut reader = H1Decoder::new(); + let mut reader = RequestDecoder::default(); assert!(reader.decode(&mut buf).unwrap().is_none()); buf.extend(b"\r\n"); - match reader.decode(&mut buf) { - Ok(Some(msg)) => { - let req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } + let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); + assert_eq!(req.version(), Version::HTTP_11); + assert_eq!(*req.method(), Method::GET); + assert_eq!(req.path(), "/test"); } #[test] fn test_headers_split_field() { let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); - let mut reader = H1Decoder::new(); + let mut reader = RequestDecoder::default(); assert!{ reader.decode(&mut buf).unwrap().is_none() } buf.extend(b"t"); @@ -765,16 +708,11 @@ mod tests { assert!{ reader.decode(&mut buf).unwrap().is_none() } buf.extend(b"t: value\r\n\r\n"); - match reader.decode(&mut buf) { - Ok(Some(msg)) => { - let req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - assert_eq!(req.headers().get("test").unwrap().as_bytes(), b"value"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } + let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); + assert_eq!(req.version(), Version::HTTP_11); + assert_eq!(*req.method(), Method::GET); + assert_eq!(req.path(), "/test"); + assert_eq!(req.headers().get("test").unwrap().as_bytes(), b"value"); } #[test] @@ -784,9 +722,8 @@ mod tests { Set-Cookie: c1=cookie1\r\n\ Set-Cookie: c2=cookie2\r\n\r\n", ); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - let req = msg.message(); + let mut reader = RequestDecoder::default(); + let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); let val: Vec<_> = req .headers() @@ -985,14 +922,13 @@ mod tests { upgrade: websocket\r\n\r\n\ some raw data", ); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); + let mut reader = RequestDecoder::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); assert!(!req.keep_alive()); assert!(req.upgrade()); assert_eq!( - reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), b"some raw data" ); } @@ -1039,22 +975,21 @@ mod tests { "GET /test HTTP/1.1\r\n\ transfer-encoding: chunked\r\n\r\n", ); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); + let mut reader = RequestDecoder::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); assert!(req.chunked().unwrap()); buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); assert_eq!( - reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), b"data" ); assert_eq!( - reader.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), b"line" ); - assert!(reader.decode(&mut buf).unwrap().unwrap().eof()); + assert!(pl.decode(&mut buf).unwrap().unwrap().eof()); } #[test] @@ -1063,10 +998,9 @@ mod tests { "GET /test HTTP/1.1\r\n\ transfer-encoding: chunked\r\n\r\n", ); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); + let mut reader = RequestDecoder::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); assert!(req.chunked().unwrap()); buf.extend( @@ -1075,19 +1009,17 @@ mod tests { transfer-encoding: chunked\r\n\r\n" .iter(), ); - let msg = reader.decode(&mut buf).unwrap().unwrap(); + let msg = pl.decode(&mut buf).unwrap().unwrap(); assert_eq!(msg.chunk().as_ref(), b"data"); - let msg = reader.decode(&mut buf).unwrap().unwrap(); + let msg = pl.decode(&mut buf).unwrap().unwrap(); assert_eq!(msg.chunk().as_ref(), b"line"); - let msg = reader.decode(&mut buf).unwrap().unwrap(); + let msg = pl.decode(&mut buf).unwrap().unwrap(); assert!(msg.eof()); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert!(msg.is_payload()); - let req2 = msg.message(); - assert!(req2.chunked().unwrap()); - assert_eq!(*req2.method(), Method::POST); - assert!(req2.chunked().unwrap()); + let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); + assert!(req.chunked().unwrap()); + assert_eq!(*req.method(), Method::POST); + assert!(req.chunked().unwrap()); } #[test] @@ -1097,30 +1029,29 @@ mod tests { transfer-encoding: chunked\r\n\r\n", ); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); + let mut reader = RequestDecoder::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); assert!(req.chunked().unwrap()); buf.extend(b"4\r\n1111\r\n"); - let msg = reader.decode(&mut buf).unwrap().unwrap(); + let msg = pl.decode(&mut buf).unwrap().unwrap(); assert_eq!(msg.chunk().as_ref(), b"1111"); buf.extend(b"4\r\ndata\r"); - let msg = reader.decode(&mut buf).unwrap().unwrap(); + let msg = pl.decode(&mut buf).unwrap().unwrap(); assert_eq!(msg.chunk().as_ref(), b"data"); buf.extend(b"\n4"); - assert!(reader.decode(&mut buf).unwrap().is_none()); + assert!(pl.decode(&mut buf).unwrap().is_none()); buf.extend(b"\r"); - assert!(reader.decode(&mut buf).unwrap().is_none()); + assert!(pl.decode(&mut buf).unwrap().is_none()); buf.extend(b"\n"); - assert!(reader.decode(&mut buf).unwrap().is_none()); + assert!(pl.decode(&mut buf).unwrap().is_none()); buf.extend(b"li"); - let msg = reader.decode(&mut buf).unwrap().unwrap(); + let msg = pl.decode(&mut buf).unwrap().unwrap(); assert_eq!(msg.chunk().as_ref(), b"li"); //trailers @@ -1128,12 +1059,12 @@ mod tests { //not_ready!(reader.parse(&mut buf, &mut readbuf)); buf.extend(b"ne\r\n0\r\n"); - let msg = reader.decode(&mut buf).unwrap().unwrap(); + let msg = pl.decode(&mut buf).unwrap().unwrap(); assert_eq!(msg.chunk().as_ref(), b"ne"); - assert!(reader.decode(&mut buf).unwrap().is_none()); + assert!(pl.decode(&mut buf).unwrap().is_none()); buf.extend(b"\r\n"); - assert!(reader.decode(&mut buf).unwrap().unwrap().eof()); + assert!(pl.decode(&mut buf).unwrap().unwrap().eof()); } #[test] @@ -1143,17 +1074,17 @@ mod tests { transfer-encoding: chunked\r\n\r\n"[..], ); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf).unwrap().unwrap(); - assert!(msg.is_payload()); - assert!(msg.message().chunked().unwrap()); + let mut reader = RequestDecoder::default(); + let (msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert!(msg.chunked().unwrap()); buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n") - let chunk = reader.decode(&mut buf).unwrap().unwrap().chunk(); + let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk(); assert_eq!(chunk, Bytes::from_static(b"data")); - let chunk = reader.decode(&mut buf).unwrap().unwrap().chunk(); + let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk(); assert_eq!(chunk, Bytes::from_static(b"line")); - let msg = reader.decode(&mut buf).unwrap().unwrap(); + let msg = pl.decode(&mut buf).unwrap().unwrap(); assert!(msg.eof()); } } From c368abdf5fcce199b7b2ff3560a7476e63da90ba Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 6 Oct 2018 20:34:19 -0700 Subject: [PATCH 021/427] remove Json type --- src/json.rs | 101 ---------------------------------------------------- src/lib.rs | 1 - 2 files changed, 102 deletions(-) diff --git a/src/json.rs b/src/json.rs index a52884894..e2c99ba3f 100644 --- a/src/json.rs +++ b/src/json.rs @@ -1,8 +1,6 @@ use bytes::BytesMut; use futures::{Future, Poll, Stream}; use http::header::CONTENT_LENGTH; -use std::fmt; -use std::ops::{Deref, DerefMut}; use mime; use serde::de::DeserializeOwned; @@ -11,105 +9,6 @@ use serde_json; use error::JsonPayloadError; use httpmessage::HttpMessage; -/// Json helper -/// -/// Json can be used for two different purpose. First is for json response -/// generation and second is for extracting typed information from request's -/// payload. -/// -/// To extract typed information from request's body, the type `T` must -/// implement the `Deserialize` trait from *serde*. -/// -/// [**JsonConfig**](dev/struct.JsonConfig.html) allows to configure extraction -/// process. -/// -/// ## Example -/// -/// ```rust,ignore -/// # extern crate actix_web; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{App, Json, Result, http}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// deserialize `Info` from request's body -/// fn index(info: Json) -> Result { -/// Ok(format!("Welcome {}!", info.username)) -/// } -/// -/// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| r.method(http::Method::POST).with(index)); // <- use `with` extractor -/// } -/// ``` -/// -/// The `Json` type allows you to respond with well-formed JSON data: simply -/// return a value of type Json where T is the type of a structure -/// to serialize into *JSON*. The type `T` must implement the `Serialize` -/// trait from *serde*. -/// -/// ```rust,ignore -/// # extern crate actix_web; -/// # #[macro_use] extern crate serde_derive; -/// # use actix_web::*; -/// # -/// #[derive(Serialize)] -/// struct MyObj { -/// name: String, -/// } -/// -/// fn index(req: HttpRequest) -> Result> { -/// Ok(Json(MyObj { -/// name: req.match_info().query("name")?, -/// })) -/// } -/// # fn main() {} -/// ``` -pub struct Json(pub T); - -impl Json { - /// Deconstruct to an inner value - pub fn into_inner(self) -> T { - self.0 - } -} - -impl Deref for Json { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Json { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl fmt::Debug for Json -where - T: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Json: {:?}", self.0) - } -} - -impl fmt::Display for Json -where - T: fmt::Display, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} - /// Request payload json parser that resolves to a deserialized `T` value. /// /// Returns error: diff --git a/src/lib.rs b/src/lib.rs index 9acdf3cdb..32f78517b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -144,7 +144,6 @@ pub use body::{Binary, Body}; pub use error::{Error, ResponseError, Result}; pub use extensions::Extensions; pub use httpmessage::HttpMessage; -pub use json::Json; pub use request::Request; pub use response::Response; From 87b83a34034416dcaed9ded3fea442a21c48f211 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 6 Oct 2018 21:07:32 -0700 Subject: [PATCH 022/427] update tests, remove unused deps --- Cargo.toml | 2 - src/error.rs | 27 ------------- src/h1/dispatcher.rs | 88 +++++++++++++++++++++---------------------- src/lib.rs | 2 - tests/test_h1v2.rs | 46 ---------------------- tests/test_server.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 133 insertions(+), 122 deletions(-) delete mode 100644 tests/test_h1v2.rs create mode 100644 tests/test_server.rs diff --git a/Cargo.toml b/Cargo.toml index 455b61488..48f199ed8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,6 @@ time = "0.1" encoding = "0.2" lazy_static = "1.0" serde_urlencoded = "^0.5.3" -url = { version="1.7", features=["query_encoding"] } cookie = { version="0.11", features=["percent-encode"] } failure = "^0.1.2" @@ -81,7 +80,6 @@ tokio = "0.1" tokio-io = "0.1" tokio-tcp = "0.1" tokio-timer = "0.2" -tokio-reactor = "0.1" tokio-current-thread = "0.1" # native-tls diff --git a/src/error.rs b/src/error.rs index dc2d45b84..26b3ca56d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,7 +16,6 @@ use serde::de::value::Error as DeError; use serde_json::error::Error as JsonError; use serde_urlencoded::ser::Error as FormError; use tokio_timer::Error as TimerError; -pub use url::ParseError as UrlParseError; // re-exports pub use cookie::ParseError as CookieParseError; @@ -196,9 +195,6 @@ impl ResponseError for FormError {} /// `InternalServerError` for `TimerError` impl ResponseError for TimerError {} -/// `InternalServerError` for `UrlParseError` -impl ResponseError for UrlParseError {} - /// Return `BAD_REQUEST` for `de::value::Error` impl ResponseError for DeError { fn error_response(&self) -> Response { @@ -552,29 +548,6 @@ impl From for ReadlinesError { } } -/// Errors which can occur when attempting to generate resource uri. -#[derive(Fail, Debug, PartialEq)] -pub enum UrlGenerationError { - /// Resource not found - #[fail(display = "Resource not found")] - ResourceNotFound, - /// Not all path pattern covered - #[fail(display = "Not all path pattern covered")] - NotEnoughElements, - /// URL parse error - #[fail(display = "{}", _0)] - ParseError(#[cause] UrlParseError), -} - -/// `InternalServerError` for `UrlGeneratorError` -impl ResponseError for UrlGenerationError {} - -impl From for UrlGenerationError { - fn from(err: UrlParseError) -> Self { - UrlGenerationError::ParseError(err) - } -} - /// Helper type that can wrap any error and generate custom response. /// /// In following example any `io::Error` will be converted into "BAD REQUEST" diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index f20013020..a39967a22 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -48,12 +48,17 @@ where state: State, payload: Option, - messages: VecDeque, + messages: VecDeque, ka_expire: Instant, ka_timer: Option, } +enum Message { + Item(Request), + Error(Response), +} + enum State { None, Response(S::Future), @@ -131,32 +136,11 @@ where } // if checked is set to true, delay disconnect until all tasks have finished. - fn client_disconnected(&mut self, _checked: bool) { + fn client_disconnected(&mut self) { self.flags.insert(Flags::READ_DISCONNECTED); if let Some(mut payload) = self.payload.take() { payload.set_error(PayloadError::Incomplete); } - - // if !checked || self.tasks.is_empty() { - // self.flags - // .insert(Flags::WRITE_DISCONNECTED | Flags::FLUSHED); - - // // notify tasks - // for mut task in self.tasks.drain(..) { - // task.disconnected(); - // match task.poll_completed() { - // Ok(Async::NotReady) => { - // // spawn not completed task, it does not require access to io - // // at this point - // spawn(HttpHandlerTaskFut::new(task.into_task())); - // } - // Ok(Async::Ready(_)) => (), - // Err(err) => { - // error!("Unhandled application error: {}", err); - // } - // } - // } - // } } /// Flush stream @@ -166,7 +150,7 @@ where Ok(Async::NotReady) => Ok(Async::NotReady), Err(err) => { debug!("Error sending data: {}", err); - self.client_disconnected(false); + self.client_disconnected(); Err(err.into()) } Ok(Async::Ready(_)) => { @@ -192,19 +176,28 @@ where let state = match self.state { State::None => loop { break if let Some(msg) = self.messages.pop_front() { - let mut task = self.service.call(msg); - match task.poll() { - Ok(Async::Ready(res)) => { - if res.body().is_streaming() { - unimplemented!() - } else { - Some(Ok(State::SendResponse(Some( - OutMessage::Response(res), - )))) + match msg { + Message::Item(msg) => { + let mut task = self.service.call(msg); + match task.poll() { + Ok(Async::Ready(res)) => { + if res.body().is_streaming() { + unimplemented!() + } else { + Some(Ok(State::SendResponse(Some( + OutMessage::Response(res), + )))) + } + } + Ok(Async::NotReady) => { + Some(Ok(State::Response(task))) + } + Err(err) => Some(Err(DispatchError::Service(err))), } } - Ok(Async::NotReady) => Some(Ok(State::Response(task))), - Err(err) => Some(Err(DispatchError::Service(err))), + Message::Error(res) => Some(Ok(State::SendResponse(Some( + OutMessage::Response(res), + )))), } } else { None @@ -249,7 +242,8 @@ where } } State::SendResponseWithPayload(ref mut item) => { - let (msg, body) = item.take().expect("SendResponse is empty"); + let (msg, body) = + item.take().expect("SendResponseWithPayload is empty"); match self.framed.start_send(msg) { Ok(AsyncSink::Ready) => { self.flags.set( @@ -271,8 +265,7 @@ where match state { Some(Ok(state)) => self.state = state, Some(Err(err)) => { - // error!("Unhandled error1: {}", err); - self.client_disconnected(false); + self.client_disconnected(); return Err(err); } None => { @@ -310,12 +303,12 @@ where Ok(Async::NotReady) => self.state = State::Response(task), Err(err) => { error!("Unhandled application error: {}", err); - self.client_disconnected(false); + self.client_disconnected(); return Err(DispatchError::Service(err)); } } } else { - self.messages.push_back(msg); + self.messages.push_back(Message::Item(msg)); } } InMessage::MessageWithPayload(msg) => { @@ -324,7 +317,7 @@ where *msg.inner.payload.borrow_mut() = Some(pl); self.payload = Some(ps); - self.messages.push_back(msg); + self.messages.push_back(Message::Item(msg)); } InMessage::Chunk(chunk) => { if let Some(ref mut payload) = self.payload { @@ -332,7 +325,9 @@ where } else { error!("Internal server error: unexpected payload chunk"); self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); - // self.push_response_entry(StatusCode::INTERNAL_SERVER_ERROR); + self.messages.push_back(Message::Error( + Response::InternalServerError().finish(), + )); self.error = Some(DispatchError::InternalError); } } @@ -342,7 +337,9 @@ where } else { error!("Internal server error: unexpected eof"); self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); - // self.push_response_entry(StatusCode::INTERNAL_SERVER_ERROR); + self.messages.push_back(Message::Error( + Response::InternalServerError().finish(), + )); self.error = Some(DispatchError::InternalError); } } @@ -363,7 +360,7 @@ where } Ok(Async::Ready(None)) => { if self.flags.contains(Flags::READ_DISCONNECTED) { - self.client_disconnected(true); + self.client_disconnected(); } break; } @@ -378,7 +375,8 @@ where } // Malformed requests should be responded with 400 - // self.push_response_entry(StatusCode::BAD_REQUEST); + self.messages + .push_back(Message::Error(Response::BadRequest().finish())); self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); self.error = Some(DispatchError::MalformedRequest); break; diff --git a/src/lib.rs b/src/lib.rs index 32f78517b..4ec097266 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -112,12 +112,10 @@ extern crate tokio; extern crate tokio_codec; extern crate tokio_current_thread; extern crate tokio_io; -extern crate tokio_reactor; extern crate tokio_tcp; extern crate tokio_timer; #[cfg(all(unix, feature = "uds"))] extern crate tokio_uds; -extern crate url; #[cfg(test)] #[macro_use] diff --git a/tests/test_h1v2.rs b/tests/test_h1v2.rs deleted file mode 100644 index 1866f29b4..000000000 --- a/tests/test_h1v2.rs +++ /dev/null @@ -1,46 +0,0 @@ -extern crate actix; -extern crate actix_http; -extern crate actix_net; -extern crate actix_web; -extern crate futures; - -use std::thread; - -use actix::System; -use actix_net::server::Server; -use actix_web::{client, test}; -use futures::future; - -use actix_http::{h1, Error, KeepAlive, Response, ServiceConfig}; - -#[test] -fn test_h1_v2() { - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - let settings = ServiceConfig::build() - .keep_alive(KeepAlive::Disabled) - .client_timeout(1000) - .client_disconnect(1000) - .server_hostname("localhost") - .server_address(addr) - .finish(); - - h1::H1Service::new(settings, |req| { - println!("REQ: {:?}", req); - future::ok::<_, Error>(Response::Ok().finish()) - }) - }).unwrap() - .run(); - }); - - let mut sys = System::new("test"); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } -} diff --git a/tests/test_server.rs b/tests/test_server.rs new file mode 100644 index 000000000..cc21416c0 --- /dev/null +++ b/tests/test_server.rs @@ -0,0 +1,90 @@ +extern crate actix; +extern crate actix_http; +extern crate actix_net; +extern crate actix_web; +extern crate futures; + +use std::{io::Read, io::Write, net, thread, time}; + +use actix::System; +use actix_net::server::Server; +use actix_web::{client, test}; +use futures::future; + +use actix_http::{h1, Error, KeepAlive, Response, ServiceConfig}; + +#[test] +fn test_h1_v2() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + let settings = ServiceConfig::build() + .keep_alive(KeepAlive::Disabled) + .client_timeout(1000) + .client_disconnect(1000) + .server_hostname("localhost") + .server_address(addr) + .finish(); + + h1::H1Service::new(settings, |_| { + future::ok::<_, Error>(Response::Ok().finish()) + }) + }).unwrap() + .run(); + }); + + let mut sys = System::new("test"); + { + let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) + .finish() + .unwrap(); + let response = sys.block_on(req.send()).unwrap(); + assert!(response.status().is_success()); + } +} + +#[test] +fn test_slow_request() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + let settings = ServiceConfig::build().client_timeout(100).finish(); + + h1::H1Service::new(settings, |_| { + future::ok::<_, Error>(Response::Ok().finish()) + }) + }).unwrap() + .run(); + }); + thread::sleep(time::Duration::from_millis(100)); + + let mut stream = net::TcpStream::connect(addr).unwrap(); + let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n"); + let mut data = String::new(); + let _ = stream.read_to_string(&mut data); + assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); +} + +#[test] +fn test_malformed_request() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + let settings = ServiceConfig::build().client_timeout(100).finish(); + h1::H1Service::new(settings, |_| { + future::ok::<_, Error>(Response::Ok().finish()) + }) + }).unwrap() + .run(); + }); + thread::sleep(time::Duration::from_millis(100)); + + let mut stream = net::TcpStream::connect(addr).unwrap(); + let _ = stream.write_all(b"GET /test/tests/test HTTP1.1\r\n"); + let mut data = String::new(); + let _ = stream.read_to_string(&mut data); + assert!(data.starts_with("HTTP/1.1 400 Bad Request")); +} From 25af82c45a01d97ae65011665fa819c0edaa31df Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 6 Oct 2018 21:14:02 -0700 Subject: [PATCH 023/427] cleanup dependencies --- .travis.yml | 17 +++-------------- Cargo.toml | 35 +---------------------------------- src/lib.rs | 14 -------------- src/response.rs | 4 +--- 4 files changed, 5 insertions(+), 65 deletions(-) diff --git a/.travis.yml b/.travis.yml index aa8a44f25..feae30596 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,17 +14,6 @@ matrix: allow_failures: - rust: nightly -env: - global: - # - RUSTFLAGS="-C link-dead-code" - - OPENSSL_VERSION=openssl-1.0.2 - -before_install: - - sudo add-apt-repository -y ppa:0k53d-karl-f830m/openssl - - sudo apt-get update -qq - - sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev - -# Add clippy before_script: - export PATH=$PATH:~/.cargo/bin @@ -32,12 +21,12 @@ script: - | if [[ "$TRAVIS_RUST_VERSION" != "nightly" ]]; then cargo clean - cargo test --features="ssl,tls,rust-tls" -- --nocapture + cargo test fi - | if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install -f cargo-tarpaulin - RUST_BACKTRACE=1 cargo tarpaulin --features="ssl,tls,rust-tls" --out Xml + RUST_BACKTRACE=1 cargo tarpaulin --out Xml bash <(curl -s https://codecov.io/bash) echo "Uploaded code coverage" fi @@ -46,7 +35,7 @@ script: #after_success: # - | # if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then -# cargo doc --features "ssl,tls,rust-tls,session" --no-deps && +# cargo doc --features "session" --no-deps && # echo "" > target/doc/index.html && # git clone https://github.com/davisp/ghp-import.git && # ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc && diff --git a/Cargo.toml b/Cargo.toml index 48f199ed8..4e66ff0ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,12 @@ repository = "https://github.com/actix/actix-web.git" documentation = "https://actix.rs/api/actix-web/stable/actix_web/" categories = ["network-programming", "asynchronous", "web-programming::http-server", - "web-programming::http-client", "web-programming::websocket"] license = "MIT/Apache-2.0" exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] [package.metadata.docs.rs] -features = ["tls", "alpn", "rust-tls", "session", "brotli", "flate2-c"] +features = ["session"] [badges] travis-ci = { repository = "actix/actix-web", branch = "master" } @@ -30,18 +29,6 @@ path = "src/lib.rs" [features] default = ["session", "cell"] -# native-tls -tls = ["native-tls", "tokio-tls", "actix-net/tls"] - -# openssl -ssl = ["openssl", "tokio-openssl", "actix-net/ssl"] - -# rustls -rust-tls = ["rustls", "tokio-rustls", "webpki", "webpki-roots", "actix-net/rust-tls"] - -# unix sockets -uds = ["tokio-uds"] - # sessions feature, session require "ring" crate and c compiler session = ["cookie/secure"] @@ -82,31 +69,11 @@ tokio-tcp = "0.1" tokio-timer = "0.2" tokio-current-thread = "0.1" -# native-tls -native-tls = { version="0.2", optional = true } -tokio-tls = { version="0.2", optional = true } - -# openssl -openssl = { version="0.10", optional = true } -tokio-openssl = { version="0.2", optional = true } - -#rustls -rustls = { version = "0.14", optional = true } -tokio-rustls = { version = "0.8", optional = true } -webpki = { version = "0.18", optional = true } -webpki-roots = { version = "0.15", optional = true } - -# unix sockets -tokio-uds = { version="0.2", optional = true } - [dev-dependencies] actix-web = "0.7" env_logger = "0.5" serde_derive = "1.0" -[build-dependencies] -version_check = "0.1" - [profile.release] lto = true opt-level = 3 diff --git a/src/lib.rs b/src/lib.rs index 4ec097266..8a7bcfa43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,21 +63,9 @@ //! //! ## Package feature //! -//! * `tls` - enables ssl support via `native-tls` crate -//! * `ssl` - enables ssl support via `openssl` crate, supports `http/2` -//! * `rust-tls` - enables ssl support via `rustls` crate, supports `http/2` -//! * `uds` - enables support for making client requests via Unix Domain Sockets. -//! Unix only. Not necessary for *serving* requests. //! * `session` - enables session support, includes `ring` crate as //! dependency -//! * `brotli` - enables `brotli` compression support, requires `c` -//! compiler -//! * `flate2-c` - enables `gzip`, `deflate` compression support, requires -//! `c` compiler -//! * `flate2-rust` - experimental rust based implementation for -//! `gzip`, `deflate` compression. //! -#![cfg_attr(actix_nightly, feature(tool_lints))] // #![warn(missing_docs)] #![allow(dead_code)] @@ -114,8 +102,6 @@ extern crate tokio_current_thread; extern crate tokio_io; extern crate tokio_tcp; extern crate tokio_timer; -#[cfg(all(unix, feature = "uds"))] -extern crate tokio_uds; #[cfg(test)] #[macro_use] diff --git a/src/response.rs b/src/response.rs index d0136b408..e834748ef 100644 --- a/src/response.rs +++ b/src/response.rs @@ -942,7 +942,7 @@ mod tests { use http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; use header::ContentEncoding; - use test::TestRequest; + // use test::TestRequest; #[test] fn test_debug() { @@ -1118,8 +1118,6 @@ mod tests { #[test] fn test_into_response() { - let req = TestRequest::default().finish(); - let resp: Response = "test".into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( From dda5b399ca56e966c34908756879f9489ac70481 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 6 Oct 2018 21:32:01 -0700 Subject: [PATCH 024/427] add content-length test --- tests/test_server.rs | 58 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/tests/test_server.rs b/tests/test_server.rs index cc21416c0..d0f364b68 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -8,10 +8,10 @@ use std::{io::Read, io::Write, net, thread, time}; use actix::System; use actix_net::server::Server; -use actix_web::{client, test}; +use actix_web::{client, test, HttpMessage}; use futures::future; -use actix_http::{h1, Error, KeepAlive, Response, ServiceConfig}; +use actix_http::{h1, Error, KeepAlive, Request, Response, ServiceConfig}; #[test] fn test_h1_v2() { @@ -88,3 +88,57 @@ fn test_malformed_request() { let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 400 Bad Request")); } + +#[test] +fn test_content_length() { + use actix_http::http::{ + header::{HeaderName, HeaderValue}, + StatusCode, + }; + + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + let settings = ServiceConfig::build().client_timeout(100).finish(); + h1::H1Service::new(settings, |req: Request| { + let indx: usize = req.uri().path()[1..].parse().unwrap(); + let statuses = [ + StatusCode::NO_CONTENT, + StatusCode::CONTINUE, + StatusCode::SWITCHING_PROTOCOLS, + StatusCode::PROCESSING, + StatusCode::OK, + StatusCode::NOT_FOUND, + ]; + future::ok::<_, Error>(Response::new(statuses[indx])) + }) + }).unwrap() + .run(); + }); + thread::sleep(time::Duration::from_millis(100)); + + let header = HeaderName::from_static("content-length"); + let value = HeaderValue::from_static("0"); + + let mut sys = System::new("test"); + { + for i in 0..4 { + let req = + client::ClientRequest::get(format!("http://{}/{}", addr, i).as_str()) + .finish() + .unwrap(); + let response = sys.block_on(req.send()).unwrap(); + assert_eq!(response.headers().get(&header), None); + } + + for i in 4..6 { + let req = + client::ClientRequest::get(format!("http://{}/{}", addr, i).as_str()) + .finish() + .unwrap(); + let response = sys.block_on(req.send()).unwrap(); + assert_eq!(response.headers().get(&header), Some(&value)); + } + } +} From b0ca6220f07fafd9862542cb703cefc1753c16e9 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 6 Oct 2018 22:36:57 -0700 Subject: [PATCH 025/427] refactor te encoding --- src/error.rs | 6 +- src/h1/codec.rs | 32 +++-------- src/h1/dispatcher.rs | 132 +++++++++++++++++++++---------------------- src/h1/service.rs | 13 +++-- 4 files changed, 83 insertions(+), 100 deletions(-) diff --git a/src/error.rs b/src/error.rs index 26b3ca56d..277814d2d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -374,7 +374,7 @@ impl ResponseError for cookie::ParseError { #[derive(Debug)] /// A set of errors that can occur during dispatching http requests -pub enum DispatchError { +pub enum DispatchError { /// Service error // #[fail(display = "Application specific error: {}", _0)] Service(E), @@ -413,13 +413,13 @@ pub enum DispatchError { Unknown, } -impl From for DispatchError { +impl From for DispatchError { fn from(err: ParseError) -> Self { DispatchError::Parse(err) } } -impl From for DispatchError { +impl From for DispatchError { fn from(err: io::Error) -> Self { DispatchError::Io(err) } diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 8ab8f2528..247b0f01c 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -54,7 +54,6 @@ pub struct Codec { // encoder part flags: Flags, - written: u64, headers_size: u32, te: ResponseEncoder, } @@ -82,31 +81,30 @@ impl Codec { version: Version::HTTP_11, flags, - written: 0, headers_size: 0, te: ResponseEncoder::default(), } } - fn written(&self) -> u64 { - self.written - } - + /// Check if request is upgrade pub fn upgrade(&self) -> bool { self.flags.contains(Flags::UPGRADE) } + /// Check if last response is keep-alive pub fn keepalive(&self) -> bool { self.flags.contains(Flags::KEEPALIVE) } + /// prepare transfer encoding + pub fn prepare_te(&mut self, res: &mut Response) { + self.te + .update(res, self.flags.contains(Flags::HEAD), self.version); + } + fn encode_response( &mut self, mut msg: Response, buffer: &mut BytesMut, ) -> io::Result<()> { - // prepare transfer encoding - self.te - .update(&mut msg, self.flags.contains(Flags::HEAD), self.version); - let ka = self.flags.contains(Flags::KEEPALIVE_ENABLED) && msg .keep_alive() .unwrap_or_else(|| self.flags.contains(Flags::KEEPALIVE)); @@ -131,12 +129,11 @@ impl Codec { msg.headers_mut() .insert(CONNECTION, HeaderValue::from_static("close")); } - let body = msg.replace_body(Body::Empty); // render message { let reason = msg.reason().as_bytes(); - if let Body::Binary(ref bytes) = body { + if let Body::Binary(ref bytes) = msg.body() { buffer.reserve( 256 + msg.headers().len() * AVERAGE_HEADER_SIZE + bytes.len() @@ -229,16 +226,6 @@ impl Codec { self.headers_size = buffer.len() as u32; } - if let Body::Binary(bytes) = body { - self.written = bytes.len() as u64; - // buffer.write(bytes.as_ref())?; - buffer.extend_from_slice(bytes.as_ref()); - } else { - // capacity, makes sense only for streaming or actor - // self.buffer_capacity = msg.write_buffer_capacity(); - - msg.replace_body(body); - } Ok(()) } } @@ -282,7 +269,6 @@ impl Encoder for Codec { ) -> Result<(), Self::Error> { match item { OutMessage::Response(res) => { - self.written = 0; self.encode_response(res, dst)?; } OutMessage::Payload(bytes) => { diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index a39967a22..7b6d31fe6 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -1,15 +1,15 @@ use std::collections::VecDeque; -use std::fmt::{Debug, Display}; use std::time::Instant; use actix_net::codec::Framed; use actix_net::service::Service; use futures::{Async, AsyncSink, Future, Poll, Sink, Stream}; +use log::Level::Debug; use tokio_io::{AsyncRead, AsyncWrite}; use tokio_timer::Delay; -use error::{ParseError, PayloadError}; +use error::{Error, ParseError, PayloadError}; use payload::{Payload, PayloadSender, PayloadStatus, PayloadWriter}; use body::Body; @@ -38,7 +38,7 @@ bitflags! { /// Dispatcher for HTTP/1.1 protocol pub struct Dispatcher where - S::Error: Debug + Display, + S::Error: Into, { service: S, flags: Flags, @@ -81,7 +81,7 @@ impl Dispatcher where T: AsyncRead + AsyncWrite, S: Service, - S::Error: Debug + Display, + S::Error: Into, { /// Create http/1 dispatcher. pub fn new(stream: T, config: ServiceConfig, service: S) -> Self { @@ -177,52 +177,34 @@ where State::None => loop { break if let Some(msg) = self.messages.pop_front() { match msg { - Message::Item(msg) => { - let mut task = self.service.call(msg); - match task.poll() { - Ok(Async::Ready(res)) => { - if res.body().is_streaming() { - unimplemented!() - } else { - Some(Ok(State::SendResponse(Some( - OutMessage::Response(res), - )))) - } - } - Ok(Async::NotReady) => { - Some(Ok(State::Response(task))) - } - Err(err) => Some(Err(DispatchError::Service(err))), - } - } - Message::Error(res) => Some(Ok(State::SendResponse(Some( + Message::Item(req) => Some(self.handle_request(req)), + Message::Error(res) => Some(State::SendResponse(Some( OutMessage::Response(res), - )))), + ))), } } else { None }; }, State::Payload(ref mut _body) => unimplemented!(), - State::Response(ref mut fut) => { - match fut.poll() { - Ok(Async::Ready(res)) => { - if res.body().is_streaming() { - unimplemented!() - } else { - Some(Ok(State::SendResponse(Some( - OutMessage::Response(res), - )))) - } - } - Ok(Async::NotReady) => None, - Err(err) => { - // it is not possible to recover from error - // during pipe handling, so just drop connection - Some(Err(DispatchError::Service(err))) + State::Response(ref mut fut) => match fut.poll() { + Ok(Async::Ready(mut res)) => { + self.framed.get_codec_mut().prepare_te(&mut res); + if res.body().is_streaming() { + unimplemented!() + } else { + Some(State::SendResponse(Some(OutMessage::Response(res)))) } } - } + Ok(Async::NotReady) => None, + Err(err) => { + let err = err.into(); + if log_enabled!(Debug) { + debug!("{:?}", err); + } + Some(State::SendResponse(Some(OutMessage::Response(err.into())))) + } + }, State::SendResponse(ref mut item) => { let msg = item.take().expect("SendResponse is empty"); match self.framed.start_send(msg) { @@ -232,13 +214,19 @@ where self.framed.get_codec().keepalive(), ); self.flags.remove(Flags::FLUSHED); - Some(Ok(State::None)) + Some(State::None) } Ok(AsyncSink::NotReady(msg)) => { *item = Some(msg); return Ok(()); } - Err(err) => Some(Err(DispatchError::Io(err))), + Err(err) => { + self.flags.insert(Flags::READ_DISCONNECTED); + if let Some(mut payload) = self.payload.take() { + payload.set_error(PayloadError::Incomplete); + } + return Err(DispatchError::Io(err)); + } } } State::SendResponseWithPayload(ref mut item) => { @@ -251,23 +239,25 @@ where self.framed.get_codec().keepalive(), ); self.flags.remove(Flags::FLUSHED); - Some(Ok(State::Payload(body))) + Some(State::Payload(body)) } Ok(AsyncSink::NotReady(msg)) => { *item = Some((msg, body)); return Ok(()); } - Err(err) => Some(Err(DispatchError::Io(err))), + Err(err) => { + self.flags.insert(Flags::READ_DISCONNECTED); + if let Some(mut payload) = self.payload.take() { + payload.set_error(PayloadError::Incomplete); + } + return Err(DispatchError::Io(err)); + } } } }; match state { - Some(Ok(state)) => self.state = state, - Some(Err(err)) => { - self.client_disconnected(); - return Err(err); - } + Some(state) => self.state = state, None => { // if read-backpressure is enabled and we consumed some data. // we may read more dataand retry @@ -283,6 +273,28 @@ where Ok(()) } + fn handle_request(&mut self, req: Request) -> State { + let mut task = self.service.call(req); + match task.poll() { + Ok(Async::Ready(mut res)) => { + self.framed.get_codec_mut().prepare_te(&mut res); + if res.body().is_streaming() { + unimplemented!() + } else { + State::SendResponse(Some(OutMessage::Response(res))) + } + } + Ok(Async::NotReady) => State::Response(task), + Err(err) => { + let err = err.into(); + if log_enabled!(Debug) { + debug!("{:?}", err); + } + State::SendResponse(Some(OutMessage::Response(err.into()))) + } + } + } + fn one_message(&mut self, msg: InMessage) -> Result<(), DispatchError> { self.flags.insert(Flags::STARTED); @@ -290,23 +302,7 @@ where InMessage::Message(msg) => { // handle request early if self.state.is_empty() { - let mut task = self.service.call(msg); - match task.poll() { - Ok(Async::Ready(res)) => { - if res.body().is_streaming() { - unimplemented!() - } else { - self.state = - State::SendResponse(Some(OutMessage::Response(res))); - } - } - Ok(Async::NotReady) => self.state = State::Response(task), - Err(err) => { - error!("Unhandled application error: {}", err); - self.client_disconnected(); - return Err(DispatchError::Service(err)); - } - } + self.state = self.handle_request(msg); } else { self.messages.push_back(Message::Item(msg)); } @@ -449,7 +445,7 @@ impl Future for Dispatcher where T: AsyncRead + AsyncWrite, S: Service, - S::Error: Debug + Display, + S::Error: Into, { type Item = (); type Error = DispatchError; diff --git a/src/h1/service.rs b/src/h1/service.rs index 02535fc8c..3ac073ad5 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -1,4 +1,3 @@ -use std::fmt::{Debug, Display}; use std::marker::PhantomData; use actix_net::codec::Framed; @@ -7,7 +6,7 @@ use futures::{future, Async, Future, Poll, Stream}; use tokio_io::{AsyncRead, AsyncWrite}; use config::ServiceConfig; -use error::{DispatchError, ParseError}; +use error::{DispatchError, Error, ParseError}; use request::Request; use response::Response; @@ -24,6 +23,8 @@ pub struct H1Service { impl H1Service where S: NewService, + S::Service: Clone, + S::Error: Into, { /// Create new `HttpService` instance. pub fn new>(cfg: ServiceConfig, service: F) -> Self { @@ -40,7 +41,7 @@ where T: AsyncRead + AsyncWrite, S: NewService + Clone, S::Service: Clone, - S::Error: Debug + Display, + S::Error: Into, { type Request = T; type Response = (); @@ -69,7 +70,7 @@ where T: AsyncRead + AsyncWrite, S: NewService, S::Service: Clone, - S::Error: Debug + Display, + S::Error: Into, { type Item = H1ServiceHandler; type Error = S::InitError; @@ -93,7 +94,7 @@ pub struct H1ServiceHandler { impl H1ServiceHandler where S: Service + Clone, - S::Error: Debug + Display, + S::Error: Into, { fn new(cfg: ServiceConfig, srv: S) -> H1ServiceHandler { H1ServiceHandler { @@ -108,7 +109,7 @@ impl Service for H1ServiceHandler where T: AsyncRead + AsyncWrite, S: Service + Clone, - S::Error: Debug + Display, + S::Error: Into, { type Request = T; type Response = (); From 8d85c45c1d1369f7e357e9f3f19c4beb75284740 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 7 Oct 2018 00:04:38 -0700 Subject: [PATCH 026/427] simplify error handling --- src/body.rs | 6 ++-- src/error.rs | 6 ++-- src/h1/dispatcher.rs | 69 ++++++++++++++++++++++---------------------- src/h1/service.rs | 13 +++++---- 4 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/body.rs b/src/body.rs index c10b067a2..c78ea8172 100644 --- a/src/body.rs +++ b/src/body.rs @@ -69,10 +69,10 @@ impl Body { /// Is this binary body. #[inline] - pub(crate) fn binary(self) -> Binary { + pub(crate) fn into_binary(self) -> Option { match self { - Body::Binary(b) => b, - _ => panic!(), + Body::Binary(b) => Some(b), + _ => None, } } } diff --git a/src/error.rs b/src/error.rs index 277814d2d..1e60c3486 100644 --- a/src/error.rs +++ b/src/error.rs @@ -374,7 +374,7 @@ impl ResponseError for cookie::ParseError { #[derive(Debug)] /// A set of errors that can occur during dispatching http requests -pub enum DispatchError { +pub enum DispatchError { /// Service error // #[fail(display = "Application specific error: {}", _0)] Service(E), @@ -413,13 +413,13 @@ pub enum DispatchError { Unknown, } -impl From for DispatchError { +impl From for DispatchError { fn from(err: ParseError) -> Self { DispatchError::Parse(err) } } -impl From for DispatchError { +impl From for DispatchError { fn from(err: io::Error) -> Self { DispatchError::Io(err) } diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 7b6d31fe6..3bf17a8b3 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -1,15 +1,15 @@ use std::collections::VecDeque; +use std::fmt::{Debug, Display}; use std::time::Instant; use actix_net::codec::Framed; use actix_net::service::Service; use futures::{Async, AsyncSink, Future, Poll, Sink, Stream}; -use log::Level::Debug; use tokio_io::{AsyncRead, AsyncWrite}; use tokio_timer::Delay; -use error::{Error, ParseError, PayloadError}; +use error::{ParseError, PayloadError}; use payload::{Payload, PayloadSender, PayloadStatus, PayloadWriter}; use body::Body; @@ -38,7 +38,7 @@ bitflags! { /// Dispatcher for HTTP/1.1 protocol pub struct Dispatcher where - S::Error: Into, + S::Error: Debug + Display, { service: S, flags: Flags, @@ -61,7 +61,7 @@ enum Message { enum State { None, - Response(S::Future), + ServiceCall(S::Future), SendResponse(Option), SendResponseWithPayload(Option<(OutMessage, Body)>), Payload(Body), @@ -81,7 +81,7 @@ impl Dispatcher where T: AsyncRead + AsyncWrite, S: Service, - S::Error: Into, + S::Error: Debug + Display, { /// Create http/1 dispatcher. pub fn new(stream: T, config: ServiceConfig, service: S) -> Self { @@ -177,7 +177,7 @@ where State::None => loop { break if let Some(msg) = self.messages.pop_front() { match msg { - Message::Item(req) => Some(self.handle_request(req)), + Message::Item(req) => Some(self.handle_request(req)?), Message::Error(res) => Some(State::SendResponse(Some( OutMessage::Response(res), ))), @@ -187,23 +187,23 @@ where }; }, State::Payload(ref mut _body) => unimplemented!(), - State::Response(ref mut fut) => match fut.poll() { - Ok(Async::Ready(mut res)) => { + State::ServiceCall(ref mut fut) => match fut + .poll() + .map_err(DispatchError::Service)? + { + Async::Ready(mut res) => { self.framed.get_codec_mut().prepare_te(&mut res); - if res.body().is_streaming() { - unimplemented!() - } else { + let body = res.replace_body(Body::Empty); + if body.is_empty() { Some(State::SendResponse(Some(OutMessage::Response(res)))) + } else { + Some(State::SendResponseWithPayload(Some(( + OutMessage::Response(res), + body, + )))) } } - Ok(Async::NotReady) => None, - Err(err) => { - let err = err.into(); - if log_enabled!(Debug) { - debug!("{:?}", err); - } - Some(State::SendResponse(Some(OutMessage::Response(err.into())))) - } + Async::NotReady => None, }, State::SendResponse(ref mut item) => { let msg = item.take().expect("SendResponse is empty"); @@ -273,25 +273,24 @@ where Ok(()) } - fn handle_request(&mut self, req: Request) -> State { + fn handle_request( + &mut self, req: Request, + ) -> Result, DispatchError> { let mut task = self.service.call(req); - match task.poll() { - Ok(Async::Ready(mut res)) => { + match task.poll().map_err(DispatchError::Service)? { + Async::Ready(mut res) => { self.framed.get_codec_mut().prepare_te(&mut res); - if res.body().is_streaming() { - unimplemented!() + let body = res.replace_body(Body::Empty); + if body.is_empty() { + Ok(State::SendResponse(Some(OutMessage::Response(res)))) } else { - State::SendResponse(Some(OutMessage::Response(res))) + Ok(State::SendResponseWithPayload(Some(( + OutMessage::Response(res), + body, + )))) } } - Ok(Async::NotReady) => State::Response(task), - Err(err) => { - let err = err.into(); - if log_enabled!(Debug) { - debug!("{:?}", err); - } - State::SendResponse(Some(OutMessage::Response(err.into()))) - } + Async::NotReady => Ok(State::ServiceCall(task)), } } @@ -302,7 +301,7 @@ where InMessage::Message(msg) => { // handle request early if self.state.is_empty() { - self.state = self.handle_request(msg); + self.state = self.handle_request(msg)?; } else { self.messages.push_back(Message::Item(msg)); } @@ -445,7 +444,7 @@ impl Future for Dispatcher where T: AsyncRead + AsyncWrite, S: Service, - S::Error: Into, + S::Error: Debug + Display, { type Item = (); type Error = DispatchError; diff --git a/src/h1/service.rs b/src/h1/service.rs index 3ac073ad5..aa59614d3 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -1,3 +1,4 @@ +use std::fmt::{Debug, Display}; use std::marker::PhantomData; use actix_net::codec::Framed; @@ -6,7 +7,7 @@ use futures::{future, Async, Future, Poll, Stream}; use tokio_io::{AsyncRead, AsyncWrite}; use config::ServiceConfig; -use error::{DispatchError, Error, ParseError}; +use error::{DispatchError, ParseError}; use request::Request; use response::Response; @@ -24,7 +25,7 @@ impl H1Service where S: NewService, S::Service: Clone, - S::Error: Into, + S::Error: Debug + Display, { /// Create new `HttpService` instance. pub fn new>(cfg: ServiceConfig, service: F) -> Self { @@ -41,7 +42,7 @@ where T: AsyncRead + AsyncWrite, S: NewService + Clone, S::Service: Clone, - S::Error: Into, + S::Error: Debug + Display, { type Request = T; type Response = (); @@ -70,7 +71,7 @@ where T: AsyncRead + AsyncWrite, S: NewService, S::Service: Clone, - S::Error: Into, + S::Error: Debug + Display, { type Item = H1ServiceHandler; type Error = S::InitError; @@ -94,7 +95,7 @@ pub struct H1ServiceHandler { impl H1ServiceHandler where S: Service + Clone, - S::Error: Into, + S::Error: Debug + Display, { fn new(cfg: ServiceConfig, srv: S) -> H1ServiceHandler { H1ServiceHandler { @@ -109,7 +110,7 @@ impl Service for H1ServiceHandler where T: AsyncRead + AsyncWrite, S: Service + Clone, - S::Error: Into, + S::Error: Debug + Display, { type Request = T; type Response = (); From 9c4a55c95c106abf8edebad1a5db8188d0bea57a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 7 Oct 2018 08:28:38 -0700 Subject: [PATCH 027/427] simplify H1Service configuration --- src/h1/service.rs | 136 ++++++++++++++++++++++++++++++++++++++++++- tests/test_server.rs | 28 ++++----- 2 files changed, 144 insertions(+), 20 deletions(-) diff --git a/src/h1/service.rs b/src/h1/service.rs index aa59614d3..eea0e6d9f 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -1,12 +1,13 @@ use std::fmt::{Debug, Display}; use std::marker::PhantomData; +use std::net; use actix_net::codec::Framed; use actix_net::service::{IntoNewService, NewService, Service}; use futures::{future, Async, Future, Poll, Stream}; use tokio_io::{AsyncRead, AsyncWrite}; -use config::ServiceConfig; +use config::{KeepAlive, ServiceConfig}; use error::{DispatchError, ParseError}; use request::Request; use response::Response; @@ -28,13 +29,20 @@ where S::Error: Debug + Display, { /// Create new `HttpService` instance. - pub fn new>(cfg: ServiceConfig, service: F) -> Self { + pub fn new>(service: F) -> Self { + let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0); + H1Service { cfg, srv: service.into_new_service(), _t: PhantomData, } } + + /// Create builder for `HttpService` instance. + pub fn build() -> H1ServiceBuilder { + H1ServiceBuilder::new() + } } impl NewService for H1Service @@ -60,6 +68,130 @@ where } } +/// A http/1 new service builder +/// +/// This type can be used to construct an instance of `ServiceConfig` through a +/// builder-like pattern. +pub struct H1ServiceBuilder { + keep_alive: KeepAlive, + client_timeout: u64, + client_disconnect: u64, + host: String, + addr: net::SocketAddr, + secure: bool, + _t: PhantomData<(T, S)>, +} + +impl H1ServiceBuilder +where + S: NewService, + S::Service: Clone, + S::Error: Debug + Display, +{ + /// Create instance of `ServiceConfigBuilder` + pub fn new() -> H1ServiceBuilder { + H1ServiceBuilder { + keep_alive: KeepAlive::Timeout(5), + client_timeout: 5000, + client_disconnect: 0, + secure: false, + host: "localhost".to_owned(), + addr: "127.0.0.1:8080".parse().unwrap(), + _t: PhantomData, + } + } + + /// Enable secure flag for current server. + /// This flags also enables `client disconnect timeout`. + /// + /// By default this flag is set to false. + pub fn secure(mut self) -> Self { + self.secure = true; + if self.client_disconnect == 0 { + self.client_disconnect = 3000; + } + self + } + + /// Set server keep-alive setting. + /// + /// By default keep alive is set to a 5 seconds. + pub fn keep_alive>(mut self, val: U) -> Self { + self.keep_alive = val.into(); + self + } + + /// Set server client timeout in milliseconds for first request. + /// + /// Defines a timeout for reading client request header. If a client does not transmit + /// the entire set headers within this time, the request is terminated with + /// the 408 (Request Time-out) error. + /// + /// To disable timeout set value to 0. + /// + /// By default client timeout is set to 5000 milliseconds. + pub fn client_timeout(mut self, val: u64) -> Self { + self.client_timeout = val; + self + } + + /// Set server connection disconnect timeout in milliseconds. + /// + /// Defines a timeout for disconnect connection. If a disconnect procedure does not complete + /// within this time, the request get dropped. This timeout affects secure connections. + /// + /// To disable timeout set value to 0. + /// + /// By default disconnect timeout is set to 3000 milliseconds. + pub fn client_disconnect(mut self, val: u64) -> Self { + self.client_disconnect = val; + self + } + + /// Set server host name. + /// + /// Host name is used by application router aa a hostname for url + /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. + /// html#method.host) documentation for more information. + /// + /// By default host name is set to a "localhost" value. + pub fn server_hostname(mut self, val: &str) -> Self { + self.host = val.to_owned(); + self + } + + /// Set server ip address. + /// + /// Host name is used by application router aa a hostname for url + /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. + /// html#method.host) documentation for more information. + /// + /// By default server address is set to a "127.0.0.1:8080" + pub fn server_address(mut self, addr: U) -> Self { + match addr.to_socket_addrs() { + Err(err) => error!("Can not convert to SocketAddr: {}", err), + Ok(mut addrs) => if let Some(addr) = addrs.next() { + self.addr = addr; + }, + } + self + } + + /// Finish service configuration and create `H1Service` instance. + pub fn finish>(self, service: F) -> H1Service { + let cfg = ServiceConfig::new( + self.keep_alive, + self.client_timeout, + self.client_disconnect, + ); + H1Service { + cfg, + srv: service.into_new_service(), + _t: PhantomData, + } + } +} + pub struct H1ServiceResponse { fut: S::Future, cfg: Option, diff --git a/tests/test_server.rs b/tests/test_server.rs index d0f364b68..8d682e120 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -11,7 +11,7 @@ use actix_net::server::Server; use actix_web::{client, test, HttpMessage}; use futures::future; -use actix_http::{h1, Error, KeepAlive, Request, Response, ServiceConfig}; +use actix_http::{h1, Error, KeepAlive, Request, Response}; #[test] fn test_h1_v2() { @@ -19,17 +19,13 @@ fn test_h1_v2() { thread::spawn(move || { Server::new() .bind("test", addr, move || { - let settings = ServiceConfig::build() + h1::H1Service::build() .keep_alive(KeepAlive::Disabled) .client_timeout(1000) .client_disconnect(1000) .server_hostname("localhost") .server_address(addr) - .finish(); - - h1::H1Service::new(settings, |_| { - future::ok::<_, Error>(Response::Ok().finish()) - }) + .finish(|_| future::ok::<_, Error>(Response::Ok().finish())) }).unwrap() .run(); }); @@ -50,11 +46,9 @@ fn test_slow_request() { thread::spawn(move || { Server::new() .bind("test", addr, move || { - let settings = ServiceConfig::build().client_timeout(100).finish(); - - h1::H1Service::new(settings, |_| { - future::ok::<_, Error>(Response::Ok().finish()) - }) + h1::H1Service::build() + .client_timeout(100) + .finish(|_| future::ok::<_, Error>(Response::Ok().finish())) }).unwrap() .run(); }); @@ -73,10 +67,9 @@ fn test_malformed_request() { thread::spawn(move || { Server::new() .bind("test", addr, move || { - let settings = ServiceConfig::build().client_timeout(100).finish(); - h1::H1Service::new(settings, |_| { - future::ok::<_, Error>(Response::Ok().finish()) - }) + h1::H1Service::build() + .client_timeout(100) + .finish(|_| future::ok::<_, Error>(Response::Ok().finish())) }).unwrap() .run(); }); @@ -100,8 +93,7 @@ fn test_content_length() { thread::spawn(move || { Server::new() .bind("test", addr, move || { - let settings = ServiceConfig::build().client_timeout(100).finish(); - h1::H1Service::new(settings, |req: Request| { + h1::H1Service::new(|req: Request| { let indx: usize = req.uri().path()[1..].parse().unwrap(); let statuses = [ StatusCode::NO_CONTENT, From 13193a0721110f0ad0dd046af76480322ae46430 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 7 Oct 2018 09:48:53 -0700 Subject: [PATCH 028/427] refactor http/1 dispatcher --- src/error.rs | 15 ++---- src/h1/decoder.rs | 16 +++---- src/h1/dispatcher.rs | 111 ++++++++++++++++++------------------------- src/h1/service.rs | 14 +++--- src/payload.rs | 7 --- tests/test_server.rs | 11 +++-- 6 files changed, 68 insertions(+), 106 deletions(-) diff --git a/src/error.rs b/src/error.rs index 1e60c3486..465b8ae0a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -341,15 +341,6 @@ pub enum PayloadError { /// A payload length is unknown. #[fail(display = "A payload length is unknown.")] UnknownLength, - /// Io error - #[fail(display = "{}", _0)] - Io(#[cause] IoError), -} - -impl From for PayloadError { - fn from(err: IoError) -> PayloadError { - PayloadError::Io(err) - } } /// `PayloadError` returns two possible results: @@ -374,7 +365,7 @@ impl ResponseError for cookie::ParseError { #[derive(Debug)] /// A set of errors that can occur during dispatching http requests -pub enum DispatchError { +pub enum DispatchError { /// Service error // #[fail(display = "Application specific error: {}", _0)] Service(E), @@ -413,13 +404,13 @@ pub enum DispatchError { Unknown, } -impl From for DispatchError { +impl From for DispatchError { fn from(err: ParseError) -> Self { DispatchError::Parse(err) } } -impl From for DispatchError { +impl From for DispatchError { fn from(err: io::Error) -> Self { DispatchError::Io(err) } diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index fb29f033c..d0c3fa048 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -219,9 +219,7 @@ impl PayloadDecoder { } pub fn eof() -> PayloadDecoder { - PayloadDecoder { - kind: Kind::Eof(false), - } + PayloadDecoder { kind: Kind::Eof } } } @@ -246,7 +244,7 @@ enum Kind { /// > the final encoding, the message body length cannot be determined /// > reliably; the server MUST respond with the 400 (Bad Request) /// > status code and then close the connection. - Eof(bool), + Eof, } #[derive(Debug, PartialEq, Clone)] @@ -309,13 +307,11 @@ impl Decoder for PayloadDecoder { } } } - Kind::Eof(ref mut is_eof) => { - if *is_eof { - Ok(Some(PayloadItem::Eof)) - } else if !src.is_empty() { - Ok(Some(PayloadItem::Chunk(src.take().freeze()))) - } else { + Kind::Eof => { + if src.is_empty() { Ok(None) + } else { + Ok(Some(PayloadItem::Chunk(src.take().freeze()))) } } } diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 3bf17a8b3..92bec3544 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -1,5 +1,5 @@ use std::collections::VecDeque; -use std::fmt::{Debug, Display}; +use std::fmt::Debug; use std::time::Instant; use actix_net::codec::Framed; @@ -27,18 +27,17 @@ bitflags! { const STARTED = 0b0000_0001; const KEEPALIVE_ENABLED = 0b0000_0010; const KEEPALIVE = 0b0000_0100; - const SHUTDOWN = 0b0000_1000; - const READ_DISCONNECTED = 0b0001_0000; - const WRITE_DISCONNECTED = 0b0010_0000; - const POLLED = 0b0100_0000; - const FLUSHED = 0b1000_0000; + const POLLED = 0b0000_1000; + const FLUSHED = 0b0001_0000; + const SHUTDOWN = 0b0010_0000; + const DISCONNECTED = 0b0100_0000; } } /// Dispatcher for HTTP/1.1 protocol pub struct Dispatcher where - S::Error: Debug + Display, + S::Error: Debug, { service: S, flags: Flags, @@ -81,7 +80,7 @@ impl Dispatcher where T: AsyncRead + AsyncWrite, S: Service, - S::Error: Debug + Display, + S::Error: Debug, { /// Create http/1 dispatcher. pub fn new(stream: T, config: ServiceConfig, service: S) -> Self { @@ -122,9 +121,8 @@ where } } - #[inline] fn can_read(&self) -> bool { - if self.flags.contains(Flags::READ_DISCONNECTED) { + if self.flags.contains(Flags::DISCONNECTED) { return false; } @@ -137,7 +135,7 @@ where // if checked is set to true, delay disconnect until all tasks have finished. fn client_disconnected(&mut self) { - self.flags.insert(Flags::READ_DISCONNECTED); + self.flags.insert(Flags::DISCONNECTED); if let Some(mut payload) = self.payload.take() { payload.set_error(PayloadError::Incomplete); } @@ -145,12 +143,11 @@ where /// Flush stream fn poll_flush(&mut self) -> Poll<(), DispatchError> { - if self.flags.contains(Flags::STARTED) && !self.flags.contains(Flags::FLUSHED) { + if !self.flags.contains(Flags::FLUSHED) { match self.framed.poll_complete() { Ok(Async::NotReady) => Ok(Async::NotReady), Err(err) => { debug!("Error sending data: {}", err); - self.client_disconnected(); Err(err.into()) } Ok(Async::Ready(_)) => { @@ -167,8 +164,7 @@ where } } - pub(self) fn poll_handler(&mut self) -> Result<(), DispatchError> { - self.poll_io()?; + fn poll_response(&mut self) -> Result<(), DispatchError> { let mut retry = self.can_read(); // process @@ -221,7 +217,6 @@ where return Ok(()); } Err(err) => { - self.flags.insert(Flags::READ_DISCONNECTED); if let Some(mut payload) = self.payload.take() { payload.set_error(PayloadError::Incomplete); } @@ -246,7 +241,6 @@ where return Ok(()); } Err(err) => { - self.flags.insert(Flags::READ_DISCONNECTED); if let Some(mut payload) = self.payload.take() { payload.set_error(PayloadError::Incomplete); } @@ -261,7 +255,7 @@ where None => { // if read-backpressure is enabled and we consumed some data. // we may read more dataand retry - if !retry && self.can_read() && self.poll_io()? { + if !retry && self.can_read() && self.poll_request()? { retry = self.can_read(); continue; } @@ -319,7 +313,7 @@ where payload.feed_data(chunk); } else { error!("Internal server error: unexpected payload chunk"); - self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); + self.flags.insert(Flags::DISCONNECTED); self.messages.push_back(Message::Error( Response::InternalServerError().finish(), )); @@ -331,7 +325,7 @@ where payload.feed_eof(); } else { error!("Internal server error: unexpected eof"); - self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); + self.flags.insert(Flags::DISCONNECTED); self.messages.push_back(Message::Error( Response::InternalServerError().finish(), )); @@ -343,7 +337,7 @@ where Ok(()) } - pub(self) fn poll_io(&mut self) -> Result> { + pub(self) fn poll_request(&mut self) -> Result> { let mut updated = false; if self.messages.len() < MAX_PIPELINED_MESSAGES { @@ -354,26 +348,25 @@ where self.one_message(msg)?; } Ok(Async::Ready(None)) => { - if self.flags.contains(Flags::READ_DISCONNECTED) { - self.client_disconnected(); - } + self.client_disconnected(); break; } Ok(Async::NotReady) => break, + Err(ParseError::Io(e)) => { + self.client_disconnected(); + self.error = Some(DispatchError::Io(e)); + break; + } Err(e) => { if let Some(mut payload) = self.payload.take() { - let e = match e { - ParseError::Io(e) => PayloadError::Io(e), - _ => PayloadError::EncodingCorrupted, - }; - payload.set_error(e); + payload.set_error(PayloadError::EncodingCorrupted); } // Malformed requests should be responded with 400 self.messages .push_back(Message::Error(Response::BadRequest().finish())); - self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); - self.error = Some(DispatchError::MalformedRequest); + self.flags.insert(Flags::DISCONNECTED); + self.error = Some(e.into()); break; } } @@ -402,8 +395,7 @@ where } else if !self.flags.contains(Flags::STARTED) { // timeout on first request (slow request) return 408 trace!("Slow request timeout"); - self.flags - .insert(Flags::STARTED | Flags::READ_DISCONNECTED); + self.flags.insert(Flags::STARTED | Flags::DISCONNECTED); self.state = State::SendResponse(Some(OutMessage::Response( Response::RequestTimeout().finish(), @@ -444,54 +436,43 @@ impl Future for Dispatcher where T: AsyncRead + AsyncWrite, S: Service, - S::Error: Debug + Display, + S::Error: Debug, { type Item = (); type Error = DispatchError; #[inline] fn poll(&mut self) -> Poll<(), Self::Error> { - self.poll_keepalive()?; - - // shutdown if self.flags.contains(Flags::SHUTDOWN) { - if self.flags.contains(Flags::WRITE_DISCONNECTED) { - return Ok(Async::Ready(())); - } + self.poll_keepalive()?; try_ready!(self.poll_flush()); - return Ok(AsyncWrite::shutdown(self.framed.get_mut())?); - } - - // process incoming requests - if !self.flags.contains(Flags::WRITE_DISCONNECTED) { - self.poll_handler()?; - - // flush stream + Ok(AsyncWrite::shutdown(self.framed.get_mut())?) + } else { + self.poll_keepalive()?; + self.poll_request()?; + self.poll_response()?; self.poll_flush()?; - // deal with keep-alive and stream eof (client-side write shutdown) + // keep-alive and stream errors if self.state.is_empty() && self.flags.contains(Flags::FLUSHED) { - // handle stream eof - if self - .flags - .intersects(Flags::READ_DISCONNECTED | Flags::WRITE_DISCONNECTED) - { - return Ok(Async::Ready(())); + if let Some(err) = self.error.take() { + Err(err) + } else if self.flags.contains(Flags::DISCONNECTED) { + Ok(Async::Ready(())) } - // no keep-alive - if self.flags.contains(Flags::STARTED) - && (!self.flags.contains(Flags::KEEPALIVE_ENABLED) - || !self.flags.contains(Flags::KEEPALIVE)) + // disconnect if keep-alive is not enabled + else if self.flags.contains(Flags::STARTED) && !self + .flags + .intersects(Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED) { self.flags.insert(Flags::SHUTDOWN); - return self.poll(); + self.poll() + } else { + Ok(Async::NotReady) } + } else { + Ok(Async::NotReady) } - Ok(Async::NotReady) - } else if let Some(err) = self.error.take() { - Err(err) - } else { - Ok(Async::Ready(())) } } } diff --git a/src/h1/service.rs b/src/h1/service.rs index eea0e6d9f..de24f52c1 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -1,4 +1,4 @@ -use std::fmt::{Debug, Display}; +use std::fmt::Debug; use std::marker::PhantomData; use std::net; @@ -26,7 +26,7 @@ impl H1Service where S: NewService, S::Service: Clone, - S::Error: Debug + Display, + S::Error: Debug, { /// Create new `HttpService` instance. pub fn new>(service: F) -> Self { @@ -50,7 +50,7 @@ where T: AsyncRead + AsyncWrite, S: NewService + Clone, S::Service: Clone, - S::Error: Debug + Display, + S::Error: Debug, { type Request = T; type Response = (); @@ -86,7 +86,7 @@ impl H1ServiceBuilder where S: NewService, S::Service: Clone, - S::Error: Debug + Display, + S::Error: Debug, { /// Create instance of `ServiceConfigBuilder` pub fn new() -> H1ServiceBuilder { @@ -203,7 +203,7 @@ where T: AsyncRead + AsyncWrite, S: NewService, S::Service: Clone, - S::Error: Debug + Display, + S::Error: Debug, { type Item = H1ServiceHandler; type Error = S::InitError; @@ -227,7 +227,7 @@ pub struct H1ServiceHandler { impl H1ServiceHandler where S: Service + Clone, - S::Error: Debug + Display, + S::Error: Debug, { fn new(cfg: ServiceConfig, srv: S) -> H1ServiceHandler { H1ServiceHandler { @@ -242,7 +242,7 @@ impl Service for H1ServiceHandler where T: AsyncRead + AsyncWrite, S: Service + Clone, - S::Error: Debug + Display, + S::Error: Debug, { type Request = T; type Response = (); diff --git a/src/payload.rs b/src/payload.rs index 3f51f6ec0..54539c408 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -522,18 +522,11 @@ where #[cfg(test)] mod tests { use super::*; - use failure::Fail; use futures::future::{lazy, result}; - use std::io; use tokio::runtime::current_thread::Runtime; #[test] fn test_error() { - let err: PayloadError = - io::Error::new(io::ErrorKind::Other, "ParseError").into(); - assert_eq!(format!("{}", err), "ParseError"); - assert_eq!(format!("{}", err.cause().unwrap()), "ParseError"); - let err = PayloadError::Incomplete; assert_eq!( format!("{}", err), diff --git a/tests/test_server.rs b/tests/test_server.rs index 8d682e120..43e3966df 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -11,7 +11,7 @@ use actix_net::server::Server; use actix_web::{client, test, HttpMessage}; use futures::future; -use actix_http::{h1, Error, KeepAlive, Request, Response}; +use actix_http::{h1, KeepAlive, Request, Response}; #[test] fn test_h1_v2() { @@ -25,10 +25,11 @@ fn test_h1_v2() { .client_disconnect(1000) .server_hostname("localhost") .server_address(addr) - .finish(|_| future::ok::<_, Error>(Response::Ok().finish())) + .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) }).unwrap() .run(); }); + thread::sleep(time::Duration::from_millis(100)); let mut sys = System::new("test"); { @@ -48,7 +49,7 @@ fn test_slow_request() { .bind("test", addr, move || { h1::H1Service::build() .client_timeout(100) - .finish(|_| future::ok::<_, Error>(Response::Ok().finish())) + .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) }).unwrap() .run(); }); @@ -69,7 +70,7 @@ fn test_malformed_request() { .bind("test", addr, move || { h1::H1Service::build() .client_timeout(100) - .finish(|_| future::ok::<_, Error>(Response::Ok().finish())) + .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) }).unwrap() .run(); }); @@ -103,7 +104,7 @@ fn test_content_length() { StatusCode::OK, StatusCode::NOT_FOUND, ]; - future::ok::<_, Error>(Response::new(statuses[indx])) + future::ok::<_, ()>(Response::new(statuses[indx])) }) }).unwrap() .run(); From 8acf9eb98a505eb5f16d72af5424763989599c51 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 7 Oct 2018 10:09:48 -0700 Subject: [PATCH 029/427] better keep-alive handling --- src/h1/dispatcher.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 92bec3544..d44e687d1 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -386,21 +386,13 @@ where if let Some(ref mut timer) = self.ka_timer { match timer.poll() { Ok(Async::Ready(_)) => { - if timer.deadline() >= self.ka_expire { - // check for any outstanding request handling - if self.state.is_empty() && self.messages.is_empty() { - // if we get timer during shutdown, just drop connection - if self.flags.contains(Flags::SHUTDOWN) { - return Err(DispatchError::DisconnectTimeout); - } else if !self.flags.contains(Flags::STARTED) { - // timeout on first request (slow request) return 408 - trace!("Slow request timeout"); - self.flags.insert(Flags::STARTED | Flags::DISCONNECTED); - self.state = - State::SendResponse(Some(OutMessage::Response( - Response::RequestTimeout().finish(), - ))); - } else { + // if we get timer during shutdown, just drop connection + if self.flags.contains(Flags::SHUTDOWN) { + return Err(DispatchError::DisconnectTimeout); + } else if timer.deadline() >= self.ka_expire { + // check for any outstanding response processing + if self.state.is_empty() { + if self.flags.contains(Flags::STARTED) { trace!("Keep-alive timeout, close connection"); self.flags.insert(Flags::SHUTDOWN); @@ -412,6 +404,14 @@ where } else { return Ok(()); } + } else { + // timeout on first request (slow request) return 408 + trace!("Slow request timeout"); + self.flags.insert(Flags::STARTED | Flags::DISCONNECTED); + self.state = + State::SendResponse(Some(OutMessage::Response( + Response::RequestTimeout().finish(), + ))); } } else if let Some(deadline) = self.config.keep_alive_expire() { timer.reset(deadline) From 30db78c19cf8c3e65a4cfefdeb6f8ccf15cec921 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 8 Oct 2018 07:55:01 -0700 Subject: [PATCH 030/427] use TakeItem instead of TakeRequest --- src/h1/mod.rs | 3 +- src/h1/service.rs | 81 ++--------------------------------------------- tests/test_ws.rs | 3 +- 3 files changed, 6 insertions(+), 81 deletions(-) diff --git a/src/h1/mod.rs b/src/h1/mod.rs index 266ebf39c..634136a47 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -6,5 +6,6 @@ mod encoder; mod service; pub use self::codec::{Codec, InMessage, OutMessage}; +pub use self::decoder::{PayloadDecoder, RequestDecoder}; pub use self::dispatcher::Dispatcher; -pub use self::service::{H1Service, H1ServiceHandler, TakeRequest}; +pub use self::service::{H1Service, H1ServiceHandler}; diff --git a/src/h1/service.rs b/src/h1/service.rs index de24f52c1..a7261df17 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -2,17 +2,15 @@ use std::fmt::Debug; use std::marker::PhantomData; use std::net; -use actix_net::codec::Framed; use actix_net::service::{IntoNewService, NewService, Service}; -use futures::{future, Async, Future, Poll, Stream}; +use futures::{Async, Future, Poll}; use tokio_io::{AsyncRead, AsyncWrite}; use config::{KeepAlive, ServiceConfig}; -use error::{DispatchError, ParseError}; +use error::DispatchError; use request::Request; use response::Response; -use super::codec::{Codec, InMessage}; use super::dispatcher::Dispatcher; /// `NewService` implementation for HTTP1 transport @@ -257,78 +255,3 @@ where Dispatcher::new(req, self.cfg.clone(), self.srv.clone()) } } - -/// `NewService` that implements, read one request from framed object feature. -pub struct TakeRequest { - _t: PhantomData, -} - -impl TakeRequest { - /// Create new `TakeRequest` instance. - pub fn new() -> Self { - TakeRequest { _t: PhantomData } - } -} - -impl NewService for TakeRequest -where - T: AsyncRead + AsyncWrite, -{ - type Request = Framed; - type Response = (Option, Framed); - type Error = ParseError; - type InitError = (); - type Service = TakeRequestService; - type Future = future::FutureResult; - - fn new_service(&self) -> Self::Future { - future::ok(TakeRequestService { _t: PhantomData }) - } -} - -/// `NewService` that implements, read one request from framed object feature. -pub struct TakeRequestService { - _t: PhantomData, -} - -impl Service for TakeRequestService -where - T: AsyncRead + AsyncWrite, -{ - type Request = Framed; - type Response = (Option, Framed); - type Error = ParseError; - type Future = TakeRequestServiceResponse; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, framed: Self::Request) -> Self::Future { - TakeRequestServiceResponse { - framed: Some(framed), - } - } -} - -pub struct TakeRequestServiceResponse -where - T: AsyncRead + AsyncWrite, -{ - framed: Option>, -} - -impl Future for TakeRequestServiceResponse -where - T: AsyncRead + AsyncWrite, -{ - type Item = (Option, Framed); - type Error = ParseError; - - fn poll(&mut self) -> Poll { - match self.framed.as_mut().unwrap().poll()? { - Async::Ready(item) => Ok(Async::Ready((item, self.framed.take().unwrap()))), - Async::NotReady => Ok(Async::NotReady), - } - } -} diff --git a/tests/test_ws.rs b/tests/test_ws.rs index a2a18ff28..8a1098747 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -12,6 +12,7 @@ use actix_net::codec::Framed; use actix_net::framed::IntoFramed; use actix_net::server::Server; use actix_net::service::NewServiceExt; +use actix_net::stream::TakeItem; use actix_web::{test, ws as web_ws}; use bytes::Bytes; use futures::future::{ok, Either}; @@ -36,7 +37,7 @@ fn test_simple() { Server::new() .bind("test", addr, move || { IntoFramed::new(|| h1::Codec::new(false)) - .and_then(h1::TakeRequest::new().map_err(|_| ())) + .and_then(TakeItem::new().map_err(|_| ())) .and_then(|(req, framed): (_, Framed<_, _>)| { // validate request if let Some(h1::InMessage::MessageWithPayload(req)) = req { From 431e33acb2a73245dd7d99e9876d5ee37028c951 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 8 Oct 2018 10:14:29 -0700 Subject: [PATCH 031/427] add Date header to response --- src/config.rs | 140 +++++++++++++++++++++++++------------------ src/h1/codec.rs | 15 ++--- src/h1/dispatcher.rs | 2 +- src/lib.rs | 9 +-- 4 files changed, 91 insertions(+), 75 deletions(-) diff --git a/src/config.rs b/src/config.rs index 4e85044f1..2e14a33e6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -48,7 +48,7 @@ struct Inner { client_timeout: u64, client_disconnect: u64, ka_enabled: bool, - date: UnsafeCell<(bool, Date)>, + timer: DateService, } impl Clone for ServiceConfig { @@ -78,7 +78,7 @@ impl ServiceConfig { ka_enabled, client_timeout, client_disconnect, - date: UnsafeCell::new((false, Date::new())), + timer: DateService::with(Duration::from_millis(500)), })) } @@ -99,17 +99,14 @@ impl ServiceConfig { self.0.ka_enabled } - fn update_date(&self) { - // Unsafe: WorkerSetting is !Sync and !Send - unsafe { (*self.0.date.get()).0 = false }; - } - #[inline] /// Client timeout for first request. pub fn client_timer(&self) -> Option { let delay = self.0.client_timeout; if delay != 0 { - Some(Delay::new(self.now() + Duration::from_millis(delay))) + Some(Delay::new( + self.0.timer.now() + Duration::from_millis(delay), + )) } else { None } @@ -119,7 +116,7 @@ impl ServiceConfig { pub fn client_timer_expire(&self) -> Option { let delay = self.0.client_timeout; if delay != 0 { - Some(self.now() + Duration::from_millis(delay)) + Some(self.0.timer.now() + Duration::from_millis(delay)) } else { None } @@ -129,7 +126,7 @@ impl ServiceConfig { pub fn client_disconnect_timer(&self) -> Option { let delay = self.0.client_disconnect; if delay != 0 { - Some(self.now() + Duration::from_millis(delay)) + Some(self.0.timer.now() + Duration::from_millis(delay)) } else { None } @@ -139,7 +136,7 @@ impl ServiceConfig { /// Return keep-alive timer delay is configured. pub fn keep_alive_timer(&self) -> Option { if let Some(ka) = self.0.keep_alive { - Some(Delay::new(self.now() + ka)) + Some(Delay::new(self.0.timer.now() + ka)) } else { None } @@ -148,57 +145,23 @@ impl ServiceConfig { /// Keep-alive expire time pub fn keep_alive_expire(&self) -> Option { if let Some(ka) = self.0.keep_alive { - Some(self.now() + ka) + Some(self.0.timer.now() + ka) } else { None } } - pub(crate) fn set_date(&self, dst: &mut BytesMut, full: bool) { - // Unsafe: WorkerSetting is !Sync and !Send - let date_bytes = unsafe { - let date = &mut (*self.0.date.get()); - if !date.0 { - date.1.update(); - date.0 = true; - - // periodic date update - let s = self.clone(); - spawn(sleep(Duration::from_millis(500)).then(move |_| { - s.update_date(); - future::ok(()) - })); - } - &date.1.bytes - }; - if full { - let mut buf: [u8; 39] = [0; 39]; - buf[..6].copy_from_slice(b"date: "); - buf[6..35].copy_from_slice(date_bytes); - buf[35..].copy_from_slice(b"\r\n\r\n"); - dst.extend_from_slice(&buf); - } else { - dst.extend_from_slice(date_bytes); - } - } - #[inline] pub(crate) fn now(&self) -> Instant { - unsafe { - let date = &mut (*self.0.date.get()); - if !date.0 { - date.1.update(); - date.0 = true; + self.0.timer.now() + } - // periodic date update - let s = self.clone(); - spawn(sleep(Duration::from_millis(500)).then(move |_| { - s.update_date(); - future::ok(()) - })); - } - date.1.current - } + pub(crate) fn set_date(&self, dst: &mut BytesMut) { + let mut buf: [u8; 39] = [0; 39]; + buf[..6].copy_from_slice(b"date: "); + buf[6..35].copy_from_slice(&self.0.timer.date().bytes); + buf[35..].copy_from_slice(b"\r\n\r\n"); + dst.extend_from_slice(&buf); } } @@ -311,7 +274,6 @@ impl ServiceConfigBuilder { } struct Date { - current: Instant, bytes: [u8; DATE_VALUE_LENGTH], pos: usize, } @@ -319,7 +281,6 @@ struct Date { impl Date { fn new() -> Date { let mut date = Date { - current: Instant::now(), bytes: [0; DATE_VALUE_LENGTH], pos: 0, }; @@ -328,7 +289,6 @@ impl Date { } fn update(&mut self) { self.pos = 0; - self.current = Instant::now(); write!(self, "{}", time::at_utc(time::get_time()).rfc822()).unwrap(); } } @@ -342,6 +302,68 @@ impl fmt::Write for Date { } } +#[derive(Clone)] +struct DateService(Rc); + +struct DateServiceInner { + interval: Duration, + current: UnsafeCell>, +} + +impl DateServiceInner { + fn new(interval: Duration) -> Self { + DateServiceInner { + interval, + current: UnsafeCell::new(None), + } + } + + fn get_ref(&self) -> &Option<(Date, Instant)> { + unsafe { &*self.current.get() } + } + + fn reset(&self) { + unsafe { (&mut *self.current.get()).take() }; + } + + fn update(&self) { + let now = Instant::now(); + let date = Date::new(); + *(unsafe { &mut *self.current.get() }) = Some((date, now)); + } +} + +impl DateService { + fn with(resolution: Duration) -> Self { + DateService(Rc::new(DateServiceInner::new(resolution))) + } + + fn check_date(&self) { + if self.0.get_ref().is_none() { + self.0.update(); + + // periodic date update + let s = self.clone(); + spawn(sleep(Duration::from_millis(500)).then(move |_| { + s.0.reset(); + future::ok(()) + })); + } + } + + fn now(&self) -> Instant { + self.check_date(); + self.0.get_ref().as_ref().unwrap().1 + } + + fn date(&self) -> &Date { + self.check_date(); + + let item = self.0.get_ref().as_ref().unwrap(); + &item.0 + } +} + #[cfg(test)] mod tests { use super::*; @@ -360,9 +382,9 @@ mod tests { let _ = rt.block_on(future::lazy(|| { let settings = ServiceConfig::new(KeepAlive::Os, 0, 0); let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); - settings.set_date(&mut buf1, true); + settings.set_date(&mut buf1); let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); - settings.set_date(&mut buf2, true); + settings.set_date(&mut buf2); assert_eq!(buf1, buf2); future::ok::<_, ()>(()) })); diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 247b0f01c..d0faad43f 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -7,6 +7,7 @@ use tokio_codec::{Decoder, Encoder}; use super::decoder::{PayloadDecoder, PayloadItem, RequestDecoder}; use super::encoder::{ResponseEncoder, ResponseLength}; use body::Body; +use config::ServiceConfig; use error::ParseError; use helpers; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; @@ -48,6 +49,7 @@ pub enum InMessage { /// HTTP/1 Codec pub struct Codec { + config: ServiceConfig, decoder: RequestDecoder, payload: Option, version: Version, @@ -62,20 +64,19 @@ impl Codec { /// Create HTTP/1 codec. /// /// `keepalive_enabled` how response `connection` header get generated. - pub fn new(keepalive_enabled: bool) -> Self { - Codec::with_pool(RequestPool::pool(), keepalive_enabled) + pub fn new(config: ServiceConfig) -> Self { + Codec::with_pool(RequestPool::pool(), config) } /// Create HTTP/1 codec with request's pool - pub(crate) fn with_pool( - pool: &'static RequestPool, keepalive_enabled: bool, - ) -> Self { - let flags = if keepalive_enabled { + pub(crate) fn with_pool(pool: &'static RequestPool, config: ServiceConfig) -> Self { + let flags = if config.keep_alive_enabled() { Flags::KEEPALIVE_ENABLED } else { Flags::empty() }; Codec { + config, decoder: RequestDecoder::with_pool(pool), payload: None, version: Version::HTTP_11, @@ -217,7 +218,7 @@ impl Codec { // optimized date header, set_date writes \r\n if !has_date { - // self.settings.set_date(&mut buffer, true); + self.config.set_date(buffer); buffer.extend_from_slice(b"\r\n"); } else { // msg eof diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index d44e687d1..c8ce7d65e 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -97,7 +97,7 @@ where } else { Flags::FLUSHED }; - let framed = Framed::new(stream, Codec::new(keepalive)); + let framed = Framed::new(stream, Codec::new(config.clone())); let (ka_expire, ka_timer) = if let Some(delay) = timeout { (delay.deadline(), Some(delay)) diff --git a/src/lib.rs b/src/lib.rs index 8a7bcfa43..85bf9c2ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,17 +48,10 @@ //! //! ## Features //! -//! * Supported *HTTP/1.x* and *HTTP/2.0* protocols +//! * Supported *HTTP/1.x* protocol //! * Streaming and pipelining //! * Keep-alive and slow requests handling //! * `WebSockets` server/client -//! * Transparent content compression/decompression (br, gzip, deflate) -//! * Configurable request routing -//! * Graceful server shutdown -//! * Multipart streams -//! * SSL support with OpenSSL or `native-tls` -//! * Middlewares (`Logger`, `Session`, `CORS`, `CSRF`, `DefaultHeaders`) -//! * Built on top of [Actix actor framework](https://github.com/actix/actix) //! * Supported Rust version: 1.26 or later //! //! ## Package feature From 805e7a4cd042f305175a8436ed7c0b6a8802ff99 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 8 Oct 2018 15:24:51 -0700 Subject: [PATCH 032/427] impl response body support --- src/h1/codec.rs | 24 +++- src/h1/dispatcher.rs | 134 ++++++++++-------- src/h1/encoder.rs | 46 ++++--- tests/test_server.rs | 314 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 427 insertions(+), 91 deletions(-) diff --git a/src/h1/codec.rs b/src/h1/codec.rs index d0faad43f..16965768f 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -6,7 +6,7 @@ use tokio_codec::{Decoder, Encoder}; use super::decoder::{PayloadDecoder, PayloadItem, RequestDecoder}; use super::encoder::{ResponseEncoder, ResponseLength}; -use body::Body; +use body::{Binary, Body}; use config::ServiceConfig; use error::ParseError; use helpers; @@ -26,12 +26,13 @@ bitflags! { const AVERAGE_HEADER_SIZE: usize = 30; +#[derive(Debug)] /// Http response pub enum OutMessage { /// Http response message Response(Response), /// Payload chunk - Payload(Bytes), + Payload(Option), } /// Incoming http/1 request @@ -151,6 +152,7 @@ impl Codec { buffer.extend_from_slice(reason); // content length + let mut len_is_set = true; match self.te.length { ResponseLength::Chunked => { buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") @@ -167,6 +169,10 @@ impl Codec { buffer.extend_from_slice(b"\r\n"); } ResponseLength::None => buffer.extend_from_slice(b"\r\n"), + ResponseLength::HeaderOrZero => { + len_is_set = false; + buffer.extend_from_slice(b"\r\n") + } } // write headers @@ -179,6 +185,9 @@ impl Codec { TRANSFER_ENCODING => continue, CONTENT_LENGTH => match self.te.length { ResponseLength::None => (), + ResponseLength::HeaderOrZero => { + len_is_set = true; + } _ => continue, }, DATE => { @@ -215,11 +224,13 @@ impl Codec { unsafe { buffer.advance_mut(pos); } + if !len_is_set { + buffer.extend_from_slice(b"content-length: 0\r\n") + } // optimized date header, set_date writes \r\n if !has_date { self.config.set_date(buffer); - buffer.extend_from_slice(b"\r\n"); } else { // msg eof buffer.extend_from_slice(b"\r\n"); @@ -272,8 +283,11 @@ impl Encoder for Codec { OutMessage::Response(res) => { self.encode_response(res, dst)?; } - OutMessage::Payload(bytes) => { - dst.extend_from_slice(&bytes); + OutMessage::Payload(Some(bytes)) => { + self.te.encode(bytes.as_ref(), dst)?; + } + OutMessage::Payload(None) => { + self.te.encode_eof(dst)?; } } Ok(()) diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index c8ce7d65e..c2ce12037 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -12,7 +12,7 @@ use tokio_timer::Delay; use error::{ParseError, PayloadError}; use payload::{Payload, PayloadSender, PayloadStatus, PayloadWriter}; -use body::Body; +use body::{Body, BodyStream}; use config::ServiceConfig; use error::DispatchError; use request::Request; @@ -61,9 +61,8 @@ enum Message { enum State { None, ServiceCall(S::Future), - SendResponse(Option), - SendResponseWithPayload(Option<(OutMessage, Body)>), - Payload(Body), + SendResponse(Option<(OutMessage, Body)>), + SendPayload(Option, Option), } impl State { @@ -99,6 +98,7 @@ where }; let framed = Framed::new(stream, Codec::new(config.clone())); + // keep-alive timer let (ka_expire, ka_timer) = if let Some(delay) = timeout { (delay.deadline(), Some(delay)) } else if let Some(delay) = config.keep_alive_timer() { @@ -174,59 +174,32 @@ where break if let Some(msg) = self.messages.pop_front() { match msg { Message::Item(req) => Some(self.handle_request(req)?), - Message::Error(res) => Some(State::SendResponse(Some( + Message::Error(res) => Some(State::SendResponse(Some(( OutMessage::Response(res), - ))), + Body::Empty, + )))), } } else { None }; }, - State::Payload(ref mut _body) => unimplemented!(), - State::ServiceCall(ref mut fut) => match fut - .poll() - .map_err(DispatchError::Service)? - { - Async::Ready(mut res) => { - self.framed.get_codec_mut().prepare_te(&mut res); - let body = res.replace_body(Body::Empty); - if body.is_empty() { - Some(State::SendResponse(Some(OutMessage::Response(res)))) - } else { - Some(State::SendResponseWithPayload(Some(( + // call inner service + State::ServiceCall(ref mut fut) => { + match fut.poll().map_err(DispatchError::Service)? { + Async::Ready(mut res) => { + self.framed.get_codec_mut().prepare_te(&mut res); + let body = res.replace_body(Body::Empty); + Some(State::SendResponse(Some(( OutMessage::Response(res), body, )))) } - } - Async::NotReady => None, - }, - State::SendResponse(ref mut item) => { - let msg = item.take().expect("SendResponse is empty"); - match self.framed.start_send(msg) { - Ok(AsyncSink::Ready) => { - self.flags.set( - Flags::KEEPALIVE, - self.framed.get_codec().keepalive(), - ); - self.flags.remove(Flags::FLUSHED); - Some(State::None) - } - Ok(AsyncSink::NotReady(msg)) => { - *item = Some(msg); - return Ok(()); - } - Err(err) => { - if let Some(mut payload) = self.payload.take() { - payload.set_error(PayloadError::Incomplete); - } - return Err(DispatchError::Io(err)); - } + Async::NotReady => None, } } - State::SendResponseWithPayload(ref mut item) => { - let (msg, body) = - item.take().expect("SendResponseWithPayload is empty"); + // send respons + State::SendResponse(ref mut item) => { + let (msg, body) = item.take().expect("SendResponse is empty"); match self.framed.start_send(msg) { Ok(AsyncSink::Ready) => { self.flags.set( @@ -234,7 +207,16 @@ where self.framed.get_codec().keepalive(), ); self.flags.remove(Flags::FLUSHED); - Some(State::Payload(body)) + match body { + Body::Empty => Some(State::None), + Body::Binary(bin) => Some(State::SendPayload( + None, + Some(OutMessage::Payload(bin.into())), + )), + Body::Streaming(stream) => { + Some(State::SendPayload(Some(stream), None)) + } + } } Ok(AsyncSink::NotReady(msg)) => { *item = Some((msg, body)); @@ -248,6 +230,48 @@ where } } } + // Send payload + State::SendPayload(ref mut stream, ref mut bin) => { + if let Some(item) = bin.take() { + match self.framed.start_send(item) { + Ok(AsyncSink::Ready) => { + self.flags.remove(Flags::FLUSHED); + } + Ok(AsyncSink::NotReady(item)) => { + *bin = Some(item); + return Ok(()); + } + Err(err) => return Err(DispatchError::Io(err)), + } + } + if let Some(ref mut stream) = stream { + match stream.poll() { + Ok(Async::Ready(Some(item))) => match self + .framed + .start_send(OutMessage::Payload(Some(item.into()))) + { + Ok(AsyncSink::Ready) => { + self.flags.remove(Flags::FLUSHED); + continue; + } + Ok(AsyncSink::NotReady(msg)) => { + *bin = Some(msg); + return Ok(()); + } + Err(err) => return Err(DispatchError::Io(err)), + }, + Ok(Async::Ready(None)) => Some(State::SendPayload( + None, + Some(OutMessage::Payload(None)), + )), + Ok(Async::NotReady) => return Ok(()), + // Err(err) => return Err(DispatchError::Io(err)), + Err(_) => return Err(DispatchError::Unknown), + } + } else { + Some(State::None) + } + } }; match state { @@ -275,19 +299,13 @@ where Async::Ready(mut res) => { self.framed.get_codec_mut().prepare_te(&mut res); let body = res.replace_body(Body::Empty); - if body.is_empty() { - Ok(State::SendResponse(Some(OutMessage::Response(res)))) - } else { - Ok(State::SendResponseWithPayload(Some(( - OutMessage::Response(res), - body, - )))) - } + Ok(State::SendResponse(Some((OutMessage::Response(res), body)))) } Async::NotReady => Ok(State::ServiceCall(task)), } } + /// Process one incoming message fn one_message(&mut self, msg: InMessage) -> Result<(), DispatchError> { self.flags.insert(Flags::STARTED); @@ -408,10 +426,12 @@ where // timeout on first request (slow request) return 408 trace!("Slow request timeout"); self.flags.insert(Flags::STARTED | Flags::DISCONNECTED); - self.state = - State::SendResponse(Some(OutMessage::Response( + self.state = State::SendResponse(Some(( + OutMessage::Response( Response::RequestTimeout().finish(), - ))); + ), + Body::Empty, + ))); } } else if let Some(deadline) = self.config.keep_alive_expire() { timer.reset(deadline) diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index 1544b2404..6e8d44cee 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -17,9 +17,13 @@ use response::Response; #[derive(Debug)] pub(crate) enum ResponseLength { Chunked, + /// Content length is 0 Zero, + /// Check if headers contains length or write 0 + HeaderOrZero, Length(usize), Length64(u64), + /// Do no set content-length None, } @@ -41,6 +45,16 @@ impl Default for ResponseEncoder { } impl ResponseEncoder { + /// Encode message + pub fn encode(&mut self, msg: &[u8], buf: &mut BytesMut) -> io::Result { + self.te.encode(msg, buf) + } + + /// Encode eof + pub fn encode_eof(&mut self, buf: &mut BytesMut) -> io::Result<()> { + self.te.encode_eof(buf) + } + pub fn update(&mut self, resp: &mut Response, head: bool, version: Version) { self.head = head; @@ -63,17 +77,13 @@ impl ResponseEncoder { let transfer = match resp.body() { Body::Empty => { - if !self.head { - self.length = match resp.status() { - StatusCode::NO_CONTENT - | StatusCode::CONTINUE - | StatusCode::SWITCHING_PROTOCOLS - | StatusCode::PROCESSING => ResponseLength::None, - _ => ResponseLength::Zero, - }; - } else { - self.length = ResponseLength::Zero; - } + self.length = match resp.status() { + StatusCode::NO_CONTENT + | StatusCode::CONTINUE + | StatusCode::SWITCHING_PROTOCOLS + | StatusCode::PROCESSING => ResponseLength::None, + _ => ResponseLength::HeaderOrZero, + }; TransferEncoding::empty() } Body::Binary(_) => { @@ -253,16 +263,22 @@ impl TransferEncoding { /// Encode eof. Return `EOF` state of encoder #[inline] - pub fn encode_eof(&mut self, buf: &mut BytesMut) -> bool { + pub fn encode_eof(&mut self, buf: &mut BytesMut) -> io::Result<()> { match self.kind { - TransferEncodingKind::Eof => true, - TransferEncodingKind::Length(rem) => rem == 0, + TransferEncodingKind::Eof => Ok(()), + TransferEncodingKind::Length(rem) => { + if rem != 0 { + Err(io::Error::new(io::ErrorKind::UnexpectedEof, "")) + } else { + Ok(()) + } + } TransferEncodingKind::Chunked(ref mut eof) => { if !*eof { *eof = true; buf.extend_from_slice(b"0\r\n\r\n"); } - true + Ok(()) } } } diff --git a/tests/test_server.rs b/tests/test_server.rs index 43e3966df..e86176097 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -2,6 +2,7 @@ extern crate actix; extern crate actix_http; extern crate actix_net; extern crate actix_web; +extern crate bytes; extern crate futures; use std::{io::Read, io::Write, net, thread, time}; @@ -9,9 +10,11 @@ use std::{io::Read, io::Write, net, thread, time}; use actix::System; use actix_net::server::Server; use actix_web::{client, test, HttpMessage}; -use futures::future; +use bytes::Bytes; +use futures::future::{self, ok}; +use futures::stream::once; -use actix_http::{h1, KeepAlive, Request, Response}; +use actix_http::{h1, Body, KeepAlive, Request, Response}; #[test] fn test_h1_v2() { @@ -33,7 +36,7 @@ fn test_h1_v2() { let mut sys = System::new("test"); { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) + let req = client::ClientRequest::get(format!("http://{}/", addr)) .finish() .unwrap(); let response = sys.block_on(req.send()).unwrap(); @@ -68,9 +71,7 @@ fn test_malformed_request() { thread::spawn(move || { Server::new() .bind("test", addr, move || { - h1::H1Service::build() - .client_timeout(100) - .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) + h1::H1Service::new(|_| future::ok::<_, ()>(Response::Ok().finish())) }).unwrap() .run(); }); @@ -117,21 +118,306 @@ fn test_content_length() { let mut sys = System::new("test"); { for i in 0..4 { - let req = - client::ClientRequest::get(format!("http://{}/{}", addr, i).as_str()) - .finish() - .unwrap(); + let req = client::ClientRequest::get(format!("http://{}/{}", addr, i)) + .finish() + .unwrap(); let response = sys.block_on(req.send()).unwrap(); assert_eq!(response.headers().get(&header), None); } for i in 4..6 { - let req = - client::ClientRequest::get(format!("http://{}/{}", addr, i).as_str()) - .finish() - .unwrap(); + let req = client::ClientRequest::get(format!("http://{}/{}", addr, i)) + .finish() + .unwrap(); let response = sys.block_on(req.send()).unwrap(); assert_eq!(response.headers().get(&header), Some(&value)); } } } + +#[test] +fn test_headers() { + let data = STR.repeat(10); + let data2 = data.clone(); + + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + let data = data.clone(); + h1::H1Service::new(move |_| { + let mut builder = Response::Ok(); + for idx in 0..90 { + builder.header( + format!("X-TEST-{}", idx).as_str(), + "TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", + ); + } + future::ok::<_, ()>(builder.body(data.clone())) + }) + }) + .unwrap() + .run() + }); + thread::sleep(time::Duration::from_millis(400)); + + let mut sys = System::new("test"); + let req = client::ClientRequest::get(format!("http://{}/", addr)) + .finish() + .unwrap(); + + let response = sys.block_on(req.send()).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = sys.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from(data2)); +} + +const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World"; + +#[test] +fn test_body() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + h1::H1Service::new(|_| future::ok::<_, ()>(Response::Ok().body(STR))) + }).unwrap() + .run(); + }); + thread::sleep(time::Duration::from_millis(100)); + + let mut sys = System::new("test"); + let req = client::ClientRequest::get(format!("http://{}/", addr)) + .finish() + .unwrap(); + let response = sys.block_on(req.send()).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = sys.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} + +#[test] +fn test_head_empty() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + h1::H1Service::new(|_| { + ok::<_, ()>(Response::Ok().content_length(STR.len() as u64).finish()) + }) + }).unwrap() + .run() + }); + thread::sleep(time::Duration::from_millis(100)); + + let mut sys = System::new("test"); + let req = client::ClientRequest::head(format!("http://{}/", addr)) + .finish() + .unwrap(); + let response = sys.block_on(req.send()).unwrap(); + assert!(response.status().is_success()); + + { + println!("RESP: {:?}", response); + let len = response + .headers() + .get(http::header::CONTENT_LENGTH) + .unwrap(); + assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); + } + + // read response + let bytes = sys.block_on(response.body()).unwrap(); + assert!(bytes.is_empty()); +} + +#[test] +fn test_head_binary() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + h1::H1Service::new(|_| { + ok::<_, ()>( + Response::Ok().content_length(STR.len() as u64).body(STR), + ) + }) + }).unwrap() + .run() + }); + thread::sleep(time::Duration::from_millis(100)); + + let mut sys = System::new("test"); + let req = client::ClientRequest::head(format!("http://{}/", addr)) + .finish() + .unwrap(); + let response = sys.block_on(req.send()).unwrap(); + assert!(response.status().is_success()); + + { + let len = response + .headers() + .get(http::header::CONTENT_LENGTH) + .unwrap(); + assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); + } + + // read response + let bytes = sys.block_on(response.body()).unwrap(); + assert!(bytes.is_empty()); +} + +#[test] +fn test_head_binary2() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + h1::H1Service::new(|_| ok::<_, ()>(Response::Ok().body(STR))) + }).unwrap() + .run() + }); + thread::sleep(time::Duration::from_millis(100)); + + let mut sys = System::new("test"); + let req = client::ClientRequest::head(format!("http://{}/", addr)) + .finish() + .unwrap(); + let response = sys.block_on(req.send()).unwrap(); + assert!(response.status().is_success()); + + { + let len = response + .headers() + .get(http::header::CONTENT_LENGTH) + .unwrap(); + assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); + } +} + +#[test] +fn test_body_length() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + h1::H1Service::new(|_| { + let body = once(Ok(Bytes::from_static(STR.as_ref()))); + ok::<_, ()>( + Response::Ok() + .content_length(STR.len() as u64) + .body(Body::Streaming(Box::new(body))), + ) + }) + }).unwrap() + .run() + }); + thread::sleep(time::Duration::from_millis(100)); + + let mut sys = System::new("test"); + let req = client::ClientRequest::get(format!("http://{}/", addr)) + .finish() + .unwrap(); + let response = sys.block_on(req.send()).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = sys.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} + +#[test] +fn test_body_chunked_explicit() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + h1::H1Service::new(|_| { + let body = once(Ok(Bytes::from_static(STR.as_ref()))); + ok::<_, ()>( + Response::Ok() + .chunked() + .body(Body::Streaming(Box::new(body))), + ) + }) + }).unwrap() + .run() + }); + thread::sleep(time::Duration::from_millis(100)); + + let mut sys = System::new("test"); + let req = client::ClientRequest::get(format!("http://{}/", addr)) + .finish() + .unwrap(); + let response = sys.block_on(req.send()).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = sys.block_on(response.body()).unwrap(); + + // decode + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} + +#[test] +fn test_body_chunked_implicit() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + h1::H1Service::new(|_| { + let body = once(Ok(Bytes::from_static(STR.as_ref()))); + ok::<_, ()>(Response::Ok().body(Body::Streaming(Box::new(body)))) + }) + }).unwrap() + .run() + }); + thread::sleep(time::Duration::from_millis(100)); + + let mut sys = System::new("test"); + let req = client::ClientRequest::get(format!("http://{}/", addr)) + .finish() + .unwrap(); + let response = sys.block_on(req.send()).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = sys.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} From 3984ad45dfb5f13a9e645b0f48265c5ec5b6834f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 8 Oct 2018 15:33:38 -0700 Subject: [PATCH 033/427] separate ResponseLength::Zero is not needed --- src/h1/codec.rs | 9 +++------ src/h1/encoder.rs | 6 ++---- tests/test_server.rs | 7 ++++++- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 16965768f..8f97d6779 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -158,7 +158,8 @@ impl Codec { buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") } ResponseLength::Zero => { - buffer.extend_from_slice(b"\r\ncontent-length: 0\r\n") + len_is_set = false; + buffer.extend_from_slice(b"\r\n") } ResponseLength::Length(len) => { helpers::write_content_length(len, buffer) @@ -169,10 +170,6 @@ impl Codec { buffer.extend_from_slice(b"\r\n"); } ResponseLength::None => buffer.extend_from_slice(b"\r\n"), - ResponseLength::HeaderOrZero => { - len_is_set = false; - buffer.extend_from_slice(b"\r\n") - } } // write headers @@ -185,7 +182,7 @@ impl Codec { TRANSFER_ENCODING => continue, CONTENT_LENGTH => match self.te.length { ResponseLength::None => (), - ResponseLength::HeaderOrZero => { + ResponseLength::Zero => { len_is_set = true; } _ => continue, diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index 6e8d44cee..ea11f11fd 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -17,10 +17,8 @@ use response::Response; #[derive(Debug)] pub(crate) enum ResponseLength { Chunked, - /// Content length is 0 - Zero, /// Check if headers contains length or write 0 - HeaderOrZero, + Zero, Length(usize), Length64(u64), /// Do no set content-length @@ -82,7 +80,7 @@ impl ResponseEncoder { | StatusCode::CONTINUE | StatusCode::SWITCHING_PROTOCOLS | StatusCode::PROCESSING => ResponseLength::None, - _ => ResponseLength::HeaderOrZero, + _ => ResponseLength::Zero, }; TransferEncoding::empty() } diff --git a/tests/test_server.rs b/tests/test_server.rs index e86176097..3b13b98ed 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -123,6 +123,12 @@ fn test_content_length() { .unwrap(); let response = sys.block_on(req.send()).unwrap(); assert_eq!(response.headers().get(&header), None); + + let req = client::ClientRequest::head(format!("http://{}/{}", addr, i)) + .finish() + .unwrap(); + let response = sys.block_on(req.send()).unwrap(); + assert_eq!(response.headers().get(&header), None); } for i in 4..6 { @@ -254,7 +260,6 @@ fn test_head_empty() { assert!(response.status().is_success()); { - println!("RESP: {:?}", response); let len = response .headers() .get(http::header::CONTENT_LENGTH) From f99a723643d3e0618068acb2631331e3c25f6dba Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 8 Oct 2018 15:52:12 -0700 Subject: [PATCH 034/427] add Default impl for ServiceConfig --- src/config.rs | 6 ++++++ tests/test_ws.rs | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index 2e14a33e6..ff22ea486 100644 --- a/src/config.rs +++ b/src/config.rs @@ -57,6 +57,12 @@ impl Clone for ServiceConfig { } } +impl Default for ServiceConfig { + fn default() -> Self { + Self::new(KeepAlive::Timeout(5), 0, 0) + } +} + impl ServiceConfig { /// Create instance of `ServiceConfig` pub(crate) fn new( diff --git a/tests/test_ws.rs b/tests/test_ws.rs index 8a1098747..00ccd1558 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -18,7 +18,7 @@ use bytes::Bytes; use futures::future::{ok, Either}; use futures::{Future, Sink, Stream}; -use actix_http::{h1, ws, ResponseError}; +use actix_http::{h1, ws, ResponseError, ServiceConfig}; fn ws_service(req: ws::Message) -> impl Future { match req { @@ -36,7 +36,7 @@ fn test_simple() { thread::spawn(move || { Server::new() .bind("test", addr, move || { - IntoFramed::new(|| h1::Codec::new(false)) + IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) .and_then(TakeItem::new().map_err(|_| ())) .and_then(|(req, framed): (_, Framed<_, _>)| { // validate request From 2b4870e65b70bfd1600eecc3a61f82d437599482 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 8 Oct 2018 16:10:07 -0700 Subject: [PATCH 035/427] fix tests on stable --- tests/test_server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_server.rs b/tests/test_server.rs index 3b13b98ed..f382eafbd 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -14,7 +14,7 @@ use bytes::Bytes; use futures::future::{self, ok}; use futures::stream::once; -use actix_http::{h1, Body, KeepAlive, Request, Response}; +use actix_http::{h1, http, Body, KeepAlive, Request, Response}; #[test] fn test_h1_v2() { From fd5da5945efe16ef6e7f08027312286cc4c2829b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 8 Oct 2018 21:23:52 -0700 Subject: [PATCH 036/427] update appveyor config --- .appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 7addc8c08..4af6cdbf8 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -37,4 +37,5 @@ build: false # Equivalent to Travis' `script` phase test_script: - - cargo test --no-default-features --features="flate2-rust" + - cargo clean + - cargo test From cb78d9d41a4063b81b18d1218819740916693821 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 8 Oct 2018 22:04:53 -0700 Subject: [PATCH 037/427] use actix-net release --- .appveyor.yml | 2 +- Cargo.toml | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 4af6cdbf8..780fdd6b5 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,6 +1,6 @@ environment: global: - PROJECT_NAME: actix + PROJECT_NAME: actix-http matrix: # Stable channel - TARGET: i686-pc-windows-msvc diff --git a/Cargo.toml b/Cargo.toml index 4e66ff0ee..7bfb82fb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,13 +36,14 @@ cell = ["actix-net/cell"] [dependencies] actix = "0.7.0" -actix-net = { git="https://github.com/actix/actix-net.git" } -#actix-net = { path = "../actix-net" } +actix-net = "0.1.0" +#actix-net = { git="https://github.com/actix/actix-net.git" } base64 = "0.9" bitflags = "1.0" http = "^0.1.8" httparse = "1.3" +failure = "^0.1.2" log = "0.4" mime = "0.3" rand = "0.5" @@ -55,8 +56,6 @@ lazy_static = "1.0" serde_urlencoded = "^0.5.3" cookie = { version="0.11", features=["percent-encode"] } -failure = "^0.1.2" - # io net2 = "0.2" bytes = "0.4" From 1407bf4f7f2a0e0ec6302b792eef45acbc4656bf Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 9 Oct 2018 10:36:40 -0700 Subject: [PATCH 038/427] simplify h1 codec messages --- README.md | 2 +- src/h1/codec.rs | 27 ++++---- src/h1/decoder.rs | 5 +- src/h1/dispatcher.rs | 154 +++++++++++++++++++++---------------------- tests/test_ws.rs | 2 +- 5 files changed, 90 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index b273ea8c5..59e6fc375 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Actix http [![Build Status](https://travis-ci.org/fafhrd91/actix-http.svg?branch=master)](https://travis-ci.org/fafhrd91/actix-http) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/fafhrd91/actix-http/branch/master/graph/badge.svg)](https://codecov.io/gh/fafhrd91/actix-http) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# Actix http [![Build Status](https://travis-ci.org/fafhrd91/actix-http.svg?branch=master)](https://travis-ci.org/fafhrd91/actix-http) [![Build status](https://ci.appveyor.com/api/projects/status/bwq6923pblqg55gk/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-http/branch/master) [![codecov](https://codecov.io/gh/fafhrd91/actix-http/branch/master/graph/badge.svg)](https://codecov.io/gh/fafhrd91/actix-http) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Actix http diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 8f97d6779..04cf395b6 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -32,20 +32,16 @@ pub enum OutMessage { /// Http response message Response(Response), /// Payload chunk - Payload(Option), + Chunk(Option), } /// Incoming http/1 request #[derive(Debug)] pub enum InMessage { /// Request - Message(Request), - /// Request with payload - MessageWithPayload(Request), + Message { req: Request, payload: bool }, /// Payload chunk - Chunk(Bytes), - /// End of payload - Eof, + Chunk(Option), } /// HTTP/1 Codec @@ -246,8 +242,8 @@ impl Decoder for Codec { fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { if self.payload.is_some() { Ok(match self.payload.as_mut().unwrap().decode(src)? { - Some(PayloadItem::Chunk(chunk)) => Some(InMessage::Chunk(chunk)), - Some(PayloadItem::Eof) => Some(InMessage::Eof), + Some(PayloadItem::Chunk(chunk)) => Some(InMessage::Chunk(Some(chunk))), + Some(PayloadItem::Eof) => Some(InMessage::Chunk(None)), None => None, }) } else if let Some((req, payload)) = self.decoder.decode(src)? { @@ -258,11 +254,10 @@ impl Decoder for Codec { self.flags.set(Flags::KEEPALIVE, req.keep_alive()); } self.payload = payload; - if self.payload.is_some() { - Ok(Some(InMessage::MessageWithPayload(req))) - } else { - Ok(Some(InMessage::Message(req))) - } + Ok(Some(InMessage::Message { + req, + payload: self.payload.is_some(), + })) } else { Ok(None) } @@ -280,10 +275,10 @@ impl Encoder for Codec { OutMessage::Response(res) => { self.encode_response(res, dst)?; } - OutMessage::Payload(Some(bytes)) => { + OutMessage::Chunk(Some(bytes)) => { self.te.encode(bytes.as_ref(), dst)?; } - OutMessage::Payload(None) => { + OutMessage::Chunk(None) => { self.te.encode_eof(dst)?; } } diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index d0c3fa048..5fe8b19c8 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -488,14 +488,13 @@ mod tests { impl InMessage { fn message(self) -> Request { match self { - InMessage::Message(msg) => msg, - InMessage::MessageWithPayload(msg) => msg, + InMessage::Message { req, payload: _ } => req, _ => panic!("error"), } } fn is_payload(&self) -> bool { match *self { - InMessage::MessageWithPayload(_) => true, + InMessage::Message { req: _, payload } => payload, _ => panic!("error"), } } diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index c2ce12037..8b7c29331 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -211,7 +211,7 @@ where Body::Empty => Some(State::None), Body::Binary(bin) => Some(State::SendPayload( None, - Some(OutMessage::Payload(bin.into())), + Some(OutMessage::Chunk(bin.into())), )), Body::Streaming(stream) => { Some(State::SendPayload(Some(stream), None)) @@ -248,7 +248,7 @@ where match stream.poll() { Ok(Async::Ready(Some(item))) => match self .framed - .start_send(OutMessage::Payload(Some(item.into()))) + .start_send(OutMessage::Chunk(Some(item.into()))) { Ok(AsyncSink::Ready) => { self.flags.remove(Flags::FLUSHED); @@ -262,7 +262,7 @@ where }, Ok(Async::Ready(None)) => Some(State::SendPayload( None, - Some(OutMessage::Payload(None)), + Some(OutMessage::Chunk(None)), )), Ok(Async::NotReady) => return Ok(()), // Err(err) => return Err(DispatchError::Io(err)), @@ -305,89 +305,85 @@ where } } - /// Process one incoming message - fn one_message(&mut self, msg: InMessage) -> Result<(), DispatchError> { - self.flags.insert(Flags::STARTED); - - match msg { - InMessage::Message(msg) => { - // handle request early - if self.state.is_empty() { - self.state = self.handle_request(msg)?; - } else { - self.messages.push_back(Message::Item(msg)); - } - } - InMessage::MessageWithPayload(msg) => { - // payload - let (ps, pl) = Payload::new(false); - *msg.inner.payload.borrow_mut() = Some(pl); - self.payload = Some(ps); - - self.messages.push_back(Message::Item(msg)); - } - InMessage::Chunk(chunk) => { - if let Some(ref mut payload) = self.payload { - payload.feed_data(chunk); - } else { - error!("Internal server error: unexpected payload chunk"); - self.flags.insert(Flags::DISCONNECTED); - self.messages.push_back(Message::Error( - Response::InternalServerError().finish(), - )); - self.error = Some(DispatchError::InternalError); - } - } - InMessage::Eof => { - if let Some(mut payload) = self.payload.take() { - payload.feed_eof(); - } else { - error!("Internal server error: unexpected eof"); - self.flags.insert(Flags::DISCONNECTED); - self.messages.push_back(Message::Error( - Response::InternalServerError().finish(), - )); - self.error = Some(DispatchError::InternalError); - } - } + /// Process one incoming requests + pub(self) fn poll_request(&mut self) -> Result> { + // limit a mount of non processed requests + if self.messages.len() >= MAX_PIPELINED_MESSAGES { + return Ok(false); } - Ok(()) - } - - pub(self) fn poll_request(&mut self) -> Result> { let mut updated = false; + 'outer: loop { + match self.framed.poll() { + Ok(Async::Ready(Some(msg))) => { + updated = true; + self.flags.insert(Flags::STARTED); - if self.messages.len() < MAX_PIPELINED_MESSAGES { - 'outer: loop { - match self.framed.poll() { - Ok(Async::Ready(Some(msg))) => { - updated = true; - self.one_message(msg)?; - } - Ok(Async::Ready(None)) => { - self.client_disconnected(); - break; - } - Ok(Async::NotReady) => break, - Err(ParseError::Io(e)) => { - self.client_disconnected(); - self.error = Some(DispatchError::Io(e)); - break; - } - Err(e) => { - if let Some(mut payload) = self.payload.take() { - payload.set_error(PayloadError::EncodingCorrupted); + match msg { + InMessage::Message { req, payload } => { + if payload { + let (ps, pl) = Payload::new(false); + *req.inner.payload.borrow_mut() = Some(pl); + self.payload = Some(ps); + } + + // handle request early + if self.state.is_empty() { + self.state = self.handle_request(req)?; + } else { + self.messages.push_back(Message::Item(req)); + } + } + InMessage::Chunk(Some(chunk)) => { + if let Some(ref mut payload) = self.payload { + payload.feed_data(chunk); + } else { + error!( + "Internal server error: unexpected payload chunk" + ); + self.flags.insert(Flags::DISCONNECTED); + self.messages.push_back(Message::Error( + Response::InternalServerError().finish(), + )); + self.error = Some(DispatchError::InternalError); + } + } + InMessage::Chunk(None) => { + if let Some(mut payload) = self.payload.take() { + payload.feed_eof(); + } else { + error!("Internal server error: unexpected eof"); + self.flags.insert(Flags::DISCONNECTED); + self.messages.push_back(Message::Error( + Response::InternalServerError().finish(), + )); + self.error = Some(DispatchError::InternalError); + } } - - // Malformed requests should be responded with 400 - self.messages - .push_back(Message::Error(Response::BadRequest().finish())); - self.flags.insert(Flags::DISCONNECTED); - self.error = Some(e.into()); - break; } } + Ok(Async::Ready(None)) => { + self.client_disconnected(); + break; + } + Ok(Async::NotReady) => break, + Err(ParseError::Io(e)) => { + self.client_disconnected(); + self.error = Some(DispatchError::Io(e)); + break; + } + Err(e) => { + if let Some(mut payload) = self.payload.take() { + payload.set_error(PayloadError::EncodingCorrupted); + } + + // Malformed requests should be responded with 400 + self.messages + .push_back(Message::Error(Response::BadRequest().finish())); + self.flags.insert(Flags::DISCONNECTED); + self.error = Some(e.into()); + break; + } } } diff --git a/tests/test_ws.rs b/tests/test_ws.rs index 00ccd1558..73590990c 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -40,7 +40,7 @@ fn test_simple() { .and_then(TakeItem::new().map_err(|_| ())) .and_then(|(req, framed): (_, Framed<_, _>)| { // validate request - if let Some(h1::InMessage::MessageWithPayload(req)) = req { + if let Some(h1::InMessage::Message { req, payload: _ }) = req { match ws::handshake(&req) { Err(e) => { // validation failed From 4a167dc89e7ce9b0c9fcd239bf7c4f5e17e0e7bc Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 9 Oct 2018 10:46:00 -0700 Subject: [PATCH 039/427] update readme example --- README.md | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 59e6fc375..3cb6f2308 100644 --- a/README.md +++ b/README.md @@ -5,27 +5,28 @@ Actix http ## Documentation & community resources * [User Guide](https://actix.rs/docs/) -* [API Documentation (Development)](https://actix.rs/actix-web/actix_web/) -* [API Documentation (Releases)](https://actix.rs/api/actix-web/stable/actix_web/) +* [API Documentation (Development)](https://actix.rs/actix-http/actix_http/) +* [API Documentation (Releases)](https://actix.rs/api/actix-http/stable/actix_http/) * [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-web](https://crates.io/crates/actix-web) +* Cargo package: [actix-http](https://crates.io/crates/actix-web) * Minimum supported Rust version: 1.26 or later ## Example ```rust -extern crate actix_web; -use actix_web::{http, server, App, Path, Responder}; - -fn index(info: Path<(u32, String)>) -> impl Responder { - format!("Hello {}! id:{}", info.1, info.0) -} +extern crate actix_http; +use actix_http::{h1, Response, ServiceConfig}; fn main() { - server::new( - || App::new() - .route("/{id}/{name}/index.html", http::Method::GET, index)) - .bind("127.0.0.1:8080").unwrap() + Server::new() + .bind("app", addr, move || { + IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) // <- create h1 codec + .and_then(TakeItem::new().map_err(|_| ())) // <- read one request + .and_then(|(req, framed): (_, Framed<_, _>)| { // <- send response and close conn + framed + .send(h1::OutMessage::Response(Response::Ok().finish())) + }) + }) .run(); } ``` @@ -41,6 +42,6 @@ at your option. ## Code of Conduct -Contribution to the actix-web crate is organized under the terms of the -Contributor Covenant, the maintainer of actix-net, @fafhrd91, promises to +Contribution to the actix-http crate is organized under the terms of the +Contributor Covenant, the maintainer of actix-http, @fafhrd91, promises to intervene to uphold that code of conduct. From 47b47af01a6baf8258c1286d3c82f6f28524e2ba Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 10 Oct 2018 13:20:00 -0700 Subject: [PATCH 040/427] refactor ws codec --- src/ws/codec.rs | 74 ++++++++++++++++++++++++++++++-------------- src/ws/frame.rs | 75 +++++++++++++++++++++++++-------------------- src/ws/mod.rs | 4 +-- src/ws/transport.rs | 6 ++-- tests/test_ws.rs | 21 ++++++++++--- 5 files changed, 113 insertions(+), 67 deletions(-) diff --git a/src/ws/codec.rs b/src/ws/codec.rs index 6e2b12090..7ba10672c 100644 --- a/src/ws/codec.rs +++ b/src/ws/codec.rs @@ -1,7 +1,7 @@ use bytes::BytesMut; use tokio_codec::{Decoder, Encoder}; -use super::frame::Frame; +use super::frame::Parser; use super::proto::{CloseReason, OpCode}; use super::ProtocolError; use body::Binary; @@ -21,6 +21,21 @@ pub enum Message { Close(Option), } +/// `WebSocket` frame +#[derive(Debug, PartialEq)] +pub enum Frame { + /// Text frame, codec does not verify utf8 encoding + Text(Option), + /// Binary frame + Binary(Option), + /// Ping message + Ping(String), + /// Pong message + Pong(String), + /// Close message with optional reason + Close(Option), +} + /// WebSockets protocol codec pub struct Codec { max_size: usize, @@ -60,29 +75,29 @@ impl Encoder for Codec { fn encode(&mut self, item: Message, dst: &mut BytesMut) -> Result<(), Self::Error> { match item { Message::Text(txt) => { - Frame::write_message(dst, txt, OpCode::Text, true, !self.server) + Parser::write_message(dst, txt, OpCode::Text, true, !self.server) } Message::Binary(bin) => { - Frame::write_message(dst, bin, OpCode::Binary, true, !self.server) + Parser::write_message(dst, bin, OpCode::Binary, true, !self.server) } Message::Ping(txt) => { - Frame::write_message(dst, txt, OpCode::Ping, true, !self.server) + Parser::write_message(dst, txt, OpCode::Ping, true, !self.server) } Message::Pong(txt) => { - Frame::write_message(dst, txt, OpCode::Pong, true, !self.server) + Parser::write_message(dst, txt, OpCode::Pong, true, !self.server) } - Message::Close(reason) => Frame::write_close(dst, reason, !self.server), + Message::Close(reason) => Parser::write_close(dst, reason, !self.server), } Ok(()) } } impl Decoder for Codec { - type Item = Message; + type Item = Frame; type Error = ProtocolError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - match Frame::parse(src, self.server, self.max_size) { + match Parser::parse(src, self.server, self.max_size) { Ok(Some((finished, opcode, payload))) => { // continuation is not supported if !finished { @@ -93,23 +108,36 @@ impl Decoder for Codec { OpCode::Continue => Err(ProtocolError::NoContinuation), OpCode::Bad => Err(ProtocolError::BadOpCode), OpCode::Close => { - let close_reason = Frame::parse_close_payload(&payload); - Ok(Some(Message::Close(close_reason))) - } - OpCode::Ping => Ok(Some(Message::Ping( - String::from_utf8_lossy(payload.as_ref()).into(), - ))), - OpCode::Pong => Ok(Some(Message::Pong( - String::from_utf8_lossy(payload.as_ref()).into(), - ))), - OpCode::Binary => Ok(Some(Message::Binary(payload))), - OpCode::Text => { - let tmp = Vec::from(payload.as_ref()); - match String::from_utf8(tmp) { - Ok(s) => Ok(Some(Message::Text(s))), - Err(_) => Err(ProtocolError::BadEncoding), + if let Some(ref pl) = payload { + let close_reason = Parser::parse_close_payload(pl); + Ok(Some(Frame::Close(close_reason))) + } else { + Ok(Some(Frame::Close(None))) } } + OpCode::Ping => { + if let Some(ref pl) = payload { + Ok(Some(Frame::Ping(String::from_utf8_lossy(pl).into()))) + } else { + Ok(Some(Frame::Ping(String::new()))) + } + } + OpCode::Pong => { + if let Some(ref pl) = payload { + Ok(Some(Frame::Pong(String::from_utf8_lossy(pl).into()))) + } else { + Ok(Some(Frame::Pong(String::new()))) + } + } + OpCode::Binary => Ok(Some(Frame::Binary(payload))), + OpCode::Text => { + Ok(Some(Frame::Text(payload))) + //let tmp = Vec::from(payload.as_ref()); + //match String::from_utf8(tmp) { + // Ok(s) => Ok(Some(Message::Text(s))), + // Err(_) => Err(ProtocolError::BadEncoding), + //} + } } } Ok(None) => Ok(None), diff --git a/src/ws/frame.rs b/src/ws/frame.rs index 38bebc283..de1b92394 100644 --- a/src/ws/frame.rs +++ b/src/ws/frame.rs @@ -9,9 +9,9 @@ use ws::ProtocolError; /// A struct representing a `WebSocket` frame. #[derive(Debug)] -pub struct Frame; +pub struct Parser; -impl Frame { +impl Parser { fn parse_metadata( src: &[u8], server: bool, max_size: usize, ) -> Result)>, ProtocolError> { @@ -87,10 +87,10 @@ impl Frame { /// Parse the input stream into a frame. pub fn parse( src: &mut BytesMut, server: bool, max_size: usize, - ) -> Result, ProtocolError> { + ) -> Result)>, ProtocolError> { // try to parse ws frame metadata let (idx, finished, opcode, length, mask) = - match Frame::parse_metadata(src, server, max_size)? { + match Parser::parse_metadata(src, server, max_size)? { None => return Ok(None), Some(res) => res, }; @@ -105,7 +105,7 @@ impl Frame { // no need for body if length == 0 { - return Ok(Some((finished, opcode, Binary::from("")))); + return Ok(Some((finished, opcode, None))); } let mut data = src.split_to(length); @@ -117,7 +117,7 @@ impl Frame { } OpCode::Close if length > 125 => { debug!("Received close frame with payload length exceeding 125. Morphing to protocol close frame."); - return Ok(Some((true, OpCode::Close, Binary::from("")))); + return Ok(Some((true, OpCode::Close, None))); } _ => (), } @@ -127,16 +127,16 @@ impl Frame { apply_mask(&mut data, mask); } - Ok(Some((finished, opcode, data.into()))) + Ok(Some((finished, opcode, Some(data)))) } /// Parse the payload of a close frame. - pub fn parse_close_payload(payload: &Binary) -> Option { + pub fn parse_close_payload(payload: &[u8]) -> Option { if payload.len() >= 2 { - let raw_code = NetworkEndian::read_u16(payload.as_ref()); + let raw_code = NetworkEndian::read_u16(payload); let code = CloseCode::from(raw_code); let description = if payload.len() > 2 { - Some(String::from_utf8_lossy(&payload.as_ref()[2..]).into()) + Some(String::from_utf8_lossy(&payload[2..]).into()) } else { None }; @@ -203,33 +203,40 @@ impl Frame { } }; - Frame::write_message(dst, payload, OpCode::Close, true, mask) + Parser::write_message(dst, payload, OpCode::Close, true, mask) } } #[cfg(test)] mod tests { use super::*; + use bytes::Bytes; struct F { finished: bool, opcode: OpCode, - payload: Binary, + payload: Bytes, } - fn is_none(frm: &Result, ProtocolError>) -> bool { + fn is_none( + frm: &Result)>, ProtocolError>, + ) -> bool { match *frm { Ok(None) => true, _ => false, } } - fn extract(frm: Result, ProtocolError>) -> F { + fn extract( + frm: Result)>, ProtocolError>, + ) -> F { match frm { Ok(Some((finished, opcode, payload))) => F { finished, opcode, - payload, + payload: payload + .map(|b| b.freeze()) + .unwrap_or_else(|| Bytes::from("")), }, _ => unreachable!("error"), } @@ -238,12 +245,12 @@ mod tests { #[test] fn test_parse() { let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]); - assert!(is_none(&Frame::parse(&mut buf, false, 1024))); + assert!(is_none(&Parser::parse(&mut buf, false, 1024))); let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]); buf.extend(b"1"); - let frame = extract(Frame::parse(&mut buf, false, 1024)); + let frame = extract(Parser::parse(&mut buf, false, 1024)); assert!(!frame.finished); assert_eq!(frame.opcode, OpCode::Text); assert_eq!(frame.payload.as_ref(), &b"1"[..]); @@ -252,7 +259,7 @@ mod tests { #[test] fn test_parse_length0() { let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0000u8][..]); - let frame = extract(Frame::parse(&mut buf, false, 1024)); + let frame = extract(Parser::parse(&mut buf, false, 1024)); assert!(!frame.finished); assert_eq!(frame.opcode, OpCode::Text); assert!(frame.payload.is_empty()); @@ -261,13 +268,13 @@ mod tests { #[test] fn test_parse_length2() { let mut buf = BytesMut::from(&[0b0000_0001u8, 126u8][..]); - assert!(is_none(&Frame::parse(&mut buf, false, 1024))); + assert!(is_none(&Parser::parse(&mut buf, false, 1024))); let mut buf = BytesMut::from(&[0b0000_0001u8, 126u8][..]); buf.extend(&[0u8, 4u8][..]); buf.extend(b"1234"); - let frame = extract(Frame::parse(&mut buf, false, 1024)); + let frame = extract(Parser::parse(&mut buf, false, 1024)); assert!(!frame.finished); assert_eq!(frame.opcode, OpCode::Text); assert_eq!(frame.payload.as_ref(), &b"1234"[..]); @@ -276,13 +283,13 @@ mod tests { #[test] fn test_parse_length4() { let mut buf = BytesMut::from(&[0b0000_0001u8, 127u8][..]); - assert!(is_none(&Frame::parse(&mut buf, false, 1024))); + assert!(is_none(&Parser::parse(&mut buf, false, 1024))); let mut buf = BytesMut::from(&[0b0000_0001u8, 127u8][..]); buf.extend(&[0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 4u8][..]); buf.extend(b"1234"); - let frame = extract(Frame::parse(&mut buf, false, 1024)); + let frame = extract(Parser::parse(&mut buf, false, 1024)); assert!(!frame.finished); assert_eq!(frame.opcode, OpCode::Text); assert_eq!(frame.payload.as_ref(), &b"1234"[..]); @@ -294,12 +301,12 @@ mod tests { buf.extend(b"0001"); buf.extend(b"1"); - assert!(Frame::parse(&mut buf, false, 1024).is_err()); + assert!(Parser::parse(&mut buf, false, 1024).is_err()); - let frame = extract(Frame::parse(&mut buf, true, 1024)); + let frame = extract(Parser::parse(&mut buf, true, 1024)); assert!(!frame.finished); assert_eq!(frame.opcode, OpCode::Text); - assert_eq!(frame.payload, vec![1u8].into()); + assert_eq!(frame.payload, Bytes::from(vec![1u8])); } #[test] @@ -307,12 +314,12 @@ mod tests { let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]); buf.extend(&[1u8]); - assert!(Frame::parse(&mut buf, true, 1024).is_err()); + assert!(Parser::parse(&mut buf, true, 1024).is_err()); - let frame = extract(Frame::parse(&mut buf, false, 1024)); + let frame = extract(Parser::parse(&mut buf, false, 1024)); assert!(!frame.finished); assert_eq!(frame.opcode, OpCode::Text); - assert_eq!(frame.payload, vec![1u8].into()); + assert_eq!(frame.payload, Bytes::from(vec![1u8])); } #[test] @@ -320,9 +327,9 @@ mod tests { let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0010u8][..]); buf.extend(&[1u8, 1u8]); - assert!(Frame::parse(&mut buf, true, 1).is_err()); + assert!(Parser::parse(&mut buf, true, 1).is_err()); - if let Err(ProtocolError::Overflow) = Frame::parse(&mut buf, false, 0) { + if let Err(ProtocolError::Overflow) = Parser::parse(&mut buf, false, 0) { } else { unreachable!("error"); } @@ -331,7 +338,7 @@ mod tests { #[test] fn test_ping_frame() { let mut buf = BytesMut::new(); - Frame::write_message(&mut buf, Vec::from("data"), OpCode::Ping, true, false); + Parser::write_message(&mut buf, Vec::from("data"), OpCode::Ping, true, false); let mut v = vec![137u8, 4u8]; v.extend(b"data"); @@ -341,7 +348,7 @@ mod tests { #[test] fn test_pong_frame() { let mut buf = BytesMut::new(); - Frame::write_message(&mut buf, Vec::from("data"), OpCode::Pong, true, false); + Parser::write_message(&mut buf, Vec::from("data"), OpCode::Pong, true, false); let mut v = vec![138u8, 4u8]; v.extend(b"data"); @@ -352,7 +359,7 @@ mod tests { fn test_close_frame() { let mut buf = BytesMut::new(); let reason = (CloseCode::Normal, "data"); - Frame::write_close(&mut buf, Some(reason.into()), false); + Parser::write_close(&mut buf, Some(reason.into()), false); let mut v = vec![136u8, 6u8, 3u8, 232u8]; v.extend(b"data"); @@ -362,7 +369,7 @@ mod tests { #[test] fn test_empty_close_frame() { let mut buf = BytesMut::new(); - Frame::write_close(&mut buf, None, false); + Parser::write_close(&mut buf, None, false); assert_eq!(&buf[..], &vec![0x88, 0x00][..]); } } diff --git a/src/ws/mod.rs b/src/ws/mod.rs index 5ebb502bb..7df1f4b4d 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -16,8 +16,8 @@ mod mask; mod proto; mod transport; -pub use self::codec::{Codec, Message}; -pub use self::frame::Frame; +pub use self::codec::{Codec, Frame, Message}; +pub use self::frame::Parser; pub use self::proto::{CloseCode, CloseReason, OpCode}; pub use self::transport::Transport; diff --git a/src/ws/transport.rs b/src/ws/transport.rs index aabeb5d5a..102d02b43 100644 --- a/src/ws/transport.rs +++ b/src/ws/transport.rs @@ -4,7 +4,7 @@ use actix_net::service::{IntoService, Service}; use futures::{Future, Poll}; use tokio_io::{AsyncRead, AsyncWrite}; -use super::{Codec, Message}; +use super::{Codec, Frame, Message}; pub struct Transport where @@ -17,7 +17,7 @@ where impl Transport where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Future: 'static, S::Error: 'static, { @@ -37,7 +37,7 @@ where impl Future for Transport where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Future: 'static, S::Error: 'static, { diff --git a/tests/test_ws.rs b/tests/test_ws.rs index 73590990c..91e212efd 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -20,12 +20,23 @@ use futures::{Future, Sink, Stream}; use actix_http::{h1, ws, ResponseError, ServiceConfig}; -fn ws_service(req: ws::Message) -> impl Future { +fn ws_service(req: ws::Frame) -> impl Future { match req { - ws::Message::Ping(msg) => ok(ws::Message::Pong(msg)), - ws::Message::Text(text) => ok(ws::Message::Text(text)), - ws::Message::Binary(bin) => ok(ws::Message::Binary(bin)), - ws::Message::Close(reason) => ok(ws::Message::Close(reason)), + ws::Frame::Ping(msg) => ok(ws::Message::Pong(msg)), + ws::Frame::Text(text) => { + let text = if let Some(pl) = text { + String::from_utf8(Vec::from(pl.as_ref())).unwrap() + } else { + String::new() + }; + ok(ws::Message::Text(text)) + } + ws::Frame::Binary(bin) => ok(ws::Message::Binary( + bin.map(|e| e.freeze()) + .unwrap_or_else(|| Bytes::from("")) + .into(), + )), + ws::Frame::Close(reason) => ok(ws::Message::Close(reason)), _ => ok(ws::Message::Close(None)), } } From 06addd55232a866dd195149c3622c9b37f5b9ae5 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 10 Oct 2018 13:23:25 -0700 Subject: [PATCH 041/427] update deps --- Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7bfb82fb3..9193aeeaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,15 +35,15 @@ session = ["cookie/secure"] cell = ["actix-net/cell"] [dependencies] -actix = "0.7.0" -actix-net = "0.1.0" +actix = "0.7.5" +actix-net = "0.1.1" #actix-net = { git="https://github.com/actix/actix-net.git" } base64 = "0.9" bitflags = "1.0" -http = "^0.1.8" +http = "0.1.8" httparse = "1.3" -failure = "^0.1.2" +failure = "0.1.2" log = "0.4" mime = "0.3" rand = "0.5" @@ -53,7 +53,7 @@ sha1 = "0.6" time = "0.1" encoding = "0.2" lazy_static = "1.0" -serde_urlencoded = "^0.5.3" +serde_urlencoded = "0.5.3" cookie = { version="0.11", features=["percent-encode"] } # io From b960b5827c844d32bb85ea560d0d9e0783c2aaf7 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 11 Oct 2018 20:15:10 -0700 Subject: [PATCH 042/427] export Uri --- src/h1/service.rs | 2 +- src/lib.rs | 5 +++-- src/request.rs | 13 ++++++++----- src/uri.rs | 6 +++--- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/h1/service.rs b/src/h1/service.rs index a7261df17..aa7a51733 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -22,7 +22,7 @@ pub struct H1Service { impl H1Service where - S: NewService, + S: NewService + Clone, S::Service: Clone, S::Error: Debug, { diff --git a/src/lib.rs b/src/lib.rs index 85bf9c2ff..5ce3ec39f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,7 +110,7 @@ mod json; mod payload; mod request; mod response; -mod uri; +pub mod uri; pub mod error; pub mod h1; @@ -148,10 +148,11 @@ pub mod http { //! Various HTTP related types // re-exports + pub use modhttp::header::{HeaderName, HeaderValue}; pub use modhttp::{Method, StatusCode, Version}; #[doc(hidden)] - pub use modhttp::{uri, Error, Extensions, HeaderMap, HttpTryFrom, Uri}; + pub use modhttp::{uri, Error, HeaderMap, HttpTryFrom, Uri}; pub use cookie::{Cookie, CookieBuilder}; diff --git a/src/request.rs b/src/request.rs index 82d8c22fa..ef28e3694 100644 --- a/src/request.rs +++ b/src/request.rs @@ -8,7 +8,7 @@ use http::{header, HeaderMap, Method, Uri, Version}; use extensions::Extensions; use httpmessage::HttpMessage; use payload::Payload; -use uri::Url as InnerUrl; +use uri::Url; bitflags! { pub(crate) struct MessageFlags: u8 { @@ -25,7 +25,7 @@ pub struct Request { pub(crate) struct InnerRequest { pub(crate) version: Version, pub(crate) method: Method, - pub(crate) url: InnerUrl, + pub(crate) url: Url, pub(crate) flags: Cell, pub(crate) headers: HeaderMap, pub(crate) extensions: RefCell, @@ -73,7 +73,7 @@ impl Request { inner: Rc::new(InnerRequest { pool, method: Method::GET, - url: InnerUrl::default(), + url: Url::default(), version: Version::HTTP_11, headers: HeaderMap::with_capacity(16), flags: Cell::new(MessageFlags::empty()), @@ -94,7 +94,7 @@ impl Request { } #[inline] - pub(crate) fn url(&self) -> &InnerUrl { + pub fn url(&self) -> &Url { &self.inner().url } @@ -162,7 +162,10 @@ impl Request { self.inner().method == Method::CONNECT } - pub(crate) fn clone(&self) -> Self { + #[doc(hidden)] + /// Note: this method should be called only as part of clone operation + /// of wrapper type. + pub fn clone_request(&self) -> Self { Request { inner: self.inner.clone(), } diff --git a/src/uri.rs b/src/uri.rs index 881cf20a8..6edd220ce 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -32,11 +32,11 @@ fn set_bit(array: &mut [u8], ch: u8) { } lazy_static! { - static ref DEFAULT_QUOTER: Quoter = { Quoter::new(b"@:", b"/+") }; + pub static ref DEFAULT_QUOTER: Quoter = { Quoter::new(b"@:", b"/+") }; } #[derive(Default, Clone, Debug)] -pub(crate) struct Url { +pub struct Url { uri: Uri, path: Option>, } @@ -61,7 +61,7 @@ impl Url { } } -pub(crate) struct Quoter { +pub struct Quoter { safe_table: [u8; 16], protected_table: [u8; 16], } From d39c018c9384963030d0adda0e29a0735d7d5179 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 13 Oct 2018 23:57:31 -0700 Subject: [PATCH 043/427] do not handle upgrade and connect requests --- src/h1/codec.rs | 43 ++++++++++++++++++++------- src/h1/decoder.rs | 45 ++++++++++++++++++++--------- src/h1/dispatcher.rs | 69 ++++++++++++++++++++++++++++++-------------- src/h1/mod.rs | 13 ++++++++- src/h1/service.rs | 5 ++-- tests/test_server.rs | 21 +++++++++----- tests/test_ws.rs | 2 +- 7 files changed, 141 insertions(+), 57 deletions(-) diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 04cf395b6..a91f5cb34 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -4,7 +4,7 @@ use std::io::{self, Write}; use bytes::{BufMut, Bytes, BytesMut}; use tokio_codec::{Decoder, Encoder}; -use super::decoder::{PayloadDecoder, PayloadItem, RequestDecoder}; +use super::decoder::{PayloadDecoder, PayloadItem, RequestDecoder, RequestPayloadType}; use super::encoder::{ResponseEncoder, ResponseLength}; use body::{Binary, Body}; use config::ServiceConfig; @@ -17,10 +17,11 @@ use response::Response; bitflags! { struct Flags: u8 { - const HEAD = 0b0000_0001; - const UPGRADE = 0b0000_0010; - const KEEPALIVE = 0b0000_0100; - const KEEPALIVE_ENABLED = 0b0001_0000; + const HEAD = 0b0000_0001; + const UPGRADE = 0b0000_0010; + const KEEPALIVE = 0b0000_0100; + const KEEPALIVE_ENABLED = 0b0000_1000; + const UNHANDLED = 0b0001_0000; } } @@ -39,11 +40,19 @@ pub enum OutMessage { #[derive(Debug)] pub enum InMessage { /// Request - Message { req: Request, payload: bool }, + Message(Request, InMessageType), /// Payload chunk Chunk(Option), } +/// Incoming request type +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InMessageType { + None, + Payload, + Unhandled, +} + /// HTTP/1 Codec pub struct Codec { config: ServiceConfig, @@ -246,6 +255,8 @@ impl Decoder for Codec { Some(PayloadItem::Eof) => Some(InMessage::Chunk(None)), None => None, }) + } else if self.flags.contains(Flags::UNHANDLED) { + Ok(None) } else if let Some((req, payload)) = self.decoder.decode(src)? { self.flags .set(Flags::HEAD, req.inner.method == Method::HEAD); @@ -253,11 +264,21 @@ impl Decoder for Codec { if self.flags.contains(Flags::KEEPALIVE_ENABLED) { self.flags.set(Flags::KEEPALIVE, req.keep_alive()); } - self.payload = payload; - Ok(Some(InMessage::Message { - req, - payload: self.payload.is_some(), - })) + let payload = match payload { + RequestPayloadType::None => { + self.payload = None; + InMessageType::None + } + RequestPayloadType::Payload(pl) => { + self.payload = Some(pl); + InMessageType::Payload + } + RequestPayloadType::Unhandled => { + self.payload = None; + InMessageType::Unhandled + } + }; + Ok(Some(InMessage::Message(req, payload))) } else { Ok(None) } diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 5fe8b19c8..c2a8d0e99 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -16,6 +16,13 @@ const MAX_HEADERS: usize = 96; pub struct RequestDecoder(&'static RequestPool); +/// Incoming request type +pub enum RequestPayloadType { + None, + Payload(PayloadDecoder), + Unhandled, +} + impl RequestDecoder { pub(crate) fn with_pool(pool: &'static RequestPool) -> RequestDecoder { RequestDecoder(pool) @@ -29,7 +36,7 @@ impl Default for RequestDecoder { } impl Decoder for RequestDecoder { - type Item = (Request, Option); + type Item = (Request, RequestPayloadType); type Error = ParseError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { @@ -149,18 +156,18 @@ impl Decoder for RequestDecoder { // https://tools.ietf.org/html/rfc7230#section-3.3.3 let decoder = if chunked { // Chunked encoding - Some(PayloadDecoder::chunked()) + RequestPayloadType::Payload(PayloadDecoder::chunked()) } else if let Some(len) = content_length { // Content-Length - Some(PayloadDecoder::length(len)) + RequestPayloadType::Payload(PayloadDecoder::length(len)) } else if has_upgrade || msg.inner.method == Method::CONNECT { // upgrade(websocket) or connect - Some(PayloadDecoder::eof()) + RequestPayloadType::Unhandled } else if src.len() >= MAX_BUFFER_SIZE { error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); return Err(ParseError::TooLarge); } else { - None + RequestPayloadType::None }; Ok(Some((msg, decoder))) @@ -481,20 +488,36 @@ mod tests { use super::*; use error::ParseError; - use h1::InMessage; + use h1::{InMessage, InMessageType}; use httpmessage::HttpMessage; use request::Request; + impl RequestPayloadType { + fn unwrap(self) -> PayloadDecoder { + match self { + RequestPayloadType::Payload(pl) => pl, + _ => panic!(), + } + } + + fn is_unhandled(&self) -> bool { + match self { + RequestPayloadType::Unhandled => true, + _ => false, + } + } + } + impl InMessage { fn message(self) -> Request { match self { - InMessage::Message { req, payload: _ } => req, + InMessage::Message(req, _) => req, _ => panic!("error"), } } fn is_payload(&self) -> bool { match *self { - InMessage::Message { req: _, payload } => payload, + InMessage::Message(_, payload) => payload == InMessageType::Payload, _ => panic!("error"), } } @@ -919,13 +942,9 @@ mod tests { ); let mut reader = RequestDecoder::default(); let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); - let mut pl = pl.unwrap(); assert!(!req.keep_alive()); assert!(req.upgrade()); - assert_eq!( - pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), - b"some raw data" - ); + assert!(pl.is_unhandled()); } #[test] diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 8b7c29331..8ae2ae8ce 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -18,7 +18,8 @@ use error::DispatchError; use request::Request; use response::Response; -use super::codec::{Codec, InMessage, OutMessage}; +use super::codec::{Codec, InMessage, InMessageType, OutMessage}; +use super::H1ServiceResult; const MAX_PIPELINED_MESSAGES: usize = 16; @@ -41,13 +42,14 @@ where { service: S, flags: Flags, - framed: Framed, + framed: Option>, error: Option>, config: ServiceConfig, state: State, payload: Option, messages: VecDeque, + unhandled: Option, ka_expire: Instant, ka_timer: Option, @@ -112,9 +114,10 @@ where state: State::None, error: None, messages: VecDeque::new(), + framed: Some(framed), + unhandled: None, service, flags, - framed, config, ka_expire, ka_timer, @@ -144,7 +147,7 @@ where /// Flush stream fn poll_flush(&mut self) -> Poll<(), DispatchError> { if !self.flags.contains(Flags::FLUSHED) { - match self.framed.poll_complete() { + match self.framed.as_mut().unwrap().poll_complete() { Ok(Async::NotReady) => Ok(Async::NotReady), Err(err) => { debug!("Error sending data: {}", err); @@ -187,7 +190,11 @@ where State::ServiceCall(ref mut fut) => { match fut.poll().map_err(DispatchError::Service)? { Async::Ready(mut res) => { - self.framed.get_codec_mut().prepare_te(&mut res); + self.framed + .as_mut() + .unwrap() + .get_codec_mut() + .prepare_te(&mut res); let body = res.replace_body(Body::Empty); Some(State::SendResponse(Some(( OutMessage::Response(res), @@ -200,11 +207,11 @@ where // send respons State::SendResponse(ref mut item) => { let (msg, body) = item.take().expect("SendResponse is empty"); - match self.framed.start_send(msg) { + match self.framed.as_mut().unwrap().start_send(msg) { Ok(AsyncSink::Ready) => { self.flags.set( Flags::KEEPALIVE, - self.framed.get_codec().keepalive(), + self.framed.as_mut().unwrap().get_codec().keepalive(), ); self.flags.remove(Flags::FLUSHED); match body { @@ -233,7 +240,7 @@ where // Send payload State::SendPayload(ref mut stream, ref mut bin) => { if let Some(item) = bin.take() { - match self.framed.start_send(item) { + match self.framed.as_mut().unwrap().start_send(item) { Ok(AsyncSink::Ready) => { self.flags.remove(Flags::FLUSHED); } @@ -248,6 +255,8 @@ where match stream.poll() { Ok(Async::Ready(Some(item))) => match self .framed + .as_mut() + .unwrap() .start_send(OutMessage::Chunk(Some(item.into()))) { Ok(AsyncSink::Ready) => { @@ -297,7 +306,11 @@ where let mut task = self.service.call(req); match task.poll().map_err(DispatchError::Service)? { Async::Ready(mut res) => { - self.framed.get_codec_mut().prepare_te(&mut res); + self.framed + .as_mut() + .unwrap() + .get_codec_mut() + .prepare_te(&mut res); let body = res.replace_body(Body::Empty); Ok(State::SendResponse(Some((OutMessage::Response(res), body)))) } @@ -314,17 +327,24 @@ where let mut updated = false; 'outer: loop { - match self.framed.poll() { + match self.framed.as_mut().unwrap().poll() { Ok(Async::Ready(Some(msg))) => { updated = true; self.flags.insert(Flags::STARTED); match msg { - InMessage::Message { req, payload } => { - if payload { - let (ps, pl) = Payload::new(false); - *req.inner.payload.borrow_mut() = Some(pl); - self.payload = Some(ps); + InMessage::Message(req, payload) => { + match payload { + InMessageType::Payload => { + let (ps, pl) = Payload::new(false); + *req.inner.payload.borrow_mut() = Some(pl); + self.payload = Some(ps); + } + InMessageType::Unhandled => { + self.unhandled = Some(req); + return Ok(updated); + } + _ => (), } // handle request early @@ -454,15 +474,16 @@ where S: Service, S::Error: Debug, { - type Item = (); + type Item = H1ServiceResult; type Error = DispatchError; #[inline] - fn poll(&mut self) -> Poll<(), Self::Error> { + fn poll(&mut self) -> Poll { if self.flags.contains(Flags::SHUTDOWN) { self.poll_keepalive()?; try_ready!(self.poll_flush()); - Ok(AsyncWrite::shutdown(self.framed.get_mut())?) + let io = self.framed.take().unwrap().into_inner(); + Ok(Async::Ready(H1ServiceResult::Shutdown(io))) } else { self.poll_keepalive()?; self.poll_request()?; @@ -474,15 +495,21 @@ where if let Some(err) = self.error.take() { Err(err) } else if self.flags.contains(Flags::DISCONNECTED) { - Ok(Async::Ready(())) + Ok(Async::Ready(H1ServiceResult::Disconnected)) + } + // unhandled request (upgrade or connect) + else if self.unhandled.is_some() { + let req = self.unhandled.take().unwrap(); + let framed = self.framed.take().unwrap(); + Ok(Async::Ready(H1ServiceResult::Unhandled(req, framed))) } // disconnect if keep-alive is not enabled else if self.flags.contains(Flags::STARTED) && !self .flags .intersects(Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED) { - self.flags.insert(Flags::SHUTDOWN); - self.poll() + let io = self.framed.take().unwrap().into_inner(); + Ok(Async::Ready(H1ServiceResult::Shutdown(io))) } else { Ok(Async::NotReady) } diff --git a/src/h1/mod.rs b/src/h1/mod.rs index 634136a47..4e196ad54 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -1,11 +1,22 @@ //! HTTP/1 implementation +use actix_net::codec::Framed; + mod codec; mod decoder; mod dispatcher; mod encoder; mod service; -pub use self::codec::{Codec, InMessage, OutMessage}; +pub use self::codec::{Codec, InMessage, InMessageType, OutMessage}; pub use self::decoder::{PayloadDecoder, RequestDecoder}; pub use self::dispatcher::Dispatcher; pub use self::service::{H1Service, H1ServiceHandler}; + +use request::Request; + +/// H1 service response type +pub enum H1ServiceResult { + Disconnected, + Shutdown(T), + Unhandled(Request, Framed), +} diff --git a/src/h1/service.rs b/src/h1/service.rs index aa7a51733..6e9d7d651 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -12,6 +12,7 @@ use request::Request; use response::Response; use super::dispatcher::Dispatcher; +use super::H1ServiceResult; /// `NewService` implementation for HTTP1 transport pub struct H1Service { @@ -51,7 +52,7 @@ where S::Error: Debug, { type Request = T; - type Response = (); + type Response = H1ServiceResult; type Error = DispatchError; type InitError = S::InitError; type Service = H1ServiceHandler; @@ -243,7 +244,7 @@ where S::Error: Debug, { type Request = T; - type Response = (); + type Response = H1ServiceResult; type Error = DispatchError; type Future = Dispatcher; diff --git a/tests/test_server.rs b/tests/test_server.rs index f382eafbd..c8de0290d 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -9,6 +9,7 @@ use std::{io::Read, io::Write, net, thread, time}; use actix::System; use actix_net::server::Server; +use actix_net::service::NewServiceExt; use actix_web::{client, test, HttpMessage}; use bytes::Bytes; use futures::future::{self, ok}; @@ -29,6 +30,7 @@ fn test_h1_v2() { .server_hostname("localhost") .server_address(addr) .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) + .map(|_| ()) }).unwrap() .run(); }); @@ -53,6 +55,7 @@ fn test_slow_request() { h1::H1Service::build() .client_timeout(100) .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) + .map(|_| ()) }).unwrap() .run(); }); @@ -72,6 +75,7 @@ fn test_malformed_request() { Server::new() .bind("test", addr, move || { h1::H1Service::new(|_| future::ok::<_, ()>(Response::Ok().finish())) + .map(|_| ()) }).unwrap() .run(); }); @@ -106,7 +110,7 @@ fn test_content_length() { StatusCode::NOT_FOUND, ]; future::ok::<_, ()>(Response::new(statuses[indx])) - }) + }).map(|_| ()) }).unwrap() .run(); }); @@ -172,7 +176,7 @@ fn test_headers() { ); } future::ok::<_, ()>(builder.body(data.clone())) - }) + }).map(|_| ()) }) .unwrap() .run() @@ -221,6 +225,7 @@ fn test_body() { Server::new() .bind("test", addr, move || { h1::H1Service::new(|_| future::ok::<_, ()>(Response::Ok().body(STR))) + .map(|_| ()) }).unwrap() .run(); }); @@ -246,7 +251,7 @@ fn test_head_empty() { .bind("test", addr, move || { h1::H1Service::new(|_| { ok::<_, ()>(Response::Ok().content_length(STR.len() as u64).finish()) - }) + }).map(|_| ()) }).unwrap() .run() }); @@ -282,7 +287,7 @@ fn test_head_binary() { ok::<_, ()>( Response::Ok().content_length(STR.len() as u64).body(STR), ) - }) + }).map(|_| ()) }).unwrap() .run() }); @@ -314,7 +319,7 @@ fn test_head_binary2() { thread::spawn(move || { Server::new() .bind("test", addr, move || { - h1::H1Service::new(|_| ok::<_, ()>(Response::Ok().body(STR))) + h1::H1Service::new(|_| ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) }).unwrap() .run() }); @@ -349,7 +354,7 @@ fn test_body_length() { .content_length(STR.len() as u64) .body(Body::Streaming(Box::new(body))), ) - }) + }).map(|_| ()) }).unwrap() .run() }); @@ -380,7 +385,7 @@ fn test_body_chunked_explicit() { .chunked() .body(Body::Streaming(Box::new(body))), ) - }) + }).map(|_| ()) }).unwrap() .run() }); @@ -409,7 +414,7 @@ fn test_body_chunked_implicit() { h1::H1Service::new(|_| { let body = once(Ok(Bytes::from_static(STR.as_ref()))); ok::<_, ()>(Response::Ok().body(Body::Streaming(Box::new(body)))) - }) + }).map(|_| ()) }).unwrap() .run() }); diff --git a/tests/test_ws.rs b/tests/test_ws.rs index 91e212efd..f475cd22d 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -51,7 +51,7 @@ fn test_simple() { .and_then(TakeItem::new().map_err(|_| ())) .and_then(|(req, framed): (_, Framed<_, _>)| { // validate request - if let Some(h1::InMessage::Message { req, payload: _ }) = req { + if let Some(h1::InMessage::Message(req, _)) = req { match ws::handshake(&req) { Err(e) => { // validation failed From 3c402a55da704ac6605d26500877bedab8565bf7 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 15 Oct 2018 15:56:47 -0700 Subject: [PATCH 044/427] added H1SimpleService --- src/h1/codec.rs | 12 ++++++ src/h1/mod.rs | 2 +- src/h1/service.rs | 99 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 110 insertions(+), 3 deletions(-) diff --git a/src/h1/codec.rs b/src/h1/codec.rs index a91f5cb34..020466482 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -103,6 +103,17 @@ impl Codec { self.flags.contains(Flags::KEEPALIVE) } + /// Check last request's message type + pub fn message_type(&self) -> InMessageType { + if self.flags.contains(Flags::UNHANDLED) { + InMessageType::Unhandled + } else if self.payload.is_none() { + InMessageType::None + } else { + InMessageType::Payload + } + } + /// prepare transfer encoding pub fn prepare_te(&mut self, res: &mut Response) { self.te @@ -275,6 +286,7 @@ impl Decoder for Codec { } RequestPayloadType::Unhandled => { self.payload = None; + self.flags.insert(Flags::UNHANDLED); InMessageType::Unhandled } }; diff --git a/src/h1/mod.rs b/src/h1/mod.rs index 4e196ad54..2a276b7a0 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -10,7 +10,7 @@ mod service; pub use self::codec::{Codec, InMessage, InMessageType, OutMessage}; pub use self::decoder::{PayloadDecoder, RequestDecoder}; pub use self::dispatcher::Dispatcher; -pub use self::service::{H1Service, H1ServiceHandler}; +pub use self::service::{H1Service, H1ServiceHandler, H1SimpleService}; use request::Request; diff --git a/src/h1/service.rs b/src/h1/service.rs index 6e9d7d651..bf92e8d2f 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -2,15 +2,18 @@ use std::fmt::Debug; use std::marker::PhantomData; use std::net; +use actix_net::codec::Framed; use actix_net::service::{IntoNewService, NewService, Service}; -use futures::{Async, Future, Poll}; +use futures::future::{ok, FutureResult}; +use futures::{Async, Future, Poll, Stream}; use tokio_io::{AsyncRead, AsyncWrite}; use config::{KeepAlive, ServiceConfig}; -use error::DispatchError; +use error::{DispatchError, ParseError}; use request::Request; use response::Response; +use super::codec::{Codec, InMessage}; use super::dispatcher::Dispatcher; use super::H1ServiceResult; @@ -191,6 +194,7 @@ where } } +#[doc(hidden)] pub struct H1ServiceResponse { fut: S::Future, cfg: Option, @@ -256,3 +260,94 @@ where Dispatcher::new(req, self.cfg.clone(), self.srv.clone()) } } + +/// `NewService` implementation for `H1SimpleServiceHandler` service +pub struct H1SimpleService { + config: ServiceConfig, + _t: PhantomData, +} + +impl H1SimpleService { + /// Create new `H1SimpleService` instance. + pub fn new() -> Self { + H1SimpleService { + config: ServiceConfig::default(), + _t: PhantomData, + } + } +} + +impl NewService for H1SimpleService +where + T: AsyncRead + AsyncWrite, +{ + type Request = T; + type Response = (Request, Framed); + type Error = ParseError; + type InitError = (); + type Service = H1SimpleServiceHandler; + type Future = FutureResult; + + fn new_service(&self) -> Self::Future { + ok(H1SimpleServiceHandler { + config: self.config.clone(), + _t: PhantomData, + }) + } +} + +/// `Service` implementation for HTTP1 transport. Reads one request and returns +/// request and framed object. +pub struct H1SimpleServiceHandler { + config: ServiceConfig, + _t: PhantomData, +} + +impl Service for H1SimpleServiceHandler +where + T: AsyncRead + AsyncWrite, +{ + type Request = T; + type Response = (Request, Framed); + type Error = ParseError; + type Future = H1SimpleServiceHandlerResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: Self::Request) -> Self::Future { + H1SimpleServiceHandlerResponse { + framed: Some(Framed::new(req, Codec::new(self.config.clone()))), + } + } +} + +#[doc(hidden)] +pub struct H1SimpleServiceHandlerResponse +where + T: AsyncRead + AsyncWrite, +{ + framed: Option>, +} + +impl Future for H1SimpleServiceHandlerResponse +where + T: AsyncRead + AsyncWrite, +{ + type Item = (Request, Framed); + type Error = ParseError; + + fn poll(&mut self) -> Poll { + match self.framed.as_mut().unwrap().poll()? { + Async::Ready(Some(req)) => match req { + InMessage::Message(req, _) => { + Ok(Async::Ready((req, self.framed.take().unwrap()))) + } + InMessage::Chunk(_) => unreachable!("Something is wrong"), + }, + Async::Ready(None) => Err(ParseError::Incomplete), + Async::NotReady => Ok(Async::NotReady), + } + } +} From 20c693b39c0e80436cc6c1d0e3641a226f8c8fe1 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 15 Oct 2018 16:46:13 -0700 Subject: [PATCH 045/427] rename service --- src/h1/mod.rs | 2 +- src/h1/service.rs | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/h1/mod.rs b/src/h1/mod.rs index 2a276b7a0..1ac65912e 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -10,7 +10,7 @@ mod service; pub use self::codec::{Codec, InMessage, InMessageType, OutMessage}; pub use self::decoder::{PayloadDecoder, RequestDecoder}; pub use self::dispatcher::Dispatcher; -pub use self::service::{H1Service, H1ServiceHandler, H1SimpleService}; +pub use self::service::{H1Service, H1ServiceHandler, OneRequest}; use request::Request; diff --git a/src/h1/service.rs b/src/h1/service.rs index bf92e8d2f..404ded6b0 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -261,23 +261,23 @@ where } } -/// `NewService` implementation for `H1SimpleServiceHandler` service -pub struct H1SimpleService { +/// `NewService` implementation for `OneRequestService` service +pub struct OneRequest { config: ServiceConfig, _t: PhantomData, } -impl H1SimpleService { +impl OneRequest { /// Create new `H1SimpleService` instance. pub fn new() -> Self { - H1SimpleService { + OneRequest { config: ServiceConfig::default(), _t: PhantomData, } } } -impl NewService for H1SimpleService +impl NewService for OneRequest where T: AsyncRead + AsyncWrite, { @@ -285,11 +285,11 @@ where type Response = (Request, Framed); type Error = ParseError; type InitError = (); - type Service = H1SimpleServiceHandler; + type Service = OneRequestService; type Future = FutureResult; fn new_service(&self) -> Self::Future { - ok(H1SimpleServiceHandler { + ok(OneRequestService { config: self.config.clone(), _t: PhantomData, }) @@ -298,40 +298,40 @@ where /// `Service` implementation for HTTP1 transport. Reads one request and returns /// request and framed object. -pub struct H1SimpleServiceHandler { +pub struct OneRequestService { config: ServiceConfig, _t: PhantomData, } -impl Service for H1SimpleServiceHandler +impl Service for OneRequestService where T: AsyncRead + AsyncWrite, { type Request = T; type Response = (Request, Framed); type Error = ParseError; - type Future = H1SimpleServiceHandlerResponse; + type Future = OneRequestServiceResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } fn call(&mut self, req: Self::Request) -> Self::Future { - H1SimpleServiceHandlerResponse { + OneRequestServiceResponse { framed: Some(Framed::new(req, Codec::new(self.config.clone()))), } } } #[doc(hidden)] -pub struct H1SimpleServiceHandlerResponse +pub struct OneRequestServiceResponse where T: AsyncRead + AsyncWrite, { framed: Option>, } -impl Future for H1SimpleServiceHandlerResponse +impl Future for OneRequestServiceResponse where T: AsyncRead + AsyncWrite, { From 9b94eaa6a8017cf203fef999c75366b56554216f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 22 Oct 2018 09:59:20 -0700 Subject: [PATCH 046/427] ws services --- src/error.rs | 7 ++ src/h1/service.rs | 5 +- src/lib.rs | 2 + src/service.rs | 185 ++++++++++++++++++++++++++++++++++++++++++++++ src/ws/mod.rs | 44 +++++++---- src/ws/service.rs | 52 +++++++++++++ tests/test_ws.rs | 9 ++- 7 files changed, 284 insertions(+), 20 deletions(-) create mode 100644 src/service.rs create mode 100644 src/ws/service.rs diff --git a/src/error.rs b/src/error.rs index 465b8ae0a..3c0902030 100644 --- a/src/error.rs +++ b/src/error.rs @@ -632,6 +632,13 @@ where } } +/// Convert Response to a Error +impl From for Error { + fn from(res: Response) -> Error { + InternalError::from_response("", res).into() + } +} + /// Helper function that creates wrapper of any error and generate *BAD /// REQUEST* response. #[allow(non_snake_case)] diff --git a/src/h1/service.rs b/src/h1/service.rs index 404ded6b0..096cb3016 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -267,7 +267,10 @@ pub struct OneRequest { _t: PhantomData, } -impl OneRequest { +impl OneRequest +where + T: AsyncRead + AsyncWrite, +{ /// Create new `H1SimpleService` instance. pub fn new() -> Self { OneRequest { diff --git a/src/lib.rs b/src/lib.rs index 5ce3ec39f..c9cebb47d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,6 +110,7 @@ mod json; mod payload; mod request; mod response; +mod service; pub mod uri; pub mod error; @@ -123,6 +124,7 @@ pub use extensions::Extensions; pub use httpmessage::HttpMessage; pub use request::Request; pub use response::Response; +pub use service::{SendError, SendResponse}; pub use self::config::{KeepAlive, ServiceConfig, ServiceConfigBuilder}; diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 000000000..4467087bc --- /dev/null +++ b/src/service.rs @@ -0,0 +1,185 @@ +use std::io; +use std::marker::PhantomData; + +use actix_net::codec::Framed; +use actix_net::service::{NewService, Service}; +use futures::future::{ok, Either, FutureResult}; +use futures::{Async, AsyncSink, Future, Poll, Sink}; +use tokio_io::AsyncWrite; + +use error::ResponseError; +use h1::{Codec, OutMessage}; +use response::Response; + +pub struct SendError(PhantomData<(T, R, E)>); + +impl Default for SendError +where + T: AsyncWrite, + E: ResponseError, +{ + fn default() -> Self { + SendError(PhantomData) + } +} + +impl NewService for SendError +where + T: AsyncWrite, + E: ResponseError, +{ + type Request = Result)>; + type Response = R; + type Error = (E, Framed); + type InitError = (); + type Service = SendError; + type Future = FutureResult; + + fn new_service(&self) -> Self::Future { + ok(SendError(PhantomData)) + } +} + +impl Service for SendError +where + T: AsyncWrite, + E: ResponseError, +{ + type Request = Result)>; + type Response = R; + type Error = (E, Framed); + type Future = Either)>, SendErrorFut>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: Self::Request) -> Self::Future { + match req { + Ok(r) => Either::A(ok(r)), + Err((e, framed)) => Either::B(SendErrorFut { + framed: Some(framed), + res: Some(OutMessage::Response(e.error_response())), + err: Some(e), + _t: PhantomData, + }), + } + } +} + +pub struct SendErrorFut { + res: Option, + framed: Option>, + err: Option, + _t: PhantomData, +} + +impl Future for SendErrorFut +where + E: ResponseError, + T: AsyncWrite, +{ + type Item = R; + type Error = (E, Framed); + + fn poll(&mut self) -> Poll { + if let Some(res) = self.res.take() { + match self.framed.as_mut().unwrap().start_send(res) { + Ok(AsyncSink::Ready) => (), + Ok(AsyncSink::NotReady(res)) => { + self.res = Some(res); + return Ok(Async::NotReady); + } + Err(_) => { + return Err((self.err.take().unwrap(), self.framed.take().unwrap())) + } + } + } + match self.framed.as_mut().unwrap().poll_complete() { + Ok(Async::Ready(_)) => { + return Err((self.err.take().unwrap(), self.framed.take().unwrap())) + } + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(_) => { + return Err((self.err.take().unwrap(), self.framed.take().unwrap())) + } + } + } +} + +pub struct SendResponse(PhantomData<(T,)>); + +impl Default for SendResponse +where + T: AsyncWrite, +{ + fn default() -> Self { + SendResponse(PhantomData) + } +} + +impl NewService for SendResponse +where + T: AsyncWrite, +{ + type Request = (Response, Framed); + type Response = Framed; + type Error = io::Error; + type InitError = (); + type Service = SendResponse; + type Future = FutureResult; + + fn new_service(&self) -> Self::Future { + ok(SendResponse(PhantomData)) + } +} + +impl Service for SendResponse +where + T: AsyncWrite, +{ + type Request = (Response, Framed); + type Response = Framed; + type Error = io::Error; + type Future = SendResponseFut; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, (res, framed): Self::Request) -> Self::Future { + SendResponseFut { + res: Some(OutMessage::Response(res)), + framed: Some(framed), + } + } +} + +pub struct SendResponseFut { + res: Option, + framed: Option>, +} + +impl Future for SendResponseFut +where + T: AsyncWrite, +{ + type Item = Framed; + type Error = io::Error; + + fn poll(&mut self) -> Poll { + if let Some(res) = self.res.take() { + match self.framed.as_mut().unwrap().start_send(res)? { + AsyncSink::Ready => (), + AsyncSink::NotReady(res) => { + self.res = Some(res); + return Ok(Async::NotReady); + } + } + } + match self.framed.as_mut().unwrap().poll_complete()? { + Async::Ready(_) => Ok(Async::Ready(self.framed.take().unwrap())), + Async::NotReady => Ok(Async::NotReady), + } + } +} diff --git a/src/ws/mod.rs b/src/ws/mod.rs index 7df1f4b4d..690c56feb 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -14,11 +14,13 @@ mod codec; mod frame; mod mask; mod proto; +mod service; mod transport; pub use self::codec::{Codec, Frame, Message}; pub use self::frame::Parser; pub use self::proto::{CloseCode, CloseReason, OpCode}; +pub use self::service::VerifyWebSockets; pub use self::transport::Transport; /// Websocket protocol errors @@ -109,15 +111,20 @@ impl ResponseError for HandshakeError { } } -/// Prepare `WebSocket` handshake response. -/// -/// This function returns handshake `Response`, ready to send to peer. -/// It does not perform any IO. -/// +/// Verify `WebSocket` handshake request and create handshake reponse. // /// `protocols` is a sequence of known protocols. On successful handshake, // /// the returned response headers contain the first protocol in this list // /// which the server also knows. pub fn handshake(req: &Request) -> Result { + verify_handshake(req)?; + Ok(handshake_response(req)) +} + +/// Verify `WebSocket` handshake request. +// /// `protocols` is a sequence of known protocols. On successful handshake, +// /// the returned response headers contain the first protocol in this list +// /// which the server also knows. +pub fn verify_handshake(req: &Request) -> Result<(), HandshakeError> { // WebSocket accepts only GET if *req.method() != Method::GET { return Err(HandshakeError::GetMethodRequired); @@ -161,17 +168,24 @@ pub fn handshake(req: &Request) -> Result { if !req.headers().contains_key(header::SEC_WEBSOCKET_KEY) { return Err(HandshakeError::BadWebsocketKey); } + Ok(()) +} + +/// Create websocket's handshake response +/// +/// This function returns handshake `Response`, ready to send to peer. +pub fn handshake_response(req: &Request) -> ResponseBuilder { let key = { let key = req.headers().get(header::SEC_WEBSOCKET_KEY).unwrap(); proto::hash_key(key.as_ref()) }; - Ok(Response::build(StatusCode::SWITCHING_PROTOCOLS) + Response::build(StatusCode::SWITCHING_PROTOCOLS) .connection_type(ConnectionType::Upgrade) .header(header::UPGRADE, "websocket") .header(header::TRANSFER_ENCODING, "chunked") .header(header::SEC_WEBSOCKET_ACCEPT, key.as_str()) - .take()) + .take() } #[cfg(test)] @@ -185,13 +199,13 @@ mod tests { let req = TestRequest::default().method(Method::POST).finish(); assert_eq!( HandshakeError::GetMethodRequired, - handshake(&req).err().unwrap() + verify_handshake(&req).err().unwrap() ); let req = TestRequest::default().finish(); assert_eq!( HandshakeError::NoWebsocketUpgrade, - handshake(&req).err().unwrap() + verify_handshake(&req).err().unwrap() ); let req = TestRequest::default() @@ -199,7 +213,7 @@ mod tests { .finish(); assert_eq!( HandshakeError::NoWebsocketUpgrade, - handshake(&req).err().unwrap() + verify_handshake(&req).err().unwrap() ); let req = TestRequest::default() @@ -209,7 +223,7 @@ mod tests { ).finish(); assert_eq!( HandshakeError::NoConnectionUpgrade, - handshake(&req).err().unwrap() + verify_handshake(&req).err().unwrap() ); let req = TestRequest::default() @@ -222,7 +236,7 @@ mod tests { ).finish(); assert_eq!( HandshakeError::NoVersionHeader, - handshake(&req).err().unwrap() + verify_handshake(&req).err().unwrap() ); let req = TestRequest::default() @@ -238,7 +252,7 @@ mod tests { ).finish(); assert_eq!( HandshakeError::UnsupportedVersion, - handshake(&req).err().unwrap() + verify_handshake(&req).err().unwrap() ); let req = TestRequest::default() @@ -254,7 +268,7 @@ mod tests { ).finish(); assert_eq!( HandshakeError::BadWebsocketKey, - handshake(&req).err().unwrap() + verify_handshake(&req).err().unwrap() ); let req = TestRequest::default() @@ -273,7 +287,7 @@ mod tests { ).finish(); assert_eq!( StatusCode::SWITCHING_PROTOCOLS, - handshake(&req).unwrap().finish().status() + handshake_response(&req).finish().status() ); } diff --git a/src/ws/service.rs b/src/ws/service.rs new file mode 100644 index 000000000..9cce4d639 --- /dev/null +++ b/src/ws/service.rs @@ -0,0 +1,52 @@ +use std::marker::PhantomData; + +use actix_net::codec::Framed; +use actix_net::service::{NewService, Service}; +use futures::future::{ok, FutureResult}; +use futures::{Async, IntoFuture, Poll}; + +use h1::Codec; +use request::Request; + +use super::{verify_handshake, HandshakeError}; + +pub struct VerifyWebSockets { + _t: PhantomData, +} + +impl Default for VerifyWebSockets { + fn default() -> Self { + VerifyWebSockets { _t: PhantomData } + } +} + +impl NewService for VerifyWebSockets { + type Request = (Request, Framed); + type Response = (Request, Framed); + type Error = (HandshakeError, Framed); + type InitError = (); + type Service = VerifyWebSockets; + type Future = FutureResult; + + fn new_service(&self) -> Self::Future { + ok(VerifyWebSockets { _t: PhantomData }) + } +} + +impl Service for VerifyWebSockets { + type Request = (Request, Framed); + type Response = (Request, Framed); + type Error = (HandshakeError, Framed); + type Future = FutureResult; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, (req, framed): Self::Request) -> Self::Future { + match verify_handshake(&req) { + Err(e) => Err((e, framed)).into_future(), + Ok(_) => Ok((req, framed)).into_future(), + } + } +} diff --git a/tests/test_ws.rs b/tests/test_ws.rs index f475cd22d..a5503c209 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -52,7 +52,7 @@ fn test_simple() { .and_then(|(req, framed): (_, Framed<_, _>)| { // validate request if let Some(h1::InMessage::Message(req, _)) = req { - match ws::handshake(&req) { + match ws::verify_handshake(&req) { Err(e) => { // validation failed let resp = e.error_response(); @@ -63,11 +63,12 @@ fn test_simple() { .map(|_| ()), ) } - Ok(mut resp) => Either::B( + Ok(_) => Either::B( // send response framed - .send(h1::OutMessage::Response(resp.finish())) - .map_err(|_| ()) + .send(h1::OutMessage::Response( + ws::handshake_response(&req).finish(), + )).map_err(|_| ()) .and_then(|framed| { // start websocket service let framed = From 09c94cb06be623d21aead3173707268b185ab78b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 22 Oct 2018 18:18:05 -0700 Subject: [PATCH 047/427] add client http codec; websockets client --- Cargo.toml | 3 + src/client/mod.rs | 6 + src/client/request.rs | 564 +++++++++++++++++++++++++++++++++++++++ src/client/response.rs | 128 +++++++++ src/h1/client.rs | 217 +++++++++++++++ src/h1/codec.rs | 76 ++---- src/h1/decoder.rs | 194 ++++++++++++-- src/h1/dispatcher.rs | 71 ++--- src/h1/encoder.rs | 34 +++ src/h1/mod.rs | 28 +- src/h1/service.rs | 8 +- src/lib.rs | 3 + src/request.rs | 53 ++-- src/service.rs | 10 +- src/ws/client/connect.rs | 95 +++++++ src/ws/client/error.rs | 87 ++++++ src/ws/client/mod.rs | 48 ++++ src/ws/client/service.rs | 270 +++++++++++++++++++ src/ws/mod.rs | 2 + tests/test_ws.rs | 53 +++- 20 files changed, 1802 insertions(+), 148 deletions(-) create mode 100644 src/client/mod.rs create mode 100644 src/client/request.rs create mode 100644 src/client/response.rs create mode 100644 src/h1/client.rs create mode 100644 src/ws/client/connect.rs create mode 100644 src/ws/client/error.rs create mode 100644 src/ws/client/mod.rs create mode 100644 src/ws/client/service.rs diff --git a/Cargo.toml b/Cargo.toml index 9193aeeaa..87bd05b8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,10 @@ time = "0.1" encoding = "0.2" lazy_static = "1.0" serde_urlencoded = "0.5.3" + cookie = { version="0.11", features=["percent-encode"] } +percent-encoding = "1.0" +url = { version="1.7", features=["query_encoding"] } # io net2 = "0.2" diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 000000000..a5582fea8 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,6 @@ +//! Http client api +mod request; +mod response; + +pub use self::request::{ClientRequest, ClientRequestBuilder}; +pub use self::response::ClientResponse; diff --git a/src/client/request.rs b/src/client/request.rs new file mode 100644 index 000000000..bf82927e9 --- /dev/null +++ b/src/client/request.rs @@ -0,0 +1,564 @@ +use std::fmt; +use std::fmt::Write as FmtWrite; +use std::io::Write; + +use bytes::{BufMut, BytesMut}; +use cookie::{Cookie, CookieJar}; +use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; +use urlcrate::Url; + +use header::{self, Header, IntoHeaderValue}; +use http::{ + uri, Error as HttpError, HeaderMap, HeaderName, HeaderValue, HttpTryFrom, Method, + Uri, Version, +}; + +/// An HTTP Client Request +/// +/// ```rust +/// # extern crate actix_web; +/// # extern crate futures; +/// # extern crate tokio; +/// # use futures::Future; +/// # use std::process; +/// use actix_web::{actix, client}; +/// +/// fn main() { +/// actix::run( +/// || client::ClientRequest::get("http://www.rust-lang.org") // <- Create request builder +/// .header("User-Agent", "Actix-web") +/// .finish().unwrap() +/// .send() // <- Send http request +/// .map_err(|_| ()) +/// .and_then(|response| { // <- server http response +/// println!("Response: {:?}", response); +/// # actix::System::current().stop(); +/// Ok(()) +/// }), +/// ); +/// } +/// ``` +pub struct ClientRequest { + uri: Uri, + method: Method, + version: Version, + headers: HeaderMap, + chunked: bool, + upgrade: bool, +} + +impl Default for ClientRequest { + fn default() -> ClientRequest { + ClientRequest { + uri: Uri::default(), + method: Method::default(), + version: Version::HTTP_11, + headers: HeaderMap::with_capacity(16), + chunked: false, + upgrade: false, + } + } +} + +impl ClientRequest { + /// Create request builder for `GET` request + pub fn get>(uri: U) -> ClientRequestBuilder { + let mut builder = ClientRequest::build(); + builder.method(Method::GET).uri(uri); + builder + } + + /// Create request builder for `HEAD` request + pub fn head>(uri: U) -> ClientRequestBuilder { + let mut builder = ClientRequest::build(); + builder.method(Method::HEAD).uri(uri); + builder + } + + /// Create request builder for `POST` request + pub fn post>(uri: U) -> ClientRequestBuilder { + let mut builder = ClientRequest::build(); + builder.method(Method::POST).uri(uri); + builder + } + + /// Create request builder for `PUT` request + pub fn put>(uri: U) -> ClientRequestBuilder { + let mut builder = ClientRequest::build(); + builder.method(Method::PUT).uri(uri); + builder + } + + /// Create request builder for `DELETE` request + pub fn delete>(uri: U) -> ClientRequestBuilder { + let mut builder = ClientRequest::build(); + builder.method(Method::DELETE).uri(uri); + builder + } +} + +impl ClientRequest { + /// Create client request builder + pub fn build() -> ClientRequestBuilder { + ClientRequestBuilder { + request: Some(ClientRequest::default()), + err: None, + cookies: None, + default_headers: true, + } + } + + /// Get the request URI + #[inline] + pub fn uri(&self) -> &Uri { + &self.uri + } + + /// Set client request URI + #[inline] + pub fn set_uri(&mut self, uri: Uri) { + self.uri = uri + } + + /// Get the request method + #[inline] + pub fn method(&self) -> &Method { + &self.method + } + + /// Set HTTP `Method` for the request + #[inline] + pub fn set_method(&mut self, method: Method) { + self.method = method + } + + /// Get HTTP version for the request + #[inline] + pub fn version(&self) -> Version { + self.version + } + + /// Set http `Version` for the request + #[inline] + pub fn set_version(&mut self, version: Version) { + self.version = version + } + + /// Get the headers from the request + #[inline] + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + /// Get a mutable reference to the headers + #[inline] + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.headers + } + + /// is chunked encoding enabled + #[inline] + pub fn chunked(&self) -> bool { + self.chunked + } + + /// is upgrade request + #[inline] + pub fn upgrade(&self) -> bool { + self.upgrade + } +} + +impl fmt::Debug for ClientRequest { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!( + f, + "\nClientRequest {:?} {}:{}", + self.version, self.method, self.uri + )?; + writeln!(f, " headers:")?; + for (key, val) in self.headers.iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } +} + +/// An HTTP Client request builder +/// +/// This type can be used to construct an instance of `ClientRequest` through a +/// builder-like pattern. +pub struct ClientRequestBuilder { + request: Option, + err: Option, + cookies: Option, + default_headers: bool, +} + +impl ClientRequestBuilder { + /// Set HTTP URI of request. + #[inline] + pub fn uri>(&mut self, uri: U) -> &mut Self { + match Url::parse(uri.as_ref()) { + Ok(url) => self._uri(url.as_str()), + Err(_) => self._uri(uri.as_ref()), + } + } + + fn _uri(&mut self, url: &str) -> &mut Self { + match Uri::try_from(url) { + Ok(uri) => { + if let Some(parts) = parts(&mut self.request, &self.err) { + parts.uri = uri; + } + } + Err(e) => self.err = Some(e.into()), + } + self + } + + /// Set HTTP method of this request. + #[inline] + pub fn method(&mut self, method: Method) -> &mut Self { + if let Some(parts) = parts(&mut self.request, &self.err) { + parts.method = method; + } + self + } + + /// Set HTTP method of this request. + #[inline] + pub fn get_method(&mut self) -> &Method { + let parts = self.request.as_ref().expect("cannot reuse request builder"); + &parts.method + } + + /// Set HTTP version of this request. + /// + /// By default requests's HTTP version depends on network stream + #[inline] + pub fn version(&mut self, version: Version) -> &mut Self { + if let Some(parts) = parts(&mut self.request, &self.err) { + parts.version = version; + } + self + } + + /// Set a header. + /// + /// ```rust + /// # extern crate mime; + /// # extern crate actix_web; + /// # use actix_web::client::*; + /// # + /// use actix_web::{client, http}; + /// + /// fn main() { + /// let req = client::ClientRequest::build() + /// .set(http::header::Date::now()) + /// .set(http::header::ContentType(mime::TEXT_HTML)) + /// .finish() + /// .unwrap(); + /// } + /// ``` + #[doc(hidden)] + pub fn set(&mut self, hdr: H) -> &mut Self { + if let Some(parts) = parts(&mut self.request, &self.err) { + match hdr.try_into() { + Ok(value) => { + parts.headers.insert(H::name(), value); + } + Err(e) => self.err = Some(e.into()), + } + } + self + } + + /// Append a header. + /// + /// Header gets appended to existing header. + /// To override header use `set_header()` method. + /// + /// ```rust + /// # extern crate http; + /// # extern crate actix_web; + /// # use actix_web::client::*; + /// # + /// use http::header; + /// + /// fn main() { + /// let req = ClientRequest::build() + /// .header("X-TEST", "value") + /// .header(header::CONTENT_TYPE, "application/json") + /// .finish() + /// .unwrap(); + /// } + /// ``` + pub fn header(&mut self, key: K, value: V) -> &mut Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + if let Some(parts) = parts(&mut self.request, &self.err) { + match HeaderName::try_from(key) { + Ok(key) => match value.try_into() { + Ok(value) => { + parts.headers.append(key, value); + } + Err(e) => self.err = Some(e.into()), + }, + Err(e) => self.err = Some(e.into()), + }; + } + self + } + + /// Set a header. + pub fn set_header(&mut self, key: K, value: V) -> &mut Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + if let Some(parts) = parts(&mut self.request, &self.err) { + match HeaderName::try_from(key) { + Ok(key) => match value.try_into() { + Ok(value) => { + parts.headers.insert(key, value); + } + Err(e) => self.err = Some(e.into()), + }, + Err(e) => self.err = Some(e.into()), + }; + } + self + } + + /// Set a header only if it is not yet set. + pub fn set_header_if_none(&mut self, key: K, value: V) -> &mut Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + if let Some(parts) = parts(&mut self.request, &self.err) { + match HeaderName::try_from(key) { + Ok(key) => if !parts.headers.contains_key(&key) { + match value.try_into() { + Ok(value) => { + parts.headers.insert(key, value); + } + Err(e) => self.err = Some(e.into()), + } + }, + Err(e) => self.err = Some(e.into()), + }; + } + self + } + + /// Enable connection upgrade + #[inline] + pub fn upgrade(&mut self) -> &mut Self { + if let Some(parts) = parts(&mut self.request, &self.err) { + parts.upgrade = true; + } + self + } + + /// Set request's content type + #[inline] + pub fn content_type(&mut self, value: V) -> &mut Self + where + HeaderValue: HttpTryFrom, + { + if let Some(parts) = parts(&mut self.request, &self.err) { + match HeaderValue::try_from(value) { + Ok(value) => { + parts.headers.insert(header::CONTENT_TYPE, value); + } + Err(e) => self.err = Some(e.into()), + }; + } + self + } + + /// Set content length + #[inline] + pub fn content_length(&mut self, len: u64) -> &mut Self { + let mut wrt = BytesMut::new().writer(); + let _ = write!(wrt, "{}", len); + self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze()) + } + + /// Set a cookie + /// + /// ```rust + /// # extern crate actix_web; + /// use actix_web::{client, http}; + /// + /// fn main() { + /// let req = client::ClientRequest::build() + /// .cookie( + /// http::Cookie::build("name", "value") + /// .domain("www.rust-lang.org") + /// .path("/") + /// .secure(true) + /// .http_only(true) + /// .finish(), + /// ) + /// .finish() + /// .unwrap(); + /// } + /// ``` + pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { + if self.cookies.is_none() { + let mut jar = CookieJar::new(); + jar.add(cookie.into_owned()); + self.cookies = Some(jar) + } else { + self.cookies.as_mut().unwrap().add(cookie.into_owned()); + } + self + } + + /// Do not add default request headers. + /// By default `Accept-Encoding` and `User-Agent` headers are set. + pub fn no_default_headers(&mut self) -> &mut Self { + self.default_headers = false; + self + } + + /// This method calls provided closure with builder reference if + /// value is `true`. + pub fn if_true(&mut self, value: bool, f: F) -> &mut Self + where + F: FnOnce(&mut ClientRequestBuilder), + { + if value { + f(self); + } + self + } + + /// This method calls provided closure with builder reference if + /// value is `Some`. + pub fn if_some(&mut self, value: Option, f: F) -> &mut Self + where + F: FnOnce(T, &mut ClientRequestBuilder), + { + if let Some(val) = value { + f(val, self); + } + self + } + + /// Set a body and generate `ClientRequest`. + /// + /// `ClientRequestBuilder` can not be used after this call. + pub fn finish(&mut self) -> Result { + if let Some(e) = self.err.take() { + return Err(e); + } + + if self.default_headers { + // enable br only for https + let https = if let Some(parts) = parts(&mut self.request, &self.err) { + parts + .uri + .scheme_part() + .map(|s| s == &uri::Scheme::HTTPS) + .unwrap_or(true) + } else { + true + }; + + if https { + self.set_header_if_none(header::ACCEPT_ENCODING, "br, gzip, deflate"); + } else { + self.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate"); + } + + // set request host header + if let Some(parts) = parts(&mut self.request, &self.err) { + if let Some(host) = parts.uri.host() { + if !parts.headers.contains_key(header::HOST) { + let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); + + let _ = match parts.uri.port() { + None | Some(80) | Some(443) => write!(wrt, "{}", host), + Some(port) => write!(wrt, "{}:{}", host, port), + }; + + match wrt.get_mut().take().freeze().try_into() { + Ok(value) => { + parts.headers.insert(header::HOST, value); + } + Err(e) => self.err = Some(e.into()), + } + } + } + } + + // user agent + self.set_header_if_none( + header::USER_AGENT, + concat!("actix-http/", env!("CARGO_PKG_VERSION")), + ); + } + + let mut request = self.request.take().expect("cannot reuse request builder"); + + // set cookies + if let Some(ref mut jar) = self.cookies { + let mut cookie = String::new(); + for c in jar.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); + } + request.headers.insert( + header::COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); + } + Ok(request) + } + + /// This method construct new `ClientRequestBuilder` + pub fn take(&mut self) -> ClientRequestBuilder { + ClientRequestBuilder { + request: self.request.take(), + err: self.err.take(), + cookies: self.cookies.take(), + default_headers: self.default_headers, + } + } +} + +#[inline] +fn parts<'a>( + parts: &'a mut Option, err: &Option, +) -> Option<&'a mut ClientRequest> { + if err.is_some() { + return None; + } + parts.as_mut() +} + +impl fmt::Debug for ClientRequestBuilder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(ref parts) = self.request { + writeln!( + f, + "\nClientRequestBuilder {:?} {}:{}", + parts.version, parts.method, parts.uri + )?; + writeln!(f, " headers:")?; + for (key, val) in parts.headers.iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } else { + write!(f, "ClientRequestBuilder(Consumed)") + } + } +} diff --git a/src/client/response.rs b/src/client/response.rs new file mode 100644 index 000000000..627a1c78e --- /dev/null +++ b/src/client/response.rs @@ -0,0 +1,128 @@ +use std::cell::{Cell, Ref, RefCell, RefMut}; +use std::fmt; +use std::rc::Rc; + +use http::{HeaderMap, Method, StatusCode, Version}; + +use extensions::Extensions; +use httpmessage::HttpMessage; +use payload::Payload; +use request::{Message, MessageFlags, MessagePool}; +use uri::Url; + +/// Client Response +pub struct ClientResponse { + pub(crate) inner: Rc, +} + +impl HttpMessage for ClientResponse { + type Stream = Payload; + + fn headers(&self) -> &HeaderMap { + &self.inner.headers + } + + #[inline] + fn payload(&self) -> Payload { + if let Some(payload) = self.inner.payload.borrow_mut().take() { + payload + } else { + Payload::empty() + } + } +} + +impl ClientResponse { + /// Create new Request instance + pub fn new() -> ClientResponse { + ClientResponse::with_pool(MessagePool::pool()) + } + + /// Create new Request instance with pool + pub(crate) fn with_pool(pool: &'static MessagePool) -> ClientResponse { + ClientResponse { + inner: Rc::new(Message { + pool, + method: Method::GET, + status: StatusCode::OK, + url: Url::default(), + version: Version::HTTP_11, + headers: HeaderMap::with_capacity(16), + flags: Cell::new(MessageFlags::empty()), + payload: RefCell::new(None), + extensions: RefCell::new(Extensions::new()), + }), + } + } + + #[inline] + pub(crate) fn inner(&self) -> &Message { + self.inner.as_ref() + } + + #[inline] + pub(crate) fn inner_mut(&mut self) -> &mut Message { + Rc::get_mut(&mut self.inner).expect("Multiple copies exist") + } + + /// Read the Request Version. + #[inline] + pub fn version(&self) -> Version { + self.inner().version + } + + /// Get the status from the server. + #[inline] + pub fn status(&self) -> StatusCode { + self.inner().status + } + + #[inline] + /// Returns Request's headers. + pub fn headers(&self) -> &HeaderMap { + &self.inner().headers + } + + #[inline] + /// Returns mutable Request's headers. + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.inner_mut().headers + } + + /// Checks if a connection should be kept alive. + #[inline] + pub fn keep_alive(&self) -> bool { + self.inner().flags.get().contains(MessageFlags::KEEPALIVE) + } + + /// Request extensions + #[inline] + pub fn extensions(&self) -> Ref { + self.inner().extensions.borrow() + } + + /// Mutable reference to a the request's extensions + #[inline] + pub fn extensions_mut(&self) -> RefMut { + self.inner().extensions.borrow_mut() + } +} + +impl Drop for ClientResponse { + fn drop(&mut self) { + if Rc::strong_count(&self.inner) == 1 { + self.inner.pool.release(self.inner.clone()); + } + } +} + +impl fmt::Debug for ClientResponse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status(),)?; + writeln!(f, " headers:")?; + for (key, val) in self.headers().iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } +} diff --git a/src/h1/client.rs b/src/h1/client.rs new file mode 100644 index 000000000..8ec1f0aea --- /dev/null +++ b/src/h1/client.rs @@ -0,0 +1,217 @@ +#![allow(unused_imports, unused_variables, dead_code)] +use std::io::{self, Write}; + +use bytes::{BufMut, Bytes, BytesMut}; +use tokio_codec::{Decoder, Encoder}; + +use super::decoder::{PayloadDecoder, PayloadItem, PayloadType, ResponseDecoder}; +use super::encoder::{RequestEncoder, ResponseLength}; +use super::{Message, MessageType}; +use body::{Binary, Body}; +use client::{ClientRequest, ClientResponse}; +use config::ServiceConfig; +use error::ParseError; +use helpers; +use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; +use http::{Method, Version}; +use request::MessagePool; + +bitflags! { + struct Flags: u8 { + const HEAD = 0b0000_0001; + const UPGRADE = 0b0000_0010; + const KEEPALIVE = 0b0000_0100; + const KEEPALIVE_ENABLED = 0b0000_1000; + const UNHANDLED = 0b0001_0000; + } +} + +const AVERAGE_HEADER_SIZE: usize = 30; + +/// HTTP/1 Codec +pub struct ClientCodec { + config: ServiceConfig, + decoder: ResponseDecoder, + payload: Option, + version: Version, + + // encoder part + flags: Flags, + headers_size: u32, + te: RequestEncoder, +} + +impl Default for ClientCodec { + fn default() -> Self { + ClientCodec::new(ServiceConfig::default()) + } +} + +impl ClientCodec { + /// Create HTTP/1 codec. + /// + /// `keepalive_enabled` how response `connection` header get generated. + pub fn new(config: ServiceConfig) -> Self { + ClientCodec::with_pool(MessagePool::pool(), config) + } + + /// Create HTTP/1 codec with request's pool + pub(crate) fn with_pool(pool: &'static MessagePool, config: ServiceConfig) -> Self { + let flags = if config.keep_alive_enabled() { + Flags::KEEPALIVE_ENABLED + } else { + Flags::empty() + }; + ClientCodec { + config, + decoder: ResponseDecoder::with_pool(pool), + payload: None, + version: Version::HTTP_11, + + flags, + headers_size: 0, + te: RequestEncoder::default(), + } + } + + /// Check if request is upgrade + pub fn upgrade(&self) -> bool { + self.flags.contains(Flags::UPGRADE) + } + + /// Check if last response is keep-alive + pub fn keepalive(&self) -> bool { + self.flags.contains(Flags::KEEPALIVE) + } + + /// Check last request's message type + pub fn message_type(&self) -> MessageType { + if self.flags.contains(Flags::UNHANDLED) { + MessageType::Unhandled + } else if self.payload.is_none() { + MessageType::None + } else { + MessageType::Payload + } + } + + /// prepare transfer encoding + pub fn prepare_te(&mut self, res: &mut ClientRequest) { + self.te + .update(res, self.flags.contains(Flags::HEAD), self.version); + } + + fn encode_response( + &mut self, msg: ClientRequest, buffer: &mut BytesMut, + ) -> io::Result<()> { + // Connection upgrade + if msg.upgrade() { + self.flags.insert(Flags::UPGRADE); + } + + // render message + { + // status line + writeln!( + Writer(buffer), + "{} {} {:?}\r", + msg.method(), + msg.uri() + .path_and_query() + .map(|u| u.as_str()) + .unwrap_or("/"), + msg.version() + ).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + // write headers + buffer.reserve(msg.headers().len() * AVERAGE_HEADER_SIZE); + for (key, value) in msg.headers() { + let v = value.as_ref(); + let k = key.as_str().as_bytes(); + buffer.reserve(k.len() + v.len() + 4); + buffer.put_slice(k); + buffer.put_slice(b": "); + buffer.put_slice(v); + buffer.put_slice(b"\r\n"); + } + + // set date header + if !msg.headers().contains_key(DATE) { + self.config.set_date(buffer); + } else { + buffer.extend_from_slice(b"\r\n"); + } + } + + Ok(()) + } +} + +impl Decoder for ClientCodec { + type Item = Message; + type Error = ParseError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + if self.payload.is_some() { + Ok(match self.payload.as_mut().unwrap().decode(src)? { + Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))), + Some(PayloadItem::Eof) => Some(Message::Chunk(None)), + None => None, + }) + } else if self.flags.contains(Flags::UNHANDLED) { + Ok(None) + } else if let Some((req, payload)) = self.decoder.decode(src)? { + self.flags + .set(Flags::HEAD, req.inner.method == Method::HEAD); + self.version = req.inner.version; + if self.flags.contains(Flags::KEEPALIVE_ENABLED) { + self.flags.set(Flags::KEEPALIVE, req.keep_alive()); + } + match payload { + PayloadType::None => self.payload = None, + PayloadType::Payload(pl) => self.payload = Some(pl), + PayloadType::Unhandled => { + self.payload = None; + self.flags.insert(Flags::UNHANDLED); + } + }; + Ok(Some(Message::Item(req))) + } else { + Ok(None) + } + } +} + +impl Encoder for ClientCodec { + type Item = Message; + type Error = io::Error; + + fn encode( + &mut self, item: Self::Item, dst: &mut BytesMut, + ) -> Result<(), Self::Error> { + match item { + Message::Item(res) => { + self.encode_response(res, dst)?; + } + Message::Chunk(Some(bytes)) => { + self.te.encode(bytes.as_ref(), dst)?; + } + Message::Chunk(None) => { + self.te.encode_eof(dst)?; + } + } + Ok(()) + } +} + +pub struct Writer<'a>(pub &'a mut BytesMut); + +impl<'a> io::Write for Writer<'a> { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.extend_from_slice(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 020466482..8f7e77e05 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -4,15 +4,16 @@ use std::io::{self, Write}; use bytes::{BufMut, Bytes, BytesMut}; use tokio_codec::{Decoder, Encoder}; -use super::decoder::{PayloadDecoder, PayloadItem, RequestDecoder, RequestPayloadType}; +use super::decoder::{PayloadDecoder, PayloadItem, PayloadType, RequestDecoder}; use super::encoder::{ResponseEncoder, ResponseLength}; +use super::{Message, MessageType}; use body::{Binary, Body}; use config::ServiceConfig; use error::ParseError; use helpers; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::{Method, Version}; -use request::{Request, RequestPool}; +use request::{MessagePool, Request}; use response::Response; bitflags! { @@ -27,32 +28,6 @@ bitflags! { const AVERAGE_HEADER_SIZE: usize = 30; -#[derive(Debug)] -/// Http response -pub enum OutMessage { - /// Http response message - Response(Response), - /// Payload chunk - Chunk(Option), -} - -/// Incoming http/1 request -#[derive(Debug)] -pub enum InMessage { - /// Request - Message(Request, InMessageType), - /// Payload chunk - Chunk(Option), -} - -/// Incoming request type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum InMessageType { - None, - Payload, - Unhandled, -} - /// HTTP/1 Codec pub struct Codec { config: ServiceConfig, @@ -71,11 +46,11 @@ impl Codec { /// /// `keepalive_enabled` how response `connection` header get generated. pub fn new(config: ServiceConfig) -> Self { - Codec::with_pool(RequestPool::pool(), config) + Codec::with_pool(MessagePool::pool(), config) } /// Create HTTP/1 codec with request's pool - pub(crate) fn with_pool(pool: &'static RequestPool, config: ServiceConfig) -> Self { + pub(crate) fn with_pool(pool: &'static MessagePool, config: ServiceConfig) -> Self { let flags = if config.keep_alive_enabled() { Flags::KEEPALIVE_ENABLED } else { @@ -104,13 +79,13 @@ impl Codec { } /// Check last request's message type - pub fn message_type(&self) -> InMessageType { + pub fn message_type(&self) -> MessageType { if self.flags.contains(Flags::UNHANDLED) { - InMessageType::Unhandled + MessageType::Unhandled } else if self.payload.is_none() { - InMessageType::None + MessageType::None } else { - InMessageType::Payload + MessageType::Payload } } @@ -256,14 +231,14 @@ impl Codec { } impl Decoder for Codec { - type Item = InMessage; + type Item = Message; type Error = ParseError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { if self.payload.is_some() { Ok(match self.payload.as_mut().unwrap().decode(src)? { - Some(PayloadItem::Chunk(chunk)) => Some(InMessage::Chunk(Some(chunk))), - Some(PayloadItem::Eof) => Some(InMessage::Chunk(None)), + Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))), + Some(PayloadItem::Eof) => Some(Message::Chunk(None)), None => None, }) } else if self.flags.contains(Flags::UNHANDLED) { @@ -275,22 +250,15 @@ impl Decoder for Codec { if self.flags.contains(Flags::KEEPALIVE_ENABLED) { self.flags.set(Flags::KEEPALIVE, req.keep_alive()); } - let payload = match payload { - RequestPayloadType::None => { - self.payload = None; - InMessageType::None - } - RequestPayloadType::Payload(pl) => { - self.payload = Some(pl); - InMessageType::Payload - } - RequestPayloadType::Unhandled => { + match payload { + PayloadType::None => self.payload = None, + PayloadType::Payload(pl) => self.payload = Some(pl), + PayloadType::Unhandled => { self.payload = None; self.flags.insert(Flags::UNHANDLED); - InMessageType::Unhandled } - }; - Ok(Some(InMessage::Message(req, payload))) + } + Ok(Some(Message::Item(req))) } else { Ok(None) } @@ -298,20 +266,20 @@ impl Decoder for Codec { } impl Encoder for Codec { - type Item = OutMessage; + type Item = Message; type Error = io::Error; fn encode( &mut self, item: Self::Item, dst: &mut BytesMut, ) -> Result<(), Self::Error> { match item { - OutMessage::Response(res) => { + Message::Item(res) => { self.encode_response(res, dst)?; } - OutMessage::Chunk(Some(bytes)) => { + Message::Chunk(Some(bytes)) => { self.te.encode(bytes.as_ref(), dst)?; } - OutMessage::Chunk(None) => { + Message::Chunk(None) => { self.te.encode_eof(dst)?; } } diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index c2a8d0e99..2c1e6c1f2 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -5,38 +5,43 @@ use futures::{Async, Poll}; use httparse; use tokio_codec::Decoder; +use client::ClientResponse; use error::ParseError; use http::header::{HeaderName, HeaderValue}; -use http::{header, HttpTryFrom, Method, Uri, Version}; -use request::{MessageFlags, Request, RequestPool}; +use http::{header, HttpTryFrom, Method, StatusCode, Uri, Version}; +use request::{MessageFlags, MessagePool, Request}; use uri::Url; const MAX_BUFFER_SIZE: usize = 131_072; const MAX_HEADERS: usize = 96; -pub struct RequestDecoder(&'static RequestPool); +/// Client request decoder +pub struct RequestDecoder(&'static MessagePool); + +/// Server response decoder +pub struct ResponseDecoder(&'static MessagePool); /// Incoming request type -pub enum RequestPayloadType { +pub enum PayloadType { None, Payload(PayloadDecoder), Unhandled, } impl RequestDecoder { - pub(crate) fn with_pool(pool: &'static RequestPool) -> RequestDecoder { + pub(crate) fn with_pool(pool: &'static MessagePool) -> RequestDecoder { RequestDecoder(pool) } } impl Default for RequestDecoder { fn default() -> RequestDecoder { - RequestDecoder::with_pool(RequestPool::pool()) + RequestDecoder::with_pool(MessagePool::pool()) } } impl Decoder for RequestDecoder { - type Item = (Request, RequestPayloadType); + type Item = (Request, PayloadType); type Error = ParseError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { @@ -77,7 +82,7 @@ impl Decoder for RequestDecoder { let slice = src.split_to(len).freeze(); // convert headers - let mut msg = RequestPool::get(self.0); + let mut msg = MessagePool::get_request(self.0); { let inner = msg.inner_mut(); inner @@ -156,18 +161,165 @@ impl Decoder for RequestDecoder { // https://tools.ietf.org/html/rfc7230#section-3.3.3 let decoder = if chunked { // Chunked encoding - RequestPayloadType::Payload(PayloadDecoder::chunked()) + PayloadType::Payload(PayloadDecoder::chunked()) } else if let Some(len) = content_length { // Content-Length - RequestPayloadType::Payload(PayloadDecoder::length(len)) + PayloadType::Payload(PayloadDecoder::length(len)) } else if has_upgrade || msg.inner.method == Method::CONNECT { // upgrade(websocket) or connect - RequestPayloadType::Unhandled + PayloadType::Unhandled } else if src.len() >= MAX_BUFFER_SIZE { error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); return Err(ParseError::TooLarge); } else { - RequestPayloadType::None + PayloadType::None + }; + + Ok(Some((msg, decoder))) + } +} + +impl ResponseDecoder { + pub(crate) fn with_pool(pool: &'static MessagePool) -> ResponseDecoder { + ResponseDecoder(pool) + } +} + +impl Default for ResponseDecoder { + fn default() -> ResponseDecoder { + ResponseDecoder::with_pool(MessagePool::pool()) + } +} + +impl Decoder for ResponseDecoder { + type Item = (ClientResponse, PayloadType); + type Error = ParseError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + // Parse http message + let mut chunked = false; + let mut content_length = None; + + let msg = { + // Unsafe: we read only this data only after httparse parses headers into. + // performance bump for pipeline benchmarks. + let mut headers: [HeaderIndex; MAX_HEADERS] = + unsafe { mem::uninitialized() }; + + let (len, version, status, headers_len) = { + let mut parsed: [httparse::Header; MAX_HEADERS] = + unsafe { mem::uninitialized() }; + + let mut res = httparse::Response::new(&mut parsed); + match res.parse(src)? { + httparse::Status::Complete(len) => { + let version = if res.version.unwrap() == 1 { + Version::HTTP_11 + } else { + Version::HTTP_10 + }; + let status = StatusCode::from_u16(res.code.unwrap()) + .map_err(|_| ParseError::Status)?; + HeaderIndex::record(src, res.headers, &mut headers); + + (len, version, status, res.headers.len()) + } + httparse::Status::Partial => return Ok(None), + } + }; + + let slice = src.split_to(len).freeze(); + + // convert headers + let mut msg = MessagePool::get_response(self.0); + { + let inner = msg.inner_mut(); + inner + .flags + .get_mut() + .set(MessageFlags::KEEPALIVE, version != Version::HTTP_10); + + for idx in headers[..headers_len].iter() { + if let Ok(name) = + HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]) + { + // Unsafe: httparse check header value for valid utf-8 + let value = unsafe { + HeaderValue::from_shared_unchecked( + slice.slice(idx.value.0, idx.value.1), + ) + }; + match name { + header::CONTENT_LENGTH => { + if let Ok(s) = value.to_str() { + if let Ok(len) = s.parse::() { + content_length = Some(len); + } else { + debug!("illegal Content-Length: {:?}", len); + return Err(ParseError::Header); + } + } else { + debug!("illegal Content-Length: {:?}", len); + return Err(ParseError::Header); + } + } + // transfer-encoding + header::TRANSFER_ENCODING => { + if let Ok(s) = value.to_str() { + chunked = s.to_lowercase().contains("chunked"); + } else { + return Err(ParseError::Header); + } + } + // connection keep-alive state + header::CONNECTION => { + let ka = if let Ok(conn) = value.to_str() { + if version == Version::HTTP_10 + && conn.contains("keep-alive") + { + true + } else { + version == Version::HTTP_11 && !(conn + .contains("close") + || conn.contains("upgrade")) + } + } else { + false + }; + inner.flags.get_mut().set(MessageFlags::KEEPALIVE, ka); + } + _ => (), + } + + inner.headers.append(name, value); + } else { + return Err(ParseError::Header); + } + } + + inner.status = status; + inner.version = version; + } + msg + }; + + // https://tools.ietf.org/html/rfc7230#section-3.3.3 + let decoder = if chunked { + // Chunked encoding + PayloadType::Payload(PayloadDecoder::chunked()) + } else if let Some(len) = content_length { + // Content-Length + PayloadType::Payload(PayloadDecoder::length(len)) + } else if msg.inner.status == StatusCode::SWITCHING_PROTOCOLS + || msg.inner.method == Method::CONNECT + { + // switching protocol or connect + PayloadType::Unhandled + } else if src.len() >= MAX_BUFFER_SIZE { + error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); + return Err(ParseError::TooLarge); + } else { + PayloadType::None }; Ok(Some((msg, decoder))) @@ -488,36 +640,30 @@ mod tests { use super::*; use error::ParseError; - use h1::{InMessage, InMessageType}; + use h1::Message; use httpmessage::HttpMessage; use request::Request; - impl RequestPayloadType { + impl PayloadType { fn unwrap(self) -> PayloadDecoder { match self { - RequestPayloadType::Payload(pl) => pl, + PayloadType::Payload(pl) => pl, _ => panic!(), } } fn is_unhandled(&self) -> bool { match self { - RequestPayloadType::Unhandled => true, + PayloadType::Unhandled => true, _ => false, } } } - impl InMessage { + impl Message { fn message(self) -> Request { match self { - InMessage::Message(req, _) => req, - _ => panic!("error"), - } - } - fn is_payload(&self) -> bool { - match *self { - InMessage::Message(_, payload) => payload == InMessageType::Payload, + Message::Item(req) => req, _ => panic!("error"), } } diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 8ae2ae8ce..a7f97e8c9 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -18,8 +18,8 @@ use error::DispatchError; use request::Request; use response::Response; -use super::codec::{Codec, InMessage, InMessageType, OutMessage}; -use super::H1ServiceResult; +use super::codec::Codec; +use super::{H1ServiceResult, Message, MessageType}; const MAX_PIPELINED_MESSAGES: usize = 16; @@ -48,14 +48,14 @@ where state: State, payload: Option, - messages: VecDeque, + messages: VecDeque, unhandled: Option, ka_expire: Instant, ka_timer: Option, } -enum Message { +enum DispatcherMessage { Item(Request), Error(Response), } @@ -63,8 +63,8 @@ enum Message { enum State { None, ServiceCall(S::Future), - SendResponse(Option<(OutMessage, Body)>), - SendPayload(Option, Option), + SendResponse(Option<(Message, Body)>), + SendPayload(Option, Option>), } impl State { @@ -176,11 +176,12 @@ where State::None => loop { break if let Some(msg) = self.messages.pop_front() { match msg { - Message::Item(req) => Some(self.handle_request(req)?), - Message::Error(res) => Some(State::SendResponse(Some(( - OutMessage::Response(res), - Body::Empty, - )))), + DispatcherMessage::Item(req) => { + Some(self.handle_request(req)?) + } + DispatcherMessage::Error(res) => Some(State::SendResponse( + Some((Message::Item(res), Body::Empty)), + )), } } else { None @@ -196,10 +197,7 @@ where .get_codec_mut() .prepare_te(&mut res); let body = res.replace_body(Body::Empty); - Some(State::SendResponse(Some(( - OutMessage::Response(res), - body, - )))) + Some(State::SendResponse(Some((Message::Item(res), body)))) } Async::NotReady => None, } @@ -216,9 +214,9 @@ where self.flags.remove(Flags::FLUSHED); match body { Body::Empty => Some(State::None), - Body::Binary(bin) => Some(State::SendPayload( + Body::Binary(mut bin) => Some(State::SendPayload( None, - Some(OutMessage::Chunk(bin.into())), + Some(Message::Chunk(Some(bin.take()))), )), Body::Streaming(stream) => { Some(State::SendPayload(Some(stream), None)) @@ -257,7 +255,7 @@ where .framed .as_mut() .unwrap() - .start_send(OutMessage::Chunk(Some(item.into()))) + .start_send(Message::Chunk(Some(item.into()))) { Ok(AsyncSink::Ready) => { self.flags.remove(Flags::FLUSHED); @@ -271,7 +269,7 @@ where }, Ok(Async::Ready(None)) => Some(State::SendPayload( None, - Some(OutMessage::Chunk(None)), + Some(Message::Chunk(None)), )), Ok(Async::NotReady) => return Ok(()), // Err(err) => return Err(DispatchError::Io(err)), @@ -312,7 +310,7 @@ where .get_codec_mut() .prepare_te(&mut res); let body = res.replace_body(Body::Empty); - Ok(State::SendResponse(Some((OutMessage::Response(res), body)))) + Ok(State::SendResponse(Some((Message::Item(res), body)))) } Async::NotReady => Ok(State::ServiceCall(task)), } @@ -333,14 +331,20 @@ where self.flags.insert(Flags::STARTED); match msg { - InMessage::Message(req, payload) => { - match payload { - InMessageType::Payload => { + Message::Item(req) => { + match self + .framed + .as_ref() + .unwrap() + .get_codec() + .message_type() + { + MessageType::Payload => { let (ps, pl) = Payload::new(false); *req.inner.payload.borrow_mut() = Some(pl); self.payload = Some(ps); } - InMessageType::Unhandled => { + MessageType::Unhandled => { self.unhandled = Some(req); return Ok(updated); } @@ -351,10 +355,10 @@ where if self.state.is_empty() { self.state = self.handle_request(req)?; } else { - self.messages.push_back(Message::Item(req)); + self.messages.push_back(DispatcherMessage::Item(req)); } } - InMessage::Chunk(Some(chunk)) => { + Message::Chunk(Some(chunk)) => { if let Some(ref mut payload) = self.payload { payload.feed_data(chunk); } else { @@ -362,19 +366,19 @@ where "Internal server error: unexpected payload chunk" ); self.flags.insert(Flags::DISCONNECTED); - self.messages.push_back(Message::Error( + self.messages.push_back(DispatcherMessage::Error( Response::InternalServerError().finish(), )); self.error = Some(DispatchError::InternalError); } } - InMessage::Chunk(None) => { + Message::Chunk(None) => { if let Some(mut payload) = self.payload.take() { payload.feed_eof(); } else { error!("Internal server error: unexpected eof"); self.flags.insert(Flags::DISCONNECTED); - self.messages.push_back(Message::Error( + self.messages.push_back(DispatcherMessage::Error( Response::InternalServerError().finish(), )); self.error = Some(DispatchError::InternalError); @@ -398,8 +402,9 @@ where } // Malformed requests should be responded with 400 - self.messages - .push_back(Message::Error(Response::BadRequest().finish())); + self.messages.push_back(DispatcherMessage::Error( + Response::BadRequest().finish(), + )); self.flags.insert(Flags::DISCONNECTED); self.error = Some(e.into()); break; @@ -443,9 +448,7 @@ where trace!("Slow request timeout"); self.flags.insert(Flags::STARTED | Flags::DISCONNECTED); self.state = State::SendResponse(Some(( - OutMessage::Response( - Response::RequestTimeout().finish(), - ), + Message::Item(Response::RequestTimeout().finish()), Body::Empty, ))); } diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index ea11f11fd..63ba05d04 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -9,6 +9,7 @@ use http::header::{HeaderValue, ACCEPT_ENCODING, CONTENT_LENGTH}; use http::{StatusCode, Version}; use body::{Binary, Body}; +use client::ClientRequest; use header::ContentEncoding; use http::Method; use request::Request; @@ -165,6 +166,39 @@ impl ResponseEncoder { } } +#[derive(Debug)] +pub(crate) struct RequestEncoder { + head: bool, + pub length: ResponseLength, + pub te: TransferEncoding, +} + +impl Default for RequestEncoder { + fn default() -> Self { + RequestEncoder { + head: false, + length: ResponseLength::None, + te: TransferEncoding::empty(), + } + } +} + +impl RequestEncoder { + /// Encode message + pub fn encode(&mut self, msg: &[u8], buf: &mut BytesMut) -> io::Result { + self.te.encode(msg, buf) + } + + /// Encode eof + pub fn encode_eof(&mut self, buf: &mut BytesMut) -> io::Result<()> { + self.te.encode_eof(buf) + } + + pub fn update(&mut self, resp: &mut ClientRequest, head: bool, version: Version) { + self.head = head; + } +} + /// Encoders to handle different Transfer-Encodings. #[derive(Debug)] pub(crate) struct TransferEncoding { diff --git a/src/h1/mod.rs b/src/h1/mod.rs index 1ac65912e..c33b193f5 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -1,13 +1,16 @@ //! HTTP/1 implementation use actix_net::codec::Framed; +use bytes::Bytes; +mod client; mod codec; mod decoder; mod dispatcher; mod encoder; mod service; -pub use self::codec::{Codec, InMessage, InMessageType, OutMessage}; +pub use self::client::ClientCodec; +pub use self::codec::Codec; pub use self::decoder::{PayloadDecoder, RequestDecoder}; pub use self::dispatcher::Dispatcher; pub use self::service::{H1Service, H1ServiceHandler, OneRequest}; @@ -20,3 +23,26 @@ pub enum H1ServiceResult { Shutdown(T), Unhandled(Request, Framed), } + +#[derive(Debug)] +/// Codec message +pub enum Message { + /// Http message + Item(T), + /// Payload chunk + Chunk(Option), +} + +impl From for Message { + fn from(item: T) -> Self { + Message::Item(item) + } +} + +/// Incoming request type +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MessageType { + None, + Payload, + Unhandled, +} diff --git a/src/h1/service.rs b/src/h1/service.rs index 096cb3016..d691ae75d 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -13,9 +13,9 @@ use error::{DispatchError, ParseError}; use request::Request; use response::Response; -use super::codec::{Codec, InMessage}; +use super::codec::Codec; use super::dispatcher::Dispatcher; -use super::H1ServiceResult; +use super::{H1ServiceResult, Message}; /// `NewService` implementation for HTTP1 transport pub struct H1Service { @@ -344,10 +344,10 @@ where fn poll(&mut self) -> Poll { match self.framed.as_mut().unwrap().poll()? { Async::Ready(Some(req)) => match req { - InMessage::Message(req, _) => { + Message::Item(req) => { Ok(Async::Ready((req, self.framed.take().unwrap()))) } - InMessage::Chunk(_) => unreachable!("Something is wrong"), + Message::Chunk(_) => unreachable!("Something is wrong"), }, Async::Ready(None) => Err(ParseError::Incomplete), Async::NotReady => Ok(Async::NotReady), diff --git a/src/lib.rs b/src/lib.rs index c9cebb47d..572c23ae1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,7 @@ extern crate http as modhttp; extern crate httparse; extern crate mime; extern crate net2; +extern crate percent_encoding; extern crate rand; extern crate serde; extern crate serde_json; @@ -95,12 +96,14 @@ extern crate tokio_current_thread; extern crate tokio_io; extern crate tokio_tcp; extern crate tokio_timer; +extern crate url as urlcrate; #[cfg(test)] #[macro_use] extern crate serde_derive; mod body; +pub mod client; mod config; mod extensions; mod header; diff --git a/src/request.rs b/src/request.rs index ef28e3694..ad0486395 100644 --- a/src/request.rs +++ b/src/request.rs @@ -3,8 +3,9 @@ use std::collections::VecDeque; use std::fmt; use std::rc::Rc; -use http::{header, HeaderMap, Method, Uri, Version}; +use http::{header, HeaderMap, Method, StatusCode, Uri, Version}; +use client::ClientResponse; use extensions::Extensions; use httpmessage::HttpMessage; use payload::Payload; @@ -17,23 +18,24 @@ bitflags! { } } -/// Request's context +/// Request pub struct Request { - pub(crate) inner: Rc, + pub(crate) inner: Rc, } -pub(crate) struct InnerRequest { +pub(crate) struct Message { pub(crate) version: Version, + pub(crate) status: StatusCode, pub(crate) method: Method, pub(crate) url: Url, pub(crate) flags: Cell, pub(crate) headers: HeaderMap, pub(crate) extensions: RefCell, pub(crate) payload: RefCell>, - pool: &'static RequestPool, + pub(crate) pool: &'static MessagePool, } -impl InnerRequest { +impl Message { #[inline] /// Reset request instance pub fn reset(&mut self) { @@ -64,15 +66,16 @@ impl HttpMessage for Request { impl Request { /// Create new Request instance pub fn new() -> Request { - Request::with_pool(RequestPool::pool()) + Request::with_pool(MessagePool::pool()) } /// Create new Request instance with pool - pub(crate) fn with_pool(pool: &'static RequestPool) -> Request { + pub(crate) fn with_pool(pool: &'static MessagePool) -> Request { Request { - inner: Rc::new(InnerRequest { + inner: Rc::new(Message { pool, method: Method::GET, + status: StatusCode::OK, url: Url::default(), version: Version::HTTP_11, headers: HeaderMap::with_capacity(16), @@ -84,12 +87,12 @@ impl Request { } #[inline] - pub(crate) fn inner(&self) -> &InnerRequest { + pub(crate) fn inner(&self) -> &Message { self.inner.as_ref() } #[inline] - pub(crate) fn inner_mut(&mut self) -> &mut InnerRequest { + pub(crate) fn inner_mut(&mut self) -> &mut Message { Rc::get_mut(&mut self.inner).expect("Multiple copies exist") } @@ -201,24 +204,24 @@ impl fmt::Debug for Request { } /// Request's objects pool -pub(crate) struct RequestPool(RefCell>>); +pub(crate) struct MessagePool(RefCell>>); -thread_local!(static POOL: &'static RequestPool = RequestPool::create()); +thread_local!(static POOL: &'static MessagePool = MessagePool::create()); -impl RequestPool { - fn create() -> &'static RequestPool { - let pool = RequestPool(RefCell::new(VecDeque::with_capacity(128))); +impl MessagePool { + fn create() -> &'static MessagePool { + let pool = MessagePool(RefCell::new(VecDeque::with_capacity(128))); Box::leak(Box::new(pool)) } /// Get default request's pool - pub fn pool() -> &'static RequestPool { + pub fn pool() -> &'static MessagePool { POOL.with(|p| *p) } /// Get Request object #[inline] - pub fn get(pool: &'static RequestPool) -> Request { + pub fn get_request(pool: &'static MessagePool) -> Request { if let Some(mut msg) = pool.0.borrow_mut().pop_front() { if let Some(r) = Rc::get_mut(&mut msg) { r.reset(); @@ -228,9 +231,21 @@ impl RequestPool { Request::with_pool(pool) } + /// Get Client Response object + #[inline] + pub fn get_response(pool: &'static MessagePool) -> ClientResponse { + if let Some(mut msg) = pool.0.borrow_mut().pop_front() { + if let Some(r) = Rc::get_mut(&mut msg) { + r.reset(); + } + return ClientResponse { inner: msg }; + } + ClientResponse::with_pool(pool) + } + #[inline] /// Release request instance - pub(crate) fn release(&self, msg: Rc) { + pub(crate) fn release(&self, msg: Rc) { let v = &mut self.0.borrow_mut(); if v.len() < 128 { v.push_front(msg); diff --git a/src/service.rs b/src/service.rs index 4467087bc..a28529145 100644 --- a/src/service.rs +++ b/src/service.rs @@ -8,7 +8,7 @@ use futures::{Async, AsyncSink, Future, Poll, Sink}; use tokio_io::AsyncWrite; use error::ResponseError; -use h1::{Codec, OutMessage}; +use h1::{Codec, Message}; use response::Response; pub struct SendError(PhantomData<(T, R, E)>); @@ -59,7 +59,7 @@ where Ok(r) => Either::A(ok(r)), Err((e, framed)) => Either::B(SendErrorFut { framed: Some(framed), - res: Some(OutMessage::Response(e.error_response())), + res: Some(Message::Item(e.error_response())), err: Some(e), _t: PhantomData, }), @@ -68,7 +68,7 @@ where } pub struct SendErrorFut { - res: Option, + res: Option>, framed: Option>, err: Option, _t: PhantomData, @@ -149,14 +149,14 @@ where fn call(&mut self, (res, framed): Self::Request) -> Self::Future { SendResponseFut { - res: Some(OutMessage::Response(res)), + res: Some(Message::Item(res)), framed: Some(framed), } } } pub struct SendResponseFut { - res: Option, + res: Option>, framed: Option>, } diff --git a/src/ws/client/connect.rs b/src/ws/client/connect.rs new file mode 100644 index 000000000..575b4e4d8 --- /dev/null +++ b/src/ws/client/connect.rs @@ -0,0 +1,95 @@ +//! Http client request +use std::str; + +use cookie::Cookie; +use http::header::{HeaderName, HeaderValue}; +use http::{Error as HttpError, HttpTryFrom}; + +use client::{ClientRequest, ClientRequestBuilder}; +use header::IntoHeaderValue; + +use super::ClientError; + +/// `WebSocket` connection +pub struct Connect { + pub(super) request: ClientRequestBuilder, + pub(super) err: Option, + pub(super) http_err: Option, + pub(super) origin: Option, + pub(super) protocols: Option, + pub(super) max_size: usize, + pub(super) server_mode: bool, +} + +impl Connect { + /// Create new websocket connection + pub fn new>(uri: S) -> Connect { + let mut cl = Connect { + request: ClientRequest::build(), + err: None, + http_err: None, + origin: None, + protocols: None, + max_size: 65_536, + server_mode: false, + }; + cl.request.uri(uri.as_ref()); + cl + } + + /// Set supported websocket protocols + pub fn protocols(mut self, protos: U) -> Self + where + U: IntoIterator + 'static, + V: AsRef, + { + let mut protos = protos + .into_iter() + .fold(String::new(), |acc, s| acc + s.as_ref() + ","); + protos.pop(); + self.protocols = Some(protos); + self + } + + /// Set cookie for handshake request + pub fn cookie(mut self, cookie: Cookie) -> Self { + self.request.cookie(cookie); + self + } + + /// Set request Origin + pub fn origin(mut self, origin: V) -> Self + where + HeaderValue: HttpTryFrom, + { + match HeaderValue::try_from(origin) { + Ok(value) => self.origin = Some(value), + Err(e) => self.http_err = Some(e.into()), + } + self + } + + /// Set max frame size + /// + /// By default max size is set to 64kb + pub fn max_frame_size(mut self, size: usize) -> Self { + self.max_size = size; + self + } + + /// Disable payload masking. By default ws client masks frame payload. + pub fn server_mode(mut self) -> Self { + self.server_mode = true; + self + } + + /// Set request header + pub fn header(mut self, key: K, value: V) -> Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + self.request.header(key, value); + self + } +} diff --git a/src/ws/client/error.rs b/src/ws/client/error.rs new file mode 100644 index 000000000..62a8800aa --- /dev/null +++ b/src/ws/client/error.rs @@ -0,0 +1,87 @@ +//! Http client request +use std::io; + +use actix_net::connector::ConnectorError; +use http::header::HeaderValue; +use http::StatusCode; + +use error::ParseError; +use http::Error as HttpError; +use ws::ProtocolError; + +/// Websocket client error +#[derive(Fail, Debug)] +pub enum ClientError { + /// Invalid url + #[fail(display = "Invalid url")] + InvalidUrl, + /// Invalid response status + #[fail(display = "Invalid response status")] + InvalidResponseStatus(StatusCode), + /// Invalid upgrade header + #[fail(display = "Invalid upgrade header")] + InvalidUpgradeHeader, + /// Invalid connection header + #[fail(display = "Invalid connection header")] + InvalidConnectionHeader(HeaderValue), + /// Missing CONNECTION header + #[fail(display = "Missing CONNECTION header")] + MissingConnectionHeader, + /// Missing SEC-WEBSOCKET-ACCEPT header + #[fail(display = "Missing SEC-WEBSOCKET-ACCEPT header")] + MissingWebSocketAcceptHeader, + /// Invalid challenge response + #[fail(display = "Invalid challenge response")] + InvalidChallengeResponse(String, HeaderValue), + /// Http parsing error + #[fail(display = "Http parsing error")] + Http(HttpError), + // /// Url parsing error + // #[fail(display = "Url parsing error")] + // Url(UrlParseError), + /// Response parsing error + #[fail(display = "Response parsing error")] + ParseError(ParseError), + /// Protocol error + #[fail(display = "{}", _0)] + Protocol(#[cause] ProtocolError), + /// Connect error + #[fail(display = "{:?}", _0)] + Connect(ConnectorError), + /// IO Error + #[fail(display = "{}", _0)] + Io(io::Error), + /// "Disconnected" + #[fail(display = "Disconnected")] + Disconnected, +} + +impl From for ClientError { + fn from(err: HttpError) -> ClientError { + ClientError::Http(err) + } +} + +impl From for ClientError { + fn from(err: ConnectorError) -> ClientError { + ClientError::Connect(err) + } +} + +impl From for ClientError { + fn from(err: ProtocolError) -> ClientError { + ClientError::Protocol(err) + } +} + +impl From for ClientError { + fn from(err: io::Error) -> ClientError { + ClientError::Io(err) + } +} + +impl From for ClientError { + fn from(err: ParseError) -> ClientError { + ClientError::ParseError(err) + } +} diff --git a/src/ws/client/mod.rs b/src/ws/client/mod.rs new file mode 100644 index 000000000..0dbf081c6 --- /dev/null +++ b/src/ws/client/mod.rs @@ -0,0 +1,48 @@ +mod connect; +mod error; +mod service; + +pub use self::connect::Connect; +pub use self::error::ClientError; +pub use self::service::Client; + +#[derive(PartialEq, Hash, Debug, Clone, Copy)] +pub(crate) enum Protocol { + Http, + Https, + Ws, + Wss, +} + +impl Protocol { + fn from(s: &str) -> Option { + match s { + "http" => Some(Protocol::Http), + "https" => Some(Protocol::Https), + "ws" => Some(Protocol::Ws), + "wss" => Some(Protocol::Wss), + _ => None, + } + } + + fn is_http(self) -> bool { + match self { + Protocol::Https | Protocol::Http => true, + _ => false, + } + } + + fn is_secure(self) -> bool { + match self { + Protocol::Https | Protocol::Wss => true, + _ => false, + } + } + + fn port(self) -> u16 { + match self { + Protocol::Http | Protocol::Ws => 80, + Protocol::Https | Protocol::Wss => 443, + } + } +} diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs new file mode 100644 index 000000000..ab0e48035 --- /dev/null +++ b/src/ws/client/service.rs @@ -0,0 +1,270 @@ +//! websockets client +use std::marker::PhantomData; + +use actix_net::codec::Framed; +use actix_net::connector::{ConnectorError, DefaultConnector}; +use actix_net::service::Service; +use base64; +use futures::future::{err, Either, FutureResult}; +use futures::{Async, Future, Poll, Sink, Stream}; +use http::header::{self, HeaderValue}; +use http::{HttpTryFrom, StatusCode}; +use rand; +use sha1::Sha1; +use tokio_io::{AsyncRead, AsyncWrite}; + +use client::ClientResponse; +use h1; +use ws::Codec; + +use super::{ClientError, Connect, Protocol}; + +/// WebSocket's client +pub struct Client +where + T: Service, + T::Response: AsyncRead + AsyncWrite, +{ + connector: T, +} + +impl Client +where + T: Service, + T::Response: AsyncRead + AsyncWrite, +{ + /// Create new websocket's client factory + pub fn new(connector: T) -> Self { + Client { connector } + } +} + +impl Default for Client> { + fn default() -> Self { + Client::new(DefaultConnector::default()) + } +} + +impl Clone for Client +where + T: Service + Clone, + T::Response: AsyncRead + AsyncWrite, +{ + fn clone(&self) -> Self { + Client { + connector: self.connector.clone(), + } + } +} + +impl Service for Client +where + T: Service, + T::Response: AsyncRead + AsyncWrite + 'static, + T::Future: 'static, +{ + type Request = Connect; + type Response = Framed; + type Error = ClientError; + type Future = Either< + FutureResult, + ClientResponseFut, + >; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.connector.poll_ready().map_err(ClientError::from) + } + + fn call(&mut self, mut req: Self::Request) -> Self::Future { + if let Some(e) = req.err.take() { + Either::A(err(e)) + } else if let Some(e) = req.http_err.take() { + Either::A(err(e.into())) + } else { + // origin + if let Some(origin) = req.origin.take() { + req.request.set_header(header::ORIGIN, origin); + } + + req.request.upgrade(); + req.request.set_header(header::UPGRADE, "websocket"); + req.request.set_header(header::CONNECTION, "upgrade"); + req.request.set_header(header::SEC_WEBSOCKET_VERSION, "13"); + + if let Some(protocols) = req.protocols.take() { + req.request + .set_header(header::SEC_WEBSOCKET_PROTOCOL, protocols.as_str()); + } + let mut request = match req.request.finish() { + Ok(req) => req, + Err(e) => return Either::A(err(e.into())), + }; + + if request.uri().host().is_none() { + return Either::A(err(ClientError::InvalidUrl)); + } + + // supported protocols + let proto = if let Some(scheme) = request.uri().scheme_part() { + match Protocol::from(scheme.as_str()) { + Some(proto) => proto, + None => return Either::A(err(ClientError::InvalidUrl)), + } + } else { + return Either::A(err(ClientError::InvalidUrl)); + }; + + // Generate a random key for the `Sec-WebSocket-Key` header. + // a base64-encoded (see Section 4 of [RFC4648]) value that, + // when decoded, is 16 bytes in length (RFC 6455) + let sec_key: [u8; 16] = rand::random(); + let key = base64::encode(&sec_key); + + request.headers_mut().insert( + header::SEC_WEBSOCKET_KEY, + HeaderValue::try_from(key.as_str()).unwrap(), + ); + + // prep connection + let host = { + let uri = request.uri(); + format!( + "{}:{}", + uri.host().unwrap(), + uri.port().unwrap_or_else(|| proto.port()) + ) + }; + + let fut = Box::new( + self.connector + .call(host) + .map_err(|e| ClientError::from(e)) + .and_then(move |io| { + // h1 protocol + let framed = Framed::new(io, h1::ClientCodec::default()); + framed + .send(request.into()) + .map_err(|e| ClientError::from(e)) + .and_then(|framed| { + framed + .into_future() + .map_err(|(e, _)| ClientError::from(e)) + }) + }), + ); + + // start handshake + Either::B(ClientResponseFut { + key, + fut, + max_size: req.max_size, + server_mode: req.server_mode, + _t: PhantomData, + }) + } + } +} + +/// Future that implementes client websocket handshake process. +/// +/// It resolves to a `Framed` instance. +pub struct ClientResponseFut +where + T: AsyncRead + AsyncWrite, +{ + fut: Box< + Future< + Item = ( + Option>, + Framed, + ), + Error = ClientError, + >, + >, + key: String, + max_size: usize, + server_mode: bool, + _t: PhantomData, +} + +impl Future for ClientResponseFut +where + T: AsyncRead + AsyncWrite, +{ + type Item = Framed; + type Error = ClientError; + + fn poll(&mut self) -> Poll { + let (item, framed) = try_ready!(self.fut.poll()); + + let res = match item { + Some(h1::Message::Item(res)) => res, + Some(h1::Message::Chunk(_)) => unreachable!(), + None => return Err(ClientError::Disconnected), + }; + + // verify response + if res.status() != StatusCode::SWITCHING_PROTOCOLS { + return Err(ClientError::InvalidResponseStatus(res.status())); + } + // Check for "UPGRADE" to websocket header + let has_hdr = if let Some(hdr) = res.headers().get(header::UPGRADE) { + if let Ok(s) = hdr.to_str() { + s.to_lowercase().contains("websocket") + } else { + false + } + } else { + false + }; + if !has_hdr { + trace!("Invalid upgrade header"); + return Err(ClientError::InvalidUpgradeHeader); + } + // Check for "CONNECTION" header + if let Some(conn) = res.headers().get(header::CONNECTION) { + if let Ok(s) = conn.to_str() { + if !s.to_lowercase().contains("upgrade") { + trace!("Invalid connection header: {}", s); + return Err(ClientError::InvalidConnectionHeader(conn.clone())); + } + } else { + trace!("Invalid connection header: {:?}", conn); + return Err(ClientError::InvalidConnectionHeader(conn.clone())); + } + } else { + trace!("Missing connection header"); + return Err(ClientError::MissingConnectionHeader); + } + + if let Some(key) = res.headers().get(header::SEC_WEBSOCKET_ACCEPT) { + // field is constructed by concatenating /key/ + // with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (RFC 6455) + const WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + let mut sha1 = Sha1::new(); + sha1.update(self.key.as_ref()); + sha1.update(WS_GUID); + let encoded = base64::encode(&sha1.digest().bytes()); + if key.as_bytes() != encoded.as_bytes() { + trace!( + "Invalid challenge response: expected: {} received: {:?}", + encoded, + key + ); + return Err(ClientError::InvalidChallengeResponse(encoded, key.clone())); + } + } else { + trace!("Missing SEC-WEBSOCKET-ACCEPT header"); + return Err(ClientError::MissingWebSocketAcceptHeader); + }; + + // websockets codec + let codec = if self.server_mode { + Codec::new().max_size(self.max_size) + } else { + Codec::new().max_size(self.max_size).client_mode() + }; + + Ok(Async::Ready(framed.into_framed(codec))) + } +} diff --git a/src/ws/mod.rs b/src/ws/mod.rs index 690c56feb..b5a667084 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -10,6 +10,7 @@ use http::{header, Method, StatusCode}; use request::Request; use response::{ConnectionType, Response, ResponseBuilder}; +mod client; mod codec; mod frame; mod mask; @@ -17,6 +18,7 @@ mod proto; mod service; mod transport; +pub use self::client::{Client, ClientError, Connect}; pub use self::codec::{Codec, Frame, Message}; pub use self::frame::Parser; pub use self::proto::{CloseCode, CloseReason, OpCode}; diff --git a/tests/test_ws.rs b/tests/test_ws.rs index a5503c209..85770fa8e 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -11,12 +11,12 @@ use actix::System; use actix_net::codec::Framed; use actix_net::framed::IntoFramed; use actix_net::server::Server; -use actix_net::service::NewServiceExt; +use actix_net::service::{NewServiceExt, Service}; use actix_net::stream::TakeItem; use actix_web::{test, ws as web_ws}; -use bytes::Bytes; -use futures::future::{ok, Either}; -use futures::{Future, Sink, Stream}; +use bytes::{Bytes, BytesMut}; +use futures::future::{lazy, ok, Either}; +use futures::{Future, IntoFuture, Sink, Stream}; use actix_http::{h1, ws, ResponseError, ServiceConfig}; @@ -51,14 +51,14 @@ fn test_simple() { .and_then(TakeItem::new().map_err(|_| ())) .and_then(|(req, framed): (_, Framed<_, _>)| { // validate request - if let Some(h1::InMessage::Message(req, _)) = req { + if let Some(h1::Message::Item(req)) = req { match ws::verify_handshake(&req) { Err(e) => { // validation failed let resp = e.error_response(); Either::A( framed - .send(h1::OutMessage::Response(resp)) + .send(h1::Message::Item(resp)) .map_err(|_| ()) .map(|_| ()), ) @@ -66,7 +66,7 @@ fn test_simple() { Ok(_) => Either::B( // send response framed - .send(h1::OutMessage::Response( + .send(h1::Message::Item( ws::handshake_response(&req).finish(), )).map_err(|_| ()) .and_then(|framed| { @@ -116,4 +116,43 @@ fn test_simple() { ))) ); } + + // client service + let mut client = sys + .block_on(lazy(|| Ok::<_, ()>(ws::Client::default()).into_future())) + .unwrap(); + let framed = sys + .block_on(client.call(ws::Connect::new(format!("http://{}/", addr)))) + .unwrap(); + + let framed = sys + .block_on(framed.send(ws::Message::Text("text".to_string()))) + .unwrap(); + let (item, framed) = sys.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!(item, Some(ws::Frame::Text(Some(BytesMut::from("text"))))); + + let framed = sys + .block_on(framed.send(ws::Message::Binary("text".into()))) + .unwrap(); + let (item, framed) = sys.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!( + item, + Some(ws::Frame::Binary(Some(Bytes::from_static(b"text").into()))) + ); + + let framed = sys + .block_on(framed.send(ws::Message::Ping("text".into()))) + .unwrap(); + let (item, framed) = sys.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!(item, Some(ws::Frame::Pong("text".to_string().into()))); + + let framed = sys + .block_on(framed.send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))) + .unwrap(); + + let (item, _framed) = sys.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!( + item, + Some(ws::Frame::Close(Some(ws::CloseCode::Normal.into()))) + ) } From 4260692034d9f78f2e99ea4cd00c12dbacf42929 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 22 Oct 2018 18:52:40 -0700 Subject: [PATCH 048/427] add DefaultClient type alias --- src/ws/client/mod.rs | 2 +- src/ws/client/service.rs | 3 +++ src/ws/mod.rs | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ws/client/mod.rs b/src/ws/client/mod.rs index 0dbf081c6..12c7229b9 100644 --- a/src/ws/client/mod.rs +++ b/src/ws/client/mod.rs @@ -4,7 +4,7 @@ mod service; pub use self::connect::Connect; pub use self::error::ClientError; -pub use self::service::Client; +pub use self::service::{Client, DefaultClient}; #[derive(PartialEq, Hash, Debug, Clone, Copy)] pub(crate) enum Protocol { diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index ab0e48035..8e97b743d 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -19,6 +19,9 @@ use ws::Codec; use super::{ClientError, Connect, Protocol}; +/// Default client, uses default connector. +pub type DefaultClient = Client>; + /// WebSocket's client pub struct Client where diff --git a/src/ws/mod.rs b/src/ws/mod.rs index b5a667084..1fbbd03dd 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -18,7 +18,7 @@ mod proto; mod service; mod transport; -pub use self::client::{Client, ClientError, Connect}; +pub use self::client::{Client, ClientError, Connect, DefaultClient}; pub use self::codec::{Codec, Frame, Message}; pub use self::frame::Parser; pub use self::proto::{CloseCode, CloseReason, OpCode}; From bc6e62349c907f633377881f95fef69399fe8416 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 23 Oct 2018 21:44:20 -0700 Subject: [PATCH 049/427] update deps; export api --- Cargo.toml | 4 ++-- src/request.rs | 24 +++++++++++++----------- src/ws/client/error.rs | 3 --- src/ws/codec.rs | 1 + 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 87bd05b8b..87e705f76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,8 +36,8 @@ cell = ["actix-net/cell"] [dependencies] actix = "0.7.5" -actix-net = "0.1.1" -#actix-net = { git="https://github.com/actix/actix-net.git" } +#actix-net = "0.1.1" +actix-net = { git="https://github.com/actix/actix-net.git" } base64 = "0.9" bitflags = "1.0" diff --git a/src/request.rs b/src/request.rs index ad0486395..07632bf05 100644 --- a/src/request.rs +++ b/src/request.rs @@ -23,16 +23,16 @@ pub struct Request { pub(crate) inner: Rc, } -pub(crate) struct Message { - pub(crate) version: Version, - pub(crate) status: StatusCode, - pub(crate) method: Method, - pub(crate) url: Url, - pub(crate) flags: Cell, - pub(crate) headers: HeaderMap, - pub(crate) extensions: RefCell, - pub(crate) payload: RefCell>, +pub struct Message { + pub version: Version, + pub status: StatusCode, + pub method: Method, + pub url: Url, + pub headers: HeaderMap, + pub extensions: RefCell, + pub payload: RefCell>, pub(crate) pool: &'static MessagePool, + pub(crate) flags: Cell, } impl Message { @@ -87,12 +87,14 @@ impl Request { } #[inline] - pub(crate) fn inner(&self) -> &Message { + #[doc(hidden)] + pub fn inner(&self) -> &Message { self.inner.as_ref() } #[inline] - pub(crate) fn inner_mut(&mut self) -> &mut Message { + #[doc(hidden)] + pub fn inner_mut(&mut self) -> &mut Message { Rc::get_mut(&mut self.inner).expect("Multiple copies exist") } diff --git a/src/ws/client/error.rs b/src/ws/client/error.rs index 62a8800aa..951cd6ac4 100644 --- a/src/ws/client/error.rs +++ b/src/ws/client/error.rs @@ -36,9 +36,6 @@ pub enum ClientError { /// Http parsing error #[fail(display = "Http parsing error")] Http(HttpError), - // /// Url parsing error - // #[fail(display = "Url parsing error")] - // Url(UrlParseError), /// Response parsing error #[fail(display = "Response parsing error")] ParseError(ParseError), diff --git a/src/ws/codec.rs b/src/ws/codec.rs index 7ba10672c..5bdc58199 100644 --- a/src/ws/codec.rs +++ b/src/ws/codec.rs @@ -36,6 +36,7 @@ pub enum Frame { Close(Option), } +#[derive(Debug)] /// WebSockets protocol codec pub struct Codec { max_size: usize, From cd0223e8b7387d57bbd32dacc97b8a6d4ef580d3 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 23 Oct 2018 22:41:30 -0700 Subject: [PATCH 050/427] update Connector usage --- src/ws/client/service.rs | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index 8e97b743d..b494564bc 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; use actix_net::codec::Framed; -use actix_net::connector::{ConnectorError, DefaultConnector}; +use actix_net::connector::{Connect as TcpConnect, ConnectorError, DefaultConnector}; use actix_net::service::Service; use base64; use futures::future::{err, Either, FutureResult}; @@ -20,7 +20,7 @@ use ws::Codec; use super::{ClientError, Connect, Protocol}; /// Default client, uses default connector. -pub type DefaultClient = Client>; +pub type DefaultClient = Client; /// WebSocket's client pub struct Client @@ -33,7 +33,7 @@ where impl Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite, { /// Create new websocket's client factory @@ -42,7 +42,7 @@ where } } -impl Default for Client> { +impl Default for Client { fn default() -> Self { Client::new(DefaultConnector::default()) } @@ -50,7 +50,7 @@ impl Default for Client> { impl Clone for Client where - T: Service + Clone, + T: Service + Clone, T::Response: AsyncRead + AsyncWrite, { fn clone(&self) -> Self { @@ -62,7 +62,7 @@ where impl Service for Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite + 'static, T::Future: 'static, { @@ -129,18 +129,14 @@ where ); // prep connection - let host = { - let uri = request.uri(); - format!( - "{}:{}", - uri.host().unwrap(), - uri.port().unwrap_or_else(|| proto.port()) - ) - }; + let connect = TcpConnect::new( + request.uri().host().unwrap(), + request.uri().port().unwrap_or_else(|| proto.port()), + ); let fut = Box::new( self.connector - .call(host) + .call(connect) .map_err(|e| ClientError::from(e)) .and_then(move |io| { // h1 protocol From 540ad18432ed7be876fe7deae9f00415c116dbf4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 24 Oct 2018 16:48:45 -0700 Subject: [PATCH 051/427] add Debug impl --- src/h1/codec.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 8f7e77e05..1484d56b1 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -1,4 +1,5 @@ #![allow(unused_imports, unused_variables, dead_code)] +use std::fmt; use std::io::{self, Write}; use bytes::{BufMut, Bytes, BytesMut}; @@ -41,6 +42,12 @@ pub struct Codec { te: ResponseEncoder, } +impl fmt::Debug for Codec { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "h1::Codec({:?})", self.flags) + } +} + impl Codec { /// Create HTTP/1 codec. /// From c2540cc59b8967291cf8166bcfb256a5091420a0 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 29 Oct 2018 16:39:46 -0700 Subject: [PATCH 052/427] clippy warnings --- src/client/request.rs | 3 ++- src/config.rs | 4 +++- src/h1/client.rs | 8 ++++++-- src/h1/codec.rs | 8 ++++++-- src/h1/decoder.rs | 16 ++++++++++++---- src/h1/dispatcher.rs | 34 +++++++++++++++++----------------- src/h1/encoder.rs | 4 +++- src/h1/service.rs | 2 +- src/response.rs | 12 ++++++++---- src/service.rs | 6 ++---- src/ws/client/service.rs | 4 ++-- src/ws/frame.rs | 14 +++++++++++--- src/ws/mask.rs | 9 +++------ 13 files changed, 76 insertions(+), 48 deletions(-) diff --git a/src/client/request.rs b/src/client/request.rs index bf82927e9..8c7949336 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -536,7 +536,8 @@ impl ClientRequestBuilder { #[inline] fn parts<'a>( - parts: &'a mut Option, err: &Option, + parts: &'a mut Option, + err: &Option, ) -> Option<&'a mut ClientRequest> { if err.is_some() { return None; diff --git a/src/config.rs b/src/config.rs index ff22ea486..833bca7f2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -66,7 +66,9 @@ impl Default for ServiceConfig { impl ServiceConfig { /// Create instance of `ServiceConfig` pub(crate) fn new( - keep_alive: KeepAlive, client_timeout: u64, client_disconnect: u64, + keep_alive: KeepAlive, + client_timeout: u64, + client_disconnect: u64, ) -> ServiceConfig { let (keep_alive, ka_enabled) = match keep_alive { KeepAlive::Timeout(val) => (val as u64, true), diff --git a/src/h1/client.rs b/src/h1/client.rs index 8ec1f0aea..b55af185f 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -102,7 +102,9 @@ impl ClientCodec { } fn encode_response( - &mut self, msg: ClientRequest, buffer: &mut BytesMut, + &mut self, + msg: ClientRequest, + buffer: &mut BytesMut, ) -> io::Result<()> { // Connection upgrade if msg.upgrade() { @@ -187,7 +189,9 @@ impl Encoder for ClientCodec { type Error = io::Error; fn encode( - &mut self, item: Self::Item, dst: &mut BytesMut, + &mut self, + item: Self::Item, + dst: &mut BytesMut, ) -> Result<(), Self::Error> { match item { Message::Item(res) => { diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 1484d56b1..7cc516d6b 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -103,7 +103,9 @@ impl Codec { } fn encode_response( - &mut self, mut msg: Response, buffer: &mut BytesMut, + &mut self, + mut msg: Response, + buffer: &mut BytesMut, ) -> io::Result<()> { let ka = self.flags.contains(Flags::KEEPALIVE_ENABLED) && msg .keep_alive() @@ -277,7 +279,9 @@ impl Encoder for Codec { type Error = io::Error; fn encode( - &mut self, item: Self::Item, dst: &mut BytesMut, + &mut self, + item: Self::Item, + dst: &mut BytesMut, ) -> Result<(), Self::Error> { match item { Message::Item(res) => { diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 2c1e6c1f2..e8d07af12 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -334,7 +334,9 @@ pub(crate) struct HeaderIndex { impl HeaderIndex { pub(crate) fn record( - bytes: &[u8], headers: &[httparse::Header], indices: &mut [HeaderIndex], + bytes: &[u8], + headers: &[httparse::Header], + indices: &mut [HeaderIndex], ) { let bytes_ptr = bytes.as_ptr() as usize; for (header, indices) in headers.iter().zip(indices.iter_mut()) { @@ -491,7 +493,10 @@ macro_rules! byte ( impl ChunkedState { fn step( - &self, body: &mut BytesMut, size: &mut u64, buf: &mut Option, + &self, + body: &mut BytesMut, + size: &mut u64, + buf: &mut Option, ) -> Poll { use self::ChunkedState::*; match *self { @@ -554,7 +559,8 @@ impl ChunkedState { } } fn read_size_lf( - rdr: &mut BytesMut, size: &mut u64, + rdr: &mut BytesMut, + size: &mut u64, ) -> Poll { match byte!(rdr) { b'\n' if *size > 0 => Ok(Async::Ready(ChunkedState::Body)), @@ -567,7 +573,9 @@ impl ChunkedState { } fn read_body( - rdr: &mut BytesMut, rem: &mut u64, buf: &mut Option, + rdr: &mut BytesMut, + rem: &mut u64, + buf: &mut Option, ) -> Poll { trace!("Chunked read, remaining={:?}", rem); diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index a7f97e8c9..513c19617 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -90,7 +90,10 @@ where /// Create http/1 dispatcher with slow request timeout. pub fn with_timeout( - stream: T, config: ServiceConfig, timeout: Option, service: S, + stream: T, + config: ServiceConfig, + timeout: Option, + service: S, ) -> Self { let keepalive = config.keep_alive_enabled(); let flags = if keepalive { @@ -173,19 +176,15 @@ where // process loop { let state = match self.state { - State::None => loop { - break if let Some(msg) = self.messages.pop_front() { - match msg { - DispatcherMessage::Item(req) => { - Some(self.handle_request(req)?) - } - DispatcherMessage::Error(res) => Some(State::SendResponse( - Some((Message::Item(res), Body::Empty)), - )), - } - } else { - None - }; + State::None => if let Some(msg) = self.messages.pop_front() { + match msg { + DispatcherMessage::Item(req) => Some(self.handle_request(req)?), + DispatcherMessage::Error(res) => Some(State::SendResponse( + Some((Message::Item(res), Body::Empty)), + )), + } + } else { + None }, // call inner service State::ServiceCall(ref mut fut) => { @@ -255,7 +254,7 @@ where .framed .as_mut() .unwrap() - .start_send(Message::Chunk(Some(item.into()))) + .start_send(Message::Chunk(Some(item))) { Ok(AsyncSink::Ready) => { self.flags.remove(Flags::FLUSHED); @@ -299,7 +298,8 @@ where } fn handle_request( - &mut self, req: Request, + &mut self, + req: Request, ) -> Result, DispatchError> { let mut task = self.service.call(req); match task.poll().map_err(DispatchError::Service)? { @@ -324,7 +324,7 @@ where } let mut updated = false; - 'outer: loop { + loop { match self.framed.as_mut().unwrap().poll() { Ok(Async::Ready(Some(msg))) => { updated = true; diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index 63ba05d04..fc0a3a1b2 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -107,7 +107,9 @@ impl ResponseEncoder { } fn streaming_encoding( - &mut self, version: Version, resp: &mut Response, + &mut self, + version: Version, + resp: &mut Response, ) -> TransferEncoding { match resp.chunked() { Some(true) => { diff --git a/src/h1/service.rs b/src/h1/service.rs index d691ae75d..7e5e8c5fc 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -253,7 +253,7 @@ where type Future = Dispatcher; fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.srv.poll_ready().map_err(|e| DispatchError::Service(e)) + self.srv.poll_ready().map_err(DispatchError::Service) } fn call(&mut self, req: Self::Request) -> Self::Future { diff --git a/src/response.rs b/src/response.rs index e834748ef..1901443fe 100644 --- a/src/response.rs +++ b/src/response.rs @@ -690,9 +690,10 @@ impl ResponseBuilder { } #[inline] -#[cfg_attr(feature = "cargo-clippy", allow(clippy::borrowed_box))] +#[cfg_attr(feature = "cargo-clippy", allow(borrowed_box))] fn parts<'a>( - parts: &'a mut Option>, err: &Option, + parts: &'a mut Option>, + err: &Option, ) -> Option<&'a mut Box> { if err.is_some() { return None; @@ -871,7 +872,8 @@ impl ResponsePool { #[inline] pub fn get_builder( - pool: &'static ResponsePool, status: StatusCode, + pool: &'static ResponsePool, + status: StatusCode, ) -> ResponseBuilder { if let Some(mut msg) = pool.0.borrow_mut().pop_front() { msg.status = status; @@ -894,7 +896,9 @@ impl ResponsePool { #[inline] pub fn get_response( - pool: &'static ResponsePool, status: StatusCode, body: Body, + pool: &'static ResponsePool, + status: StatusCode, + body: Body, ) -> Response { if let Some(mut msg) = pool.0.borrow_mut().pop_front() { msg.status = status; diff --git a/src/service.rs b/src/service.rs index a28529145..16dcf8c6b 100644 --- a/src/service.rs +++ b/src/service.rs @@ -97,12 +97,10 @@ where } match self.framed.as_mut().unwrap().poll_complete() { Ok(Async::Ready(_)) => { - return Err((self.err.take().unwrap(), self.framed.take().unwrap())) + Err((self.err.take().unwrap(), self.framed.take().unwrap())) } Ok(Async::NotReady) => Ok(Async::NotReady), - Err(_) => { - return Err((self.err.take().unwrap(), self.framed.take().unwrap())) - } + Err(_) => Err((self.err.take().unwrap(), self.framed.take().unwrap())), } } } diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index b494564bc..485ce5620 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -137,13 +137,13 @@ where let fut = Box::new( self.connector .call(connect) - .map_err(|e| ClientError::from(e)) + .map_err(ClientError::from) .and_then(move |io| { // h1 protocol let framed = Framed::new(io, h1::ClientCodec::default()); framed .send(request.into()) - .map_err(|e| ClientError::from(e)) + .map_err(ClientError::from) .and_then(|framed| { framed .into_future() diff --git a/src/ws/frame.rs b/src/ws/frame.rs index de1b92394..ca5f0e892 100644 --- a/src/ws/frame.rs +++ b/src/ws/frame.rs @@ -13,7 +13,9 @@ pub struct Parser; impl Parser { fn parse_metadata( - src: &[u8], server: bool, max_size: usize, + src: &[u8], + server: bool, + max_size: usize, ) -> Result)>, ProtocolError> { let chunk_len = src.len(); @@ -86,7 +88,9 @@ impl Parser { /// Parse the input stream into a frame. pub fn parse( - src: &mut BytesMut, server: bool, max_size: usize, + src: &mut BytesMut, + server: bool, + max_size: usize, ) -> Result)>, ProtocolError> { // try to parse ws frame metadata let (idx, finished, opcode, length, mask) = @@ -148,7 +152,11 @@ impl Parser { /// Generate binary representation pub fn write_message>( - dst: &mut BytesMut, pl: B, op: OpCode, fin: bool, mask: bool, + dst: &mut BytesMut, + pl: B, + op: OpCode, + fin: bool, + mask: bool, ) { let payload = pl.into(); let one: u8 = if fin { diff --git a/src/ws/mask.rs b/src/ws/mask.rs index a88c21afb..e9bfb3d56 100644 --- a/src/ws/mask.rs +++ b/src/ws/mask.rs @@ -1,5 +1,5 @@ //! This is code from [Tungstenite project](https://github.com/snapview/tungstenite-rs) -#![cfg_attr(feature = "cargo-clippy", allow(clippy::cast_ptr_alignment))] +#![cfg_attr(feature = "cargo-clippy", allow(cast_ptr_alignment))] use std::ptr::copy_nonoverlapping; use std::slice; @@ -19,7 +19,7 @@ impl<'a> ShortSlice<'a> { /// Faster version of `apply_mask()` which operates on 8-byte blocks. #[inline] -#[cfg_attr(feature = "cargo-clippy", allow(clippy::cast_lossless))] +#[cfg_attr(feature = "cargo-clippy", allow(cast_lossless))] pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) { // Extend the mask to 64 bits let mut mask_u64 = ((mask_u32 as u64) << 32) | (mask_u32 as u64); @@ -50,10 +50,7 @@ pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) { // TODO: copy_nonoverlapping here compiles to call memcpy. While it is not so // inefficient, it could be done better. The compiler does not understand that // a `ShortSlice` must be smaller than a u64. -#[cfg_attr( - feature = "cargo-clippy", - allow(clippy::needless_pass_by_value) -)] +#[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))] fn xor_short(buf: ShortSlice, mask: u64) { // Unsafe: we know that a `ShortSlice` fits in a u64 unsafe { From 148cf73003c10728b773d3d0fd6256c4ff6016f1 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 30 Oct 2018 11:46:44 -0700 Subject: [PATCH 053/427] allow to create response with error message --- src/error.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/error.rs b/src/error.rs index 3c0902030..3064cda49 100644 --- a/src/error.rs +++ b/src/error.rs @@ -98,6 +98,14 @@ impl Error { Fail::downcast_ref(self.cause.as_fail()); compat.and_then(|e| e.get_ref().downcast_ref()) } + + /// Converts error to a response instance and set error message as response body + pub fn response_with_message(self) -> Response { + let message = format!("{}", self); + let mut resp: Response = self.into(); + resp.set_body(message); + resp + } } /// Helper trait to downcast a response error into a fail. From 79bcbb8a101cf448ebb8bc7e7c11f3249f0b0e19 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 30 Oct 2018 11:50:30 -0700 Subject: [PATCH 054/427] use error message --- src/service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service.rs b/src/service.rs index 16dcf8c6b..5879b3881 100644 --- a/src/service.rs +++ b/src/service.rs @@ -59,7 +59,7 @@ where Ok(r) => Either::A(ok(r)), Err((e, framed)) => Either::B(SendErrorFut { framed: Some(framed), - res: Some(Message::Item(e.error_response())), + res: Some(Message::Item(e.response_with_message())), err: Some(e), _t: PhantomData, }), From da82e249547530a809fe5b3c3c4252d4abbbfef5 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 30 Oct 2018 11:53:48 -0700 Subject: [PATCH 055/427] render error message as body --- src/service.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/service.rs b/src/service.rs index 5879b3881..c76530320 100644 --- a/src/service.rs +++ b/src/service.rs @@ -57,12 +57,16 @@ where fn call(&mut self, req: Self::Request) -> Self::Future { match req { Ok(r) => Either::A(ok(r)), - Err((e, framed)) => Either::B(SendErrorFut { - framed: Some(framed), - res: Some(Message::Item(e.response_with_message())), - err: Some(e), - _t: PhantomData, - }), + Err((e, framed)) => { + let mut resp = e.error_response(); + resp.set_body(format!("{}", e)); + Either::B(SendErrorFut { + framed: Some(framed), + res: Some(resp.into()), + err: Some(e), + _t: PhantomData, + }) + } } } } From f1587243c264af76aa731162687c3ba752b8a42e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 5 Nov 2018 19:32:03 -0800 Subject: [PATCH 056/427] fix body decoding --- Cargo.toml | 1 + src/h1/codec.rs | 63 +++++++++++++++++++++++++++++++++++++++++++- src/h1/decoder.rs | 11 -------- src/h1/dispatcher.rs | 2 ++ src/h1/mod.rs | 29 ++++++++++++++++++++ 5 files changed, 94 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 87e705f76..23499bbf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ cell = ["actix-net/cell"] [dependencies] actix = "0.7.5" #actix-net = "0.1.1" +#actix-net = { path="../actix-net/" } actix-net = { git="https://github.com/actix/actix-net.git" } base64 = "0.9" diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 7cc516d6b..dd1e27e08 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -42,6 +42,12 @@ pub struct Codec { te: ResponseEncoder, } +impl Default for Codec { + fn default() -> Self { + Codec::new(ServiceConfig::default()) + } +} + impl fmt::Debug for Codec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "h1::Codec({:?})", self.flags) @@ -247,7 +253,10 @@ impl Decoder for Codec { if self.payload.is_some() { Ok(match self.payload.as_mut().unwrap().decode(src)? { Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))), - Some(PayloadItem::Eof) => Some(Message::Chunk(None)), + Some(PayloadItem::Eof) => { + self.payload.take(); + Some(Message::Chunk(None)) + } None => None, }) } else if self.flags.contains(Flags::UNHANDLED) { @@ -297,3 +306,55 @@ impl Encoder for Codec { Ok(()) } } + +#[cfg(test)] +mod tests { + use std::{cmp, io}; + + use bytes::{Buf, Bytes, BytesMut}; + use http::{Method, Version}; + use tokio_io::{AsyncRead, AsyncWrite}; + + use super::*; + use error::ParseError; + use h1::Message; + use httpmessage::HttpMessage; + use request::Request; + + #[test] + fn test_http_request_chunked_payload_and_next_message() { + let mut codec = Codec::default(); + + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + let item = codec.decode(&mut buf).unwrap().unwrap(); + let req = item.message(); + + assert_eq!(req.method(), Method::GET); + assert!(req.chunked().unwrap()); + + buf.extend( + b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\ + POST /test2 HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n" + .iter(), + ); + + let msg = codec.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"data"); + + let msg = codec.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"line"); + + let msg = codec.decode(&mut buf).unwrap().unwrap(); + assert!(msg.eof()); + + // decode next message + let item = codec.decode(&mut buf).unwrap().unwrap(); + let req = item.message(); + assert_eq!(*req.method(), Method::POST); + assert!(req.chunked().unwrap()); + } +} diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index e8d07af12..cfa0879d7 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -648,9 +648,7 @@ mod tests { use super::*; use error::ParseError; - use h1::Message; use httpmessage::HttpMessage; - use request::Request; impl PayloadType { fn unwrap(self) -> PayloadDecoder { @@ -668,15 +666,6 @@ mod tests { } } - impl Message { - fn message(self) -> Request { - match self { - Message::Item(req) => req, - _ => panic!("error"), - } - } - } - impl PayloadItem { fn chunk(self) -> Bytes { match self { diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 513c19617..fb30d881f 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -370,6 +370,7 @@ where Response::InternalServerError().finish(), )); self.error = Some(DispatchError::InternalError); + break; } } Message::Chunk(None) => { @@ -382,6 +383,7 @@ where Response::InternalServerError().finish(), )); self.error = Some(DispatchError::InternalError); + break; } } } diff --git a/src/h1/mod.rs b/src/h1/mod.rs index c33b193f5..e73771778 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -46,3 +46,32 @@ pub enum MessageType { Payload, Unhandled, } + +#[cfg(test)] +mod tests { + use super::*; + + impl Message { + pub fn message(self) -> Request { + match self { + Message::Item(req) => req, + _ => panic!("error"), + } + } + + pub fn chunk(self) -> Bytes { + match self { + Message::Chunk(Some(data)) => data, + _ => panic!("error"), + } + } + + pub fn eof(self) -> bool { + match self { + Message::Chunk(None) => true, + Message::Chunk(Some(_)) => false, + _ => panic!("error"), + } + } + } +} From 6a1d560f227294b0edfb7a6e11e4acbc75cd8015 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 8 Nov 2018 09:30:53 -0800 Subject: [PATCH 057/427] fix keep-alive timer reset --- Cargo.toml | 5 ++--- src/h1/dispatcher.rs | 9 ++++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 23499bbf6..fcc169b76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,9 +36,8 @@ cell = ["actix-net/cell"] [dependencies] actix = "0.7.5" -#actix-net = "0.1.1" -#actix-net = { path="../actix-net/" } -actix-net = { git="https://github.com/actix/actix-net.git" } +actix-net = "0.2.0" +#actix-net = { git="https://github.com/actix/actix-net.git" } base64 = "0.9" bitflags = "1.0" diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index fb30d881f..cc9ec7217 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -441,7 +441,8 @@ where if let Some(deadline) = self.config.client_disconnect_timer() { - timer.reset(deadline) + timer.reset(deadline); + let _ = timer.poll(); } else { return Ok(()); } @@ -455,10 +456,12 @@ where ))); } } else if let Some(deadline) = self.config.keep_alive_expire() { - timer.reset(deadline) + timer.reset(deadline); + let _ = timer.poll(); } } else { - timer.reset(self.ka_expire) + timer.reset(self.ka_expire); + let _ = timer.poll(); } } Ok(Async::NotReady) => (), From dea39030bcf882c879c724d6b4d803735424e1dd Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 8 Nov 2018 20:38:40 -0800 Subject: [PATCH 058/427] properly handle upgrade header if content-length header is set --- src/h1/decoder.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index cfa0879d7..472e29936 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -141,6 +141,13 @@ impl Decoder for RequestDecoder { } header::UPGRADE => { has_upgrade = true; + // check content-length, some clients (dart) + // sends "content-length: 0" with websocket upgrade + if let Ok(val) = value.to_str() { + if val == "websocket" { + content_length = None; + } + } } _ => (), } From b25b083866be7369af890e74a401b33767c856f2 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 8 Nov 2018 20:45:48 -0800 Subject: [PATCH 059/427] do not stop on keep-alive timer if sink is not completly flushed --- src/h1/dispatcher.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index cc9ec7217..f4dfdb219 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -432,7 +432,7 @@ where return Err(DispatchError::DisconnectTimeout); } else if timer.deadline() >= self.ka_expire { // check for any outstanding response processing - if self.state.is_empty() { + if self.state.is_empty() && self.flags.contains(Flags::FLUSHED) { if self.flags.contains(Flags::STARTED) { trace!("Keep-alive timeout, close connection"); self.flags.insert(Flags::SHUTDOWN); From 537144f0b9ef865935c3e542c40dab52da1bba96 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 11 Nov 2018 23:12:54 -0800 Subject: [PATCH 060/427] add http client connector service --- Cargo.toml | 26 +- src/client/connect.rs | 80 ++++++ src/client/connection.rs | 79 ++++++ src/client/connector.rs | 500 +++++++++++++++++++++++++++++++++ src/client/error.rs | 77 ++++++ src/client/mod.rs | 8 + src/client/pool.rs | 579 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 7 + 8 files changed, 1354 insertions(+), 2 deletions(-) create mode 100644 src/client/connect.rs create mode 100644 src/client/connection.rs create mode 100644 src/client/connector.rs create mode 100644 src/client/error.rs create mode 100644 src/client/pool.rs diff --git a/Cargo.toml b/Cargo.toml index fcc169b76..e586f34ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,16 +34,26 @@ session = ["cookie/secure"] cell = ["actix-net/cell"] +# tls +tls = ["native-tls", "actix-net/tls"] + +# openssl +ssl = ["openssl", "actix-net/ssl"] + +# rustls +rust-tls = ["rustls", "actix-net/rust-tls"] + [dependencies] actix = "0.7.5" -actix-net = "0.2.0" -#actix-net = { git="https://github.com/actix/actix-net.git" } +#actix-net = "0.2.0" +actix-net = { git="https://github.com/actix/actix-net.git" } base64 = "0.9" bitflags = "1.0" http = "0.1.8" httparse = "1.3" failure = "0.1.2" +indexmap = "1.0" log = "0.4" mime = "0.3" rand = "0.5" @@ -61,6 +71,7 @@ url = { version="1.7", features=["query_encoding"] } # io net2 = "0.2" +slab = "0.4" bytes = "0.4" byteorder = "1.2" futures = "0.1" @@ -70,6 +81,17 @@ tokio-io = "0.1" tokio-tcp = "0.1" tokio-timer = "0.2" tokio-current-thread = "0.1" +trust-dns-proto = "0.5.0" +trust-dns-resolver = "0.10.0" + +# native-tls +native-tls = { version="0.2", optional = true } + +# openssl +openssl = { version="0.10", optional = true } + +#rustls +rustls = { version = "^0.14", optional = true } [dev-dependencies] actix-web = "0.7" diff --git a/src/client/connect.rs b/src/client/connect.rs new file mode 100644 index 000000000..40c3e8ec9 --- /dev/null +++ b/src/client/connect.rs @@ -0,0 +1,80 @@ +use actix_net::connector::RequestPort; +use actix_net::resolver::RequestHost; +use http::uri::Uri; +use http::{Error as HttpError, HttpTryFrom}; + +use super::error::{ConnectorError, InvalidUrlKind}; +use super::pool::Key; + +#[derive(Debug)] +/// `Connect` type represents a message that can be sent to +/// `Connector` with a connection request. +pub struct Connect { + pub(crate) uri: Uri, +} + +impl Connect { + /// Construct `Uri` instance and create `Connect` message. + pub fn new(uri: U) -> Result + where + Uri: HttpTryFrom, + { + Ok(Connect { + uri: Uri::try_from(uri).map_err(|e| e.into())?, + }) + } + + /// Create `Connect` message for specified `Uri` + pub fn with(uri: Uri) -> Connect { + Connect { uri } + } + + pub(crate) fn is_secure(&self) -> bool { + if let Some(scheme) = self.uri.scheme_part() { + scheme.as_str() == "https" + } else { + false + } + } + + pub(crate) fn key(&self) -> Key { + self.uri.authority_part().unwrap().clone().into() + } + + pub(crate) fn validate(&self) -> Result<(), ConnectorError> { + if self.uri.host().is_none() { + Err(ConnectorError::InvalidUrl(InvalidUrlKind::MissingHost)) + } else if self.uri.scheme_part().is_none() { + Err(ConnectorError::InvalidUrl(InvalidUrlKind::MissingScheme)) + } else if let Some(scheme) = self.uri.scheme_part() { + match scheme.as_str() { + "http" | "ws" | "https" | "wss" => Ok(()), + _ => Err(ConnectorError::InvalidUrl(InvalidUrlKind::UnknownScheme)), + } + } else { + Ok(()) + } + } +} + +impl RequestHost for Connect { + fn host(&self) -> &str { + &self.uri.host().unwrap() + } +} + +impl RequestPort for Connect { + fn port(&self) -> u16 { + if let Some(port) = self.uri.port() { + port + } else if let Some(scheme) = self.uri.scheme_part() { + match scheme.as_str() { + "http" | "ws" => 80, + "https" | "wss" => 443, + _ => 80, + } + } else { + 80 + } + } +} diff --git a/src/client/connection.rs b/src/client/connection.rs new file mode 100644 index 000000000..294e100c8 --- /dev/null +++ b/src/client/connection.rs @@ -0,0 +1,79 @@ +use std::{fmt, io, time}; + +use futures::Poll; +use tokio_io::{AsyncRead, AsyncWrite}; + +use super::pool::Acquired; + +/// HTTP client connection +pub struct Connection { + io: T, + created: time::Instant, + pool: Option>, +} + +impl fmt::Debug for Connection +where + T: AsyncRead + AsyncWrite + fmt::Debug + 'static, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Connection {:?}", self.io) + } +} + +impl Connection { + pub(crate) fn new(io: T, created: time::Instant, pool: Acquired) -> Self { + Connection { + io, + created, + pool: Some(pool), + } + } + + /// Raw IO stream + pub fn get_mut(&mut self) -> &mut T { + &mut self.io + } + + /// Close connection + pub fn close(mut self) { + if let Some(mut pool) = self.pool.take() { + pool.close(self) + } + } + + /// Release this connection to the connection pool + pub fn release(mut self) { + if let Some(mut pool) = self.pool.take() { + pool.release(self) + } + } + + pub(crate) fn into_inner(self) -> (T, time::Instant) { + (self.io, self.created) + } +} + +impl io::Read for Connection { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.io.read(buf) + } +} + +impl AsyncRead for Connection {} + +impl io::Write for Connection { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.io.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.io.flush() + } +} + +impl AsyncWrite for Connection { + fn shutdown(&mut self) -> Poll<(), io::Error> { + self.io.shutdown() + } +} diff --git a/src/client/connector.rs b/src/client/connector.rs new file mode 100644 index 000000000..97da074d1 --- /dev/null +++ b/src/client/connector.rs @@ -0,0 +1,500 @@ +use std::time::Duration; +use std::{fmt, io}; + +use actix_net::connector::TcpConnector; +use actix_net::resolver::Resolver; +use actix_net::service::{Service, ServiceExt}; +use actix_net::timeout::{TimeoutError, TimeoutService}; +use futures::future::Either; +use futures::Poll; +use tokio_io::{AsyncRead, AsyncWrite}; +use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; + +use super::connect::Connect; +use super::connection::Connection; +use super::error::ConnectorError; +use super::pool::ConnectionPool; + +#[cfg(feature = "ssl")] +use actix_net::ssl::OpensslConnector; +#[cfg(feature = "ssl")] +use openssl::ssl::{SslConnector, SslMethod}; + +#[cfg(not(feature = "ssl"))] +type SslConnector = (); + +/// Http client connector builde instance. +/// `Connector` type uses builder-like pattern for connector service construction. +pub struct Connector { + resolver: Resolver, + timeout: Duration, + conn_lifetime: Duration, + conn_keep_alive: Duration, + disconnect_timeout: Duration, + limit: usize, + #[allow(dead_code)] + connector: SslConnector, +} + +impl Default for Connector { + fn default() -> Connector { + let connector = { + #[cfg(feature = "ssl")] + { + SslConnector::builder(SslMethod::tls()).unwrap().build() + } + #[cfg(not(feature = "ssl"))] + { + () + } + }; + + Connector { + connector, + resolver: Resolver::default(), + timeout: Duration::from_secs(1), + conn_lifetime: Duration::from_secs(75), + conn_keep_alive: Duration::from_secs(15), + disconnect_timeout: Duration::from_millis(3000), + limit: 100, + } + } +} + +impl Connector { + /// Use custom resolver configuration. + pub fn resolver_config(mut self, cfg: ResolverConfig, opts: ResolverOpts) -> Self { + self.resolver = Resolver::new(cfg, opts); + self + } + + /// Connection timeout, i.e. max time to connect to remote host including dns name resolution. + /// Set to 1 second by default. + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } + + #[cfg(feature = "ssl")] + /// Use custom `SslConnector` instance. + pub fn ssl(mut self, connector: SslConnector) -> Self { + self.connector = connector; + self + } + + /// Set total number of simultaneous connections per type of scheme. + /// + /// If limit is 0, the connector has no limit. + /// The default limit size is 100. + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + /// Set keep-alive period for opened connection. + /// + /// Keep-alive period is the period between connection usage. If + /// the delay between repeated usages of the same connection + /// exceeds this period, the connection is closed. + /// Default keep-alive period is 15 seconds. + pub fn conn_keep_alive(mut self, dur: Duration) -> Self { + self.conn_keep_alive = dur; + self + } + + /// Set max lifetime period for connection. + /// + /// Connection lifetime is max lifetime of any opened connection + /// until it is closed regardless of keep-alive period. + /// Default lifetime period is 75 seconds. + pub fn conn_lifetime(mut self, dur: Duration) -> Self { + self.conn_lifetime = dur; + self + } + + /// Set server connection disconnect timeout in milliseconds. + /// + /// Defines a timeout for disconnect connection. If a disconnect procedure does not complete + /// within this time, the socket get dropped. This timeout affects only secure connections. + /// + /// To disable timeout set value to 0. + /// + /// By default disconnect timeout is set to 3000 milliseconds. + pub fn disconnect_timeout(mut self, dur: Duration) -> Self { + self.disconnect_timeout = dur; + self + } + + /// Finish configuration process and create connector service. + pub fn service( + self, + ) -> impl Service< + Request = Connect, + Response = impl AsyncRead + AsyncWrite + fmt::Debug, + Error = ConnectorError, + > + Clone { + #[cfg(not(feature = "ssl"))] + { + let connector = TimeoutService::new( + self.timeout, + self.resolver + .map_err(ConnectorError::from) + .and_then(TcpConnector::default().from_err()), + ).map_err(|e| match e { + TimeoutError::Service(e) => e, + TimeoutError::Timeout => ConnectorError::Timeout, + }); + + connect_impl::InnerConnector { + tcp_pool: ConnectionPool::new( + connector, + self.conn_lifetime, + self.conn_keep_alive, + None, + self.limit, + ), + } + } + #[cfg(feature = "ssl")] + { + let ssl_service = TimeoutService::new( + self.timeout, + self.resolver + .clone() + .map_err(ConnectorError::from) + .and_then(TcpConnector::default().from_err()) + .and_then( + OpensslConnector::service(self.connector) + .map_err(ConnectorError::SslError), + ), + ).map_err(|e| match e { + TimeoutError::Service(e) => e, + TimeoutError::Timeout => ConnectorError::Timeout, + }); + + let tcp_service = TimeoutService::new( + self.timeout, + self.resolver + .map_err(ConnectorError::from) + .and_then(TcpConnector::default().from_err()), + ).map_err(|e| match e { + TimeoutError::Service(e) => e, + TimeoutError::Timeout => ConnectorError::Timeout, + }); + + connect_impl::InnerConnector { + tcp_pool: ConnectionPool::new( + tcp_service, + self.conn_lifetime, + self.conn_keep_alive, + None, + self.limit, + ), + ssl_pool: ConnectionPool::new( + ssl_service, + self.conn_lifetime, + self.conn_keep_alive, + Some(self.disconnect_timeout), + self.limit, + ), + } + } + } +} + +#[cfg(not(feature = "ssl"))] +mod connect_impl { + use super::*; + use futures::future::{err, FutureResult}; + + pub(crate) struct InnerConnector + where + Io: AsyncRead + AsyncWrite + 'static, + T: Service, + { + pub(crate) tcp_pool: ConnectionPool, + } + + impl Clone for InnerConnector + where + Io: AsyncRead + AsyncWrite + 'static, + T: Service + + Clone, + { + fn clone(&self) -> Self { + InnerConnector { + tcp_pool: self.tcp_pool.clone(), + } + } + } + + impl Service for InnerConnector + where + Io: AsyncRead + AsyncWrite + 'static, + T: Service, + { + type Request = Connect; + type Response = Connection; + type Error = ConnectorError; + type Future = Either< + as Service>::Future, + FutureResult, ConnectorError>, + >; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.tcp_pool.poll_ready() + } + + fn call(&mut self, req: Self::Request) -> Self::Future { + if req.is_secure() { + Either::B(err(ConnectorError::SslIsNotSupported)) + } else if let Err(e) = req.validate() { + Either::B(err(e)) + } else { + Either::A(self.tcp_pool.call(req)) + } + } + } +} + +#[cfg(feature = "ssl")] +mod connect_impl { + use std::marker::PhantomData; + + use futures::future::{err, FutureResult}; + use futures::{Async, Future, Poll}; + + use super::*; + + pub(crate) struct InnerConnector + where + Io1: AsyncRead + AsyncWrite + 'static, + Io2: AsyncRead + AsyncWrite + 'static, + T1: Service< + Request = Connect, + Response = (Connect, Io1), + Error = ConnectorError, + >, + T2: Service< + Request = Connect, + Response = (Connect, Io2), + Error = ConnectorError, + >, + { + pub(crate) tcp_pool: ConnectionPool, + pub(crate) ssl_pool: ConnectionPool, + } + + impl Clone for InnerConnector + where + Io1: AsyncRead + AsyncWrite + 'static, + Io2: AsyncRead + AsyncWrite + 'static, + T1: Service< + Request = Connect, + Response = (Connect, Io1), + Error = ConnectorError, + > + Clone, + T2: Service< + Request = Connect, + Response = (Connect, Io2), + Error = ConnectorError, + > + Clone, + { + fn clone(&self) -> Self { + InnerConnector { + tcp_pool: self.tcp_pool.clone(), + ssl_pool: self.ssl_pool.clone(), + } + } + } + + impl Service for InnerConnector + where + Io1: AsyncRead + AsyncWrite + 'static, + Io2: AsyncRead + AsyncWrite + 'static, + T1: Service< + Request = Connect, + Response = (Connect, Io1), + Error = ConnectorError, + >, + T2: Service< + Request = Connect, + Response = (Connect, Io2), + Error = ConnectorError, + >, + { + type Request = Connect; + type Response = IoEither, Connection>; + type Error = ConnectorError; + type Future = Either< + FutureResult, + Either< + InnerConnectorResponseA, + InnerConnectorResponseB, + >, + >; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.tcp_pool.poll_ready() + } + + fn call(&mut self, req: Self::Request) -> Self::Future { + if let Err(e) = req.validate() { + Either::A(err(e)) + } else if req.is_secure() { + Either::B(Either::A(InnerConnectorResponseA { + fut: self.tcp_pool.call(req), + _t: PhantomData, + })) + } else { + Either::B(Either::B(InnerConnectorResponseB { + fut: self.ssl_pool.call(req), + _t: PhantomData, + })) + } + } + } + + pub(crate) struct InnerConnectorResponseA + where + Io1: AsyncRead + AsyncWrite + 'static, + T: Service, + { + fut: as Service>::Future, + _t: PhantomData, + } + + impl Future for InnerConnectorResponseA + where + T: Service, + Io1: AsyncRead + AsyncWrite + 'static, + Io2: AsyncRead + AsyncWrite + 'static, + { + type Item = IoEither, Connection>; + type Error = ConnectorError; + + fn poll(&mut self) -> Poll { + match self.fut.poll()? { + Async::NotReady => Ok(Async::NotReady), + Async::Ready(res) => Ok(Async::Ready(IoEither::A(res))), + } + } + } + + pub(crate) struct InnerConnectorResponseB + where + Io2: AsyncRead + AsyncWrite + 'static, + T: Service, + { + fut: as Service>::Future, + _t: PhantomData, + } + + impl Future for InnerConnectorResponseB + where + T: Service, + Io1: AsyncRead + AsyncWrite + 'static, + Io2: AsyncRead + AsyncWrite + 'static, + { + type Item = IoEither, Connection>; + type Error = ConnectorError; + + fn poll(&mut self) -> Poll { + match self.fut.poll()? { + Async::NotReady => Ok(Async::NotReady), + Async::Ready(res) => Ok(Async::Ready(IoEither::B(res))), + } + } + } +} + +pub(crate) enum IoEither { + A(Io1), + B(Io2), +} + +impl io::Read for IoEither +where + Io1: io::Read, + Io2: io::Read, +{ + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match self { + IoEither::A(ref mut io) => io.read(buf), + IoEither::B(ref mut io) => io.read(buf), + } + } +} + +impl AsyncRead for IoEither +where + Io1: AsyncRead, + Io2: AsyncRead, +{ + unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { + match self { + IoEither::A(ref io) => io.prepare_uninitialized_buffer(buf), + IoEither::B(ref io) => io.prepare_uninitialized_buffer(buf), + } + } +} + +impl AsyncWrite for IoEither +where + Io1: AsyncWrite, + Io2: AsyncWrite, +{ + fn shutdown(&mut self) -> Poll<(), io::Error> { + match self { + IoEither::A(ref mut io) => io.shutdown(), + IoEither::B(ref mut io) => io.shutdown(), + } + } + + fn poll_write(&mut self, buf: &[u8]) -> Poll { + match self { + IoEither::A(ref mut io) => io.poll_write(buf), + IoEither::B(ref mut io) => io.poll_write(buf), + } + } + + fn poll_flush(&mut self) -> Poll<(), io::Error> { + match self { + IoEither::A(ref mut io) => io.poll_flush(), + IoEither::B(ref mut io) => io.poll_flush(), + } + } +} + +impl io::Write for IoEither +where + Io1: io::Write, + Io2: io::Write, +{ + fn flush(&mut self) -> io::Result<()> { + match self { + IoEither::A(ref mut io) => io.flush(), + IoEither::B(ref mut io) => io.flush(), + } + } + + fn write(&mut self, buf: &[u8]) -> io::Result { + match self { + IoEither::A(ref mut io) => io.write(buf), + IoEither::B(ref mut io) => io.write(buf), + } + } +} + +impl fmt::Debug for IoEither +where + Io1: fmt::Debug, + Io2: fmt::Debug, +{ + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + IoEither::A(ref io) => io.fmt(fmt), + IoEither::B(ref io) => io.fmt(fmt), + } + } +} diff --git a/src/client/error.rs b/src/client/error.rs new file mode 100644 index 000000000..ba6407230 --- /dev/null +++ b/src/client/error.rs @@ -0,0 +1,77 @@ +use std::io; + +use trust_dns_resolver::error::ResolveError; + +#[cfg(feature = "ssl")] +use openssl::ssl::Error as SslError; + +#[cfg(all( + feature = "tls", + not(any(feature = "ssl", feature = "rust-tls")) +))] +use native_tls::Error as SslError; + +#[cfg(all( + feature = "rust-tls", + not(any(feature = "tls", feature = "ssl")) +))] +use std::io::Error as SslError; + +/// A set of errors that can occur while connecting to an HTTP host +#[derive(Fail, Debug)] +pub enum ConnectorError { + /// Invalid URL + #[fail(display = "Invalid URL")] + InvalidUrl(InvalidUrlKind), + + /// SSL feature is not enabled + #[fail(display = "SSL is not supported")] + SslIsNotSupported, + + /// SSL error + #[cfg(any(feature = "tls", feature = "ssl", feature = "rust-tls"))] + #[fail(display = "{}", _0)] + SslError(#[cause] SslError), + + /// Failed to resolve the hostname + #[fail(display = "Failed resolving hostname: {}", _0)] + Resolver(ResolveError), + + /// No dns records + #[fail(display = "No dns records found for the input")] + NoRecords, + + /// Connecting took too long + #[fail(display = "Timeout out while establishing connection")] + Timeout, + + /// Connector has been disconnected + #[fail(display = "Internal error: connector has been disconnected")] + Disconnected, + + /// Connection io error + #[fail(display = "{}", _0)] + IoError(io::Error), +} + +#[derive(Fail, Debug)] +pub enum InvalidUrlKind { + #[fail(display = "Missing url scheme")] + MissingScheme, + #[fail(display = "Unknown url scheme")] + UnknownScheme, + #[fail(display = "Missing host name")] + MissingHost, +} + +impl From for ConnectorError { + fn from(err: io::Error) -> ConnectorError { + ConnectorError::IoError(err) + } +} + +impl From for ConnectorError { + fn from(err: ResolveError) -> ConnectorError { + ConnectorError::Resolver(err) + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs index a5582fea8..714e6c694 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,6 +1,14 @@ //! Http client api +mod connect; +mod connection; +mod connector; +mod error; +mod pool; mod request; mod response; +pub use self::connect::Connect; +pub use self::connector::Connector; +pub use self::error::{ConnectorError, InvalidUrlKind}; pub use self::request::{ClientRequest, ClientRequestBuilder}; pub use self::response::ClientResponse; diff --git a/src/client/pool.rs b/src/client/pool.rs new file mode 100644 index 000000000..6ff8c96ce --- /dev/null +++ b/src/client/pool.rs @@ -0,0 +1,579 @@ +use std::cell::RefCell; +use std::collections::{HashMap, VecDeque}; +use std::io; +use std::rc::Rc; +use std::time::{Duration, Instant}; + +use actix_net::service::Service; +use futures::future::{ok, Either, FutureResult}; +use futures::sync::oneshot; +use futures::task::AtomicTask; +use futures::{Async, Future, Poll}; +use http::uri::Authority; +use indexmap::IndexSet; +use slab::Slab; +use tokio_current_thread::spawn; +use tokio_io::{AsyncRead, AsyncWrite}; +use tokio_timer::{sleep, Delay}; + +use super::connect::Connect; +use super::connection::Connection; +use super::error::ConnectorError; + +#[derive(Hash, Eq, PartialEq, Clone, Debug)] +pub(crate) struct Key { + authority: Authority, +} + +impl From for Key { + fn from(authority: Authority) -> Key { + Key { authority } + } +} + +#[derive(Debug)] +struct AvailableConnection { + io: T, + used: Instant, + created: Instant, +} + +/// Connections pool +pub(crate) struct ConnectionPool( + T, + Rc>>, +); + +impl ConnectionPool +where + Io: AsyncRead + AsyncWrite + 'static, + T: Service, +{ + pub(crate) fn new( + connector: T, + conn_lifetime: Duration, + conn_keep_alive: Duration, + disconnect_timeout: Option, + limit: usize, + ) -> Self { + ConnectionPool( + connector, + Rc::new(RefCell::new(Inner { + conn_lifetime, + conn_keep_alive, + disconnect_timeout, + limit, + acquired: 0, + waiters: Slab::new(), + waiters_queue: IndexSet::new(), + available: HashMap::new(), + task: AtomicTask::new(), + })), + ) + } +} + +impl Clone for ConnectionPool +where + T: Clone, + Io: AsyncRead + AsyncWrite + 'static, +{ + fn clone(&self) -> Self { + ConnectionPool(self.0.clone(), self.1.clone()) + } +} + +impl Service for ConnectionPool +where + Io: AsyncRead + AsyncWrite + 'static, + T: Service, +{ + type Request = Connect; + type Response = Connection; + type Error = ConnectorError; + type Future = Either< + FutureResult, ConnectorError>, + Either, OpenConnection>, + >; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.0.poll_ready() + } + + fn call(&mut self, req: Self::Request) -> Self::Future { + let key = req.key(); + + // acquire connection + match self.1.as_ref().borrow_mut().acquire(&key) { + Acquire::Acquired(io, created) => { + // use existing connection + Either::A(ok(Connection::new( + io, + created, + Acquired(key, Some(self.1.clone())), + ))) + } + Acquire::NotAvailable => { + // connection is not available, wait + let (rx, token) = self.1.as_ref().borrow_mut().wait_for(req); + Either::B(Either::A(WaitForConnection { + rx, + key, + token, + inner: Some(self.1.clone()), + })) + } + Acquire::Available => { + // open new connection + Either::B(Either::B(OpenConnection::new( + key, + self.1.clone(), + self.0.call(req), + ))) + } + } + } +} + +#[doc(hidden)] +pub struct WaitForConnection +where + Io: AsyncRead + AsyncWrite + 'static, +{ + key: Key, + token: usize, + rx: oneshot::Receiver, ConnectorError>>, + inner: Option>>>, +} + +impl Drop for WaitForConnection +where + Io: AsyncRead + AsyncWrite + 'static, +{ + fn drop(&mut self) { + if let Some(i) = self.inner.take() { + let mut inner = i.as_ref().borrow_mut(); + inner.release_waiter(&self.key, self.token); + inner.check_availibility(); + } + } +} + +impl Future for WaitForConnection +where + Io: AsyncRead + AsyncWrite, +{ + type Item = Connection; + type Error = ConnectorError; + + fn poll(&mut self) -> Poll { + match self.rx.poll() { + Ok(Async::Ready(item)) => match item { + Err(err) => Err(err), + Ok(conn) => { + let _ = self.inner.take(); + Ok(Async::Ready(conn)) + } + }, + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(_) => { + let _ = self.inner.take(); + Err(ConnectorError::Disconnected) + } + } + } +} + +#[doc(hidden)] +pub struct OpenConnection +where + Io: AsyncRead + AsyncWrite + 'static, +{ + fut: F, + key: Key, + inner: Option>>>, +} + +impl OpenConnection +where + F: Future, + Io: AsyncRead + AsyncWrite + 'static, +{ + fn new(key: Key, inner: Rc>>, fut: F) -> Self { + OpenConnection { + key, + fut, + inner: Some(inner), + } + } +} + +impl Drop for OpenConnection +where + Io: AsyncRead + AsyncWrite + 'static, +{ + fn drop(&mut self) { + if let Some(inner) = self.inner.take() { + let mut inner = inner.as_ref().borrow_mut(); + inner.release(); + inner.check_availibility(); + } + } +} + +impl Future for OpenConnection +where + F: Future, + Io: AsyncRead + AsyncWrite, +{ + type Item = Connection; + type Error = ConnectorError; + + fn poll(&mut self) -> Poll { + match self.fut.poll() { + Err(err) => Err(err.into()), + Ok(Async::Ready((_, io))) => { + let _ = self.inner.take(); + Ok(Async::Ready(Connection::new( + io, + Instant::now(), + Acquired(self.key.clone(), self.inner.clone()), + ))) + } + Ok(Async::NotReady) => Ok(Async::NotReady), + } + } +} + +struct OpenWaitingConnection +where + Io: AsyncRead + AsyncWrite + 'static, +{ + fut: F, + key: Key, + rx: Option, ConnectorError>>>, + inner: Option>>>, +} + +impl OpenWaitingConnection +where + F: Future + 'static, + Io: AsyncRead + AsyncWrite + 'static, +{ + fn spawn( + key: Key, + rx: oneshot::Sender, ConnectorError>>, + inner: Rc>>, + fut: F, + ) { + spawn(OpenWaitingConnection { + key, + fut, + rx: Some(rx), + inner: Some(inner), + }) + } +} + +impl Drop for OpenWaitingConnection +where + Io: AsyncRead + AsyncWrite + 'static, +{ + fn drop(&mut self) { + if let Some(inner) = self.inner.take() { + let mut inner = inner.as_ref().borrow_mut(); + inner.release(); + inner.check_availibility(); + } + } +} + +impl Future for OpenWaitingConnection +where + F: Future, + Io: AsyncRead + AsyncWrite, +{ + type Item = (); + type Error = (); + + fn poll(&mut self) -> Poll { + match self.fut.poll() { + Err(err) => { + let _ = self.inner.take(); + if let Some(rx) = self.rx.take() { + let _ = rx.send(Err(err)); + } + Err(()) + } + Ok(Async::Ready((_, io))) => { + let _ = self.inner.take(); + if let Some(rx) = self.rx.take() { + let _ = rx.send(Ok(Connection::new( + io, + Instant::now(), + Acquired(self.key.clone(), self.inner.clone()), + ))); + } + Ok(Async::Ready(())) + } + Ok(Async::NotReady) => Ok(Async::NotReady), + } + } +} + +enum Acquire { + Acquired(T, Instant), + Available, + NotAvailable, +} + +pub(crate) struct Inner +where + Io: AsyncRead + AsyncWrite + 'static, +{ + conn_lifetime: Duration, + conn_keep_alive: Duration, + disconnect_timeout: Option, + limit: usize, + acquired: usize, + available: HashMap>>, + waiters: Slab<( + Connect, + oneshot::Sender, ConnectorError>>, + )>, + waiters_queue: IndexSet<(Key, usize)>, + task: AtomicTask, +} + +impl Inner +where + Io: AsyncRead + AsyncWrite + 'static, +{ + /// connection is not available, wait + fn wait_for( + &mut self, + connect: Connect, + ) -> ( + oneshot::Receiver, ConnectorError>>, + usize, + ) { + let (tx, rx) = oneshot::channel(); + + let key = connect.key(); + let entry = self.waiters.vacant_entry(); + let token = entry.key(); + entry.insert((connect, tx)); + assert!(!self.waiters_queue.insert((key, token))); + (rx, token) + } + + fn release_waiter(&mut self, key: &Key, token: usize) { + self.waiters.remove(token); + self.waiters_queue.remove(&(key.clone(), token)); + } + + fn acquire(&mut self, key: &Key) -> Acquire { + // check limits + if self.limit > 0 && self.acquired >= self.limit { + return Acquire::NotAvailable; + } + + self.reserve(); + + // check if open connection is available + // cleanup stale connections at the same time + if let Some(ref mut connections) = self.available.get_mut(key) { + let now = Instant::now(); + while let Some(conn) = connections.pop_back() { + // check if it still usable + if (now - conn.used) > self.conn_keep_alive + || (now - conn.created) > self.conn_lifetime + { + if let Some(timeout) = self.disconnect_timeout { + spawn(CloseConnection::new(conn.io, timeout)) + } + } else { + let mut io = conn.io; + let mut buf = [0; 2]; + match io.read(&mut buf) { + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => (), + Ok(n) if n > 0 => { + if let Some(timeout) = self.disconnect_timeout { + spawn(CloseConnection::new(io, timeout)) + } + continue; + } + Ok(_) | Err(_) => continue, + } + return Acquire::Acquired(io, conn.created); + } + } + } + Acquire::Available + } + + fn reserve(&mut self) { + self.acquired += 1; + } + + fn release(&mut self) { + self.acquired -= 1; + } + + fn release_conn(&mut self, key: &Key, io: Io, created: Instant) { + self.acquired -= 1; + self.available + .entry(key.clone()) + .or_insert_with(VecDeque::new) + .push_back(AvailableConnection { + io, + created, + used: Instant::now(), + }); + } + + fn release_close(&mut self, io: Io) { + self.acquired -= 1; + if let Some(timeout) = self.disconnect_timeout { + spawn(CloseConnection::new(io, timeout)) + } + } + + fn check_availibility(&self) { + if !self.waiters_queue.is_empty() && self.acquired < self.limit { + self.task.notify() + } + } +} + +struct ConnectorPoolSupport +where + Io: AsyncRead + AsyncWrite + 'static, +{ + connector: T, + inner: Rc>>, +} + +impl Future for ConnectorPoolSupport +where + Io: AsyncRead + AsyncWrite + 'static, + T: Service, + T::Future: 'static, +{ + type Item = (); + type Error = (); + + fn poll(&mut self) -> Poll { + let mut inner = self.inner.as_ref().borrow_mut(); + inner.task.register(); + + // check waiters + loop { + let (key, token) = { + if let Some((key, token)) = inner.waiters_queue.get_index(0) { + (key.clone(), *token) + } else { + break; + } + }; + match inner.acquire(&key) { + Acquire::NotAvailable => break, + Acquire::Acquired(io, created) => { + let (_, tx) = inner.waiters.remove(token); + if let Err(conn) = tx.send(Ok(Connection::new( + io, + created, + Acquired(key.clone(), Some(self.inner.clone())), + ))) { + let (io, created) = conn.unwrap().into_inner(); + inner.release_conn(&key, io, created); + } + } + Acquire::Available => { + let (connect, tx) = inner.waiters.remove(token); + OpenWaitingConnection::spawn( + key.clone(), + tx, + self.inner.clone(), + self.connector.call(connect), + ); + } + } + let _ = inner.waiters_queue.swap_remove_index(0); + } + + Ok(Async::NotReady) + } +} + +struct CloseConnection { + io: T, + timeout: Delay, +} + +impl CloseConnection +where + T: AsyncWrite, +{ + fn new(io: T, timeout: Duration) -> Self { + CloseConnection { + io, + timeout: sleep(timeout), + } + } +} + +impl Future for CloseConnection +where + T: AsyncWrite, +{ + type Item = (); + type Error = (); + + fn poll(&mut self) -> Poll<(), ()> { + match self.timeout.poll() { + Ok(Async::Ready(_)) | Err(_) => Ok(Async::Ready(())), + Ok(Async::NotReady) => match self.io.shutdown() { + Ok(Async::Ready(_)) | Err(_) => Ok(Async::Ready(())), + Ok(Async::NotReady) => Ok(Async::NotReady), + }, + } + } +} + +pub(crate) struct Acquired( + Key, + Option>>>, +); + +impl Acquired +where + T: AsyncRead + AsyncWrite + 'static, +{ + pub(crate) fn close(&mut self, conn: Connection) { + if let Some(inner) = self.1.take() { + let (io, _) = conn.into_inner(); + inner.as_ref().borrow_mut().release_close(io); + } + } + pub(crate) fn release(&mut self, conn: Connection) { + if let Some(inner) = self.1.take() { + let (io, created) = conn.into_inner(); + inner + .as_ref() + .borrow_mut() + .release_conn(&self.0, io, created); + } + } +} + +impl Drop for Acquired +where + T: AsyncRead + AsyncWrite + 'static, +{ + fn drop(&mut self) { + if let Some(inner) = self.1.take() { + inner.as_ref().borrow_mut().release(); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 572c23ae1..32369d167 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,6 +83,7 @@ extern crate cookie; extern crate encoding; extern crate http as modhttp; extern crate httparse; +extern crate indexmap; extern crate mime; extern crate net2; extern crate percent_encoding; @@ -90,18 +91,24 @@ extern crate rand; extern crate serde; extern crate serde_json; extern crate serde_urlencoded; +extern crate slab; extern crate tokio; extern crate tokio_codec; extern crate tokio_current_thread; extern crate tokio_io; extern crate tokio_tcp; extern crate tokio_timer; +extern crate trust_dns_proto; +extern crate trust_dns_resolver; extern crate url as urlcrate; #[cfg(test)] #[macro_use] extern crate serde_derive; +#[cfg(feature = "ssl")] +extern crate openssl; + mod body; pub mod client; mod config; From 550c5f55b68a1eb00793c71f0bdd41c7ac12e3dd Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 13 Nov 2018 22:53:30 -0800 Subject: [PATCH 061/427] add simple http client --- src/body.rs | 136 ++++++++++++++++++++++++++++- src/client/connect.rs | 12 +-- src/client/connection.rs | 4 +- src/client/connector.rs | 2 +- src/client/error.rs | 40 +++++++++ src/client/mod.rs | 6 +- src/client/pipeline.rs | 174 ++++++++++++++++++++++++++++++++++++ src/client/pool.rs | 67 +++++++------- src/client/request.rs | 184 ++++++++++++++++++++++++--------------- src/client/response.rs | 49 +++++++---- src/h1/client.rs | 60 ++++++------- src/h1/codec.rs | 14 ++- src/h1/decoder.rs | 8 +- src/h1/dispatcher.rs | 2 +- src/h1/encoder.rs | 4 +- src/h1/mod.rs | 11 ++- src/request.rs | 5 +- src/ws/client/service.rs | 7 +- tests/test_client.rs | 147 +++++++++++++++++++++++++++++++ 19 files changed, 745 insertions(+), 187 deletions(-) create mode 100644 src/client/pipeline.rs create mode 100644 tests/test_client.rs diff --git a/src/body.rs b/src/body.rs index c78ea8172..e001273c4 100644 --- a/src/body.rs +++ b/src/body.rs @@ -1,13 +1,39 @@ -use bytes::{Bytes, BytesMut}; -use futures::Stream; use std::sync::Arc; use std::{fmt, mem}; +use bytes::{Bytes, BytesMut}; +use futures::{Async, Poll, Stream}; + use error::Error; /// Type represent streaming body pub type BodyStream = Box>; +/// Different type of bory +pub enum BodyType { + None, + Zero, + Sized(usize), + Unsized, +} + +/// Type that provides this trait can be streamed to a peer. +pub trait MessageBody { + fn tp(&self) -> BodyType; + + fn poll_next(&mut self) -> Poll, Error>; +} + +impl MessageBody for () { + fn tp(&self) -> BodyType { + BodyType::Zero + } + + fn poll_next(&mut self) -> Poll, Error> { + Ok(Async::Ready(None)) + } +} + /// Represents various types of http message body. pub enum Body { /// Empty response. `Content-Length` header is set to `0` @@ -241,6 +267,112 @@ impl AsRef<[u8]> for Binary { } } +impl MessageBody for Bytes { + fn tp(&self) -> BodyType { + BodyType::Sized(self.len()) + } + + fn poll_next(&mut self) -> Poll, Error> { + if self.is_empty() { + Ok(Async::Ready(None)) + } else { + Ok(Async::Ready(Some(mem::replace(self, Bytes::new())))) + } + } +} + +impl MessageBody for &'static str { + fn tp(&self) -> BodyType { + BodyType::Sized(self.len()) + } + + fn poll_next(&mut self) -> Poll, Error> { + if self.is_empty() { + Ok(Async::Ready(None)) + } else { + Ok(Async::Ready(Some(Bytes::from_static( + mem::replace(self, "").as_ref(), + )))) + } + } +} + +impl MessageBody for &'static [u8] { + fn tp(&self) -> BodyType { + BodyType::Sized(self.len()) + } + + fn poll_next(&mut self) -> Poll, Error> { + if self.is_empty() { + Ok(Async::Ready(None)) + } else { + Ok(Async::Ready(Some(Bytes::from_static(mem::replace( + self, b"", + ))))) + } + } +} + +impl MessageBody for Vec { + fn tp(&self) -> BodyType { + BodyType::Sized(self.len()) + } + + fn poll_next(&mut self) -> Poll, Error> { + if self.is_empty() { + Ok(Async::Ready(None)) + } else { + Ok(Async::Ready(Some(Bytes::from(mem::replace( + self, + Vec::new(), + ))))) + } + } +} + +impl MessageBody for String { + fn tp(&self) -> BodyType { + BodyType::Sized(self.len()) + } + + fn poll_next(&mut self) -> Poll, Error> { + if self.is_empty() { + Ok(Async::Ready(None)) + } else { + Ok(Async::Ready(Some(Bytes::from( + mem::replace(self, String::new()).into_bytes(), + )))) + } + } +} + +#[doc(hidden)] +pub struct MessageBodyStream { + stream: S, +} + +impl MessageBodyStream +where + S: Stream, +{ + pub fn new(stream: S) -> Self { + MessageBodyStream { stream } + } +} + +impl MessageBody for MessageBodyStream +where + S: Stream, +{ + fn tp(&self) -> BodyType { + BodyType::Unsized + } + + fn poll_next(&mut self) -> Poll, Error> { + self.stream.poll() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/client/connect.rs b/src/client/connect.rs index 40c3e8ec9..a445228e3 100644 --- a/src/client/connect.rs +++ b/src/client/connect.rs @@ -14,8 +14,13 @@ pub struct Connect { } impl Connect { + /// Create `Connect` message for specified `Uri` + pub fn new(uri: Uri) -> Connect { + Connect { uri } + } + /// Construct `Uri` instance and create `Connect` message. - pub fn new(uri: U) -> Result + pub fn try_from(uri: U) -> Result where Uri: HttpTryFrom, { @@ -24,11 +29,6 @@ impl Connect { }) } - /// Create `Connect` message for specified `Uri` - pub fn with(uri: Uri) -> Connect { - Connect { uri } - } - pub(crate) fn is_secure(&self) -> bool { if let Some(scheme) = self.uri.scheme_part() { scheme.as_str() == "https" diff --git a/src/client/connection.rs b/src/client/connection.rs index 294e100c8..eec64267a 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -6,7 +6,7 @@ use tokio_io::{AsyncRead, AsyncWrite}; use super::pool::Acquired; /// HTTP client connection -pub struct Connection { +pub struct Connection { io: T, created: time::Instant, pool: Option>, @@ -14,7 +14,7 @@ pub struct Connection { impl fmt::Debug for Connection where - T: AsyncRead + AsyncWrite + fmt::Debug + 'static, + T: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Connection {:?}", self.io) diff --git a/src/client/connector.rs b/src/client/connector.rs index 97da074d1..1eae135f2 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -130,7 +130,7 @@ impl Connector { self, ) -> impl Service< Request = Connect, - Response = impl AsyncRead + AsyncWrite + fmt::Debug, + Response = Connection, Error = ConnectorError, > + Clone { #[cfg(not(feature = "ssl"))] diff --git a/src/client/error.rs b/src/client/error.rs index ba6407230..2c4753642 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -17,6 +17,8 @@ use native_tls::Error as SslError; ))] use std::io::Error as SslError; +use error::{Error, ParseError}; + /// A set of errors that can occur while connecting to an HTTP host #[derive(Fail, Debug)] pub enum ConnectorError { @@ -75,3 +77,41 @@ impl From for ConnectorError { ConnectorError::Resolver(err) } } + +/// A set of errors that can occur during request sending and response reading +#[derive(Debug)] +pub enum SendRequestError { + /// Failed to connect to host + // #[fail(display = "Failed to connect to host: {}", _0)] + Connector(ConnectorError), + /// Error sending request + Send(io::Error), + /// Error parsing response + Response(ParseError), + /// Error sending request body + Body(Error), +} + +impl From for SendRequestError { + fn from(err: io::Error) -> SendRequestError { + SendRequestError::Send(err) + } +} + +impl From for SendRequestError { + fn from(err: ConnectorError) -> SendRequestError { + SendRequestError::Connector(err) + } +} + +impl From for SendRequestError { + fn from(err: ParseError) -> SendRequestError { + SendRequestError::Response(err) + } +} + +impl From for SendRequestError { + fn from(err: Error) -> SendRequestError { + SendRequestError::Body(err) + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs index 714e6c694..da0cbc670 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -3,12 +3,14 @@ mod connect; mod connection; mod connector; mod error; +mod pipeline; mod pool; mod request; mod response; pub use self::connect::Connect; +pub use self::connection::Connection; pub use self::connector::Connector; -pub use self::error::{ConnectorError, InvalidUrlKind}; -pub use self::request::{ClientRequest, ClientRequestBuilder}; +pub use self::error::{ConnectorError, InvalidUrlKind, SendRequestError}; +pub use self::request::{ClientRequest, ClientRequestBuilder, RequestHead}; pub use self::response::ClientResponse; diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs new file mode 100644 index 000000000..fff900131 --- /dev/null +++ b/src/client/pipeline.rs @@ -0,0 +1,174 @@ +use std::collections::VecDeque; + +use actix_net::codec::Framed; +use actix_net::service::Service; +use bytes::Bytes; +use futures::future::{err, ok, Either}; +use futures::{Async, Future, Poll, Sink, Stream}; +use tokio_io::{AsyncRead, AsyncWrite}; + +use super::error::{ConnectorError, SendRequestError}; +use super::request::RequestHead; +use super::response::ClientResponse; +use super::{Connect, Connection}; +use body::{BodyStream, BodyType, MessageBody}; +use error::Error; +use h1; + +pub fn send_request( + head: RequestHead, + body: B, + connector: &mut T, +) -> impl Future +where + T: Service, Error = ConnectorError>, + B: MessageBody, + Io: AsyncRead + AsyncWrite + 'static, +{ + let tp = body.tp(); + + connector + .call(Connect::new(head.uri.clone())) + .from_err() + .map(|io| Framed::new(io, h1::ClientCodec::default())) + .and_then(|framed| framed.send((head, tp).into()).from_err()) + .and_then(move |framed| match body.tp() { + BodyType::None | BodyType::Zero => Either::A(ok(framed)), + _ => Either::B(SendBody::new(body, framed)), + }).and_then(|framed| { + framed + .into_future() + .map_err(|(e, _)| SendRequestError::from(e)) + .and_then(|(item, framed)| { + if let Some(item) = item { + let mut res = item.into_item().unwrap(); + match framed.get_codec().message_type() { + h1::MessageType::None => release_connection(framed), + _ => res.payload = Some(Payload::stream(framed)), + } + ok(res) + } else { + err(ConnectorError::Disconnected.into()) + } + }) + }) +} + +struct SendBody { + body: Option, + framed: Option, h1::ClientCodec>>, + write_buf: VecDeque>, + flushed: bool, +} + +impl SendBody +where + Io: AsyncRead + AsyncWrite + 'static, + B: MessageBody, +{ + fn new(body: B, framed: Framed, h1::ClientCodec>) -> Self { + SendBody { + body: Some(body), + framed: Some(framed), + write_buf: VecDeque::new(), + flushed: true, + } + } +} + +impl Future for SendBody +where + Io: AsyncRead + AsyncWrite + 'static, + B: MessageBody, +{ + type Item = Framed, h1::ClientCodec>; + type Error = SendRequestError; + + fn poll(&mut self) -> Poll { + let mut body_ready = true; + loop { + while body_ready + && self.body.is_some() + && !self.framed.as_ref().unwrap().is_write_buf_full() + { + match self.body.as_mut().unwrap().poll_next()? { + Async::Ready(None) => { + self.flushed = false; + self.framed + .as_mut() + .unwrap() + .start_send(h1::Message::Chunk(None))?; + break; + } + Async::Ready(Some(chunk)) => { + self.flushed = false; + self.framed + .as_mut() + .unwrap() + .start_send(h1::Message::Chunk(Some(chunk)))?; + } + Async::NotReady => body_ready = false, + } + } + + if !self.flushed { + match self.framed.as_mut().unwrap().poll_complete()? { + Async::Ready(_) => { + self.flushed = true; + continue; + } + Async::NotReady => return Ok(Async::NotReady), + } + } + + if self.body.is_none() { + return Ok(Async::Ready(self.framed.take().unwrap())); + } + return Ok(Async::NotReady); + } + } +} + +struct Payload { + framed: Option, h1::ClientCodec>>, +} + +impl Payload { + fn stream(framed: Framed, h1::ClientCodec>) -> BodyStream { + Box::new(Payload { + framed: Some(framed), + }) + } +} + +impl Stream for Payload { + type Item = Bytes; + type Error = Error; + + fn poll(&mut self) -> Poll, Error> { + match self.framed.as_mut().unwrap().poll()? { + Async::NotReady => Ok(Async::NotReady), + Async::Ready(Some(chunk)) => match chunk { + h1::Message::Chunk(Some(chunk)) => Ok(Async::Ready(Some(chunk))), + h1::Message::Chunk(None) => { + release_connection(self.framed.take().unwrap()); + Ok(Async::Ready(None)) + } + h1::Message::Item(_) => unreachable!(), + }, + Async::Ready(None) => Ok(Async::Ready(None)), + } + } +} + +fn release_connection(framed: Framed, h1::ClientCodec>) +where + Io: AsyncRead + AsyncWrite + 'static, +{ + let parts = framed.into_parts(); + if parts.read_buf.is_empty() && parts.write_buf.is_empty() { + parts.io.release() + } else { + parts.io.close() + } +} diff --git a/src/client/pool.rs b/src/client/pool.rs index 6ff8c96ce..25296a6dd 100644 --- a/src/client/pool.rs +++ b/src/client/pool.rs @@ -327,10 +327,7 @@ enum Acquire { NotAvailable, } -pub(crate) struct Inner -where - Io: AsyncRead + AsyncWrite + 'static, -{ +pub(crate) struct Inner { conn_lifetime: Duration, conn_keep_alive: Duration, disconnect_timeout: Option, @@ -345,6 +342,33 @@ where task: AtomicTask, } +impl Inner { + fn reserve(&mut self) { + self.acquired += 1; + } + + fn release(&mut self) { + self.acquired -= 1; + } + + fn release_waiter(&mut self, key: &Key, token: usize) { + self.waiters.remove(token); + self.waiters_queue.remove(&(key.clone(), token)); + } + + fn release_conn(&mut self, key: &Key, io: Io, created: Instant) { + self.acquired -= 1; + self.available + .entry(key.clone()) + .or_insert_with(VecDeque::new) + .push_back(AvailableConnection { + io, + created, + used: Instant::now(), + }); + } +} + impl Inner where Io: AsyncRead + AsyncWrite + 'static, @@ -367,11 +391,6 @@ where (rx, token) } - fn release_waiter(&mut self, key: &Key, token: usize) { - self.waiters.remove(token); - self.waiters_queue.remove(&(key.clone(), token)); - } - fn acquire(&mut self, key: &Key) -> Acquire { // check limits if self.limit > 0 && self.acquired >= self.limit { @@ -412,26 +431,6 @@ where Acquire::Available } - fn reserve(&mut self) { - self.acquired += 1; - } - - fn release(&mut self) { - self.acquired -= 1; - } - - fn release_conn(&mut self, key: &Key, io: Io, created: Instant) { - self.acquired -= 1; - self.available - .entry(key.clone()) - .or_insert_with(VecDeque::new) - .push_back(AvailableConnection { - io, - created, - used: Instant::now(), - }); - } - fn release_close(&mut self, io: Io) { self.acquired -= 1; if let Some(timeout) = self.disconnect_timeout { @@ -541,10 +540,7 @@ where } } -pub(crate) struct Acquired( - Key, - Option>>>, -); +pub(crate) struct Acquired(Key, Option>>>); impl Acquired where @@ -567,10 +563,7 @@ where } } -impl Drop for Acquired -where - T: AsyncRead + AsyncWrite + 'static, -{ +impl Drop for Acquired { fn drop(&mut self) { if let Some(inner) = self.1.take() { inner.as_ref().borrow_mut().release(); diff --git a/src/client/request.rs b/src/client/request.rs index 8c7949336..603374135 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -2,17 +2,25 @@ use std::fmt; use std::fmt::Write as FmtWrite; use std::io::Write; -use bytes::{BufMut, BytesMut}; +use actix_net::service::Service; +use bytes::{BufMut, Bytes, BytesMut}; use cookie::{Cookie, CookieJar}; +use futures::{Future, Stream}; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; +use tokio_io::{AsyncRead, AsyncWrite}; use urlcrate::Url; +use body::{MessageBody, MessageBodyStream}; +use error::Error; use header::{self, Header, IntoHeaderValue}; use http::{ uri, Error as HttpError, HeaderMap, HeaderName, HeaderValue, HttpTryFrom, Method, Uri, Version, }; +use super::response::ClientResponse; +use super::{pipeline, Connect, Connection, ConnectorError, SendRequestError}; + /// An HTTP Client Request /// /// ```rust @@ -38,29 +46,40 @@ use http::{ /// ); /// } /// ``` -pub struct ClientRequest { - uri: Uri, - method: Method, - version: Version, - headers: HeaderMap, - chunked: bool, - upgrade: bool, +pub struct ClientRequest { + head: RequestHead, + body: B, } -impl Default for ClientRequest { - fn default() -> ClientRequest { - ClientRequest { +pub struct RequestHead { + pub uri: Uri, + pub method: Method, + pub version: Version, + pub headers: HeaderMap, +} + +impl Default for RequestHead { + fn default() -> RequestHead { + RequestHead { uri: Uri::default(), method: Method::default(), version: Version::HTTP_11, headers: HeaderMap::with_capacity(16), - chunked: false, - upgrade: false, } } } -impl ClientRequest { +impl ClientRequest<()> { + /// Create client request builder + pub fn build() -> ClientRequestBuilder { + ClientRequestBuilder { + head: Some(RequestHead::default()), + err: None, + cookies: None, + default_headers: true, + } + } + /// Create request builder for `GET` request pub fn get>(uri: U) -> ClientRequestBuilder { let mut builder = ClientRequest::build(); @@ -97,87 +116,90 @@ impl ClientRequest { } } -impl ClientRequest { - /// Create client request builder - pub fn build() -> ClientRequestBuilder { - ClientRequestBuilder { - request: Some(ClientRequest::default()), - err: None, - cookies: None, - default_headers: true, - } - } - +impl ClientRequest +where + B: MessageBody, +{ /// Get the request URI #[inline] pub fn uri(&self) -> &Uri { - &self.uri + &self.head.uri } /// Set client request URI #[inline] pub fn set_uri(&mut self, uri: Uri) { - self.uri = uri + self.head.uri = uri } /// Get the request method #[inline] pub fn method(&self) -> &Method { - &self.method + &self.head.method } /// Set HTTP `Method` for the request #[inline] pub fn set_method(&mut self, method: Method) { - self.method = method + self.head.method = method } /// Get HTTP version for the request #[inline] pub fn version(&self) -> Version { - self.version + self.head.version } /// Set http `Version` for the request #[inline] pub fn set_version(&mut self, version: Version) { - self.version = version + self.head.version = version } /// Get the headers from the request #[inline] pub fn headers(&self) -> &HeaderMap { - &self.headers + &self.head.headers } /// Get a mutable reference to the headers #[inline] pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.headers + &mut self.head.headers } - /// is chunked encoding enabled - #[inline] - pub fn chunked(&self) -> bool { - self.chunked + /// Deconstruct ClientRequest to a RequestHead and body tuple + pub fn into_parts(self) -> (RequestHead, B) { + (self.head, self.body) } - /// is upgrade request - #[inline] - pub fn upgrade(&self) -> bool { - self.upgrade + // Send request + /// + /// This method returns a future that resolves to a ClientResponse + pub fn send( + self, + connector: &mut T, + ) -> impl Future + where + T: Service, Error = ConnectorError>, + Io: AsyncRead + AsyncWrite + 'static, + { + pipeline::send_request(self.head, self.body, connector) } } -impl fmt::Debug for ClientRequest { +impl fmt::Debug for ClientRequest +where + B: MessageBody, +{ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!( f, "\nClientRequest {:?} {}:{}", - self.version, self.method, self.uri + self.head.version, self.head.method, self.head.uri )?; writeln!(f, " headers:")?; - for (key, val) in self.headers.iter() { + for (key, val) in self.head.headers.iter() { writeln!(f, " {:?}: {:?}", key, val)?; } Ok(()) @@ -189,7 +211,7 @@ impl fmt::Debug for ClientRequest { /// This type can be used to construct an instance of `ClientRequest` through a /// builder-like pattern. pub struct ClientRequestBuilder { - request: Option, + head: Option, err: Option, cookies: Option, default_headers: bool, @@ -208,7 +230,7 @@ impl ClientRequestBuilder { fn _uri(&mut self, url: &str) -> &mut Self { match Uri::try_from(url) { Ok(uri) => { - if let Some(parts) = parts(&mut self.request, &self.err) { + if let Some(parts) = parts(&mut self.head, &self.err) { parts.uri = uri; } } @@ -220,7 +242,7 @@ impl ClientRequestBuilder { /// Set HTTP method of this request. #[inline] pub fn method(&mut self, method: Method) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { + if let Some(parts) = parts(&mut self.head, &self.err) { parts.method = method; } self @@ -229,7 +251,7 @@ impl ClientRequestBuilder { /// Set HTTP method of this request. #[inline] pub fn get_method(&mut self) -> &Method { - let parts = self.request.as_ref().expect("cannot reuse request builder"); + let parts = self.head.as_ref().expect("cannot reuse request builder"); &parts.method } @@ -238,7 +260,7 @@ impl ClientRequestBuilder { /// By default requests's HTTP version depends on network stream #[inline] pub fn version(&mut self, version: Version) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { + if let Some(parts) = parts(&mut self.head, &self.err) { parts.version = version; } self @@ -263,7 +285,7 @@ impl ClientRequestBuilder { /// ``` #[doc(hidden)] pub fn set(&mut self, hdr: H) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { + if let Some(parts) = parts(&mut self.head, &self.err) { match hdr.try_into() { Ok(value) => { parts.headers.insert(H::name(), value); @@ -299,7 +321,7 @@ impl ClientRequestBuilder { HeaderName: HttpTryFrom, V: IntoHeaderValue, { - if let Some(parts) = parts(&mut self.request, &self.err) { + if let Some(parts) = parts(&mut self.head, &self.err) { match HeaderName::try_from(key) { Ok(key) => match value.try_into() { Ok(value) => { @@ -319,7 +341,7 @@ impl ClientRequestBuilder { HeaderName: HttpTryFrom, V: IntoHeaderValue, { - if let Some(parts) = parts(&mut self.request, &self.err) { + if let Some(parts) = parts(&mut self.head, &self.err) { match HeaderName::try_from(key) { Ok(key) => match value.try_into() { Ok(value) => { @@ -339,7 +361,7 @@ impl ClientRequestBuilder { HeaderName: HttpTryFrom, V: IntoHeaderValue, { - if let Some(parts) = parts(&mut self.request, &self.err) { + if let Some(parts) = parts(&mut self.head, &self.err) { match HeaderName::try_from(key) { Ok(key) => if !parts.headers.contains_key(&key) { match value.try_into() { @@ -357,11 +379,12 @@ impl ClientRequestBuilder { /// Enable connection upgrade #[inline] - pub fn upgrade(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.upgrade = true; - } - self + pub fn upgrade(&mut self, value: V) -> &mut Self + where + V: IntoHeaderValue, + { + self.set_header(header::UPGRADE, value) + .set_header(header::CONNECTION, "upgrade") } /// Set request's content type @@ -370,7 +393,7 @@ impl ClientRequestBuilder { where HeaderValue: HttpTryFrom, { - if let Some(parts) = parts(&mut self.request, &self.err) { + if let Some(parts) = parts(&mut self.head, &self.err) { match HeaderValue::try_from(value) { Ok(value) => { parts.headers.insert(header::CONTENT_TYPE, value); @@ -454,14 +477,17 @@ impl ClientRequestBuilder { /// Set a body and generate `ClientRequest`. /// /// `ClientRequestBuilder` can not be used after this call. - pub fn finish(&mut self) -> Result { + pub fn body( + &mut self, + body: B, + ) -> Result, HttpError> { if let Some(e) = self.err.take() { return Err(e); } if self.default_headers { // enable br only for https - let https = if let Some(parts) = parts(&mut self.request, &self.err) { + let https = if let Some(parts) = parts(&mut self.head, &self.err) { parts .uri .scheme_part() @@ -478,7 +504,7 @@ impl ClientRequestBuilder { } // set request host header - if let Some(parts) = parts(&mut self.request, &self.err) { + if let Some(parts) = parts(&mut self.head, &self.err) { if let Some(host) = parts.uri.host() { if !parts.headers.contains_key(header::HOST) { let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); @@ -505,7 +531,7 @@ impl ClientRequestBuilder { ); } - let mut request = self.request.take().expect("cannot reuse request builder"); + let mut head = self.head.take().expect("cannot reuse request builder"); // set cookies if let Some(ref mut jar) = self.cookies { @@ -515,18 +541,38 @@ impl ClientRequestBuilder { let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); let _ = write!(&mut cookie, "; {}={}", name, value); } - request.headers.insert( + head.headers.insert( header::COOKIE, HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), ); } - Ok(request) + Ok(ClientRequest { head, body }) + } + + /// Set an streaming body and generate `ClientRequest`. + /// + /// `ClientRequestBuilder` can not be used after this call. + pub fn stream( + &mut self, + stream: S, + ) -> Result, HttpError> + where + S: Stream, + { + self.body(MessageBodyStream::new(stream)) + } + + /// Set an empty body and generate `ClientRequest`. + /// + /// `ClientRequestBuilder` can not be used after this call. + pub fn finish(&mut self) -> Result, HttpError> { + self.body(()) } /// This method construct new `ClientRequestBuilder` pub fn take(&mut self) -> ClientRequestBuilder { ClientRequestBuilder { - request: self.request.take(), + head: self.head.take(), err: self.err.take(), cookies: self.cookies.take(), default_headers: self.default_headers, @@ -536,9 +582,9 @@ impl ClientRequestBuilder { #[inline] fn parts<'a>( - parts: &'a mut Option, + parts: &'a mut Option, err: &Option, -) -> Option<&'a mut ClientRequest> { +) -> Option<&'a mut RequestHead> { if err.is_some() { return None; } @@ -547,7 +593,7 @@ fn parts<'a>( impl fmt::Debug for ClientRequestBuilder { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(ref parts) = self.request { + if let Some(ref parts) = self.head { writeln!( f, "\nClientRequestBuilder {:?} {}:{}", diff --git a/src/client/response.rs b/src/client/response.rs index 627a1c78e..0d5a87a0d 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -2,35 +2,38 @@ use std::cell::{Cell, Ref, RefCell, RefMut}; use std::fmt; use std::rc::Rc; +use bytes::Bytes; +use futures::{Async, Poll, Stream}; use http::{HeaderMap, Method, StatusCode, Version}; +use body::BodyStream; +use error::Error; use extensions::Extensions; -use httpmessage::HttpMessage; -use payload::Payload; use request::{Message, MessageFlags, MessagePool}; use uri::Url; /// Client Response pub struct ClientResponse { pub(crate) inner: Rc, + pub(crate) payload: Option, } -impl HttpMessage for ClientResponse { - type Stream = Payload; +// impl HttpMessage for ClientResponse { +// type Stream = Payload; - fn headers(&self) -> &HeaderMap { - &self.inner.headers - } +// fn headers(&self) -> &HeaderMap { +// &self.inner.headers +// } - #[inline] - fn payload(&self) -> Payload { - if let Some(payload) = self.inner.payload.borrow_mut().take() { - payload - } else { - Payload::empty() - } - } -} +// #[inline] +// fn payload(&self) -> Payload { +// if let Some(payload) = self.inner.payload.borrow_mut().take() { +// payload +// } else { +// Payload::empty() +// } +// } +// } impl ClientResponse { /// Create new Request instance @@ -52,6 +55,7 @@ impl ClientResponse { payload: RefCell::new(None), extensions: RefCell::new(Extensions::new()), }), + payload: None, } } @@ -108,6 +112,19 @@ impl ClientResponse { } } +impl Stream for ClientResponse { + type Item = Bytes; + type Error = Error; + + fn poll(&mut self) -> Poll, Error> { + if let Some(ref mut payload) = self.payload { + payload.poll() + } else { + Ok(Async::Ready(None)) + } + } +} + impl Drop for ClientResponse { fn drop(&mut self) { if Rc::strong_count(&self.inner) == 1 { diff --git a/src/h1/client.rs b/src/h1/client.rs index b55af185f..9ace98e0e 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -7,12 +7,14 @@ use tokio_codec::{Decoder, Encoder}; use super::decoder::{PayloadDecoder, PayloadItem, PayloadType, ResponseDecoder}; use super::encoder::{RequestEncoder, ResponseLength}; use super::{Message, MessageType}; -use body::{Binary, Body}; -use client::{ClientRequest, ClientResponse}; +use body::{Binary, Body, BodyType}; +use client::{ClientResponse, RequestHead}; use config::ServiceConfig; use error::ParseError; use helpers; -use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; +use http::header::{ + HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE, +}; use http::{Method, Version}; use request::MessagePool; @@ -22,7 +24,7 @@ bitflags! { const UPGRADE = 0b0000_0010; const KEEPALIVE = 0b0000_0100; const KEEPALIVE_ENABLED = 0b0000_1000; - const UNHANDLED = 0b0001_0000; + const STREAM = 0b0001_0000; } } @@ -86,8 +88,8 @@ impl ClientCodec { /// Check last request's message type pub fn message_type(&self) -> MessageType { - if self.flags.contains(Flags::UNHANDLED) { - MessageType::Unhandled + if self.flags.contains(Flags::STREAM) { + MessageType::Stream } else if self.payload.is_none() { MessageType::None } else { @@ -96,38 +98,31 @@ impl ClientCodec { } /// prepare transfer encoding - pub fn prepare_te(&mut self, res: &mut ClientRequest) { + pub fn prepare_te(&mut self, head: &mut RequestHead, btype: BodyType) { self.te - .update(res, self.flags.contains(Flags::HEAD), self.version); + .update(head, self.flags.contains(Flags::HEAD), self.version); } fn encode_response( &mut self, - msg: ClientRequest, + msg: RequestHead, + btype: BodyType, buffer: &mut BytesMut, ) -> io::Result<()> { - // Connection upgrade - if msg.upgrade() { - self.flags.insert(Flags::UPGRADE); - } - // render message { // status line writeln!( Writer(buffer), "{} {} {:?}\r", - msg.method(), - msg.uri() - .path_and_query() - .map(|u| u.as_str()) - .unwrap_or("/"), - msg.version() + msg.method, + msg.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"), + msg.version ).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; // write headers - buffer.reserve(msg.headers().len() * AVERAGE_HEADER_SIZE); - for (key, value) in msg.headers() { + buffer.reserve(msg.headers.len() * AVERAGE_HEADER_SIZE); + for (key, value) in &msg.headers { let v = value.as_ref(); let k = key.as_str().as_bytes(); buffer.reserve(k.len() + v.len() + 4); @@ -135,10 +130,15 @@ impl ClientCodec { buffer.put_slice(b": "); buffer.put_slice(v); buffer.put_slice(b"\r\n"); + + // Connection upgrade + if key == UPGRADE { + self.flags.insert(Flags::UPGRADE); + } } // set date header - if !msg.headers().contains_key(DATE) { + if !msg.headers.contains_key(DATE) { self.config.set_date(buffer); } else { buffer.extend_from_slice(b"\r\n"); @@ -160,8 +160,6 @@ impl Decoder for ClientCodec { Some(PayloadItem::Eof) => Some(Message::Chunk(None)), None => None, }) - } else if self.flags.contains(Flags::UNHANDLED) { - Ok(None) } else if let Some((req, payload)) = self.decoder.decode(src)? { self.flags .set(Flags::HEAD, req.inner.method == Method::HEAD); @@ -172,9 +170,9 @@ impl Decoder for ClientCodec { match payload { PayloadType::None => self.payload = None, PayloadType::Payload(pl) => self.payload = Some(pl), - PayloadType::Unhandled => { - self.payload = None; - self.flags.insert(Flags::UNHANDLED); + PayloadType::Stream(pl) => { + self.payload = Some(pl); + self.flags.insert(Flags::STREAM); } }; Ok(Some(Message::Item(req))) @@ -185,7 +183,7 @@ impl Decoder for ClientCodec { } impl Encoder for ClientCodec { - type Item = Message; + type Item = Message<(RequestHead, BodyType)>; type Error = io::Error; fn encode( @@ -194,8 +192,8 @@ impl Encoder for ClientCodec { dst: &mut BytesMut, ) -> Result<(), Self::Error> { match item { - Message::Item(res) => { - self.encode_response(res, dst)?; + Message::Item((msg, btype)) => { + self.encode_response(msg, btype, dst)?; } Message::Chunk(Some(bytes)) => { self.te.encode(bytes.as_ref(), dst)?; diff --git a/src/h1/codec.rs b/src/h1/codec.rs index dd1e27e08..44a1b81fc 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -23,7 +23,7 @@ bitflags! { const UPGRADE = 0b0000_0010; const KEEPALIVE = 0b0000_0100; const KEEPALIVE_ENABLED = 0b0000_1000; - const UNHANDLED = 0b0001_0000; + const STREAM = 0b0001_0000; } } @@ -93,8 +93,8 @@ impl Codec { /// Check last request's message type pub fn message_type(&self) -> MessageType { - if self.flags.contains(Flags::UNHANDLED) { - MessageType::Unhandled + if self.flags.contains(Flags::STREAM) { + MessageType::Stream } else if self.payload.is_none() { MessageType::None } else { @@ -259,8 +259,6 @@ impl Decoder for Codec { } None => None, }) - } else if self.flags.contains(Flags::UNHANDLED) { - Ok(None) } else if let Some((req, payload)) = self.decoder.decode(src)? { self.flags .set(Flags::HEAD, req.inner.method == Method::HEAD); @@ -271,9 +269,9 @@ impl Decoder for Codec { match payload { PayloadType::None => self.payload = None, PayloadType::Payload(pl) => self.payload = Some(pl), - PayloadType::Unhandled => { - self.payload = None; - self.flags.insert(Flags::UNHANDLED); + PayloadType::Stream(pl) => { + self.payload = Some(pl); + self.flags.insert(Flags::STREAM); } } Ok(Some(Message::Item(req))) diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 472e29936..f2a3ee3f7 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -25,7 +25,7 @@ pub struct ResponseDecoder(&'static MessagePool); pub enum PayloadType { None, Payload(PayloadDecoder), - Unhandled, + Stream(PayloadDecoder), } impl RequestDecoder { @@ -174,7 +174,7 @@ impl Decoder for RequestDecoder { PayloadType::Payload(PayloadDecoder::length(len)) } else if has_upgrade || msg.inner.method == Method::CONNECT { // upgrade(websocket) or connect - PayloadType::Unhandled + PayloadType::Stream(PayloadDecoder::eof()) } else if src.len() >= MAX_BUFFER_SIZE { error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); return Err(ParseError::TooLarge); @@ -321,7 +321,7 @@ impl Decoder for ResponseDecoder { || msg.inner.method == Method::CONNECT { // switching protocol or connect - PayloadType::Unhandled + PayloadType::Stream(PayloadDecoder::eof()) } else if src.len() >= MAX_BUFFER_SIZE { error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); return Err(ParseError::TooLarge); @@ -667,7 +667,7 @@ mod tests { fn is_unhandled(&self) -> bool { match self { - PayloadType::Unhandled => true, + PayloadType::Stream(_) => true, _ => false, } } diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index f4dfdb219..b23af9648 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -344,7 +344,7 @@ where *req.inner.payload.borrow_mut() = Some(pl); self.payload = Some(ps); } - MessageType::Unhandled => { + MessageType::Stream => { self.unhandled = Some(req); return Ok(updated); } diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index fc0a3a1b2..caad113d9 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -9,7 +9,7 @@ use http::header::{HeaderValue, ACCEPT_ENCODING, CONTENT_LENGTH}; use http::{StatusCode, Version}; use body::{Binary, Body}; -use client::ClientRequest; +use client::RequestHead; use header::ContentEncoding; use http::Method; use request::Request; @@ -196,7 +196,7 @@ impl RequestEncoder { self.te.encode_eof(buf) } - pub fn update(&mut self, resp: &mut ClientRequest, head: bool, version: Version) { + pub fn update(&mut self, resp: &mut RequestHead, head: bool, version: Version) { self.head = head; } } diff --git a/src/h1/mod.rs b/src/h1/mod.rs index e73771778..e7e0759b9 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -33,6 +33,15 @@ pub enum Message { Chunk(Option), } +impl Message { + pub fn into_item(self) -> Option { + match self { + Message::Item(item) => Some(item), + _ => None, + } + } +} + impl From for Message { fn from(item: T) -> Self { Message::Item(item) @@ -44,7 +53,7 @@ impl From for Message { pub enum MessageType { None, Payload, - Unhandled, + Stream, } #[cfg(test)] diff --git a/src/request.rs b/src/request.rs index 07632bf05..593080fa6 100644 --- a/src/request.rs +++ b/src/request.rs @@ -240,7 +240,10 @@ impl MessagePool { if let Some(r) = Rc::get_mut(&mut msg) { r.reset(); } - return ClientResponse { inner: msg }; + return ClientResponse { + inner: msg, + payload: None, + }; } ClientResponse::with_pool(pool) } diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index 485ce5620..80cf98684 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -13,6 +13,7 @@ use rand; use sha1::Sha1; use tokio_io::{AsyncRead, AsyncWrite}; +use body::BodyType; use client::ClientResponse; use h1; use ws::Codec; @@ -89,9 +90,7 @@ where req.request.set_header(header::ORIGIN, origin); } - req.request.upgrade(); - req.request.set_header(header::UPGRADE, "websocket"); - req.request.set_header(header::CONNECTION, "upgrade"); + req.request.upgrade("websocket"); req.request.set_header(header::SEC_WEBSOCKET_VERSION, "13"); if let Some(protocols) = req.protocols.take() { @@ -142,7 +141,7 @@ where // h1 protocol let framed = Framed::new(io, h1::ClientCodec::default()); framed - .send(request.into()) + .send((request.into_parts().0, BodyType::None).into()) .map_err(ClientError::from) .and_then(|framed| { framed diff --git a/tests/test_client.rs b/tests/test_client.rs new file mode 100644 index 000000000..6741a2c61 --- /dev/null +++ b/tests/test_client.rs @@ -0,0 +1,147 @@ +extern crate actix; +extern crate actix_http; +extern crate actix_net; +extern crate bytes; +extern crate futures; + +use std::{thread, time}; + +use actix::System; +use actix_net::server::Server; +use actix_net::service::NewServiceExt; +use futures::future::{self, lazy, ok}; + +use actix_http::{client, h1, test, Request, Response}; + +const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World"; + +#[test] +fn test_h1_v2() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + h1::H1Service::build() + .finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) + .map(|_| ()) + }).unwrap() + .run(); + }); + thread::sleep(time::Duration::from_millis(100)); + + let mut sys = System::new("test"); + let mut connector = sys + .block_on(lazy(|| Ok::<_, ()>(client::Connector::default().service()))) + .unwrap(); + + let req = client::ClientRequest::get(format!("http://{}/", addr)) + .finish() + .unwrap(); + + let response = sys.block_on(req.send(&mut connector)).unwrap(); + assert!(response.status().is_success()); + + let request = client::ClientRequest::get(format!("http://{}/", addr)) + .header("x-test", "111") + .finish() + .unwrap(); + let repr = format!("{:?}", request); + assert!(repr.contains("ClientRequest")); + assert!(repr.contains("x-test")); + + let response = sys.block_on(request.send(&mut connector)).unwrap(); + assert!(response.status().is_success()); + + // read response + // let bytes = srv.execute(response.body()).unwrap(); + // assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + let request = client::ClientRequest::post(format!("http://{}/", addr)) + .finish() + .unwrap(); + let response = sys.block_on(request.send(&mut connector)).unwrap(); + assert!(response.status().is_success()); + + // read response + // let bytes = srv.execute(response.body()).unwrap(); + // assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} + +#[test] +fn test_connection_close() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + h1::H1Service::build() + .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) + .map(|_| ()) + }).unwrap() + .run(); + }); + thread::sleep(time::Duration::from_millis(100)); + + let mut sys = System::new("test"); + let mut connector = sys + .block_on(lazy(|| Ok::<_, ()>(client::Connector::default().service()))) + .unwrap(); + + let request = client::ClientRequest::get(format!("http://{}/", addr)) + .header("Connection", "close") + .finish() + .unwrap(); + let response = sys.block_on(request.send(&mut connector)).unwrap(); + assert!(response.status().is_success()); +} + +#[test] +fn test_with_query_parameter() { + let addr = test::TestServer::unused_addr(); + thread::spawn(move || { + Server::new() + .bind("test", addr, move || { + h1::H1Service::build() + .finish(|req: Request| { + if req.uri().query().unwrap().contains("qp=") { + ok::<_, ()>(Response::Ok().finish()) + } else { + ok::<_, ()>(Response::BadRequest().finish()) + } + }).map(|_| ()) + }).unwrap() + .run(); + }); + thread::sleep(time::Duration::from_millis(100)); + + let mut sys = System::new("test"); + let mut connector = sys + .block_on(lazy(|| Ok::<_, ()>(client::Connector::default().service()))) + .unwrap(); + + let request = client::ClientRequest::get(format!("http://{}/?qp=5", addr)) + .finish() + .unwrap(); + + let response = sys.block_on(request.send(&mut connector)).unwrap(); + assert!(response.status().is_success()); +} From 6297fe0d4117bd93a4e215b36fee91e7bcb8d750 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 14 Nov 2018 09:38:16 -0800 Subject: [PATCH 062/427] refactor client response payload handling --- Cargo.toml | 3 +- src/body.rs | 7 ++- src/client/pipeline.rs | 51 +++++++++++++------- src/client/response.rs | 45 +++++++++-------- src/error.rs | 8 +++- src/h1/client.rs | 106 ++++++++++++++++++++++++++++++----------- src/h1/dispatcher.rs | 7 ++- src/h1/mod.rs | 2 +- src/payload.rs | 8 ++-- src/request.rs | 2 +- tests/test_client.rs | 10 ++-- 11 files changed, 166 insertions(+), 83 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e586f34ed..074e53631 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,8 @@ rust-tls = ["rustls", "actix-net/rust-tls"] [dependencies] actix = "0.7.5" #actix-net = "0.2.0" -actix-net = { git="https://github.com/actix/actix-net.git" } +#actix-net = { git="https://github.com/actix/actix-net.git" } +actix-net = { path="../actix-net" } base64 = "0.9" bitflags = "1.0" diff --git a/src/body.rs b/src/body.rs index e001273c4..1165909e8 100644 --- a/src/body.rs +++ b/src/body.rs @@ -4,10 +4,13 @@ use std::{fmt, mem}; use bytes::{Bytes, BytesMut}; use futures::{Async, Poll, Stream}; -use error::Error; +use error::{Error, PayloadError}; /// Type represent streaming body -pub type BodyStream = Box>; +pub type BodyStream = Box>; + +/// Type represent streaming payload +pub type PayloadStream = Box>; /// Different type of bory pub enum BodyType { diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs index fff900131..4081b6354 100644 --- a/src/client/pipeline.rs +++ b/src/client/pipeline.rs @@ -11,8 +11,8 @@ use super::error::{ConnectorError, SendRequestError}; use super::request::RequestHead; use super::response::ClientResponse; use super::{Connect, Connection}; -use body::{BodyStream, BodyType, MessageBody}; -use error::Error; +use body::{BodyType, MessageBody, PayloadStream}; +use error::PayloadError; use h1; pub fn send_request( @@ -44,7 +44,7 @@ where let mut res = item.into_item().unwrap(); match framed.get_codec().message_type() { h1::MessageType::None => release_connection(framed), - _ => res.payload = Some(Payload::stream(framed)), + _ => *res.payload.borrow_mut() = Some(Payload::stream(framed)), } ok(res) } else { @@ -129,41 +129,56 @@ where } } -struct Payload { - framed: Option, h1::ClientCodec>>, +struct EmptyPayload; + +impl Stream for EmptyPayload { + type Item = Bytes; + type Error = PayloadError; + + fn poll(&mut self) -> Poll, Self::Error> { + Ok(Async::Ready(None)) + } +} + +pub(crate) struct Payload { + framed: Option, h1::ClientPayloadCodec>>, +} + +impl Payload<()> { + pub fn empty() -> PayloadStream { + Box::new(EmptyPayload) + } } impl Payload { - fn stream(framed: Framed, h1::ClientCodec>) -> BodyStream { + fn stream(framed: Framed, h1::ClientCodec>) -> PayloadStream { Box::new(Payload { - framed: Some(framed), + framed: Some(framed.map_codec(|codec| codec.into_payload_codec())), }) } } impl Stream for Payload { type Item = Bytes; - type Error = Error; + type Error = PayloadError; - fn poll(&mut self) -> Poll, Error> { + fn poll(&mut self) -> Poll, Self::Error> { match self.framed.as_mut().unwrap().poll()? { Async::NotReady => Ok(Async::NotReady), - Async::Ready(Some(chunk)) => match chunk { - h1::Message::Chunk(Some(chunk)) => Ok(Async::Ready(Some(chunk))), - h1::Message::Chunk(None) => { - release_connection(self.framed.take().unwrap()); - Ok(Async::Ready(None)) - } - h1::Message::Item(_) => unreachable!(), + Async::Ready(Some(chunk)) => if let Some(chunk) = chunk { + Ok(Async::Ready(Some(chunk))) + } else { + release_connection(self.framed.take().unwrap()); + Ok(Async::Ready(None)) }, Async::Ready(None) => Ok(Async::Ready(None)), } } } -fn release_connection(framed: Framed, h1::ClientCodec>) +fn release_connection(framed: Framed, U>) where - Io: AsyncRead + AsyncWrite + 'static, + T: AsyncRead + AsyncWrite + 'static, { let parts = framed.into_parts(); if parts.read_buf.is_empty() && parts.write_buf.is_empty() { diff --git a/src/client/response.rs b/src/client/response.rs index 0d5a87a0d..e8e63e4f7 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -6,34 +6,37 @@ use bytes::Bytes; use futures::{Async, Poll, Stream}; use http::{HeaderMap, Method, StatusCode, Version}; -use body::BodyStream; -use error::Error; +use body::PayloadStream; +use error::PayloadError; use extensions::Extensions; +use httpmessage::HttpMessage; use request::{Message, MessageFlags, MessagePool}; use uri::Url; +use super::pipeline::Payload; + /// Client Response pub struct ClientResponse { pub(crate) inner: Rc, - pub(crate) payload: Option, + pub(crate) payload: RefCell>, } -// impl HttpMessage for ClientResponse { -// type Stream = Payload; +impl HttpMessage for ClientResponse { + type Stream = PayloadStream; -// fn headers(&self) -> &HeaderMap { -// &self.inner.headers -// } + fn headers(&self) -> &HeaderMap { + &self.inner.headers + } -// #[inline] -// fn payload(&self) -> Payload { -// if let Some(payload) = self.inner.payload.borrow_mut().take() { -// payload -// } else { -// Payload::empty() -// } -// } -// } + #[inline] + fn payload(&self) -> Self::Stream { + if let Some(payload) = self.payload.borrow_mut().take() { + payload + } else { + Payload::empty() + } + } +} impl ClientResponse { /// Create new Request instance @@ -55,7 +58,7 @@ impl ClientResponse { payload: RefCell::new(None), extensions: RefCell::new(Extensions::new()), }), - payload: None, + payload: RefCell::new(None), } } @@ -114,10 +117,10 @@ impl ClientResponse { impl Stream for ClientResponse { type Item = Bytes; - type Error = Error; + type Error = PayloadError; - fn poll(&mut self) -> Poll, Error> { - if let Some(ref mut payload) = self.payload { + fn poll(&mut self) -> Poll, Self::Error> { + if let Some(ref mut payload) = &mut *self.payload.borrow_mut() { payload.poll() } else { Ok(Async::Ready(None)) diff --git a/src/error.rs b/src/error.rs index 3064cda49..956ec4eb4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -339,7 +339,7 @@ impl From for ParseError { pub enum PayloadError { /// A payload reached EOF, but is not complete. #[fail(display = "A payload reached EOF, but is not complete.")] - Incomplete, + Incomplete(Option), /// Content encoding stream corruption #[fail(display = "Can not decode content-encoding.")] EncodingCorrupted, @@ -351,6 +351,12 @@ pub enum PayloadError { UnknownLength, } +impl From for PayloadError { + fn from(err: io::Error) -> Self { + PayloadError::Incomplete(Some(err)) + } +} + /// `PayloadError` returns two possible results: /// /// - `Overflow` returns `PayloadTooLarge` diff --git a/src/h1/client.rs b/src/h1/client.rs index 9ace98e0e..eb7bdc233 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -10,7 +10,7 @@ use super::{Message, MessageType}; use body::{Binary, Body, BodyType}; use client::{ClientResponse, RequestHead}; use config::ServiceConfig; -use error::ParseError; +use error::{ParseError, PayloadError}; use helpers; use http::header::{ HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE, @@ -32,6 +32,15 @@ const AVERAGE_HEADER_SIZE: usize = 30; /// HTTP/1 Codec pub struct ClientCodec { + inner: ClientCodecInner, +} + +/// HTTP/1 Payload Codec +pub struct ClientPayloadCodec { + inner: ClientCodecInner, +} + +struct ClientCodecInner { config: ServiceConfig, decoder: ResponseDecoder, payload: Option, @@ -65,32 +74,34 @@ impl ClientCodec { Flags::empty() }; ClientCodec { - config, - decoder: ResponseDecoder::with_pool(pool), - payload: None, - version: Version::HTTP_11, + inner: ClientCodecInner { + config, + decoder: ResponseDecoder::with_pool(pool), + payload: None, + version: Version::HTTP_11, - flags, - headers_size: 0, - te: RequestEncoder::default(), + flags, + headers_size: 0, + te: RequestEncoder::default(), + }, } } /// Check if request is upgrade pub fn upgrade(&self) -> bool { - self.flags.contains(Flags::UPGRADE) + self.inner.flags.contains(Flags::UPGRADE) } /// Check if last response is keep-alive pub fn keepalive(&self) -> bool { - self.flags.contains(Flags::KEEPALIVE) + self.inner.flags.contains(Flags::KEEPALIVE) } /// Check last request's message type pub fn message_type(&self) -> MessageType { - if self.flags.contains(Flags::STREAM) { + if self.inner.flags.contains(Flags::STREAM) { MessageType::Stream - } else if self.payload.is_none() { + } else if self.inner.payload.is_none() { MessageType::None } else { MessageType::Payload @@ -99,10 +110,27 @@ impl ClientCodec { /// prepare transfer encoding pub fn prepare_te(&mut self, head: &mut RequestHead, btype: BodyType) { - self.te - .update(head, self.flags.contains(Flags::HEAD), self.version); + self.inner.te.update( + head, + self.inner.flags.contains(Flags::HEAD), + self.inner.version, + ); } + /// Convert message codec to a payload codec + pub fn into_payload_codec(self) -> ClientPayloadCodec { + ClientPayloadCodec { inner: self.inner } + } +} + +impl ClientPayloadCodec { + /// Transform payload codec to a message codec + pub fn into_message_codec(self) -> ClientCodec { + ClientCodec { inner: self.inner } + } +} + +impl ClientCodecInner { fn encode_response( &mut self, msg: RequestHead, @@ -154,25 +182,26 @@ impl Decoder for ClientCodec { type Error = ParseError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - if self.payload.is_some() { - Ok(match self.payload.as_mut().unwrap().decode(src)? { + if self.inner.payload.is_some() { + Ok(match self.inner.payload.as_mut().unwrap().decode(src)? { Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))), Some(PayloadItem::Eof) => Some(Message::Chunk(None)), None => None, }) - } else if let Some((req, payload)) = self.decoder.decode(src)? { - self.flags + } else if let Some((req, payload)) = self.inner.decoder.decode(src)? { + self.inner + .flags .set(Flags::HEAD, req.inner.method == Method::HEAD); - self.version = req.inner.version; - if self.flags.contains(Flags::KEEPALIVE_ENABLED) { - self.flags.set(Flags::KEEPALIVE, req.keep_alive()); + self.inner.version = req.inner.version; + if self.inner.flags.contains(Flags::KEEPALIVE_ENABLED) { + self.inner.flags.set(Flags::KEEPALIVE, req.keep_alive()); } match payload { - PayloadType::None => self.payload = None, - PayloadType::Payload(pl) => self.payload = Some(pl), + PayloadType::None => self.inner.payload = None, + PayloadType::Payload(pl) => self.inner.payload = Some(pl), PayloadType::Stream(pl) => { - self.payload = Some(pl); - self.flags.insert(Flags::STREAM); + self.inner.payload = Some(pl); + self.inner.flags.insert(Flags::STREAM); } }; Ok(Some(Message::Item(req))) @@ -182,6 +211,27 @@ impl Decoder for ClientCodec { } } +impl Decoder for ClientPayloadCodec { + type Item = Option; + type Error = PayloadError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + assert!( + self.inner.payload.is_some(), + "Payload decoder is not specified" + ); + + Ok(match self.inner.payload.as_mut().unwrap().decode(src)? { + Some(PayloadItem::Chunk(chunk)) => Some(Some(chunk)), + Some(PayloadItem::Eof) => { + self.inner.payload.take(); + Some(None) + } + None => None, + }) + } +} + impl Encoder for ClientCodec { type Item = Message<(RequestHead, BodyType)>; type Error = io::Error; @@ -193,13 +243,13 @@ impl Encoder for ClientCodec { ) -> Result<(), Self::Error> { match item { Message::Item((msg, btype)) => { - self.encode_response(msg, btype, dst)?; + self.inner.encode_response(msg, btype, dst)?; } Message::Chunk(Some(bytes)) => { - self.te.encode(bytes.as_ref(), dst)?; + self.inner.te.encode(bytes.as_ref(), dst)?; } Message::Chunk(None) => { - self.te.encode_eof(dst)?; + self.inner.te.encode_eof(dst)?; } } Ok(()) diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index b23af9648..71d6435c1 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -143,7 +143,7 @@ where fn client_disconnected(&mut self) { self.flags.insert(Flags::DISCONNECTED); if let Some(mut payload) = self.payload.take() { - payload.set_error(PayloadError::Incomplete); + payload.set_error(PayloadError::Incomplete(None)); } } @@ -228,7 +228,7 @@ where } Err(err) => { if let Some(mut payload) = self.payload.take() { - payload.set_error(PayloadError::Incomplete); + payload.set_error(PayloadError::Incomplete(None)); } return Err(DispatchError::Io(err)); } @@ -236,7 +236,10 @@ where } // Send payload State::SendPayload(ref mut stream, ref mut bin) => { + println!("SEND payload"); if let Some(item) = bin.take() { + let mut framed = self.framed.as_mut().unwrap(); + if framed.is_ match self.framed.as_mut().unwrap().start_send(item) { Ok(AsyncSink::Ready) => { self.flags.remove(Flags::FLUSHED); diff --git a/src/h1/mod.rs b/src/h1/mod.rs index e7e0759b9..21261e99c 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -9,7 +9,7 @@ mod dispatcher; mod encoder; mod service; -pub use self::client::ClientCodec; +pub use self::client::{ClientCodec, ClientPayloadCodec}; pub use self::codec::Codec; pub use self::decoder::{PayloadDecoder, RequestDecoder}; pub use self::dispatcher::Dispatcher; diff --git a/src/payload.rs b/src/payload.rs index 54539c408..b05924969 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -527,7 +527,7 @@ mod tests { #[test] fn test_error() { - let err = PayloadError::Incomplete; + let err = PayloadError::Incomplete(None); assert_eq!( format!("{}", err), "A payload reached EOF, but is not complete." @@ -584,7 +584,7 @@ mod tests { assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); - sender.set_error(PayloadError::Incomplete); + sender.set_error(PayloadError::Incomplete(None)); payload.readany().err().unwrap(); let res: Result<(), ()> = Ok(()); result(res) @@ -644,7 +644,7 @@ mod tests { ); assert_eq!(payload.len, 4); - sender.set_error(PayloadError::Incomplete); + sender.set_error(PayloadError::Incomplete(None)); payload.read_exact(10).err().unwrap(); let res: Result<(), ()> = Ok(()); @@ -677,7 +677,7 @@ mod tests { ); assert_eq!(payload.len, 0); - sender.set_error(PayloadError::Incomplete); + sender.set_error(PayloadError::Incomplete(None)); payload.read_until(b"b").err().unwrap(); let res: Result<(), ()> = Ok(()); diff --git a/src/request.rs b/src/request.rs index 593080fa6..fb5cb183b 100644 --- a/src/request.rs +++ b/src/request.rs @@ -242,7 +242,7 @@ impl MessagePool { } return ClientResponse { inner: msg, - payload: None, + payload: RefCell::new(None), }; } ClientResponse::with_pool(pool) diff --git a/tests/test_client.rs b/tests/test_client.rs index 6741a2c61..40920d1b8 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -9,8 +9,10 @@ use std::{thread, time}; use actix::System; use actix_net::server::Server; use actix_net::service::NewServiceExt; +use bytes::Bytes; use futures::future::{self, lazy, ok}; +use actix_http::HttpMessage; use actix_http::{client, h1, test, Request, Response}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ @@ -73,8 +75,8 @@ fn test_h1_v2() { assert!(response.status().is_success()); // read response - // let bytes = srv.execute(response.body()).unwrap(); - // assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + let bytes = sys.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); let request = client::ClientRequest::post(format!("http://{}/", addr)) .finish() @@ -83,8 +85,8 @@ fn test_h1_v2() { assert!(response.status().is_success()); // read response - // let bytes = srv.execute(response.body()).unwrap(); - // assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + let bytes = sys.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } #[test] From 03ad9a3105d95aaf5c8f3bb2a58cd8d600380344 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 14 Nov 2018 10:52:40 -0800 Subject: [PATCH 063/427] simplify client decoder --- Cargo.toml | 6 +-- src/client/pipeline.rs | 18 +++------ src/h1/client.rs | 16 +++----- src/h1/dispatcher.rs | 79 +++++++++++++++------------------------- src/h1/mod.rs | 9 ----- src/ws/client/service.rs | 8 +--- 6 files changed, 46 insertions(+), 90 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 074e53631..80d245951 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,9 +45,9 @@ rust-tls = ["rustls", "actix-net/rust-tls"] [dependencies] actix = "0.7.5" -#actix-net = "0.2.0" -#actix-net = { git="https://github.com/actix/actix-net.git" } -actix-net = { path="../actix-net" } +#actix-net = "0.2.2" +actix-net = { git="https://github.com/actix/actix-net.git" } +#actix-net = { path="../actix-net" } base64 = "0.9" bitflags = "1.0" diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs index 4081b6354..24ec8366d 100644 --- a/src/client/pipeline.rs +++ b/src/client/pipeline.rs @@ -40,11 +40,12 @@ where .into_future() .map_err(|(e, _)| SendRequestError::from(e)) .and_then(|(item, framed)| { - if let Some(item) = item { - let mut res = item.into_item().unwrap(); + if let Some(res) = item { match framed.get_codec().message_type() { h1::MessageType::None => release_connection(framed), - _ => *res.payload.borrow_mut() = Some(Payload::stream(framed)), + _ => { + *res.payload.borrow_mut() = Some(Payload::stream(framed)) + } } ok(res) } else { @@ -92,21 +93,14 @@ where && !self.framed.as_ref().unwrap().is_write_buf_full() { match self.body.as_mut().unwrap().poll_next()? { - Async::Ready(None) => { + Async::Ready(item) => { self.flushed = false; self.framed .as_mut() .unwrap() - .start_send(h1::Message::Chunk(None))?; + .force_send(h1::Message::Chunk(item))?; break; } - Async::Ready(Some(chunk)) => { - self.flushed = false; - self.framed - .as_mut() - .unwrap() - .start_send(h1::Message::Chunk(Some(chunk)))?; - } Async::NotReady => body_ready = false, } } diff --git a/src/h1/client.rs b/src/h1/client.rs index eb7bdc233..d2ac20348 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -178,17 +178,13 @@ impl ClientCodecInner { } impl Decoder for ClientCodec { - type Item = Message; + type Item = ClientResponse; type Error = ParseError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - if self.inner.payload.is_some() { - Ok(match self.inner.payload.as_mut().unwrap().decode(src)? { - Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))), - Some(PayloadItem::Eof) => Some(Message::Chunk(None)), - None => None, - }) - } else if let Some((req, payload)) = self.inner.decoder.decode(src)? { + debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set"); + + if let Some((req, payload)) = self.inner.decoder.decode(src)? { self.inner .flags .set(Flags::HEAD, req.inner.method == Method::HEAD); @@ -204,7 +200,7 @@ impl Decoder for ClientCodec { self.inner.flags.insert(Flags::STREAM); } }; - Ok(Some(Message::Item(req))) + Ok(Some(req)) } else { Ok(None) } @@ -216,7 +212,7 @@ impl Decoder for ClientPayloadCodec { type Error = PayloadError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - assert!( + debug_assert!( self.inner.payload.is_some(), "Payload decoder is not specified" ); diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 71d6435c1..470d3df93 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -64,7 +64,7 @@ enum State { None, ServiceCall(S::Future), SendResponse(Option<(Message, Body)>), - SendPayload(Option, Option>), + SendPayload(BodyStream), } impl State { @@ -204,21 +204,23 @@ where // send respons State::SendResponse(ref mut item) => { let (msg, body) = item.take().expect("SendResponse is empty"); - match self.framed.as_mut().unwrap().start_send(msg) { + let framed = self.framed.as_mut().unwrap(); + match framed.start_send(msg) { Ok(AsyncSink::Ready) => { - self.flags.set( - Flags::KEEPALIVE, - self.framed.as_mut().unwrap().get_codec().keepalive(), - ); + self.flags + .set(Flags::KEEPALIVE, framed.get_codec().keepalive()); self.flags.remove(Flags::FLUSHED); match body { Body::Empty => Some(State::None), - Body::Binary(mut bin) => Some(State::SendPayload( - None, - Some(Message::Chunk(Some(bin.take()))), - )), Body::Streaming(stream) => { - Some(State::SendPayload(Some(stream), None)) + Some(State::SendPayload(stream)) + } + Body::Binary(mut bin) => { + self.flags.remove(Flags::FLUSHED); + framed + .force_send(Message::Chunk(Some(bin.take())))?; + framed.force_send(Message::Chunk(None))?; + Some(State::None) } } } @@ -235,51 +237,28 @@ where } } // Send payload - State::SendPayload(ref mut stream, ref mut bin) => { - println!("SEND payload"); - if let Some(item) = bin.take() { - let mut framed = self.framed.as_mut().unwrap(); - if framed.is_ - match self.framed.as_mut().unwrap().start_send(item) { - Ok(AsyncSink::Ready) => { - self.flags.remove(Flags::FLUSHED); - } - Ok(AsyncSink::NotReady(item)) => { - *bin = Some(item); - return Ok(()); - } - Err(err) => return Err(DispatchError::Io(err)), - } - } - if let Some(ref mut stream) = stream { - match stream.poll() { - Ok(Async::Ready(Some(item))) => match self - .framed - .as_mut() - .unwrap() - .start_send(Message::Chunk(Some(item))) - { - Ok(AsyncSink::Ready) => { + State::SendPayload(ref mut stream) => { + let mut framed = self.framed.as_mut().unwrap(); + loop { + if !framed.is_write_buf_full() { + match stream.poll().map_err(|_| DispatchError::Unknown)? { + Async::Ready(Some(item)) => { self.flags.remove(Flags::FLUSHED); + framed.force_send(Message::Chunk(Some(item)))?; continue; } - Ok(AsyncSink::NotReady(msg)) => { - *bin = Some(msg); - return Ok(()); + Async::Ready(None) => { + self.flags.remove(Flags::FLUSHED); + framed.force_send(Message::Chunk(None))?; } - Err(err) => return Err(DispatchError::Io(err)), - }, - Ok(Async::Ready(None)) => Some(State::SendPayload( - None, - Some(Message::Chunk(None)), - )), - Ok(Async::NotReady) => return Ok(()), - // Err(err) => return Err(DispatchError::Io(err)), - Err(_) => return Err(DispatchError::Unknown), + Async::NotReady => return Ok(()), + } + } else { + return Ok(()); } - } else { - Some(State::None) + break; } + None } }; diff --git a/src/h1/mod.rs b/src/h1/mod.rs index 21261e99c..818387004 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -33,15 +33,6 @@ pub enum Message { Chunk(Option), } -impl Message { - pub fn into_item(self) -> Option { - match self { - Message::Item(item) => Some(item), - _ => None, - } - } -} - impl From for Message { fn from(item: T) -> Self { Message::Item(item) diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index 80cf98684..34a151444 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -172,10 +172,7 @@ where { fut: Box< Future< - Item = ( - Option>, - Framed, - ), + Item = (Option, Framed), Error = ClientError, >, >, @@ -196,8 +193,7 @@ where let (item, framed) = try_ready!(self.fut.poll()); let res = match item { - Some(h1::Message::Item(res)) => res, - Some(h1::Message::Chunk(_)) => unreachable!(), + Some(res) => res, None => return Err(ClientError::Disconnected), }; From 6e7560e28781c71318af9fc28b29bd54a50fa83f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 14 Nov 2018 18:57:58 -0800 Subject: [PATCH 064/427] SendResponse service sends body as well --- src/service.rs | 109 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 89 insertions(+), 20 deletions(-) diff --git a/src/service.rs b/src/service.rs index c76530320..774efb74e 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,13 +1,13 @@ -use std::io; use std::marker::PhantomData; use actix_net::codec::Framed; use actix_net::service::{NewService, Service}; use futures::future::{ok, Either, FutureResult}; use futures::{Async, AsyncSink, Future, Poll, Sink}; -use tokio_io::AsyncWrite; +use tokio_io::{AsyncRead, AsyncWrite}; -use error::ResponseError; +use body::Body; +use error::{Error, ResponseError}; use h1::{Codec, Message}; use response::Response; @@ -113,20 +113,43 @@ pub struct SendResponse(PhantomData<(T,)>); impl Default for SendResponse where - T: AsyncWrite, + T: AsyncRead + AsyncWrite, { fn default() -> Self { SendResponse(PhantomData) } } +impl SendResponse +where + T: AsyncRead + AsyncWrite, +{ + pub fn send( + mut framed: Framed, + mut res: Response, + ) -> impl Future, Error = Error> { + // init codec + framed.get_codec_mut().prepare_te(&mut res); + + // extract body from response + let body = res.replace_body(Body::Empty); + + // write response + SendResponseFut { + res: Some(Message::Item(res)), + body: Some(body), + framed: Some(framed), + } + } +} + impl NewService for SendResponse where - T: AsyncWrite, + T: AsyncRead + AsyncWrite, { type Request = (Response, Framed); type Response = Framed; - type Error = io::Error; + type Error = Error; type InitError = (); type Service = SendResponse; type Future = FutureResult; @@ -138,20 +161,23 @@ where impl Service for SendResponse where - T: AsyncWrite, + T: AsyncRead + AsyncWrite, { type Request = (Response, Framed); type Response = Framed; - type Error = io::Error; + type Error = Error; type Future = SendResponseFut; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } - fn call(&mut self, (res, framed): Self::Request) -> Self::Future { + fn call(&mut self, (mut res, mut framed): Self::Request) -> Self::Future { + framed.get_codec_mut().prepare_te(&mut res); + let body = res.replace_body(Body::Empty); SendResponseFut { res: Some(Message::Item(res)), + body: Some(body), framed: Some(framed), } } @@ -159,29 +185,72 @@ where pub struct SendResponseFut { res: Option>, + body: Option, framed: Option>, } impl Future for SendResponseFut where - T: AsyncWrite, + T: AsyncRead + AsyncWrite, { type Item = Framed; - type Error = io::Error; + type Error = Error; fn poll(&mut self) -> Poll { - if let Some(res) = self.res.take() { - match self.framed.as_mut().unwrap().start_send(res)? { - AsyncSink::Ready => (), - AsyncSink::NotReady(res) => { - self.res = Some(res); - return Ok(Async::NotReady); + // send response + if self.res.is_some() { + let framed = self.framed.as_mut().unwrap(); + if !framed.is_write_buf_full() { + if let Some(res) = self.res.take() { + println!("SEND RESP: {:?}", res); + framed.force_send(res)?; } } } - match self.framed.as_mut().unwrap().poll_complete()? { - Async::Ready(_) => Ok(Async::Ready(self.framed.take().unwrap())), - Async::NotReady => Ok(Async::NotReady), + + // send body + if self.res.is_none() && self.body.is_some() { + let framed = self.framed.as_mut().unwrap(); + if !framed.is_write_buf_full() { + let body = self.body.take().unwrap(); + match body { + Body::Empty => (), + Body::Streaming(mut stream) => loop { + match stream.poll()? { + Async::Ready(item) => { + let done = item.is_none(); + framed.force_send(Message::Chunk(item.into()))?; + if !done { + if !framed.is_write_buf_full() { + continue; + } else { + self.body = Some(Body::Streaming(stream)); + break; + } + } + } + Async::NotReady => { + self.body = Some(Body::Streaming(stream)); + break; + } + } + }, + Body::Binary(mut bin) => { + framed.force_send(Message::Chunk(Some(bin.take())))?; + framed.force_send(Message::Chunk(None))?; + } + } + } } + + // flush + match self.framed.as_mut().unwrap().poll_complete()? { + Async::Ready(_) => if self.res.is_some() || self.body.is_some() { + return self.poll(); + }, + Async::NotReady => return Ok(Async::NotReady), + } + + Ok(Async::Ready(self.framed.take().unwrap())) } } From acd42f92d8f507358c5884dd387b48c69e4eeca4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 14 Nov 2018 19:08:52 -0800 Subject: [PATCH 065/427] remove debug print --- src/service.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/service.rs b/src/service.rs index 774efb74e..3aa5d1e41 100644 --- a/src/service.rs +++ b/src/service.rs @@ -202,7 +202,6 @@ where let framed = self.framed.as_mut().unwrap(); if !framed.is_write_buf_full() { if let Some(res) = self.res.take() { - println!("SEND RESP: {:?}", res); framed.force_send(res)?; } } From 6d9733cdf79ae196015624e13be16515c96d9479 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 15 Nov 2018 11:10:23 -0800 Subject: [PATCH 066/427] define generic client Connection trait --- src/client/connection.rs | 69 ++++++++++++++++++++++++++-------------- src/client/connector.rs | 58 ++++++++++++++++++++++----------- src/client/pipeline.rs | 44 ++++++++++++++----------- src/client/pool.rs | 32 +++++++++---------- src/client/request.rs | 7 ++-- 5 files changed, 129 insertions(+), 81 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index eec64267a..363a4ece9 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -5,14 +5,23 @@ use tokio_io::{AsyncRead, AsyncWrite}; use super::pool::Acquired; +pub trait Connection: AsyncRead + AsyncWrite + 'static { + /// Close connection + fn close(&mut self); + + /// Release connection to the connection pool + fn release(&mut self); +} + +#[doc(hidden)] /// HTTP client connection -pub struct Connection { - io: T, +pub struct IoConnection { + io: Option, created: time::Instant, pool: Option>, } -impl fmt::Debug for Connection +impl fmt::Debug for IoConnection where T: fmt::Debug, { @@ -21,59 +30,73 @@ where } } -impl Connection { +impl IoConnection { pub(crate) fn new(io: T, created: time::Instant, pool: Acquired) -> Self { - Connection { - io, + IoConnection { created, + io: Some(io), pool: Some(pool), } } /// Raw IO stream pub fn get_mut(&mut self) -> &mut T { - &mut self.io + self.io.as_mut().unwrap() } + pub(crate) fn into_inner(self) -> (T, time::Instant) { + (self.io.unwrap(), self.created) + } +} + +impl Connection for IoConnection { /// Close connection - pub fn close(mut self) { + fn close(&mut self) { if let Some(mut pool) = self.pool.take() { - pool.close(self) + if let Some(io) = self.io.take() { + pool.close(IoConnection { + io: Some(io), + created: self.created, + pool: None, + }) + } } } /// Release this connection to the connection pool - pub fn release(mut self) { + fn release(&mut self) { if let Some(mut pool) = self.pool.take() { - pool.release(self) + if let Some(io) = self.io.take() { + pool.release(IoConnection { + io: Some(io), + created: self.created, + pool: None, + }) + } } } - - pub(crate) fn into_inner(self) -> (T, time::Instant) { - (self.io, self.created) - } } -impl io::Read for Connection { +impl io::Read for IoConnection { fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.io.read(buf) + self.io.as_mut().unwrap().read(buf) } } -impl AsyncRead for Connection {} +impl AsyncRead for IoConnection {} -impl io::Write for Connection { +impl io::Write for IoConnection { fn write(&mut self, buf: &[u8]) -> io::Result { - self.io.write(buf) + self.io.as_mut().unwrap().write(buf) } fn flush(&mut self) -> io::Result<()> { - self.io.flush() + self.io.as_mut().unwrap().flush() } } -impl AsyncWrite for Connection { +impl AsyncWrite for IoConnection { fn shutdown(&mut self) -> Poll<(), io::Error> { - self.io.shutdown() + self.io.as_mut().unwrap().shutdown() } } diff --git a/src/client/connector.rs b/src/client/connector.rs index 1eae135f2..818085214 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -11,7 +11,7 @@ use tokio_io::{AsyncRead, AsyncWrite}; use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; use super::connect::Connect; -use super::connection::Connection; +use super::connection::{Connection, IoConnection}; use super::error::ConnectorError; use super::pool::ConnectionPool; @@ -130,7 +130,7 @@ impl Connector { self, ) -> impl Service< Request = Connect, - Response = Connection, + Response = impl Connection, Error = ConnectorError, > + Clone { #[cfg(not(feature = "ssl"))] @@ -234,11 +234,11 @@ mod connect_impl { T: Service, { type Request = Connect; - type Response = Connection; + type Response = IoConnection; type Error = ConnectorError; type Future = Either< as Service>::Future, - FutureResult, ConnectorError>, + FutureResult, ConnectorError>, >; fn poll_ready(&mut self) -> Poll<(), Self::Error> { @@ -324,7 +324,7 @@ mod connect_impl { >, { type Request = Connect; - type Response = IoEither, Connection>; + type Response = IoEither, IoConnection>; type Error = ConnectorError; type Future = Either< FutureResult, @@ -342,13 +342,13 @@ mod connect_impl { if let Err(e) = req.validate() { Either::A(err(e)) } else if req.is_secure() { - Either::B(Either::A(InnerConnectorResponseA { - fut: self.tcp_pool.call(req), + Either::B(Either::B(InnerConnectorResponseB { + fut: self.ssl_pool.call(req), _t: PhantomData, })) } else { - Either::B(Either::B(InnerConnectorResponseB { - fut: self.ssl_pool.call(req), + Either::B(Either::A(InnerConnectorResponseA { + fut: self.tcp_pool.call(req), _t: PhantomData, })) } @@ -370,7 +370,7 @@ mod connect_impl { Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { - type Item = IoEither, Connection>; + type Item = IoEither, IoConnection>; type Error = ConnectorError; fn poll(&mut self) -> Poll { @@ -396,7 +396,7 @@ mod connect_impl { Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { - type Item = IoEither, Connection>; + type Item = IoEither, IoConnection>; type Error = ConnectorError; fn poll(&mut self) -> Poll { @@ -413,10 +413,30 @@ pub(crate) enum IoEither { B(Io2), } +impl Connection for IoEither +where + Io1: Connection, + Io2: Connection, +{ + fn close(&mut self) { + match self { + IoEither::A(ref mut io) => io.close(), + IoEither::B(ref mut io) => io.close(), + } + } + + fn release(&mut self) { + match self { + IoEither::A(ref mut io) => io.release(), + IoEither::B(ref mut io) => io.release(), + } + } +} + impl io::Read for IoEither where - Io1: io::Read, - Io2: io::Read, + Io1: Connection, + Io2: Connection, { fn read(&mut self, buf: &mut [u8]) -> io::Result { match self { @@ -428,8 +448,8 @@ where impl AsyncRead for IoEither where - Io1: AsyncRead, - Io2: AsyncRead, + Io1: Connection, + Io2: Connection, { unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { match self { @@ -441,8 +461,8 @@ where impl AsyncWrite for IoEither where - Io1: AsyncWrite, - Io2: AsyncWrite, + Io1: Connection, + Io2: Connection, { fn shutdown(&mut self) -> Poll<(), io::Error> { match self { @@ -468,8 +488,8 @@ where impl io::Write for IoEither where - Io1: io::Write, - Io2: io::Write, + Io1: Connection, + Io2: Connection, { fn flush(&mut self) -> io::Result<()> { match self { diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs index 24ec8366d..dc6a644dc 100644 --- a/src/client/pipeline.rs +++ b/src/client/pipeline.rs @@ -15,27 +15,32 @@ use body::{BodyType, MessageBody, PayloadStream}; use error::PayloadError; use h1; -pub fn send_request( +pub(crate) fn send_request( head: RequestHead, body: B, connector: &mut T, ) -> impl Future where - T: Service, Error = ConnectorError>, + T: Service, B: MessageBody, - Io: AsyncRead + AsyncWrite + 'static, + I: Connection, { let tp = body.tp(); connector + // connect to the host .call(Connect::new(head.uri.clone())) .from_err() + // create Framed and send reqest .map(|io| Framed::new(io, h1::ClientCodec::default())) .and_then(|framed| framed.send((head, tp).into()).from_err()) + // send request body .and_then(move |framed| match body.tp() { BodyType::None | BodyType::Zero => Either::A(ok(framed)), _ => Either::B(SendBody::new(body, framed)), - }).and_then(|framed| { + }) + // read response and init read body + .and_then(|framed| { framed .into_future() .map_err(|(e, _)| SendRequestError::from(e)) @@ -55,19 +60,20 @@ where }) } -struct SendBody { +/// Future responsible for sending request body to the peer +struct SendBody { body: Option, - framed: Option, h1::ClientCodec>>, + framed: Option>, write_buf: VecDeque>, flushed: bool, } -impl SendBody +impl SendBody where - Io: AsyncRead + AsyncWrite + 'static, + I: AsyncRead + AsyncWrite + 'static, B: MessageBody, { - fn new(body: B, framed: Framed, h1::ClientCodec>) -> Self { + fn new(body: B, framed: Framed) -> Self { SendBody { body: Some(body), framed: Some(framed), @@ -77,12 +83,12 @@ where } } -impl Future for SendBody +impl Future for SendBody where - Io: AsyncRead + AsyncWrite + 'static, + I: Connection, B: MessageBody, { - type Item = Framed, h1::ClientCodec>; + type Item = Framed; type Error = SendRequestError; fn poll(&mut self) -> Poll { @@ -135,7 +141,7 @@ impl Stream for EmptyPayload { } pub(crate) struct Payload { - framed: Option, h1::ClientPayloadCodec>>, + framed: Option>, } impl Payload<()> { @@ -144,15 +150,15 @@ impl Payload<()> { } } -impl Payload { - fn stream(framed: Framed, h1::ClientCodec>) -> PayloadStream { +impl Payload { + fn stream(framed: Framed) -> PayloadStream { Box::new(Payload { framed: Some(framed.map_codec(|codec| codec.into_payload_codec())), }) } } -impl Stream for Payload { +impl Stream for Payload { type Item = Bytes; type Error = PayloadError; @@ -170,11 +176,11 @@ impl Stream for Payload { } } -fn release_connection(framed: Framed, U>) +fn release_connection(framed: Framed) where - T: AsyncRead + AsyncWrite + 'static, + T: Connection, { - let parts = framed.into_parts(); + let mut parts = framed.into_parts(); if parts.read_buf.is_empty() && parts.write_buf.is_empty() { parts.io.release() } else { diff --git a/src/client/pool.rs b/src/client/pool.rs index 25296a6dd..44008f346 100644 --- a/src/client/pool.rs +++ b/src/client/pool.rs @@ -17,7 +17,7 @@ use tokio_io::{AsyncRead, AsyncWrite}; use tokio_timer::{sleep, Delay}; use super::connect::Connect; -use super::connection::Connection; +use super::connection::IoConnection; use super::error::ConnectorError; #[derive(Hash, Eq, PartialEq, Clone, Debug)] @@ -89,10 +89,10 @@ where T: Service, { type Request = Connect; - type Response = Connection; + type Response = IoConnection; type Error = ConnectorError; type Future = Either< - FutureResult, ConnectorError>, + FutureResult, ConnectorError>, Either, OpenConnection>, >; @@ -107,7 +107,7 @@ where match self.1.as_ref().borrow_mut().acquire(&key) { Acquire::Acquired(io, created) => { // use existing connection - Either::A(ok(Connection::new( + Either::A(ok(IoConnection::new( io, created, Acquired(key, Some(self.1.clone())), @@ -142,7 +142,7 @@ where { key: Key, token: usize, - rx: oneshot::Receiver, ConnectorError>>, + rx: oneshot::Receiver, ConnectorError>>, inner: Option>>>, } @@ -163,7 +163,7 @@ impl Future for WaitForConnection where Io: AsyncRead + AsyncWrite, { - type Item = Connection; + type Item = IoConnection; type Error = ConnectorError; fn poll(&mut self) -> Poll { @@ -226,7 +226,7 @@ where F: Future, Io: AsyncRead + AsyncWrite, { - type Item = Connection; + type Item = IoConnection; type Error = ConnectorError; fn poll(&mut self) -> Poll { @@ -234,7 +234,7 @@ where Err(err) => Err(err.into()), Ok(Async::Ready((_, io))) => { let _ = self.inner.take(); - Ok(Async::Ready(Connection::new( + Ok(Async::Ready(IoConnection::new( io, Instant::now(), Acquired(self.key.clone(), self.inner.clone()), @@ -251,7 +251,7 @@ where { fut: F, key: Key, - rx: Option, ConnectorError>>>, + rx: Option, ConnectorError>>>, inner: Option>>>, } @@ -262,7 +262,7 @@ where { fn spawn( key: Key, - rx: oneshot::Sender, ConnectorError>>, + rx: oneshot::Sender, ConnectorError>>, inner: Rc>>, fut: F, ) { @@ -308,7 +308,7 @@ where Ok(Async::Ready((_, io))) => { let _ = self.inner.take(); if let Some(rx) = self.rx.take() { - let _ = rx.send(Ok(Connection::new( + let _ = rx.send(Ok(IoConnection::new( io, Instant::now(), Acquired(self.key.clone(), self.inner.clone()), @@ -336,7 +336,7 @@ pub(crate) struct Inner { available: HashMap>>, waiters: Slab<( Connect, - oneshot::Sender, ConnectorError>>, + oneshot::Sender, ConnectorError>>, )>, waiters_queue: IndexSet<(Key, usize)>, task: AtomicTask, @@ -378,7 +378,7 @@ where &mut self, connect: Connect, ) -> ( - oneshot::Receiver, ConnectorError>>, + oneshot::Receiver, ConnectorError>>, usize, ) { let (tx, rx) = oneshot::channel(); @@ -479,7 +479,7 @@ where Acquire::NotAvailable => break, Acquire::Acquired(io, created) => { let (_, tx) = inner.waiters.remove(token); - if let Err(conn) = tx.send(Ok(Connection::new( + if let Err(conn) = tx.send(Ok(IoConnection::new( io, created, Acquired(key.clone(), Some(self.inner.clone())), @@ -546,13 +546,13 @@ impl Acquired where T: AsyncRead + AsyncWrite + 'static, { - pub(crate) fn close(&mut self, conn: Connection) { + pub(crate) fn close(&mut self, conn: IoConnection) { if let Some(inner) = self.1.take() { let (io, _) = conn.into_inner(); inner.as_ref().borrow_mut().release_close(io); } } - pub(crate) fn release(&mut self, conn: Connection) { + pub(crate) fn release(&mut self, conn: IoConnection) { if let Some(inner) = self.1.take() { let (io, created) = conn.into_inner(); inner diff --git a/src/client/request.rs b/src/client/request.rs index 603374135..602abed59 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -7,7 +7,6 @@ use bytes::{BufMut, Bytes, BytesMut}; use cookie::{Cookie, CookieJar}; use futures::{Future, Stream}; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; -use tokio_io::{AsyncRead, AsyncWrite}; use urlcrate::Url; use body::{MessageBody, MessageBodyStream}; @@ -176,13 +175,13 @@ where // Send request /// /// This method returns a future that resolves to a ClientResponse - pub fn send( + pub fn send( self, connector: &mut T, ) -> impl Future where - T: Service, Error = ConnectorError>, - Io: AsyncRead + AsyncWrite + 'static, + T: Service, + I: Connection, { pipeline::send_request(self.head, self.body, connector) } From 3b7bc41418ad51a8e2acea2b7dbbc84a68914a29 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 15 Nov 2018 22:34:29 -0800 Subject: [PATCH 067/427] use RequestHead for Request --- src/client/mod.rs | 2 +- src/client/pipeline.rs | 2 +- src/client/request.rs | 19 +++---------- src/client/response.rs | 16 +++++------ src/h1/client.rs | 8 +++--- src/h1/codec.rs | 4 +-- src/h1/decoder.rs | 14 +++++----- src/h1/encoder.rs | 3 +-- src/request.rs | 61 ++++++++++++++++++++++++++---------------- src/test.rs | 6 ++--- 10 files changed, 68 insertions(+), 67 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index da0cbc670..76c3f8b88 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -12,5 +12,5 @@ pub use self::connect::Connect; pub use self::connection::Connection; pub use self::connector::Connector; pub use self::error::{ConnectorError, InvalidUrlKind, SendRequestError}; -pub use self::request::{ClientRequest, ClientRequestBuilder, RequestHead}; +pub use self::request::{ClientRequest, ClientRequestBuilder}; pub use self::response::ClientResponse; diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs index dc6a644dc..26bd1c62a 100644 --- a/src/client/pipeline.rs +++ b/src/client/pipeline.rs @@ -8,12 +8,12 @@ use futures::{Async, Future, Poll, Sink, Stream}; use tokio_io::{AsyncRead, AsyncWrite}; use super::error::{ConnectorError, SendRequestError}; -use super::request::RequestHead; use super::response::ClientResponse; use super::{Connect, Connection}; use body::{BodyType, MessageBody, PayloadStream}; use error::PayloadError; use h1; +use request::RequestHead; pub(crate) fn send_request( head: RequestHead, diff --git a/src/client/request.rs b/src/client/request.rs index 602abed59..c4c7f2f6a 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -16,6 +16,7 @@ use http::{ uri, Error as HttpError, HeaderMap, HeaderName, HeaderValue, HttpTryFrom, Method, Uri, Version, }; +use request::RequestHead; use super::response::ClientResponse; use super::{pipeline, Connect, Connection, ConnectorError, SendRequestError}; @@ -50,21 +51,9 @@ pub struct ClientRequest { body: B, } -pub struct RequestHead { - pub uri: Uri, - pub method: Method, - pub version: Version, - pub headers: HeaderMap, -} - -impl Default for RequestHead { - fn default() -> RequestHead { - RequestHead { - uri: Uri::default(), - method: Method::default(), - version: Version::HTTP_11, - headers: HeaderMap::with_capacity(16), - } +impl RequestHead { + pub fn clear(&mut self) { + self.headers.clear() } } diff --git a/src/client/response.rs b/src/client/response.rs index e8e63e4f7..56e13fa47 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -4,13 +4,13 @@ use std::rc::Rc; use bytes::Bytes; use futures::{Async, Poll, Stream}; -use http::{HeaderMap, Method, StatusCode, Version}; +use http::{HeaderMap, StatusCode, Version}; use body::PayloadStream; use error::PayloadError; use extensions::Extensions; use httpmessage::HttpMessage; -use request::{Message, MessageFlags, MessagePool}; +use request::{Message, MessageFlags, MessagePool, RequestHead}; use uri::Url; use super::pipeline::Payload; @@ -25,7 +25,7 @@ impl HttpMessage for ClientResponse { type Stream = PayloadStream; fn headers(&self) -> &HeaderMap { - &self.inner.headers + &self.inner.head.headers } #[inline] @@ -49,11 +49,9 @@ impl ClientResponse { ClientResponse { inner: Rc::new(Message { pool, - method: Method::GET, + head: RequestHead::default(), status: StatusCode::OK, url: Url::default(), - version: Version::HTTP_11, - headers: HeaderMap::with_capacity(16), flags: Cell::new(MessageFlags::empty()), payload: RefCell::new(None), extensions: RefCell::new(Extensions::new()), @@ -75,7 +73,7 @@ impl ClientResponse { /// Read the Request Version. #[inline] pub fn version(&self) -> Version { - self.inner().version + self.inner().head.version } /// Get the status from the server. @@ -87,13 +85,13 @@ impl ClientResponse { #[inline] /// Returns Request's headers. pub fn headers(&self) -> &HeaderMap { - &self.inner().headers + &self.inner().head.headers } #[inline] /// Returns mutable Request's headers. pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.inner_mut().headers + &mut self.inner_mut().head.headers } /// Checks if a connection should be kept alive. diff --git a/src/h1/client.rs b/src/h1/client.rs index d2ac20348..81a6f5689 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -8,7 +8,7 @@ use super::decoder::{PayloadDecoder, PayloadItem, PayloadType, ResponseDecoder}; use super::encoder::{RequestEncoder, ResponseLength}; use super::{Message, MessageType}; use body::{Binary, Body, BodyType}; -use client::{ClientResponse, RequestHead}; +use client::ClientResponse; use config::ServiceConfig; use error::{ParseError, PayloadError}; use helpers; @@ -16,7 +16,7 @@ use http::header::{ HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE, }; use http::{Method, Version}; -use request::MessagePool; +use request::{MessagePool, RequestHead}; bitflags! { struct Flags: u8 { @@ -187,8 +187,8 @@ impl Decoder for ClientCodec { if let Some((req, payload)) = self.inner.decoder.decode(src)? { self.inner .flags - .set(Flags::HEAD, req.inner.method == Method::HEAD); - self.inner.version = req.inner.version; + .set(Flags::HEAD, req.inner.head.method == Method::HEAD); + self.inner.version = req.inner.head.version; if self.inner.flags.contains(Flags::KEEPALIVE_ENABLED) { self.inner.flags.set(Flags::KEEPALIVE, req.keep_alive()); } diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 44a1b81fc..57e848982 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -261,8 +261,8 @@ impl Decoder for Codec { }) } else if let Some((req, payload)) = self.decoder.decode(src)? { self.flags - .set(Flags::HEAD, req.inner.method == Method::HEAD); - self.version = req.inner.version; + .set(Flags::HEAD, req.inner.head.method == Method::HEAD); + self.version = req.inner.head.version; if self.flags.contains(Flags::KEEPALIVE_ENABLED) { self.flags.set(Flags::KEEPALIVE, req.keep_alive()); } diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index f2a3ee3f7..75bd0f7c1 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -152,15 +152,15 @@ impl Decoder for RequestDecoder { _ => (), } - inner.headers.append(name, value); + inner.head.headers.append(name, value); } else { return Err(ParseError::Header); } } inner.url = path; - inner.method = method; - inner.version = version; + inner.head.method = method; + inner.head.version = version; } msg }; @@ -172,7 +172,7 @@ impl Decoder for RequestDecoder { } else if let Some(len) = content_length { // Content-Length PayloadType::Payload(PayloadDecoder::length(len)) - } else if has_upgrade || msg.inner.method == Method::CONNECT { + } else if has_upgrade || msg.inner.head.method == Method::CONNECT { // upgrade(websocket) or connect PayloadType::Stream(PayloadDecoder::eof()) } else if src.len() >= MAX_BUFFER_SIZE { @@ -298,14 +298,14 @@ impl Decoder for ResponseDecoder { _ => (), } - inner.headers.append(name, value); + inner.head.headers.append(name, value); } else { return Err(ParseError::Header); } } inner.status = status; - inner.version = version; + inner.head.version = version; } msg }; @@ -318,7 +318,7 @@ impl Decoder for ResponseDecoder { // Content-Length PayloadType::Payload(PayloadDecoder::length(len)) } else if msg.inner.status == StatusCode::SWITCHING_PROTOCOLS - || msg.inner.method == Method::CONNECT + || msg.inner.head.method == Method::CONNECT { // switching protocol or connect PayloadType::Stream(PayloadDecoder::eof()) diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index caad113d9..7527eeea3 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -9,10 +9,9 @@ use http::header::{HeaderValue, ACCEPT_ENCODING, CONTENT_LENGTH}; use http::{StatusCode, Version}; use body::{Binary, Body}; -use client::RequestHead; use header::ContentEncoding; use http::Method; -use request::Request; +use request::{Request, RequestHead}; use response::Response; #[derive(Debug)] diff --git a/src/request.rs b/src/request.rs index fb5cb183b..edad4d508 100644 --- a/src/request.rs +++ b/src/request.rs @@ -23,12 +23,28 @@ pub struct Request { pub(crate) inner: Rc, } -pub struct Message { - pub version: Version, - pub status: StatusCode, +pub struct RequestHead { + pub uri: Uri, pub method: Method, - pub url: Url, + pub version: Version, pub headers: HeaderMap, +} + +impl Default for RequestHead { + fn default() -> RequestHead { + RequestHead { + uri: Uri::default(), + method: Method::default(), + version: Version::HTTP_11, + headers: HeaderMap::with_capacity(16), + } + } +} + +pub struct Message { + pub head: RequestHead, + pub url: Url, + pub status: StatusCode, pub extensions: RefCell, pub payload: RefCell>, pub(crate) pool: &'static MessagePool, @@ -39,7 +55,7 @@ impl Message { #[inline] /// Reset request instance pub fn reset(&mut self) { - self.headers.clear(); + self.head.clear(); self.extensions.borrow_mut().clear(); self.flags.set(MessageFlags::empty()); *self.payload.borrow_mut() = None; @@ -50,7 +66,7 @@ impl HttpMessage for Request { type Stream = Payload; fn headers(&self) -> &HeaderMap { - &self.inner.headers + &self.inner.head.headers } #[inline] @@ -74,11 +90,9 @@ impl Request { Request { inner: Rc::new(Message { pool, - method: Method::GET, - status: StatusCode::OK, url: Url::default(), - version: Version::HTTP_11, - headers: HeaderMap::with_capacity(16), + head: RequestHead::default(), + status: StatusCode::OK, flags: Cell::new(MessageFlags::empty()), payload: RefCell::new(None), extensions: RefCell::new(Extensions::new()), @@ -98,27 +112,28 @@ impl Request { Rc::get_mut(&mut self.inner).expect("Multiple copies exist") } - #[inline] - pub fn url(&self) -> &Url { - &self.inner().url - } - - /// Read the Request Uri. + /// Request's uri. #[inline] pub fn uri(&self) -> &Uri { - self.inner().url.uri() + &self.inner().head.uri + } + + /// Mutable reference to the request's uri. + #[inline] + pub fn uri_mut(&mut self) -> &mut Uri { + &mut self.inner_mut().head.uri } /// Read the Request method. #[inline] pub fn method(&self) -> &Method { - &self.inner().method + &self.inner().head.method } /// Read the Request Version. #[inline] pub fn version(&self) -> Version { - self.inner().version + self.inner().head.version } /// The target path of this Request. @@ -130,13 +145,13 @@ impl Request { #[inline] /// Returns Request's headers. pub fn headers(&self) -> &HeaderMap { - &self.inner().headers + &self.inner().head.headers } #[inline] /// Returns mutable Request's headers. pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.inner_mut().headers + &mut self.inner_mut().head.headers } /// Checks if a connection should be kept alive. @@ -159,12 +174,12 @@ impl Request { /// Check if request requires connection upgrade pub fn upgrade(&self) -> bool { - if let Some(conn) = self.inner().headers.get(header::CONNECTION) { + if let Some(conn) = self.inner().head.headers.get(header::CONNECTION) { if let Ok(s) = conn.to_str() { return s.to_lowercase().contains("upgrade"); } } - self.inner().method == Method::CONNECT + self.inner().head.method == Method::CONNECT } #[doc(hidden)] diff --git a/src/test.rs b/src/test.rs index 71145cee8..b0b8dc83d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -391,10 +391,10 @@ impl TestRequest { let mut req = Request::new(); { let inner = req.inner_mut(); - inner.method = method; + inner.head.method = method; inner.url = InnerUrl::new(uri); - inner.version = version; - inner.headers = headers; + inner.head.version = version; + inner.head.headers = headers; *inner.payload.borrow_mut() = payload; } // req.set_cookies(cookies); From 625469f0f421fef4ee7266fd459c6efed31cbdb0 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 16 Nov 2018 19:28:07 -0800 Subject: [PATCH 068/427] refactor decoder --- src/client/pipeline.rs | 2 +- src/client/request.rs | 2 +- src/client/response.rs | 64 ++--- src/h1/client.rs | 21 +- src/h1/codec.rs | 13 +- src/h1/decoder.rs | 551 ++++++++++++++++++++--------------------- src/h1/encoder.rs | 3 +- src/h1/mod.rs | 1 - src/lib.rs | 1 + src/message.rs | 163 ++++++++++++ src/request.rs | 148 ++--------- src/test.rs | 2 +- src/uri.rs | 17 +- 13 files changed, 493 insertions(+), 495 deletions(-) create mode 100644 src/message.rs diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs index 26bd1c62a..17ba93e7c 100644 --- a/src/client/pipeline.rs +++ b/src/client/pipeline.rs @@ -13,7 +13,7 @@ use super::{Connect, Connection}; use body::{BodyType, MessageBody, PayloadStream}; use error::PayloadError; use h1; -use request::RequestHead; +use message::RequestHead; pub(crate) fn send_request( head: RequestHead, diff --git a/src/client/request.rs b/src/client/request.rs index c4c7f2f6a..d3d1544c2 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -16,7 +16,7 @@ use http::{ uri, Error as HttpError, HeaderMap, HeaderName, HeaderValue, HttpTryFrom, Method, Uri, Version, }; -use request::RequestHead; +use message::RequestHead; use super::response::ClientResponse; use super::{pipeline, Connect, Connection, ConnectorError, SendRequestError}; diff --git a/src/client/response.rs b/src/client/response.rs index 56e13fa47..797c88df8 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -1,6 +1,5 @@ -use std::cell::{Cell, Ref, RefCell, RefMut}; +use std::cell::RefCell; use std::fmt; -use std::rc::Rc; use bytes::Bytes; use futures::{Async, Poll, Stream}; @@ -8,16 +7,14 @@ use http::{HeaderMap, StatusCode, Version}; use body::PayloadStream; use error::PayloadError; -use extensions::Extensions; use httpmessage::HttpMessage; -use request::{Message, MessageFlags, MessagePool, RequestHead}; -use uri::Url; +use message::{MessageFlags, ResponseHead}; use super::pipeline::Payload; /// Client Response pub struct ClientResponse { - pub(crate) inner: Rc, + pub(crate) head: ResponseHead, pub(crate) payload: RefCell>, } @@ -25,7 +22,7 @@ impl HttpMessage for ClientResponse { type Stream = PayloadStream; fn headers(&self) -> &HeaderMap { - &self.inner.head.headers + &self.head.headers } #[inline] @@ -41,75 +38,50 @@ impl HttpMessage for ClientResponse { impl ClientResponse { /// Create new Request instance pub fn new() -> ClientResponse { - ClientResponse::with_pool(MessagePool::pool()) - } - - /// Create new Request instance with pool - pub(crate) fn with_pool(pool: &'static MessagePool) -> ClientResponse { ClientResponse { - inner: Rc::new(Message { - pool, - head: RequestHead::default(), - status: StatusCode::OK, - url: Url::default(), - flags: Cell::new(MessageFlags::empty()), - payload: RefCell::new(None), - extensions: RefCell::new(Extensions::new()), - }), + head: ResponseHead::default(), payload: RefCell::new(None), } } #[inline] - pub(crate) fn inner(&self) -> &Message { - self.inner.as_ref() + pub(crate) fn head(&self) -> &ResponseHead { + &self.head } #[inline] - pub(crate) fn inner_mut(&mut self) -> &mut Message { - Rc::get_mut(&mut self.inner).expect("Multiple copies exist") + pub(crate) fn head_mut(&mut self) -> &mut ResponseHead { + &mut self.head } /// Read the Request Version. #[inline] pub fn version(&self) -> Version { - self.inner().head.version + self.head().version.clone().unwrap() } /// Get the status from the server. #[inline] pub fn status(&self) -> StatusCode { - self.inner().status + self.head().status } #[inline] /// Returns Request's headers. pub fn headers(&self) -> &HeaderMap { - &self.inner().head.headers + &self.head().headers } #[inline] /// Returns mutable Request's headers. pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.inner_mut().head.headers + &mut self.head_mut().headers } /// Checks if a connection should be kept alive. #[inline] pub fn keep_alive(&self) -> bool { - self.inner().flags.get().contains(MessageFlags::KEEPALIVE) - } - - /// Request extensions - #[inline] - pub fn extensions(&self) -> Ref { - self.inner().extensions.borrow() - } - - /// Mutable reference to a the request's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut { - self.inner().extensions.borrow_mut() + self.head().flags.contains(MessageFlags::KEEPALIVE) } } @@ -126,14 +98,6 @@ impl Stream for ClientResponse { } } -impl Drop for ClientResponse { - fn drop(&mut self) { - if Rc::strong_count(&self.inner) == 1 { - self.inner.pool.release(self.inner.clone()); - } - } -} - impl fmt::Debug for ClientResponse { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status(),)?; diff --git a/src/h1/client.rs b/src/h1/client.rs index 81a6f5689..8d4051d3e 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -4,7 +4,7 @@ use std::io::{self, Write}; use bytes::{BufMut, Bytes, BytesMut}; use tokio_codec::{Decoder, Encoder}; -use super::decoder::{PayloadDecoder, PayloadItem, PayloadType, ResponseDecoder}; +use super::decoder::{MessageDecoder, PayloadDecoder, PayloadItem, PayloadType}; use super::encoder::{RequestEncoder, ResponseLength}; use super::{Message, MessageType}; use body::{Binary, Body, BodyType}; @@ -16,7 +16,7 @@ use http::header::{ HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE, }; use http::{Method, Version}; -use request::{MessagePool, RequestHead}; +use message::{MessagePool, RequestHead}; bitflags! { struct Flags: u8 { @@ -42,7 +42,7 @@ pub struct ClientPayloadCodec { struct ClientCodecInner { config: ServiceConfig, - decoder: ResponseDecoder, + decoder: MessageDecoder, payload: Option, version: Version, @@ -63,11 +63,6 @@ impl ClientCodec { /// /// `keepalive_enabled` how response `connection` header get generated. pub fn new(config: ServiceConfig) -> Self { - ClientCodec::with_pool(MessagePool::pool(), config) - } - - /// Create HTTP/1 codec with request's pool - pub(crate) fn with_pool(pool: &'static MessagePool, config: ServiceConfig) -> Self { let flags = if config.keep_alive_enabled() { Flags::KEEPALIVE_ENABLED } else { @@ -76,7 +71,7 @@ impl ClientCodec { ClientCodec { inner: ClientCodecInner { config, - decoder: ResponseDecoder::with_pool(pool), + decoder: MessageDecoder::default(), payload: None, version: Version::HTTP_11, @@ -185,10 +180,10 @@ impl Decoder for ClientCodec { debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set"); if let Some((req, payload)) = self.inner.decoder.decode(src)? { - self.inner - .flags - .set(Flags::HEAD, req.inner.head.method == Method::HEAD); - self.inner.version = req.inner.head.version; + // self.inner + // .flags + // .set(Flags::HEAD, req.head.method == Method::HEAD); + // self.inner.version = req.head.version; if self.inner.flags.contains(Flags::KEEPALIVE_ENABLED) { self.inner.flags.set(Flags::KEEPALIVE, req.keep_alive()); } diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 57e848982..c1c6091de 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -5,7 +5,7 @@ use std::io::{self, Write}; use bytes::{BufMut, Bytes, BytesMut}; use tokio_codec::{Decoder, Encoder}; -use super::decoder::{PayloadDecoder, PayloadItem, PayloadType, RequestDecoder}; +use super::decoder::{MessageDecoder, PayloadDecoder, PayloadItem, PayloadType}; use super::encoder::{ResponseEncoder, ResponseLength}; use super::{Message, MessageType}; use body::{Binary, Body}; @@ -14,7 +14,7 @@ use error::ParseError; use helpers; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::{Method, Version}; -use request::{MessagePool, Request}; +use request::Request; use response::Response; bitflags! { @@ -32,7 +32,7 @@ const AVERAGE_HEADER_SIZE: usize = 30; /// HTTP/1 Codec pub struct Codec { config: ServiceConfig, - decoder: RequestDecoder, + decoder: MessageDecoder, payload: Option, version: Version, @@ -59,11 +59,6 @@ impl Codec { /// /// `keepalive_enabled` how response `connection` header get generated. pub fn new(config: ServiceConfig) -> Self { - Codec::with_pool(MessagePool::pool(), config) - } - - /// Create HTTP/1 codec with request's pool - pub(crate) fn with_pool(pool: &'static MessagePool, config: ServiceConfig) -> Self { let flags = if config.keep_alive_enabled() { Flags::KEEPALIVE_ENABLED } else { @@ -71,7 +66,7 @@ impl Codec { }; Codec { config, - decoder: RequestDecoder::with_pool(pool), + decoder: MessageDecoder::default(), payload: None, version: Version::HTTP_11, diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 75bd0f7c1..fe2aa707a 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -1,3 +1,4 @@ +use std::marker::PhantomData; use std::{io, mem}; use bytes::{Bytes, BytesMut}; @@ -8,327 +9,305 @@ use tokio_codec::Decoder; use client::ClientResponse; use error::ParseError; use http::header::{HeaderName, HeaderValue}; -use http::{header, HttpTryFrom, Method, StatusCode, Uri, Version}; -use request::{MessageFlags, MessagePool, Request}; -use uri::Url; +use http::{header, HeaderMap, HttpTryFrom, Method, StatusCode, Uri, Version}; +use message::MessageFlags; +use request::Request; const MAX_BUFFER_SIZE: usize = 131_072; const MAX_HEADERS: usize = 96; -/// Client request decoder -pub struct RequestDecoder(&'static MessagePool); - -/// Server response decoder -pub struct ResponseDecoder(&'static MessagePool); +/// Incoming messagd decoder +pub(crate) struct MessageDecoder(PhantomData); /// Incoming request type -pub enum PayloadType { +pub(crate) enum PayloadType { None, Payload(PayloadDecoder), Stream(PayloadDecoder), } -impl RequestDecoder { - pub(crate) fn with_pool(pool: &'static MessagePool) -> RequestDecoder { - RequestDecoder(pool) +impl Default for MessageDecoder { + fn default() -> Self { + MessageDecoder(PhantomData) } } -impl Default for RequestDecoder { - fn default() -> RequestDecoder { - RequestDecoder::with_pool(MessagePool::pool()) - } -} - -impl Decoder for RequestDecoder { - type Item = (Request, PayloadType); +impl Decoder for MessageDecoder { + type Item = (T, PayloadType); type Error = ParseError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - // Parse http message + T::decode(src) + } +} + +pub(crate) enum PayloadLength { + None, + Chunked, + Upgrade, + Length(u64), +} + +pub(crate) trait MessageTypeDecoder: Sized { + fn keep_alive(&mut self); + + fn headers_mut(&mut self) -> &mut HeaderMap; + + fn decode(src: &mut BytesMut) -> Result, ParseError>; + + fn process_headers( + &mut self, + slice: &Bytes, + version: Version, + raw_headers: &[HeaderIndex], + ) -> Result { + let mut ka = version != Version::HTTP_10; let mut has_upgrade = false; let mut chunked = false; let mut content_length = None; - let msg = { - // Unsafe: we read only this data only after httparse parses headers into. - // performance bump for pipeline benchmarks. - let mut headers: [HeaderIndex; MAX_HEADERS] = + { + let headers = self.headers_mut(); + + for idx in raw_headers.iter() { + if let Ok(name) = HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]) + { + // Unsafe: httparse check header value for valid utf-8 + let value = unsafe { + HeaderValue::from_shared_unchecked( + slice.slice(idx.value.0, idx.value.1), + ) + }; + match name { + header::CONTENT_LENGTH => { + if let Ok(s) = value.to_str() { + if let Ok(len) = s.parse::() { + content_length = Some(len); + } else { + debug!("illegal Content-Length: {:?}", s); + return Err(ParseError::Header); + } + } else { + debug!("illegal Content-Length: {:?}", value); + return Err(ParseError::Header); + } + } + // transfer-encoding + header::TRANSFER_ENCODING => { + if let Ok(s) = value.to_str() { + chunked = s.to_lowercase().contains("chunked"); + } else { + return Err(ParseError::Header); + } + } + // connection keep-alive state + header::CONNECTION => { + ka = if let Ok(conn) = value.to_str() { + if version == Version::HTTP_10 + && conn.contains("keep-alive") + { + true + } else { + version == Version::HTTP_11 && !(conn + .contains("close") + || conn.contains("upgrade")) + } + } else { + false + } + } + header::UPGRADE => { + has_upgrade = true; + // check content-length, some clients (dart) + // sends "content-length: 0" with websocket upgrade + if let Ok(val) = value.to_str() { + if val == "websocket" { + content_length = None; + } + } + } + _ => (), + } + + headers.append(name, value); + } else { + return Err(ParseError::Header); + } + } + } + + if ka { + self.keep_alive(); + } + + if chunked { + Ok(PayloadLength::Chunked) + } else if let Some(len) = content_length { + Ok(PayloadLength::Length(len)) + } else if has_upgrade { + Ok(PayloadLength::Upgrade) + } else { + Ok(PayloadLength::None) + } + } +} + +impl MessageTypeDecoder for Request { + fn keep_alive(&mut self) { + self.inner_mut().flags.set(MessageFlags::KEEPALIVE); + } + + fn headers_mut(&mut self) -> &mut HeaderMap { + self.headers_mut() + } + + fn decode(src: &mut BytesMut) -> Result, ParseError> { + // Unsafe: we read only this data only after httparse parses headers into. + // performance bump for pipeline benchmarks. + let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { mem::uninitialized() }; + + let (len, method, uri, version, headers_len) = { + let mut parsed: [httparse::Header; MAX_HEADERS] = unsafe { mem::uninitialized() }; - let (len, method, path, version, headers_len) = { - let mut parsed: [httparse::Header; MAX_HEADERS] = - unsafe { mem::uninitialized() }; - - let mut req = httparse::Request::new(&mut parsed); - match req.parse(src)? { - httparse::Status::Complete(len) => { - let method = Method::from_bytes(req.method.unwrap().as_bytes()) - .map_err(|_| ParseError::Method)?; - let path = Url::new(Uri::try_from(req.path.unwrap())?); - let version = if req.version.unwrap() == 1 { - Version::HTTP_11 - } else { - Version::HTTP_10 - }; - HeaderIndex::record(src, req.headers, &mut headers); - - (len, method, path, version, req.headers.len()) - } - httparse::Status::Partial => return Ok(None), - } - }; - - let slice = src.split_to(len).freeze(); - - // convert headers - let mut msg = MessagePool::get_request(self.0); - { - let inner = msg.inner_mut(); - inner - .flags - .get_mut() - .set(MessageFlags::KEEPALIVE, version != Version::HTTP_10); - - for idx in headers[..headers_len].iter() { - if let Ok(name) = - HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]) - { - // Unsafe: httparse check header value for valid utf-8 - let value = unsafe { - HeaderValue::from_shared_unchecked( - slice.slice(idx.value.0, idx.value.1), - ) - }; - match name { - header::CONTENT_LENGTH => { - if let Ok(s) = value.to_str() { - if let Ok(len) = s.parse::() { - content_length = Some(len); - } else { - debug!("illegal Content-Length: {:?}", len); - return Err(ParseError::Header); - } - } else { - debug!("illegal Content-Length: {:?}", len); - return Err(ParseError::Header); - } - } - // transfer-encoding - header::TRANSFER_ENCODING => { - if let Ok(s) = value.to_str() { - chunked = s.to_lowercase().contains("chunked"); - } else { - return Err(ParseError::Header); - } - } - // connection keep-alive state - header::CONNECTION => { - let ka = if let Ok(conn) = value.to_str() { - if version == Version::HTTP_10 - && conn.contains("keep-alive") - { - true - } else { - version == Version::HTTP_11 && !(conn - .contains("close") - || conn.contains("upgrade")) - } - } else { - false - }; - inner.flags.get_mut().set(MessageFlags::KEEPALIVE, ka); - } - header::UPGRADE => { - has_upgrade = true; - // check content-length, some clients (dart) - // sends "content-length: 0" with websocket upgrade - if let Ok(val) = value.to_str() { - if val == "websocket" { - content_length = None; - } - } - } - _ => (), - } - - inner.head.headers.append(name, value); + let mut req = httparse::Request::new(&mut parsed); + match req.parse(src)? { + httparse::Status::Complete(len) => { + let method = Method::from_bytes(req.method.unwrap().as_bytes()) + .map_err(|_| ParseError::Method)?; + let uri = Uri::try_from(req.path.unwrap())?; + let version = if req.version.unwrap() == 1 { + Version::HTTP_11 } else { - return Err(ParseError::Header); - } - } + Version::HTTP_10 + }; + HeaderIndex::record(src, req.headers, &mut headers); - inner.url = path; - inner.head.method = method; - inner.head.version = version; + (len, method, uri, version, req.headers.len()) + } + httparse::Status::Partial => return Ok(None), } - msg }; + // convert headers + let mut msg = Request::new(); + + let len = msg.process_headers( + &src.split_to(len).freeze(), + version, + &headers[..headers_len], + )?; + // https://tools.ietf.org/html/rfc7230#section-3.3.3 - let decoder = if chunked { - // Chunked encoding - PayloadType::Payload(PayloadDecoder::chunked()) - } else if let Some(len) = content_length { - // Content-Length - PayloadType::Payload(PayloadDecoder::length(len)) - } else if has_upgrade || msg.inner.head.method == Method::CONNECT { - // upgrade(websocket) or connect - PayloadType::Stream(PayloadDecoder::eof()) - } else if src.len() >= MAX_BUFFER_SIZE { - error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); - return Err(ParseError::TooLarge); - } else { - PayloadType::None + let decoder = match len { + PayloadLength::Chunked => { + // Chunked encoding + PayloadType::Payload(PayloadDecoder::chunked()) + } + PayloadLength::Length(len) => { + // Content-Length + PayloadType::Payload(PayloadDecoder::length(len)) + } + PayloadLength::Upgrade => { + // upgrade(websocket) or connect + PayloadType::Stream(PayloadDecoder::eof()) + } + PayloadLength::None => { + if method == Method::CONNECT { + // upgrade(websocket) or connect + PayloadType::Stream(PayloadDecoder::eof()) + } else if src.len() >= MAX_BUFFER_SIZE { + error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); + return Err(ParseError::TooLarge); + } else { + PayloadType::None + } + } }; + { + let inner = msg.inner_mut(); + inner.url.update(&uri); + inner.head.uri = uri; + inner.head.method = method; + inner.head.version = version; + } + Ok(Some((msg, decoder))) } } -impl ResponseDecoder { - pub(crate) fn with_pool(pool: &'static MessagePool) -> ResponseDecoder { - ResponseDecoder(pool) +impl MessageTypeDecoder for ClientResponse { + fn keep_alive(&mut self) { + self.head.flags.insert(MessageFlags::KEEPALIVE); } -} -impl Default for ResponseDecoder { - fn default() -> ResponseDecoder { - ResponseDecoder::with_pool(MessagePool::pool()) + fn headers_mut(&mut self) -> &mut HeaderMap { + self.headers_mut() } -} -impl Decoder for ResponseDecoder { - type Item = (ClientResponse, PayloadType); - type Error = ParseError; + fn decode(src: &mut BytesMut) -> Result, ParseError> { + // Unsafe: we read only this data only after httparse parses headers into. + // performance bump for pipeline benchmarks. + let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { mem::uninitialized() }; - fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - // Parse http message - let mut chunked = false; - let mut content_length = None; - - let msg = { - // Unsafe: we read only this data only after httparse parses headers into. - // performance bump for pipeline benchmarks. - let mut headers: [HeaderIndex; MAX_HEADERS] = + let (len, version, status, headers_len) = { + let mut parsed: [httparse::Header; MAX_HEADERS] = unsafe { mem::uninitialized() }; - let (len, version, status, headers_len) = { - let mut parsed: [httparse::Header; MAX_HEADERS] = - unsafe { mem::uninitialized() }; - - let mut res = httparse::Response::new(&mut parsed); - match res.parse(src)? { - httparse::Status::Complete(len) => { - let version = if res.version.unwrap() == 1 { - Version::HTTP_11 - } else { - Version::HTTP_10 - }; - let status = StatusCode::from_u16(res.code.unwrap()) - .map_err(|_| ParseError::Status)?; - HeaderIndex::record(src, res.headers, &mut headers); - - (len, version, status, res.headers.len()) - } - httparse::Status::Partial => return Ok(None), - } - }; - - let slice = src.split_to(len).freeze(); - - // convert headers - let mut msg = MessagePool::get_response(self.0); - { - let inner = msg.inner_mut(); - inner - .flags - .get_mut() - .set(MessageFlags::KEEPALIVE, version != Version::HTTP_10); - - for idx in headers[..headers_len].iter() { - if let Ok(name) = - HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]) - { - // Unsafe: httparse check header value for valid utf-8 - let value = unsafe { - HeaderValue::from_shared_unchecked( - slice.slice(idx.value.0, idx.value.1), - ) - }; - match name { - header::CONTENT_LENGTH => { - if let Ok(s) = value.to_str() { - if let Ok(len) = s.parse::() { - content_length = Some(len); - } else { - debug!("illegal Content-Length: {:?}", len); - return Err(ParseError::Header); - } - } else { - debug!("illegal Content-Length: {:?}", len); - return Err(ParseError::Header); - } - } - // transfer-encoding - header::TRANSFER_ENCODING => { - if let Ok(s) = value.to_str() { - chunked = s.to_lowercase().contains("chunked"); - } else { - return Err(ParseError::Header); - } - } - // connection keep-alive state - header::CONNECTION => { - let ka = if let Ok(conn) = value.to_str() { - if version == Version::HTTP_10 - && conn.contains("keep-alive") - { - true - } else { - version == Version::HTTP_11 && !(conn - .contains("close") - || conn.contains("upgrade")) - } - } else { - false - }; - inner.flags.get_mut().set(MessageFlags::KEEPALIVE, ka); - } - _ => (), - } - - inner.head.headers.append(name, value); + let mut res = httparse::Response::new(&mut parsed); + match res.parse(src)? { + httparse::Status::Complete(len) => { + let version = if res.version.unwrap() == 1 { + Version::HTTP_11 } else { - return Err(ParseError::Header); - } - } + Version::HTTP_10 + }; + let status = StatusCode::from_u16(res.code.unwrap()) + .map_err(|_| ParseError::Status)?; + HeaderIndex::record(src, res.headers, &mut headers); - inner.status = status; - inner.head.version = version; + (len, version, status, res.headers.len()) + } + httparse::Status::Partial => return Ok(None), } - msg }; + let mut msg = ClientResponse::new(); + + // convert headers + let len = msg.process_headers( + &src.split_to(len).freeze(), + version, + &headers[..headers_len], + )?; + // https://tools.ietf.org/html/rfc7230#section-3.3.3 - let decoder = if chunked { - // Chunked encoding - PayloadType::Payload(PayloadDecoder::chunked()) - } else if let Some(len) = content_length { - // Content-Length - PayloadType::Payload(PayloadDecoder::length(len)) - } else if msg.inner.status == StatusCode::SWITCHING_PROTOCOLS - || msg.inner.head.method == Method::CONNECT - { - // switching protocol or connect - PayloadType::Stream(PayloadDecoder::eof()) - } else if src.len() >= MAX_BUFFER_SIZE { - error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); - return Err(ParseError::TooLarge); - } else { - PayloadType::None + let decoder = match len { + PayloadLength::Chunked => { + // Chunked encoding + PayloadType::Payload(PayloadDecoder::chunked()) + } + PayloadLength::Length(len) => { + // Content-Length + PayloadType::Payload(PayloadDecoder::length(len)) + } + _ => { + if status == StatusCode::SWITCHING_PROTOCOLS { + // switching protocol or connect + PayloadType::Stream(PayloadDecoder::eof()) + } else if src.len() >= MAX_BUFFER_SIZE { + error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); + return Err(ParseError::TooLarge); + } else { + PayloadType::None + } + } }; + msg.head.status = status; + msg.head.version = Some(version); + Ok(Some((msg, decoder))) } } @@ -690,7 +669,7 @@ mod tests { macro_rules! parse_ready { ($e:expr) => {{ - match RequestDecoder::default().decode($e) { + match MessageDecoder::::default().decode($e) { Ok(Some((msg, _))) => msg, Ok(_) => unreachable!("Eof during parsing http request"), Err(err) => unreachable!("Error during parsing http request: {:?}", err), @@ -700,7 +679,7 @@ mod tests { macro_rules! expect_parse_err { ($e:expr) => {{ - match RequestDecoder::default().decode($e) { + match MessageDecoder::::default().decode($e) { Err(err) => match err { ParseError::Io(_) => unreachable!("Parse error expected"), _ => (), @@ -779,7 +758,7 @@ mod tests { fn test_parse() { let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); - let mut reader = RequestDecoder::default(); + let mut reader = MessageDecoder::::default(); match reader.decode(&mut buf) { Ok(Some((req, _))) => { assert_eq!(req.version(), Version::HTTP_11); @@ -794,7 +773,7 @@ mod tests { fn test_parse_partial() { let mut buf = BytesMut::from("PUT /test HTTP/1"); - let mut reader = RequestDecoder::default(); + let mut reader = MessageDecoder::::default(); assert!(reader.decode(&mut buf).unwrap().is_none()); buf.extend(b".1\r\n\r\n"); @@ -808,7 +787,7 @@ mod tests { fn test_parse_post() { let mut buf = BytesMut::from("POST /test2 HTTP/1.0\r\n\r\n"); - let mut reader = RequestDecoder::default(); + let mut reader = MessageDecoder::::default(); let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); assert_eq!(req.version(), Version::HTTP_10); assert_eq!(*req.method(), Method::POST); @@ -820,7 +799,7 @@ mod tests { let mut buf = BytesMut::from("GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); - let mut reader = RequestDecoder::default(); + let mut reader = MessageDecoder::::default(); let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); let mut pl = pl.unwrap(); assert_eq!(req.version(), Version::HTTP_11); @@ -837,7 +816,7 @@ mod tests { let mut buf = BytesMut::from("\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); - let mut reader = RequestDecoder::default(); + let mut reader = MessageDecoder::::default(); let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); let mut pl = pl.unwrap(); assert_eq!(req.version(), Version::HTTP_11); @@ -852,7 +831,7 @@ mod tests { #[test] fn test_parse_partial_eof() { let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); - let mut reader = RequestDecoder::default(); + let mut reader = MessageDecoder::::default(); assert!(reader.decode(&mut buf).unwrap().is_none()); buf.extend(b"\r\n"); @@ -866,7 +845,7 @@ mod tests { fn test_headers_split_field() { let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); - let mut reader = RequestDecoder::default(); + let mut reader = MessageDecoder::::default(); assert!{ reader.decode(&mut buf).unwrap().is_none() } buf.extend(b"t"); @@ -890,7 +869,7 @@ mod tests { Set-Cookie: c1=cookie1\r\n\ Set-Cookie: c2=cookie2\r\n\r\n", ); - let mut reader = RequestDecoder::default(); + let mut reader = MessageDecoder::::default(); let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); let val: Vec<_> = req @@ -1090,7 +1069,7 @@ mod tests { upgrade: websocket\r\n\r\n\ some raw data", ); - let mut reader = RequestDecoder::default(); + let mut reader = MessageDecoder::::default(); let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); assert!(!req.keep_alive()); assert!(req.upgrade()); @@ -1139,7 +1118,7 @@ mod tests { "GET /test HTTP/1.1\r\n\ transfer-encoding: chunked\r\n\r\n", ); - let mut reader = RequestDecoder::default(); + let mut reader = MessageDecoder::::default(); let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); let mut pl = pl.unwrap(); assert!(req.chunked().unwrap()); @@ -1162,7 +1141,7 @@ mod tests { "GET /test HTTP/1.1\r\n\ transfer-encoding: chunked\r\n\r\n", ); - let mut reader = RequestDecoder::default(); + let mut reader = MessageDecoder::::default(); let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); let mut pl = pl.unwrap(); assert!(req.chunked().unwrap()); @@ -1193,7 +1172,7 @@ mod tests { transfer-encoding: chunked\r\n\r\n", ); - let mut reader = RequestDecoder::default(); + let mut reader = MessageDecoder::::default(); let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); let mut pl = pl.unwrap(); assert!(req.chunked().unwrap()); @@ -1238,7 +1217,7 @@ mod tests { transfer-encoding: chunked\r\n\r\n"[..], ); - let mut reader = RequestDecoder::default(); + let mut reader = MessageDecoder::::default(); let (msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); let mut pl = pl.unwrap(); assert!(msg.chunked().unwrap()); diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index 7527eeea3..de45351d1 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -11,7 +11,8 @@ use http::{StatusCode, Version}; use body::{Binary, Body}; use header::ContentEncoding; use http::Method; -use request::{Request, RequestHead}; +use message::RequestHead; +use request::Request; use response::Response; #[derive(Debug)] diff --git a/src/h1/mod.rs b/src/h1/mod.rs index 818387004..395d66199 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -11,7 +11,6 @@ mod service; pub use self::client::{ClientCodec, ClientPayloadCodec}; pub use self::codec::Codec; -pub use self::decoder::{PayloadDecoder, RequestDecoder}; pub use self::dispatcher::Dispatcher; pub use self::service::{H1Service, H1ServiceHandler, OneRequest}; diff --git a/src/lib.rs b/src/lib.rs index 32369d167..f64876e31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,6 +117,7 @@ mod header; mod httpcodes; mod httpmessage; mod json; +mod message; mod payload; mod request; mod response; diff --git a/src/message.rs b/src/message.rs new file mode 100644 index 000000000..b38c2f801 --- /dev/null +++ b/src/message.rs @@ -0,0 +1,163 @@ +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::rc::Rc; + +use http::{HeaderMap, Method, StatusCode, Uri, Version}; + +use extensions::Extensions; +use payload::Payload; +use uri::Url; + +#[doc(hidden)] +pub trait Head: Default + 'static { + fn clear(&mut self); + + fn pool() -> &'static MessagePool; +} + +bitflags! { + pub(crate) struct MessageFlags: u8 { + const KEEPALIVE = 0b0000_0001; + } +} + +pub struct RequestHead { + pub uri: Uri, + pub method: Method, + pub version: Version, + pub headers: HeaderMap, + pub(crate) flags: MessageFlags, +} + +impl Default for RequestHead { + fn default() -> RequestHead { + RequestHead { + uri: Uri::default(), + method: Method::default(), + version: Version::HTTP_11, + headers: HeaderMap::with_capacity(16), + flags: MessageFlags::empty(), + } + } +} + +impl Head for RequestHead { + fn clear(&mut self) { + self.headers.clear(); + self.flags = MessageFlags::empty(); + } + + fn pool() -> &'static MessagePool { + REQUEST_POOL.with(|p| *p) + } +} + +pub struct ResponseHead { + pub version: Option, + pub status: StatusCode, + pub headers: HeaderMap, + pub reason: Option<&'static str>, + pub(crate) flags: MessageFlags, +} + +impl Default for ResponseHead { + fn default() -> ResponseHead { + ResponseHead { + version: None, + status: StatusCode::OK, + headers: HeaderMap::with_capacity(16), + reason: None, + flags: MessageFlags::empty(), + } + } +} + +impl Head for ResponseHead { + fn clear(&mut self) { + self.headers.clear(); + self.flags = MessageFlags::empty(); + } + + fn pool() -> &'static MessagePool { + RESPONSE_POOL.with(|p| *p) + } +} + +pub struct Message { + pub head: T, + pub url: Url, + pub status: StatusCode, + pub extensions: RefCell, + pub payload: RefCell>, + pub(crate) pool: &'static MessagePool, + pub(crate) flags: Cell, +} + +impl Message { + #[inline] + /// Reset request instance + pub fn reset(&mut self) { + self.head.clear(); + self.extensions.borrow_mut().clear(); + self.flags.set(MessageFlags::empty()); + *self.payload.borrow_mut() = None; + } +} + +impl Default for Message { + fn default() -> Self { + Message { + pool: T::pool(), + url: Url::default(), + head: T::default(), + status: StatusCode::OK, + flags: Cell::new(MessageFlags::empty()), + payload: RefCell::new(None), + extensions: RefCell::new(Extensions::new()), + } + } +} + +#[doc(hidden)] +/// Request's objects pool +pub struct MessagePool(RefCell>>>); + +thread_local!(static REQUEST_POOL: &'static MessagePool = MessagePool::::create()); +thread_local!(static RESPONSE_POOL: &'static MessagePool = MessagePool::::create()); + +impl MessagePool { + /// Get default request's pool + pub fn pool() -> &'static MessagePool { + REQUEST_POOL.with(|p| *p) + } + + /// Get Request object + #[inline] + pub fn get_message() -> Rc> { + REQUEST_POOL.with(|pool| { + if let Some(mut msg) = pool.0.borrow_mut().pop_front() { + if let Some(r) = Rc::get_mut(&mut msg) { + r.reset(); + } + return msg; + } + Rc::new(Message::default()) + }) + } +} + +impl MessagePool { + fn create() -> &'static MessagePool { + let pool = MessagePool(RefCell::new(VecDeque::with_capacity(128))); + Box::leak(Box::new(pool)) + } + + #[inline] + /// Release request instance + pub(crate) fn release(&self, msg: Rc>) { + let v = &mut self.0.borrow_mut(); + if v.len() < 128 { + v.push_front(msg); + } + } +} diff --git a/src/request.rs b/src/request.rs index edad4d508..1e191047a 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,65 +1,18 @@ -use std::cell::{Cell, Ref, RefCell, RefMut}; -use std::collections::VecDeque; +use std::cell::{Ref, RefMut}; use std::fmt; use std::rc::Rc; -use http::{header, HeaderMap, Method, StatusCode, Uri, Version}; +use http::{header, HeaderMap, Method, Uri, Version}; -use client::ClientResponse; use extensions::Extensions; use httpmessage::HttpMessage; use payload::Payload; -use uri::Url; -bitflags! { - pub(crate) struct MessageFlags: u8 { - const KEEPALIVE = 0b0000_0001; - const CONN_INFO = 0b0000_0010; - } -} +use message::{Message, MessageFlags, MessagePool, RequestHead}; /// Request pub struct Request { - pub(crate) inner: Rc, -} - -pub struct RequestHead { - pub uri: Uri, - pub method: Method, - pub version: Version, - pub headers: HeaderMap, -} - -impl Default for RequestHead { - fn default() -> RequestHead { - RequestHead { - uri: Uri::default(), - method: Method::default(), - version: Version::HTTP_11, - headers: HeaderMap::with_capacity(16), - } - } -} - -pub struct Message { - pub head: RequestHead, - pub url: Url, - pub status: StatusCode, - pub extensions: RefCell, - pub payload: RefCell>, - pub(crate) pool: &'static MessagePool, - pub(crate) flags: Cell, -} - -impl Message { - #[inline] - /// Reset request instance - pub fn reset(&mut self) { - self.head.clear(); - self.extensions.borrow_mut().clear(); - self.flags.set(MessageFlags::empty()); - *self.payload.borrow_mut() = None; - } + pub(crate) inner: Rc>, } impl HttpMessage for Request { @@ -82,33 +35,35 @@ impl HttpMessage for Request { impl Request { /// Create new Request instance pub fn new() -> Request { - Request::with_pool(MessagePool::pool()) - } - - /// Create new Request instance with pool - pub(crate) fn with_pool(pool: &'static MessagePool) -> Request { Request { - inner: Rc::new(Message { - pool, - url: Url::default(), - head: RequestHead::default(), - status: StatusCode::OK, - flags: Cell::new(MessageFlags::empty()), - payload: RefCell::new(None), - extensions: RefCell::new(Extensions::new()), - }), + inner: MessagePool::get_message(), } } + // /// Create new Request instance with pool + // pub(crate) fn with_pool(pool: &'static MessagePool) -> Request { + // Request { + // inner: Rc::new(Message { + // pool, + // url: Url::default(), + // head: RequestHead::default(), + // status: StatusCode::OK, + // flags: Cell::new(MessageFlags::empty()), + // payload: RefCell::new(None), + // extensions: RefCell::new(Extensions::new()), + // }), + // } + // } + #[inline] #[doc(hidden)] - pub fn inner(&self) -> &Message { + pub fn inner(&self) -> &Message { self.inner.as_ref() } #[inline] #[doc(hidden)] - pub fn inner_mut(&mut self) -> &mut Message { + pub fn inner_mut(&mut self) -> &mut Message { Rc::get_mut(&mut self.inner).expect("Multiple copies exist") } @@ -139,7 +94,11 @@ impl Request { /// The target path of this Request. #[inline] pub fn path(&self) -> &str { - self.inner().url.path() + if let Some(path) = self.inner().url.path() { + path + } else { + self.inner().head.uri.path() + } } #[inline] @@ -219,56 +178,3 @@ impl fmt::Debug for Request { Ok(()) } } - -/// Request's objects pool -pub(crate) struct MessagePool(RefCell>>); - -thread_local!(static POOL: &'static MessagePool = MessagePool::create()); - -impl MessagePool { - fn create() -> &'static MessagePool { - let pool = MessagePool(RefCell::new(VecDeque::with_capacity(128))); - Box::leak(Box::new(pool)) - } - - /// Get default request's pool - pub fn pool() -> &'static MessagePool { - POOL.with(|p| *p) - } - - /// Get Request object - #[inline] - pub fn get_request(pool: &'static MessagePool) -> Request { - if let Some(mut msg) = pool.0.borrow_mut().pop_front() { - if let Some(r) = Rc::get_mut(&mut msg) { - r.reset(); - } - return Request { inner: msg }; - } - Request::with_pool(pool) - } - - /// Get Client Response object - #[inline] - pub fn get_response(pool: &'static MessagePool) -> ClientResponse { - if let Some(mut msg) = pool.0.borrow_mut().pop_front() { - if let Some(r) = Rc::get_mut(&mut msg) { - r.reset(); - } - return ClientResponse { - inner: msg, - payload: RefCell::new(None), - }; - } - ClientResponse::with_pool(pool) - } - - #[inline] - /// Release request instance - pub(crate) fn release(&self, msg: Rc) { - let v = &mut self.0.borrow_mut(); - if v.len() < 128 { - v.push_front(msg); - } - } -} diff --git a/src/test.rs b/src/test.rs index b0b8dc83d..439749343 100644 --- a/src/test.rs +++ b/src/test.rs @@ -392,7 +392,7 @@ impl TestRequest { { let inner = req.inner_mut(); inner.head.method = method; - inner.url = InnerUrl::new(uri); + inner.url = InnerUrl::new(&uri); inner.head.version = version; inner.head.headers = headers; *inner.payload.borrow_mut() = payload; diff --git a/src/uri.rs b/src/uri.rs index 6edd220ce..89f6d3b1e 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -37,27 +37,22 @@ lazy_static! { #[derive(Default, Clone, Debug)] pub struct Url { - uri: Uri, path: Option>, } impl Url { - pub fn new(uri: Uri) -> Url { + pub fn new(uri: &Uri) -> Url { let path = DEFAULT_QUOTER.requote(uri.path().as_bytes()); - Url { uri, path } + Url { path } } - pub fn uri(&self) -> &Uri { - &self.uri + pub(crate) fn update(&mut self, uri: &Uri) { + self.path = DEFAULT_QUOTER.requote(uri.path().as_bytes()); } - pub fn path(&self) -> &str { - if let Some(ref s) = self.path { - s - } else { - self.uri.path() - } + pub fn path(&self) -> Option<&str> { + self.path.as_ref().map(|s| s.as_str()) } } From aa20e2670d44b893ae47ad01c61fc1fd3b65dcce Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 16 Nov 2018 21:09:33 -0800 Subject: [PATCH 069/427] refactor h1 dispatcher --- src/h1/client.rs | 69 +++++++-- src/h1/decoder.rs | 90 +++++------- src/h1/dispatcher.rs | 338 ++++++++++++++++++++++--------------------- 3 files changed, 267 insertions(+), 230 deletions(-) diff --git a/src/h1/client.rs b/src/h1/client.rs index 8d4051d3e..f871cb338 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -125,6 +125,15 @@ impl ClientPayloadCodec { } } +fn prn_version(ver: Version) -> &'static str { + match ver { + Version::HTTP_09 => "HTTP/0.9", + Version::HTTP_10 => "HTTP/1.0", + Version::HTTP_11 => "HTTP/1.1", + Version::HTTP_2 => "HTTP/2.0", + } +} + impl ClientCodecInner { fn encode_response( &mut self, @@ -135,33 +144,63 @@ impl ClientCodecInner { // render message { // status line - writeln!( + write!( Writer(buffer), - "{} {} {:?}\r", + "{} {} {}", msg.method, msg.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"), - msg.version + prn_version(msg.version) ).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; // write headers buffer.reserve(msg.headers.len() * AVERAGE_HEADER_SIZE); - for (key, value) in &msg.headers { - let v = value.as_ref(); - let k = key.as_str().as_bytes(); - buffer.reserve(k.len() + v.len() + 4); - buffer.put_slice(k); - buffer.put_slice(b": "); - buffer.put_slice(v); - buffer.put_slice(b"\r\n"); - // Connection upgrade - if key == UPGRADE { - self.flags.insert(Flags::UPGRADE); + // content length + let mut len_is_set = true; + match btype { + BodyType::Sized(len) => { + buffer.extend_from_slice(b"\r\ncontent-length: "); + write!(buffer.writer(), "{}", len)?; + buffer.extend_from_slice(b"\r\n"); } + BodyType::Unsized => { + buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") + } + BodyType::Zero => { + len_is_set = false; + buffer.extend_from_slice(b"\r\n") + } + BodyType::None => buffer.extend_from_slice(b"\r\n"), + } + + let mut has_date = false; + + for (key, value) in &msg.headers { + match *key { + TRANSFER_ENCODING => continue, + CONTENT_LENGTH => match btype { + BodyType::None => (), + BodyType::Zero => len_is_set = true, + _ => continue, + }, + DATE => has_date = true, + UPGRADE => self.flags.insert(Flags::UPGRADE), + _ => (), + } + + buffer.put_slice(key.as_ref()); + buffer.put_slice(b": "); + buffer.put_slice(value.as_ref()); + buffer.put_slice(b"\r\n"); + } + + // set content length + if !len_is_set { + buffer.extend_from_slice(b"content-length: 0\r\n") } // set date header - if !msg.headers.contains_key(DATE) { + if !has_date { self.config.set_date(buffer); } else { buffer.extend_from_slice(b"\r\n"); diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index fe2aa707a..61cab7ad8 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -42,10 +42,9 @@ impl Decoder for MessageDecoder { } pub(crate) enum PayloadLength { - None, - Chunked, + Payload(PayloadType), Upgrade, - Length(u64), + None, } pub(crate) trait MessageTypeDecoder: Sized { @@ -55,7 +54,7 @@ pub(crate) trait MessageTypeDecoder: Sized { fn decode(src: &mut BytesMut) -> Result, ParseError>; - fn process_headers( + fn set_headers( &mut self, slice: &Bytes, version: Version, @@ -140,10 +139,17 @@ pub(crate) trait MessageTypeDecoder: Sized { self.keep_alive(); } + // https://tools.ietf.org/html/rfc7230#section-3.3.3 if chunked { - Ok(PayloadLength::Chunked) + // Chunked encoding + Ok(PayloadLength::Payload(PayloadType::Payload( + PayloadDecoder::chunked(), + ))) } else if let Some(len) = content_length { - Ok(PayloadLength::Length(len)) + // Content-Length + Ok(PayloadLength::Payload(PayloadType::Payload( + PayloadDecoder::length(len), + ))) } else if has_upgrade { Ok(PayloadLength::Upgrade) } else { @@ -166,7 +172,7 @@ impl MessageTypeDecoder for Request { // performance bump for pipeline benchmarks. let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { mem::uninitialized() }; - let (len, method, uri, version, headers_len) = { + let (len, method, uri, ver, h_len) = { let mut parsed: [httparse::Header; MAX_HEADERS] = unsafe { mem::uninitialized() }; @@ -189,35 +195,24 @@ impl MessageTypeDecoder for Request { } }; - // convert headers let mut msg = Request::new(); - let len = msg.process_headers( - &src.split_to(len).freeze(), - version, - &headers[..headers_len], - )?; + // convert headers + let len = + msg.set_headers(&src.split_to(len).freeze(), ver, &headers[..h_len])?; - // https://tools.ietf.org/html/rfc7230#section-3.3.3 + // payload decoder let decoder = match len { - PayloadLength::Chunked => { - // Chunked encoding - PayloadType::Payload(PayloadDecoder::chunked()) - } - PayloadLength::Length(len) => { - // Content-Length - PayloadType::Payload(PayloadDecoder::length(len)) - } + PayloadLength::Payload(pl) => pl, PayloadLength::Upgrade => { - // upgrade(websocket) or connect + // upgrade(websocket) PayloadType::Stream(PayloadDecoder::eof()) } PayloadLength::None => { if method == Method::CONNECT { - // upgrade(websocket) or connect PayloadType::Stream(PayloadDecoder::eof()) } else if src.len() >= MAX_BUFFER_SIZE { - error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); + trace!("MAX_BUFFER_SIZE unprocessed data reached, closing"); return Err(ParseError::TooLarge); } else { PayloadType::None @@ -230,7 +225,7 @@ impl MessageTypeDecoder for Request { inner.url.update(&uri); inner.head.uri = uri; inner.head.method = method; - inner.head.version = version; + inner.head.version = ver; } Ok(Some((msg, decoder))) @@ -251,7 +246,7 @@ impl MessageTypeDecoder for ClientResponse { // performance bump for pipeline benchmarks. let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { mem::uninitialized() }; - let (len, version, status, headers_len) = { + let (len, ver, status, h_len) = { let mut parsed: [httparse::Header; MAX_HEADERS] = unsafe { mem::uninitialized() }; @@ -276,37 +271,26 @@ impl MessageTypeDecoder for ClientResponse { let mut msg = ClientResponse::new(); // convert headers - let len = msg.process_headers( - &src.split_to(len).freeze(), - version, - &headers[..headers_len], - )?; + let len = + msg.set_headers(&src.split_to(len).freeze(), ver, &headers[..h_len])?; - // https://tools.ietf.org/html/rfc7230#section-3.3.3 - let decoder = match len { - PayloadLength::Chunked => { - // Chunked encoding - PayloadType::Payload(PayloadDecoder::chunked()) - } - PayloadLength::Length(len) => { - // Content-Length - PayloadType::Payload(PayloadDecoder::length(len)) - } - _ => { - if status == StatusCode::SWITCHING_PROTOCOLS { - // switching protocol or connect - PayloadType::Stream(PayloadDecoder::eof()) - } else if src.len() >= MAX_BUFFER_SIZE { - error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); - return Err(ParseError::TooLarge); - } else { - PayloadType::None - } + // message payload + let decoder = if let PayloadLength::Payload(pl) = len { + pl + } else { + if status == StatusCode::SWITCHING_PROTOCOLS { + // switching protocol or connect + PayloadType::Stream(PayloadDecoder::eof()) + } else if src.len() >= MAX_BUFFER_SIZE { + error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); + return Err(ParseError::TooLarge); + } else { + PayloadType::None } }; msg.head.status = status; - msg.head.version = Some(version); + msg.head.version = Some(ver); Ok(Some((msg, decoder))) } diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 470d3df93..4a0bce723 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -1,11 +1,12 @@ use std::collections::VecDeque; use std::fmt::Debug; +use std::mem; use std::time::Instant; use actix_net::codec::Framed; use actix_net::service::Service; -use futures::{Async, AsyncSink, Future, Poll, Sink, Stream}; +use futures::{Async, Future, Poll, Sink, Stream}; use tokio_io::{AsyncRead, AsyncWrite}; use tokio_timer::Delay; @@ -37,12 +38,19 @@ bitflags! { /// Dispatcher for HTTP/1.1 protocol pub struct Dispatcher +where + S::Error: Debug, +{ + inner: Option>, +} + +struct InnerDispatcher where S::Error: Debug, { service: S, flags: Flags, - framed: Option>, + framed: Framed, error: Option>, config: ServiceConfig, @@ -63,7 +71,6 @@ enum DispatcherMessage { enum State { None, ServiceCall(S::Future), - SendResponse(Option<(Message, Body)>), SendPayload(BodyStream), } @@ -113,20 +120,29 @@ where }; Dispatcher { - payload: None, - state: State::None, - error: None, - messages: VecDeque::new(), - framed: Some(framed), - unhandled: None, - service, - flags, - config, - ka_expire, - ka_timer, + inner: Some(InnerDispatcher { + framed, + payload: None, + state: State::None, + error: None, + messages: VecDeque::new(), + unhandled: None, + service, + flags, + config, + ka_expire, + ka_timer, + }), } } +} +impl InnerDispatcher +where + T: AsyncRead + AsyncWrite, + S: Service, + S::Error: Debug, +{ fn can_read(&self) -> bool { if self.flags.contains(Flags::DISCONNECTED) { return false; @@ -150,7 +166,7 @@ where /// Flush stream fn poll_flush(&mut self) -> Poll<(), DispatchError> { if !self.flags.contains(Flags::FLUSHED) { - match self.framed.as_mut().unwrap().poll_complete() { + match self.framed.poll_complete() { Ok(Async::NotReady) => Ok(Async::NotReady), Err(err) => { debug!("Error sending data: {}", err); @@ -170,90 +186,82 @@ where } } + fn send_response( + &mut self, + message: Response, + body: Body, + ) -> Result, DispatchError> { + self.framed + .force_send(Message::Item(message)) + .map_err(|err| { + if let Some(mut payload) = self.payload.take() { + payload.set_error(PayloadError::Incomplete(None)); + } + DispatchError::Io(err) + })?; + + self.flags + .set(Flags::KEEPALIVE, self.framed.get_codec().keepalive()); + self.flags.remove(Flags::FLUSHED); + match body { + Body::Empty => Ok(State::None), + Body::Streaming(stream) => Ok(State::SendPayload(stream)), + Body::Binary(mut bin) => { + self.flags.remove(Flags::FLUSHED); + self.framed.force_send(Message::Chunk(Some(bin.take())))?; + self.framed.force_send(Message::Chunk(None))?; + Ok(State::None) + } + } + } + fn poll_response(&mut self) -> Result<(), DispatchError> { let mut retry = self.can_read(); - - // process loop { - let state = match self.state { - State::None => if let Some(msg) = self.messages.pop_front() { - match msg { - DispatcherMessage::Item(req) => Some(self.handle_request(req)?), - DispatcherMessage::Error(res) => Some(State::SendResponse( - Some((Message::Item(res), Body::Empty)), - )), + let state = match mem::replace(&mut self.state, State::None) { + State::None => match self.messages.pop_front() { + Some(DispatcherMessage::Item(req)) => { + Some(self.handle_request(req)?) } - } else { - None + Some(DispatcherMessage::Error(res)) => { + Some(self.send_response(res, Body::Empty)?) + } + None => None, }, - // call inner service - State::ServiceCall(ref mut fut) => { + State::ServiceCall(mut fut) => { match fut.poll().map_err(DispatchError::Service)? { Async::Ready(mut res) => { - self.framed - .as_mut() - .unwrap() - .get_codec_mut() - .prepare_te(&mut res); + self.framed.get_codec_mut().prepare_te(&mut res); let body = res.replace_body(Body::Empty); - Some(State::SendResponse(Some((Message::Item(res), body)))) + Some(self.send_response(res, body)?) } - Async::NotReady => None, - } - } - // send respons - State::SendResponse(ref mut item) => { - let (msg, body) = item.take().expect("SendResponse is empty"); - let framed = self.framed.as_mut().unwrap(); - match framed.start_send(msg) { - Ok(AsyncSink::Ready) => { - self.flags - .set(Flags::KEEPALIVE, framed.get_codec().keepalive()); - self.flags.remove(Flags::FLUSHED); - match body { - Body::Empty => Some(State::None), - Body::Streaming(stream) => { - Some(State::SendPayload(stream)) - } - Body::Binary(mut bin) => { - self.flags.remove(Flags::FLUSHED); - framed - .force_send(Message::Chunk(Some(bin.take())))?; - framed.force_send(Message::Chunk(None))?; - Some(State::None) - } - } - } - Ok(AsyncSink::NotReady(msg)) => { - *item = Some((msg, body)); - return Ok(()); - } - Err(err) => { - if let Some(mut payload) = self.payload.take() { - payload.set_error(PayloadError::Incomplete(None)); - } - return Err(DispatchError::Io(err)); + Async::NotReady => { + self.state = State::ServiceCall(fut); + None } } } - // Send payload - State::SendPayload(ref mut stream) => { - let mut framed = self.framed.as_mut().unwrap(); + State::SendPayload(mut stream) => { loop { - if !framed.is_write_buf_full() { + if !self.framed.is_write_buf_full() { match stream.poll().map_err(|_| DispatchError::Unknown)? { Async::Ready(Some(item)) => { self.flags.remove(Flags::FLUSHED); - framed.force_send(Message::Chunk(Some(item)))?; + self.framed + .force_send(Message::Chunk(Some(item)))?; continue; } Async::Ready(None) => { self.flags.remove(Flags::FLUSHED); - framed.force_send(Message::Chunk(None))?; + self.framed.force_send(Message::Chunk(None))?; + } + Async::NotReady => { + self.state = State::SendPayload(stream); + return Ok(()); } - Async::NotReady => return Ok(()), } } else { + self.state = State::SendPayload(stream); return Ok(()); } break; @@ -266,7 +274,7 @@ where Some(state) => self.state = state, None => { // if read-backpressure is enabled and we consumed some data. - // we may read more dataand retry + // we may read more data and retry if !retry && self.can_read() && self.poll_request()? { retry = self.can_read(); continue; @@ -286,13 +294,9 @@ where let mut task = self.service.call(req); match task.poll().map_err(DispatchError::Service)? { Async::Ready(mut res) => { - self.framed - .as_mut() - .unwrap() - .get_codec_mut() - .prepare_te(&mut res); + self.framed.get_codec_mut().prepare_te(&mut res); let body = res.replace_body(Body::Empty); - Ok(State::SendResponse(Some((Message::Item(res), body)))) + self.send_response(res, body) } Async::NotReady => Ok(State::ServiceCall(task)), } @@ -307,20 +311,14 @@ where let mut updated = false; loop { - match self.framed.as_mut().unwrap().poll() { + match self.framed.poll() { Ok(Async::Ready(Some(msg))) => { updated = true; self.flags.insert(Flags::STARTED); match msg { Message::Item(req) => { - match self - .framed - .as_ref() - .unwrap() - .get_codec() - .message_type() - { + match self.framed.get_codec().message_type() { MessageType::Payload => { let (ps, pl) = Payload::new(false); *req.inner.payload.borrow_mut() = Some(pl); @@ -406,52 +404,58 @@ where /// keep-alive timer fn poll_keepalive(&mut self) -> Result<(), DispatchError> { - if let Some(ref mut timer) = self.ka_timer { - match timer.poll() { - Ok(Async::Ready(_)) => { - // if we get timer during shutdown, just drop connection - if self.flags.contains(Flags::SHUTDOWN) { - return Err(DispatchError::DisconnectTimeout); - } else if timer.deadline() >= self.ka_expire { - // check for any outstanding response processing - if self.state.is_empty() && self.flags.contains(Flags::FLUSHED) { - if self.flags.contains(Flags::STARTED) { - trace!("Keep-alive timeout, close connection"); - self.flags.insert(Flags::SHUTDOWN); + if self.ka_timer.is_some() { + return Ok(()); + } + match self.ka_timer.as_mut().unwrap().poll().map_err(|e| { + error!("Timer error {:?}", e); + DispatchError::Unknown + })? { + Async::Ready(_) => { + // if we get timeout during shutdown, drop connection + if self.flags.contains(Flags::SHUTDOWN) { + return Err(DispatchError::DisconnectTimeout); + } else if self.ka_timer.as_mut().unwrap().deadline() >= self.ka_expire { + // check for any outstanding response processing + if self.state.is_empty() && self.flags.contains(Flags::FLUSHED) { + if self.flags.contains(Flags::STARTED) { + trace!("Keep-alive timeout, close connection"); + self.flags.insert(Flags::SHUTDOWN); - // start shutdown timer - if let Some(deadline) = - self.config.client_disconnect_timer() - { + // start shutdown timer + if let Some(deadline) = self.config.client_disconnect_timer() + { + self.ka_timer.as_mut().map(|timer| { timer.reset(deadline); let _ = timer.poll(); - } else { - return Ok(()); - } + }); } else { - // timeout on first request (slow request) return 408 - trace!("Slow request timeout"); - self.flags.insert(Flags::STARTED | Flags::DISCONNECTED); - self.state = State::SendResponse(Some(( - Message::Item(Response::RequestTimeout().finish()), - Body::Empty, - ))); + return Ok(()); } - } else if let Some(deadline) = self.config.keep_alive_expire() { + } else { + // timeout on first request (slow request) return 408 + trace!("Slow request timeout"); + self.flags.insert(Flags::STARTED | Flags::DISCONNECTED); + self.state = self.send_response( + Response::RequestTimeout().finish(), + Body::Empty, + )?; + } + } else if let Some(deadline) = self.config.keep_alive_expire() { + self.ka_timer.as_mut().map(|timer| { timer.reset(deadline); let _ = timer.poll(); - } - } else { - timer.reset(self.ka_expire); - let _ = timer.poll(); + }); } - } - Ok(Async::NotReady) => (), - Err(e) => { - error!("Timer error {:?}", e); - return Err(DispatchError::Unknown); + } else { + let expire = self.ka_expire; + self.ka_timer.as_mut().map(|timer| { + timer.reset(expire); + let _ = timer.poll(); + }); } } + Async::NotReady => (), } Ok(()) @@ -469,43 +473,53 @@ where #[inline] fn poll(&mut self) -> Poll { - if self.flags.contains(Flags::SHUTDOWN) { - self.poll_keepalive()?; - try_ready!(self.poll_flush()); - let io = self.framed.take().unwrap().into_inner(); - Ok(Async::Ready(H1ServiceResult::Shutdown(io))) - } else { - self.poll_keepalive()?; - self.poll_request()?; - self.poll_response()?; - self.poll_flush()?; - - // keep-alive and stream errors - if self.state.is_empty() && self.flags.contains(Flags::FLUSHED) { - if let Some(err) = self.error.take() { - Err(err) - } else if self.flags.contains(Flags::DISCONNECTED) { - Ok(Async::Ready(H1ServiceResult::Disconnected)) - } - // unhandled request (upgrade or connect) - else if self.unhandled.is_some() { - let req = self.unhandled.take().unwrap(); - let framed = self.framed.take().unwrap(); - Ok(Async::Ready(H1ServiceResult::Unhandled(req, framed))) - } - // disconnect if keep-alive is not enabled - else if self.flags.contains(Flags::STARTED) && !self - .flags - .intersects(Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED) - { - let io = self.framed.take().unwrap().into_inner(); - Ok(Async::Ready(H1ServiceResult::Shutdown(io))) - } else { - Ok(Async::NotReady) - } + let shutdown = if let Some(ref mut inner) = self.inner { + if inner.flags.contains(Flags::SHUTDOWN) { + inner.poll_keepalive()?; + try_ready!(inner.poll_flush()); + true } else { - Ok(Async::NotReady) + inner.poll_keepalive()?; + inner.poll_request()?; + inner.poll_response()?; + inner.poll_flush()?; + + // keep-alive and stream errors + if inner.state.is_empty() && inner.flags.contains(Flags::FLUSHED) { + if let Some(err) = inner.error.take() { + return Err(err); + } else if inner.flags.contains(Flags::DISCONNECTED) { + return Ok(Async::Ready(H1ServiceResult::Disconnected)); + } + // unhandled request (upgrade or connect) + else if inner.unhandled.is_some() { + false + } + // disconnect if keep-alive is not enabled + else if inner.flags.contains(Flags::STARTED) && !inner + .flags + .intersects(Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED) + { + true + } else { + return Ok(Async::NotReady); + } + } else { + return Ok(Async::NotReady); + } } + } else { + unreachable!() + }; + + let mut inner = self.inner.take().unwrap(); + if shutdown { + Ok(Async::Ready(H1ServiceResult::Shutdown( + inner.framed.into_inner(), + ))) + } else { + let req = inner.unhandled.take().unwrap(); + Ok(Async::Ready(H1ServiceResult::Unhandled(req, inner.framed))) } } } From 3a4b16a6d57458f9071cb22732d07ab7f2d864e2 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 16 Nov 2018 21:30:37 -0800 Subject: [PATCH 070/427] use BodyLength for request and response body --- src/body.rs | 36 +++++++++++++++++++----------------- src/client/pipeline.rs | 12 ++++++------ src/h1/client.rs | 27 ++++++++++++++------------- src/h1/codec.rs | 20 +++++++++----------- src/h1/encoder.rs | 39 ++++++++++++++------------------------- src/ws/client/service.rs | 4 ++-- 6 files changed, 64 insertions(+), 74 deletions(-) diff --git a/src/body.rs b/src/body.rs index 1165909e8..6e6239c3e 100644 --- a/src/body.rs +++ b/src/body.rs @@ -12,24 +12,26 @@ pub type BodyStream = Box>; /// Type represent streaming payload pub type PayloadStream = Box>; -/// Different type of bory -pub enum BodyType { +#[derive(Debug)] +/// Different type of body +pub enum BodyLength { None, Zero, Sized(usize), + Sized64(u64), Unsized, } /// Type that provides this trait can be streamed to a peer. pub trait MessageBody { - fn tp(&self) -> BodyType; + fn length(&self) -> BodyLength; fn poll_next(&mut self) -> Poll, Error>; } impl MessageBody for () { - fn tp(&self) -> BodyType { - BodyType::Zero + fn length(&self) -> BodyLength { + BodyLength::Zero } fn poll_next(&mut self) -> Poll, Error> { @@ -271,8 +273,8 @@ impl AsRef<[u8]> for Binary { } impl MessageBody for Bytes { - fn tp(&self) -> BodyType { - BodyType::Sized(self.len()) + fn length(&self) -> BodyLength { + BodyLength::Sized(self.len()) } fn poll_next(&mut self) -> Poll, Error> { @@ -285,8 +287,8 @@ impl MessageBody for Bytes { } impl MessageBody for &'static str { - fn tp(&self) -> BodyType { - BodyType::Sized(self.len()) + fn length(&self) -> BodyLength { + BodyLength::Sized(self.len()) } fn poll_next(&mut self) -> Poll, Error> { @@ -301,8 +303,8 @@ impl MessageBody for &'static str { } impl MessageBody for &'static [u8] { - fn tp(&self) -> BodyType { - BodyType::Sized(self.len()) + fn length(&self) -> BodyLength { + BodyLength::Sized(self.len()) } fn poll_next(&mut self) -> Poll, Error> { @@ -317,8 +319,8 @@ impl MessageBody for &'static [u8] { } impl MessageBody for Vec { - fn tp(&self) -> BodyType { - BodyType::Sized(self.len()) + fn length(&self) -> BodyLength { + BodyLength::Sized(self.len()) } fn poll_next(&mut self) -> Poll, Error> { @@ -334,8 +336,8 @@ impl MessageBody for Vec { } impl MessageBody for String { - fn tp(&self) -> BodyType { - BodyType::Sized(self.len()) + fn length(&self) -> BodyLength { + BodyLength::Sized(self.len()) } fn poll_next(&mut self) -> Poll, Error> { @@ -367,8 +369,8 @@ impl MessageBody for MessageBodyStream where S: Stream, { - fn tp(&self) -> BodyType { - BodyType::Unsized + fn length(&self) -> BodyLength { + BodyLength::Unsized } fn poll_next(&mut self) -> Poll, Error> { diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs index 17ba93e7c..93b349e93 100644 --- a/src/client/pipeline.rs +++ b/src/client/pipeline.rs @@ -10,7 +10,7 @@ use tokio_io::{AsyncRead, AsyncWrite}; use super::error::{ConnectorError, SendRequestError}; use super::response::ClientResponse; use super::{Connect, Connection}; -use body::{BodyType, MessageBody, PayloadStream}; +use body::{BodyLength, MessageBody, PayloadStream}; use error::PayloadError; use h1; use message::RequestHead; @@ -25,7 +25,7 @@ where B: MessageBody, I: Connection, { - let tp = body.tp(); + let len = body.length(); connector // connect to the host @@ -33,10 +33,10 @@ where .from_err() // create Framed and send reqest .map(|io| Framed::new(io, h1::ClientCodec::default())) - .and_then(|framed| framed.send((head, tp).into()).from_err()) + .and_then(|framed| framed.send((head, len).into()).from_err()) // send request body - .and_then(move |framed| match body.tp() { - BodyType::None | BodyType::Zero => Either::A(ok(framed)), + .and_then(move |framed| match body.length() { + BodyLength::None | BodyLength::Zero => Either::A(ok(framed)), _ => Either::B(SendBody::new(body, framed)), }) // read response and init read body @@ -64,7 +64,7 @@ where struct SendBody { body: Option, framed: Option>, - write_buf: VecDeque>, + write_buf: VecDeque>, flushed: bool, } diff --git a/src/h1/client.rs b/src/h1/client.rs index f871cb338..8b367acc3 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -5,9 +5,9 @@ use bytes::{BufMut, Bytes, BytesMut}; use tokio_codec::{Decoder, Encoder}; use super::decoder::{MessageDecoder, PayloadDecoder, PayloadItem, PayloadType}; -use super::encoder::{RequestEncoder, ResponseLength}; +use super::encoder::RequestEncoder; use super::{Message, MessageType}; -use body::{Binary, Body, BodyType}; +use body::{Binary, Body, BodyLength}; use client::ClientResponse; use config::ServiceConfig; use error::{ParseError, PayloadError}; @@ -104,7 +104,7 @@ impl ClientCodec { } /// prepare transfer encoding - pub fn prepare_te(&mut self, head: &mut RequestHead, btype: BodyType) { + pub fn prepare_te(&mut self, head: &mut RequestHead, length: BodyLength) { self.inner.te.update( head, self.inner.flags.contains(Flags::HEAD), @@ -138,7 +138,7 @@ impl ClientCodecInner { fn encode_response( &mut self, msg: RequestHead, - btype: BodyType, + length: BodyLength, buffer: &mut BytesMut, ) -> io::Result<()> { // render message @@ -157,20 +157,21 @@ impl ClientCodecInner { // content length let mut len_is_set = true; - match btype { - BodyType::Sized(len) => { + match length { + BodyLength::Sized(len) => helpers::write_content_length(len, buffer), + BodyLength::Sized64(len) => { buffer.extend_from_slice(b"\r\ncontent-length: "); write!(buffer.writer(), "{}", len)?; buffer.extend_from_slice(b"\r\n"); } - BodyType::Unsized => { + BodyLength::Unsized => { buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") } - BodyType::Zero => { + BodyLength::Zero => { len_is_set = false; buffer.extend_from_slice(b"\r\n") } - BodyType::None => buffer.extend_from_slice(b"\r\n"), + BodyLength::None => buffer.extend_from_slice(b"\r\n"), } let mut has_date = false; @@ -178,9 +179,9 @@ impl ClientCodecInner { for (key, value) in &msg.headers { match *key { TRANSFER_ENCODING => continue, - CONTENT_LENGTH => match btype { - BodyType::None => (), - BodyType::Zero => len_is_set = true, + CONTENT_LENGTH => match length { + BodyLength::None => (), + BodyLength::Zero => len_is_set = true, _ => continue, }, DATE => has_date = true, @@ -263,7 +264,7 @@ impl Decoder for ClientPayloadCodec { } impl Encoder for ClientCodec { - type Item = Message<(RequestHead, BodyType)>; + type Item = Message<(RequestHead, BodyLength)>; type Error = io::Error; fn encode( diff --git a/src/h1/codec.rs b/src/h1/codec.rs index c1c6091de..a6f39242b 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -6,9 +6,9 @@ use bytes::{BufMut, Bytes, BytesMut}; use tokio_codec::{Decoder, Encoder}; use super::decoder::{MessageDecoder, PayloadDecoder, PayloadItem, PayloadType}; -use super::encoder::{ResponseEncoder, ResponseLength}; +use super::encoder::ResponseEncoder; use super::{Message, MessageType}; -use body::{Binary, Body}; +use body::{Binary, Body, BodyLength}; use config::ServiceConfig; use error::ParseError; use helpers; @@ -155,22 +155,20 @@ impl Codec { // content length let mut len_is_set = true; match self.te.length { - ResponseLength::Chunked => { + BodyLength::Unsized => { buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") } - ResponseLength::Zero => { + BodyLength::Zero => { len_is_set = false; buffer.extend_from_slice(b"\r\n") } - ResponseLength::Length(len) => { - helpers::write_content_length(len, buffer) - } - ResponseLength::Length64(len) => { + BodyLength::Sized(len) => helpers::write_content_length(len, buffer), + BodyLength::Sized64(len) => { buffer.extend_from_slice(b"\r\ncontent-length: "); write!(buffer.writer(), "{}", len)?; buffer.extend_from_slice(b"\r\n"); } - ResponseLength::None => buffer.extend_from_slice(b"\r\n"), + BodyLength::None => buffer.extend_from_slice(b"\r\n"), } // write headers @@ -182,8 +180,8 @@ impl Codec { match *key { TRANSFER_ENCODING => continue, CONTENT_LENGTH => match self.te.length { - ResponseLength::None => (), - ResponseLength::Zero => { + BodyLength::None => (), + BodyLength::Zero => { len_is_set = true; } _ => continue, diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index de45351d1..5cfdd01b9 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -8,28 +8,17 @@ use bytes::{Bytes, BytesMut}; use http::header::{HeaderValue, ACCEPT_ENCODING, CONTENT_LENGTH}; use http::{StatusCode, Version}; -use body::{Binary, Body}; +use body::{Binary, Body, BodyLength}; use header::ContentEncoding; use http::Method; use message::RequestHead; use request::Request; use response::Response; -#[derive(Debug)] -pub(crate) enum ResponseLength { - Chunked, - /// Check if headers contains length or write 0 - Zero, - Length(usize), - Length64(u64), - /// Do no set content-length - None, -} - #[derive(Debug)] pub(crate) struct ResponseEncoder { head: bool, - pub length: ResponseLength, + pub length: BodyLength, pub te: TransferEncoding, } @@ -37,7 +26,7 @@ impl Default for ResponseEncoder { fn default() -> Self { ResponseEncoder { head: false, - length: ResponseLength::None, + length: BodyLength::None, te: TransferEncoding::empty(), } } @@ -80,18 +69,18 @@ impl ResponseEncoder { StatusCode::NO_CONTENT | StatusCode::CONTINUE | StatusCode::SWITCHING_PROTOCOLS - | StatusCode::PROCESSING => ResponseLength::None, - _ => ResponseLength::Zero, + | StatusCode::PROCESSING => BodyLength::None, + _ => BodyLength::Zero, }; TransferEncoding::empty() } Body::Binary(_) => { - self.length = ResponseLength::Length(len); + self.length = BodyLength::Sized(len); TransferEncoding::length(len as u64) } Body::Streaming(_) => { if resp.upgrade() { - self.length = ResponseLength::None; + self.length = BodyLength::None; TransferEncoding::eof() } else { self.streaming_encoding(version, resp) @@ -115,10 +104,10 @@ impl ResponseEncoder { Some(true) => { // Enable transfer encoding if version == Version::HTTP_2 { - self.length = ResponseLength::None; + self.length = BodyLength::None; TransferEncoding::eof() } else { - self.length = ResponseLength::Chunked; + self.length = BodyLength::Unsized; TransferEncoding::chunked() } } @@ -145,7 +134,7 @@ impl ResponseEncoder { if !chunked { if let Some(len) = len { - self.length = ResponseLength::Length64(len); + self.length = BodyLength::Sized64(len); TransferEncoding::length(len) } else { TransferEncoding::eof() @@ -154,11 +143,11 @@ impl ResponseEncoder { // Enable transfer encoding match version { Version::HTTP_11 => { - self.length = ResponseLength::Chunked; + self.length = BodyLength::Unsized; TransferEncoding::chunked() } _ => { - self.length = ResponseLength::None; + self.length = BodyLength::None; TransferEncoding::eof() } } @@ -171,7 +160,7 @@ impl ResponseEncoder { #[derive(Debug)] pub(crate) struct RequestEncoder { head: bool, - pub length: ResponseLength, + pub length: BodyLength, pub te: TransferEncoding, } @@ -179,7 +168,7 @@ impl Default for RequestEncoder { fn default() -> Self { RequestEncoder { head: false, - length: ResponseLength::None, + length: BodyLength::None, te: TransferEncoding::empty(), } } diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index 34a151444..94be59f6e 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -13,7 +13,7 @@ use rand; use sha1::Sha1; use tokio_io::{AsyncRead, AsyncWrite}; -use body::BodyType; +use body::BodyLength; use client::ClientResponse; use h1; use ws::Codec; @@ -141,7 +141,7 @@ where // h1 protocol let framed = Framed::new(io, h1::ClientCodec::default()); framed - .send((request.into_parts().0, BodyType::None).into()) + .send((request.into_parts().0, BodyLength::None).into()) .map_err(ClientError::from) .and_then(|framed| { framed From f0bd4d868e12f2f1b227c681e9cadd007937d94a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 17 Nov 2018 08:56:40 -0800 Subject: [PATCH 071/427] simplify server response type --- src/message.rs | 2 + src/response.rs | 119 +++++++++++++++++++++--------------------------- 2 files changed, 53 insertions(+), 68 deletions(-) diff --git a/src/message.rs b/src/message.rs index b38c2f801..f0275093f 100644 --- a/src/message.rs +++ b/src/message.rs @@ -74,6 +74,8 @@ impl Default for ResponseHead { impl Head for ResponseHead { fn clear(&mut self) { + self.reason = None; + self.version = None; self.headers.clear(); self.flags = MessageFlags::empty(); } diff --git a/src/response.rs b/src/response.rs index 1901443fe..0b20f41b8 100644 --- a/src/response.rs +++ b/src/response.rs @@ -12,6 +12,7 @@ use http::{Error as HttpError, HeaderMap, HttpTryFrom, StatusCode, Version}; use serde::Serialize; use serde_json; +use message::{Head, ResponseHead, MessageFlags}; use body::Body; use error::Error; use header::{ContentEncoding, Header, IntoHeaderValue}; @@ -31,7 +32,7 @@ pub enum ConnectionType { } /// An HTTP Response -pub struct Response(Box, &'static ResponsePool); +pub struct Response(Box); impl Response { #[inline] @@ -92,7 +93,6 @@ impl Response { } ResponseBuilder { - pool: self.1, response: Some(self.0), err: None, cookies: jar, @@ -108,33 +108,33 @@ impl Response { /// Get the HTTP version of this response #[inline] pub fn version(&self) -> Option { - self.get_ref().version + self.get_ref().head.version } /// Get the headers from the response #[inline] pub fn headers(&self) -> &HeaderMap { - &self.get_ref().headers + &self.get_ref().head.headers } /// Get a mutable reference to the headers #[inline] pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.get_mut().headers + &mut self.get_mut().head.headers } /// Get an iterator for the cookies set by this response #[inline] pub fn cookies(&self) -> CookieIter { CookieIter { - iter: self.get_ref().headers.get_all(header::SET_COOKIE).iter(), + iter: self.get_ref().head.headers.get_all(header::SET_COOKIE).iter(), } } /// Add a cookie to this response #[inline] pub fn add_cookie(&mut self, cookie: &Cookie) -> Result<(), HttpError> { - let h = &mut self.get_mut().headers; + let h = &mut self.get_mut().head.headers; HeaderValue::from_str(&cookie.to_string()) .map(|c| { h.append(header::SET_COOKIE, c); @@ -145,7 +145,7 @@ impl Response { /// the number of cookies removed. #[inline] pub fn del_cookie(&mut self, name: &str) -> usize { - let h = &mut self.get_mut().headers; + let h = &mut self.get_mut().head.headers; let vals: Vec = h .get_all(header::SET_COOKIE) .iter() @@ -171,23 +171,23 @@ impl Response { /// Get the response status code #[inline] pub fn status(&self) -> StatusCode { - self.get_ref().status + self.get_ref().head.status } /// Set the `StatusCode` for this response #[inline] pub fn status_mut(&mut self) -> &mut StatusCode { - &mut self.get_mut().status + &mut self.get_mut().head.status } /// Get custom reason for the response #[inline] pub fn reason(&self) -> &str { - if let Some(reason) = self.get_ref().reason { + if let Some(reason) = self.get_ref().head.reason { reason } else { self.get_ref() - .status + .head.status .canonical_reason() .unwrap_or("") } @@ -196,7 +196,7 @@ impl Response { /// Set the custom reason for the response #[inline] pub fn set_reason(&mut self, reason: &'static str) -> &mut Self { - self.get_mut().reason = Some(reason); + self.get_mut().head.reason = Some(reason); self } @@ -279,7 +279,7 @@ impl Response { } pub(crate) fn release(self) { - self.1.release(self.0); + ResponsePool::release(self.0); } pub(crate) fn into_parts(self) -> ResponseParts { @@ -287,10 +287,7 @@ impl Response { } pub(crate) fn from_parts(parts: ResponseParts) -> Response { - Response( - Box::new(InnerResponse::from_parts(parts)), - ResponsePool::get_pool(), - ) + Response(Box::new(InnerResponse::from_parts(parts))) } } @@ -299,13 +296,13 @@ impl fmt::Debug for Response { let res = writeln!( f, "\nResponse {:?} {}{}", - self.get_ref().version, - self.get_ref().status, - self.get_ref().reason.unwrap_or("") + self.get_ref().head.version, + self.get_ref().head.status, + self.get_ref().head.reason.unwrap_or("") ); let _ = writeln!(f, " encoding: {:?}", self.get_ref().encoding); let _ = writeln!(f, " headers:"); - for (key, val) in self.get_ref().headers.iter() { + for (key, val) in self.get_ref().head.headers.iter() { let _ = writeln!(f, " {:?}: {:?}", key, val); } res @@ -335,7 +332,6 @@ impl<'a> Iterator for CookieIter<'a> { /// This type can be used to construct an instance of `Response` through a /// builder-like pattern. pub struct ResponseBuilder { - pool: &'static ResponsePool, response: Option>, err: Option, cookies: Option, @@ -346,7 +342,7 @@ impl ResponseBuilder { #[inline] pub fn status(&mut self, status: StatusCode) -> &mut Self { if let Some(parts) = parts(&mut self.response, &self.err) { - parts.status = status; + parts.head.status = status; } self } @@ -357,7 +353,7 @@ impl ResponseBuilder { #[inline] pub fn version(&mut self, version: Version) -> &mut Self { if let Some(parts) = parts(&mut self.response, &self.err) { - parts.version = Some(version); + parts.head.version = Some(version); } self } @@ -382,7 +378,7 @@ impl ResponseBuilder { if let Some(parts) = parts(&mut self.response, &self.err) { match hdr.try_into() { Ok(value) => { - parts.headers.append(H::name(), value); + parts.head.headers.append(H::name(), value); } Err(e) => self.err = Some(e.into()), } @@ -413,7 +409,7 @@ impl ResponseBuilder { match HeaderName::try_from(key) { Ok(key) => match value.try_into() { Ok(value) => { - parts.headers.append(key, value); + parts.head.headers.append(key, value); } Err(e) => self.err = Some(e.into()), }, @@ -427,7 +423,7 @@ impl ResponseBuilder { #[inline] pub fn reason(&mut self, reason: &'static str) -> &mut Self { if let Some(parts) = parts(&mut self.response, &self.err) { - parts.reason = Some(reason); + parts.head.reason = Some(reason); } self } @@ -496,7 +492,7 @@ impl ResponseBuilder { if let Some(parts) = parts(&mut self.response, &self.err) { match HeaderValue::try_from(value) { Ok(value) => { - parts.headers.insert(header::CONTENT_TYPE, value); + parts.head.headers.insert(header::CONTENT_TYPE, value); } Err(e) => self.err = Some(e.into()), }; @@ -620,13 +616,13 @@ impl ResponseBuilder { if let Some(ref jar) = self.cookies { for cookie in jar.delta() { match HeaderValue::from_str(&cookie.to_string()) { - Ok(val) => response.headers.append(header::SET_COOKIE, val), + Ok(val) => response.head.headers.append(header::SET_COOKIE, val), Err(e) => return Error::from(e).into(), }; } } response.body = body.into(); - Response(response, self.pool) + Response(response) } #[inline] @@ -656,7 +652,7 @@ impl ResponseBuilder { Ok(body) => { let contains = if let Some(parts) = parts(&mut self.response, &self.err) { - parts.headers.contains_key(header::CONTENT_TYPE) + parts.head.headers.contains_key(header::CONTENT_TYPE) } else { true }; @@ -681,7 +677,6 @@ impl ResponseBuilder { /// This method construct new `ResponseBuilder` pub fn take(&mut self) -> ResponseBuilder { ResponseBuilder { - pool: self.pool, response: self.response.take(), err: self.err.take(), cookies: self.cookies.take(), @@ -765,12 +760,8 @@ impl From for Response { } } -#[derive(Debug)] struct InnerResponse { - version: Option, - headers: HeaderMap, - status: StatusCode, - reason: Option<&'static str>, + head: ResponseHead, body: Body, chunked: Option, encoding: Option, @@ -778,13 +769,11 @@ struct InnerResponse { write_capacity: usize, response_size: u64, error: Option, + pool: &'static ResponsePool, } pub(crate) struct ResponseParts { - version: Option, - headers: HeaderMap, - status: StatusCode, - reason: Option<&'static str>, + head: ResponseHead, body: Option, encoding: Option, connection_type: Option, @@ -793,13 +782,17 @@ pub(crate) struct ResponseParts { impl InnerResponse { #[inline] - fn new(status: StatusCode, body: Body) -> InnerResponse { + fn new(status: StatusCode, body: Body, pool: &'static ResponsePool) -> InnerResponse { InnerResponse { - status, + head: ResponseHead { + status, + version: None, + headers: HeaderMap::with_capacity(16), + reason: None, + flags: MessageFlags::empty(), + }, body, - version: None, - headers: HeaderMap::with_capacity(16), - reason: None, + pool, chunked: None, encoding: None, connection_type: None, @@ -822,10 +815,7 @@ impl InnerResponse { ResponseParts { body, - version: self.version, - headers: self.headers, - status: self.status, - reason: self.reason, + head: self.head, encoding: self.encoding, connection_type: self.connection_type, error: self.error, @@ -841,16 +831,14 @@ impl InnerResponse { InnerResponse { body, - status: parts.status, - version: parts.version, - headers: parts.headers, - reason: parts.reason, + head: parts.head, chunked: None, encoding: parts.encoding, connection_type: parts.connection_type, response_size: 0, write_capacity: MAX_WRITE_BUFFER_SIZE, error: parts.error, + pool: ResponsePool::pool(), } } } @@ -876,17 +864,15 @@ impl ResponsePool { status: StatusCode, ) -> ResponseBuilder { if let Some(mut msg) = pool.0.borrow_mut().pop_front() { - msg.status = status; + msg.head.status = status; ResponseBuilder { - pool, response: Some(msg), err: None, cookies: None, } } else { - let msg = Box::new(InnerResponse::new(status, Body::Empty)); + let msg = Box::new(InnerResponse::new(status, Body::Empty, pool)); ResponseBuilder { - pool, response: Some(msg), err: None, cookies: None, @@ -901,12 +887,11 @@ impl ResponsePool { body: Body, ) -> Response { if let Some(mut msg) = pool.0.borrow_mut().pop_front() { - msg.status = status; + msg.head.status = status; msg.body = body; - Response(msg, pool) + Response(msg) } else { - let msg = Box::new(InnerResponse::new(status, body)); - Response(msg, pool) + Response(Box::new(InnerResponse::new(status, body, pool))) } } @@ -921,13 +906,11 @@ impl ResponsePool { } #[inline] - fn release(&self, mut inner: Box) { - let mut p = self.0.borrow_mut(); + fn release(mut inner: Box) { + let mut p = inner.pool.0.borrow_mut(); if p.len() < 128 { - inner.headers.clear(); - inner.version = None; + inner.head.clear(); inner.chunked = None; - inner.reason = None; inner.encoding = None; inner.connection_type = None; inner.response_size = 0; From e73a97884af8d0c738aaf32cb6142b703620d46f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 17 Nov 2018 09:03:35 -0800 Subject: [PATCH 072/427] do not allow to set server response version --- src/client/response.rs | 2 +- src/h1/codec.rs | 7 +++---- src/h1/decoder.rs | 2 +- src/h1/encoder.rs | 2 -- src/message.rs | 5 ++--- src/response.rs | 43 ++++++++++++++++-------------------------- 6 files changed, 23 insertions(+), 38 deletions(-) diff --git a/src/client/response.rs b/src/client/response.rs index 797c88df8..41c18562a 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -57,7 +57,7 @@ impl ClientResponse { /// Read the Request Version. #[inline] pub fn version(&self) -> Version { - self.head().version.clone().unwrap() + self.head().version } /// Get the status from the server. diff --git a/src/h1/codec.rs b/src/h1/codec.rs index a6f39242b..bb8afa710 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -113,7 +113,6 @@ impl Codec { .unwrap_or_else(|| self.flags.contains(Flags::KEEPALIVE)); // Connection upgrade - let version = msg.version().unwrap_or_else(|| self.version); if msg.upgrade() { self.flags.insert(Flags::UPGRADE); self.flags.remove(Flags::KEEPALIVE); @@ -123,11 +122,11 @@ impl Codec { // keep-alive else if ka { self.flags.insert(Flags::KEEPALIVE); - if version < Version::HTTP_11 { + if self.version < Version::HTTP_11 { msg.headers_mut() .insert(CONNECTION, HeaderValue::from_static("keep-alive")); } - } else if version >= Version::HTTP_11 { + } else if self.version >= Version::HTTP_11 { self.flags.remove(Flags::KEEPALIVE); msg.headers_mut() .insert(CONNECTION, HeaderValue::from_static("close")); @@ -149,7 +148,7 @@ impl Codec { } // status line - helpers::write_status_line(version, msg.status().as_u16(), buffer); + helpers::write_status_line(self.version, msg.status().as_u16(), buffer); buffer.extend_from_slice(reason); // content length diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 61cab7ad8..26154ef1e 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -290,7 +290,7 @@ impl MessageTypeDecoder for ClientResponse { }; msg.head.status = status; - msg.head.version = Some(ver); + msg.head.version = ver; Ok(Some((msg, decoder))) } diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index 5cfdd01b9..421d0b961 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -45,8 +45,6 @@ impl ResponseEncoder { pub fn update(&mut self, resp: &mut Response, head: bool, version: Version) { self.head = head; - - let version = resp.version().unwrap_or_else(|| version); let mut len = 0; let has_body = match resp.body() { diff --git a/src/message.rs b/src/message.rs index f0275093f..3dcb203de 100644 --- a/src/message.rs +++ b/src/message.rs @@ -53,7 +53,7 @@ impl Head for RequestHead { } pub struct ResponseHead { - pub version: Option, + pub version: Version, pub status: StatusCode, pub headers: HeaderMap, pub reason: Option<&'static str>, @@ -63,7 +63,7 @@ pub struct ResponseHead { impl Default for ResponseHead { fn default() -> ResponseHead { ResponseHead { - version: None, + version: Version::default(), status: StatusCode::OK, headers: HeaderMap::with_capacity(16), reason: None, @@ -75,7 +75,6 @@ impl Default for ResponseHead { impl Head for ResponseHead { fn clear(&mut self) { self.reason = None; - self.version = None; self.headers.clear(); self.flags = MessageFlags::empty(); } diff --git a/src/response.rs b/src/response.rs index 0b20f41b8..f96facbae 100644 --- a/src/response.rs +++ b/src/response.rs @@ -12,10 +12,10 @@ use http::{Error as HttpError, HeaderMap, HttpTryFrom, StatusCode, Version}; use serde::Serialize; use serde_json; -use message::{Head, ResponseHead, MessageFlags}; use body::Body; use error::Error; use header::{ContentEncoding, Header, IntoHeaderValue}; +use message::{Head, MessageFlags, ResponseHead}; /// max write buffer size 64k pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; @@ -105,12 +105,6 @@ impl Response { self.get_ref().error.as_ref() } - /// Get the HTTP version of this response - #[inline] - pub fn version(&self) -> Option { - self.get_ref().head.version - } - /// Get the headers from the response #[inline] pub fn headers(&self) -> &HeaderMap { @@ -127,7 +121,12 @@ impl Response { #[inline] pub fn cookies(&self) -> CookieIter { CookieIter { - iter: self.get_ref().head.headers.get_all(header::SET_COOKIE).iter(), + iter: self + .get_ref() + .head + .headers + .get_all(header::SET_COOKIE) + .iter(), } } @@ -187,7 +186,8 @@ impl Response { reason } else { self.get_ref() - .head.status + .head + .status .canonical_reason() .unwrap_or("") } @@ -347,17 +347,6 @@ impl ResponseBuilder { self } - /// Set HTTP version of this response. - /// - /// By default response's http version depends on request's version. - #[inline] - pub fn version(&mut self, version: Version) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.head.version = Some(version); - } - self - } - /// Set a header. /// /// ```rust,ignore @@ -782,11 +771,15 @@ pub(crate) struct ResponseParts { impl InnerResponse { #[inline] - fn new(status: StatusCode, body: Body, pool: &'static ResponsePool) -> InnerResponse { + fn new( + status: StatusCode, + body: Body, + pool: &'static ResponsePool, + ) -> InnerResponse { InnerResponse { head: ResponseHead { status, - version: None, + version: Version::default(), headers: HeaderMap::with_capacity(16), reason: None, flags: MessageFlags::empty(), @@ -999,11 +992,7 @@ mod tests { #[test] fn test_basic_builder() { - let resp = Response::Ok() - .header("X-TEST", "value") - .version(Version::HTTP_10) - .finish(); - assert_eq!(resp.version(), Some(Version::HTTP_10)); + let resp = Response::Ok().header("X-TEST", "value").finish(); assert_eq!(resp.status(), StatusCode::OK); } From 7fed50bcaefde84c6e9424112e220fe789f99a57 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 17 Nov 2018 20:21:28 -0800 Subject: [PATCH 073/427] refactor response body management --- src/body.rs | 118 +++------------- src/client/pipeline.rs | 4 + src/client/request.rs | 6 - src/error.rs | 9 +- src/h1/client.rs | 8 +- src/h1/codec.rs | 26 ++-- src/h1/dispatcher.rs | 80 ++++++----- src/h1/encoder.rs | 122 +++------------- src/h1/service.rs | 54 ++++--- src/lib.rs | 2 +- src/response.rs | 309 ++++++++++++++++------------------------- src/service.rs | 144 +++++++++---------- 12 files changed, 334 insertions(+), 548 deletions(-) diff --git a/src/body.rs b/src/body.rs index 6e6239c3e..3b4e0113d 100644 --- a/src/body.rs +++ b/src/body.rs @@ -1,5 +1,5 @@ +use std::mem; use std::sync::Arc; -use std::{fmt, mem}; use bytes::{Bytes, BytesMut}; use futures::{Async, Poll, Stream}; @@ -19,7 +19,8 @@ pub enum BodyLength { Zero, Sized(usize), Sized64(u64), - Unsized, + Chunked, + Stream, } /// Type that provides this trait can be streamed to a peer. @@ -39,17 +40,6 @@ impl MessageBody for () { } } -/// Represents various types of http message body. -pub enum Body { - /// Empty response. `Content-Length` header is set to `0` - Empty, - /// Specific response body. - Binary(Binary), - /// Unspecified streaming response. Developer is responsible for setting - /// right `Content-Length` or `Transfer-Encoding` headers. - Streaming(BodyStream), -} - /// Represents various types of binary body. /// `Content-Length` header is set to length of the body. #[derive(Debug, PartialEq)] @@ -65,84 +55,6 @@ pub enum Binary { SharedVec(Arc>), } -impl Body { - /// Does this body streaming. - #[inline] - pub fn is_streaming(&self) -> bool { - match *self { - Body::Streaming(_) => true, - _ => false, - } - } - - /// Is this binary body. - #[inline] - pub fn is_binary(&self) -> bool { - match *self { - Body::Binary(_) => true, - _ => false, - } - } - - /// Is this binary empy. - #[inline] - pub fn is_empty(&self) -> bool { - match *self { - Body::Empty => true, - _ => false, - } - } - - /// Create body from slice (copy) - pub fn from_slice(s: &[u8]) -> Body { - Body::Binary(Binary::Bytes(Bytes::from(s))) - } - - /// Is this binary body. - #[inline] - pub(crate) fn into_binary(self) -> Option { - match self { - Body::Binary(b) => Some(b), - _ => None, - } - } -} - -impl PartialEq for Body { - fn eq(&self, other: &Body) -> bool { - match *self { - Body::Empty => match *other { - Body::Empty => true, - _ => false, - }, - Body::Binary(ref b) => match *other { - Body::Binary(ref b2) => b == b2, - _ => false, - }, - Body::Streaming(_) => false, - } - } -} - -impl fmt::Debug for Body { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Body::Empty => write!(f, "Body::Empty"), - Body::Binary(ref b) => write!(f, "Body::Binary({:?})", b), - Body::Streaming(_) => write!(f, "Body::Streaming(_)"), - } - } -} - -impl From for Body -where - T: Into, -{ - fn from(b: T) -> Body { - Body::Binary(b.into()) - } -} - impl Binary { #[inline] /// Returns `true` if body is empty @@ -286,6 +198,22 @@ impl MessageBody for Bytes { } } +impl MessageBody for BytesMut { + fn length(&self) -> BodyLength { + BodyLength::Sized(self.len()) + } + + fn poll_next(&mut self) -> Poll, Error> { + if self.is_empty() { + Ok(Async::Ready(None)) + } else { + Ok(Async::Ready(Some( + mem::replace(self, BytesMut::new()).freeze(), + ))) + } + } +} + impl MessageBody for &'static str { fn length(&self) -> BodyLength { BodyLength::Sized(self.len()) @@ -370,7 +298,7 @@ where S: Stream, { fn length(&self) -> BodyLength { - BodyLength::Unsized + BodyLength::Chunked } fn poll_next(&mut self) -> Poll, Error> { @@ -382,12 +310,6 @@ where mod tests { use super::*; - #[test] - fn test_body_is_streaming() { - assert_eq!(Body::Empty.is_streaming(), false); - assert_eq!(Body::Binary(Binary::from("")).is_streaming(), false); - } - #[test] fn test_is_empty() { assert_eq!(Binary::from("").is_empty(), true); diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs index 93b349e93..56c22bd2a 100644 --- a/src/client/pipeline.rs +++ b/src/client/pipeline.rs @@ -100,6 +100,10 @@ where { match self.body.as_mut().unwrap().poll_next()? { Async::Ready(item) => { + // check if body is done + if item.is_none() { + let _ = self.body.take(); + } self.flushed = false; self.framed .as_mut() diff --git a/src/client/request.rs b/src/client/request.rs index d3d1544c2..f0b76ed04 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -51,12 +51,6 @@ pub struct ClientRequest { body: B, } -impl RequestHead { - pub fn clear(&mut self) { - self.headers.clear() - } -} - impl ClientRequest<()> { /// Create client request builder pub fn build() -> ClientRequestBuilder { diff --git a/src/error.rs b/src/error.rs index 956ec4eb4..1f70396c3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -100,11 +100,10 @@ impl Error { } /// Converts error to a response instance and set error message as response body - pub fn response_with_message(self) -> Response { + pub fn response_with_message(self) -> Response { let message = format!("{}", self); - let mut resp: Response = self.into(); - resp.set_body(message); - resp + let resp: Response = self.into(); + resp.set_body(message) } } @@ -637,7 +636,7 @@ where InternalErrorType::Status(st) => Response::new(st), InternalErrorType::Response(ref resp) => { if let Some(resp) = resp.lock().unwrap().take() { - Response::from_parts(resp) + Response::<()>::from_parts(resp) } else { Response::new(StatusCode::INTERNAL_SERVER_ERROR) } diff --git a/src/h1/client.rs b/src/h1/client.rs index 8b367acc3..ace504668 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -7,7 +7,7 @@ use tokio_codec::{Decoder, Encoder}; use super::decoder::{MessageDecoder, PayloadDecoder, PayloadItem, PayloadType}; use super::encoder::RequestEncoder; use super::{Message, MessageType}; -use body::{Binary, Body, BodyLength}; +use body::{Binary, BodyLength}; use client::ClientResponse; use config::ServiceConfig; use error::{ParseError, PayloadError}; @@ -164,14 +164,16 @@ impl ClientCodecInner { write!(buffer.writer(), "{}", len)?; buffer.extend_from_slice(b"\r\n"); } - BodyLength::Unsized => { + BodyLength::Chunked => { buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") } BodyLength::Zero => { len_is_set = false; buffer.extend_from_slice(b"\r\n") } - BodyLength::None => buffer.extend_from_slice(b"\r\n"), + BodyLength::None | BodyLength::Stream => { + buffer.extend_from_slice(b"\r\n") + } } let mut has_date = false; diff --git a/src/h1/codec.rs b/src/h1/codec.rs index bb8afa710..6bc20b180 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -8,12 +8,13 @@ use tokio_codec::{Decoder, Encoder}; use super::decoder::{MessageDecoder, PayloadDecoder, PayloadItem, PayloadType}; use super::encoder::ResponseEncoder; use super::{Message, MessageType}; -use body::{Binary, Body, BodyLength}; +use body::{Binary, BodyLength}; use config::ServiceConfig; use error::ParseError; use helpers; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::{Method, Version}; +use message::ResponseHead; use request::Request; use response::Response; @@ -98,9 +99,9 @@ impl Codec { } /// prepare transfer encoding - pub fn prepare_te(&mut self, res: &mut Response) { + pub fn prepare_te(&mut self, head: &mut ResponseHead, length: &mut BodyLength) { self.te - .update(res, self.flags.contains(Flags::HEAD), self.version); + .update(head, self.flags.contains(Flags::HEAD), self.version, length); } fn encode_response( @@ -135,17 +136,8 @@ impl Codec { // render message { let reason = msg.reason().as_bytes(); - if let Body::Binary(ref bytes) = msg.body() { - buffer.reserve( - 256 + msg.headers().len() * AVERAGE_HEADER_SIZE - + bytes.len() - + reason.len(), - ); - } else { - buffer.reserve( - 256 + msg.headers().len() * AVERAGE_HEADER_SIZE + reason.len(), - ); - } + buffer + .reserve(256 + msg.headers().len() * AVERAGE_HEADER_SIZE + reason.len()); // status line helpers::write_status_line(self.version, msg.status().as_u16(), buffer); @@ -154,7 +146,7 @@ impl Codec { // content length let mut len_is_set = true; match self.te.length { - BodyLength::Unsized => { + BodyLength::Chunked => { buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") } BodyLength::Zero => { @@ -167,7 +159,9 @@ impl Codec { write!(buffer.writer(), "{}", len)?; buffer.extend_from_slice(b"\r\n"); } - BodyLength::None => buffer.extend_from_slice(b"\r\n"), + BodyLength::None | BodyLength::Stream => { + buffer.extend_from_slice(b"\r\n") + } } // write headers diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 4a0bce723..508962b43 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -13,7 +13,7 @@ use tokio_timer::Delay; use error::{ParseError, PayloadError}; use payload::{Payload, PayloadSender, PayloadStatus, PayloadWriter}; -use body::{Body, BodyStream}; +use body::{BodyLength, MessageBody}; use config::ServiceConfig; use error::DispatchError; use request::Request; @@ -37,14 +37,14 @@ bitflags! { } /// Dispatcher for HTTP/1.1 protocol -pub struct Dispatcher +pub struct Dispatcher where S::Error: Debug, { - inner: Option>, + inner: Option>, } -struct InnerDispatcher +struct InnerDispatcher where S::Error: Debug, { @@ -54,7 +54,7 @@ where error: Option>, config: ServiceConfig, - state: State, + state: State, payload: Option, messages: VecDeque, unhandled: Option, @@ -68,13 +68,13 @@ enum DispatcherMessage { Error(Response), } -enum State { +enum State { None, ServiceCall(S::Future), - SendPayload(BodyStream), + SendPayload(B), } -impl State { +impl State { fn is_empty(&self) -> bool { if let State::None = self { true @@ -84,11 +84,12 @@ impl State { } } -impl Dispatcher +impl Dispatcher where T: AsyncRead + AsyncWrite, - S: Service, + S: Service>, S::Error: Debug, + B: MessageBody, { /// Create http/1 dispatcher. pub fn new(stream: T, config: ServiceConfig, service: S) -> Self { @@ -137,11 +138,12 @@ where } } -impl InnerDispatcher +impl InnerDispatcher where T: AsyncRead + AsyncWrite, - S: Service, + S: Service>, S::Error: Debug, + B: MessageBody, { fn can_read(&self) -> bool { if self.flags.contains(Flags::DISCONNECTED) { @@ -186,11 +188,11 @@ where } } - fn send_response( + fn send_response( &mut self, message: Response, - body: Body, - ) -> Result, DispatchError> { + body: B1, + ) -> Result, DispatchError> { self.framed .force_send(Message::Item(message)) .map_err(|err| { @@ -203,15 +205,9 @@ where self.flags .set(Flags::KEEPALIVE, self.framed.get_codec().keepalive()); self.flags.remove(Flags::FLUSHED); - match body { - Body::Empty => Ok(State::None), - Body::Streaming(stream) => Ok(State::SendPayload(stream)), - Body::Binary(mut bin) => { - self.flags.remove(Flags::FLUSHED); - self.framed.force_send(Message::Chunk(Some(bin.take())))?; - self.framed.force_send(Message::Chunk(None))?; - Ok(State::None) - } + match body.length() { + BodyLength::None | BodyLength::Zero => Ok(State::None), + _ => Ok(State::SendPayload(body)), } } @@ -224,15 +220,18 @@ where Some(self.handle_request(req)?) } Some(DispatcherMessage::Error(res)) => { - Some(self.send_response(res, Body::Empty)?) + self.send_response(res, ())?; + None } None => None, }, State::ServiceCall(mut fut) => { match fut.poll().map_err(DispatchError::Service)? { Async::Ready(mut res) => { - self.framed.get_codec_mut().prepare_te(&mut res); - let body = res.replace_body(Body::Empty); + let (mut res, body) = res.replace_body(()); + self.framed + .get_codec_mut() + .prepare_te(res.head_mut(), &mut body.length()); Some(self.send_response(res, body)?) } Async::NotReady => { @@ -244,7 +243,10 @@ where State::SendPayload(mut stream) => { loop { if !self.framed.is_write_buf_full() { - match stream.poll().map_err(|_| DispatchError::Unknown)? { + match stream + .poll_next() + .map_err(|_| DispatchError::Unknown)? + { Async::Ready(Some(item)) => { self.flags.remove(Flags::FLUSHED); self.framed @@ -290,12 +292,14 @@ where fn handle_request( &mut self, req: Request, - ) -> Result, DispatchError> { + ) -> Result, DispatchError> { let mut task = self.service.call(req); match task.poll().map_err(DispatchError::Service)? { - Async::Ready(mut res) => { - self.framed.get_codec_mut().prepare_te(&mut res); - let body = res.replace_body(Body::Empty); + Async::Ready(res) => { + let (mut res, body) = res.replace_body(()); + self.framed + .get_codec_mut() + .prepare_te(res.head_mut(), &mut body.length()); self.send_response(res, body) } Async::NotReady => Ok(State::ServiceCall(task)), @@ -436,10 +440,9 @@ where // timeout on first request (slow request) return 408 trace!("Slow request timeout"); self.flags.insert(Flags::STARTED | Flags::DISCONNECTED); - self.state = self.send_response( - Response::RequestTimeout().finish(), - Body::Empty, - )?; + let _ = self + .send_response(Response::RequestTimeout().finish(), ()); + self.state = State::None; } } else if let Some(deadline) = self.config.keep_alive_expire() { self.ka_timer.as_mut().map(|timer| { @@ -462,11 +465,12 @@ where } } -impl Future for Dispatcher +impl Future for Dispatcher where T: AsyncRead + AsyncWrite, - S: Service, + S: Service>, S::Error: Debug, + B: MessageBody, { type Item = H1ServiceResult; type Error = DispatchError; diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index 421d0b961..aee17c1f0 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -8,10 +8,10 @@ use bytes::{Bytes, BytesMut}; use http::header::{HeaderValue, ACCEPT_ENCODING, CONTENT_LENGTH}; use http::{StatusCode, Version}; -use body::{Binary, Body, BodyLength}; +use body::{Binary, BodyLength}; use header::ContentEncoding; use http::Method; -use message::RequestHead; +use message::{RequestHead, ResponseHead}; use request::Request; use response::Response; @@ -43,116 +43,36 @@ impl ResponseEncoder { self.te.encode_eof(buf) } - pub fn update(&mut self, resp: &mut Response, head: bool, version: Version) { + pub fn update( + &mut self, + resp: &mut ResponseHead, + head: bool, + version: Version, + length: &mut BodyLength, + ) { self.head = head; - let mut len = 0; - - let has_body = match resp.body() { - Body::Empty => false, - Body::Binary(ref bin) => { - len = bin.len(); - true - } - _ => true, - }; - - let has_body = match resp.body() { - Body::Empty => false, - _ => true, - }; - - let transfer = match resp.body() { - Body::Empty => { - self.length = match resp.status() { + let transfer = match length { + BodyLength::Zero => { + match resp.status { StatusCode::NO_CONTENT | StatusCode::CONTINUE | StatusCode::SWITCHING_PROTOCOLS - | StatusCode::PROCESSING => BodyLength::None, - _ => BodyLength::Zero, - }; + | StatusCode::PROCESSING => *length = BodyLength::None, + _ => (), + } TransferEncoding::empty() } - Body::Binary(_) => { - self.length = BodyLength::Sized(len); - TransferEncoding::length(len as u64) - } - Body::Streaming(_) => { - if resp.upgrade() { - self.length = BodyLength::None; - TransferEncoding::eof() - } else { - self.streaming_encoding(version, resp) - } - } + BodyLength::Sized(len) => TransferEncoding::length(*len as u64), + BodyLength::Sized64(len) => TransferEncoding::length(*len), + BodyLength::Chunked => TransferEncoding::chunked(), + BodyLength::Stream => TransferEncoding::eof(), + BodyLength::None => TransferEncoding::length(0), }; // check for head response - if self.head { - resp.set_body(Body::Empty); - } else { + if !self.head { self.te = transfer; } } - - fn streaming_encoding( - &mut self, - version: Version, - resp: &mut Response, - ) -> TransferEncoding { - match resp.chunked() { - Some(true) => { - // Enable transfer encoding - if version == Version::HTTP_2 { - self.length = BodyLength::None; - TransferEncoding::eof() - } else { - self.length = BodyLength::Unsized; - TransferEncoding::chunked() - } - } - Some(false) => TransferEncoding::eof(), - None => { - // if Content-Length is specified, then use it as length hint - let (len, chunked) = - if let Some(len) = resp.headers().get(CONTENT_LENGTH) { - // Content-Length - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - (Some(len), false) - } else { - error!("illegal Content-Length: {:?}", len); - (None, false) - } - } else { - error!("illegal Content-Length: {:?}", len); - (None, false) - } - } else { - (None, true) - }; - - if !chunked { - if let Some(len) = len { - self.length = BodyLength::Sized64(len); - TransferEncoding::length(len) - } else { - TransferEncoding::eof() - } - } else { - // Enable transfer encoding - match version { - Version::HTTP_11 => { - self.length = BodyLength::Unsized; - TransferEncoding::chunked() - } - _ => { - self.length = BodyLength::None; - TransferEncoding::eof() - } - } - } - } - } - } } #[derive(Debug)] diff --git a/src/h1/service.rs b/src/h1/service.rs index 7e5e8c5fc..0f0452ee7 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -8,6 +8,7 @@ use futures::future::{ok, FutureResult}; use futures::{Async, Future, Poll, Stream}; use tokio_io::{AsyncRead, AsyncWrite}; +use body::MessageBody; use config::{KeepAlive, ServiceConfig}; use error::{DispatchError, ParseError}; use request::Request; @@ -18,17 +19,18 @@ use super::dispatcher::Dispatcher; use super::{H1ServiceResult, Message}; /// `NewService` implementation for HTTP1 transport -pub struct H1Service { +pub struct H1Service { srv: S, cfg: ServiceConfig, - _t: PhantomData, + _t: PhantomData<(T, B)>, } -impl H1Service +impl H1Service where - S: NewService + Clone, + S: NewService> + Clone, S::Service: Clone, S::Error: Debug, + B: MessageBody, { /// Create new `HttpService` instance. pub fn new>(service: F) -> Self { @@ -47,19 +49,20 @@ where } } -impl NewService for H1Service +impl NewService for H1Service where T: AsyncRead + AsyncWrite, - S: NewService + Clone, + S: NewService> + Clone, S::Service: Clone, S::Error: Debug, + B: MessageBody, { type Request = T; type Response = H1ServiceResult; type Error = DispatchError; type InitError = S::InitError; - type Service = H1ServiceHandler; - type Future = H1ServiceResponse; + type Service = H1ServiceHandler; + type Future = H1ServiceResponse; fn new_service(&self) -> Self::Future { H1ServiceResponse { @@ -180,7 +183,11 @@ where } /// Finish service configuration and create `H1Service` instance. - pub fn finish>(self, service: F) -> H1Service { + pub fn finish(self, service: F) -> H1Service + where + B: MessageBody, + F: IntoNewService, + { let cfg = ServiceConfig::new( self.keep_alive, self.client_timeout, @@ -195,20 +202,21 @@ where } #[doc(hidden)] -pub struct H1ServiceResponse { +pub struct H1ServiceResponse { fut: S::Future, cfg: Option, - _t: PhantomData, + _t: PhantomData<(T, B)>, } -impl Future for H1ServiceResponse +impl Future for H1ServiceResponse where T: AsyncRead + AsyncWrite, - S: NewService, + S: NewService>, S::Service: Clone, S::Error: Debug, + B: MessageBody, { - type Item = H1ServiceHandler; + type Item = H1ServiceHandler; type Error = S::InitError; fn poll(&mut self) -> Poll { @@ -221,18 +229,19 @@ where } /// `Service` implementation for HTTP1 transport -pub struct H1ServiceHandler { +pub struct H1ServiceHandler { srv: S, cfg: ServiceConfig, - _t: PhantomData, + _t: PhantomData<(T, B)>, } -impl H1ServiceHandler +impl H1ServiceHandler where - S: Service + Clone, + S: Service> + Clone, S::Error: Debug, + B: MessageBody, { - fn new(cfg: ServiceConfig, srv: S) -> H1ServiceHandler { + fn new(cfg: ServiceConfig, srv: S) -> H1ServiceHandler { H1ServiceHandler { srv, cfg, @@ -241,16 +250,17 @@ where } } -impl Service for H1ServiceHandler +impl Service for H1ServiceHandler where T: AsyncRead + AsyncWrite, - S: Service + Clone, + S: Service> + Clone, S::Error: Debug, + B: MessageBody, { type Request = T; type Response = H1ServiceResult; type Error = DispatchError; - type Future = Dispatcher; + type Future = Dispatcher; fn poll_ready(&mut self) -> Poll<(), Self::Error> { self.srv.poll_ready().map_err(DispatchError::Service) diff --git a/src/lib.rs b/src/lib.rs index f64876e31..4b43cae40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,7 +129,7 @@ pub mod h1; pub(crate) mod helpers; pub mod test; pub mod ws; -pub use body::{Binary, Body}; +pub use body::{Binary, MessageBody}; pub use error::{Error, ResponseError, Result}; pub use extensions::Extensions; pub use httpmessage::HttpMessage; diff --git a/src/response.rs b/src/response.rs index f96facbae..b3e599826 100644 --- a/src/response.rs +++ b/src/response.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::collections::VecDeque; use std::io::Write; -use std::{fmt, mem, str}; +use std::{fmt, str}; use bytes::{BufMut, Bytes, BytesMut}; use cookie::{Cookie, CookieJar}; @@ -12,7 +12,7 @@ use http::{Error as HttpError, HeaderMap, HttpTryFrom, StatusCode, Version}; use serde::Serialize; use serde_json; -use body::Body; +use body::{MessageBody, MessageBodyStream}; use error::Error; use header::{ContentEncoding, Header, IntoHeaderValue}; use message::{Head, MessageFlags, ResponseHead}; @@ -32,19 +32,9 @@ pub enum ConnectionType { } /// An HTTP Response -pub struct Response(Box); - -impl Response { - #[inline] - fn get_ref(&self) -> &InnerResponse { - self.0.as_ref() - } - - #[inline] - fn get_mut(&mut self) -> &mut InnerResponse { - self.0.as_mut() - } +pub struct Response(Box, B); +impl Response<()> { /// Create http response builder with specific status. #[inline] pub fn build(status: StatusCode) -> ResponseBuilder { @@ -60,13 +50,7 @@ impl Response { /// Constructs a response #[inline] pub fn new(status: StatusCode) -> Response { - ResponsePool::with_body(status, Body::Empty) - } - - /// Constructs a response with body - #[inline] - pub fn with_body>(status: StatusCode, body: B) -> Response { - ResponsePool::with_body(status, body.into()) + ResponsePool::with_body(status, ()) } /// Constructs an error response @@ -98,6 +82,29 @@ impl Response { cookies: jar, } } +} + +impl Response { + #[inline] + fn get_ref(&self) -> &InnerResponse { + self.0.as_ref() + } + + #[inline] + fn get_mut(&mut self) -> &mut InnerResponse { + self.0.as_mut() + } + + #[inline] + pub(crate) fn head_mut(&mut self) -> &mut ResponseHead { + &mut self.0.as_mut().head + } + + /// Constructs a response with body + #[inline] + pub fn with_body(status: StatusCode, body: B) -> Response { + ResponsePool::with_body(status, body.into()) + } /// The source `error` for this response #[inline] @@ -105,6 +112,39 @@ impl Response { self.get_ref().error.as_ref() } + /// Get the response status code + #[inline] + pub fn status(&self) -> StatusCode { + self.get_ref().head.status + } + + /// Set the `StatusCode` for this response + #[inline] + pub fn status_mut(&mut self) -> &mut StatusCode { + &mut self.get_mut().head.status + } + + /// Get custom reason for the response + #[inline] + pub fn reason(&self) -> &str { + if let Some(reason) = self.get_ref().head.reason { + reason + } else { + self.get_ref() + .head + .status + .canonical_reason() + .unwrap_or("") + } + } + + /// Set the custom reason for the response + #[inline] + pub fn set_reason(&mut self, reason: &'static str) -> &mut Self { + self.get_mut().head.reason = Some(reason); + self + } + /// Get the headers from the response #[inline] pub fn headers(&self) -> &HeaderMap { @@ -167,39 +207,6 @@ impl Response { count } - /// Get the response status code - #[inline] - pub fn status(&self) -> StatusCode { - self.get_ref().head.status - } - - /// Set the `StatusCode` for this response - #[inline] - pub fn status_mut(&mut self) -> &mut StatusCode { - &mut self.get_mut().head.status - } - - /// Get custom reason for the response - #[inline] - pub fn reason(&self) -> &str { - if let Some(reason) = self.get_ref().head.reason { - reason - } else { - self.get_ref() - .head - .status - .canonical_reason() - .unwrap_or("") - } - } - - /// Set the custom reason for the response - #[inline] - pub fn set_reason(&mut self, reason: &'static str) -> &mut Self { - self.get_mut().head.reason = Some(reason); - self - } - /// Set connection type pub fn set_connection_type(&mut self, conn: ConnectionType) -> &mut Self { self.get_mut().connection_type = Some(conn); @@ -224,38 +231,20 @@ impl Response { } } - /// is chunked encoding enabled - #[inline] - pub fn chunked(&self) -> Option { - self.get_ref().chunked - } - - /// Content encoding - #[inline] - pub fn content_encoding(&self) -> Option { - self.get_ref().encoding - } - - /// Set content encoding - pub fn set_content_encoding(&mut self, enc: ContentEncoding) -> &mut Self { - self.get_mut().encoding = Some(enc); - self - } - /// Get body os this response #[inline] - pub fn body(&self) -> &Body { - &self.get_ref().body + pub fn body(&self) -> &B { + &self.1 } /// Set a body - pub fn set_body>(&mut self, body: B) { - self.get_mut().body = body.into(); + pub fn set_body(self, body: B2) -> Response { + Response(self.0, body) } /// Set a body and return previous body value - pub fn replace_body>(&mut self, body: B) -> Body { - mem::replace(&mut self.get_mut().body, body.into()) + pub fn replace_body(self, body: B2) -> (Response, B) { + (Response(self.0, body), self.1) } /// Size of response in bytes, excluding HTTP headers @@ -268,16 +257,6 @@ impl Response { self.get_mut().response_size = size; } - /// Set write buffer capacity - pub fn write_buffer_capacity(&self) -> usize { - self.get_ref().write_capacity - } - - /// Set write buffer capacity - pub fn set_write_buffer_capacity(&mut self, cap: usize) { - self.get_mut().write_capacity = cap; - } - pub(crate) fn release(self) { ResponsePool::release(self.0); } @@ -287,7 +266,7 @@ impl Response { } pub(crate) fn from_parts(parts: ResponseParts) -> Response { - Response(Box::new(InnerResponse::from_parts(parts))) + Response(Box::new(InnerResponse::from_parts(parts)), ()) } } @@ -454,24 +433,6 @@ impl ResponseBuilder { self.connection_type(ConnectionType::Close) } - /// Enables automatic chunked transfer encoding - #[inline] - pub fn chunked(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.chunked = Some(true); - } - self - } - - /// Force disable chunked encoding - #[inline] - pub fn no_chunking(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.chunked = Some(false); - } - self - } - /// Set response content type #[inline] pub fn content_type(&mut self, value: V) -> &mut Self @@ -580,63 +541,73 @@ impl ResponseBuilder { self } - /// Set write buffer capacity - /// - /// This parameter makes sense only for streaming response - /// or actor. If write buffer reaches specified capacity, stream or actor - /// get paused. - /// - /// Default write buffer capacity is 64kb - pub fn write_buffer_capacity(&mut self, cap: usize) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.write_capacity = cap; - } - self - } + // /// Set write buffer capacity + // /// + // /// This parameter makes sense only for streaming response + // /// or actor. If write buffer reaches specified capacity, stream or actor + // /// get paused. + // /// + // /// Default write buffer capacity is 64kb + // pub fn write_buffer_capacity(&mut self, cap: usize) -> &mut Self { + // if let Some(parts) = parts(&mut self.response, &self.err) { + // parts.write_capacity = cap; + // } + // self + // } /// Set a body and generate `Response`. /// /// `ResponseBuilder` can not be used after this call. - pub fn body>(&mut self, body: B) -> Response { - if let Some(e) = self.err.take() { - return Error::from(e).into(); - } + pub fn body(&mut self, body: B) -> Response { + let mut error = if let Some(e) = self.err.take() { + Some(Error::from(e)) + } else { + None + }; + let mut response = self.response.take().expect("cannot reuse response builder"); if let Some(ref jar) = self.cookies { for cookie in jar.delta() { match HeaderValue::from_str(&cookie.to_string()) { - Ok(val) => response.head.headers.append(header::SET_COOKIE, val), - Err(e) => return Error::from(e).into(), + Ok(val) => { + let _ = response.head.headers.append(header::SET_COOKIE, val); + } + Err(e) => if error.is_none() { + error = Some(Error::from(e)); + }, }; } } - response.body = body.into(); - Response(response) + if let Some(error) = error { + response.error = Some(error); + } + + Response(response, body) } #[inline] /// Set a streaming body and generate `Response`. /// /// `ResponseBuilder` can not be used after this call. - pub fn streaming(&mut self, stream: S) -> Response + pub fn streaming(&mut self, stream: S) -> Response where S: Stream + 'static, E: Into, { - self.body(Body::Streaming(Box::new(stream.map_err(|e| e.into())))) + self.body(MessageBodyStream::new(stream.map_err(|e| e.into()))) } /// Set a json body and generate `Response` /// /// `ResponseBuilder` can not be used after this call. - pub fn json(&mut self, value: T) -> Response { + pub fn json(&mut self, value: T) -> Response { self.json2(&value) } /// Set a json body and generate `Response` /// /// `ResponseBuilder` can not be used after this call. - pub fn json2(&mut self, value: &T) -> Response { + pub fn json2(&mut self, value: &T) -> Response { match serde_json::to_string(value) { Ok(body) => { let contains = if let Some(parts) = parts(&mut self.response, &self.err) @@ -651,7 +622,10 @@ impl ResponseBuilder { self.body(body) } - Err(e) => Error::from(e).into(), + Err(e) => { + let mut res: Response = Error::from(e).into(); + res.replace_body(String::new()).0 + } } } @@ -659,8 +633,8 @@ impl ResponseBuilder { /// Set an empty body and generate `Response` /// /// `ResponseBuilder` can not be used after this call. - pub fn finish(&mut self) -> Response { - self.body(Body::Empty) + pub fn finish(&mut self) -> Response<()> { + self.body(()) } /// This method construct new `ResponseBuilder` @@ -701,7 +675,7 @@ impl From for Response { } } -impl From<&'static str> for Response { +impl From<&'static str> for Response<&'static str> { fn from(val: &'static str) -> Self { Response::Ok() .content_type("text/plain; charset=utf-8") @@ -709,7 +683,7 @@ impl From<&'static str> for Response { } } -impl From<&'static [u8]> for Response { +impl From<&'static [u8]> for Response<&'static [u8]> { fn from(val: &'static [u8]) -> Self { Response::Ok() .content_type("application/octet-stream") @@ -717,7 +691,7 @@ impl From<&'static [u8]> for Response { } } -impl From for Response { +impl From for Response { fn from(val: String) -> Self { Response::Ok() .content_type("text/plain; charset=utf-8") @@ -725,15 +699,7 @@ impl From for Response { } } -impl<'a> From<&'a String> for Response { - fn from(val: &'a String) -> Self { - Response::build(StatusCode::OK) - .content_type("text/plain; charset=utf-8") - .body(val) - } -} - -impl From for Response { +impl From for Response { fn from(val: Bytes) -> Self { Response::Ok() .content_type("application/octet-stream") @@ -741,7 +707,7 @@ impl From for Response { } } -impl From for Response { +impl From for Response { fn from(val: BytesMut) -> Self { Response::Ok() .content_type("application/octet-stream") @@ -751,8 +717,6 @@ impl From for Response { struct InnerResponse { head: ResponseHead, - body: Body, - chunked: Option, encoding: Option, connection_type: Option, write_capacity: usize, @@ -763,7 +727,6 @@ struct InnerResponse { pub(crate) struct ResponseParts { head: ResponseHead, - body: Option, encoding: Option, connection_type: Option, error: Option, @@ -771,11 +734,7 @@ pub(crate) struct ResponseParts { impl InnerResponse { #[inline] - fn new( - status: StatusCode, - body: Body, - pool: &'static ResponsePool, - ) -> InnerResponse { + fn new(status: StatusCode, pool: &'static ResponsePool) -> InnerResponse { InnerResponse { head: ResponseHead { status, @@ -784,9 +743,7 @@ impl InnerResponse { reason: None, flags: MessageFlags::empty(), }, - body, pool, - chunked: None, encoding: None, connection_type: None, response_size: 0, @@ -796,18 +753,8 @@ impl InnerResponse { } /// This is for failure, we can not have Send + Sync on Streaming and Actor response - fn into_parts(mut self) -> ResponseParts { - let body = match mem::replace(&mut self.body, Body::Empty) { - Body::Empty => None, - Body::Binary(mut bin) => Some(bin.take()), - Body::Streaming(_) => { - error!("Streaming or Actor body is not support by error response"); - None - } - }; - + fn into_parts(self) -> ResponseParts { ResponseParts { - body, head: self.head, encoding: self.encoding, connection_type: self.connection_type, @@ -816,16 +763,8 @@ impl InnerResponse { } fn from_parts(parts: ResponseParts) -> InnerResponse { - let body = if let Some(ref body) = parts.body { - Body::Binary(body.clone().into()) - } else { - Body::Empty - }; - InnerResponse { - body, head: parts.head, - chunked: None, encoding: parts.encoding, connection_type: parts.connection_type, response_size: 0, @@ -864,7 +803,7 @@ impl ResponsePool { cookies: None, } } else { - let msg = Box::new(InnerResponse::new(status, Body::Empty, pool)); + let msg = Box::new(InnerResponse::new(status, pool)); ResponseBuilder { response: Some(msg), err: None, @@ -874,17 +813,16 @@ impl ResponsePool { } #[inline] - pub fn get_response( + pub fn get_response( pool: &'static ResponsePool, status: StatusCode, - body: Body, - ) -> Response { + body: B, + ) -> Response { if let Some(mut msg) = pool.0.borrow_mut().pop_front() { msg.head.status = status; - msg.body = body; - Response(msg) + Response(msg, body) } else { - Response(Box::new(InnerResponse::new(status, body, pool))) + Response(Box::new(InnerResponse::new(status, pool)), body) } } @@ -894,7 +832,7 @@ impl ResponsePool { } #[inline] - fn with_body(status: StatusCode, body: Body) -> Response { + fn with_body(status: StatusCode, body: B) -> Response { POOL.with(|pool| ResponsePool::get_response(pool, status, body)) } @@ -903,7 +841,6 @@ impl ResponsePool { let mut p = inner.pool.0.borrow_mut(); if p.len() < 128 { inner.head.clear(); - inner.chunked = None; inner.encoding = None; inner.connection_type = None; inner.response_size = 0; diff --git a/src/service.rs b/src/service.rs index 3aa5d1e41..ac92a0f76 100644 --- a/src/service.rs +++ b/src/service.rs @@ -6,7 +6,7 @@ use futures::future::{ok, Either, FutureResult}; use futures::{Async, AsyncSink, Future, Poll, Sink}; use tokio_io::{AsyncRead, AsyncWrite}; -use body::Body; +use body::MessageBody; use error::{Error, ResponseError}; use h1::{Codec, Message}; use response::Response; @@ -58,11 +58,11 @@ where match req { Ok(r) => Either::A(ok(r)), Err((e, framed)) => { - let mut resp = e.error_response(); - resp.set_body(format!("{}", e)); + let mut res = e.error_response().set_body(format!("{}", e)); + let (res, _body) = res.replace_body(()); Either::B(SendErrorFut { framed: Some(framed), - res: Some(resp.into()), + res: Some(res.into()), err: Some(e), _t: PhantomData, }) @@ -109,30 +109,30 @@ where } } -pub struct SendResponse(PhantomData<(T,)>); +pub struct SendResponse(PhantomData<(T, B)>); -impl Default for SendResponse -where - T: AsyncRead + AsyncWrite, -{ +impl Default for SendResponse { fn default() -> Self { SendResponse(PhantomData) } } -impl SendResponse +impl SendResponse where T: AsyncRead + AsyncWrite, + B: MessageBody, { pub fn send( mut framed: Framed, - mut res: Response, + res: Response, ) -> impl Future, Error = Error> { - // init codec - framed.get_codec_mut().prepare_te(&mut res); - // extract body from response - let body = res.replace_body(Body::Empty); + let (mut res, body) = res.replace_body(()); + + // init codec + framed + .get_codec_mut() + .prepare_te(&mut res.head_mut(), &mut body.length()); // write response SendResponseFut { @@ -143,15 +143,16 @@ where } } -impl NewService for SendResponse +impl NewService for SendResponse where T: AsyncRead + AsyncWrite, + B: MessageBody, { - type Request = (Response, Framed); + type Request = (Response, Framed); type Response = Framed; type Error = Error; type InitError = (); - type Service = SendResponse; + type Service = SendResponse; type Future = FutureResult; fn new_service(&self) -> Self::Future { @@ -159,22 +160,25 @@ where } } -impl Service for SendResponse +impl Service for SendResponse where T: AsyncRead + AsyncWrite, + B: MessageBody, { - type Request = (Response, Framed); + type Request = (Response, Framed); type Response = Framed; type Error = Error; - type Future = SendResponseFut; + type Future = SendResponseFut; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } - fn call(&mut self, (mut res, mut framed): Self::Request) -> Self::Future { - framed.get_codec_mut().prepare_te(&mut res); - let body = res.replace_body(Body::Empty); + fn call(&mut self, (res, mut framed): Self::Request) -> Self::Future { + let (mut res, body) = res.replace_body(()); + framed + .get_codec_mut() + .prepare_te(res.head_mut(), &mut body.length()); SendResponseFut { res: Some(Message::Item(res)), body: Some(body), @@ -183,73 +187,69 @@ where } } -pub struct SendResponseFut { +pub struct SendResponseFut { res: Option>, - body: Option, + body: Option, framed: Option>, } -impl Future for SendResponseFut +impl Future for SendResponseFut where T: AsyncRead + AsyncWrite, + B: MessageBody, { type Item = Framed; type Error = Error; fn poll(&mut self) -> Poll { - // send response - if self.res.is_some() { + loop { + let mut body_ready = self.body.is_some(); let framed = self.framed.as_mut().unwrap(); - if !framed.is_write_buf_full() { - if let Some(res) = self.res.take() { - framed.force_send(res)?; - } - } - } - // send body - if self.res.is_none() && self.body.is_some() { - let framed = self.framed.as_mut().unwrap(); - if !framed.is_write_buf_full() { - let body = self.body.take().unwrap(); - match body { - Body::Empty => (), - Body::Streaming(mut stream) => loop { - match stream.poll()? { - Async::Ready(item) => { - let done = item.is_none(); - framed.force_send(Message::Chunk(item.into()))?; - if !done { - if !framed.is_write_buf_full() { - continue; - } else { - self.body = Some(Body::Streaming(stream)); - break; - } - } - } - Async::NotReady => { - self.body = Some(Body::Streaming(stream)); - break; + // send body + if self.res.is_none() && self.body.is_some() { + while body_ready && self.body.is_some() && !framed.is_write_buf_full() { + match self.body.as_mut().unwrap().poll_next()? { + Async::Ready(item) => { + // body is done + if item.is_none() { + let _ = self.body.take(); } + framed.force_send(Message::Chunk(item))?; } - }, - Body::Binary(mut bin) => { - framed.force_send(Message::Chunk(Some(bin.take())))?; - framed.force_send(Message::Chunk(None))?; + Async::NotReady => body_ready = false, } } } - } - // flush - match self.framed.as_mut().unwrap().poll_complete()? { - Async::Ready(_) => if self.res.is_some() || self.body.is_some() { - return self.poll(); - }, - Async::NotReady => return Ok(Async::NotReady), - } + // flush write buffer + if !framed.is_write_buf_empty() { + match framed.poll_complete()? { + Async::Ready(_) => if body_ready { + continue; + } else { + return Ok(Async::NotReady); + }, + Async::NotReady => return Ok(Async::NotReady), + } + } - Ok(Async::Ready(self.framed.take().unwrap())) + // send response + if let Some(res) = self.res.take() { + framed.force_send(res)?; + continue; + } + + if self.body.is_some() { + if body_ready { + continue; + } else { + return Ok(Async::NotReady); + } + } else { + break; + } + } + return Ok(Async::Ready(self.framed.take().unwrap())); } } From 8fea1367c739224f6365185a6ec6b1c4efdd5a8f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 18 Nov 2018 13:48:42 -0800 Subject: [PATCH 074/427] re-introduce Body type, use Body as default body type for Response --- src/body.rs | 371 ++++++++++++++++++++--------------------- src/client/pipeline.rs | 4 +- src/client/request.rs | 9 +- src/error.rs | 5 +- src/h1/client.rs | 6 +- src/h1/codec.rs | 10 +- src/h1/dispatcher.rs | 18 +- src/h1/encoder.rs | 4 +- src/lib.rs | 5 +- src/response.rs | 149 ++++++++--------- src/service.rs | 4 +- src/test.rs | 7 +- src/ws/codec.rs | 5 +- src/ws/frame.rs | 5 +- tests/test_server.rs | 22 +-- tests/test_ws.rs | 20 +-- 16 files changed, 309 insertions(+), 335 deletions(-) diff --git a/src/body.rs b/src/body.rs index 3b4e0113d..a44bc6038 100644 --- a/src/body.rs +++ b/src/body.rs @@ -1,22 +1,19 @@ -use std::mem; -use std::sync::Arc; +use std::marker::PhantomData; +use std::{fmt, mem}; use bytes::{Bytes, BytesMut}; use futures::{Async, Poll, Stream}; use error::{Error, PayloadError}; -/// Type represent streaming body -pub type BodyStream = Box>; - /// Type represent streaming payload pub type PayloadStream = Box>; -#[derive(Debug)] +#[derive(Debug, PartialEq)] /// Different type of body pub enum BodyLength { None, - Zero, + Empty, Sized(usize), Sized64(u64), Chunked, @@ -32,7 +29,7 @@ pub trait MessageBody { impl MessageBody for () { fn length(&self) -> BodyLength { - BodyLength::Zero + BodyLength::Empty } fn poll_next(&mut self) -> Poll, Error> { @@ -40,150 +37,129 @@ impl MessageBody for () { } } -/// Represents various types of binary body. -/// `Content-Length` header is set to length of the body. -#[derive(Debug, PartialEq)] -pub enum Binary { - /// Bytes body +/// Represents various types of http message body. +pub enum Body { + /// Empty response. `Content-Length` header is not set. + None, + /// Zero sized response body. `Content-Length` header is set to `0`. + Empty, + /// Specific response body. Bytes(Bytes), - /// Static slice - Slice(&'static [u8]), - /// Shared string body - #[doc(hidden)] - SharedString(Arc), - /// Shared vec body - SharedVec(Arc>), + /// Generic message body. + Message(Box), } -impl Binary { - #[inline] - /// Returns `true` if body is empty - pub fn is_empty(&self) -> bool { - self.len() == 0 +impl Body { + /// Create body from slice (copy) + pub fn from_slice(s: &[u8]) -> Body { + Body::Bytes(Bytes::from(s)) } - #[inline] - /// Length of body in bytes - pub fn len(&self) -> usize { - match *self { - Binary::Bytes(ref bytes) => bytes.len(), - Binary::Slice(slice) => slice.len(), - Binary::SharedString(ref s) => s.len(), - Binary::SharedVec(ref s) => s.len(), - } - } - - /// Create binary body from slice - pub fn from_slice(s: &[u8]) -> Binary { - Binary::Bytes(Bytes::from(s)) - } - - /// Convert Binary to a Bytes instance - pub fn take(&mut self) -> Bytes { - mem::replace(self, Binary::Slice(b"")).into() + /// Create body from generic message body. + pub fn from_message(body: B) -> Body { + Body::Message(Box::new(body)) } } -impl Clone for Binary { - fn clone(&self) -> Binary { - match *self { - Binary::Bytes(ref bytes) => Binary::Bytes(bytes.clone()), - Binary::Slice(slice) => Binary::Bytes(Bytes::from(slice)), - Binary::SharedString(ref s) => Binary::SharedString(s.clone()), - Binary::SharedVec(ref s) => Binary::SharedVec(s.clone()), - } - } -} - -impl Into for Binary { - fn into(self) -> Bytes { +impl MessageBody for Body { + fn length(&self) -> BodyLength { match self { - Binary::Bytes(bytes) => bytes, - Binary::Slice(slice) => Bytes::from(slice), - Binary::SharedString(s) => Bytes::from(s.as_str()), - Binary::SharedVec(s) => Bytes::from(AsRef::<[u8]>::as_ref(s.as_ref())), + Body::None => BodyLength::None, + Body::Empty => BodyLength::Empty, + Body::Bytes(ref bin) => BodyLength::Sized(bin.len()), + Body::Message(ref body) => body.length(), + } + } + + fn poll_next(&mut self) -> Poll, Error> { + match self { + Body::None => Ok(Async::Ready(None)), + Body::Empty => Ok(Async::Ready(None)), + Body::Bytes(ref mut bin) => { + if bin.len() == 0 { + Ok(Async::Ready(None)) + } else { + Ok(Async::Ready(Some(bin.slice_to(bin.len())))) + } + } + Body::Message(ref mut body) => body.poll_next(), } } } -impl From<&'static str> for Binary { - fn from(s: &'static str) -> Binary { - Binary::Slice(s.as_ref()) - } -} - -impl From<&'static [u8]> for Binary { - fn from(s: &'static [u8]) -> Binary { - Binary::Slice(s) - } -} - -impl From> for Binary { - fn from(vec: Vec) -> Binary { - Binary::Bytes(Bytes::from(vec)) - } -} - -impl From for Binary { - fn from(s: String) -> Binary { - Binary::Bytes(Bytes::from(s)) - } -} - -impl<'a> From<&'a String> for Binary { - fn from(s: &'a String) -> Binary { - Binary::Bytes(Bytes::from(AsRef::<[u8]>::as_ref(&s))) - } -} - -impl From for Binary { - fn from(s: Bytes) -> Binary { - Binary::Bytes(s) - } -} - -impl From for Binary { - fn from(s: BytesMut) -> Binary { - Binary::Bytes(s.freeze()) - } -} - -impl From> for Binary { - fn from(body: Arc) -> Binary { - Binary::SharedString(body) - } -} - -impl<'a> From<&'a Arc> for Binary { - fn from(body: &'a Arc) -> Binary { - Binary::SharedString(Arc::clone(body)) - } -} - -impl From>> for Binary { - fn from(body: Arc>) -> Binary { - Binary::SharedVec(body) - } -} - -impl<'a> From<&'a Arc>> for Binary { - fn from(body: &'a Arc>) -> Binary { - Binary::SharedVec(Arc::clone(body)) - } -} - -impl AsRef<[u8]> for Binary { - #[inline] - fn as_ref(&self) -> &[u8] { +impl PartialEq for Body { + fn eq(&self, other: &Body) -> bool { match *self { - Binary::Bytes(ref bytes) => bytes.as_ref(), - Binary::Slice(slice) => slice, - Binary::SharedString(ref s) => s.as_bytes(), - Binary::SharedVec(ref s) => s.as_ref().as_ref(), + Body::None => match *other { + Body::None => true, + _ => false, + }, + Body::Empty => match *other { + Body::Empty => true, + _ => false, + }, + Body::Bytes(ref b) => match *other { + Body::Bytes(ref b2) => b == b2, + _ => false, + }, + Body::Message(_) => false, } } } +impl fmt::Debug for Body { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Body::None => write!(f, "Body::None"), + Body::Empty => write!(f, "Body::Zero"), + Body::Bytes(ref b) => write!(f, "Body::Bytes({:?})", b), + Body::Message(_) => write!(f, "Body::Message(_)"), + } + } +} + +impl From<&'static str> for Body { + fn from(s: &'static str) -> Body { + Body::Bytes(Bytes::from_static(s.as_ref())) + } +} + +impl From<&'static [u8]> for Body { + fn from(s: &'static [u8]) -> Body { + Body::Bytes(Bytes::from_static(s.as_ref())) + } +} + +impl From> for Body { + fn from(vec: Vec) -> Body { + Body::Bytes(Bytes::from(vec)) + } +} + +impl From for Body { + fn from(s: String) -> Body { + s.into_bytes().into() + } +} + +impl<'a> From<&'a String> for Body { + fn from(s: &'a String) -> Body { + Body::Bytes(Bytes::from(AsRef::<[u8]>::as_ref(&s))) + } +} + +impl From for Body { + fn from(s: Bytes) -> Body { + Body::Bytes(s) + } +} + +impl From for Body { + fn from(s: BytesMut) -> Body { + Body::Bytes(s.freeze()) + } +} + impl MessageBody for Bytes { fn length(&self) -> BodyLength { BodyLength::Sized(self.len()) @@ -279,26 +255,62 @@ impl MessageBody for String { } } -#[doc(hidden)] -pub struct MessageBodyStream { +/// Type represent streaming body. +/// Response does not contain `content-length` header and appropriate transfer encoding is used. +pub struct BodyStream { stream: S, + _t: PhantomData, } -impl MessageBodyStream +impl BodyStream where - S: Stream, + S: Stream, + E: Into, { pub fn new(stream: S) -> Self { - MessageBodyStream { stream } + BodyStream { + stream, + _t: PhantomData, + } } } -impl MessageBody for MessageBodyStream +impl MessageBody for BodyStream +where + S: Stream, + E: Into, +{ + fn length(&self) -> BodyLength { + BodyLength::Chunked + } + + fn poll_next(&mut self) -> Poll, Error> { + self.stream.poll().map_err(|e| e.into()) + } +} + +/// Type represent streaming body. This body implementation should be used +/// if total size of stream is known. Data get sent as is without using transfer encoding. +pub struct SizedStream { + size: usize, + stream: S, +} + +impl SizedStream +where + S: Stream, +{ + pub fn new(size: usize, stream: S) -> Self { + SizedStream { size, stream } + } +} + +impl MessageBody for SizedStream where S: Stream, { fn length(&self) -> BodyLength { - BodyLength::Chunked + BodyLength::Sized(self.size) } fn poll_next(&mut self) -> Poll, Error> { @@ -310,78 +322,61 @@ where mod tests { use super::*; - #[test] - fn test_is_empty() { - assert_eq!(Binary::from("").is_empty(), true); - assert_eq!(Binary::from("test").is_empty(), false); + impl Body { + pub(crate) fn get_ref(&self) -> &[u8] { + match *self { + Body::Bytes(ref bin) => &bin, + _ => panic!(), + } + } } #[test] fn test_static_str() { - assert_eq!(Binary::from("test").len(), 4); - assert_eq!(Binary::from("test").as_ref(), b"test"); + assert_eq!(Body::from("").length(), BodyLength::Sized(0)); + assert_eq!(Body::from("test").length(), BodyLength::Sized(4)); + assert_eq!(Body::from("test").get_ref(), b"test"); } #[test] fn test_static_bytes() { - assert_eq!(Binary::from(b"test".as_ref()).len(), 4); - assert_eq!(Binary::from(b"test".as_ref()).as_ref(), b"test"); - assert_eq!(Binary::from_slice(b"test".as_ref()).len(), 4); - assert_eq!(Binary::from_slice(b"test".as_ref()).as_ref(), b"test"); + assert_eq!(Body::from(b"test".as_ref()).length(), BodyLength::Sized(4)); + assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test"); + assert_eq!( + Body::from_slice(b"test".as_ref()).length(), + BodyLength::Sized(4) + ); + assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test"); } #[test] fn test_vec() { - assert_eq!(Binary::from(Vec::from("test")).len(), 4); - assert_eq!(Binary::from(Vec::from("test")).as_ref(), b"test"); + assert_eq!(Body::from(Vec::from("test")).length(), BodyLength::Sized(4)); + assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test"); } #[test] fn test_bytes() { - assert_eq!(Binary::from(Bytes::from("test")).len(), 4); - assert_eq!(Binary::from(Bytes::from("test")).as_ref(), b"test"); - } - - #[test] - fn test_arc_string() { - let b = Arc::new("test".to_owned()); - assert_eq!(Binary::from(b.clone()).len(), 4); - assert_eq!(Binary::from(b.clone()).as_ref(), b"test"); - assert_eq!(Binary::from(&b).len(), 4); - assert_eq!(Binary::from(&b).as_ref(), b"test"); + assert_eq!( + Body::from(Bytes::from("test")).length(), + BodyLength::Sized(4) + ); + assert_eq!(Body::from(Bytes::from("test")).get_ref(), b"test"); } #[test] fn test_string() { let b = "test".to_owned(); - assert_eq!(Binary::from(b.clone()).len(), 4); - assert_eq!(Binary::from(b.clone()).as_ref(), b"test"); - assert_eq!(Binary::from(&b).len(), 4); - assert_eq!(Binary::from(&b).as_ref(), b"test"); - } - - #[test] - fn test_shared_vec() { - let b = Arc::new(Vec::from(&b"test"[..])); - assert_eq!(Binary::from(b.clone()).len(), 4); - assert_eq!(Binary::from(b.clone()).as_ref(), &b"test"[..]); - assert_eq!(Binary::from(&b).len(), 4); - assert_eq!(Binary::from(&b).as_ref(), &b"test"[..]); + assert_eq!(Body::from(b.clone()).length(), BodyLength::Sized(4)); + assert_eq!(Body::from(b.clone()).get_ref(), b"test"); + assert_eq!(Body::from(&b).length(), BodyLength::Sized(4)); + assert_eq!(Body::from(&b).get_ref(), b"test"); } #[test] fn test_bytes_mut() { let b = BytesMut::from("test"); - assert_eq!(Binary::from(b.clone()).len(), 4); - assert_eq!(Binary::from(b).as_ref(), b"test"); - } - - #[test] - fn test_binary_into() { - let bytes = Bytes::from_static(b"test"); - let b: Bytes = Binary::from("test").into(); - assert_eq!(b, bytes); - let b: Bytes = Binary::from(bytes.clone()).into(); - assert_eq!(b, bytes); + assert_eq!(Body::from(b.clone()).length(), BodyLength::Sized(4)); + assert_eq!(Body::from(b).get_ref(), b"test"); } } diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs index 56c22bd2a..8be860ae9 100644 --- a/src/client/pipeline.rs +++ b/src/client/pipeline.rs @@ -36,7 +36,9 @@ where .and_then(|framed| framed.send((head, len).into()).from_err()) // send request body .and_then(move |framed| match body.length() { - BodyLength::None | BodyLength::Zero => Either::A(ok(framed)), + BodyLength::None | BodyLength::Empty | BodyLength::Sized(0) => { + Either::A(ok(framed)) + } _ => Either::B(SendBody::new(body, framed)), }) // read response and init read body diff --git a/src/client/request.rs b/src/client/request.rs index f0b76ed04..dd418a6fe 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -9,7 +9,7 @@ use futures::{Future, Stream}; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use urlcrate::Url; -use body::{MessageBody, MessageBodyStream}; +use body::{BodyStream, MessageBody}; use error::Error; use header::{self, Header, IntoHeaderValue}; use http::{ @@ -534,14 +534,15 @@ impl ClientRequestBuilder { /// Set an streaming body and generate `ClientRequest`. /// /// `ClientRequestBuilder` can not be used after this call. - pub fn stream( + pub fn stream( &mut self, stream: S, ) -> Result, HttpError> where - S: Stream, + S: Stream, + E: Into + 'static, { - self.body(MessageBodyStream::new(stream)) + self.body(BodyStream::new(stream)) } /// Set an empty body and generate `ClientRequest`. diff --git a/src/error.rs b/src/error.rs index 1f70396c3..2e0c2382a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -20,6 +20,7 @@ use tokio_timer::Error as TimerError; // re-exports pub use cookie::ParseError as CookieParseError; +use body::Body; use response::{Response, ResponseParts}; /// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html) @@ -100,10 +101,10 @@ impl Error { } /// Converts error to a response instance and set error message as response body - pub fn response_with_message(self) -> Response { + pub fn response_with_message(self) -> Response { let message = format!("{}", self); let resp: Response = self.into(); - resp.set_body(message) + resp.set_body(Body::from(message)) } } diff --git a/src/h1/client.rs b/src/h1/client.rs index ace504668..2cb2fb2e0 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -7,7 +7,7 @@ use tokio_codec::{Decoder, Encoder}; use super::decoder::{MessageDecoder, PayloadDecoder, PayloadItem, PayloadType}; use super::encoder::RequestEncoder; use super::{Message, MessageType}; -use body::{Binary, BodyLength}; +use body::BodyLength; use client::ClientResponse; use config::ServiceConfig; use error::{ParseError, PayloadError}; @@ -167,7 +167,7 @@ impl ClientCodecInner { BodyLength::Chunked => { buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") } - BodyLength::Zero => { + BodyLength::Empty => { len_is_set = false; buffer.extend_from_slice(b"\r\n") } @@ -183,7 +183,7 @@ impl ClientCodecInner { TRANSFER_ENCODING => continue, CONTENT_LENGTH => match length { BodyLength::None => (), - BodyLength::Zero => len_is_set = true, + BodyLength::Empty => len_is_set = true, _ => continue, }, DATE => has_date = true, diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 6bc20b180..b4a62a50e 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -8,7 +8,7 @@ use tokio_codec::{Decoder, Encoder}; use super::decoder::{MessageDecoder, PayloadDecoder, PayloadItem, PayloadType}; use super::encoder::ResponseEncoder; use super::{Message, MessageType}; -use body::{Binary, BodyLength}; +use body::BodyLength; use config::ServiceConfig; use error::ParseError; use helpers; @@ -106,7 +106,7 @@ impl Codec { fn encode_response( &mut self, - mut msg: Response, + mut msg: Response<()>, buffer: &mut BytesMut, ) -> io::Result<()> { let ka = self.flags.contains(Flags::KEEPALIVE_ENABLED) && msg @@ -149,7 +149,7 @@ impl Codec { BodyLength::Chunked => { buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") } - BodyLength::Zero => { + BodyLength::Empty => { len_is_set = false; buffer.extend_from_slice(b"\r\n") } @@ -174,7 +174,7 @@ impl Codec { TRANSFER_ENCODING => continue, CONTENT_LENGTH => match self.te.length { BodyLength::None => (), - BodyLength::Zero => { + BodyLength::Empty => { len_is_set = true; } _ => continue, @@ -268,7 +268,7 @@ impl Decoder for Codec { } impl Encoder for Codec { - type Item = Message; + type Item = Message>; type Error = io::Error; fn encode( diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 508962b43..ff9d54e66 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -65,7 +65,7 @@ where enum DispatcherMessage { Item(Request), - Error(Response), + Error(Response<()>), } enum State { @@ -190,7 +190,7 @@ where fn send_response( &mut self, - message: Response, + message: Response<()>, body: B1, ) -> Result, DispatchError> { self.framed @@ -206,7 +206,7 @@ where .set(Flags::KEEPALIVE, self.framed.get_codec().keepalive()); self.flags.remove(Flags::FLUSHED); match body.length() { - BodyLength::None | BodyLength::Zero => Ok(State::None), + BodyLength::None | BodyLength::Empty => Ok(State::None), _ => Ok(State::SendPayload(body)), } } @@ -351,7 +351,7 @@ where ); self.flags.insert(Flags::DISCONNECTED); self.messages.push_back(DispatcherMessage::Error( - Response::InternalServerError().finish(), + Response::InternalServerError().finish().drop_body(), )); self.error = Some(DispatchError::InternalError); break; @@ -364,7 +364,7 @@ where error!("Internal server error: unexpected eof"); self.flags.insert(Flags::DISCONNECTED); self.messages.push_back(DispatcherMessage::Error( - Response::InternalServerError().finish(), + Response::InternalServerError().finish().drop_body(), )); self.error = Some(DispatchError::InternalError); break; @@ -389,7 +389,7 @@ where // Malformed requests should be responded with 400 self.messages.push_back(DispatcherMessage::Error( - Response::BadRequest().finish(), + Response::BadRequest().finish().drop_body(), )); self.flags.insert(Flags::DISCONNECTED); self.error = Some(e.into()); @@ -440,8 +440,10 @@ where // timeout on first request (slow request) return 408 trace!("Slow request timeout"); self.flags.insert(Flags::STARTED | Flags::DISCONNECTED); - let _ = self - .send_response(Response::RequestTimeout().finish(), ()); + let _ = self.send_response( + Response::RequestTimeout().finish().drop_body(), + (), + ); self.state = State::None; } } else if let Some(deadline) = self.config.keep_alive_expire() { diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index aee17c1f0..fd52d7316 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -8,7 +8,7 @@ use bytes::{Bytes, BytesMut}; use http::header::{HeaderValue, ACCEPT_ENCODING, CONTENT_LENGTH}; use http::{StatusCode, Version}; -use body::{Binary, BodyLength}; +use body::BodyLength; use header::ContentEncoding; use http::Method; use message::{RequestHead, ResponseHead}; @@ -52,7 +52,7 @@ impl ResponseEncoder { ) { self.head = head; let transfer = match length { - BodyLength::Zero => { + BodyLength::Empty => { match resp.status { StatusCode::NO_CONTENT | StatusCode::CONTINUE diff --git a/src/lib.rs b/src/lib.rs index 4b43cae40..615f04010 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,7 +109,7 @@ extern crate serde_derive; #[cfg(feature = "ssl")] extern crate openssl; -mod body; +pub mod body; pub mod client; mod config; mod extensions; @@ -129,7 +129,7 @@ pub mod h1; pub(crate) mod helpers; pub mod test; pub mod ws; -pub use body::{Binary, MessageBody}; +pub use body::{Body, MessageBody}; pub use error::{Error, ResponseError, Result}; pub use extensions::Extensions; pub use httpmessage::HttpMessage; @@ -150,7 +150,6 @@ pub mod dev { //! use actix_http::dev::*; //! ``` - pub use body::BodyStream; pub use httpmessage::{MessageBody, Readlines, UrlEncoded}; pub use json::JsonBody; pub use payload::{Payload, PayloadBuffer}; diff --git a/src/response.rs b/src/response.rs index b3e599826..a6f7c81c2 100644 --- a/src/response.rs +++ b/src/response.rs @@ -12,9 +12,9 @@ use http::{Error as HttpError, HeaderMap, HttpTryFrom, StatusCode, Version}; use serde::Serialize; use serde_json; -use body::{MessageBody, MessageBodyStream}; +use body::{Body, BodyStream, MessageBody}; use error::Error; -use header::{ContentEncoding, Header, IntoHeaderValue}; +use header::{Header, IntoHeaderValue}; use message::{Head, MessageFlags, ResponseHead}; /// max write buffer size 64k @@ -32,9 +32,9 @@ pub enum ConnectionType { } /// An HTTP Response -pub struct Response(Box, B); +pub struct Response(Box, B); -impl Response<()> { +impl Response { /// Create http response builder with specific status. #[inline] pub fn build(status: StatusCode) -> ResponseBuilder { @@ -50,7 +50,7 @@ impl Response<()> { /// Constructs a response #[inline] pub fn new(status: StatusCode) -> Response { - ResponsePool::with_body(status, ()) + ResponsePool::with_body(status, Body::Empty) } /// Constructs an error response @@ -242,6 +242,11 @@ impl Response { Response(self.0, body) } + /// Drop request's body + pub fn drop_body(self) -> Response<()> { + Response(self.0, ()) + } + /// Set a body and return previous body value pub fn replace_body(self, body: B2) -> (Response, B) { (Response(self.0, body), self.1) @@ -252,7 +257,7 @@ impl Response { self.get_ref().response_size } - /// Set content encoding + /// Set response size pub(crate) fn set_response_size(&mut self, size: u64) { self.get_mut().response_size = size; } @@ -266,7 +271,7 @@ impl Response { } pub(crate) fn from_parts(parts: ResponseParts) -> Response { - Response(Box::new(InnerResponse::from_parts(parts)), ()) + Response(Box::new(InnerResponse::from_parts(parts)), Body::Empty) } } @@ -279,7 +284,6 @@ impl fmt::Debug for Response { self.get_ref().head.status, self.get_ref().head.reason.unwrap_or("") ); - let _ = writeln!(f, " encoding: {:?}", self.get_ref().encoding); let _ = writeln!(f, " headers:"); for (key, val) in self.get_ref().head.headers.iter() { let _ = writeln!(f, " {:?}: {:?}", key, val); @@ -396,20 +400,6 @@ impl ResponseBuilder { self } - /// Set content encoding. - /// - /// By default `ContentEncoding::Auto` is used, which automatically - /// negotiates content encoding based on request's `Accept-Encoding` - /// headers. To enforce specific encoding, use specific - /// ContentEncoding` value. - #[inline] - pub fn content_encoding(&mut self, enc: ContentEncoding) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.encoding = Some(enc); - } - self - } - /// Set connection type #[inline] #[doc(hidden)] @@ -558,7 +548,14 @@ impl ResponseBuilder { /// Set a body and generate `Response`. /// /// `ResponseBuilder` can not be used after this call. - pub fn body(&mut self, body: B) -> Response { + pub fn body>(&mut self, body: B) -> Response { + self.message_body(body.into()) + } + + /// Set a body and generate `Response`. + /// + /// `ResponseBuilder` can not be used after this call. + pub fn message_body(&mut self, body: B) -> Response { let mut error = if let Some(e) = self.err.take() { Some(Error::from(e)) } else { @@ -589,25 +586,25 @@ impl ResponseBuilder { /// Set a streaming body and generate `Response`. /// /// `ResponseBuilder` can not be used after this call. - pub fn streaming(&mut self, stream: S) -> Response + pub fn streaming(&mut self, stream: S) -> Response where S: Stream + 'static, - E: Into, + E: Into + 'static, { - self.body(MessageBodyStream::new(stream.map_err(|e| e.into()))) + self.body(Body::from_message(BodyStream::new(stream))) } /// Set a json body and generate `Response` /// /// `ResponseBuilder` can not be used after this call. - pub fn json(&mut self, value: T) -> Response { + pub fn json(&mut self, value: T) -> Response { self.json2(&value) } /// Set a json body and generate `Response` /// /// `ResponseBuilder` can not be used after this call. - pub fn json2(&mut self, value: &T) -> Response { + pub fn json2(&mut self, value: &T) -> Response { match serde_json::to_string(value) { Ok(body) => { let contains = if let Some(parts) = parts(&mut self.response, &self.err) @@ -620,12 +617,9 @@ impl ResponseBuilder { self.header(header::CONTENT_TYPE, "application/json"); } - self.body(body) - } - Err(e) => { - let mut res: Response = Error::from(e).into(); - res.replace_body(String::new()).0 + self.body(Body::from(body)) } + Err(e) => Error::from(e).into(), } } @@ -633,8 +627,8 @@ impl ResponseBuilder { /// Set an empty body and generate `Response` /// /// `ResponseBuilder` can not be used after this call. - pub fn finish(&mut self) -> Response<()> { - self.body(()) + pub fn finish(&mut self) -> Response { + self.body(Body::Empty) } /// This method construct new `ResponseBuilder` @@ -675,7 +669,7 @@ impl From for Response { } } -impl From<&'static str> for Response<&'static str> { +impl From<&'static str> for Response { fn from(val: &'static str) -> Self { Response::Ok() .content_type("text/plain; charset=utf-8") @@ -683,7 +677,7 @@ impl From<&'static str> for Response<&'static str> { } } -impl From<&'static [u8]> for Response<&'static [u8]> { +impl From<&'static [u8]> for Response { fn from(val: &'static [u8]) -> Self { Response::Ok() .content_type("application/octet-stream") @@ -691,7 +685,7 @@ impl From<&'static [u8]> for Response<&'static [u8]> { } } -impl From for Response { +impl From for Response { fn from(val: String) -> Self { Response::Ok() .content_type("text/plain; charset=utf-8") @@ -699,7 +693,15 @@ impl From for Response { } } -impl From for Response { +impl<'a> From<&'a String> for Response { + fn from(val: &'a String) -> Self { + Response::Ok() + .content_type("text/plain; charset=utf-8") + .body(val) + } +} + +impl From for Response { fn from(val: Bytes) -> Self { Response::Ok() .content_type("application/octet-stream") @@ -707,7 +709,7 @@ impl From for Response { } } -impl From for Response { +impl From for Response { fn from(val: BytesMut) -> Self { Response::Ok() .content_type("application/octet-stream") @@ -717,7 +719,6 @@ impl From for Response { struct InnerResponse { head: ResponseHead, - encoding: Option, connection_type: Option, write_capacity: usize, response_size: u64, @@ -727,7 +728,6 @@ struct InnerResponse { pub(crate) struct ResponseParts { head: ResponseHead, - encoding: Option, connection_type: Option, error: Option, } @@ -744,7 +744,6 @@ impl InnerResponse { flags: MessageFlags::empty(), }, pool, - encoding: None, connection_type: None, response_size: 0, write_capacity: MAX_WRITE_BUFFER_SIZE, @@ -756,7 +755,6 @@ impl InnerResponse { fn into_parts(self) -> ResponseParts { ResponseParts { head: self.head, - encoding: self.encoding, connection_type: self.connection_type, error: self.error, } @@ -765,7 +763,6 @@ impl InnerResponse { fn from_parts(parts: ResponseParts) -> InnerResponse { InnerResponse { head: parts.head, - encoding: parts.encoding, connection_type: parts.connection_type, response_size: 0, write_capacity: MAX_WRITE_BUFFER_SIZE, @@ -841,7 +838,6 @@ impl ResponsePool { let mut p = inner.pool.0.borrow_mut(); if p.len() < 128 { inner.head.clear(); - inner.encoding = None; inner.connection_type = None; inner.response_size = 0; inner.error = None; @@ -854,11 +850,10 @@ impl ResponsePool { #[cfg(test)] mod tests { use super::*; - use body::Binary; + use body::Body; use http; use http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; - use header::ContentEncoding; // use test::TestRequest; #[test] @@ -953,24 +948,24 @@ mod tests { assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") } - #[test] - fn test_content_encoding() { - let resp = Response::build(StatusCode::OK).finish(); - assert_eq!(resp.content_encoding(), None); + // #[test] + // fn test_content_encoding() { + // let resp = Response::build(StatusCode::OK).finish(); + // assert_eq!(resp.content_encoding(), None); - #[cfg(feature = "brotli")] - { - let resp = Response::build(StatusCode::OK) - .content_encoding(ContentEncoding::Br) - .finish(); - assert_eq!(resp.content_encoding(), Some(ContentEncoding::Br)); - } + // #[cfg(feature = "brotli")] + // { + // let resp = Response::build(StatusCode::OK) + // .content_encoding(ContentEncoding::Br) + // .finish(); + // assert_eq!(resp.content_encoding(), Some(ContentEncoding::Br)); + // } - let resp = Response::build(StatusCode::OK) - .content_encoding(ContentEncoding::Gzip) - .finish(); - assert_eq!(resp.content_encoding(), Some(ContentEncoding::Gzip)); - } + // let resp = Response::build(StatusCode::OK) + // .content_encoding(ContentEncoding::Gzip) + // .finish(); + // assert_eq!(resp.content_encoding(), Some(ContentEncoding::Gzip)); + // } #[test] fn test_json() { @@ -1020,15 +1015,6 @@ mod tests { ); } - impl Body { - pub(crate) fn bin_ref(&self) -> &Binary { - match *self { - Body::Binary(ref bin) => bin, - _ => panic!(), - } - } - } - #[test] fn test_into_response() { let resp: Response = "test".into(); @@ -1038,7 +1024,7 @@ mod tests { HeaderValue::from_static("text/plain; charset=utf-8") ); assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from("test")); + assert_eq!(resp.body().get_ref(), b"test"); let resp: Response = b"test".as_ref().into(); assert_eq!(resp.status(), StatusCode::OK); @@ -1047,7 +1033,7 @@ mod tests { HeaderValue::from_static("application/octet-stream") ); assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(b"test".as_ref())); + assert_eq!(resp.body().get_ref(), b"test"); let resp: Response = "test".to_owned().into(); assert_eq!(resp.status(), StatusCode::OK); @@ -1056,7 +1042,7 @@ mod tests { HeaderValue::from_static("text/plain; charset=utf-8") ); assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from("test".to_owned())); + assert_eq!(resp.body().get_ref(), b"test"); let resp: Response = (&"test".to_owned()).into(); assert_eq!(resp.status(), StatusCode::OK); @@ -1065,7 +1051,7 @@ mod tests { HeaderValue::from_static("text/plain; charset=utf-8") ); assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(&"test".to_owned())); + assert_eq!(resp.body().get_ref(), b"test"); let b = Bytes::from_static(b"test"); let resp: Response = b.into(); @@ -1075,10 +1061,7 @@ mod tests { HeaderValue::from_static("application/octet-stream") ); assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.body().bin_ref(), - &Binary::from(Bytes::from_static(b"test")) - ); + assert_eq!(resp.body().get_ref(), b"test"); let b = Bytes::from_static(b"test"); let resp: Response = b.into(); @@ -1088,7 +1071,7 @@ mod tests { HeaderValue::from_static("application/octet-stream") ); assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(BytesMut::from("test"))); + assert_eq!(resp.body().get_ref(), b"test"); let b = BytesMut::from("test"); let resp: Response = b.into(); @@ -1098,7 +1081,7 @@ mod tests { HeaderValue::from_static("application/octet-stream") ); assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(BytesMut::from("test"))); + assert_eq!(resp.body().get_ref(), b"test"); } #[test] diff --git a/src/service.rs b/src/service.rs index ac92a0f76..51a16b48b 100644 --- a/src/service.rs +++ b/src/service.rs @@ -72,7 +72,7 @@ where } pub struct SendErrorFut { - res: Option>, + res: Option>>, framed: Option>, err: Option, _t: PhantomData, @@ -188,7 +188,7 @@ where } pub struct SendResponseFut { - res: Option>, + res: Option>>, body: Option, framed: Option>, } diff --git a/src/test.rs b/src/test.rs index 439749343..c6f258a77 100644 --- a/src/test.rs +++ b/src/test.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use actix::System; +use bytes::Bytes; use cookie::Cookie; use futures::Future; use http::header::HeaderName; @@ -11,7 +12,6 @@ use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; use net2::TcpBuilder; use tokio::runtime::current_thread::Runtime; -use body::Binary; use header::{Header, IntoHeaderValue}; use payload::Payload; use request::Request; @@ -362,10 +362,9 @@ impl TestRequest { } /// Set request payload - pub fn set_payload>(mut self, data: B) -> Self { - let mut data = data.into(); + pub fn set_payload>(mut self, data: B) -> Self { let mut payload = Payload::empty(); - payload.unread_data(data.take()); + payload.unread_data(data.into()); self.payload = Some(payload); self } diff --git a/src/ws/codec.rs b/src/ws/codec.rs index 5bdc58199..10c505287 100644 --- a/src/ws/codec.rs +++ b/src/ws/codec.rs @@ -1,10 +1,9 @@ -use bytes::BytesMut; +use bytes::{Bytes, BytesMut}; use tokio_codec::{Decoder, Encoder}; use super::frame::Parser; use super::proto::{CloseReason, OpCode}; use super::ProtocolError; -use body::Binary; /// `WebSocket` Message #[derive(Debug, PartialEq)] @@ -12,7 +11,7 @@ pub enum Message { /// Text message Text(String), /// Binary message - Binary(Binary), + Binary(Bytes), /// Ping message Ping(String), /// Pong message diff --git a/src/ws/frame.rs b/src/ws/frame.rs index ca5f0e892..56d282964 100644 --- a/src/ws/frame.rs +++ b/src/ws/frame.rs @@ -1,8 +1,7 @@ use byteorder::{ByteOrder, LittleEndian, NetworkEndian}; -use bytes::{BufMut, BytesMut}; +use bytes::{BufMut, Bytes, BytesMut}; use rand; -use body::Binary; use ws::mask::apply_mask; use ws::proto::{CloseCode, CloseReason, OpCode}; use ws::ProtocolError; @@ -151,7 +150,7 @@ impl Parser { } /// Generate binary representation - pub fn write_message>( + pub fn write_message>( dst: &mut BytesMut, pl: B, op: OpCode, diff --git a/tests/test_server.rs b/tests/test_server.rs index c8de0290d..c499cf635 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -15,7 +15,7 @@ use bytes::Bytes; use futures::future::{self, ok}; use futures::stream::once; -use actix_http::{h1, http, Body, KeepAlive, Request, Response}; +use actix_http::{body, h1, http, Body, Error, KeepAlive, Request, Response}; #[test] fn test_h1_v2() { @@ -349,11 +349,9 @@ fn test_body_length() { .bind("test", addr, move || { h1::H1Service::new(|_| { let body = once(Ok(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>( - Response::Ok() - .content_length(STR.len() as u64) - .body(Body::Streaming(Box::new(body))), - ) + ok::<_, ()>(Response::Ok().body(Body::from_message( + body::SizedStream::new(STR.len(), body), + ))) }).map(|_| ()) }).unwrap() .run() @@ -379,12 +377,8 @@ fn test_body_chunked_explicit() { Server::new() .bind("test", addr, move || { h1::H1Service::new(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>( - Response::Ok() - .chunked() - .body(Body::Streaming(Box::new(body))), - ) + let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); + ok::<_, ()>(Response::Ok().streaming(body)) }).map(|_| ()) }).unwrap() .run() @@ -412,8 +406,8 @@ fn test_body_chunked_implicit() { Server::new() .bind("test", addr, move || { h1::H1Service::new(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>(Response::Ok().body(Body::Streaming(Box::new(body)))) + let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); + ok::<_, ()>(Response::Ok().streaming(body)) }).map(|_| ()) }).unwrap() .run() diff --git a/tests/test_ws.rs b/tests/test_ws.rs index 85770fa8e..c246d5e47 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -18,7 +18,7 @@ use bytes::{Bytes, BytesMut}; use futures::future::{lazy, ok, Either}; use futures::{Future, IntoFuture, Sink, Stream}; -use actix_http::{h1, ws, ResponseError, ServiceConfig}; +use actix_http::{h1, ws, ResponseError, SendResponse, ServiceConfig}; fn ws_service(req: ws::Frame) -> impl Future { match req { @@ -55,20 +55,19 @@ fn test_simple() { match ws::verify_handshake(&req) { Err(e) => { // validation failed - let resp = e.error_response(); Either::A( - framed - .send(h1::Message::Item(resp)) + SendResponse::send(framed, e.error_response()) .map_err(|_| ()) .map(|_| ()), ) } - Ok(_) => Either::B( - // send response - framed - .send(h1::Message::Item( + Ok(_) => { + Either::B( + // send handshake response + SendResponse::send( + framed, ws::handshake_response(&req).finish(), - )).map_err(|_| ()) + ).map_err(|_| ()) .and_then(|framed| { // start websocket service let framed = @@ -76,7 +75,8 @@ fn test_simple() { ws::Transport::with(framed, ws_service) .map_err(|_| ()) }), - ), + ) + } } } else { panic!() From adad2033141499ee071e75cc3a74eba67b2bcd6a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 18 Nov 2018 17:52:56 -0800 Subject: [PATCH 075/427] refactor encoder/decoder impl --- Cargo.toml | 4 +- src/body.rs | 7 ++-- src/client/pipeline.rs | 2 +- src/client/request.rs | 17 ++++++++- src/client/response.rs | 4 +- src/h1/client.rs | 45 ++++++++++------------ src/h1/codec.rs | 87 +++++++++++++++++++----------------------- src/h1/decoder.rs | 18 +++++++-- src/h1/dispatcher.rs | 35 +++++++---------- src/h1/encoder.rs | 17 ++------- src/lib.rs | 3 +- src/message.rs | 85 ++++++++++++++++++++++++++++++++++++++++- src/request.rs | 4 +- src/response.rs | 86 +++++++++-------------------------------- src/service.rs | 49 +++++++++--------------- src/ws/mod.rs | 4 +- tests/test_server.rs | 22 +++++++---- 17 files changed, 255 insertions(+), 234 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 80d245951..2eef1c50f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,9 +91,11 @@ native-tls = { version="0.2", optional = true } # openssl openssl = { version="0.10", optional = true } -#rustls +# rustls rustls = { version = "^0.14", optional = true } +backtrace="*" + [dev-dependencies] actix-web = "0.7" env_logger = "0.5" diff --git a/src/body.rs b/src/body.rs index a44bc6038..3449448e3 100644 --- a/src/body.rs +++ b/src/body.rs @@ -9,7 +9,7 @@ use error::{Error, PayloadError}; /// Type represent streaming payload pub type PayloadStream = Box>; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Copy, Clone)] /// Different type of body pub enum BodyLength { None, @@ -76,10 +76,11 @@ impl MessageBody for Body { Body::None => Ok(Async::Ready(None)), Body::Empty => Ok(Async::Ready(None)), Body::Bytes(ref mut bin) => { - if bin.len() == 0 { + let len = bin.len(); + if len == 0 { Ok(Async::Ready(None)) } else { - Ok(Async::Ready(Some(bin.slice_to(bin.len())))) + Ok(Async::Ready(Some(bin.split_to(len)))) } } Body::Message(ref mut body) => body.poll_next(), diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs index 8be860ae9..63551cffa 100644 --- a/src/client/pipeline.rs +++ b/src/client/pipeline.rs @@ -33,7 +33,7 @@ where .from_err() // create Framed and send reqest .map(|io| Framed::new(io, h1::ClientCodec::default())) - .and_then(|framed| framed.send((head, len).into()).from_err()) + .and_then(move |framed| framed.send((head, len).into()).from_err()) // send request body .and_then(move |framed| match body.length() { BodyLength::None | BodyLength::Empty | BodyLength::Sized(0) => { diff --git a/src/client/request.rs b/src/client/request.rs index dd418a6fe..cc65b9db1 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -16,7 +16,7 @@ use http::{ uri, Error as HttpError, HeaderMap, HeaderName, HeaderValue, HttpTryFrom, Method, Uri, Version, }; -use message::RequestHead; +use message::{Head, RequestHead}; use super::response::ClientResponse; use super::{pipeline, Connect, Connection, ConnectorError, SendRequestError}; @@ -365,8 +365,21 @@ impl ClientRequestBuilder { where V: IntoHeaderValue, { + { + if let Some(parts) = parts(&mut self.head, &self.err) { + parts.set_upgrade(); + } + } self.set_header(header::UPGRADE, value) - .set_header(header::CONNECTION, "upgrade") + } + + /// Close connection + #[inline] + pub fn close(&mut self) -> &mut Self { + if let Some(parts) = parts(&mut self.head, &self.err) { + parts.force_close(); + } + self } /// Set request's content type diff --git a/src/client/response.rs b/src/client/response.rs index 41c18562a..dc7b13c18 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -8,7 +8,7 @@ use http::{HeaderMap, StatusCode, Version}; use body::PayloadStream; use error::PayloadError; use httpmessage::HttpMessage; -use message::{MessageFlags, ResponseHead}; +use message::{Head, ResponseHead}; use super::pipeline::Payload; @@ -81,7 +81,7 @@ impl ClientResponse { /// Checks if a connection should be kept alive. #[inline] pub fn keep_alive(&self) -> bool { - self.head().flags.contains(MessageFlags::KEEPALIVE) + self.head().keep_alive() } } diff --git a/src/h1/client.rs b/src/h1/client.rs index 2cb2fb2e0..e2d1eefe6 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -16,7 +16,7 @@ use http::header::{ HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE, }; use http::{Method, Version}; -use message::{MessagePool, RequestHead}; +use message::{Head, MessagePool, RequestHead}; bitflags! { struct Flags: u8 { @@ -135,7 +135,7 @@ fn prn_version(ver: Version) -> &'static str { } impl ClientCodecInner { - fn encode_response( + fn encode_request( &mut self, msg: RequestHead, length: BodyLength, @@ -146,7 +146,7 @@ impl ClientCodecInner { // status line write!( Writer(buffer), - "{} {} {}", + "{} {} {}\r\n", msg.method, msg.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"), prn_version(msg.version) @@ -156,38 +156,26 @@ impl ClientCodecInner { buffer.reserve(msg.headers.len() * AVERAGE_HEADER_SIZE); // content length - let mut len_is_set = true; match length { BodyLength::Sized(len) => helpers::write_content_length(len, buffer), BodyLength::Sized64(len) => { - buffer.extend_from_slice(b"\r\ncontent-length: "); + buffer.extend_from_slice(b"content-length: "); write!(buffer.writer(), "{}", len)?; buffer.extend_from_slice(b"\r\n"); } BodyLength::Chunked => { - buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") - } - BodyLength::Empty => { - len_is_set = false; - buffer.extend_from_slice(b"\r\n") - } - BodyLength::None | BodyLength::Stream => { - buffer.extend_from_slice(b"\r\n") + buffer.extend_from_slice(b"transfer-encoding: chunked\r\n") } + BodyLength::Empty => buffer.extend_from_slice(b"content-length: 0\r\n"), + BodyLength::None | BodyLength::Stream => (), } let mut has_date = false; for (key, value) in &msg.headers { match *key { - TRANSFER_ENCODING => continue, - CONTENT_LENGTH => match length { - BodyLength::None => (), - BodyLength::Empty => len_is_set = true, - _ => continue, - }, + TRANSFER_ENCODING | CONNECTION | CONTENT_LENGTH => continue, DATE => has_date = true, - UPGRADE => self.flags.insert(Flags::UPGRADE), _ => (), } @@ -197,12 +185,19 @@ impl ClientCodecInner { buffer.put_slice(b"\r\n"); } - // set content length - if !len_is_set { - buffer.extend_from_slice(b"content-length: 0\r\n") + // Connection header + if msg.upgrade() { + self.flags.set(Flags::UPGRADE, msg.upgrade()); + buffer.extend_from_slice(b"connection: upgrade\r\n"); + } else if msg.keep_alive() { + if self.version < Version::HTTP_11 { + buffer.extend_from_slice(b"connection: keep-alive\r\n"); + } + } else if self.version >= Version::HTTP_11 { + buffer.extend_from_slice(b"connection: close\r\n"); } - // set date header + // Date header if !has_date { self.config.set_date(buffer); } else { @@ -276,7 +271,7 @@ impl Encoder for ClientCodec { ) -> Result<(), Self::Error> { match item { Message::Item((msg, btype)) => { - self.inner.encode_response(msg, btype, dst)?; + self.inner.encode_request(msg, btype, dst)?; } Message::Chunk(Some(bytes)) => { self.inner.te.encode(bytes.as_ref(), dst)?; diff --git a/src/h1/codec.rs b/src/h1/codec.rs index b4a62a50e..117c8cde1 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -13,8 +13,8 @@ use config::ServiceConfig; use error::ParseError; use helpers; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; -use http::{Method, Version}; -use message::ResponseHead; +use http::{Method, StatusCode, Version}; +use message::{Head, ResponseHead}; use request::Request; use response::Response; @@ -99,69 +99,71 @@ impl Codec { } /// prepare transfer encoding - pub fn prepare_te(&mut self, head: &mut ResponseHead, length: &mut BodyLength) { + fn prepare_te(&mut self, head: &mut ResponseHead, length: BodyLength) { self.te .update(head, self.flags.contains(Flags::HEAD), self.version, length); } fn encode_response( &mut self, - mut msg: Response<()>, + msg: &mut ResponseHead, + length: BodyLength, buffer: &mut BytesMut, ) -> io::Result<()> { - let ka = self.flags.contains(Flags::KEEPALIVE_ENABLED) && msg - .keep_alive() - .unwrap_or_else(|| self.flags.contains(Flags::KEEPALIVE)); + msg.version = self.version; // Connection upgrade if msg.upgrade() { self.flags.insert(Flags::UPGRADE); self.flags.remove(Flags::KEEPALIVE); - msg.headers_mut() + msg.headers .insert(CONNECTION, HeaderValue::from_static("upgrade")); } // keep-alive - else if ka { + else if self.flags.contains(Flags::KEEPALIVE_ENABLED) && msg.keep_alive() { self.flags.insert(Flags::KEEPALIVE); if self.version < Version::HTTP_11 { - msg.headers_mut() + msg.headers .insert(CONNECTION, HeaderValue::from_static("keep-alive")); } } else if self.version >= Version::HTTP_11 { self.flags.remove(Flags::KEEPALIVE); - msg.headers_mut() + msg.headers .insert(CONNECTION, HeaderValue::from_static("close")); } // render message { let reason = msg.reason().as_bytes(); - buffer - .reserve(256 + msg.headers().len() * AVERAGE_HEADER_SIZE + reason.len()); + buffer.reserve(256 + msg.headers.len() * AVERAGE_HEADER_SIZE + reason.len()); // status line - helpers::write_status_line(self.version, msg.status().as_u16(), buffer); + helpers::write_status_line(self.version, msg.status.as_u16(), buffer); buffer.extend_from_slice(reason); // content length - let mut len_is_set = true; - match self.te.length { - BodyLength::Chunked => { - buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") - } - BodyLength::Empty => { - len_is_set = false; - buffer.extend_from_slice(b"\r\n") - } - BodyLength::Sized(len) => helpers::write_content_length(len, buffer), - BodyLength::Sized64(len) => { - buffer.extend_from_slice(b"\r\ncontent-length: "); - write!(buffer.writer(), "{}", len)?; - buffer.extend_from_slice(b"\r\n"); - } - BodyLength::None | BodyLength::Stream => { - buffer.extend_from_slice(b"\r\n") - } + match msg.status { + StatusCode::NO_CONTENT + | StatusCode::CONTINUE + | StatusCode::SWITCHING_PROTOCOLS + | StatusCode::PROCESSING => buffer.extend_from_slice(b"\r\n"), + _ => match length { + BodyLength::Chunked => { + buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") + } + BodyLength::Empty => { + buffer.extend_from_slice(b"\r\ncontent-length: 0\r\n"); + } + BodyLength::Sized(len) => helpers::write_content_length(len, buffer), + BodyLength::Sized64(len) => { + buffer.extend_from_slice(b"\r\ncontent-length: "); + write!(buffer.writer(), "{}", len)?; + buffer.extend_from_slice(b"\r\n"); + } + BodyLength::None | BodyLength::Stream => { + buffer.extend_from_slice(b"\r\n") + } + }, } // write headers @@ -169,16 +171,9 @@ impl Codec { let mut has_date = false; let mut remaining = buffer.remaining_mut(); let mut buf = unsafe { &mut *(buffer.bytes_mut() as *mut [u8]) }; - for (key, value) in msg.headers() { + for (key, value) in &msg.headers { match *key { - TRANSFER_ENCODING => continue, - CONTENT_LENGTH => match self.te.length { - BodyLength::None => (), - BodyLength::Empty => { - len_is_set = true; - } - _ => continue, - }, + TRANSFER_ENCODING | CONTENT_LENGTH => continue, DATE => { has_date = true; } @@ -213,9 +208,6 @@ impl Codec { unsafe { buffer.advance_mut(pos); } - if !len_is_set { - buffer.extend_from_slice(b"content-length: 0\r\n") - } // optimized date header, set_date writes \r\n if !has_date { @@ -268,7 +260,7 @@ impl Decoder for Codec { } impl Encoder for Codec { - type Item = Message>; + type Item = Message<(Response<()>, BodyLength)>; type Error = io::Error; fn encode( @@ -277,8 +269,9 @@ impl Encoder for Codec { dst: &mut BytesMut, ) -> Result<(), Self::Error> { match item { - Message::Item(res) => { - self.encode_response(res, dst)?; + Message::Item((mut res, length)) => { + self.prepare_te(res.head_mut(), length); + self.encode_response(res.head_mut(), length, dst)?; } Message::Chunk(Some(bytes)) => { self.te.encode(bytes.as_ref(), dst)?; diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 26154ef1e..12008a777 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -10,7 +10,7 @@ use client::ClientResponse; use error::ParseError; use http::header::{HeaderName, HeaderValue}; use http::{header, HeaderMap, HttpTryFrom, Method, StatusCode, Uri, Version}; -use message::MessageFlags; +use message::Head; use request::Request; const MAX_BUFFER_SIZE: usize = 131_072; @@ -50,6 +50,8 @@ pub(crate) enum PayloadLength { pub(crate) trait MessageTypeDecoder: Sized { fn keep_alive(&mut self); + fn force_close(&mut self); + fn headers_mut(&mut self) -> &mut HeaderMap; fn decode(src: &mut BytesMut) -> Result, ParseError>; @@ -137,6 +139,8 @@ pub(crate) trait MessageTypeDecoder: Sized { if ka { self.keep_alive(); + } else { + self.force_close(); } // https://tools.ietf.org/html/rfc7230#section-3.3.3 @@ -160,7 +164,11 @@ pub(crate) trait MessageTypeDecoder: Sized { impl MessageTypeDecoder for Request { fn keep_alive(&mut self) { - self.inner_mut().flags.set(MessageFlags::KEEPALIVE); + self.inner_mut().head.set_keep_alive() + } + + fn force_close(&mut self) { + self.inner_mut().head.force_close() } fn headers_mut(&mut self) -> &mut HeaderMap { @@ -234,7 +242,11 @@ impl MessageTypeDecoder for Request { impl MessageTypeDecoder for ClientResponse { fn keep_alive(&mut self) { - self.head.flags.insert(MessageFlags::KEEPALIVE); + self.head.set_keep_alive(); + } + + fn force_close(&mut self) { + self.head.force_close(); } fn headers_mut(&mut self) -> &mut HeaderMap { diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index ff9d54e66..bf0abb046 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -30,7 +30,6 @@ bitflags! { const KEEPALIVE_ENABLED = 0b0000_0010; const KEEPALIVE = 0b0000_0100; const POLLED = 0b0000_1000; - const FLUSHED = 0b0001_0000; const SHUTDOWN = 0b0010_0000; const DISCONNECTED = 0b0100_0000; } @@ -105,9 +104,9 @@ where ) -> Self { let keepalive = config.keep_alive_enabled(); let flags = if keepalive { - Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED | Flags::FLUSHED + Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED } else { - Flags::FLUSHED + Flags::empty() }; let framed = Framed::new(stream, Codec::new(config.clone())); @@ -167,7 +166,7 @@ where /// Flush stream fn poll_flush(&mut self) -> Poll<(), DispatchError> { - if !self.flags.contains(Flags::FLUSHED) { + if !self.framed.is_write_buf_empty() { match self.framed.poll_complete() { Ok(Async::NotReady) => Ok(Async::NotReady), Err(err) => { @@ -179,7 +178,6 @@ where if self.payload.is_some() && self.state.is_empty() { return Err(DispatchError::PayloadIsNotConsumed); } - self.flags.insert(Flags::FLUSHED); Ok(Async::Ready(())) } } @@ -194,7 +192,7 @@ where body: B1, ) -> Result, DispatchError> { self.framed - .force_send(Message::Item(message)) + .force_send(Message::Item((message, body.length()))) .map_err(|err| { if let Some(mut payload) = self.payload.take() { payload.set_error(PayloadError::Incomplete(None)); @@ -204,7 +202,6 @@ where self.flags .set(Flags::KEEPALIVE, self.framed.get_codec().keepalive()); - self.flags.remove(Flags::FLUSHED); match body.length() { BodyLength::None | BodyLength::Empty => Ok(State::None), _ => Ok(State::SendPayload(body)), @@ -228,10 +225,7 @@ where State::ServiceCall(mut fut) => { match fut.poll().map_err(DispatchError::Service)? { Async::Ready(mut res) => { - let (mut res, body) = res.replace_body(()); - self.framed - .get_codec_mut() - .prepare_te(res.head_mut(), &mut body.length()); + let (res, body) = res.replace_body(()); Some(self.send_response(res, body)?) } Async::NotReady => { @@ -248,13 +242,11 @@ where .map_err(|_| DispatchError::Unknown)? { Async::Ready(Some(item)) => { - self.flags.remove(Flags::FLUSHED); self.framed .force_send(Message::Chunk(Some(item)))?; continue; } Async::Ready(None) => { - self.flags.remove(Flags::FLUSHED); self.framed.force_send(Message::Chunk(None))?; } Async::NotReady => { @@ -296,10 +288,7 @@ where let mut task = self.service.call(req); match task.poll().map_err(DispatchError::Service)? { Async::Ready(res) => { - let (mut res, body) = res.replace_body(()); - self.framed - .get_codec_mut() - .prepare_te(res.head_mut(), &mut body.length()); + let (res, body) = res.replace_body(()); self.send_response(res, body) } Async::NotReady => Ok(State::ServiceCall(task)), @@ -408,7 +397,7 @@ where /// keep-alive timer fn poll_keepalive(&mut self) -> Result<(), DispatchError> { - if self.ka_timer.is_some() { + if self.ka_timer.is_none() { return Ok(()); } match self.ka_timer.as_mut().unwrap().poll().map_err(|e| { @@ -421,7 +410,7 @@ where return Err(DispatchError::DisconnectTimeout); } else if self.ka_timer.as_mut().unwrap().deadline() >= self.ka_expire { // check for any outstanding response processing - if self.state.is_empty() && self.flags.contains(Flags::FLUSHED) { + if self.state.is_empty() && self.framed.is_write_buf_empty() { if self.flags.contains(Flags::STARTED) { trace!("Keep-alive timeout, close connection"); self.flags.insert(Flags::SHUTDOWN); @@ -490,12 +479,14 @@ where inner.poll_response()?; inner.poll_flush()?; + if inner.flags.contains(Flags::DISCONNECTED) { + return Ok(Async::Ready(H1ServiceResult::Disconnected)); + } + // keep-alive and stream errors - if inner.state.is_empty() && inner.flags.contains(Flags::FLUSHED) { + if inner.state.is_empty() && inner.framed.is_write_buf_empty() { if let Some(err) = inner.error.take() { return Err(err); - } else if inner.flags.contains(Flags::DISCONNECTED) { - return Ok(Async::Ready(H1ServiceResult::Disconnected)); } // unhandled request (upgrade or connect) else if inner.unhandled.is_some() { diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index fd52d7316..1664af162 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -48,22 +48,13 @@ impl ResponseEncoder { resp: &mut ResponseHead, head: bool, version: Version, - length: &mut BodyLength, + length: BodyLength, ) { self.head = head; let transfer = match length { - BodyLength::Empty => { - match resp.status { - StatusCode::NO_CONTENT - | StatusCode::CONTINUE - | StatusCode::SWITCHING_PROTOCOLS - | StatusCode::PROCESSING => *length = BodyLength::None, - _ => (), - } - TransferEncoding::empty() - } - BodyLength::Sized(len) => TransferEncoding::length(*len as u64), - BodyLength::Sized64(len) => TransferEncoding::length(*len), + BodyLength::Empty => TransferEncoding::empty(), + BodyLength::Sized(len) => TransferEncoding::length(len as u64), + BodyLength::Sized64(len) => TransferEncoding::length(len), BodyLength::Chunked => TransferEncoding::chunked(), BodyLength::Stream => TransferEncoding::eof(), BodyLength::None => TransferEncoding::length(0), diff --git a/src/lib.rs b/src/lib.rs index 615f04010..57ff5df78 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,6 +109,8 @@ extern crate serde_derive; #[cfg(feature = "ssl")] extern crate openssl; +extern crate backtrace; + pub mod body; pub mod client; mod config; @@ -173,5 +175,4 @@ pub mod http { pub use header::*; } pub use header::ContentEncoding; - pub use response::ConnectionType; } diff --git a/src/message.rs b/src/message.rs index 3dcb203de..e7ce22fcd 100644 --- a/src/message.rs +++ b/src/message.rs @@ -12,12 +12,41 @@ use uri::Url; pub trait Head: Default + 'static { fn clear(&mut self); + fn flags(&self) -> MessageFlags; + + fn flags_mut(&mut self) -> &mut MessageFlags; + fn pool() -> &'static MessagePool; + + /// Set upgrade + fn set_upgrade(&mut self) { + *self.flags_mut() = MessageFlags::UPGRADE; + } + + /// Check if request is upgrade request + fn upgrade(&self) -> bool { + self.flags().contains(MessageFlags::UPGRADE) + } + + /// Set keep-alive + fn set_keep_alive(&mut self) { + *self.flags_mut() = MessageFlags::KEEP_ALIVE; + } + + /// Check if request is keep-alive + fn keep_alive(&self) -> bool; + + /// Set force-close connection + fn force_close(&mut self) { + *self.flags_mut() = MessageFlags::FORCE_CLOSE; + } } bitflags! { - pub(crate) struct MessageFlags: u8 { - const KEEPALIVE = 0b0000_0001; + pub struct MessageFlags: u8 { + const KEEP_ALIVE = 0b0000_0001; + const FORCE_CLOSE = 0b0000_0010; + const UPGRADE = 0b0000_0100; } } @@ -47,6 +76,25 @@ impl Head for RequestHead { self.flags = MessageFlags::empty(); } + fn flags(&self) -> MessageFlags { + self.flags + } + + fn flags_mut(&mut self) -> &mut MessageFlags { + &mut self.flags + } + + /// Check if request is keep-alive + fn keep_alive(&self) -> bool { + if self.flags().contains(MessageFlags::FORCE_CLOSE) { + false + } else if self.flags().contains(MessageFlags::KEEP_ALIVE) { + true + } else { + self.version <= Version::HTTP_11 + } + } + fn pool() -> &'static MessagePool { REQUEST_POOL.with(|p| *p) } @@ -79,11 +127,44 @@ impl Head for ResponseHead { self.flags = MessageFlags::empty(); } + fn flags(&self) -> MessageFlags { + self.flags + } + + fn flags_mut(&mut self) -> &mut MessageFlags { + &mut self.flags + } + + /// Check if response is keep-alive + fn keep_alive(&self) -> bool { + if self.flags().contains(MessageFlags::FORCE_CLOSE) { + false + } else if self.flags().contains(MessageFlags::KEEP_ALIVE) { + true + } else { + self.version <= Version::HTTP_11 + } + } + fn pool() -> &'static MessagePool { RESPONSE_POOL.with(|p| *p) } } +impl ResponseHead { + /// Get custom reason for the response + #[inline] + pub fn reason(&self) -> &str { + if let Some(reason) = self.reason { + reason + } else { + self.status + .canonical_reason() + .unwrap_or("") + } + } +} + pub struct Message { pub head: T, pub url: Url, diff --git a/src/request.rs b/src/request.rs index 1e191047a..1ee47edb4 100644 --- a/src/request.rs +++ b/src/request.rs @@ -8,7 +8,7 @@ use extensions::Extensions; use httpmessage::HttpMessage; use payload::Payload; -use message::{Message, MessageFlags, MessagePool, RequestHead}; +use message::{Head, Message, MessagePool, RequestHead}; /// Request pub struct Request { @@ -116,7 +116,7 @@ impl Request { /// Checks if a connection should be kept alive. #[inline] pub fn keep_alive(&self) -> bool { - self.inner().flags.get().contains(MessageFlags::KEEPALIVE) + self.inner().head.keep_alive() } /// Request extensions diff --git a/src/response.rs b/src/response.rs index a6f7c81c2..542d4963e 100644 --- a/src/response.rs +++ b/src/response.rs @@ -20,17 +20,6 @@ use message::{Head, MessageFlags, ResponseHead}; /// max write buffer size 64k pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; -/// Represents various types of connection -#[derive(Copy, Clone, PartialEq, Debug)] -pub enum ConnectionType { - /// Close connection after response - Close, - /// Keep connection alive after response - KeepAlive, - /// Connection is upgraded to different type - Upgrade, -} - /// An HTTP Response pub struct Response(Box, B); @@ -124,27 +113,6 @@ impl Response { &mut self.get_mut().head.status } - /// Get custom reason for the response - #[inline] - pub fn reason(&self) -> &str { - if let Some(reason) = self.get_ref().head.reason { - reason - } else { - self.get_ref() - .head - .status - .canonical_reason() - .unwrap_or("") - } - } - - /// Set the custom reason for the response - #[inline] - pub fn set_reason(&mut self, reason: &'static str) -> &mut Self { - self.get_mut().head.reason = Some(reason); - self - } - /// Get the headers from the response #[inline] pub fn headers(&self) -> &HeaderMap { @@ -207,28 +175,15 @@ impl Response { count } - /// Set connection type - pub fn set_connection_type(&mut self, conn: ConnectionType) -> &mut Self { - self.get_mut().connection_type = Some(conn); - self - } - /// Connection upgrade status #[inline] pub fn upgrade(&self) -> bool { - self.get_ref().connection_type == Some(ConnectionType::Upgrade) + self.get_ref().head.upgrade() } /// Keep-alive status for this connection - pub fn keep_alive(&self) -> Option { - if let Some(ct) = self.get_ref().connection_type { - match ct { - ConnectionType::KeepAlive => Some(true), - ConnectionType::Close | ConnectionType::Upgrade => Some(false), - } - } else { - None - } + pub fn keep_alive(&self) -> bool { + self.get_ref().head.keep_alive() } /// Get body os this response @@ -275,19 +230,20 @@ impl Response { } } -impl fmt::Debug for Response { +impl fmt::Debug for Response { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let res = writeln!( f, "\nResponse {:?} {}{}", self.get_ref().head.version, self.get_ref().head.status, - self.get_ref().head.reason.unwrap_or("") + self.get_ref().head.reason.unwrap_or(""), ); let _ = writeln!(f, " headers:"); for (key, val) in self.get_ref().head.headers.iter() { let _ = writeln!(f, " {:?}: {:?}", key, val); } + let _ = writeln!(f, " body: {:?}", self.body().length()); res } } @@ -400,27 +356,31 @@ impl ResponseBuilder { self } - /// Set connection type + /// Set connection type to KeepAlive #[inline] - #[doc(hidden)] - pub fn connection_type(&mut self, conn: ConnectionType) -> &mut Self { + pub fn keep_alive(&mut self) -> &mut Self { if let Some(parts) = parts(&mut self.response, &self.err) { - parts.connection_type = Some(conn); + parts.head.set_keep_alive(); } self } /// Set connection type to Upgrade #[inline] - #[doc(hidden)] pub fn upgrade(&mut self) -> &mut Self { - self.connection_type(ConnectionType::Upgrade) + if let Some(parts) = parts(&mut self.response, &self.err) { + parts.head.set_upgrade(); + } + self } /// Force close connection, even if it is marked as keep-alive #[inline] pub fn force_close(&mut self) -> &mut Self { - self.connection_type(ConnectionType::Close) + if let Some(parts) = parts(&mut self.response, &self.err) { + parts.head.force_close(); + } + self } /// Set response content type @@ -719,8 +679,6 @@ impl From for Response { struct InnerResponse { head: ResponseHead, - connection_type: Option, - write_capacity: usize, response_size: u64, error: Option, pool: &'static ResponsePool, @@ -728,7 +686,6 @@ struct InnerResponse { pub(crate) struct ResponseParts { head: ResponseHead, - connection_type: Option, error: Option, } @@ -744,9 +701,7 @@ impl InnerResponse { flags: MessageFlags::empty(), }, pool, - connection_type: None, response_size: 0, - write_capacity: MAX_WRITE_BUFFER_SIZE, error: None, } } @@ -755,7 +710,6 @@ impl InnerResponse { fn into_parts(self) -> ResponseParts { ResponseParts { head: self.head, - connection_type: self.connection_type, error: self.error, } } @@ -763,9 +717,7 @@ impl InnerResponse { fn from_parts(parts: ResponseParts) -> InnerResponse { InnerResponse { head: parts.head, - connection_type: parts.connection_type, response_size: 0, - write_capacity: MAX_WRITE_BUFFER_SIZE, error: parts.error, pool: ResponsePool::pool(), } @@ -838,10 +790,8 @@ impl ResponsePool { let mut p = inner.pool.0.borrow_mut(); if p.len() < 128 { inner.head.clear(); - inner.connection_type = None; inner.response_size = 0; inner.error = None; - inner.write_capacity = MAX_WRITE_BUFFER_SIZE; p.push_front(inner); } } @@ -937,7 +887,7 @@ mod tests { #[test] fn test_force_close() { let resp = Response::build(StatusCode::OK).force_close().finish(); - assert!(!resp.keep_alive().unwrap()) + assert!(!resp.keep_alive()) } #[test] diff --git a/src/service.rs b/src/service.rs index 51a16b48b..f934305ce 100644 --- a/src/service.rs +++ b/src/service.rs @@ -3,10 +3,10 @@ use std::marker::PhantomData; use actix_net::codec::Framed; use actix_net::service::{NewService, Service}; use futures::future::{ok, Either, FutureResult}; -use futures::{Async, AsyncSink, Future, Poll, Sink}; +use futures::{Async, Future, Poll, Sink}; use tokio_io::{AsyncRead, AsyncWrite}; -use body::MessageBody; +use body::{BodyLength, MessageBody}; use error::{Error, ResponseError}; use h1::{Codec, Message}; use response::Response; @@ -15,7 +15,7 @@ pub struct SendError(PhantomData<(T, R, E)>); impl Default for SendError where - T: AsyncWrite, + T: AsyncRead + AsyncWrite, E: ResponseError, { fn default() -> Self { @@ -25,7 +25,7 @@ where impl NewService for SendError where - T: AsyncWrite, + T: AsyncRead + AsyncWrite, E: ResponseError, { type Request = Result)>; @@ -42,7 +42,7 @@ where impl Service for SendError where - T: AsyncWrite, + T: AsyncRead + AsyncWrite, E: ResponseError, { type Request = Result)>; @@ -62,7 +62,7 @@ where let (res, _body) = res.replace_body(()); Either::B(SendErrorFut { framed: Some(framed), - res: Some(res.into()), + res: Some((res, BodyLength::Empty).into()), err: Some(e), _t: PhantomData, }) @@ -72,7 +72,7 @@ where } pub struct SendErrorFut { - res: Option>>, + res: Option, BodyLength)>>, framed: Option>, err: Option, _t: PhantomData, @@ -81,22 +81,15 @@ pub struct SendErrorFut { impl Future for SendErrorFut where E: ResponseError, - T: AsyncWrite, + T: AsyncRead + AsyncWrite, { type Item = R; type Error = (E, Framed); fn poll(&mut self) -> Poll { if let Some(res) = self.res.take() { - match self.framed.as_mut().unwrap().start_send(res) { - Ok(AsyncSink::Ready) => (), - Ok(AsyncSink::NotReady(res)) => { - self.res = Some(res); - return Ok(Async::NotReady); - } - Err(_) => { - return Err((self.err.take().unwrap(), self.framed.take().unwrap())) - } + if let Err(_) = self.framed.as_mut().unwrap().force_send(res) { + return Err((self.err.take().unwrap(), self.framed.take().unwrap())); } } match self.framed.as_mut().unwrap().poll_complete() { @@ -123,20 +116,15 @@ where B: MessageBody, { pub fn send( - mut framed: Framed, + framed: Framed, res: Response, ) -> impl Future, Error = Error> { // extract body from response - let (mut res, body) = res.replace_body(()); - - // init codec - framed - .get_codec_mut() - .prepare_te(&mut res.head_mut(), &mut body.length()); + let (res, body) = res.replace_body(()); // write response SendResponseFut { - res: Some(Message::Item(res)), + res: Some(Message::Item((res, body.length()))), body: Some(body), framed: Some(framed), } @@ -174,13 +162,10 @@ where Ok(Async::Ready(())) } - fn call(&mut self, (res, mut framed): Self::Request) -> Self::Future { - let (mut res, body) = res.replace_body(()); - framed - .get_codec_mut() - .prepare_te(res.head_mut(), &mut body.length()); + fn call(&mut self, (res, framed): Self::Request) -> Self::Future { + let (res, body) = res.replace_body(()); SendResponseFut { - res: Some(Message::Item(res)), + res: Some(Message::Item((res, body.length()))), body: Some(body), framed: Some(framed), } @@ -188,7 +173,7 @@ where } pub struct SendResponseFut { - res: Option>>, + res: Option, BodyLength)>>, body: Option, framed: Option>, } diff --git a/src/ws/mod.rs b/src/ws/mod.rs index 1fbbd03dd..5c86d8c49 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -8,7 +8,7 @@ use std::io; use error::ResponseError; use http::{header, Method, StatusCode}; use request::Request; -use response::{ConnectionType, Response, ResponseBuilder}; +use response::{Response, ResponseBuilder}; mod client; mod codec; @@ -183,7 +183,7 @@ pub fn handshake_response(req: &Request) -> ResponseBuilder { }; Response::build(StatusCode::SWITCHING_PROTOCOLS) - .connection_type(ConnectionType::Upgrade) + .upgrade() .header(header::UPGRADE, "websocket") .header(header::TRANSFER_ENCODING, "chunked") .header(header::SEC_WEBSOCKET_ACCEPT, key.as_str()) diff --git a/tests/test_server.rs b/tests/test_server.rs index c499cf635..9d16e92e3 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -12,10 +12,13 @@ use actix_net::server::Server; use actix_net::service::NewServiceExt; use actix_web::{client, test, HttpMessage}; use bytes::Bytes; -use futures::future::{self, ok}; +use futures::future::{self, lazy, ok}; use futures::stream::once; -use actix_http::{body, h1, http, Body, Error, KeepAlive, Request, Response}; +use actix_http::{ + body, client as client2, h1, http, Body, Error, HttpMessage as HttpMessage2, + KeepAlive, Request, Response, +}; #[test] fn test_h1_v2() { @@ -181,14 +184,19 @@ fn test_headers() { .unwrap() .run() }); - thread::sleep(time::Duration::from_millis(400)); + thread::sleep(time::Duration::from_millis(200)); let mut sys = System::new("test"); - let req = client::ClientRequest::get(format!("http://{}/", addr)) + let mut connector = sys + .block_on(lazy(|| { + Ok::<_, ()>(client2::Connector::default().service()) + })).unwrap(); + + let req = client2::ClientRequest::get(format!("http://{}/", addr)) .finish() .unwrap(); - let response = sys.block_on(req.send()).unwrap(); + let response = sys.block_on(req.send(&mut connector)).unwrap(); assert!(response.status().is_success()); // read response @@ -249,9 +257,7 @@ fn test_head_empty() { thread::spawn(move || { Server::new() .bind("test", addr, move || { - h1::H1Service::new(|_| { - ok::<_, ()>(Response::Ok().content_length(STR.len() as u64).finish()) - }).map(|_| ()) + h1::H1Service::new(|_| ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) }).unwrap() .run() }); From 7d3adaa6a8f47ef4101ba1a16e9e6360453237d4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 18 Nov 2018 18:17:38 -0800 Subject: [PATCH 076/427] replace message flags with ConnectionType --- src/client/request.rs | 6 +-- src/h1/decoder.rs | 14 +++-- src/lib.rs | 1 + src/message.rs | 115 +++++++++++++++++------------------------- src/response.rs | 10 ++-- 5 files changed, 64 insertions(+), 82 deletions(-) diff --git a/src/client/request.rs b/src/client/request.rs index cc65b9db1..735ce4937 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -16,7 +16,7 @@ use http::{ uri, Error as HttpError, HeaderMap, HeaderName, HeaderValue, HttpTryFrom, Method, Uri, Version, }; -use message::{Head, RequestHead}; +use message::{ConnectionType, Head, RequestHead}; use super::response::ClientResponse; use super::{pipeline, Connect, Connection, ConnectorError, SendRequestError}; @@ -367,7 +367,7 @@ impl ClientRequestBuilder { { { if let Some(parts) = parts(&mut self.head, &self.err) { - parts.set_upgrade(); + parts.set_connection_type(ConnectionType::Upgrade); } } self.set_header(header::UPGRADE, value) @@ -377,7 +377,7 @@ impl ClientRequestBuilder { #[inline] pub fn close(&mut self) -> &mut Self { if let Some(parts) = parts(&mut self.head, &self.err) { - parts.force_close(); + parts.set_connection_type(ConnectionType::Close); } self } diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 12008a777..12dfe1657 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -10,7 +10,7 @@ use client::ClientResponse; use error::ParseError; use http::header::{HeaderName, HeaderValue}; use http::{header, HeaderMap, HttpTryFrom, Method, StatusCode, Uri, Version}; -use message::Head; +use message::{ConnectionType, Head}; use request::Request; const MAX_BUFFER_SIZE: usize = 131_072; @@ -164,11 +164,15 @@ pub(crate) trait MessageTypeDecoder: Sized { impl MessageTypeDecoder for Request { fn keep_alive(&mut self) { - self.inner_mut().head.set_keep_alive() + self.inner_mut() + .head + .set_connection_type(ConnectionType::KeepAlive) } fn force_close(&mut self) { - self.inner_mut().head.force_close() + self.inner_mut() + .head + .set_connection_type(ConnectionType::Close) } fn headers_mut(&mut self) -> &mut HeaderMap { @@ -242,11 +246,11 @@ impl MessageTypeDecoder for Request { impl MessageTypeDecoder for ClientResponse { fn keep_alive(&mut self) { - self.head.set_keep_alive(); + self.head.set_connection_type(ConnectionType::KeepAlive); } fn force_close(&mut self) { - self.head.force_close(); + self.head.set_connection_type(ConnectionType::Close); } fn headers_mut(&mut self) -> &mut HeaderMap { diff --git a/src/lib.rs b/src/lib.rs index 57ff5df78..3024b753f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -175,4 +175,5 @@ pub mod http { pub use header::*; } pub use header::ContentEncoding; + pub use message::ConnectionType; } diff --git a/src/message.rs b/src/message.rs index e7ce22fcd..03e18f082 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,4 +1,4 @@ -use std::cell::{Cell, RefCell}; +use std::cell::RefCell; use std::collections::VecDeque; use std::rc::Rc; @@ -8,46 +8,36 @@ use extensions::Extensions; use payload::Payload; use uri::Url; +/// Represents various types of connection +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum ConnectionType { + /// Close connection after response + Close, + /// Keep connection alive after response + KeepAlive, + /// Connection is upgraded to different type + Upgrade, +} + #[doc(hidden)] pub trait Head: Default + 'static { fn clear(&mut self); - fn flags(&self) -> MessageFlags; + /// Connection type + fn connection_type(&self) -> ConnectionType; - fn flags_mut(&mut self) -> &mut MessageFlags; + /// Set connection type of the message + fn set_connection_type(&mut self, ctype: ConnectionType); + + fn upgrade(&self) -> bool { + self.connection_type() == ConnectionType::Upgrade + } + + fn keep_alive(&self) -> bool { + self.connection_type() == ConnectionType::KeepAlive + } fn pool() -> &'static MessagePool; - - /// Set upgrade - fn set_upgrade(&mut self) { - *self.flags_mut() = MessageFlags::UPGRADE; - } - - /// Check if request is upgrade request - fn upgrade(&self) -> bool { - self.flags().contains(MessageFlags::UPGRADE) - } - - /// Set keep-alive - fn set_keep_alive(&mut self) { - *self.flags_mut() = MessageFlags::KEEP_ALIVE; - } - - /// Check if request is keep-alive - fn keep_alive(&self) -> bool; - - /// Set force-close connection - fn force_close(&mut self) { - *self.flags_mut() = MessageFlags::FORCE_CLOSE; - } -} - -bitflags! { - pub struct MessageFlags: u8 { - const KEEP_ALIVE = 0b0000_0001; - const FORCE_CLOSE = 0b0000_0010; - const UPGRADE = 0b0000_0100; - } } pub struct RequestHead { @@ -55,7 +45,7 @@ pub struct RequestHead { pub method: Method, pub version: Version, pub headers: HeaderMap, - pub(crate) flags: MessageFlags, + ctype: Option, } impl Default for RequestHead { @@ -65,33 +55,28 @@ impl Default for RequestHead { method: Method::default(), version: Version::HTTP_11, headers: HeaderMap::with_capacity(16), - flags: MessageFlags::empty(), + ctype: None, } } } impl Head for RequestHead { fn clear(&mut self) { + self.ctype = None; self.headers.clear(); - self.flags = MessageFlags::empty(); } - fn flags(&self) -> MessageFlags { - self.flags + fn set_connection_type(&mut self, ctype: ConnectionType) { + self.ctype = Some(ctype) } - fn flags_mut(&mut self) -> &mut MessageFlags { - &mut self.flags - } - - /// Check if request is keep-alive - fn keep_alive(&self) -> bool { - if self.flags().contains(MessageFlags::FORCE_CLOSE) { - false - } else if self.flags().contains(MessageFlags::KEEP_ALIVE) { - true + fn connection_type(&self) -> ConnectionType { + if let Some(ct) = self.ctype { + ct + } else if self.version <= Version::HTTP_11 { + ConnectionType::Close } else { - self.version <= Version::HTTP_11 + ConnectionType::KeepAlive } } @@ -105,7 +90,7 @@ pub struct ResponseHead { pub status: StatusCode, pub headers: HeaderMap, pub reason: Option<&'static str>, - pub(crate) flags: MessageFlags, + pub(crate) ctype: Option, } impl Default for ResponseHead { @@ -115,34 +100,29 @@ impl Default for ResponseHead { status: StatusCode::OK, headers: HeaderMap::with_capacity(16), reason: None, - flags: MessageFlags::empty(), + ctype: None, } } } impl Head for ResponseHead { fn clear(&mut self) { + self.ctype = None; self.reason = None; self.headers.clear(); - self.flags = MessageFlags::empty(); } - fn flags(&self) -> MessageFlags { - self.flags + fn set_connection_type(&mut self, ctype: ConnectionType) { + self.ctype = Some(ctype) } - fn flags_mut(&mut self) -> &mut MessageFlags { - &mut self.flags - } - - /// Check if response is keep-alive - fn keep_alive(&self) -> bool { - if self.flags().contains(MessageFlags::FORCE_CLOSE) { - false - } else if self.flags().contains(MessageFlags::KEEP_ALIVE) { - true + fn connection_type(&self) -> ConnectionType { + if let Some(ct) = self.ctype { + ct + } else if self.version <= Version::HTTP_11 { + ConnectionType::Close } else { - self.version <= Version::HTTP_11 + ConnectionType::KeepAlive } } @@ -172,7 +152,6 @@ pub struct Message { pub extensions: RefCell, pub payload: RefCell>, pub(crate) pool: &'static MessagePool, - pub(crate) flags: Cell, } impl Message { @@ -181,7 +160,6 @@ impl Message { pub fn reset(&mut self) { self.head.clear(); self.extensions.borrow_mut().clear(); - self.flags.set(MessageFlags::empty()); *self.payload.borrow_mut() = None; } } @@ -193,7 +171,6 @@ impl Default for Message { url: Url::default(), head: T::default(), status: StatusCode::OK, - flags: Cell::new(MessageFlags::empty()), payload: RefCell::new(None), extensions: RefCell::new(Extensions::new()), } diff --git a/src/response.rs b/src/response.rs index 542d4963e..f2ed72925 100644 --- a/src/response.rs +++ b/src/response.rs @@ -15,7 +15,7 @@ use serde_json; use body::{Body, BodyStream, MessageBody}; use error::Error; use header::{Header, IntoHeaderValue}; -use message::{Head, MessageFlags, ResponseHead}; +use message::{ConnectionType, Head, ResponseHead}; /// max write buffer size 64k pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; @@ -360,7 +360,7 @@ impl ResponseBuilder { #[inline] pub fn keep_alive(&mut self) -> &mut Self { if let Some(parts) = parts(&mut self.response, &self.err) { - parts.head.set_keep_alive(); + parts.head.set_connection_type(ConnectionType::KeepAlive); } self } @@ -369,7 +369,7 @@ impl ResponseBuilder { #[inline] pub fn upgrade(&mut self) -> &mut Self { if let Some(parts) = parts(&mut self.response, &self.err) { - parts.head.set_upgrade(); + parts.head.set_connection_type(ConnectionType::Upgrade); } self } @@ -378,7 +378,7 @@ impl ResponseBuilder { #[inline] pub fn force_close(&mut self) -> &mut Self { if let Some(parts) = parts(&mut self.response, &self.err) { - parts.head.force_close(); + parts.head.set_connection_type(ConnectionType::Close); } self } @@ -698,7 +698,7 @@ impl InnerResponse { version: Version::default(), headers: HeaderMap::with_capacity(16), reason: None, - flags: MessageFlags::empty(), + ctype: None, }, pool, response_size: 0, From 22d4523c93db4f750ea2866a1e34ad3142773554 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 18 Nov 2018 18:31:44 -0800 Subject: [PATCH 077/427] update actix-net --- Cargo.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2eef1c50f..50bacddbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,9 +45,8 @@ rust-tls = ["rustls", "actix-net/rust-tls"] [dependencies] actix = "0.7.5" -#actix-net = "0.2.2" -actix-net = { git="https://github.com/actix/actix-net.git" } -#actix-net = { path="../actix-net" } +actix-net = "0.2.3" +#actix-net = { git="https://github.com/actix/actix-net.git" } base64 = "0.9" bitflags = "1.0" From 7d6643032408720bbcb4355cf3bc453e8724c272 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 18 Nov 2018 20:08:43 -0800 Subject: [PATCH 078/427] move url module to different crate --- Cargo.toml | 5 +- src/h1/decoder.rs | 1 - src/lib.rs | 3 - src/message.rs | 5 -- src/request.rs | 6 +- src/test.rs | 3 +- src/uri.rs | 169 ---------------------------------------------- 7 files changed, 3 insertions(+), 189 deletions(-) delete mode 100644 src/uri.rs diff --git a/Cargo.toml b/Cargo.toml index 50bacddbd..b2c7c0848 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ base64 = "0.9" bitflags = "1.0" http = "0.1.8" httparse = "1.3" -failure = "0.1.2" +failure = "0.1.3" indexmap = "1.0" log = "0.4" mime = "0.3" @@ -62,7 +62,6 @@ serde_json = "1.0" sha1 = "0.6" time = "0.1" encoding = "0.2" -lazy_static = "1.0" serde_urlencoded = "0.5.3" cookie = { version="0.11", features=["percent-encode"] } @@ -93,8 +92,6 @@ openssl = { version="0.10", optional = true } # rustls rustls = { version = "^0.14", optional = true } -backtrace="*" - [dev-dependencies] actix-web = "0.7" env_logger = "0.5" diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 12dfe1657..de0df367f 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -234,7 +234,6 @@ impl MessageTypeDecoder for Request { { let inner = msg.inner_mut(); - inner.url.update(&uri); inner.head.uri = uri; inner.head.method = method; inner.head.version = ver; diff --git a/src/lib.rs b/src/lib.rs index 3024b753f..bee172f0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,8 +76,6 @@ extern crate bitflags; #[macro_use] extern crate failure; #[macro_use] -extern crate lazy_static; -#[macro_use] extern crate futures; extern crate cookie; extern crate encoding; @@ -124,7 +122,6 @@ mod payload; mod request; mod response; mod service; -pub mod uri; pub mod error; pub mod h1; diff --git a/src/message.rs b/src/message.rs index 03e18f082..e1059fa48 100644 --- a/src/message.rs +++ b/src/message.rs @@ -6,7 +6,6 @@ use http::{HeaderMap, Method, StatusCode, Uri, Version}; use extensions::Extensions; use payload::Payload; -use uri::Url; /// Represents various types of connection #[derive(Copy, Clone, PartialEq, Debug)] @@ -147,8 +146,6 @@ impl ResponseHead { pub struct Message { pub head: T, - pub url: Url, - pub status: StatusCode, pub extensions: RefCell, pub payload: RefCell>, pub(crate) pool: &'static MessagePool, @@ -168,9 +165,7 @@ impl Default for Message { fn default() -> Self { Message { pool: T::pool(), - url: Url::default(), head: T::default(), - status: StatusCode::OK, payload: RefCell::new(None), extensions: RefCell::new(Extensions::new()), } diff --git a/src/request.rs b/src/request.rs index 1ee47edb4..d529c09f9 100644 --- a/src/request.rs +++ b/src/request.rs @@ -94,11 +94,7 @@ impl Request { /// The target path of this Request. #[inline] pub fn path(&self) -> &str { - if let Some(path) = self.inner().url.path() { - path - } else { - self.inner().head.uri.path() - } + self.inner().head.uri.path() } #[inline] diff --git a/src/test.rs b/src/test.rs index c6f258a77..5442a4149 100644 --- a/src/test.rs +++ b/src/test.rs @@ -15,7 +15,6 @@ use tokio::runtime::current_thread::Runtime; use header::{Header, IntoHeaderValue}; use payload::Payload; use request::Request; -use uri::Url as InnerUrl; // use ws; /// The `TestServer` type. @@ -390,8 +389,8 @@ impl TestRequest { let mut req = Request::new(); { let inner = req.inner_mut(); + inner.head.uri = uri; inner.head.method = method; - inner.url = InnerUrl::new(&uri); inner.head.version = version; inner.head.headers = headers; *inner.payload.borrow_mut() = payload; diff --git a/src/uri.rs b/src/uri.rs deleted file mode 100644 index 89f6d3b1e..000000000 --- a/src/uri.rs +++ /dev/null @@ -1,169 +0,0 @@ -use http::Uri; -use std::rc::Rc; - -#[allow(dead_code)] -const GEN_DELIMS: &[u8] = b":/?#[]@"; -#[allow(dead_code)] -const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,"; -#[allow(dead_code)] -const SUB_DELIMS: &[u8] = b"!$'()*,+?=;"; -#[allow(dead_code)] -const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;"; -#[allow(dead_code)] -const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz - ABCDEFGHIJKLMNOPQRSTUVWXYZ - 1234567890 - -._~"; -const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz - ABCDEFGHIJKLMNOPQRSTUVWXYZ - 1234567890 - -._~ - !$'()*,"; -const QS: &[u8] = b"+&=;b"; - -#[inline] -fn bit_at(array: &[u8], ch: u8) -> bool { - array[(ch >> 3) as usize] & (1 << (ch & 7)) != 0 -} - -#[inline] -fn set_bit(array: &mut [u8], ch: u8) { - array[(ch >> 3) as usize] |= 1 << (ch & 7) -} - -lazy_static! { - pub static ref DEFAULT_QUOTER: Quoter = { Quoter::new(b"@:", b"/+") }; -} - -#[derive(Default, Clone, Debug)] -pub struct Url { - path: Option>, -} - -impl Url { - pub fn new(uri: &Uri) -> Url { - let path = DEFAULT_QUOTER.requote(uri.path().as_bytes()); - - Url { path } - } - - pub(crate) fn update(&mut self, uri: &Uri) { - self.path = DEFAULT_QUOTER.requote(uri.path().as_bytes()); - } - - pub fn path(&self) -> Option<&str> { - self.path.as_ref().map(|s| s.as_str()) - } -} - -pub struct Quoter { - safe_table: [u8; 16], - protected_table: [u8; 16], -} - -impl Quoter { - pub fn new(safe: &[u8], protected: &[u8]) -> Quoter { - let mut q = Quoter { - safe_table: [0; 16], - protected_table: [0; 16], - }; - - // prepare safe table - for i in 0..128 { - if ALLOWED.contains(&i) { - set_bit(&mut q.safe_table, i); - } - if QS.contains(&i) { - set_bit(&mut q.safe_table, i); - } - } - - for ch in safe { - set_bit(&mut q.safe_table, *ch) - } - - // prepare protected table - for ch in protected { - set_bit(&mut q.safe_table, *ch); - set_bit(&mut q.protected_table, *ch); - } - - q - } - - pub fn requote(&self, val: &[u8]) -> Option> { - let mut has_pct = 0; - let mut pct = [b'%', 0, 0]; - let mut idx = 0; - let mut cloned: Option> = None; - - let len = val.len(); - while idx < len { - let ch = val[idx]; - - if has_pct != 0 { - pct[has_pct] = val[idx]; - has_pct += 1; - if has_pct == 3 { - has_pct = 0; - let buf = cloned.as_mut().unwrap(); - - if let Some(ch) = restore_ch(pct[1], pct[2]) { - if ch < 128 { - if bit_at(&self.protected_table, ch) { - buf.extend_from_slice(&pct); - idx += 1; - continue; - } - - if bit_at(&self.safe_table, ch) { - buf.push(ch); - idx += 1; - continue; - } - } - buf.push(ch); - } else { - buf.extend_from_slice(&pct[..]); - } - } - } else if ch == b'%' { - has_pct = 1; - if cloned.is_none() { - let mut c = Vec::with_capacity(len); - c.extend_from_slice(&val[..idx]); - cloned = Some(c); - } - } else if let Some(ref mut cloned) = cloned { - cloned.push(ch) - } - idx += 1; - } - - if let Some(data) = cloned { - // Unsafe: we get data from http::Uri, which does utf-8 checks already - // this code only decodes valid pct encoded values - Some(Rc::new(unsafe { String::from_utf8_unchecked(data) })) - } else { - None - } - } -} - -#[inline] -fn from_hex(v: u8) -> Option { - if v >= b'0' && v <= b'9' { - Some(v - 0x30) // ord('0') == 0x30 - } else if v >= b'A' && v <= b'F' { - Some(v - 0x41 + 10) // ord('A') == 0x41 - } else if v > b'a' && v <= b'f' { - Some(v - 0x61 + 10) // ord('a') == 0x61 - } else { - None - } -} - -#[inline] -fn restore_ch(d1: u8, d2: u8) -> Option { - from_hex(d1).and_then(|d1| from_hex(d2).and_then(move |d2| Some(d1 << 4 | d2))) -} From 18fcddfd637b77f11bea8d17826ff31bd019f277 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 18 Nov 2018 20:25:59 -0800 Subject: [PATCH 079/427] remove backtrace dep --- src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bee172f0c..e5f50011b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,8 +107,6 @@ extern crate serde_derive; #[cfg(feature = "ssl")] extern crate openssl; -extern crate backtrace; - pub mod body; pub mod client; mod config; @@ -173,4 +171,4 @@ pub mod http { } pub use header::ContentEncoding; pub use message::ConnectionType; -} +} \ No newline at end of file From 1ca6b44bae381f9042795fea63665aa9d480ccf2 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 18 Nov 2018 21:48:20 -0800 Subject: [PATCH 080/427] add TestServer --- src/lib.rs | 2 +- src/test.rs | 447 +++++++++++++++++++++---------------------- tests/test_client.rs | 113 ++++------- tests/test_server.rs | 354 ++++++++++++---------------------- tests/test_ws.rs | 128 ++++++------- 5 files changed, 425 insertions(+), 619 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e5f50011b..5256dd190 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,4 +171,4 @@ pub mod http { } pub use header::ContentEncoding; pub use message::ConnectionType; -} \ No newline at end of file +} diff --git a/src/test.rs b/src/test.rs index 5442a4149..9e14129b6 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,251 +1,31 @@ //! Various helpers for Actix applications to use during testing. -use std::net; use std::str::FromStr; +use std::sync::mpsc; +use std::{net, thread}; use actix::System; +use actix_net::codec::Framed; +use actix_net::server::{Server, StreamServiceFactory}; +use actix_net::service::Service; use bytes::Bytes; use cookie::Cookie; -use futures::Future; +use futures::future::{lazy, Future}; use http::header::HeaderName; use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; use net2::TcpBuilder; use tokio::runtime::current_thread::Runtime; +use tokio_io::{AsyncRead, AsyncWrite}; +use body::MessageBody; +use client::{ + ClientRequest, ClientRequestBuilder, ClientResponse, Connect, Connection, Connector, + ConnectorError, SendRequestError, +}; use header::{Header, IntoHeaderValue}; use payload::Payload; use request::Request; -// use ws; - -/// The `TestServer` type. -/// -/// `TestServer` is very simple test server that simplify process of writing -/// integration tests cases for actix web applications. -/// -/// # Examples -/// -/// ```rust,ignore -/// # extern crate actix_web; -/// # use actix_web::*; -/// # -/// # fn my_handler(req: &HttpRequest) -> Response { -/// # Response::Ok().into() -/// # } -/// # -/// # fn main() { -/// use actix_web::test::TestServer; -/// -/// let mut srv = TestServer::new(|app| app.handler(my_handler)); -/// -/// let req = srv.get().finish().unwrap(); -/// let response = srv.execute(req.send()).unwrap(); -/// assert!(response.status().is_success()); -/// # } -/// ``` -pub struct TestServer { - addr: net::SocketAddr, - rt: Runtime, - ssl: bool, -} - -impl TestServer { - /// Start new test server - /// - /// This method accepts configuration method. You can add - /// middlewares or set handlers for test application. - pub fn new(_config: F) -> Self - where - F: Fn() + Clone + Send + 'static, - { - unimplemented!() - } - - /// Get firat available unused address - pub fn unused_addr() -> net::SocketAddr { - let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); - let socket = TcpBuilder::new_v4().unwrap(); - socket.bind(&addr).unwrap(); - socket.reuse_address(true).unwrap(); - let tcp = socket.to_tcp_listener().unwrap(); - tcp.local_addr().unwrap() - } - - /// Construct test server url - pub fn addr(&self) -> net::SocketAddr { - self.addr - } - - /// Construct test server url - pub fn url(&self, uri: &str) -> String { - if uri.starts_with('/') { - format!( - "{}://localhost:{}{}", - if self.ssl { "https" } else { "http" }, - self.addr.port(), - uri - ) - } else { - format!( - "{}://localhost:{}/{}", - if self.ssl { "https" } else { "http" }, - self.addr.port(), - uri - ) - } - } - - /// Stop http server - fn stop(&mut self) { - System::current().stop(); - } - - /// Execute future on current core - pub fn execute(&mut self, fut: F) -> Result - where - F: Future, - { - self.rt.block_on(fut) - } - - // /// Connect to websocket server at a given path - // pub fn ws_at( - // &mut self, path: &str, - // ) -> Result<(ws::ClientReader, ws::ClientWriter), ws::ClientError> { - // let url = self.url(path); - // self.rt - // .block_on(ws::Client::with_connector(url, self.conn.clone()).connect()) - // } - - // /// Connect to a websocket server - // pub fn ws( - // &mut self, - // ) -> Result<(ws::ClientReader, ws::ClientWriter), ws::ClientError> { - // self.ws_at("/") - // } - - // /// Create `GET` request - // pub fn get(&self) -> ClientRequestBuilder { - // ClientRequest::get(self.url("/").as_str()) - // } - - // /// Create `POST` request - // pub fn post(&self) -> ClientRequestBuilder { - // ClientRequest::post(self.url("/").as_str()) - // } - - // /// Create `HEAD` request - // pub fn head(&self) -> ClientRequestBuilder { - // ClientRequest::head(self.url("/").as_str()) - // } - - // /// Connect to test http server - // pub fn client(&self, meth: Method, path: &str) -> ClientRequestBuilder { - // ClientRequest::build() - // .method(meth) - // .uri(self.url(path).as_str()) - // .with_connector(self.conn.clone()) - // .take() - // } -} - -impl Drop for TestServer { - fn drop(&mut self) { - self.stop() - } -} - -// /// An `TestServer` builder -// /// -// /// This type can be used to construct an instance of `TestServer` through a -// /// builder-like pattern. -// pub struct TestServerBuilder -// where -// F: Fn() -> S + Send + Clone + 'static, -// { -// state: F, -// } - -// impl TestServerBuilder -// where -// F: Fn() -> S + Send + Clone + 'static, -// { -// /// Create a new test server -// pub fn new(state: F) -> TestServerBuilder { -// TestServerBuilder { state } -// } - -// #[allow(unused_mut)] -// /// Configure test application and run test server -// pub fn start(mut self, config: C) -> TestServer -// where -// C: Fn(&mut TestApp) + Clone + Send + 'static, -// { -// let (tx, rx) = mpsc::channel(); - -// let mut has_ssl = false; - -// #[cfg(any(feature = "alpn", feature = "ssl"))] -// { -// has_ssl = has_ssl || self.ssl.is_some(); -// } - -// #[cfg(feature = "rust-tls")] -// { -// has_ssl = has_ssl || self.rust_ssl.is_some(); -// } - -// // run server in separate thread -// thread::spawn(move || { -// let addr = TestServer::unused_addr(); - -// let sys = System::new("actix-test-server"); -// let state = self.state; -// let mut srv = HttpServer::new(move || { -// let mut app = TestApp::new(state()); -// config(&mut app); -// app -// }).workers(1) -// .keep_alive(5) -// .disable_signals(); - -// tx.send((System::current(), addr, TestServer::get_conn())) -// .unwrap(); - -// #[cfg(any(feature = "alpn", feature = "ssl"))] -// { -// let ssl = self.ssl.take(); -// if let Some(ssl) = ssl { -// let tcp = net::TcpListener::bind(addr).unwrap(); -// srv = srv.listen_ssl(tcp, ssl).unwrap(); -// } -// } -// #[cfg(feature = "rust-tls")] -// { -// let ssl = self.rust_ssl.take(); -// if let Some(ssl) = ssl { -// let tcp = net::TcpListener::bind(addr).unwrap(); -// srv = srv.listen_rustls(tcp, ssl); -// } -// } -// if !has_ssl { -// let tcp = net::TcpListener::bind(addr).unwrap(); -// srv = srv.listen(tcp); -// } -// srv.start(); - -// sys.run(); -// }); - -// let (system, addr, conn) = rx.recv().unwrap(); -// System::set_current(system); -// TestServer { -// addr, -// conn, -// ssl: has_ssl, -// rt: Runtime::new().unwrap(), -// } -// } -// } +use ws; /// Test `Request` builder /// @@ -486,3 +266,204 @@ impl TestRequest { // } // } } + +/// The `TestServer` type. +/// +/// `TestServer` is very simple test server that simplify process of writing +/// integration tests cases for actix web applications. +/// +/// # Examples +/// +/// ```rust +/// # extern crate actix_web; +/// # use actix_web::*; +/// # +/// # fn my_handler(req: &HttpRequest) -> HttpResponse { +/// # HttpResponse::Ok().into() +/// # } +/// # +/// # fn main() { +/// use actix_web::test::TestServer; +/// +/// let mut srv = TestServer::new(|app| app.handler(my_handler)); +/// +/// let req = srv.get().finish().unwrap(); +/// let response = srv.execute(req.send()).unwrap(); +/// assert!(response.status().is_success()); +/// # } +/// ``` +pub struct TestServer; + +/// +pub struct TestServerRuntime { + addr: net::SocketAddr, + conn: T, + rt: Runtime, +} + +impl TestServer { + /// Start new test server with application factory + pub fn with_factory( + factory: F, + ) -> TestServerRuntime< + impl Service + + Clone, + > { + let (tx, rx) = mpsc::channel(); + + // run server in separate thread + thread::spawn(move || { + let sys = System::new("actix-test-server"); + let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); + let local_addr = tcp.local_addr().unwrap(); + + Server::default() + .listen("test", tcp, factory) + .workers(1) + .disable_signals() + .start(); + + tx.send((System::current(), local_addr)).unwrap(); + sys.run(); + }); + + let (system, addr) = rx.recv().unwrap(); + System::set_current(system); + + let mut rt = Runtime::new().unwrap(); + let conn = rt + .block_on(lazy(|| Ok::<_, ()>(TestServer::new_connector()))) + .unwrap(); + + TestServerRuntime { addr, conn, rt } + } + + fn new_connector( +) -> impl Service + + Clone { + #[cfg(feature = "ssl")] + { + use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; + + let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); + builder.set_verify(SslVerifyMode::NONE); + Connector::default().ssl(builder.build()).service() + } + #[cfg(not(feature = "ssl"))] + { + Connector::default().service() + } + } + + /// Get firat available unused address + pub fn unused_addr() -> net::SocketAddr { + let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); + let socket = TcpBuilder::new_v4().unwrap(); + socket.bind(&addr).unwrap(); + socket.reuse_address(true).unwrap(); + let tcp = socket.to_tcp_listener().unwrap(); + tcp.local_addr().unwrap() + } +} + +impl TestServerRuntime { + /// Execute future on current core + pub fn block_on(&mut self, fut: F) -> Result + where + F: Future, + { + self.rt.block_on(fut) + } + + /// Construct test server url + pub fn addr(&self) -> net::SocketAddr { + self.addr + } + + /// Construct test server url + pub fn url(&self, uri: &str) -> String { + if uri.starts_with('/') { + format!("http://localhost:{}{}", self.addr.port(), uri) + } else { + format!("http://localhost:{}/{}", self.addr.port(), uri) + } + } + + /// Create `GET` request + pub fn get(&self) -> ClientRequestBuilder { + ClientRequest::get(self.url("/").as_str()) + } + + /// Create `POST` request + pub fn post(&self) -> ClientRequestBuilder { + ClientRequest::post(self.url("/").as_str()) + } + + /// Create `HEAD` request + pub fn head(&self) -> ClientRequestBuilder { + ClientRequest::head(self.url("/").as_str()) + } + + /// Connect to test http server + pub fn client(&self, meth: Method, path: &str) -> ClientRequestBuilder { + ClientRequest::build() + .method(meth) + .uri(self.url(path).as_str()) + .take() + } + + /// Http connector + pub fn connector(&mut self) -> &mut T { + &mut self.conn + } + + /// Http connector + pub fn new_connector(&mut self) -> T + where + T: Clone, + { + self.conn.clone() + } + + /// Stop http server + fn stop(&mut self) { + System::current().stop(); + } +} + +impl TestServerRuntime +where + T: Service + Clone, + T::Response: Connection, +{ + /// Connect to websocket server at a given path + pub fn ws_at( + &mut self, + path: &str, + ) -> Result, ws::ClientError> { + let url = self.url(path); + self.rt + .block_on(ws::Client::default().call(ws::Connect::new(url))) + } + + /// Connect to a websocket server + pub fn ws( + &mut self, + ) -> Result, ws::ClientError> { + self.ws_at("/") + } + + /// Send request and read response message + pub fn send_request( + &mut self, + req: ClientRequest, + ) -> Result { + self.rt.block_on(req.send(&mut self.conn)) + } +} + +impl Drop for TestServerRuntime { + fn drop(&mut self) { + self.stop() + } +} diff --git a/tests/test_client.rs b/tests/test_client.rs index 40920d1b8..4a4ccb7dd 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -4,16 +4,12 @@ extern crate actix_net; extern crate bytes; extern crate futures; -use std::{thread, time}; - -use actix::System; -use actix_net::server::Server; use actix_net::service::NewServiceExt; use bytes::Bytes; -use futures::future::{self, lazy, ok}; +use futures::future::{self, ok}; use actix_http::HttpMessage; -use actix_http::{client, h1, test, Request, Response}; +use actix_http::{client, h1, test::TestServer, Request, Response}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -39,111 +35,70 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ #[test] fn test_h1_v2() { - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - h1::H1Service::build() - .finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) - .map(|_| ()) - }).unwrap() - .run(); + let mut srv = TestServer::with_factory(move || { + h1::H1Service::build() + .finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) + .map(|_| ()) }); - thread::sleep(time::Duration::from_millis(100)); + let mut connector = srv.new_connector(); - let mut sys = System::new("test"); - let mut connector = sys - .block_on(lazy(|| Ok::<_, ()>(client::Connector::default().service()))) - .unwrap(); - - let req = client::ClientRequest::get(format!("http://{}/", addr)) - .finish() - .unwrap(); - - let response = sys.block_on(req.send(&mut connector)).unwrap(); + let request = srv.get().finish().unwrap(); + let response = srv.block_on(request.send(&mut connector)).unwrap(); assert!(response.status().is_success()); - let request = client::ClientRequest::get(format!("http://{}/", addr)) - .header("x-test", "111") - .finish() - .unwrap(); + let request = srv.get().header("x-test", "111").finish().unwrap(); let repr = format!("{:?}", request); assert!(repr.contains("ClientRequest")); assert!(repr.contains("x-test")); - let response = sys.block_on(request.send(&mut connector)).unwrap(); + let response = srv.block_on(request.send(&mut connector)).unwrap(); assert!(response.status().is_success()); // read response - let bytes = sys.block_on(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); - let request = client::ClientRequest::post(format!("http://{}/", addr)) - .finish() - .unwrap(); - let response = sys.block_on(request.send(&mut connector)).unwrap(); + let request = srv.post().finish().unwrap(); + let response = srv.block_on(request.send(&mut connector)).unwrap(); assert!(response.status().is_success()); // read response - let bytes = sys.block_on(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } #[test] fn test_connection_close() { - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - h1::H1Service::build() - .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) - .map(|_| ()) - }).unwrap() - .run(); + let mut srv = TestServer::with_factory(move || { + h1::H1Service::build() + .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) + .map(|_| ()) }); - thread::sleep(time::Duration::from_millis(100)); + let mut connector = srv.new_connector(); - let mut sys = System::new("test"); - let mut connector = sys - .block_on(lazy(|| Ok::<_, ()>(client::Connector::default().service()))) - .unwrap(); - - let request = client::ClientRequest::get(format!("http://{}/", addr)) - .header("Connection", "close") - .finish() - .unwrap(); - let response = sys.block_on(request.send(&mut connector)).unwrap(); + let request = srv.get().close().finish().unwrap(); + let response = srv.block_on(request.send(&mut connector)).unwrap(); assert!(response.status().is_success()); } #[test] fn test_with_query_parameter() { - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - h1::H1Service::build() - .finish(|req: Request| { - if req.uri().query().unwrap().contains("qp=") { - ok::<_, ()>(Response::Ok().finish()) - } else { - ok::<_, ()>(Response::BadRequest().finish()) - } - }).map(|_| ()) - }).unwrap() - .run(); + let mut srv = TestServer::with_factory(move || { + h1::H1Service::build() + .finish(|req: Request| { + if req.uri().query().unwrap().contains("qp=") { + ok::<_, ()>(Response::Ok().finish()) + } else { + ok::<_, ()>(Response::BadRequest().finish()) + } + }).map(|_| ()) }); - thread::sleep(time::Duration::from_millis(100)); + let mut connector = srv.new_connector(); - let mut sys = System::new("test"); - let mut connector = sys - .block_on(lazy(|| Ok::<_, ()>(client::Connector::default().service()))) - .unwrap(); - - let request = client::ClientRequest::get(format!("http://{}/?qp=5", addr)) + let request = client::ClientRequest::get(srv.url("/?qp=5")) .finish() .unwrap(); - let response = sys.block_on(request.send(&mut connector)).unwrap(); + let response = srv.block_on(request.send(&mut connector)).unwrap(); assert!(response.status().is_success()); } diff --git a/tests/test_server.rs b/tests/test_server.rs index 9d16e92e3..a01af4f0d 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1,70 +1,48 @@ extern crate actix; extern crate actix_http; extern crate actix_net; -extern crate actix_web; extern crate bytes; extern crate futures; -use std::{io::Read, io::Write, net, thread, time}; +use std::{io::Read, io::Write, net}; -use actix::System; -use actix_net::server::Server; use actix_net::service::NewServiceExt; -use actix_web::{client, test, HttpMessage}; use bytes::Bytes; -use futures::future::{self, lazy, ok}; +use futures::future::{self, ok}; use futures::stream::once; use actix_http::{ - body, client as client2, h1, http, Body, Error, HttpMessage as HttpMessage2, - KeepAlive, Request, Response, + body, client, h1, http, test, Body, Error, HttpMessage as HttpMessage2, KeepAlive, + Request, Response, }; #[test] fn test_h1_v2() { - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - h1::H1Service::build() - .keep_alive(KeepAlive::Disabled) - .client_timeout(1000) - .client_disconnect(1000) - .server_hostname("localhost") - .server_address(addr) - .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) - .map(|_| ()) - }).unwrap() - .run(); + let mut srv = test::TestServer::with_factory(|| { + h1::H1Service::build() + .keep_alive(KeepAlive::Disabled) + .client_timeout(1000) + .client_disconnect(1000) + .server_hostname("localhost") + .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) + .map(|_| ()) }); - thread::sleep(time::Duration::from_millis(100)); - let mut sys = System::new("test"); - { - let req = client::ClientRequest::get(format!("http://{}/", addr)) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } + let req = client::ClientRequest::get(srv.url("/")).finish().unwrap(); + let response = srv.send_request(req).unwrap(); + assert!(response.status().is_success()); } #[test] fn test_slow_request() { - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - h1::H1Service::build() - .client_timeout(100) - .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) - .map(|_| ()) - }).unwrap() - .run(); + let srv = test::TestServer::with_factory(|| { + h1::H1Service::build() + .client_timeout(100) + .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) + .map(|_| ()) }); - thread::sleep(time::Duration::from_millis(100)); - let mut stream = net::TcpStream::connect(addr).unwrap(); + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n"); let mut data = String::new(); let _ = stream.read_to_string(&mut data); @@ -73,18 +51,11 @@ fn test_slow_request() { #[test] fn test_malformed_request() { - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - h1::H1Service::new(|_| future::ok::<_, ()>(Response::Ok().finish())) - .map(|_| ()) - }).unwrap() - .run(); + let srv = test::TestServer::with_factory(|| { + h1::H1Service::new(|_| future::ok::<_, ()>(Response::Ok().finish())).map(|_| ()) }); - thread::sleep(time::Duration::from_millis(100)); - let mut stream = net::TcpStream::connect(addr).unwrap(); + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test/tests/test HTTP1.1\r\n"); let mut data = String::new(); let _ = stream.read_to_string(&mut data); @@ -98,51 +69,42 @@ fn test_content_length() { StatusCode, }; - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - h1::H1Service::new(|req: Request| { - let indx: usize = req.uri().path()[1..].parse().unwrap(); - let statuses = [ - StatusCode::NO_CONTENT, - StatusCode::CONTINUE, - StatusCode::SWITCHING_PROTOCOLS, - StatusCode::PROCESSING, - StatusCode::OK, - StatusCode::NOT_FOUND, - ]; - future::ok::<_, ()>(Response::new(statuses[indx])) - }).map(|_| ()) - }).unwrap() - .run(); + let mut srv = test::TestServer::with_factory(|| { + h1::H1Service::new(|req: Request| { + let indx: usize = req.uri().path()[1..].parse().unwrap(); + let statuses = [ + StatusCode::NO_CONTENT, + StatusCode::CONTINUE, + StatusCode::SWITCHING_PROTOCOLS, + StatusCode::PROCESSING, + StatusCode::OK, + StatusCode::NOT_FOUND, + ]; + future::ok::<_, ()>(Response::new(statuses[indx])) + }).map(|_| ()) }); - thread::sleep(time::Duration::from_millis(100)); let header = HeaderName::from_static("content-length"); let value = HeaderValue::from_static("0"); - let mut sys = System::new("test"); { for i in 0..4 { - let req = client::ClientRequest::get(format!("http://{}/{}", addr, i)) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); + let req = client::ClientRequest::get(srv.url("/")).finish().unwrap(); + let response = srv.send_request(req).unwrap(); assert_eq!(response.headers().get(&header), None); - let req = client::ClientRequest::head(format!("http://{}/{}", addr, i)) + let req = client::ClientRequest::head(srv.url(&format!("/{}", i))) .finish() .unwrap(); - let response = sys.block_on(req.send()).unwrap(); + let response = srv.send_request(req).unwrap(); assert_eq!(response.headers().get(&header), None); } for i in 4..6 { - let req = client::ClientRequest::get(format!("http://{}/{}", addr, i)) + let req = client::ClientRequest::get(srv.url(&format!("/{}", i))) .finish() .unwrap(); - let response = sys.block_on(req.send()).unwrap(); + let response = srv.send_request(req).unwrap(); assert_eq!(response.headers().get(&header), Some(&value)); } } @@ -153,54 +115,41 @@ fn test_headers() { let data = STR.repeat(10); let data2 = data.clone(); - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - let data = data.clone(); - h1::H1Service::new(move |_| { - let mut builder = Response::Ok(); - for idx in 0..90 { - builder.header( - format!("X-TEST-{}", idx).as_str(), - "TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", - ); - } - future::ok::<_, ()>(builder.body(data.clone())) - }).map(|_| ()) - }) - .unwrap() - .run() + let mut srv = test::TestServer::with_factory(move || { + let data = data.clone(); + h1::H1Service::new(move |_| { + let mut builder = Response::Ok(); + for idx in 0..90 { + builder.header( + format!("X-TEST-{}", idx).as_str(), + "TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", + ); + } + future::ok::<_, ()>(builder.body(data.clone())) + }).map(|_| ()) }); - thread::sleep(time::Duration::from_millis(200)); - let mut sys = System::new("test"); - let mut connector = sys - .block_on(lazy(|| { - Ok::<_, ()>(client2::Connector::default().service()) - })).unwrap(); + let mut connector = srv.new_connector(); - let req = client2::ClientRequest::get(format!("http://{}/", addr)) - .finish() - .unwrap(); + let req = srv.get().finish().unwrap(); - let response = sys.block_on(req.send(&mut connector)).unwrap(); + let response = srv.block_on(req.send(&mut connector)).unwrap(); assert!(response.status().is_success()); // read response - let bytes = sys.block_on(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from(data2)); } @@ -228,46 +177,27 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ #[test] fn test_body() { - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - h1::H1Service::new(|_| future::ok::<_, ()>(Response::Ok().body(STR))) - .map(|_| ()) - }).unwrap() - .run(); + let mut srv = test::TestServer::with_factory(|| { + h1::H1Service::new(|_| future::ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) }); - thread::sleep(time::Duration::from_millis(100)); - let mut sys = System::new("test"); - let req = client::ClientRequest::get(format!("http://{}/", addr)) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); + let req = srv.get().finish().unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response - let bytes = sys.block_on(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } #[test] fn test_head_empty() { - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - h1::H1Service::new(|_| ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) - }).unwrap() - .run() + let mut srv = test::TestServer::with_factory(|| { + h1::H1Service::new(|_| ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) }); - thread::sleep(time::Duration::from_millis(100)); - let mut sys = System::new("test"); - let req = client::ClientRequest::head(format!("http://{}/", addr)) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); + let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); { @@ -279,31 +209,20 @@ fn test_head_empty() { } // read response - let bytes = sys.block_on(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert!(bytes.is_empty()); } #[test] fn test_head_binary() { - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - h1::H1Service::new(|_| { - ok::<_, ()>( - Response::Ok().content_length(STR.len() as u64).body(STR), - ) - }).map(|_| ()) - }).unwrap() - .run() + let mut srv = test::TestServer::with_factory(|| { + h1::H1Service::new(|_| { + ok::<_, ()>(Response::Ok().content_length(STR.len() as u64).body(STR)) + }).map(|_| ()) }); - thread::sleep(time::Duration::from_millis(100)); - let mut sys = System::new("test"); - let req = client::ClientRequest::head(format!("http://{}/", addr)) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); + let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); { @@ -315,27 +234,18 @@ fn test_head_binary() { } // read response - let bytes = sys.block_on(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert!(bytes.is_empty()); } #[test] fn test_head_binary2() { - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - h1::H1Service::new(|_| ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) - }).unwrap() - .run() + let mut srv = test::TestServer::with_factory(|| { + h1::H1Service::new(|_| ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) }); - thread::sleep(time::Duration::from_millis(100)); - let mut sys = System::new("test"); - let req = client::ClientRequest::head(format!("http://{}/", addr)) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); + let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); { @@ -349,57 +259,40 @@ fn test_head_binary2() { #[test] fn test_body_length() { - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - h1::H1Service::new(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>(Response::Ok().body(Body::from_message( - body::SizedStream::new(STR.len(), body), - ))) - }).map(|_| ()) - }).unwrap() - .run() + let mut srv = test::TestServer::with_factory(|| { + h1::H1Service::new(|_| { + let body = once(Ok(Bytes::from_static(STR.as_ref()))); + ok::<_, ()>( + Response::Ok() + .body(Body::from_message(body::SizedStream::new(STR.len(), body))), + ) + }).map(|_| ()) }); - thread::sleep(time::Duration::from_millis(100)); - let mut sys = System::new("test"); - let req = client::ClientRequest::get(format!("http://{}/", addr)) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); + let req = srv.get().finish().unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response - let bytes = sys.block_on(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } #[test] fn test_body_chunked_explicit() { - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - h1::H1Service::new(|_| { - let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>(Response::Ok().streaming(body)) - }).map(|_| ()) - }).unwrap() - .run() + let mut srv = test::TestServer::with_factory(|| { + h1::H1Service::new(|_| { + let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); + ok::<_, ()>(Response::Ok().streaming(body)) + }).map(|_| ()) }); - thread::sleep(time::Duration::from_millis(100)); - let mut sys = System::new("test"); - let req = client::ClientRequest::get(format!("http://{}/", addr)) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); + let req = srv.get().finish().unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response - let bytes = sys.block_on(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode assert_eq!(bytes, Bytes::from_static(STR.as_ref())); @@ -407,27 +300,18 @@ fn test_body_chunked_explicit() { #[test] fn test_body_chunked_implicit() { - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - h1::H1Service::new(|_| { - let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>(Response::Ok().streaming(body)) - }).map(|_| ()) - }).unwrap() - .run() + let mut srv = test::TestServer::with_factory(|| { + h1::H1Service::new(|_| { + let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); + ok::<_, ()>(Response::Ok().streaming(body)) + }).map(|_| ()) }); - thread::sleep(time::Duration::from_millis(100)); - let mut sys = System::new("test"); - let req = client::ClientRequest::get(format!("http://{}/", addr)) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); + let req = srv.get().finish().unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response - let bytes = sys.block_on(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } diff --git a/tests/test_ws.rs b/tests/test_ws.rs index c246d5e47..21f635129 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -5,20 +5,18 @@ extern crate actix_web; extern crate bytes; extern crate futures; -use std::{io, thread}; +use std::io; -use actix::System; use actix_net::codec::Framed; use actix_net::framed::IntoFramed; -use actix_net::server::Server; -use actix_net::service::{NewServiceExt, Service}; +use actix_net::service::NewServiceExt; use actix_net::stream::TakeItem; -use actix_web::{test, ws as web_ws}; +use actix_web::ws as web_ws; use bytes::{Bytes, BytesMut}; -use futures::future::{lazy, ok, Either}; -use futures::{Future, IntoFuture, Sink, Stream}; +use futures::future::{ok, Either}; +use futures::{Future, Sink, Stream}; -use actix_http::{h1, ws, ResponseError, SendResponse, ServiceConfig}; +use actix_http::{h1, test, ws, ResponseError, SendResponse, ServiceConfig}; fn ws_service(req: ws::Frame) -> impl Future { match req { @@ -43,72 +41,66 @@ fn ws_service(req: ws::Frame) -> impl Future)| { - // validate request - if let Some(h1::Message::Item(req)) = req { - match ws::verify_handshake(&req) { - Err(e) => { - // validation failed - Either::A( - SendResponse::send(framed, e.error_response()) - .map_err(|_| ()) - .map(|_| ()), - ) - } - Ok(_) => { - Either::B( - // send handshake response - SendResponse::send( - framed, - ws::handshake_response(&req).finish(), - ).map_err(|_| ()) - .and_then(|framed| { - // start websocket service - let framed = - framed.into_framed(ws::Codec::new()); - ws::Transport::with(framed, ws_service) - .map_err(|_| ()) - }), - ) - } - } - } else { - panic!() + let mut srv = test::TestServer::with_factory(|| { + IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) + .and_then(TakeItem::new().map_err(|_| ())) + .and_then(|(req, framed): (_, Framed<_, _>)| { + // validate request + if let Some(h1::Message::Item(req)) = req { + match ws::verify_handshake(&req) { + Err(e) => { + // validation failed + Either::A( + SendResponse::send(framed, e.error_response()) + .map_err(|_| ()) + .map(|_| ()), + ) } - }) - }).unwrap() - .run(); + Ok(_) => { + Either::B( + // send handshake response + SendResponse::send( + framed, + ws::handshake_response(&req).finish(), + ).map_err(|_| ()) + .and_then(|framed| { + // start websocket service + let framed = framed.into_framed(ws::Codec::new()); + ws::Transport::with(framed, ws_service) + .map_err(|_| ()) + }), + ) + } + } + } else { + panic!() + } + }) }); - let mut sys = System::new("test"); { - let (reader, mut writer) = sys - .block_on(web_ws::Client::new(format!("http://{}/", addr)).connect()) - .unwrap(); + let url = srv.url("/"); + + let (reader, mut writer) = + srv.block_on(web_ws::Client::new(url).connect()).unwrap(); writer.text("text"); - let (item, reader) = sys.block_on(reader.into_future()).unwrap(); + let (item, reader) = srv.block_on(reader.into_future()).unwrap(); assert_eq!(item, Some(web_ws::Message::Text("text".to_owned()))); writer.binary(b"text".as_ref()); - let (item, reader) = sys.block_on(reader.into_future()).unwrap(); + let (item, reader) = srv.block_on(reader.into_future()).unwrap(); assert_eq!( item, Some(web_ws::Message::Binary(Bytes::from_static(b"text").into())) ); writer.ping("ping"); - let (item, reader) = sys.block_on(reader.into_future()).unwrap(); + let (item, reader) = srv.block_on(reader.into_future()).unwrap(); assert_eq!(item, Some(web_ws::Message::Pong("ping".to_owned()))); writer.close(Some(web_ws::CloseCode::Normal.into())); - let (item, _) = sys.block_on(reader.into_future()).unwrap(); + let (item, _) = srv.block_on(reader.into_future()).unwrap(); assert_eq!( item, Some(web_ws::Message::Close(Some( @@ -118,39 +110,33 @@ fn test_simple() { } // client service - let mut client = sys - .block_on(lazy(|| Ok::<_, ()>(ws::Client::default()).into_future())) - .unwrap(); - let framed = sys - .block_on(client.call(ws::Connect::new(format!("http://{}/", addr)))) - .unwrap(); - - let framed = sys + let framed = srv.ws().unwrap(); + let framed = srv .block_on(framed.send(ws::Message::Text("text".to_string()))) .unwrap(); - let (item, framed) = sys.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); assert_eq!(item, Some(ws::Frame::Text(Some(BytesMut::from("text"))))); - let framed = sys + let framed = srv .block_on(framed.send(ws::Message::Binary("text".into()))) .unwrap(); - let (item, framed) = sys.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); assert_eq!( item, Some(ws::Frame::Binary(Some(Bytes::from_static(b"text").into()))) ); - let framed = sys + let framed = srv .block_on(framed.send(ws::Message::Ping("text".into()))) .unwrap(); - let (item, framed) = sys.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); assert_eq!(item, Some(ws::Frame::Pong("text".to_string().into()))); - let framed = sys + let framed = srv .block_on(framed.send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))) .unwrap(); - let (item, _framed) = sys.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + let (item, _framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); assert_eq!( item, Some(ws::Frame::Close(Some(ws::CloseCode::Normal.into()))) From 3901239128ac9d076cdaaeb46220117a8043f7ad Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 19 Nov 2018 14:57:12 -0800 Subject: [PATCH 081/427] unify requedt/response encoder --- src/client/pipeline.rs | 13 +- src/h1/client.rs | 185 ++++++++++------------------ src/h1/codec.rs | 184 +++++++--------------------- src/h1/decoder.rs | 97 +++++++-------- src/h1/encoder.rs | 268 ++++++++++++++++++++++++++++++++--------- src/message.rs | 8 +- src/request.rs | 21 ++-- src/response.rs | 61 +++++++++- src/test.rs | 2 +- src/ws/mod.rs | 3 +- tests/test_server.rs | 4 +- tests/test_ws.rs | 7 +- 12 files changed, 448 insertions(+), 405 deletions(-) diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs index 63551cffa..e1d8421e9 100644 --- a/src/client/pipeline.rs +++ b/src/client/pipeline.rs @@ -49,7 +49,10 @@ where .and_then(|(item, framed)| { if let Some(res) = item { match framed.get_codec().message_type() { - h1::MessageType::None => release_connection(framed), + h1::MessageType::None => { + let force_close = !framed.get_codec().keepalive(); + release_connection(framed, force_close) + } _ => { *res.payload.borrow_mut() = Some(Payload::stream(framed)) } @@ -174,7 +177,9 @@ impl Stream for Payload { Async::Ready(Some(chunk)) => if let Some(chunk) = chunk { Ok(Async::Ready(Some(chunk))) } else { - release_connection(self.framed.take().unwrap()); + let framed = self.framed.take().unwrap(); + let force_close = framed.get_codec().keepalive(); + release_connection(framed, force_close); Ok(Async::Ready(None)) }, Async::Ready(None) => Ok(Async::Ready(None)), @@ -182,12 +187,12 @@ impl Stream for Payload { } } -fn release_connection(framed: Framed) +fn release_connection(framed: Framed, force_close: bool) where T: Connection, { let mut parts = framed.into_parts(); - if parts.read_buf.is_empty() && parts.write_buf.is_empty() { + if !force_close && parts.read_buf.is_empty() && parts.write_buf.is_empty() { parts.io.release() } else { parts.io.close() diff --git a/src/h1/client.rs b/src/h1/client.rs index e2d1eefe6..7704ba97a 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -4,8 +4,8 @@ use std::io::{self, Write}; use bytes::{BufMut, Bytes, BytesMut}; use tokio_codec::{Decoder, Encoder}; -use super::decoder::{MessageDecoder, PayloadDecoder, PayloadItem, PayloadType}; -use super::encoder::RequestEncoder; +use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; +use super::{decoder, encoder}; use super::{Message, MessageType}; use body::BodyLength; use client::ClientResponse; @@ -16,13 +16,11 @@ use http::header::{ HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE, }; use http::{Method, Version}; -use message::{Head, MessagePool, RequestHead}; +use message::{ConnectionType, Head, MessagePool, RequestHead}; bitflags! { struct Flags: u8 { const HEAD = 0b0000_0001; - const UPGRADE = 0b0000_0010; - const KEEPALIVE = 0b0000_0100; const KEEPALIVE_ENABLED = 0b0000_1000; const STREAM = 0b0001_0000; } @@ -42,14 +40,15 @@ pub struct ClientPayloadCodec { struct ClientCodecInner { config: ServiceConfig, - decoder: MessageDecoder, + decoder: decoder::MessageDecoder, payload: Option, version: Version, + ctype: ConnectionType, // encoder part flags: Flags, headers_size: u32, - te: RequestEncoder, + encoder: encoder::MessageEncoder, } impl Default for ClientCodec { @@ -71,25 +70,26 @@ impl ClientCodec { ClientCodec { inner: ClientCodecInner { config, - decoder: MessageDecoder::default(), + decoder: decoder::MessageDecoder::default(), payload: None, version: Version::HTTP_11, + ctype: ConnectionType::Close, flags, headers_size: 0, - te: RequestEncoder::default(), + encoder: encoder::MessageEncoder::default(), }, } } /// Check if request is upgrade pub fn upgrade(&self) -> bool { - self.inner.flags.contains(Flags::UPGRADE) + self.inner.ctype == ConnectionType::Upgrade } /// Check if last response is keep-alive pub fn keepalive(&self) -> bool { - self.inner.flags.contains(Flags::KEEPALIVE) + self.inner.ctype == ConnectionType::KeepAlive } /// Check last request's message type @@ -103,15 +103,6 @@ impl ClientCodec { } } - /// prepare transfer encoding - pub fn prepare_te(&mut self, head: &mut RequestHead, length: BodyLength) { - self.inner.te.update( - head, - self.inner.flags.contains(Flags::HEAD), - self.inner.version, - ); - } - /// Convert message codec to a payload codec pub fn into_payload_codec(self) -> ClientPayloadCodec { ClientPayloadCodec { inner: self.inner } @@ -119,96 +110,17 @@ impl ClientCodec { } impl ClientPayloadCodec { + /// Check if last response is keep-alive + pub fn keepalive(&self) -> bool { + self.inner.ctype == ConnectionType::KeepAlive + } + /// Transform payload codec to a message codec pub fn into_message_codec(self) -> ClientCodec { ClientCodec { inner: self.inner } } } -fn prn_version(ver: Version) -> &'static str { - match ver { - Version::HTTP_09 => "HTTP/0.9", - Version::HTTP_10 => "HTTP/1.0", - Version::HTTP_11 => "HTTP/1.1", - Version::HTTP_2 => "HTTP/2.0", - } -} - -impl ClientCodecInner { - fn encode_request( - &mut self, - msg: RequestHead, - length: BodyLength, - buffer: &mut BytesMut, - ) -> io::Result<()> { - // render message - { - // status line - write!( - Writer(buffer), - "{} {} {}\r\n", - msg.method, - msg.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"), - prn_version(msg.version) - ).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - - // write headers - buffer.reserve(msg.headers.len() * AVERAGE_HEADER_SIZE); - - // content length - match length { - BodyLength::Sized(len) => helpers::write_content_length(len, buffer), - BodyLength::Sized64(len) => { - buffer.extend_from_slice(b"content-length: "); - write!(buffer.writer(), "{}", len)?; - buffer.extend_from_slice(b"\r\n"); - } - BodyLength::Chunked => { - buffer.extend_from_slice(b"transfer-encoding: chunked\r\n") - } - BodyLength::Empty => buffer.extend_from_slice(b"content-length: 0\r\n"), - BodyLength::None | BodyLength::Stream => (), - } - - let mut has_date = false; - - for (key, value) in &msg.headers { - match *key { - TRANSFER_ENCODING | CONNECTION | CONTENT_LENGTH => continue, - DATE => has_date = true, - _ => (), - } - - buffer.put_slice(key.as_ref()); - buffer.put_slice(b": "); - buffer.put_slice(value.as_ref()); - buffer.put_slice(b"\r\n"); - } - - // Connection header - if msg.upgrade() { - self.flags.set(Flags::UPGRADE, msg.upgrade()); - buffer.extend_from_slice(b"connection: upgrade\r\n"); - } else if msg.keep_alive() { - if self.version < Version::HTTP_11 { - buffer.extend_from_slice(b"connection: keep-alive\r\n"); - } - } else if self.version >= Version::HTTP_11 { - buffer.extend_from_slice(b"connection: close\r\n"); - } - - // Date header - if !has_date { - self.config.set_date(buffer); - } else { - buffer.extend_from_slice(b"\r\n"); - } - } - - Ok(()) - } -} - impl Decoder for ClientCodec { type Item = ClientResponse; type Error = ParseError; @@ -217,21 +129,27 @@ impl Decoder for ClientCodec { debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set"); if let Some((req, payload)) = self.inner.decoder.decode(src)? { - // self.inner - // .flags - // .set(Flags::HEAD, req.head.method == Method::HEAD); - // self.inner.version = req.head.version; - if self.inner.flags.contains(Flags::KEEPALIVE_ENABLED) { - self.inner.flags.set(Flags::KEEPALIVE, req.keep_alive()); + if let Some(ctype) = req.head().ctype { + // do not use peer's keep-alive + self.inner.ctype = if ctype == ConnectionType::KeepAlive { + self.inner.ctype + } else { + ctype + }; } - match payload { - PayloadType::None => self.inner.payload = None, - PayloadType::Payload(pl) => self.inner.payload = Some(pl), - PayloadType::Stream(pl) => { - self.inner.payload = Some(pl); - self.inner.flags.insert(Flags::STREAM); + + if !self.inner.flags.contains(Flags::HEAD) { + match payload { + PayloadType::None => self.inner.payload = None, + PayloadType::Payload(pl) => self.inner.payload = Some(pl), + PayloadType::Stream(pl) => { + self.inner.payload = Some(pl); + self.inner.flags.insert(Flags::STREAM); + } } - }; + } else { + self.inner.payload = None; + } Ok(Some(req)) } else { Ok(None) @@ -270,14 +188,39 @@ impl Encoder for ClientCodec { dst: &mut BytesMut, ) -> Result<(), Self::Error> { match item { - Message::Item((msg, btype)) => { - self.inner.encode_request(msg, btype, dst)?; + Message::Item((mut msg, length)) => { + let inner = &mut self.inner; + inner.version = msg.version; + inner.flags.set(Flags::HEAD, msg.method == Method::HEAD); + + // connection status + inner.ctype = match msg.connection_type() { + ConnectionType::KeepAlive => { + if inner.flags.contains(Flags::KEEPALIVE_ENABLED) { + ConnectionType::KeepAlive + } else { + ConnectionType::Close + } + } + ConnectionType::Upgrade => ConnectionType::Upgrade, + ConnectionType::Close => ConnectionType::Close, + }; + + inner.encoder.encode( + dst, + &mut msg, + false, + inner.version, + length, + inner.ctype, + &inner.config, + )?; } Message::Chunk(Some(bytes)) => { - self.inner.te.encode(bytes.as_ref(), dst)?; + self.inner.encoder.encode_chunk(bytes.as_ref(), dst)?; } Message::Chunk(None) => { - self.inner.te.encode_eof(dst)?; + self.inner.encoder.encode_eof(dst)?; } } Ok(()) diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 117c8cde1..f9f455e5d 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -5,8 +5,8 @@ use std::io::{self, Write}; use bytes::{BufMut, Bytes, BytesMut}; use tokio_codec::{Decoder, Encoder}; -use super::decoder::{MessageDecoder, PayloadDecoder, PayloadItem, PayloadType}; -use super::encoder::ResponseEncoder; +use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; +use super::{decoder, encoder}; use super::{Message, MessageType}; use body::BodyLength; use config::ServiceConfig; @@ -14,15 +14,13 @@ use error::ParseError; use helpers; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::{Method, StatusCode, Version}; -use message::{Head, ResponseHead}; +use message::{ConnectionType, Head, ResponseHead}; use request::Request; use response::Response; bitflags! { struct Flags: u8 { const HEAD = 0b0000_0001; - const UPGRADE = 0b0000_0010; - const KEEPALIVE = 0b0000_0100; const KEEPALIVE_ENABLED = 0b0000_1000; const STREAM = 0b0001_0000; } @@ -33,14 +31,15 @@ const AVERAGE_HEADER_SIZE: usize = 30; /// HTTP/1 Codec pub struct Codec { config: ServiceConfig, - decoder: MessageDecoder, + decoder: decoder::MessageDecoder, payload: Option, version: Version, + ctype: ConnectionType, // encoder part flags: Flags, headers_size: u32, - te: ResponseEncoder, + encoder: encoder::MessageEncoder>, } impl Default for Codec { @@ -67,24 +66,25 @@ impl Codec { }; Codec { config, - decoder: MessageDecoder::default(), + decoder: decoder::MessageDecoder::default(), payload: None, version: Version::HTTP_11, + ctype: ConnectionType::Close, flags, headers_size: 0, - te: ResponseEncoder::default(), + encoder: encoder::MessageEncoder::default(), } } /// Check if request is upgrade pub fn upgrade(&self) -> bool { - self.flags.contains(Flags::UPGRADE) + self.ctype == ConnectionType::Upgrade } /// Check if last response is keep-alive pub fn keepalive(&self) -> bool { - self.flags.contains(Flags::KEEPALIVE) + self.ctype == ConnectionType::KeepAlive } /// Check last request's message type @@ -97,130 +97,6 @@ impl Codec { MessageType::Payload } } - - /// prepare transfer encoding - fn prepare_te(&mut self, head: &mut ResponseHead, length: BodyLength) { - self.te - .update(head, self.flags.contains(Flags::HEAD), self.version, length); - } - - fn encode_response( - &mut self, - msg: &mut ResponseHead, - length: BodyLength, - buffer: &mut BytesMut, - ) -> io::Result<()> { - msg.version = self.version; - - // Connection upgrade - if msg.upgrade() { - self.flags.insert(Flags::UPGRADE); - self.flags.remove(Flags::KEEPALIVE); - msg.headers - .insert(CONNECTION, HeaderValue::from_static("upgrade")); - } - // keep-alive - else if self.flags.contains(Flags::KEEPALIVE_ENABLED) && msg.keep_alive() { - self.flags.insert(Flags::KEEPALIVE); - if self.version < Version::HTTP_11 { - msg.headers - .insert(CONNECTION, HeaderValue::from_static("keep-alive")); - } - } else if self.version >= Version::HTTP_11 { - self.flags.remove(Flags::KEEPALIVE); - msg.headers - .insert(CONNECTION, HeaderValue::from_static("close")); - } - - // render message - { - let reason = msg.reason().as_bytes(); - buffer.reserve(256 + msg.headers.len() * AVERAGE_HEADER_SIZE + reason.len()); - - // status line - helpers::write_status_line(self.version, msg.status.as_u16(), buffer); - buffer.extend_from_slice(reason); - - // content length - match msg.status { - StatusCode::NO_CONTENT - | StatusCode::CONTINUE - | StatusCode::SWITCHING_PROTOCOLS - | StatusCode::PROCESSING => buffer.extend_from_slice(b"\r\n"), - _ => match length { - BodyLength::Chunked => { - buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") - } - BodyLength::Empty => { - buffer.extend_from_slice(b"\r\ncontent-length: 0\r\n"); - } - BodyLength::Sized(len) => helpers::write_content_length(len, buffer), - BodyLength::Sized64(len) => { - buffer.extend_from_slice(b"\r\ncontent-length: "); - write!(buffer.writer(), "{}", len)?; - buffer.extend_from_slice(b"\r\n"); - } - BodyLength::None | BodyLength::Stream => { - buffer.extend_from_slice(b"\r\n") - } - }, - } - - // write headers - let mut pos = 0; - let mut has_date = false; - let mut remaining = buffer.remaining_mut(); - let mut buf = unsafe { &mut *(buffer.bytes_mut() as *mut [u8]) }; - for (key, value) in &msg.headers { - match *key { - TRANSFER_ENCODING | CONTENT_LENGTH => continue, - DATE => { - has_date = true; - } - _ => (), - } - - let v = value.as_ref(); - let k = key.as_str().as_bytes(); - let len = k.len() + v.len() + 4; - if len > remaining { - unsafe { - buffer.advance_mut(pos); - } - pos = 0; - buffer.reserve(len); - remaining = buffer.remaining_mut(); - unsafe { - buf = &mut *(buffer.bytes_mut() as *mut _); - } - } - - buf[pos..pos + k.len()].copy_from_slice(k); - pos += k.len(); - buf[pos..pos + 2].copy_from_slice(b": "); - pos += 2; - buf[pos..pos + v.len()].copy_from_slice(v); - pos += v.len(); - buf[pos..pos + 2].copy_from_slice(b"\r\n"); - pos += 2; - remaining -= len; - } - unsafe { - buffer.advance_mut(pos); - } - - // optimized date header, set_date writes \r\n - if !has_date { - self.config.set_date(buffer); - } else { - // msg eof - buffer.extend_from_slice(b"\r\n"); - } - self.headers_size = buffer.len() as u32; - } - - Ok(()) - } } impl Decoder for Codec { @@ -240,9 +116,12 @@ impl Decoder for Codec { } else if let Some((req, payload)) = self.decoder.decode(src)? { self.flags .set(Flags::HEAD, req.inner.head.method == Method::HEAD); - self.version = req.inner.head.version; - if self.flags.contains(Flags::KEEPALIVE_ENABLED) { - self.flags.set(Flags::KEEPALIVE, req.keep_alive()); + self.version = req.inner().head.version; + self.ctype = req.inner().head.connection_type(); + if self.ctype == ConnectionType::KeepAlive + && !self.flags.contains(Flags::KEEPALIVE_ENABLED) + { + self.ctype = ConnectionType::Close } match payload { PayloadType::None => self.payload = None, @@ -270,14 +149,35 @@ impl Encoder for Codec { ) -> Result<(), Self::Error> { match item { Message::Item((mut res, length)) => { - self.prepare_te(res.head_mut(), length); - self.encode_response(res.head_mut(), length, dst)?; + // connection status + self.ctype = if let Some(ct) = res.head().ctype { + if ct == ConnectionType::KeepAlive { + self.ctype + } else { + ct + } + } else { + self.ctype + }; + + // encode message + let len = dst.len(); + self.encoder.encode( + dst, + &mut res, + self.flags.contains(Flags::HEAD), + self.version, + length, + self.ctype, + &self.config, + )?; + self.headers_size = (dst.len() - len) as u32; } Message::Chunk(Some(bytes)) => { - self.te.encode(bytes.as_ref(), dst)?; + self.encoder.encode_chunk(bytes.as_ref(), dst)?; } Message::Chunk(None) => { - self.te.encode_eof(dst)?; + self.encoder.encode_eof(dst)?; } } Ok(()) diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index de0df367f..a081a5cf3 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -10,15 +10,16 @@ use client::ClientResponse; use error::ParseError; use http::header::{HeaderName, HeaderValue}; use http::{header, HeaderMap, HttpTryFrom, Method, StatusCode, Uri, Version}; -use message::{ConnectionType, Head}; +use message::ConnectionType; use request::Request; const MAX_BUFFER_SIZE: usize = 131_072; const MAX_HEADERS: usize = 96; /// Incoming messagd decoder -pub(crate) struct MessageDecoder(PhantomData); +pub(crate) struct MessageDecoder(PhantomData); +#[derive(Debug)] /// Incoming request type pub(crate) enum PayloadType { None, @@ -26,13 +27,13 @@ pub(crate) enum PayloadType { Stream(PayloadDecoder), } -impl Default for MessageDecoder { +impl Default for MessageDecoder { fn default() -> Self { MessageDecoder(PhantomData) } } -impl Decoder for MessageDecoder { +impl Decoder for MessageDecoder { type Item = (T, PayloadType); type Error = ParseError; @@ -47,10 +48,8 @@ pub(crate) enum PayloadLength { None, } -pub(crate) trait MessageTypeDecoder: Sized { - fn keep_alive(&mut self); - - fn force_close(&mut self); +pub(crate) trait MessageType: Sized { + fn set_connection_type(&mut self, ctype: Option); fn headers_mut(&mut self) -> &mut HeaderMap; @@ -59,10 +58,9 @@ pub(crate) trait MessageTypeDecoder: Sized { fn set_headers( &mut self, slice: &Bytes, - version: Version, raw_headers: &[HeaderIndex], ) -> Result { - let mut ka = version != Version::HTTP_10; + let mut ka = None; let mut has_upgrade = false; let mut chunked = false; let mut content_length = None; @@ -104,18 +102,18 @@ pub(crate) trait MessageTypeDecoder: Sized { // connection keep-alive state header::CONNECTION => { ka = if let Ok(conn) = value.to_str() { - if version == Version::HTTP_10 - && conn.contains("keep-alive") - { - true + if conn.contains("keep-alive") { + Some(ConnectionType::KeepAlive) + } else if conn.contains("close") { + Some(ConnectionType::Close) + } else if conn.contains("upgrade") { + Some(ConnectionType::Upgrade) } else { - version == Version::HTTP_11 && !(conn - .contains("close") - || conn.contains("upgrade")) + None } } else { - false - } + None + }; } header::UPGRADE => { has_upgrade = true; @@ -136,12 +134,7 @@ pub(crate) trait MessageTypeDecoder: Sized { } } } - - if ka { - self.keep_alive(); - } else { - self.force_close(); - } + self.set_connection_type(ka); // https://tools.ietf.org/html/rfc7230#section-3.3.3 if chunked { @@ -162,17 +155,9 @@ pub(crate) trait MessageTypeDecoder: Sized { } } -impl MessageTypeDecoder for Request { - fn keep_alive(&mut self) { - self.inner_mut() - .head - .set_connection_type(ConnectionType::KeepAlive) - } - - fn force_close(&mut self) { - self.inner_mut() - .head - .set_connection_type(ConnectionType::Close) +impl MessageType for Request { + fn set_connection_type(&mut self, ctype: Option) { + self.inner_mut().head.ctype = ctype; } fn headers_mut(&mut self) -> &mut HeaderMap { @@ -210,8 +195,7 @@ impl MessageTypeDecoder for Request { let mut msg = Request::new(); // convert headers - let len = - msg.set_headers(&src.split_to(len).freeze(), ver, &headers[..h_len])?; + let len = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; // payload decoder let decoder = match len { @@ -243,13 +227,9 @@ impl MessageTypeDecoder for Request { } } -impl MessageTypeDecoder for ClientResponse { - fn keep_alive(&mut self) { - self.head.set_connection_type(ConnectionType::KeepAlive); - } - - fn force_close(&mut self) { - self.head.set_connection_type(ConnectionType::Close); +impl MessageType for ClientResponse { + fn set_connection_type(&mut self, ctype: Option) { + self.head.ctype = ctype; } fn headers_mut(&mut self) -> &mut HeaderMap { @@ -286,8 +266,7 @@ impl MessageTypeDecoder for ClientResponse { let mut msg = ClientResponse::new(); // convert headers - let len = - msg.set_headers(&src.split_to(len).freeze(), ver, &headers[..h_len])?; + let len = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; // message payload let decoder = if let PayloadLength::Payload(pl) = len { @@ -634,6 +613,7 @@ mod tests { use super::*; use error::ParseError; use httpmessage::HttpMessage; + use message::Head; impl PayloadType { fn unwrap(self) -> PayloadDecoder { @@ -886,7 +866,7 @@ mod tests { let mut buf = BytesMut::from("GET /test HTTP/1.0\r\n\r\n"); let req = parse_ready!(&mut buf); - assert!(!req.keep_alive()); + assert_eq!(req.head().connection_type(), ConnectionType::Close); } #[test] @@ -894,7 +874,7 @@ mod tests { let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); let req = parse_ready!(&mut buf); - assert!(req.keep_alive()); + assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); } #[test] @@ -905,7 +885,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert!(!req.keep_alive()); + assert_eq!(req.inner().head.ctype, Some(ConnectionType::Close)); } #[test] @@ -916,7 +896,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert!(!req.keep_alive()); + assert_eq!(req.inner().head.ctype, Some(ConnectionType::Close)); } #[test] @@ -927,7 +907,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert!(req.keep_alive()); + assert_eq!(req.inner().head.ctype, Some(ConnectionType::KeepAlive)); } #[test] @@ -938,7 +918,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert!(req.keep_alive()); + assert_eq!(req.inner().head.ctype, Some(ConnectionType::KeepAlive)); } #[test] @@ -949,7 +929,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert!(!req.keep_alive()); + assert_eq!(req.inner().head.connection_type(), ConnectionType::Close); } #[test] @@ -960,7 +940,11 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert!(req.keep_alive()); + assert_eq!(req.inner().head.ctype, None); + assert_eq!( + req.inner().head.connection_type(), + ConnectionType::KeepAlive + ); } #[test] @@ -973,6 +957,7 @@ mod tests { let req = parse_ready!(&mut buf); assert!(req.upgrade()); + assert_eq!(req.inner().head.ctype, Some(ConnectionType::Upgrade)); } #[test] @@ -1070,7 +1055,7 @@ mod tests { ); let mut reader = MessageDecoder::::default(); let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); - assert!(!req.keep_alive()); + assert_eq!(req.inner().head.ctype, Some(ConnectionType::Upgrade)); assert!(req.upgrade()); assert!(pl.is_unhandled()); } diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index 1664af162..f9b4aab35 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -1,40 +1,217 @@ #![allow(unused_imports, unused_variables, dead_code)] use std::fmt::Write as FmtWrite; use std::io::Write; +use std::marker::PhantomData; use std::str::FromStr; use std::{cmp, fmt, io, mem}; -use bytes::{Bytes, BytesMut}; -use http::header::{HeaderValue, ACCEPT_ENCODING, CONTENT_LENGTH}; -use http::{StatusCode, Version}; +use bytes::{BufMut, Bytes, BytesMut}; +use http::header::{ + HeaderValue, ACCEPT_ENCODING, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, +}; +use http::{HeaderMap, StatusCode, Version}; use body::BodyLength; +use config::ServiceConfig; use header::ContentEncoding; +use helpers; use http::Method; -use message::{RequestHead, ResponseHead}; +use message::{ConnectionType, RequestHead, ResponseHead}; use request::Request; use response::Response; +const AVERAGE_HEADER_SIZE: usize = 30; + #[derive(Debug)] -pub(crate) struct ResponseEncoder { - head: bool, +pub(crate) struct MessageEncoder { pub length: BodyLength, pub te: TransferEncoding, + _t: PhantomData, } -impl Default for ResponseEncoder { +impl Default for MessageEncoder { fn default() -> Self { - ResponseEncoder { - head: false, + MessageEncoder { length: BodyLength::None, te: TransferEncoding::empty(), + _t: PhantomData, } } } -impl ResponseEncoder { +pub(crate) trait MessageType: Sized { + fn status(&self) -> Option; + + fn connection_type(&self) -> Option; + + fn headers(&self) -> &HeaderMap; + + fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()>; + + fn encode_headers( + &mut self, + dst: &mut BytesMut, + version: Version, + mut length: BodyLength, + ctype: ConnectionType, + config: &ServiceConfig, + ) -> io::Result<()> { + let mut skip_len = length != BodyLength::Stream; + + // Content length + if let Some(status) = self.status() { + match status { + StatusCode::NO_CONTENT + | StatusCode::CONTINUE + | StatusCode::PROCESSING => length = BodyLength::None, + StatusCode::SWITCHING_PROTOCOLS => { + skip_len = true; + length = BodyLength::Stream; + } + _ => (), + } + } + match length { + BodyLength::Chunked => { + dst.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") + } + BodyLength::Empty => { + dst.extend_from_slice(b"\r\ncontent-length: 0\r\n"); + } + BodyLength::Sized(len) => helpers::write_content_length(len, dst), + BodyLength::Sized64(len) => { + dst.extend_from_slice(b"\r\ncontent-length: "); + write!(dst.writer(), "{}", len)?; + dst.extend_from_slice(b"\r\n"); + } + BodyLength::None | BodyLength::Stream => dst.extend_from_slice(b"\r\n"), + } + + // Connection + match ctype { + ConnectionType::Upgrade => dst.extend_from_slice(b"connection: upgrade\r\n"), + ConnectionType::KeepAlive if version < Version::HTTP_11 => { + dst.extend_from_slice(b"connection: keep-alive\r\n") + } + ConnectionType::Close if version >= Version::HTTP_11 => { + dst.extend_from_slice(b"connection: close\r\n") + } + _ => (), + } + + // write headers + let mut pos = 0; + let mut has_date = false; + let mut remaining = dst.remaining_mut(); + let mut buf = unsafe { &mut *(dst.bytes_mut() as *mut [u8]) }; + for (key, value) in self.headers() { + match key { + &CONNECTION => continue, + &TRANSFER_ENCODING | &CONTENT_LENGTH if skip_len => continue, + &DATE => { + has_date = true; + } + _ => (), + } + + let v = value.as_ref(); + let k = key.as_str().as_bytes(); + let len = k.len() + v.len() + 4; + if len > remaining { + unsafe { + dst.advance_mut(pos); + } + pos = 0; + dst.reserve(len); + remaining = dst.remaining_mut(); + unsafe { + buf = &mut *(dst.bytes_mut() as *mut _); + } + } + + buf[pos..pos + k.len()].copy_from_slice(k); + pos += k.len(); + buf[pos..pos + 2].copy_from_slice(b": "); + pos += 2; + buf[pos..pos + v.len()].copy_from_slice(v); + pos += v.len(); + buf[pos..pos + 2].copy_from_slice(b"\r\n"); + pos += 2; + remaining -= len; + } + unsafe { + dst.advance_mut(pos); + } + + // optimized date header, set_date writes \r\n + if !has_date { + config.set_date(dst); + } else { + // msg eof + dst.extend_from_slice(b"\r\n"); + } + + Ok(()) + } +} + +impl MessageType for Response<()> { + fn status(&self) -> Option { + Some(self.head().status) + } + + fn connection_type(&self) -> Option { + self.head().ctype + } + + fn headers(&self) -> &HeaderMap { + &self.head().headers + } + + fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> { + let head = self.head(); + let reason = head.reason().as_bytes(); + dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE + reason.len()); + + // status line + helpers::write_status_line(head.version, head.status.as_u16(), dst); + dst.extend_from_slice(reason); + Ok(()) + } +} + +impl MessageType for RequestHead { + fn status(&self) -> Option { + None + } + + fn connection_type(&self) -> Option { + self.ctype + } + + fn headers(&self) -> &HeaderMap { + &self.headers + } + + fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> { + write!( + Writer(dst), + "{} {} {}", + self.method, + self.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"), + match self.version { + Version::HTTP_09 => "HTTP/0.9", + Version::HTTP_10 => "HTTP/1.0", + Version::HTTP_11 => "HTTP/1.1", + Version::HTTP_2 => "HTTP/2.0", + } + ).map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + } +} + +impl MessageEncoder { /// Encode message - pub fn encode(&mut self, msg: &[u8], buf: &mut BytesMut) -> io::Result { + pub fn encode_chunk(&mut self, msg: &[u8], buf: &mut BytesMut) -> io::Result { self.te.encode(msg, buf) } @@ -43,59 +220,32 @@ impl ResponseEncoder { self.te.encode_eof(buf) } - pub fn update( + pub fn encode( &mut self, - resp: &mut ResponseHead, + dst: &mut BytesMut, + message: &mut T, head: bool, version: Version, length: BodyLength, - ) { - self.head = head; - let transfer = match length { - BodyLength::Empty => TransferEncoding::empty(), - BodyLength::Sized(len) => TransferEncoding::length(len as u64), - BodyLength::Sized64(len) => TransferEncoding::length(len), - BodyLength::Chunked => TransferEncoding::chunked(), - BodyLength::Stream => TransferEncoding::eof(), - BodyLength::None => TransferEncoding::length(0), - }; - // check for head response - if !self.head { - self.te = transfer; + ctype: ConnectionType, + config: &ServiceConfig, + ) -> io::Result<()> { + // transfer encoding + if !head { + self.te = match length { + BodyLength::Empty => TransferEncoding::empty(), + BodyLength::Sized(len) => TransferEncoding::length(len as u64), + BodyLength::Sized64(len) => TransferEncoding::length(len), + BodyLength::Chunked => TransferEncoding::chunked(), + BodyLength::Stream => TransferEncoding::eof(), + BodyLength::None => TransferEncoding::empty(), + }; + } else { + self.te = TransferEncoding::empty(); } - } -} -#[derive(Debug)] -pub(crate) struct RequestEncoder { - head: bool, - pub length: BodyLength, - pub te: TransferEncoding, -} - -impl Default for RequestEncoder { - fn default() -> Self { - RequestEncoder { - head: false, - length: BodyLength::None, - te: TransferEncoding::empty(), - } - } -} - -impl RequestEncoder { - /// Encode message - pub fn encode(&mut self, msg: &[u8], buf: &mut BytesMut) -> io::Result { - self.te.encode(msg, buf) - } - - /// Encode eof - pub fn encode_eof(&mut self, buf: &mut BytesMut) -> io::Result<()> { - self.te.encode_eof(buf) - } - - pub fn update(&mut self, resp: &mut RequestHead, head: bool, version: Version) { - self.head = head; + message.encode_status(dst)?; + message.encode_headers(dst, version, length, ctype, config) } } @@ -123,7 +273,7 @@ impl TransferEncoding { #[inline] pub fn empty() -> TransferEncoding { TransferEncoding { - kind: TransferEncodingKind::Eof, + kind: TransferEncodingKind::Length(0), } } diff --git a/src/message.rs b/src/message.rs index e1059fa48..d3d52e2f9 100644 --- a/src/message.rs +++ b/src/message.rs @@ -39,12 +39,13 @@ pub trait Head: Default + 'static { fn pool() -> &'static MessagePool; } +#[derive(Debug)] pub struct RequestHead { pub uri: Uri, pub method: Method, pub version: Version, pub headers: HeaderMap, - ctype: Option, + pub ctype: Option, } impl Default for RequestHead { @@ -72,7 +73,7 @@ impl Head for RequestHead { fn connection_type(&self) -> ConnectionType { if let Some(ct) = self.ctype { ct - } else if self.version <= Version::HTTP_11 { + } else if self.version < Version::HTTP_11 { ConnectionType::Close } else { ConnectionType::KeepAlive @@ -84,6 +85,7 @@ impl Head for RequestHead { } } +#[derive(Debug)] pub struct ResponseHead { pub version: Version, pub status: StatusCode, @@ -118,7 +120,7 @@ impl Head for ResponseHead { fn connection_type(&self) -> ConnectionType { if let Some(ct) = self.ctype { ct - } else if self.version <= Version::HTTP_11 { + } else if self.version < Version::HTTP_11 { ConnectionType::Close } else { ConnectionType::KeepAlive diff --git a/src/request.rs b/src/request.rs index d529c09f9..248555e60 100644 --- a/src/request.rs +++ b/src/request.rs @@ -8,7 +8,7 @@ use extensions::Extensions; use httpmessage::HttpMessage; use payload::Payload; -use message::{Head, Message, MessagePool, RequestHead}; +use message::{Message, MessagePool, RequestHead}; /// Request pub struct Request { @@ -67,6 +67,19 @@ impl Request { Rc::get_mut(&mut self.inner).expect("Multiple copies exist") } + #[inline] + /// Http message part of the request + pub fn head(&self) -> &RequestHead { + &self.inner.as_ref().head + } + + #[inline] + #[doc(hidden)] + /// Mutable reference to a http message part of the request + pub fn head_mut(&mut self) -> &mut RequestHead { + &mut self.inner_mut().head + } + /// Request's uri. #[inline] pub fn uri(&self) -> &Uri { @@ -109,12 +122,6 @@ impl Request { &mut self.inner_mut().head.headers } - /// Checks if a connection should be kept alive. - #[inline] - pub fn keep_alive(&self) -> bool { - self.inner().head.keep_alive() - } - /// Request extensions #[inline] pub fn extensions(&self) -> Ref { diff --git a/src/response.rs b/src/response.rs index f2ed72925..bc730718d 100644 --- a/src/response.rs +++ b/src/response.rs @@ -85,7 +85,14 @@ impl Response { } #[inline] - pub(crate) fn head_mut(&mut self) -> &mut ResponseHead { + /// Http message part of the response + pub fn head(&self) -> &ResponseHead { + &self.0.as_ref().head + } + + #[inline] + /// Mutable reference to a http message part of the response + pub fn head_mut(&mut self) -> &mut ResponseHead { &mut self.0.as_mut().head } @@ -314,7 +321,7 @@ impl ResponseBuilder { self } - /// Set a header. + /// Append a header to existing headers. /// /// ```rust,ignore /// # extern crate actix_web; @@ -347,6 +354,39 @@ impl ResponseBuilder { self } + /// Set a header. + /// + /// ```rust,ignore + /// # extern crate actix_web; + /// use actix_web::{http, Request, Response}; + /// + /// fn index(req: HttpRequest) -> Response { + /// Response::Ok() + /// .set_header("X-TEST", "value") + /// .set_header(http::header::CONTENT_TYPE, "application/json") + /// .finish() + /// } + /// fn main() {} + /// ``` + pub fn set_header(&mut self, key: K, value: V) -> &mut Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + if let Some(parts) = parts(&mut self.response, &self.err) { + match HeaderName::try_from(key) { + Ok(key) => match value.try_into() { + Ok(value) => { + parts.head.headers.insert(key, value); + } + Err(e) => self.err = Some(e.into()), + }, + Err(e) => self.err = Some(e.into()), + }; + } + self + } + /// Set the custom reason for the response. #[inline] pub fn reason(&mut self, reason: &'static str) -> &mut Self { @@ -367,11 +407,14 @@ impl ResponseBuilder { /// Set connection type to Upgrade #[inline] - pub fn upgrade(&mut self) -> &mut Self { + pub fn upgrade(&mut self, value: V) -> &mut Self + where + V: IntoHeaderValue, + { if let Some(parts) = parts(&mut self.response, &self.err) { parts.head.set_connection_type(ConnectionType::Upgrade); } - self + self.set_header(header::UPGRADE, value) } /// Force close connection, even if it is marked as keep-alive @@ -880,8 +923,14 @@ mod tests { #[test] fn test_upgrade() { - let resp = Response::build(StatusCode::OK).upgrade().finish(); - assert!(resp.upgrade()) + let resp = Response::build(StatusCode::OK) + .upgrade("websocket") + .finish(); + assert!(resp.upgrade()); + assert_eq!( + resp.headers().get(header::UPGRADE).unwrap(), + HeaderValue::from_static("websocket") + ); } #[test] diff --git a/src/test.rs b/src/test.rs index 9e14129b6..8f90246b4 100644 --- a/src/test.rs +++ b/src/test.rs @@ -443,7 +443,7 @@ where ) -> Result, ws::ClientError> { let url = self.url(path); self.rt - .block_on(ws::Client::default().call(ws::Connect::new(url))) + .block_on(lazy(|| ws::Client::default().call(ws::Connect::new(url)))) } /// Connect to a websocket server diff --git a/src/ws/mod.rs b/src/ws/mod.rs index 5c86d8c49..f1c91714a 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -183,8 +183,7 @@ pub fn handshake_response(req: &Request) -> ResponseBuilder { }; Response::build(StatusCode::SWITCHING_PROTOCOLS) - .upgrade() - .header(header::UPGRADE, "websocket") + .upgrade("websocket") .header(header::TRANSFER_ENCODING, "chunked") .header(header::SEC_WEBSOCKET_ACCEPT, key.as_str()) .take() diff --git a/tests/test_server.rs b/tests/test_server.rs index a01af4f0d..c6e03e285 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -89,7 +89,9 @@ fn test_content_length() { { for i in 0..4 { - let req = client::ClientRequest::get(srv.url("/")).finish().unwrap(); + let req = client::ClientRequest::get(srv.url(&format!("/{}", i))) + .finish() + .unwrap(); let response = srv.send_request(req).unwrap(); assert_eq!(response.headers().get(&header), None); diff --git a/tests/test_ws.rs b/tests/test_ws.rs index 21f635129..22ce3ca29 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -13,7 +13,7 @@ use actix_net::service::NewServiceExt; use actix_net::stream::TakeItem; use actix_web::ws as web_ws; use bytes::{Bytes, BytesMut}; -use futures::future::{ok, Either}; +use futures::future::{lazy, ok, Either}; use futures::{Future, Sink, Stream}; use actix_http::{h1, test, ws, ResponseError, SendResponse, ServiceConfig}; @@ -81,8 +81,9 @@ fn test_simple() { { let url = srv.url("/"); - let (reader, mut writer) = - srv.block_on(web_ws::Client::new(url).connect()).unwrap(); + let (reader, mut writer) = srv + .block_on(lazy(|| web_ws::Client::new(url).connect())) + .unwrap(); writer.text("text"); let (item, reader) = srv.block_on(reader.into_future()).unwrap(); From 6b60c9e2302abb1a40d06af271a08c7cf19a1cc8 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 19 Nov 2018 16:11:58 -0800 Subject: [PATCH 082/427] add debug impl for H1ServiceResult --- src/h1/mod.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/h1/mod.rs b/src/h1/mod.rs index 395d66199..da80e55e9 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -1,4 +1,6 @@ //! HTTP/1 implementation +use std::fmt; + use actix_net::codec::Framed; use bytes::Bytes; @@ -23,6 +25,20 @@ pub enum H1ServiceResult { Unhandled(Request, Framed), } +impl fmt::Debug for H1ServiceResult { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + H1ServiceResult::Disconnected => write!(f, "H1ServiceResult::Disconnected"), + H1ServiceResult::Shutdown(ref v) => { + write!(f, "H1ServiceResult::Shutdown({:?})", v) + } + H1ServiceResult::Unhandled(ref req, _) => { + write!(f, "H1ServiceResult::Unhandled({:?})", req) + } + } + } +} + #[derive(Debug)] /// Codec message pub enum Message { From e1fc6dea844f0ffe47d12d2b724b93cb6c23479b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 19 Nov 2018 16:39:40 -0800 Subject: [PATCH 083/427] restore execute method --- src/test.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test.rs b/src/test.rs index 8f90246b4..84a959c41 100644 --- a/src/test.rs +++ b/src/test.rs @@ -375,6 +375,14 @@ impl TestServerRuntime { self.rt.block_on(fut) } + /// Execute future on current core + pub fn execute(&mut self, fut: F) -> Result + where + F: Future, + { + self.rt.block_on(fut) + } + /// Construct test server url pub fn addr(&self) -> net::SocketAddr { self.addr From 186d3d727a07bccb6423d36aa9475ece16ef7211 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 20 Nov 2018 10:55:50 -0800 Subject: [PATCH 084/427] add kee-alive tests --- src/h1/dispatcher.rs | 27 +++++---- tests/test_server.rs | 130 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 11 deletions(-) diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index bf0abb046..550d27c24 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -409,7 +409,7 @@ where if self.flags.contains(Flags::SHUTDOWN) { return Err(DispatchError::DisconnectTimeout); } else if self.ka_timer.as_mut().unwrap().deadline() >= self.ka_expire { - // check for any outstanding response processing + // check for any outstanding tasks if self.state.is_empty() && self.framed.is_write_buf_empty() { if self.flags.contains(Flags::STARTED) { trace!("Keep-alive timeout, close connection"); @@ -427,12 +427,16 @@ where } } else { // timeout on first request (slow request) return 408 - trace!("Slow request timeout"); - self.flags.insert(Flags::STARTED | Flags::DISCONNECTED); - let _ = self.send_response( - Response::RequestTimeout().finish().drop_body(), - (), - ); + if !self.flags.contains(Flags::STARTED) { + trace!("Slow request timeout"); + let _ = self.send_response( + Response::RequestTimeout().finish().drop_body(), + (), + ); + } else { + trace!("Keep-alive connection timeout"); + } + self.flags.insert(Flags::STARTED | Flags::SHUTDOWN); self.state = State::None; } } else if let Some(deadline) = self.config.keep_alive_expire() { @@ -493,11 +497,14 @@ where false } // disconnect if keep-alive is not enabled - else if inner.flags.contains(Flags::STARTED) && !inner - .flags - .intersects(Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED) + else if inner.flags.contains(Flags::STARTED) + && !inner.flags.intersects(Flags::KEEPALIVE) { true + } + // disconnect if shutdown + else if inner.flags.contains(Flags::SHUTDOWN) { + true } else { return Ok(Async::NotReady); } diff --git a/tests/test_server.rs b/tests/test_server.rs index c6e03e285..16c65aac1 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -4,7 +4,9 @@ extern crate actix_net; extern crate bytes; extern crate futures; -use std::{io::Read, io::Write, net}; +use std::io::{Read, Write}; +use std::time::Duration; +use std::{net, thread}; use actix_net::service::NewServiceExt; use bytes::Bytes; @@ -62,6 +64,132 @@ fn test_malformed_request() { assert!(data.starts_with("HTTP/1.1 400 Bad Request")); } +#[test] +fn test_keepalive() { + let srv = test::TestServer::with_factory(|| { + h1::H1Service::build() + .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) + .map(|_| ()) + }); + + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); + let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n\r\n"); + let mut data = vec![0; 1024]; + let _ = stream.read(&mut data); + assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + + let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n\r\n"); + let mut data = vec![0; 1024]; + let _ = stream.read(&mut data); + assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); +} + +#[test] +fn test_keepalive_timeout() { + let srv = test::TestServer::with_factory(|| { + h1::H1Service::build() + .keep_alive(1) + .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) + .map(|_| ()) + }); + + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); + let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n\r\n"); + let mut data = vec![0; 1024]; + let _ = stream.read(&mut data); + assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + thread::sleep(Duration::from_millis(1100)); + + let mut data = vec![0; 1024]; + let res = stream.read(&mut data).unwrap(); + assert_eq!(res, 0); +} + +#[test] +fn test_keepalive_close() { + let srv = test::TestServer::with_factory(|| { + h1::H1Service::build() + .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) + .map(|_| ()) + }); + + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); + let _ = + stream.write_all(b"GET /test/tests/test HTTP/1.1\r\nconnection: close\r\n\r\n"); + let mut data = vec![0; 1024]; + let _ = stream.read(&mut data); + assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + + let mut data = vec![0; 1024]; + let res = stream.read(&mut data).unwrap(); + assert_eq!(res, 0); +} + +#[test] +fn test_keepalive_http10_default_close() { + let srv = test::TestServer::with_factory(|| { + h1::H1Service::build() + .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) + .map(|_| ()) + }); + + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); + let _ = stream.write_all(b"GET /test/tests/test HTTP/1.0\r\n\r\n"); + let mut data = vec![0; 1024]; + let _ = stream.read(&mut data); + assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + + let mut data = vec![0; 1024]; + let res = stream.read(&mut data).unwrap(); + assert_eq!(res, 0); +} + +#[test] +fn test_keepalive_http10() { + let srv = test::TestServer::with_factory(|| { + h1::H1Service::build() + .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) + .map(|_| ()) + }); + + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); + let _ = stream + .write_all(b"GET /test/tests/test HTTP/1.0\r\nconnection: keep-alive\r\n\r\n"); + let mut data = vec![0; 1024]; + let _ = stream.read(&mut data); + assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); + let _ = stream.write_all(b"GET /test/tests/test HTTP/1.0\r\n\r\n"); + let mut data = vec![0; 1024]; + let _ = stream.read(&mut data); + assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + + let mut data = vec![0; 1024]; + let res = stream.read(&mut data).unwrap(); + assert_eq!(res, 0); +} + +#[test] +fn test_keepalive_disabled() { + let srv = test::TestServer::with_factory(|| { + h1::H1Service::build() + .keep_alive(KeepAlive::Disabled) + .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) + .map(|_| ()) + }); + + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); + let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n\r\n"); + let mut data = vec![0; 1024]; + let _ = stream.read(&mut data); + assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + + let mut data = vec![0; 1024]; + let res = stream.read(&mut data).unwrap(); + assert_eq!(res, 0); +} + #[test] fn test_content_length() { use actix_http::http::{ From ab3e12f2b4a8a177421120bca0239a9e4cf3d874 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 20 Nov 2018 11:23:05 -0800 Subject: [PATCH 085/427] set server response version --- src/h1/codec.rs | 3 +++ tests/test_server.rs | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/h1/codec.rs b/src/h1/codec.rs index f9f455e5d..3174e78ea 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -149,6 +149,9 @@ impl Encoder for Codec { ) -> Result<(), Self::Error> { match item { Message::Item((mut res, length)) => { + // set response version + res.head_mut().version = self.version; + // connection status self.ctype = if let Some(ct) = res.head().ctype { if ct == ConnectionType::KeepAlive { diff --git a/tests/test_server.rs b/tests/test_server.rs index 16c65aac1..a153e584d 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -137,7 +137,7 @@ fn test_keepalive_http10_default_close() { let _ = stream.write_all(b"GET /test/tests/test HTTP/1.0\r\n\r\n"); let mut data = vec![0; 1024]; let _ = stream.read(&mut data); - assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + assert_eq!(&data[..17], b"HTTP/1.0 200 OK\r\n"); let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); @@ -157,13 +157,13 @@ fn test_keepalive_http10() { .write_all(b"GET /test/tests/test HTTP/1.0\r\nconnection: keep-alive\r\n\r\n"); let mut data = vec![0; 1024]; let _ = stream.read(&mut data); - assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + assert_eq!(&data[..17], b"HTTP/1.0 200 OK\r\n"); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test/tests/test HTTP/1.0\r\n\r\n"); let mut data = vec![0; 1024]; let _ = stream.read(&mut data); - assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + assert_eq!(&data[..17], b"HTTP/1.0 200 OK\r\n"); let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); From 1a322966ff2b25af1213459bd6d62fb9d4b6fcbd Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 21 Nov 2018 07:49:24 -0800 Subject: [PATCH 086/427] handle response errors --- src/body.rs | 40 ++++++++++++++++++++++ src/h1/dispatcher.rs | 14 ++++---- src/response.rs | 81 ++++++++++++++++++++++---------------------- src/service.rs | 4 +-- tests/test_server.rs | 22 ++++++++++++ 5 files changed, 111 insertions(+), 50 deletions(-) diff --git a/src/body.rs b/src/body.rs index 3449448e3..cc4e77af5 100644 --- a/src/body.rs +++ b/src/body.rs @@ -37,6 +37,37 @@ impl MessageBody for () { } } +pub enum ResponseBody { + Body(B), + Other(Body), +} + +impl ResponseBody { + pub fn as_ref(&self) -> Option<&B> { + if let ResponseBody::Body(ref b) = self { + Some(b) + } else { + None + } + } +} + +impl MessageBody for ResponseBody { + fn length(&self) -> BodyLength { + match self { + ResponseBody::Body(ref body) => body.length(), + ResponseBody::Other(ref body) => body.length(), + } + } + + fn poll_next(&mut self) -> Poll, Error> { + match self { + ResponseBody::Body(ref mut body) => body.poll_next(), + ResponseBody::Other(ref mut body) => body.poll_next(), + } + } +} + /// Represents various types of http message body. pub enum Body { /// Empty response. `Content-Length` header is not set. @@ -332,6 +363,15 @@ mod tests { } } + impl ResponseBody { + pub(crate) fn get_ref(&self) -> &[u8] { + match *self { + ResponseBody::Body(ref b) => b.get_ref(), + ResponseBody::Other(ref b) => b.get_ref(), + } + } + } + #[test] fn test_static_str() { assert_eq!(Body::from("").length(), BodyLength::Sized(0)); diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 550d27c24..48c8e710b 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -13,7 +13,7 @@ use tokio_timer::Delay; use error::{ParseError, PayloadError}; use payload::{Payload, PayloadSender, PayloadStatus, PayloadWriter}; -use body::{BodyLength, MessageBody}; +use body::{Body, BodyLength, MessageBody, ResponseBody}; use config::ServiceConfig; use error::DispatchError; use request::Request; @@ -70,7 +70,7 @@ enum DispatcherMessage { enum State { None, ServiceCall(S::Future), - SendPayload(B), + SendPayload(ResponseBody), } impl State { @@ -186,11 +186,11 @@ where } } - fn send_response( + fn send_response( &mut self, message: Response<()>, - body: B1, - ) -> Result, DispatchError> { + body: ResponseBody, + ) -> Result, DispatchError> { self.framed .force_send(Message::Item((message, body.length()))) .map_err(|err| { @@ -217,7 +217,7 @@ where Some(self.handle_request(req)?) } Some(DispatcherMessage::Error(res)) => { - self.send_response(res, ())?; + self.send_response(res, ResponseBody::Other(Body::Empty))?; None } None => None, @@ -431,7 +431,7 @@ where trace!("Slow request timeout"); let _ = self.send_response( Response::RequestTimeout().finish().drop_body(), - (), + ResponseBody::Other(Body::Empty), ); } else { trace!("Keep-alive connection timeout"); diff --git a/src/response.rs b/src/response.rs index bc730718d..ae68189d8 100644 --- a/src/response.rs +++ b/src/response.rs @@ -12,7 +12,7 @@ use http::{Error as HttpError, HeaderMap, HttpTryFrom, StatusCode, Version}; use serde::Serialize; use serde_json; -use body::{Body, BodyStream, MessageBody}; +use body::{Body, BodyStream, MessageBody, ResponseBody}; use error::Error; use header::{Header, IntoHeaderValue}; use message::{ConnectionType, Head, ResponseHead}; @@ -21,7 +21,7 @@ use message::{ConnectionType, Head, ResponseHead}; pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; /// An HTTP Response -pub struct Response(Box, B); +pub struct Response(Box, ResponseBody); impl Response { /// Create http response builder with specific status. @@ -71,6 +71,15 @@ impl Response { cookies: jar, } } + + /// Convert response to response with body + pub fn into_body(self) -> Response { + let b = match self.1 { + ResponseBody::Body(b) => b, + ResponseBody::Other(b) => b, + }; + Response(self.0, ResponseBody::Other(b)) + } } impl Response { @@ -195,23 +204,26 @@ impl Response { /// Get body os this response #[inline] - pub fn body(&self) -> &B { + pub(crate) fn body(&self) -> &ResponseBody { &self.1 } /// Set a body - pub fn set_body(self, body: B2) -> Response { - Response(self.0, body) + pub(crate) fn set_body(self, body: B2) -> Response { + Response(self.0, ResponseBody::Body(body)) } /// Drop request's body - pub fn drop_body(self) -> Response<()> { - Response(self.0, ()) + pub(crate) fn drop_body(self) -> Response<()> { + Response(self.0, ResponseBody::Body(())) } /// Set a body and return previous body value - pub fn replace_body(self, body: B2) -> (Response, B) { - (Response(self.0, body), self.1) + pub(crate) fn replace_body( + self, + body: B2, + ) -> (Response, ResponseBody) { + (Response(self.0, ResponseBody::Body(body)), self.1) } /// Size of response in bytes, excluding HTTP headers @@ -233,7 +245,10 @@ impl Response { } pub(crate) fn from_parts(parts: ResponseParts) -> Response { - Response(Box::new(InnerResponse::from_parts(parts)), Body::Empty) + Response( + Box::new(InnerResponse::from_parts(parts)), + ResponseBody::Body(Body::Empty), + ) } } @@ -250,7 +265,7 @@ impl fmt::Debug for Response { for (key, val) in self.get_ref().head.headers.iter() { let _ = writeln!(f, " {:?}: {:?}", key, val); } - let _ = writeln!(f, " body: {:?}", self.body().length()); + let _ = writeln!(f, " body: {:?}", self.1.length()); res } } @@ -559,11 +574,9 @@ impl ResponseBuilder { /// /// `ResponseBuilder` can not be used after this call. pub fn message_body(&mut self, body: B) -> Response { - let mut error = if let Some(e) = self.err.take() { - Some(Error::from(e)) - } else { - None - }; + if let Some(e) = self.err.take() { + return Response::from(Error::from(e)).into_body(); + } let mut response = self.response.take().expect("cannot reuse response builder"); if let Some(ref jar) = self.cookies { @@ -572,17 +585,12 @@ impl ResponseBuilder { Ok(val) => { let _ = response.head.headers.append(header::SET_COOKIE, val); } - Err(e) => if error.is_none() { - error = Some(Error::from(e)); - }, + Err(e) => return Response::from(Error::from(e)).into_body(), }; } } - if let Some(error) = error { - response.error = Some(error); - } - Response(response, body) + Response(response, ResponseBody::Body(body)) } #[inline] @@ -812,9 +820,12 @@ impl ResponsePool { ) -> Response { if let Some(mut msg) = pool.0.borrow_mut().pop_front() { msg.head.status = status; - Response(msg, body) + Response(msg, ResponseBody::Body(body)) } else { - Response(Box::new(InnerResponse::new(status, pool)), body) + Response( + Box::new(InnerResponse::new(status, pool)), + ResponseBody::Body(body), + ) } } @@ -971,10 +982,7 @@ mod tests { let resp = Response::build(StatusCode::OK).json(vec!["v1", "v2", "v3"]); let ct = resp.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("application/json")); - assert_eq!( - *resp.body(), - Body::from(Bytes::from_static(b"[\"v1\",\"v2\",\"v3\"]")) - ); + assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]"); } #[test] @@ -984,10 +992,7 @@ mod tests { .json(vec!["v1", "v2", "v3"]); let ct = resp.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("text/json")); - assert_eq!( - *resp.body(), - Body::from(Bytes::from_static(b"[\"v1\",\"v2\",\"v3\"]")) - ); + assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]"); } #[test] @@ -995,10 +1000,7 @@ mod tests { let resp = Response::build(StatusCode::OK).json2(&vec!["v1", "v2", "v3"]); let ct = resp.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("application/json")); - assert_eq!( - *resp.body(), - Body::from(Bytes::from_static(b"[\"v1\",\"v2\",\"v3\"]")) - ); + assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]"); } #[test] @@ -1008,10 +1010,7 @@ mod tests { .json2(&vec!["v1", "v2", "v3"]); let ct = resp.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("text/json")); - assert_eq!( - *resp.body(), - Body::from(Bytes::from_static(b"[\"v1\",\"v2\",\"v3\"]")) - ); + assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]"); } #[test] diff --git a/src/service.rs b/src/service.rs index f934305ce..6a31b6bb8 100644 --- a/src/service.rs +++ b/src/service.rs @@ -6,7 +6,7 @@ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, Poll, Sink}; use tokio_io::{AsyncRead, AsyncWrite}; -use body::{BodyLength, MessageBody}; +use body::{BodyLength, MessageBody, ResponseBody}; use error::{Error, ResponseError}; use h1::{Codec, Message}; use response::Response; @@ -174,7 +174,7 @@ where pub struct SendResponseFut { res: Option, BodyLength)>>, - body: Option, + body: Option>, framed: Option>, } diff --git a/tests/test_server.rs b/tests/test_server.rs index a153e584d..300b38a80 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -445,3 +445,25 @@ fn test_body_chunked_implicit() { let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } + +#[test] +fn test_response_http_error_handling() { + let mut srv = test::TestServer::with_factory(|| { + h1::H1Service::new(|_| { + let broken_header = Bytes::from_static(b"\0\0\0"); + ok::<_, ()>( + Response::Ok() + .header(http::header::CONTENT_TYPE, broken_header) + .body(STR), + ) + }).map(|_| ()) + }); + + let req = srv.get().finish().unwrap(); + let response = srv.send_request(req).unwrap(); + assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert!(bytes.is_empty()); +} From 41d68c87d926899cdce05ae3bf9c89c23fa1ac50 Mon Sep 17 00:00:00 2001 From: Ali Shirvani Date: Fri, 23 Nov 2018 07:42:40 +0330 Subject: [PATCH 087/427] hello-world example added. --- examples/hello-world.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 examples/hello-world.rs diff --git a/examples/hello-world.rs b/examples/hello-world.rs new file mode 100644 index 000000000..44e453df4 --- /dev/null +++ b/examples/hello-world.rs @@ -0,0 +1,35 @@ +#[macro_use] +extern crate log; +extern crate env_logger; + +extern crate actix_http; +extern crate actix_net; +extern crate futures; +extern crate http; + +use actix_http::{h1, Response}; +use actix_net::server::Server; +use actix_net::service::NewServiceExt; +use futures::future; +use http::header::{HeaderValue}; +use std::env; + +fn main() { + env::set_var("RUST_LOG", "hello_world=info"); + env_logger::init(); + + Server::new().bind("hello-world", "127.0.0.1:8080", || { + h1::H1Service::build() + .client_timeout(1000) + .client_disconnect(1000) + .server_hostname("localhost") + .finish(|_req| { + info!("{:?}", _req); + let mut res = Response::Ok(); + res.header("x-head", HeaderValue::from_static("dummy value!")); + future::ok::<_, ()>(res.body("Hello world!")) + }) + .map(|_| ()) + }).unwrap().run(); +} + From d5ca6e21e2daef2637b0be028990e7264055436c Mon Sep 17 00:00:00 2001 From: Ali Shirvani Date: Sat, 24 Nov 2018 11:29:14 +0330 Subject: [PATCH 088/427] simple echo server. --- examples/echo.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 examples/echo.rs diff --git a/examples/echo.rs b/examples/echo.rs new file mode 100644 index 000000000..91a3c76ad --- /dev/null +++ b/examples/echo.rs @@ -0,0 +1,42 @@ +#[macro_use] +extern crate log; +extern crate env_logger; + +extern crate actix_http; +extern crate actix_net; +extern crate futures; +extern crate http; +extern crate bytes; + +use actix_http::{h1, Response, Request}; +use bytes::Bytes; +use actix_net::server::Server; +use actix_net::service::NewServiceExt; +use futures::Future; +use http::header::{HeaderValue}; +use actix_http::HttpMessage; +use std::env; + +fn main() { + env::set_var("RUST_LOG", "echo=info"); + env_logger::init(); + + Server::new().bind("echo", "127.0.0.1:8080", || { + h1::H1Service::build() + .client_timeout(1000) + .client_disconnect(1000) + .server_hostname("localhost") + .finish(|_req: Request| { + _req.body() + .limit(512) + .and_then(|bytes: Bytes| { + info!("request body: {:?}", bytes); + let mut res = Response::Ok(); + res.header("x-head", HeaderValue::from_static("dummy value!")); + Ok(res.body(bytes)) + }) + }) + .map(|_| ()) + }).unwrap().run(); +} + From c3c2286e3ac3c1c9ac6914cc430a9a20dd899721 Mon Sep 17 00:00:00 2001 From: Ali Shirvani Date: Sat, 24 Nov 2018 17:07:30 +0330 Subject: [PATCH 089/427] An other hello word example and update sample in README.md --- README.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3cb6f2308..e7205893d 100644 --- a/README.md +++ b/README.md @@ -14,20 +14,23 @@ Actix http ## Example ```rust +// see examples/framed_hello.rs for complete list of used crates. extern crate actix_http; use actix_http::{h1, Response, ServiceConfig}; fn main() { - Server::new() - .bind("app", addr, move || { - IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) // <- create h1 codec - .and_then(TakeItem::new().map_err(|_| ())) // <- read one request - .and_then(|(req, framed): (_, Framed<_, _>)| { // <- send response and close conn - framed - .send(h1::OutMessage::Response(Response::Ok().finish())) - }) - }) - .run(); + env::set_var("RUST_LOG", "framed_hello=info"); + env_logger::init(); + + Server::new().bind("framed_hello", "127.0.0.1:8080", || { + IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) // <- create h1 codec + .and_then(TakeItem::new().map_err(|_| ())) // <- read one request + .and_then(|(_req, _framed): (_, Framed<_, _>)| { // <- send response and close conn + SendResponse::send(_framed, Response::Ok().body("Hello world!")) + .map_err(|_| ()) + .map(|_| ()) + }) + }).unwrap().run(); } ``` From d5b264034299bcaa75f3bd21b4d36dba574c2295 Mon Sep 17 00:00:00 2001 From: Ali Shirvani Date: Sat, 24 Nov 2018 17:08:17 +0330 Subject: [PATCH 090/427] add framed_hello.rs --- examples/framed_hello.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 examples/framed_hello.rs diff --git a/examples/framed_hello.rs b/examples/framed_hello.rs new file mode 100644 index 000000000..76d23d08c --- /dev/null +++ b/examples/framed_hello.rs @@ -0,0 +1,33 @@ +extern crate log; +extern crate env_logger; + +extern crate actix_http; +extern crate actix_net; +extern crate futures; +extern crate http; +extern crate bytes; + +use actix_http::{h1, ServiceConfig, SendResponse, Response}; +use actix_net::framed::IntoFramed; +use actix_net::codec::Framed; +use actix_net::stream::TakeItem; +use actix_net::server::Server; +use actix_net::service::NewServiceExt; +use futures::Future; +use std::env; + +fn main() { + env::set_var("RUST_LOG", "framed_hello=info"); + env_logger::init(); + + Server::new().bind("framed_hello", "127.0.0.1:8080", || { + IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) + .and_then(TakeItem::new().map_err(|_| ())) + .and_then(|(_req, _framed): (_, Framed<_, _>)| { + SendResponse::send(_framed, Response::Ok().body("Hello world!")) + .map_err(|_| ()) + .map(|_| ()) + }) + }).unwrap().run(); +} + From 7a97de3a1e222fa15e8495315dcbcedea11bdfd8 Mon Sep 17 00:00:00 2001 From: Ali Shirvani Date: Sat, 24 Nov 2018 17:17:34 +0330 Subject: [PATCH 091/427] update readme. --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e7205893d..093a0b949 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,10 @@ extern crate actix_http; use actix_http::{h1, Response, ServiceConfig}; fn main() { - env::set_var("RUST_LOG", "framed_hello=info"); - env_logger::init(); - Server::new().bind("framed_hello", "127.0.0.1:8080", || { IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) // <- create h1 codec - .and_then(TakeItem::new().map_err(|_| ())) // <- read one request - .and_then(|(_req, _framed): (_, Framed<_, _>)| { // <- send response and close conn + .and_then(TakeItem::new().map_err(|_| ())) // <- read one request + .and_then(|(_req, _framed): (_, Framed<_, _>)| { // <- send response and close conn SendResponse::send(_framed, Response::Ok().body("Hello world!")) .map_err(|_| ()) .map(|_| ()) From ca1b460924219f0cc2e10bf500094238e953c45e Mon Sep 17 00:00:00 2001 From: Ali Shirvani Date: Sun, 25 Nov 2018 05:48:33 +0330 Subject: [PATCH 092/427] comments aligned. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 093a0b949..be8160968 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ use actix_http::{h1, Response, ServiceConfig}; fn main() { Server::new().bind("framed_hello", "127.0.0.1:8080", || { IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) // <- create h1 codec - .and_then(TakeItem::new().map_err(|_| ())) // <- read one request - .and_then(|(_req, _framed): (_, Framed<_, _>)| { // <- send response and close conn + .and_then(TakeItem::new().map_err(|_| ())) // <- read one request + .and_then(|(_req, _framed): (_, Framed<_, _>)| { // <- send response and close conn SendResponse::send(_framed, Response::Ok().body("Hello world!")) .map_err(|_| ()) .map(|_| ()) From 9c038ee1891afe204b06c2e22dcdc947f6d3469b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 25 Nov 2018 20:14:42 -1000 Subject: [PATCH 093/427] allow to use Uri for client request --- src/client/connector.rs | 6 ++++++ src/client/mod.rs | 2 +- src/client/request.rs | 40 +++++++++++++++++++++++++--------------- src/lib.rs | 3 +++ 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/client/connector.rs b/src/client/connector.rs index 818085214..42cba9dec 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -62,6 +62,12 @@ impl Default for Connector { } impl Connector { + /// Use custom resolver. + pub fn resolver(mut self, resolver: Resolver) -> Self { + self.resolver = resolver;; + self + } + /// Use custom resolver configuration. pub fn resolver_config(mut self, cfg: ResolverConfig, opts: ResolverOpts) -> Self { self.resolver = Resolver::new(cfg, opts); diff --git a/src/client/mod.rs b/src/client/mod.rs index 76c3f8b88..dcc4f5d48 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -13,4 +13,4 @@ pub use self::connection::Connection; pub use self::connector::Connector; pub use self::error::{ConnectorError, InvalidUrlKind, SendRequestError}; pub use self::request::{ClientRequest, ClientRequestBuilder}; -pub use self::response::ClientResponse; +pub use self::response::ClientResponse; \ No newline at end of file diff --git a/src/client/request.rs b/src/client/request.rs index 735ce4937..dd29d7978 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -7,7 +7,6 @@ use bytes::{BufMut, Bytes, BytesMut}; use cookie::{Cookie, CookieJar}; use futures::{Future, Stream}; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; -use urlcrate::Url; use body::{BodyStream, MessageBody}; use error::Error; @@ -63,35 +62,50 @@ impl ClientRequest<()> { } /// Create request builder for `GET` request - pub fn get>(uri: U) -> ClientRequestBuilder { + pub fn get(uri: U) -> ClientRequestBuilder + where + Uri: HttpTryFrom, + { let mut builder = ClientRequest::build(); builder.method(Method::GET).uri(uri); builder } /// Create request builder for `HEAD` request - pub fn head>(uri: U) -> ClientRequestBuilder { + pub fn head(uri: U) -> ClientRequestBuilder + where + Uri: HttpTryFrom, + { let mut builder = ClientRequest::build(); builder.method(Method::HEAD).uri(uri); builder } /// Create request builder for `POST` request - pub fn post>(uri: U) -> ClientRequestBuilder { + pub fn post(uri: U) -> ClientRequestBuilder + where + Uri: HttpTryFrom, + { let mut builder = ClientRequest::build(); builder.method(Method::POST).uri(uri); builder } /// Create request builder for `PUT` request - pub fn put>(uri: U) -> ClientRequestBuilder { + pub fn put(uri: U) -> ClientRequestBuilder + where + Uri: HttpTryFrom, + { let mut builder = ClientRequest::build(); builder.method(Method::PUT).uri(uri); builder } /// Create request builder for `DELETE` request - pub fn delete>(uri: U) -> ClientRequestBuilder { + pub fn delete(uri: U) -> ClientRequestBuilder + where + Uri: HttpTryFrom, + { let mut builder = ClientRequest::build(); builder.method(Method::DELETE).uri(uri); builder @@ -202,15 +216,11 @@ pub struct ClientRequestBuilder { impl ClientRequestBuilder { /// Set HTTP URI of request. #[inline] - pub fn uri>(&mut self, uri: U) -> &mut Self { - match Url::parse(uri.as_ref()) { - Ok(url) => self._uri(url.as_str()), - Err(_) => self._uri(uri.as_ref()), - } - } - - fn _uri(&mut self, url: &str) -> &mut Self { - match Uri::try_from(url) { + pub fn uri(&mut self, uri: U) -> &mut Self + where + Uri: HttpTryFrom, + { + match Uri::try_from(uri) { Ok(uri) => { if let Some(parts) = parts(&mut self.head, &self.err) { parts.uri = uri; diff --git a/src/lib.rs b/src/lib.rs index 5256dd190..4870eb64f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -163,6 +163,9 @@ pub mod http { #[doc(hidden)] pub use modhttp::{uri, Error, HeaderMap, HttpTryFrom, Uri}; + #[doc(hidden)] + pub use modhttp::uri::PathAndQuery; + pub use cookie::{Cookie, CookieBuilder}; /// Various http headers From 397804a786d0a4e8167706b46a4bb08808bd17cd Mon Sep 17 00:00:00 2001 From: Ali Shirvani Date: Wed, 28 Nov 2018 09:15:08 +0330 Subject: [PATCH 094/427] echo example with `impl Future` --- examples/echo2.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 examples/echo2.rs diff --git a/examples/echo2.rs b/examples/echo2.rs new file mode 100644 index 000000000..7d8a428f9 --- /dev/null +++ b/examples/echo2.rs @@ -0,0 +1,47 @@ +#[macro_use] +extern crate log; +extern crate env_logger; + +extern crate actix_http; +extern crate actix_net; +extern crate futures; +extern crate http; +extern crate bytes; + +use actix_http::{h1, Response, Request, Error}; +use bytes::Bytes; +use actix_net::server::Server; +use actix_net::service::NewServiceExt; +use futures::Future; +use http::header::{HeaderValue}; +use actix_http::HttpMessage; +use std::env; + +fn handle_request(_req: Request) -> impl Future{ + _req.body() + .limit(512) + .from_err() + .and_then(|bytes: Bytes| { + info!("request body: {:?}", bytes); + let mut res = Response::Ok(); + res.header("x-head", HeaderValue::from_static("dummy value!")); + Ok(res.body(bytes)) + }) +} + +fn main() { + env::set_var("RUST_LOG", "echo=info"); + env_logger::init(); + + Server::new().bind("echo", "127.0.0.1:8080", || { + h1::H1Service::build() + .client_timeout(1000) + .client_disconnect(1000) + .server_hostname("localhost") + .finish(|_req: Request| { + handle_request(_req) + }) + .map(|_| ()) + }).unwrap().run(); +} + From 4028f6f6fd63cad32636d5f1b461f372302c9af4 Mon Sep 17 00:00:00 2001 From: Ali Shirvani Date: Wed, 28 Nov 2018 09:42:04 +0330 Subject: [PATCH 095/427] http crate removed, cargo fmt --- examples/echo.rs | 34 +++++++++++++--------------- examples/echo2.rs | 49 ++++++++++++++++++---------------------- examples/framed_hello.rs | 31 +++++++++++++------------ examples/hello-world.rs | 30 ++++++++++++------------ src/client/mod.rs | 2 +- 5 files changed, 70 insertions(+), 76 deletions(-) diff --git a/examples/echo.rs b/examples/echo.rs index 91a3c76ad..c98863e52 100644 --- a/examples/echo.rs +++ b/examples/echo.rs @@ -4,39 +4,37 @@ extern crate env_logger; extern crate actix_http; extern crate actix_net; +extern crate bytes; extern crate futures; extern crate http; -extern crate bytes; -use actix_http::{h1, Response, Request}; -use bytes::Bytes; +use actix_http::HttpMessage; +use actix_http::{h1, Request, Response}; use actix_net::server::Server; use actix_net::service::NewServiceExt; +use bytes::Bytes; use futures::Future; -use http::header::{HeaderValue}; -use actix_http::HttpMessage; +use http::header::HeaderValue; use std::env; fn main() { env::set_var("RUST_LOG", "echo=info"); env_logger::init(); - Server::new().bind("echo", "127.0.0.1:8080", || { - h1::H1Service::build() - .client_timeout(1000) - .client_disconnect(1000) - .server_hostname("localhost") - .finish(|_req: Request| { - _req.body() - .limit(512) - .and_then(|bytes: Bytes| { + Server::new() + .bind("echo", "127.0.0.1:8080", || { + h1::H1Service::build() + .client_timeout(1000) + .client_disconnect(1000) + .server_hostname("localhost") + .finish(|_req: Request| { + _req.body().limit(512).and_then(|bytes: Bytes| { info!("request body: {:?}", bytes); let mut res = Response::Ok(); res.header("x-head", HeaderValue::from_static("dummy value!")); Ok(res.body(bytes)) }) - }) - .map(|_| ()) - }).unwrap().run(); + }).map(|_| ()) + }).unwrap() + .run(); } - diff --git a/examples/echo2.rs b/examples/echo2.rs index 7d8a428f9..4c144b433 100644 --- a/examples/echo2.rs +++ b/examples/echo2.rs @@ -4,44 +4,39 @@ extern crate env_logger; extern crate actix_http; extern crate actix_net; -extern crate futures; -extern crate http; extern crate bytes; +extern crate futures; -use actix_http::{h1, Response, Request, Error}; -use bytes::Bytes; +use actix_http::http::HeaderValue; +use actix_http::HttpMessage; +use actix_http::{h1, Error, Request, Response}; use actix_net::server::Server; use actix_net::service::NewServiceExt; +use bytes::Bytes; use futures::Future; -use http::header::{HeaderValue}; -use actix_http::HttpMessage; use std::env; -fn handle_request(_req: Request) -> impl Future{ - _req.body() - .limit(512) - .from_err() - .and_then(|bytes: Bytes| { - info!("request body: {:?}", bytes); - let mut res = Response::Ok(); - res.header("x-head", HeaderValue::from_static("dummy value!")); - Ok(res.body(bytes)) - }) +fn handle_request(_req: Request) -> impl Future { + _req.body().limit(512).from_err().and_then(|bytes: Bytes| { + info!("request body: {:?}", bytes); + let mut res = Response::Ok(); + res.header("x-head", HeaderValue::from_static("dummy value!")); + Ok(res.body(bytes)) + }) } fn main() { env::set_var("RUST_LOG", "echo=info"); env_logger::init(); - Server::new().bind("echo", "127.0.0.1:8080", || { - h1::H1Service::build() - .client_timeout(1000) - .client_disconnect(1000) - .server_hostname("localhost") - .finish(|_req: Request| { - handle_request(_req) - }) - .map(|_| ()) - }).unwrap().run(); + Server::new() + .bind("echo", "127.0.0.1:8080", || { + h1::H1Service::build() + .client_timeout(1000) + .client_disconnect(1000) + .server_hostname("localhost") + .finish(|_req: Request| handle_request(_req)) + .map(|_| ()) + }).unwrap() + .run(); } - diff --git a/examples/framed_hello.rs b/examples/framed_hello.rs index 76d23d08c..0c9175a9e 100644 --- a/examples/framed_hello.rs +++ b/examples/framed_hello.rs @@ -1,18 +1,18 @@ -extern crate log; extern crate env_logger; +extern crate log; extern crate actix_http; extern crate actix_net; +extern crate bytes; extern crate futures; extern crate http; -extern crate bytes; -use actix_http::{h1, ServiceConfig, SendResponse, Response}; -use actix_net::framed::IntoFramed; +use actix_http::{h1, Response, SendResponse, ServiceConfig}; use actix_net::codec::Framed; -use actix_net::stream::TakeItem; +use actix_net::framed::IntoFramed; use actix_net::server::Server; use actix_net::service::NewServiceExt; +use actix_net::stream::TakeItem; use futures::Future; use std::env; @@ -20,14 +20,15 @@ fn main() { env::set_var("RUST_LOG", "framed_hello=info"); env_logger::init(); - Server::new().bind("framed_hello", "127.0.0.1:8080", || { - IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) - .and_then(TakeItem::new().map_err(|_| ())) - .and_then(|(_req, _framed): (_, Framed<_, _>)| { - SendResponse::send(_framed, Response::Ok().body("Hello world!")) - .map_err(|_| ()) - .map(|_| ()) - }) - }).unwrap().run(); + Server::new() + .bind("framed_hello", "127.0.0.1:8080", || { + IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) + .and_then(TakeItem::new().map_err(|_| ())) + .and_then(|(_req, _framed): (_, Framed<_, _>)| { + SendResponse::send(_framed, Response::Ok().body("Hello world!")) + .map_err(|_| ()) + .map(|_| ()) + }) + }).unwrap() + .run(); } - diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 44e453df4..74ff509b3 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -11,25 +11,25 @@ use actix_http::{h1, Response}; use actix_net::server::Server; use actix_net::service::NewServiceExt; use futures::future; -use http::header::{HeaderValue}; +use http::header::HeaderValue; use std::env; fn main() { env::set_var("RUST_LOG", "hello_world=info"); env_logger::init(); - Server::new().bind("hello-world", "127.0.0.1:8080", || { - h1::H1Service::build() - .client_timeout(1000) - .client_disconnect(1000) - .server_hostname("localhost") - .finish(|_req| { - info!("{:?}", _req); - let mut res = Response::Ok(); - res.header("x-head", HeaderValue::from_static("dummy value!")); - future::ok::<_, ()>(res.body("Hello world!")) - }) - .map(|_| ()) - }).unwrap().run(); + Server::new() + .bind("hello-world", "127.0.0.1:8080", || { + h1::H1Service::build() + .client_timeout(1000) + .client_disconnect(1000) + .server_hostname("localhost") + .finish(|_req| { + info!("{:?}", _req); + let mut res = Response::Ok(); + res.header("x-head", HeaderValue::from_static("dummy value!")); + future::ok::<_, ()>(res.body("Hello world!")) + }).map(|_| ()) + }).unwrap() + .run(); } - diff --git a/src/client/mod.rs b/src/client/mod.rs index dcc4f5d48..76c3f8b88 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -13,4 +13,4 @@ pub use self::connection::Connection; pub use self::connector::Connector; pub use self::error::{ConnectorError, InvalidUrlKind, SendRequestError}; pub use self::request::{ClientRequest, ClientRequestBuilder}; -pub use self::response::ClientResponse; \ No newline at end of file +pub use self::response::ClientResponse; From 06387fc778f8d941921127a72f29d9777566b804 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 28 Nov 2018 09:02:31 -1000 Subject: [PATCH 096/427] display parse error for ws client errors --- src/ws/client/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ws/client/error.rs b/src/ws/client/error.rs index 951cd6ac4..729a00ce5 100644 --- a/src/ws/client/error.rs +++ b/src/ws/client/error.rs @@ -37,7 +37,7 @@ pub enum ClientError { #[fail(display = "Http parsing error")] Http(HttpError), /// Response parsing error - #[fail(display = "Response parsing error")] + #[fail(display = "Response parsing error: {}", _0)] ParseError(ParseError), /// Protocol error #[fail(display = "{}", _0)] From d269904fbffe4b24a35508d44796b7e3373f9e36 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 28 Nov 2018 09:10:13 -1000 Subject: [PATCH 097/427] add cause for nested errors --- src/ws/client/error.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ws/client/error.rs b/src/ws/client/error.rs index 729a00ce5..589648eea 100644 --- a/src/ws/client/error.rs +++ b/src/ws/client/error.rs @@ -35,19 +35,19 @@ pub enum ClientError { InvalidChallengeResponse(String, HeaderValue), /// Http parsing error #[fail(display = "Http parsing error")] - Http(HttpError), + Http(#[cause] HttpError), /// Response parsing error #[fail(display = "Response parsing error: {}", _0)] - ParseError(ParseError), + ParseError(#[cause] ParseError), /// Protocol error #[fail(display = "{}", _0)] Protocol(#[cause] ProtocolError), /// Connect error - #[fail(display = "{:?}", _0)] + #[fail(display = "Connector error: {:?}", _0)] Connect(ConnectorError), /// IO Error #[fail(display = "{}", _0)] - Io(io::Error), + Io(#[cause] io::Error), /// "Disconnected" #[fail(display = "Disconnected")] Disconnected, From 5003c00efbdeb8ef76b8137a3d604d7ba4c16d4d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 30 Nov 2018 11:57:57 -0800 Subject: [PATCH 098/427] use new Service and NewService traits --- Cargo.toml | 4 +-- rustfmt.toml | 2 +- src/client/connector.rs | 56 ++++++++++++---------------------------- src/client/pipeline.rs | 2 +- src/client/pool.rs | 11 ++++---- src/client/request.rs | 2 +- src/h1/dispatcher.rs | 14 +++++----- src/h1/service.rs | 34 +++++++++++------------- src/service.rs | 16 +++++------- src/test.rs | 9 +++---- src/ws/client/service.rs | 13 +++++----- src/ws/service.rs | 8 +++--- src/ws/transport.rs | 10 +++---- 13 files changed, 73 insertions(+), 108 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b2c7c0848..b1dd69ac1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,8 +45,8 @@ rust-tls = ["rustls", "actix-net/rust-tls"] [dependencies] actix = "0.7.5" -actix-net = "0.2.3" -#actix-net = { git="https://github.com/actix/actix-net.git" } +#actix-net = "0.3.0" +actix-net = { git="https://github.com/actix/actix-net.git" } base64 = "0.9" bitflags = "1.0" diff --git a/rustfmt.toml b/rustfmt.toml index 4fff285e7..5fcaaca0f 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,5 +1,5 @@ max_width = 89 reorder_imports = true #wrap_comments = true -fn_args_density = "Compressed" +#fn_args_density = "Compressed" #use_small_heuristics = false diff --git a/src/client/connector.rs b/src/client/connector.rs index 42cba9dec..3729ce394 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -134,11 +134,8 @@ impl Connector { /// Finish configuration process and create connector service. pub fn service( self, - ) -> impl Service< - Request = Connect, - Response = impl Connection, - Error = ConnectorError, - > + Clone { + ) -> impl Service + Clone + { #[cfg(not(feature = "ssl"))] { let connector = TimeoutService::new( @@ -216,7 +213,7 @@ mod connect_impl { pub(crate) struct InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { pub(crate) tcp_pool: ConnectionPool, } @@ -224,8 +221,7 @@ mod connect_impl { impl Clone for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service - + Clone, + T: Service + Clone, { fn clone(&self) -> Self { InnerConnector { @@ -234,16 +230,15 @@ mod connect_impl { } } - impl Service for InnerConnector + impl Service for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { - type Request = Connect; type Response = IoConnection; type Error = ConnectorError; type Future = Either< - as Service>::Future, + as Service>::Future, FutureResult, ConnectorError>, >; @@ -251,7 +246,7 @@ mod connect_impl { self.tcp_pool.poll_ready() } - fn call(&mut self, req: Self::Request) -> Self::Future { + fn call(&mut self, req: Connect) -> Self::Future { if req.is_secure() { Either::B(err(ConnectorError::SslIsNotSupported)) } else if let Err(e) = req.validate() { @@ -295,16 +290,8 @@ mod connect_impl { where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service< - Request = Connect, - Response = (Connect, Io1), - Error = ConnectorError, - > + Clone, - T2: Service< - Request = Connect, - Response = (Connect, Io2), - Error = ConnectorError, - > + Clone, + T1: Service + Clone, + T2: Service + Clone, { fn clone(&self) -> Self { InnerConnector { @@ -318,18 +305,9 @@ mod connect_impl { where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service< - Request = Connect, - Response = (Connect, Io1), - Error = ConnectorError, - >, - T2: Service< - Request = Connect, - Response = (Connect, Io2), - Error = ConnectorError, - >, + T1: Service, + T2: Service, { - type Request = Connect; type Response = IoEither, IoConnection>; type Error = ConnectorError; type Future = Either< @@ -344,7 +322,7 @@ mod connect_impl { self.tcp_pool.poll_ready() } - fn call(&mut self, req: Self::Request) -> Self::Future { + fn call(&mut self, req: Connect) -> Self::Future { if let Err(e) = req.validate() { Either::A(err(e)) } else if req.is_secure() { @@ -364,7 +342,7 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseA where Io1: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { fut: as Service>::Future, _t: PhantomData, @@ -372,7 +350,7 @@ mod connect_impl { impl Future for InnerConnectorResponseA where - T: Service, + T: Service, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { @@ -390,7 +368,7 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseB where Io2: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { fut: as Service>::Future, _t: PhantomData, @@ -398,7 +376,7 @@ mod connect_impl { impl Future for InnerConnectorResponseB where - T: Service, + T: Service, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs index e1d8421e9..e75265507 100644 --- a/src/client/pipeline.rs +++ b/src/client/pipeline.rs @@ -21,7 +21,7 @@ pub(crate) fn send_request( connector: &mut T, ) -> impl Future where - T: Service, + T: Service, B: MessageBody, I: Connection, { diff --git a/src/client/pool.rs b/src/client/pool.rs index 44008f346..decf80194 100644 --- a/src/client/pool.rs +++ b/src/client/pool.rs @@ -47,7 +47,7 @@ pub(crate) struct ConnectionPool( impl ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { pub(crate) fn new( connector: T, @@ -83,12 +83,11 @@ where } } -impl Service for ConnectionPool +impl Service for ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { - type Request = Connect; type Response = IoConnection; type Error = ConnectorError; type Future = Either< @@ -100,7 +99,7 @@ where self.0.poll_ready() } - fn call(&mut self, req: Self::Request) -> Self::Future { + fn call(&mut self, req: Connect) -> Self::Future { let key = req.key(); // acquire connection @@ -456,7 +455,7 @@ where impl Future for ConnectorPoolSupport where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, T::Future: 'static, { type Item = (); diff --git a/src/client/request.rs b/src/client/request.rs index dd29d7978..e71c3ffd8 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -177,7 +177,7 @@ where connector: &mut T, ) -> impl Future where - T: Service, + T: Service, I: Connection, { pipeline::send_request(self.head, self.body, connector) diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 48c8e710b..5e2742aa5 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -36,14 +36,14 @@ bitflags! { } /// Dispatcher for HTTP/1.1 protocol -pub struct Dispatcher +pub struct Dispatcher, B: MessageBody> where S::Error: Debug, { inner: Option>, } -struct InnerDispatcher +struct InnerDispatcher, B: MessageBody> where S::Error: Debug, { @@ -67,13 +67,13 @@ enum DispatcherMessage { Error(Response<()>), } -enum State { +enum State, B: MessageBody> { None, ServiceCall(S::Future), SendPayload(ResponseBody), } -impl State { +impl, B: MessageBody> State { fn is_empty(&self) -> bool { if let State::None = self { true @@ -86,7 +86,7 @@ impl State { impl Dispatcher where T: AsyncRead + AsyncWrite, - S: Service>, + S: Service>, S::Error: Debug, B: MessageBody, { @@ -140,7 +140,7 @@ where impl InnerDispatcher where T: AsyncRead + AsyncWrite, - S: Service>, + S: Service>, S::Error: Debug, B: MessageBody, { @@ -463,7 +463,7 @@ where impl Future for Dispatcher where T: AsyncRead + AsyncWrite, - S: Service>, + S: Service>, S::Error: Debug, B: MessageBody, { diff --git a/src/h1/service.rs b/src/h1/service.rs index 0f0452ee7..e21d0fb60 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -27,13 +27,13 @@ pub struct H1Service { impl H1Service where - S: NewService> + Clone, + S: NewService> + Clone, S::Service: Clone, S::Error: Debug, B: MessageBody, { /// Create new `HttpService` instance. - pub fn new>(service: F) -> Self { + pub fn new>(service: F) -> Self { let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0); H1Service { @@ -49,15 +49,14 @@ where } } -impl NewService for H1Service +impl NewService for H1Service where T: AsyncRead + AsyncWrite, - S: NewService> + Clone, + S: NewService> + Clone, S::Service: Clone, S::Error: Debug, B: MessageBody, { - type Request = T; type Response = H1ServiceResult; type Error = DispatchError; type InitError = S::InitError; @@ -89,7 +88,7 @@ pub struct H1ServiceBuilder { impl H1ServiceBuilder where - S: NewService, + S: NewService, S::Service: Clone, S::Error: Debug, { @@ -186,7 +185,7 @@ where pub fn finish(self, service: F) -> H1Service where B: MessageBody, - F: IntoNewService, + F: IntoNewService, { let cfg = ServiceConfig::new( self.keep_alive, @@ -202,7 +201,7 @@ where } #[doc(hidden)] -pub struct H1ServiceResponse { +pub struct H1ServiceResponse, B> { fut: S::Future, cfg: Option, _t: PhantomData<(T, B)>, @@ -211,7 +210,7 @@ pub struct H1ServiceResponse { impl Future for H1ServiceResponse where T: AsyncRead + AsyncWrite, - S: NewService>, + S: NewService>, S::Service: Clone, S::Error: Debug, B: MessageBody, @@ -237,7 +236,7 @@ pub struct H1ServiceHandler { impl H1ServiceHandler where - S: Service> + Clone, + S: Service> + Clone, S::Error: Debug, B: MessageBody, { @@ -250,14 +249,13 @@ where } } -impl Service for H1ServiceHandler +impl Service for H1ServiceHandler where T: AsyncRead + AsyncWrite, - S: Service> + Clone, + S: Service> + Clone, S::Error: Debug, B: MessageBody, { - type Request = T; type Response = H1ServiceResult; type Error = DispatchError; type Future = Dispatcher; @@ -266,7 +264,7 @@ where self.srv.poll_ready().map_err(DispatchError::Service) } - fn call(&mut self, req: Self::Request) -> Self::Future { + fn call(&mut self, req: T) -> Self::Future { Dispatcher::new(req, self.cfg.clone(), self.srv.clone()) } } @@ -290,11 +288,10 @@ where } } -impl NewService for OneRequest +impl NewService for OneRequest where T: AsyncRead + AsyncWrite, { - type Request = T; type Response = (Request, Framed); type Error = ParseError; type InitError = (); @@ -316,11 +313,10 @@ pub struct OneRequestService { _t: PhantomData, } -impl Service for OneRequestService +impl Service for OneRequestService where T: AsyncRead + AsyncWrite, { - type Request = T; type Response = (Request, Framed); type Error = ParseError; type Future = OneRequestServiceResponse; @@ -329,7 +325,7 @@ where Ok(Async::Ready(())) } - fn call(&mut self, req: Self::Request) -> Self::Future { + fn call(&mut self, req: T) -> Self::Future { OneRequestServiceResponse { framed: Some(Framed::new(req, Codec::new(self.config.clone()))), } diff --git a/src/service.rs b/src/service.rs index 6a31b6bb8..aa507acb8 100644 --- a/src/service.rs +++ b/src/service.rs @@ -23,12 +23,11 @@ where } } -impl NewService for SendError +impl NewService)>> for SendError where T: AsyncRead + AsyncWrite, E: ResponseError, { - type Request = Result)>; type Response = R; type Error = (E, Framed); type InitError = (); @@ -40,12 +39,11 @@ where } } -impl Service for SendError +impl Service)>> for SendError where T: AsyncRead + AsyncWrite, E: ResponseError, { - type Request = Result)>; type Response = R; type Error = (E, Framed); type Future = Either)>, SendErrorFut>; @@ -54,7 +52,7 @@ where Ok(Async::Ready(())) } - fn call(&mut self, req: Self::Request) -> Self::Future { + fn call(&mut self, req: Result)>) -> Self::Future { match req { Ok(r) => Either::A(ok(r)), Err((e, framed)) => { @@ -131,12 +129,11 @@ where } } -impl NewService for SendResponse +impl NewService<(Response, Framed)> for SendResponse where T: AsyncRead + AsyncWrite, B: MessageBody, { - type Request = (Response, Framed); type Response = Framed; type Error = Error; type InitError = (); @@ -148,12 +145,11 @@ where } } -impl Service for SendResponse +impl Service<(Response, Framed)> for SendResponse where T: AsyncRead + AsyncWrite, B: MessageBody, { - type Request = (Response, Framed); type Response = Framed; type Error = Error; type Future = SendResponseFut; @@ -162,7 +158,7 @@ where Ok(Async::Ready(())) } - fn call(&mut self, (res, framed): Self::Request) -> Self::Future { + fn call(&mut self, (res, framed): (Response, Framed)) -> Self::Future { let (res, body) = res.replace_body(()); SendResponseFut { res: Some(Message::Item((res, body.length()))), diff --git a/src/test.rs b/src/test.rs index 84a959c41..3d12e344b 100644 --- a/src/test.rs +++ b/src/test.rs @@ -306,8 +306,7 @@ impl TestServer { pub fn with_factory( factory: F, ) -> TestServerRuntime< - impl Service - + Clone, + impl Service + Clone, > { let (tx, rx) = mpsc::channel(); @@ -339,8 +338,8 @@ impl TestServer { } fn new_connector( -) -> impl Service - + Clone { +) -> impl Service + Clone + { #[cfg(feature = "ssl")] { use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; @@ -441,7 +440,7 @@ impl TestServerRuntime { impl TestServerRuntime where - T: Service + Clone, + T: Service + Clone, T::Response: Connection, { /// Connect to websocket server at a given path diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index 94be59f6e..68f8032e6 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -26,7 +26,7 @@ pub type DefaultClient = Client; /// WebSocket's client pub struct Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite, { connector: T, @@ -34,7 +34,7 @@ where impl Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite, { /// Create new websocket's client factory @@ -51,7 +51,7 @@ impl Default for Client { impl Clone for Client where - T: Service + Clone, + T: Service + Clone, T::Response: AsyncRead + AsyncWrite, { fn clone(&self) -> Self { @@ -61,13 +61,12 @@ where } } -impl Service for Client +impl Service for Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite + 'static, T::Future: 'static, { - type Request = Connect; type Response = Framed; type Error = ClientError; type Future = Either< @@ -79,7 +78,7 @@ where self.connector.poll_ready().map_err(ClientError::from) } - fn call(&mut self, mut req: Self::Request) -> Self::Future { + fn call(&mut self, mut req: Connect) -> Self::Future { if let Some(e) = req.err.take() { Either::A(err(e)) } else if let Some(e) = req.http_err.take() { diff --git a/src/ws/service.rs b/src/ws/service.rs index 9cce4d639..118a2244a 100644 --- a/src/ws/service.rs +++ b/src/ws/service.rs @@ -20,8 +20,7 @@ impl Default for VerifyWebSockets { } } -impl NewService for VerifyWebSockets { - type Request = (Request, Framed); +impl NewService<(Request, Framed)> for VerifyWebSockets { type Response = (Request, Framed); type Error = (HandshakeError, Framed); type InitError = (); @@ -33,8 +32,7 @@ impl NewService for VerifyWebSockets { } } -impl Service for VerifyWebSockets { - type Request = (Request, Framed); +impl Service<(Request, Framed)> for VerifyWebSockets { type Response = (Request, Framed); type Error = (HandshakeError, Framed); type Future = FutureResult; @@ -43,7 +41,7 @@ impl Service for VerifyWebSockets { Ok(Async::Ready(())) } - fn call(&mut self, (req, framed): Self::Request) -> Self::Future { + fn call(&mut self, (req, framed): (Request, Framed)) -> Self::Future { match verify_handshake(&req) { Err(e) => Err((e, framed)).into_future(), Ok(_) => Ok((req, framed)).into_future(), diff --git a/src/ws/transport.rs b/src/ws/transport.rs index 102d02b43..8cd79cb03 100644 --- a/src/ws/transport.rs +++ b/src/ws/transport.rs @@ -8,7 +8,7 @@ use super::{Codec, Frame, Message}; pub struct Transport where - S: Service, + S: Service, T: AsyncRead + AsyncWrite, { inner: FramedTransport, @@ -17,17 +17,17 @@ where impl Transport where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Future: 'static, S::Error: 'static, { - pub fn new>(io: T, service: F) -> Self { + pub fn new>(io: T, service: F) -> Self { Transport { inner: FramedTransport::new(Framed::new(io, Codec::new()), service), } } - pub fn with>(framed: Framed, service: F) -> Self { + pub fn with>(framed: Framed, service: F) -> Self { Transport { inner: FramedTransport::new(framed, service), } @@ -37,7 +37,7 @@ where impl Future for Transport where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Future: 'static, S::Error: 'static, { From c0f8bc9e902036ef72b7be105e2fe0a7e8b7c6b3 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 30 Nov 2018 16:04:33 -0800 Subject: [PATCH 099/427] fix ssl support --- src/client/connector.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/connector.rs b/src/client/connector.rs index 3729ce394..9d0841541 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -272,12 +272,12 @@ mod connect_impl { Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, T1: Service< - Request = Connect, + Connect, Response = (Connect, Io1), Error = ConnectorError, >, T2: Service< - Request = Connect, + Connect, Response = (Connect, Io2), Error = ConnectorError, >, @@ -301,7 +301,7 @@ mod connect_impl { } } - impl Service for InnerConnector + impl Service for InnerConnector where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, @@ -344,7 +344,7 @@ mod connect_impl { Io1: AsyncRead + AsyncWrite + 'static, T: Service, { - fut: as Service>::Future, + fut: as Service>::Future, _t: PhantomData, } @@ -370,7 +370,7 @@ mod connect_impl { Io2: AsyncRead + AsyncWrite + 'static, T: Service, { - fut: as Service>::Future, + fut: as Service>::Future, _t: PhantomData, } From e9121025b7abde064124584118c7689a3e5b519e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 6 Dec 2018 14:32:52 -0800 Subject: [PATCH 100/427] convert to 2018 edition --- Cargo.toml | 1 + examples/echo.rs | 6 ++- examples/echo2.rs | 3 +- examples/framed_hello.rs | 3 +- examples/hello-world.rs | 6 ++- src/body.rs | 2 +- src/client/connector.rs | 21 ++++------ src/client/error.rs | 13 ++----- src/client/pipeline.rs | 26 +++++++------ src/client/request.rs | 24 ++++++------ src/client/response.rs | 8 ++-- src/config.rs | 9 +++-- src/error.rs | 7 ++-- src/h1/client.rs | 21 +++++----- src/h1/codec.rs | 19 ++++----- src/h1/decoder.rs | 21 +++++----- src/h1/dispatcher.rs | 22 +++++------ src/h1/encoder.rs | 20 +++++----- src/h1/mod.rs | 2 +- src/h1/service.rs | 21 +++++----- src/header.rs | 9 ++--- src/httpcodes.rs | 3 +- src/httpmessage.rs | 39 ++++++++++++------- src/json.rs | 22 +++++++---- src/lib.rs | 83 +++++++++------------------------------- src/message.rs | 4 +- src/payload.rs | 26 ++++++++----- src/request.rs | 9 ++--- src/response.rs | 11 +++--- src/service.rs | 22 ++++++----- src/test.rs | 14 +++---- src/ws/client/connect.rs | 5 +-- src/ws/client/error.rs | 9 ++--- src/ws/client/service.rs | 11 +++--- src/ws/frame.rs | 7 ++-- src/ws/mod.rs | 47 +++++++++++++++-------- src/ws/service.rs | 4 +- tests/test_client.rs | 3 +- tests/test_server.rs | 18 ++++++--- tests/test_ws.rs | 3 +- 40 files changed, 310 insertions(+), 294 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b1dd69ac1..57ed3f9b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ categories = ["network-programming", "asynchronous", "web-programming::websocket"] license = "MIT/Apache-2.0" exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] +edition = "2018" [package.metadata.docs.rs] features = ["session"] diff --git a/examples/echo.rs b/examples/echo.rs index c98863e52..0453ad6a7 100644 --- a/examples/echo.rs +++ b/examples/echo.rs @@ -34,7 +34,9 @@ fn main() { res.header("x-head", HeaderValue::from_static("dummy value!")); Ok(res.body(bytes)) }) - }).map(|_| ()) - }).unwrap() + }) + .map(|_| ()) + }) + .unwrap() .run(); } diff --git a/examples/echo2.rs b/examples/echo2.rs index 4c144b433..3206ff507 100644 --- a/examples/echo2.rs +++ b/examples/echo2.rs @@ -37,6 +37,7 @@ fn main() { .server_hostname("localhost") .finish(|_req: Request| handle_request(_req)) .map(|_| ()) - }).unwrap() + }) + .unwrap() .run(); } diff --git a/examples/framed_hello.rs b/examples/framed_hello.rs index 0c9175a9e..6c53d27f3 100644 --- a/examples/framed_hello.rs +++ b/examples/framed_hello.rs @@ -29,6 +29,7 @@ fn main() { .map_err(|_| ()) .map(|_| ()) }) - }).unwrap() + }) + .unwrap() .run(); } diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 74ff509b3..b477f191d 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -29,7 +29,9 @@ fn main() { let mut res = Response::Ok(); res.header("x-head", HeaderValue::from_static("dummy value!")); future::ok::<_, ()>(res.body("Hello world!")) - }).map(|_| ()) - }).unwrap() + }) + .map(|_| ()) + }) + .unwrap() .run(); } diff --git a/src/body.rs b/src/body.rs index cc4e77af5..4b71e9bb9 100644 --- a/src/body.rs +++ b/src/body.rs @@ -4,7 +4,7 @@ use std::{fmt, mem}; use bytes::{Bytes, BytesMut}; use futures::{Async, Poll, Stream}; -use error::{Error, PayloadError}; +use crate::error::{Error, PayloadError}; /// Type represent streaming payload pub type PayloadStream = Box>; diff --git a/src/client/connector.rs b/src/client/connector.rs index 9d0841541..74ee6a7e3 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -143,7 +143,8 @@ impl Connector { self.resolver .map_err(ConnectorError::from) .and_then(TcpConnector::default().from_err()), - ).map_err(|e| match e { + ) + .map_err(|e| match e { TimeoutError::Service(e) => e, TimeoutError::Timeout => ConnectorError::Timeout, }); @@ -170,7 +171,8 @@ impl Connector { OpensslConnector::service(self.connector) .map_err(ConnectorError::SslError), ), - ).map_err(|e| match e { + ) + .map_err(|e| match e { TimeoutError::Service(e) => e, TimeoutError::Timeout => ConnectorError::Timeout, }); @@ -180,7 +182,8 @@ impl Connector { self.resolver .map_err(ConnectorError::from) .and_then(TcpConnector::default().from_err()), - ).map_err(|e| match e { + ) + .map_err(|e| match e { TimeoutError::Service(e) => e, TimeoutError::Timeout => ConnectorError::Timeout, }); @@ -271,16 +274,8 @@ mod connect_impl { where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service< - Connect, - Response = (Connect, Io1), - Error = ConnectorError, - >, - T2: Service< - Connect, - Response = (Connect, Io2), - Error = ConnectorError, - >, + T1: Service, + T2: Service, { pub(crate) tcp_pool: ConnectionPool, pub(crate) ssl_pool: ConnectionPool, diff --git a/src/client/error.rs b/src/client/error.rs index 2c4753642..d2a0f38ec 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -1,23 +1,18 @@ use std::io; +use failure::Fail; use trust_dns_resolver::error::ResolveError; #[cfg(feature = "ssl")] use openssl::ssl::Error as SslError; -#[cfg(all( - feature = "tls", - not(any(feature = "ssl", feature = "rust-tls")) -))] +#[cfg(all(feature = "tls", not(any(feature = "ssl", feature = "rust-tls"))))] use native_tls::Error as SslError; -#[cfg(all( - feature = "rust-tls", - not(any(feature = "tls", feature = "ssl")) -))] +#[cfg(all(feature = "rust-tls", not(any(feature = "tls", feature = "ssl"))))] use std::io::Error as SslError; -use error::{Error, ParseError}; +use crate::error::{Error, ParseError}; /// A set of errors that can occur while connecting to an HTTP host #[derive(Fail, Debug)] diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs index e75265507..fc1e53e8f 100644 --- a/src/client/pipeline.rs +++ b/src/client/pipeline.rs @@ -10,10 +10,10 @@ use tokio_io::{AsyncRead, AsyncWrite}; use super::error::{ConnectorError, SendRequestError}; use super::response::ClientResponse; use super::{Connect, Connection}; -use body::{BodyLength, MessageBody, PayloadStream}; -use error::PayloadError; -use h1; -use message::RequestHead; +use crate::body::{BodyLength, MessageBody, PayloadStream}; +use crate::error::PayloadError; +use crate::h1; +use crate::message::RequestHead; pub(crate) fn send_request( head: RequestHead, @@ -174,14 +174,16 @@ impl Stream for Payload { fn poll(&mut self) -> Poll, Self::Error> { match self.framed.as_mut().unwrap().poll()? { Async::NotReady => Ok(Async::NotReady), - Async::Ready(Some(chunk)) => if let Some(chunk) = chunk { - Ok(Async::Ready(Some(chunk))) - } else { - let framed = self.framed.take().unwrap(); - let force_close = framed.get_codec().keepalive(); - release_connection(framed, force_close); - Ok(Async::Ready(None)) - }, + Async::Ready(Some(chunk)) => { + if let Some(chunk) = chunk { + Ok(Async::Ready(Some(chunk))) + } else { + let framed = self.framed.take().unwrap(); + let force_close = framed.get_codec().keepalive(); + release_connection(framed, force_close); + Ok(Async::Ready(None)) + } + } Async::Ready(None) => Ok(Async::Ready(None)), } } diff --git a/src/client/request.rs b/src/client/request.rs index e71c3ffd8..5f294bbd8 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -8,14 +8,14 @@ use cookie::{Cookie, CookieJar}; use futures::{Future, Stream}; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; -use body::{BodyStream, MessageBody}; -use error::Error; -use header::{self, Header, IntoHeaderValue}; -use http::{ +use crate::body::{BodyStream, MessageBody}; +use crate::error::Error; +use crate::header::{self, Header, IntoHeaderValue}; +use crate::http::{ uri, Error as HttpError, HeaderMap, HeaderName, HeaderValue, HttpTryFrom, Method, Uri, Version, }; -use message::{ConnectionType, Head, RequestHead}; +use crate::message::{ConnectionType, Head, RequestHead}; use super::response::ClientResponse; use super::{pipeline, Connect, Connection, ConnectorError, SendRequestError}; @@ -355,14 +355,16 @@ impl ClientRequestBuilder { { if let Some(parts) = parts(&mut self.head, &self.err) { match HeaderName::try_from(key) { - Ok(key) => if !parts.headers.contains_key(&key) { - match value.try_into() { - Ok(value) => { - parts.headers.insert(key, value); + Ok(key) => { + if !parts.headers.contains_key(&key) { + match value.try_into() { + Ok(value) => { + parts.headers.insert(key, value); + } + Err(e) => self.err = Some(e.into()), } - Err(e) => self.err = Some(e.into()), } - }, + } Err(e) => self.err = Some(e.into()), }; } diff --git a/src/client/response.rs b/src/client/response.rs index dc7b13c18..6bfdfc321 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -5,10 +5,10 @@ use bytes::Bytes; use futures::{Async, Poll, Stream}; use http::{HeaderMap, StatusCode, Version}; -use body::PayloadStream; -use error::PayloadError; -use httpmessage::HttpMessage; -use message::{Head, ResponseHead}; +use crate::body::PayloadStream; +use crate::error::PayloadError; +use crate::httpmessage::HttpMessage; +use crate::message::{Head, ResponseHead}; use super::pipeline::Payload; diff --git a/src/config.rs b/src/config.rs index 833bca7f2..661c0901f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,7 @@ use std::{fmt, net}; use bytes::BytesMut; use futures::{future, Future}; +use log::error; use time; use tokio_current_thread::spawn; use tokio_timer::{sleep, Delay}; @@ -268,9 +269,11 @@ impl ServiceConfigBuilder { pub fn server_address(mut self, addr: S) -> Self { match addr.to_socket_addrs() { Err(err) => error!("Can not convert to SocketAddr: {}", err), - Ok(mut addrs) => if let Some(addr) = addrs.next() { - self.addr = addr; - }, + Ok(mut addrs) => { + if let Some(addr) = addrs.next() { + self.addr = addr; + } + } } self } diff --git a/src/error.rs b/src/error.rs index 2e0c2382a..280f9a329 100644 --- a/src/error.rs +++ b/src/error.rs @@ -20,8 +20,8 @@ use tokio_timer::Error as TimerError; // re-exports pub use cookie::ParseError as CookieParseError; -use body::Body; -use response::{Response, ResponseParts}; +use crate::body::Body; +use crate::response::{Response, ResponseParts}; /// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html) /// for actix web operations @@ -186,7 +186,8 @@ impl From for Error { /// Compatibility for `failure::Error` impl ResponseError for failure::Compat where T: fmt::Display + fmt::Debug + Sync + Send + 'static -{} +{ +} impl From for Error { fn from(err: failure::Error) -> Error { diff --git a/src/h1/client.rs b/src/h1/client.rs index 7704ba97a..f547983fa 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -1,22 +1,23 @@ #![allow(unused_imports, unused_variables, dead_code)] use std::io::{self, Write}; +use bitflags::bitflags; use bytes::{BufMut, Bytes, BytesMut}; +use http::header::{ + HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE, +}; +use http::{Method, Version}; use tokio_codec::{Decoder, Encoder}; use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; use super::{decoder, encoder}; use super::{Message, MessageType}; -use body::BodyLength; -use client::ClientResponse; -use config::ServiceConfig; -use error::{ParseError, PayloadError}; -use helpers; -use http::header::{ - HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE, -}; -use http::{Method, Version}; -use message::{ConnectionType, Head, MessagePool, RequestHead}; +use crate::body::BodyLength; +use crate::client::ClientResponse; +use crate::config::ServiceConfig; +use crate::error::{ParseError, PayloadError}; +use crate::helpers; +use crate::message::{ConnectionType, Head, MessagePool, RequestHead}; bitflags! { struct Flags: u8 { diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 3174e78ea..54c1ce2e6 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -2,21 +2,22 @@ use std::fmt; use std::io::{self, Write}; +use bitflags::bitflags; use bytes::{BufMut, Bytes, BytesMut}; +use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; +use http::{Method, StatusCode, Version}; use tokio_codec::{Decoder, Encoder}; use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; use super::{decoder, encoder}; use super::{Message, MessageType}; -use body::BodyLength; -use config::ServiceConfig; -use error::ParseError; -use helpers; -use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; -use http::{Method, StatusCode, Version}; -use message::{ConnectionType, Head, ResponseHead}; -use request::Request; -use response::Response; +use crate::body::BodyLength; +use crate::config::ServiceConfig; +use crate::error::ParseError; +use crate::helpers; +use crate::message::{ConnectionType, Head, ResponseHead}; +use crate::request::Request; +use crate::response::Response; bitflags! { struct Flags: u8 { diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index a081a5cf3..26b28440b 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -3,15 +3,16 @@ use std::{io, mem}; use bytes::{Bytes, BytesMut}; use futures::{Async, Poll}; -use httparse; -use tokio_codec::Decoder; - -use client::ClientResponse; -use error::ParseError; use http::header::{HeaderName, HeaderValue}; use http::{header, HeaderMap, HttpTryFrom, Method, StatusCode, Uri, Version}; -use message::ConnectionType; -use request::Request; +use httparse; +use log::{debug, error, trace}; +use tokio_codec::Decoder; + +use crate::client::ClientResponse; +use crate::error::ParseError; +use crate::message::ConnectionType; +use crate::request::Request; const MAX_BUFFER_SIZE: usize = 131_072; const MAX_HEADERS: usize = 96; @@ -825,13 +826,13 @@ mod tests { let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); let mut reader = MessageDecoder::::default(); - assert!{ reader.decode(&mut buf).unwrap().is_none() } + assert! { reader.decode(&mut buf).unwrap().is_none() } buf.extend(b"t"); - assert!{ reader.decode(&mut buf).unwrap().is_none() } + assert! { reader.decode(&mut buf).unwrap().is_none() } buf.extend(b"es"); - assert!{ reader.decode(&mut buf).unwrap().is_none() } + assert! { reader.decode(&mut buf).unwrap().is_none() } buf.extend(b"t: value\r\n\r\n"); let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 5e2742aa5..59b419c3e 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -5,19 +5,19 @@ use std::time::Instant; use actix_net::codec::Framed; use actix_net::service::Service; - -use futures::{Async, Future, Poll, Sink, Stream}; +use bitflags::bitflags; +use futures::{try_ready, Async, Future, Poll, Sink, Stream}; +use log::{debug, error, trace}; use tokio_io::{AsyncRead, AsyncWrite}; use tokio_timer::Delay; -use error::{ParseError, PayloadError}; -use payload::{Payload, PayloadSender, PayloadStatus, PayloadWriter}; - -use body::{Body, BodyLength, MessageBody, ResponseBody}; -use config::ServiceConfig; -use error::DispatchError; -use request::Request; -use response::Response; +use crate::body::{Body, BodyLength, MessageBody, ResponseBody}; +use crate::config::ServiceConfig; +use crate::error::DispatchError; +use crate::error::{ParseError, PayloadError}; +use crate::payload::{Payload, PayloadSender, PayloadStatus, PayloadWriter}; +use crate::request::Request; +use crate::response::Response; use super::codec::Codec; use super::{H1ServiceResult, Message, MessageType}; @@ -224,7 +224,7 @@ where }, State::ServiceCall(mut fut) => { match fut.poll().map_err(DispatchError::Service)? { - Async::Ready(mut res) => { + Async::Ready(res) => { let (res, body) = res.replace_body(()); Some(self.send_response(res, body)?) } diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index f9b4aab35..92456520a 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -9,16 +9,15 @@ use bytes::{BufMut, Bytes, BytesMut}; use http::header::{ HeaderValue, ACCEPT_ENCODING, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, }; -use http::{HeaderMap, StatusCode, Version}; +use http::{HeaderMap, Method, StatusCode, Version}; -use body::BodyLength; -use config::ServiceConfig; -use header::ContentEncoding; -use helpers; -use http::Method; -use message::{ConnectionType, RequestHead, ResponseHead}; -use request::Request; -use response::Response; +use crate::body::BodyLength; +use crate::config::ServiceConfig; +use crate::header::ContentEncoding; +use crate::helpers; +use crate::message::{ConnectionType, RequestHead, ResponseHead}; +use crate::request::Request; +use crate::response::Response; const AVERAGE_HEADER_SIZE: usize = 30; @@ -205,7 +204,8 @@ impl MessageType for RequestHead { Version::HTTP_11 => "HTTP/1.1", Version::HTTP_2 => "HTTP/2.0", } - ).map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + ) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) } } diff --git a/src/h1/mod.rs b/src/h1/mod.rs index da80e55e9..461ecb959 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -16,7 +16,7 @@ pub use self::codec::Codec; pub use self::dispatcher::Dispatcher; pub use self::service::{H1Service, H1ServiceHandler, OneRequest}; -use request::Request; +use crate::request::Request; /// H1 service response type pub enum H1ServiceResult { diff --git a/src/h1/service.rs b/src/h1/service.rs index e21d0fb60..1a5a587c0 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -5,14 +5,15 @@ use std::net; use actix_net::codec::Framed; use actix_net::service::{IntoNewService, NewService, Service}; use futures::future::{ok, FutureResult}; -use futures::{Async, Future, Poll, Stream}; +use futures::{try_ready, Async, Future, Poll, Stream}; +use log::error; use tokio_io::{AsyncRead, AsyncWrite}; -use body::MessageBody; -use config::{KeepAlive, ServiceConfig}; -use error::{DispatchError, ParseError}; -use request::Request; -use response::Response; +use crate::body::MessageBody; +use crate::config::{KeepAlive, ServiceConfig}; +use crate::error::{DispatchError, ParseError}; +use crate::request::Request; +use crate::response::Response; use super::codec::Codec; use super::dispatcher::Dispatcher; @@ -174,9 +175,11 @@ where pub fn server_address(mut self, addr: U) -> Self { match addr.to_socket_addrs() { Err(err) => error!("Can not convert to SocketAddr: {}", err), - Ok(mut addrs) => if let Some(addr) = addrs.next() { - self.addr = addr; - }, + Ok(mut addrs) => { + if let Some(addr) = addrs.next() { + self.addr = addr; + } + } } self } diff --git a/src/header.rs b/src/header.rs index b1ba6524a..6276dd4f4 100644 --- a/src/header.rs +++ b/src/header.rs @@ -1,13 +1,12 @@ //! Various http headers use bytes::Bytes; +pub use http::header::*; +use http::Error as HttpError; use mime::Mime; -use modhttp::Error as HttpError; -pub use modhttp::header::*; - -use error::ParseError; -use httpmessage::HttpMessage; +use crate::error::ParseError; +use crate::httpmessage::HttpMessage; #[doc(hidden)] /// A trait for any object that will represent a header field and value. diff --git a/src/httpcodes.rs b/src/httpcodes.rs index 7d42a1cc3..80722734a 100644 --- a/src/httpcodes.rs +++ b/src/httpcodes.rs @@ -1,7 +1,8 @@ //! Basic http responses #![allow(non_upper_case_globals)] use http::StatusCode; -use response::{Response, ResponseBuilder}; + +use crate::response::{Response, ResponseBuilder}; macro_rules! STATIC_RESP { ($name:ident, $status:expr) => { diff --git a/src/httpmessage.rs b/src/httpmessage.rs index f68f3650b..e239a7337 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -10,11 +10,11 @@ use serde::de::DeserializeOwned; use serde_urlencoded; use std::str; -use error::{ +use crate::error::{ ContentTypeError, ParseError, PayloadError, ReadlinesError, UrlencodedError, }; -use header::Header; -use json::JsonBody; +use crate::header::Header; +use crate::json::JsonBody; /// Trait that implements general purpose operations on http messages pub trait HttpMessage: Sized { @@ -438,7 +438,8 @@ where body.extend_from_slice(&chunk); Ok(body) } - }).map(|body| body.freeze()), + }) + .map(|body| body.freeze()), )); self.poll() } @@ -546,7 +547,8 @@ where body.extend_from_slice(&chunk); Ok(body) } - }).and_then(move |body| { + }) + .and_then(move |body| { if (encoding as *const Encoding) == UTF_8 { serde_urlencoded::from_bytes::(&body) .map_err(|_| UrlencodedError::Parse) @@ -604,7 +606,8 @@ mod tests { let req = TestRequest::with_header( "content-type", "applicationadfadsfasdflknadsfklnadsfjson", - ).finish(); + ) + .finish(); assert_eq!(Err(ContentTypeError::ParseError), req.mime_type()); } @@ -619,7 +622,8 @@ mod tests { let req = TestRequest::with_header( "content-type", "application/json; charset=ISO-8859-2", - ).finish(); + ) + .finish(); assert_eq!(ISO_8859_2.name(), req.encoding().unwrap().name()); } @@ -631,7 +635,8 @@ mod tests { let req = TestRequest::with_header( "content-type", "application/json; charset=kkkttktk", - ).finish(); + ) + .finish(); assert_eq!( Some(ContentTypeError::UnknownEncoding), req.encoding().err() @@ -651,7 +656,8 @@ mod tests { .header( header::TRANSFER_ENCODING, Bytes::from_static(b"some va\xadscc\xacas0xsdasdlue"), - ).finish(); + ) + .finish(); assert!(req.chunked().is_err()); } @@ -689,7 +695,8 @@ mod tests { let req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "xxxx") + ) + .header(header::CONTENT_LENGTH, "xxxx") .finish(); assert_eq!( req.urlencoded::().poll().err().unwrap(), @@ -699,7 +706,8 @@ mod tests { let req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "1000000") + ) + .header(header::CONTENT_LENGTH, "1000000") .finish(); assert_eq!( req.urlencoded::().poll().err().unwrap(), @@ -720,7 +728,8 @@ mod tests { let req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "11") + ) + .header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) .finish(); @@ -735,7 +744,8 @@ mod tests { let req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8", - ).header(header::CONTENT_LENGTH, "11") + ) + .header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) .finish(); @@ -786,7 +796,8 @@ mod tests { b"Lorem Ipsum is simply dummy text of the printing and typesetting\n\ industry. Lorem Ipsum has been the industry's standard dummy\n\ Contrary to popular belief, Lorem Ipsum is not simply random text.", - )).finish(); + )) + .finish(); let mut r = Readlines::new(&req); match r.poll().ok().unwrap() { Async::Ready(Some(s)) => assert_eq!( diff --git a/src/json.rs b/src/json.rs index e2c99ba3f..0b6ac377f 100644 --- a/src/json.rs +++ b/src/json.rs @@ -6,8 +6,8 @@ use mime; use serde::de::DeserializeOwned; use serde_json; -use error::JsonPayloadError; -use httpmessage::HttpMessage; +use crate::error::JsonPayloadError; +use crate::httpmessage::HttpMessage; /// Request payload json parser that resolves to a deserialized `T` value. /// @@ -124,7 +124,8 @@ impl Future for JsonBod body.extend_from_slice(&chunk); Ok(body) } - }).and_then(|body| Ok(serde_json::from_slice::(&body)?)); + }) + .and_then(|body| Ok(serde_json::from_slice::(&body)?)); self.fut = Some(Box::new(fut)); self.poll() } @@ -170,7 +171,8 @@ mod tests { .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/text"), - ).finish(); + ) + .finish(); let mut json = req.json::(); assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); @@ -178,10 +180,12 @@ mod tests { .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), - ).header( + ) + .header( header::CONTENT_LENGTH, header::HeaderValue::from_static("10000"), - ).finish(); + ) + .finish(); let mut json = req.json::().limit(100); assert_eq!(json.poll().err().unwrap(), JsonPayloadError::Overflow); @@ -189,10 +193,12 @@ mod tests { .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), - ).header( + ) + .header( header::CONTENT_LENGTH, header::HeaderValue::from_static("16"), - ).set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + ) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .finish(); let mut json = req.json::(); diff --git a/src/lib.rs b/src/lib.rs index 4870eb64f..76c3a9679 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,56 +62,12 @@ // #![warn(missing_docs)] #![allow(dead_code)] -extern crate actix; -extern crate actix_net; -#[macro_use] -extern crate log; -extern crate base64; -extern crate byteorder; -extern crate bytes; -extern crate sha1; -extern crate time; -#[macro_use] -extern crate bitflags; -#[macro_use] -extern crate failure; -#[macro_use] -extern crate futures; -extern crate cookie; -extern crate encoding; -extern crate http as modhttp; -extern crate httparse; -extern crate indexmap; -extern crate mime; -extern crate net2; -extern crate percent_encoding; -extern crate rand; -extern crate serde; -extern crate serde_json; -extern crate serde_urlencoded; -extern crate slab; -extern crate tokio; -extern crate tokio_codec; -extern crate tokio_current_thread; -extern crate tokio_io; -extern crate tokio_tcp; -extern crate tokio_timer; -extern crate trust_dns_proto; -extern crate trust_dns_resolver; -extern crate url as urlcrate; - -#[cfg(test)] -#[macro_use] -extern crate serde_derive; - -#[cfg(feature = "ssl")] -extern crate openssl; - pub mod body; pub mod client; mod config; mod extensions; mod header; +mod helpers; mod httpcodes; mod httpmessage; mod json; @@ -123,18 +79,17 @@ mod service; pub mod error; pub mod h1; -pub(crate) mod helpers; pub mod test; pub mod ws; -pub use body::{Body, MessageBody}; -pub use error::{Error, ResponseError, Result}; -pub use extensions::Extensions; -pub use httpmessage::HttpMessage; -pub use request::Request; -pub use response::Response; -pub use service::{SendError, SendResponse}; +pub use self::body::{Body, MessageBody}; pub use self::config::{KeepAlive, ServiceConfig, ServiceConfigBuilder}; +pub use self::error::{Error, ResponseError, Result}; +pub use self::extensions::Extensions; +pub use self::httpmessage::HttpMessage; +pub use self::request::Request; +pub use self::response::Response; +pub use self::service::{SendError, SendResponse}; pub mod dev { //! The `actix-web` prelude for library developers @@ -147,31 +102,31 @@ pub mod dev { //! use actix_http::dev::*; //! ``` - pub use httpmessage::{MessageBody, Readlines, UrlEncoded}; - pub use json::JsonBody; - pub use payload::{Payload, PayloadBuffer}; - pub use response::ResponseBuilder; + pub use crate::httpmessage::{MessageBody, Readlines, UrlEncoded}; + pub use crate::json::JsonBody; + pub use crate::payload::{Payload, PayloadBuffer}; + pub use crate::response::ResponseBuilder; } pub mod http { //! Various HTTP related types // re-exports - pub use modhttp::header::{HeaderName, HeaderValue}; - pub use modhttp::{Method, StatusCode, Version}; + pub use http::header::{HeaderName, HeaderValue}; + pub use http::{Method, StatusCode, Version}; #[doc(hidden)] - pub use modhttp::{uri, Error, HeaderMap, HttpTryFrom, Uri}; + pub use http::{uri, Error, HeaderMap, HttpTryFrom, Uri}; #[doc(hidden)] - pub use modhttp::uri::PathAndQuery; + pub use http::uri::PathAndQuery; pub use cookie::{Cookie, CookieBuilder}; /// Various http headers pub mod header { - pub use header::*; + pub use crate::header::*; } - pub use header::ContentEncoding; - pub use message::ConnectionType; + pub use crate::header::ContentEncoding; + pub use crate::message::ConnectionType; } diff --git a/src/message.rs b/src/message.rs index d3d52e2f9..31d61f63d 100644 --- a/src/message.rs +++ b/src/message.rs @@ -4,8 +4,8 @@ use std::rc::Rc; use http::{HeaderMap, Method, StatusCode, Uri, Version}; -use extensions::Extensions; -use payload::Payload; +use crate::extensions::Extensions; +use crate::payload::Payload; /// Represents various types of connection #[derive(Copy, Clone, PartialEq, Debug)] diff --git a/src/payload.rs b/src/payload.rs index b05924969..37f06d433 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -9,7 +9,7 @@ use std::cmp; use std::collections::VecDeque; use std::rc::{Rc, Weak}; -use error::PayloadError; +use crate::error::PayloadError; /// max buffer size 32k pub(crate) const MAX_BUFFER_SIZE: usize = 32_768; @@ -515,7 +515,8 @@ where .fold(BytesMut::new(), |mut b, c| { b.extend_from_slice(c); b - }).freeze() + }) + .freeze() } } @@ -547,7 +548,8 @@ mod tests { let res: Result<(), ()> = Ok(()); result(res) - })).unwrap(); + })) + .unwrap(); } #[test] @@ -571,7 +573,8 @@ mod tests { let res: Result<(), ()> = Ok(()); result(res) - })).unwrap(); + })) + .unwrap(); } #[test] @@ -588,7 +591,8 @@ mod tests { payload.readany().err().unwrap(); let res: Result<(), ()> = Ok(()); result(res) - })).unwrap(); + })) + .unwrap(); } #[test] @@ -616,7 +620,8 @@ mod tests { let res: Result<(), ()> = Ok(()); result(res) - })).unwrap(); + })) + .unwrap(); } #[test] @@ -649,7 +654,8 @@ mod tests { let res: Result<(), ()> = Ok(()); result(res) - })).unwrap(); + })) + .unwrap(); } #[test] @@ -682,7 +688,8 @@ mod tests { let res: Result<(), ()> = Ok(()); result(res) - })).unwrap(); + })) + .unwrap(); } #[test] @@ -703,6 +710,7 @@ mod tests { let res: Result<(), ()> = Ok(()); result(res) - })).unwrap(); + })) + .unwrap(); } } diff --git a/src/request.rs b/src/request.rs index 248555e60..60ddee19b 100644 --- a/src/request.rs +++ b/src/request.rs @@ -4,11 +4,10 @@ use std::rc::Rc; use http::{header, HeaderMap, Method, Uri, Version}; -use extensions::Extensions; -use httpmessage::HttpMessage; -use payload::Payload; - -use message::{Message, MessagePool, RequestHead}; +use crate::extensions::Extensions; +use crate::httpmessage::HttpMessage; +use crate::message::{Message, MessagePool, RequestHead}; +use crate::payload::Payload; /// Request pub struct Request { diff --git a/src/response.rs b/src/response.rs index ae68189d8..e506cd163 100644 --- a/src/response.rs +++ b/src/response.rs @@ -12,10 +12,10 @@ use http::{Error as HttpError, HeaderMap, HttpTryFrom, StatusCode, Version}; use serde::Serialize; use serde_json; -use body::{Body, BodyStream, MessageBody, ResponseBody}; -use error::Error; -use header::{Header, IntoHeaderValue}; -use message::{ConnectionType, Head, ResponseHead}; +use crate::body::{Body, BodyStream, MessageBody, ResponseBody}; +use crate::error::Error; +use crate::header::{Header, IntoHeaderValue}; +use crate::message::{ConnectionType, Head, ResponseHead}; /// max write buffer size 64k pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; @@ -161,7 +161,8 @@ impl Response { HeaderValue::from_str(&cookie.to_string()) .map(|c| { h.append(header::SET_COOKIE, c); - }).map_err(|e| e.into()) + }) + .map_err(|e| e.into()) } /// Remove all cookies with the given name from this response. Returns diff --git a/src/service.rs b/src/service.rs index aa507acb8..a6a820977 100644 --- a/src/service.rs +++ b/src/service.rs @@ -6,10 +6,10 @@ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, Poll, Sink}; use tokio_io::{AsyncRead, AsyncWrite}; -use body::{BodyLength, MessageBody, ResponseBody}; -use error::{Error, ResponseError}; -use h1::{Codec, Message}; -use response::Response; +use crate::body::{BodyLength, MessageBody, ResponseBody}; +use crate::error::{Error, ResponseError}; +use crate::h1::{Codec, Message}; +use crate::response::Response; pub struct SendError(PhantomData<(T, R, E)>); @@ -56,7 +56,7 @@ where match req { Ok(r) => Either::A(ok(r)), Err((e, framed)) => { - let mut res = e.error_response().set_body(format!("{}", e)); + let res = e.error_response().set_body(format!("{}", e)); let (res, _body) = res.replace_body(()); Either::B(SendErrorFut { framed: Some(framed), @@ -206,11 +206,13 @@ where // flush write buffer if !framed.is_write_buf_empty() { match framed.poll_complete()? { - Async::Ready(_) => if body_ready { - continue; - } else { - return Ok(Async::NotReady); - }, + Async::Ready(_) => { + if body_ready { + continue; + } else { + return Ok(Async::NotReady); + } + } Async::NotReady => return Ok(Async::NotReady), } } diff --git a/src/test.rs b/src/test.rs index 3d12e344b..308d2b4df 100644 --- a/src/test.rs +++ b/src/test.rs @@ -17,15 +17,15 @@ use net2::TcpBuilder; use tokio::runtime::current_thread::Runtime; use tokio_io::{AsyncRead, AsyncWrite}; -use body::MessageBody; -use client::{ +use crate::body::MessageBody; +use crate::client::{ ClientRequest, ClientRequestBuilder, ClientResponse, Connect, Connection, Connector, ConnectorError, SendRequestError, }; -use header::{Header, IntoHeaderValue}; -use payload::Payload; -use request::Request; -use ws; +use crate::header::{Header, IntoHeaderValue}; +use crate::payload::Payload; +use crate::request::Request; +use crate::ws; /// Test `Request` builder /// @@ -338,7 +338,7 @@ impl TestServer { } fn new_connector( -) -> impl Service + Clone + ) -> impl Service + Clone { #[cfg(feature = "ssl")] { diff --git a/src/ws/client/connect.rs b/src/ws/client/connect.rs index 575b4e4d8..09d025631 100644 --- a/src/ws/client/connect.rs +++ b/src/ws/client/connect.rs @@ -5,10 +5,9 @@ use cookie::Cookie; use http::header::{HeaderName, HeaderValue}; use http::{Error as HttpError, HttpTryFrom}; -use client::{ClientRequest, ClientRequestBuilder}; -use header::IntoHeaderValue; - use super::ClientError; +use crate::client::{ClientRequest, ClientRequestBuilder}; +use crate::header::IntoHeaderValue; /// `WebSocket` connection pub struct Connect { diff --git a/src/ws/client/error.rs b/src/ws/client/error.rs index 589648eea..62a3f47a9 100644 --- a/src/ws/client/error.rs +++ b/src/ws/client/error.rs @@ -2,12 +2,11 @@ use std::io; use actix_net::connector::ConnectorError; -use http::header::HeaderValue; -use http::StatusCode; +use failure::Fail; +use http::{header::HeaderValue, Error as HttpError, StatusCode}; -use error::ParseError; -use http::Error as HttpError; -use ws::ProtocolError; +use crate::error::ParseError; +use crate::ws::ProtocolError; /// Websocket client error #[derive(Fail, Debug)] diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index 68f8032e6..8dd407b0e 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -6,17 +6,18 @@ use actix_net::connector::{Connect as TcpConnect, ConnectorError, DefaultConnect use actix_net::service::Service; use base64; use futures::future::{err, Either, FutureResult}; -use futures::{Async, Future, Poll, Sink, Stream}; +use futures::{try_ready, Async, Future, Poll, Sink, Stream}; use http::header::{self, HeaderValue}; use http::{HttpTryFrom, StatusCode}; +use log::trace; use rand; use sha1::Sha1; use tokio_io::{AsyncRead, AsyncWrite}; -use body::BodyLength; -use client::ClientResponse; -use h1; -use ws::Codec; +use crate::body::BodyLength; +use crate::client::ClientResponse; +use crate::h1; +use crate::ws::Codec; use super::{ClientError, Connect, Protocol}; diff --git a/src/ws/frame.rs b/src/ws/frame.rs index 56d282964..32ad4ef4f 100644 --- a/src/ws/frame.rs +++ b/src/ws/frame.rs @@ -1,10 +1,11 @@ use byteorder::{ByteOrder, LittleEndian, NetworkEndian}; use bytes::{BufMut, Bytes, BytesMut}; +use log::debug; use rand; -use ws::mask::apply_mask; -use ws::proto::{CloseCode, CloseReason, OpCode}; -use ws::ProtocolError; +use crate::ws::mask::apply_mask; +use crate::ws::proto::{CloseCode, CloseReason, OpCode}; +use crate::ws::ProtocolError; /// A struct representing a `WebSocket` frame. #[derive(Debug)] diff --git a/src/ws/mod.rs b/src/ws/mod.rs index f1c91714a..ccd9ef4ac 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -5,10 +5,12 @@ //! communicate with the peer. use std::io; -use error::ResponseError; +use failure::Fail; use http::{header, Method, StatusCode}; -use request::Request; -use response::{Response, ResponseBuilder}; + +use crate::error::ResponseError; +use crate::request::Request; +use crate::response::{Response, ResponseBuilder}; mod client; mod codec; @@ -221,7 +223,8 @@ mod tests { .header( header::UPGRADE, header::HeaderValue::from_static("websocket"), - ).finish(); + ) + .finish(); assert_eq!( HandshakeError::NoConnectionUpgrade, verify_handshake(&req).err().unwrap() @@ -231,10 +234,12 @@ mod tests { .header( header::UPGRADE, header::HeaderValue::from_static("websocket"), - ).header( + ) + .header( header::CONNECTION, header::HeaderValue::from_static("upgrade"), - ).finish(); + ) + .finish(); assert_eq!( HandshakeError::NoVersionHeader, verify_handshake(&req).err().unwrap() @@ -244,13 +249,16 @@ mod tests { .header( header::UPGRADE, header::HeaderValue::from_static("websocket"), - ).header( + ) + .header( header::CONNECTION, header::HeaderValue::from_static("upgrade"), - ).header( + ) + .header( header::SEC_WEBSOCKET_VERSION, header::HeaderValue::from_static("5"), - ).finish(); + ) + .finish(); assert_eq!( HandshakeError::UnsupportedVersion, verify_handshake(&req).err().unwrap() @@ -260,13 +268,16 @@ mod tests { .header( header::UPGRADE, header::HeaderValue::from_static("websocket"), - ).header( + ) + .header( header::CONNECTION, header::HeaderValue::from_static("upgrade"), - ).header( + ) + .header( header::SEC_WEBSOCKET_VERSION, header::HeaderValue::from_static("13"), - ).finish(); + ) + .finish(); assert_eq!( HandshakeError::BadWebsocketKey, verify_handshake(&req).err().unwrap() @@ -276,16 +287,20 @@ mod tests { .header( header::UPGRADE, header::HeaderValue::from_static("websocket"), - ).header( + ) + .header( header::CONNECTION, header::HeaderValue::from_static("upgrade"), - ).header( + ) + .header( header::SEC_WEBSOCKET_VERSION, header::HeaderValue::from_static("13"), - ).header( + ) + .header( header::SEC_WEBSOCKET_KEY, header::HeaderValue::from_static("13"), - ).finish(); + ) + .finish(); assert_eq!( StatusCode::SWITCHING_PROTOCOLS, handshake_response(&req).finish().status() diff --git a/src/ws/service.rs b/src/ws/service.rs index 118a2244a..846278147 100644 --- a/src/ws/service.rs +++ b/src/ws/service.rs @@ -5,8 +5,8 @@ use actix_net::service::{NewService, Service}; use futures::future::{ok, FutureResult}; use futures::{Async, IntoFuture, Poll}; -use h1::Codec; -use request::Request; +use crate::h1::Codec; +use crate::request::Request; use super::{verify_handshake, HandshakeError}; diff --git a/tests/test_client.rs b/tests/test_client.rs index 4a4ccb7dd..3e14cd7b7 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -91,7 +91,8 @@ fn test_with_query_parameter() { } else { ok::<_, ()>(Response::BadRequest().finish()) } - }).map(|_| ()) + }) + .map(|_| ()) }); let mut connector = srv.new_connector(); diff --git a/tests/test_server.rs b/tests/test_server.rs index 300b38a80..d169e5a98 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -209,7 +209,8 @@ fn test_content_length() { StatusCode::NOT_FOUND, ]; future::ok::<_, ()>(Response::new(statuses[indx])) - }).map(|_| ()) + }) + .map(|_| ()) }); let header = HeaderName::from_static("content-length"); @@ -348,7 +349,8 @@ fn test_head_binary() { let mut srv = test::TestServer::with_factory(|| { h1::H1Service::new(|_| { ok::<_, ()>(Response::Ok().content_length(STR.len() as u64).body(STR)) - }).map(|_| ()) + }) + .map(|_| ()) }); let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); @@ -396,7 +398,8 @@ fn test_body_length() { Response::Ok() .body(Body::from_message(body::SizedStream::new(STR.len(), body))), ) - }).map(|_| ()) + }) + .map(|_| ()) }); let req = srv.get().finish().unwrap(); @@ -414,7 +417,8 @@ fn test_body_chunked_explicit() { h1::H1Service::new(|_| { let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); ok::<_, ()>(Response::Ok().streaming(body)) - }).map(|_| ()) + }) + .map(|_| ()) }); let req = srv.get().finish().unwrap(); @@ -434,7 +438,8 @@ fn test_body_chunked_implicit() { h1::H1Service::new(|_| { let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); ok::<_, ()>(Response::Ok().streaming(body)) - }).map(|_| ()) + }) + .map(|_| ()) }); let req = srv.get().finish().unwrap(); @@ -456,7 +461,8 @@ fn test_response_http_error_handling() { .header(http::header::CONTENT_TYPE, broken_header) .body(STR), ) - }).map(|_| ()) + }) + .map(|_| ()) }); let req = srv.get().finish().unwrap(); diff --git a/tests/test_ws.rs b/tests/test_ws.rs index 22ce3ca29..f890c586e 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -62,7 +62,8 @@ fn test_simple() { SendResponse::send( framed, ws::handshake_response(&req).finish(), - ).map_err(|_| ()) + ) + .map_err(|_| ()) .and_then(|framed| { // start websocket service let framed = framed.into_framed(ws::Codec::new()); From 9f4d48f7a1b28f23537b043f17d48bdcb1a2f10b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 6 Dec 2018 15:03:01 -0800 Subject: [PATCH 101/427] update tests --- src/h1/codec.rs | 8 ++++---- src/h1/decoder.rs | 6 +++--- src/httpcodes.rs | 4 ++-- src/httpmessage.rs | 3 ++- src/json.rs | 3 ++- src/response.rs | 6 +++--- src/ws/mod.rs | 2 +- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 54c1ce2e6..d67d36085 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -197,10 +197,10 @@ mod tests { use tokio_io::{AsyncRead, AsyncWrite}; use super::*; - use error::ParseError; - use h1::Message; - use httpmessage::HttpMessage; - use request::Request; + use crate::error::ParseError; + use crate::h1::Message; + use crate::httpmessage::HttpMessage; + use crate::request::Request; #[test] fn test_http_request_chunked_payload_and_next_message() { diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 26b28440b..e5971f750 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -612,9 +612,9 @@ mod tests { use tokio_io::{AsyncRead, AsyncWrite}; use super::*; - use error::ParseError; - use httpmessage::HttpMessage; - use message::Head; + use crate::error::ParseError; + use crate::httpmessage::HttpMessage; + use crate::message::Head; impl PayloadType { fn unwrap(self) -> PayloadDecoder { diff --git a/src/httpcodes.rs b/src/httpcodes.rs index 80722734a..7806bd806 100644 --- a/src/httpcodes.rs +++ b/src/httpcodes.rs @@ -73,9 +73,9 @@ impl Response { #[cfg(test)] mod tests { - use body::Body; + use crate::body::Body; + use crate::response::Response; use http::StatusCode; - use response::Response; #[test] fn test_build() { diff --git a/src/httpmessage.rs b/src/httpmessage.rs index e239a7337..373b7ed45 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -568,11 +568,12 @@ where #[cfg(test)] mod tests { use super::*; + use crate::test::TestRequest; use encoding::all::ISO_8859_2; use encoding::Encoding; use futures::Async; use mime; - use test::TestRequest; + use serde_derive::Deserialize; #[test] fn test_content_type() { diff --git a/src/json.rs b/src/json.rs index 0b6ac377f..bfecf0cc3 100644 --- a/src/json.rs +++ b/src/json.rs @@ -137,8 +137,9 @@ mod tests { use bytes::Bytes; use futures::Async; use http::header; + use serde_derive::{Deserialize, Serialize}; - use test::TestRequest; + use crate::test::TestRequest; impl PartialEq for JsonPayloadError { fn eq(&self, other: &JsonPayloadError) -> bool { diff --git a/src/response.rs b/src/response.rs index e506cd163..5cff612f7 100644 --- a/src/response.rs +++ b/src/response.rs @@ -855,9 +855,9 @@ impl ResponsePool { #[cfg(test)] mod tests { use super::*; - use body::Body; - use http; - use http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; + use crate::body::Body; + use crate::http; + use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; // use test::TestRequest; diff --git a/src/ws/mod.rs b/src/ws/mod.rs index ccd9ef4ac..02667c964 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -194,8 +194,8 @@ pub fn handshake_response(req: &Request) -> ResponseBuilder { #[cfg(test)] mod tests { use super::*; + use crate::test::TestRequest; use http::{header, Method}; - use test::TestRequest; #[test] fn test_handshake() { From aaae368ed9e59d41ec03a7025bb95fc419e89566 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 10 Dec 2018 18:08:33 -0800 Subject: [PATCH 102/427] use new actix crates --- Cargo.toml | 56 ++++++++++++--------------- examples/echo.rs | 17 ++------- examples/echo2.rs | 16 ++------ examples/framed_hello.rs | 21 +++------- examples/hello-world.rs | 16 ++------ src/client/connect.rs | 3 +- src/client/connection.rs | 2 +- src/client/connector.rs | 13 +++---- src/client/error.rs | 32 ++++++++++++---- src/client/pipeline.rs | 5 +-- src/client/pool.rs | 6 +-- src/client/request.rs | 36 +++++++++--------- src/config.rs | 6 +-- src/error.rs | 6 +-- src/h1/client.rs | 2 +- src/h1/codec.rs | 4 +- src/h1/decoder.rs | 4 +- src/h1/dispatcher.rs | 5 +-- src/h1/mod.rs | 2 +- src/h1/service.rs | 5 +-- src/payload.rs | 2 +- src/service.rs | 5 +-- src/test.rs | 16 ++++---- src/ws/client/error.rs | 2 +- src/ws/client/service.rs | 7 ++-- src/ws/codec.rs | 2 +- src/ws/service.rs | 4 +- src/ws/transport.rs | 7 ++-- tests/test_client.rs | 9 +---- tests/test_server.rs | 8 +--- tests/test_ws.rs | 82 +++++++++++++++++++--------------------- 31 files changed, 174 insertions(+), 227 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 57ed3f9b7..5afeda941 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,61 +28,55 @@ name = "actix_http" path = "src/lib.rs" [features] -default = ["session", "cell"] +default = ["session"] # sessions feature, session require "ring" crate and c compiler session = ["cookie/secure"] -cell = ["actix-net/cell"] - -# tls -tls = ["native-tls", "actix-net/tls"] - # openssl -ssl = ["openssl", "actix-net/ssl"] - -# rustls -rust-tls = ["rustls", "actix-net/rust-tls"] +ssl = ["openssl", "actix-connector/ssl"] [dependencies] -actix = "0.7.5" -#actix-net = "0.3.0" -actix-net = { git="https://github.com/actix/actix-net.git" } +actix-service = "0.1.1" +actix-codec = { git="https://github.com/actix/actix-net.git" } +actix-connector = { git="https://github.com/actix/actix-net.git" } +actix-rt = { git="https://github.com/actix/actix-net.git" } +actix-server = { git="https://github.com/actix/actix-net.git" } +actix-utils = { git="https://github.com/actix/actix-net.git" } + +# actix-codec = { path="../actix-net/actix-codec/" } +# actix-connector = { path="../actix-net/actix-connector/" } +# actix-rt = { path="../actix-net/actix-rt/" } +# actix-server = { path="../actix-net/actix-server/" } +# actix-utils = { path="../actix-net/actix-utils/" } base64 = "0.9" bitflags = "1.0" +bytes = "0.4" +byteorder = "1.2" +cookie = { version="0.11", features=["percent-encode"] } +encoding = "0.2" +failure = "0.1.3" +futures = "0.1" http = "0.1.8" httparse = "1.3" -failure = "0.1.3" indexmap = "1.0" log = "0.4" mime = "0.3" +net2 = "0.2" +percent-encoding = "1.0" rand = "0.5" serde = "1.0" serde_json = "1.0" sha1 = "0.6" -time = "0.1" -encoding = "0.2" -serde_urlencoded = "0.5.3" - -cookie = { version="0.11", features=["percent-encode"] } -percent-encoding = "1.0" -url = { version="1.7", features=["query_encoding"] } - -# io -net2 = "0.2" slab = "0.4" -bytes = "0.4" -byteorder = "1.2" -futures = "0.1" -tokio-codec = "0.1" -tokio = "0.1" -tokio-io = "0.1" +serde_urlencoded = "0.5.3" +time = "0.1" tokio-tcp = "0.1" tokio-timer = "0.2" -tokio-current-thread = "0.1" trust-dns-proto = "0.5.0" trust-dns-resolver = "0.10.0" +url = { version="1.7", features=["query_encoding"] } # native-tls native-tls = { version="0.2", optional = true } diff --git a/examples/echo.rs b/examples/echo.rs index 0453ad6a7..3bfb04d7c 100644 --- a/examples/echo.rs +++ b/examples/echo.rs @@ -1,27 +1,18 @@ -#[macro_use] -extern crate log; -extern crate env_logger; - -extern crate actix_http; -extern crate actix_net; -extern crate bytes; -extern crate futures; -extern crate http; - use actix_http::HttpMessage; use actix_http::{h1, Request, Response}; -use actix_net::server::Server; -use actix_net::service::NewServiceExt; +use actix_server::Server; +use actix_service::NewService; use bytes::Bytes; use futures::Future; use http::header::HeaderValue; +use log::info; use std::env; fn main() { env::set_var("RUST_LOG", "echo=info"); env_logger::init(); - Server::new() + Server::build() .bind("echo", "127.0.0.1:8080", || { h1::H1Service::build() .client_timeout(1000) diff --git a/examples/echo2.rs b/examples/echo2.rs index 3206ff507..0e2bc9d52 100644 --- a/examples/echo2.rs +++ b/examples/echo2.rs @@ -1,19 +1,11 @@ -#[macro_use] -extern crate log; -extern crate env_logger; - -extern crate actix_http; -extern crate actix_net; -extern crate bytes; -extern crate futures; - use actix_http::http::HeaderValue; use actix_http::HttpMessage; use actix_http::{h1, Error, Request, Response}; -use actix_net::server::Server; -use actix_net::service::NewServiceExt; +use actix_server::Server; +use actix_service::NewService; use bytes::Bytes; use futures::Future; +use log::info; use std::env; fn handle_request(_req: Request) -> impl Future { @@ -29,7 +21,7 @@ fn main() { env::set_var("RUST_LOG", "echo=info"); env_logger::init(); - Server::new() + Server::build() .bind("echo", "127.0.0.1:8080", || { h1::H1Service::build() .client_timeout(1000) diff --git a/examples/framed_hello.rs b/examples/framed_hello.rs index 6c53d27f3..5bbc3be9b 100644 --- a/examples/framed_hello.rs +++ b/examples/framed_hello.rs @@ -1,18 +1,9 @@ -extern crate env_logger; -extern crate log; - -extern crate actix_http; -extern crate actix_net; -extern crate bytes; -extern crate futures; -extern crate http; - +use actix_codec::Framed; use actix_http::{h1, Response, SendResponse, ServiceConfig}; -use actix_net::codec::Framed; -use actix_net::framed::IntoFramed; -use actix_net::server::Server; -use actix_net::service::NewServiceExt; -use actix_net::stream::TakeItem; +use actix_server::Server; +use actix_service::NewService; +use actix_utils::framed::IntoFramed; +use actix_utils::stream::TakeItem; use futures::Future; use std::env; @@ -20,7 +11,7 @@ fn main() { env::set_var("RUST_LOG", "framed_hello=info"); env_logger::init(); - Server::new() + Server::build() .bind("framed_hello", "127.0.0.1:8080", || { IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) .and_then(TakeItem::new().map_err(|_| ())) diff --git a/examples/hello-world.rs b/examples/hello-world.rs index b477f191d..e0c322a22 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -1,24 +1,16 @@ -#[macro_use] -extern crate log; -extern crate env_logger; - -extern crate actix_http; -extern crate actix_net; -extern crate futures; -extern crate http; - use actix_http::{h1, Response}; -use actix_net::server::Server; -use actix_net::service::NewServiceExt; +use actix_server::Server; +use actix_service::NewService; use futures::future; use http::header::HeaderValue; +use log::info; use std::env; fn main() { env::set_var("RUST_LOG", "hello_world=info"); env_logger::init(); - Server::new() + Server::build() .bind("hello-world", "127.0.0.1:8080", || { h1::H1Service::build() .client_timeout(1000) diff --git a/src/client/connect.rs b/src/client/connect.rs index a445228e3..f4112cfaa 100644 --- a/src/client/connect.rs +++ b/src/client/connect.rs @@ -1,5 +1,4 @@ -use actix_net::connector::RequestPort; -use actix_net::resolver::RequestHost; +use actix_connector::{RequestHost, RequestPort}; use http::uri::Uri; use http::{Error as HttpError, HttpTryFrom}; diff --git a/src/client/connection.rs b/src/client/connection.rs index 363a4ece9..ed156bf84 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -1,7 +1,7 @@ use std::{fmt, io, time}; +use actix_codec::{AsyncRead, AsyncWrite}; use futures::Poll; -use tokio_io::{AsyncRead, AsyncWrite}; use super::pool::Acquired; diff --git a/src/client/connector.rs b/src/client/connector.rs index 74ee6a7e3..fdc996ffe 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -1,13 +1,12 @@ use std::time::Duration; use std::{fmt, io}; -use actix_net::connector::TcpConnector; -use actix_net::resolver::Resolver; -use actix_net::service::{Service, ServiceExt}; -use actix_net::timeout::{TimeoutError, TimeoutService}; +use actix_codec::{AsyncRead, AsyncWrite}; +use actix_connector::{Resolver, TcpConnector}; +use actix_service::Service; +use actix_utils::timeout::{TimeoutError, TimeoutService}; use futures::future::Either; use futures::Poll; -use tokio_io::{AsyncRead, AsyncWrite}; use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; use super::connect::Connect; @@ -16,7 +15,7 @@ use super::error::ConnectorError; use super::pool::ConnectionPool; #[cfg(feature = "ssl")] -use actix_net::ssl::OpensslConnector; +use actix_connector::ssl::OpensslConnector; #[cfg(feature = "ssl")] use openssl::ssl::{SslConnector, SslMethod}; @@ -169,7 +168,7 @@ impl Connector { .and_then(TcpConnector::default().from_err()) .and_then( OpensslConnector::service(self.connector) - .map_err(ConnectorError::SslError), + .map_err(ConnectorError::from), ), ) .map_err(|e| match e { diff --git a/src/client/error.rs b/src/client/error.rs index d2a0f38ec..815bc1edc 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -4,13 +4,7 @@ use failure::Fail; use trust_dns_resolver::error::ResolveError; #[cfg(feature = "ssl")] -use openssl::ssl::Error as SslError; - -#[cfg(all(feature = "tls", not(any(feature = "ssl", feature = "rust-tls"))))] -use native_tls::Error as SslError; - -#[cfg(all(feature = "rust-tls", not(any(feature = "tls", feature = "ssl"))))] -use std::io::Error as SslError; +use openssl::ssl::{Error as SslError, HandshakeError}; use crate::error::{Error, ParseError}; @@ -26,7 +20,7 @@ pub enum ConnectorError { SslIsNotSupported, /// SSL error - #[cfg(any(feature = "tls", feature = "ssl", feature = "rust-tls"))] + #[cfg(feature = "ssl")] #[fail(display = "{}", _0)] SslError(#[cause] SslError), @@ -73,6 +67,28 @@ impl From for ConnectorError { } } +#[cfg(feature = "ssl")] +impl From for ConnectorError { + fn from(err: SslError) -> ConnectorError { + ConnectorError::SslError(err) + } +} + +#[cfg(feature = "ssl")] +impl From> for ConnectorError { + fn from(err: HandshakeError) -> ConnectorError { + match err { + HandshakeError::SetupFailure(stack) => SslError::from(stack).into(), + HandshakeError::Failure(stream) => { + SslError::from(stream.into_error()).into() + } + HandshakeError::WouldBlock(stream) => { + SslError::from(stream.into_error()).into() + } + } + } +} + /// A set of errors that can occur during request sending and response reading #[derive(Debug)] pub enum SendRequestError { diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs index fc1e53e8f..8d946d644 100644 --- a/src/client/pipeline.rs +++ b/src/client/pipeline.rs @@ -1,11 +1,10 @@ use std::collections::VecDeque; -use actix_net::codec::Framed; -use actix_net::service::Service; +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_service::Service; use bytes::Bytes; use futures::future::{err, ok, Either}; use futures::{Async, Future, Poll, Sink, Stream}; -use tokio_io::{AsyncRead, AsyncWrite}; use super::error::{ConnectorError, SendRequestError}; use super::response::ClientResponse; diff --git a/src/client/pool.rs b/src/client/pool.rs index decf80194..94e96899e 100644 --- a/src/client/pool.rs +++ b/src/client/pool.rs @@ -4,7 +4,9 @@ use std::io; use std::rc::Rc; use std::time::{Duration, Instant}; -use actix_net::service::Service; +use actix_codec::{AsyncRead, AsyncWrite}; +use actix_rt::spawn; +use actix_service::Service; use futures::future::{ok, Either, FutureResult}; use futures::sync::oneshot; use futures::task::AtomicTask; @@ -12,8 +14,6 @@ use futures::{Async, Future, Poll}; use http::uri::Authority; use indexmap::IndexSet; use slab::Slab; -use tokio_current_thread::spawn; -use tokio_io::{AsyncRead, AsyncWrite}; use tokio_timer::{sleep, Delay}; use super::connect::Connect; diff --git a/src/client/request.rs b/src/client/request.rs index 5f294bbd8..fbb1e840b 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -2,7 +2,7 @@ use std::fmt; use std::fmt::Write as FmtWrite; use std::io::Write; -use actix_net::service::Service; +use actix_service::Service; use bytes::{BufMut, Bytes, BytesMut}; use cookie::{Cookie, CookieJar}; use futures::{Future, Stream}; @@ -23,26 +23,24 @@ use super::{pipeline, Connect, Connection, ConnectorError, SendRequestError}; /// An HTTP Client Request /// /// ```rust -/// # extern crate actix_web; -/// # extern crate futures; -/// # extern crate tokio; -/// # use futures::Future; -/// # use std::process; -/// use actix_web::{actix, client}; +/// use futures::future::{Future, lazy}; +/// use actix_rt::System; +/// use actix_http::client; /// /// fn main() { -/// actix::run( -/// || client::ClientRequest::get("http://www.rust-lang.org") // <- Create request builder -/// .header("User-Agent", "Actix-web") -/// .finish().unwrap() -/// .send() // <- Send http request -/// .map_err(|_| ()) -/// .and_then(|response| { // <- server http response -/// println!("Response: {:?}", response); -/// # actix::System::current().stop(); -/// Ok(()) -/// }), -/// ); +/// System::new("test").block_on(lazy(|| { +/// let mut connector = client::Connector::default().service(); +/// client::ClientRequest::get("http://www.rust-lang.org") // <- Create request builder +/// .header("User-Agent", "Actix-web") +/// .finish().unwrap() +/// .send(&mut connector) // <- Send http request +/// .map_err(|_| ()) +/// .and_then(|response| { // <- server http response +/// println!("Response: {:?}", response); +/// # actix_rt::System::current().stop(); +/// Ok(()) +/// }) +/// })); /// } /// ``` pub struct ClientRequest { diff --git a/src/config.rs b/src/config.rs index 661c0901f..67c928fb7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,11 +4,11 @@ use std::rc::Rc; use std::time::{Duration, Instant}; use std::{fmt, net}; +use actix_rt::spawn; use bytes::BytesMut; use futures::{future, Future}; use log::error; use time; -use tokio_current_thread::spawn; use tokio_timer::{sleep, Delay}; // "Sun, 06 Nov 1994 08:49:37 GMT".len() @@ -378,8 +378,8 @@ impl DateService { #[cfg(test)] mod tests { use super::*; + use actix_rt::System; use futures::future; - use tokio::runtime::current_thread; #[test] fn test_date_len() { @@ -388,7 +388,7 @@ mod tests { #[test] fn test_date() { - let mut rt = current_thread::Runtime::new().unwrap(); + let mut rt = System::new("test"); let _ = rt.block_on(future::lazy(|| { let settings = ServiceConfig::new(KeepAlive::Os, 0, 0); diff --git a/src/error.rs b/src/error.rs index 280f9a329..49602bd98 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,7 +5,7 @@ use std::string::FromUtf8Error; use std::sync::Mutex; use std::{fmt, io, result}; -use actix::MailboxError; +// use actix::MailboxError; use cookie; use failure::{self, Backtrace, Fail}; use futures::Canceled; @@ -250,8 +250,8 @@ impl ResponseError for header::InvalidHeaderValueBytes { /// `InternalServerError` for `futures::Canceled` impl ResponseError for Canceled {} -/// `InternalServerError` for `actix::MailboxError` -impl ResponseError for MailboxError {} +// /// `InternalServerError` for `actix::MailboxError` +// impl ResponseError for MailboxError {} /// A set of errors that can occur during parsing HTTP streams #[derive(Fail, Debug)] diff --git a/src/h1/client.rs b/src/h1/client.rs index f547983fa..de4d10e1b 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -1,13 +1,13 @@ #![allow(unused_imports, unused_variables, dead_code)] use std::io::{self, Write}; +use actix_codec::{Decoder, Encoder}; use bitflags::bitflags; use bytes::{BufMut, Bytes, BytesMut}; use http::header::{ HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE, }; use http::{Method, Version}; -use tokio_codec::{Decoder, Encoder}; use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; use super::{decoder, encoder}; diff --git a/src/h1/codec.rs b/src/h1/codec.rs index d67d36085..fbc8b4a58 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -2,11 +2,11 @@ use std::fmt; use std::io::{self, Write}; +use actix_codec::{Decoder, Encoder}; use bitflags::bitflags; use bytes::{BufMut, Bytes, BytesMut}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::{Method, StatusCode, Version}; -use tokio_codec::{Decoder, Encoder}; use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; use super::{decoder, encoder}; @@ -192,9 +192,9 @@ impl Encoder for Codec { mod tests { use std::{cmp, io}; + use actix_codec::{AsyncRead, AsyncWrite}; use bytes::{Buf, Bytes, BytesMut}; use http::{Method, Version}; - use tokio_io::{AsyncRead, AsyncWrite}; use super::*; use crate::error::ParseError; diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index e5971f750..460d2b3aa 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -1,13 +1,13 @@ use std::marker::PhantomData; use std::{io, mem}; +use actix_codec::Decoder; use bytes::{Bytes, BytesMut}; use futures::{Async, Poll}; use http::header::{HeaderName, HeaderValue}; use http::{header, HeaderMap, HttpTryFrom, Method, StatusCode, Uri, Version}; use httparse; use log::{debug, error, trace}; -use tokio_codec::Decoder; use crate::client::ClientResponse; use crate::error::ParseError; @@ -607,9 +607,9 @@ impl ChunkedState { mod tests { use std::{cmp, io}; + use actix_codec::{AsyncRead, AsyncWrite}; use bytes::{Buf, Bytes, BytesMut}; use http::{Method, Version}; - use tokio_io::{AsyncRead, AsyncWrite}; use super::*; use crate::error::ParseError; diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 59b419c3e..569b1deeb 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -3,12 +3,11 @@ use std::fmt::Debug; use std::mem; use std::time::Instant; -use actix_net::codec::Framed; -use actix_net::service::Service; +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_service::Service; use bitflags::bitflags; use futures::{try_ready, Async, Future, Poll, Sink, Stream}; use log::{debug, error, trace}; -use tokio_io::{AsyncRead, AsyncWrite}; use tokio_timer::Delay; use crate::body::{Body, BodyLength, MessageBody, ResponseBody}; diff --git a/src/h1/mod.rs b/src/h1/mod.rs index 461ecb959..a375b60d3 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -1,7 +1,7 @@ //! HTTP/1 implementation use std::fmt; -use actix_net::codec::Framed; +use actix_codec::Framed; use bytes::Bytes; mod client; diff --git a/src/h1/service.rs b/src/h1/service.rs index 1a5a587c0..7c2589cc8 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -2,12 +2,11 @@ use std::fmt::Debug; use std::marker::PhantomData; use std::net; -use actix_net::codec::Framed; -use actix_net::service::{IntoNewService, NewService, Service}; +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_service::{IntoNewService, NewService, Service}; use futures::future::{ok, FutureResult}; use futures::{try_ready, Async, Future, Poll, Stream}; use log::error; -use tokio_io::{AsyncRead, AsyncWrite}; use crate::body::MessageBody; use crate::config::{KeepAlive, ServiceConfig}; diff --git a/src/payload.rs b/src/payload.rs index 37f06d433..ea266f70b 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -523,8 +523,8 @@ where #[cfg(test)] mod tests { use super::*; + use actix_rt::Runtime; use futures::future::{lazy, result}; - use tokio::runtime::current_thread::Runtime; #[test] fn test_error() { diff --git a/src/service.rs b/src/service.rs index a6a820977..f98234e76 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,10 +1,9 @@ use std::marker::PhantomData; -use actix_net::codec::Framed; -use actix_net::service::{NewService, Service}; +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_service::{NewService, Service}; use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, Poll, Sink}; -use tokio_io::{AsyncRead, AsyncWrite}; use crate::body::{BodyLength, MessageBody, ResponseBody}; use crate::error::{Error, ResponseError}; diff --git a/src/test.rs b/src/test.rs index 308d2b4df..c264ac47a 100644 --- a/src/test.rs +++ b/src/test.rs @@ -3,10 +3,10 @@ use std::str::FromStr; use std::sync::mpsc; use std::{net, thread}; -use actix::System; -use actix_net::codec::Framed; -use actix_net::server::{Server, StreamServiceFactory}; -use actix_net::service::Service; +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_rt::{Runtime, System}; +use actix_server::{Server, StreamServiceFactory}; +use actix_service::Service; use bytes::Bytes; use cookie::Cookie; @@ -14,8 +14,6 @@ use futures::future::{lazy, Future}; use http::header::HeaderName; use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; use net2::TcpBuilder; -use tokio::runtime::current_thread::Runtime; -use tokio_io::{AsyncRead, AsyncWrite}; use crate::body::MessageBody; use crate::client::{ @@ -316,7 +314,7 @@ impl TestServer { let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); let local_addr = tcp.local_addr().unwrap(); - Server::default() + Server::build() .listen("test", tcp, factory) .workers(1) .disable_signals() @@ -390,9 +388,9 @@ impl TestServerRuntime { /// Construct test server url pub fn url(&self, uri: &str) -> String { if uri.starts_with('/') { - format!("http://localhost:{}{}", self.addr.port(), uri) + format!("http://127.0.0.1:{}{}", self.addr.port(), uri) } else { - format!("http://localhost:{}/{}", self.addr.port(), uri) + format!("http://127.0.0.1:{}/{}", self.addr.port(), uri) } } diff --git a/src/ws/client/error.rs b/src/ws/client/error.rs index 62a3f47a9..02c723755 100644 --- a/src/ws/client/error.rs +++ b/src/ws/client/error.rs @@ -1,7 +1,7 @@ //! Http client request use std::io; -use actix_net::connector::ConnectorError; +use actix_connector::ConnectorError; use failure::Fail; use http::{header::HeaderValue, Error as HttpError, StatusCode}; diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index 8dd407b0e..c48b6e0c1 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -1,9 +1,9 @@ //! websockets client use std::marker::PhantomData; -use actix_net::codec::Framed; -use actix_net::connector::{Connect as TcpConnect, ConnectorError, DefaultConnector}; -use actix_net::service::Service; +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_connector::{Connect as TcpConnect, ConnectorError, DefaultConnector}; +use actix_service::Service; use base64; use futures::future::{err, Either, FutureResult}; use futures::{try_ready, Async, Future, Poll, Sink, Stream}; @@ -12,7 +12,6 @@ use http::{HttpTryFrom, StatusCode}; use log::trace; use rand; use sha1::Sha1; -use tokio_io::{AsyncRead, AsyncWrite}; use crate::body::BodyLength; use crate::client::ClientResponse; diff --git a/src/ws/codec.rs b/src/ws/codec.rs index 10c505287..286d15f8c 100644 --- a/src/ws/codec.rs +++ b/src/ws/codec.rs @@ -1,5 +1,5 @@ +use actix_codec::{Decoder, Encoder}; use bytes::{Bytes, BytesMut}; -use tokio_codec::{Decoder, Encoder}; use super::frame::Parser; use super::proto::{CloseReason, OpCode}; diff --git a/src/ws/service.rs b/src/ws/service.rs index 846278147..8189b1955 100644 --- a/src/ws/service.rs +++ b/src/ws/service.rs @@ -1,7 +1,7 @@ use std::marker::PhantomData; -use actix_net::codec::Framed; -use actix_net::service::{NewService, Service}; +use actix_codec::Framed; +use actix_service::{NewService, Service}; use futures::future::{ok, FutureResult}; use futures::{Async, IntoFuture, Poll}; diff --git a/src/ws/transport.rs b/src/ws/transport.rs index 8cd79cb03..f59ad67a7 100644 --- a/src/ws/transport.rs +++ b/src/ws/transport.rs @@ -1,8 +1,7 @@ -use actix_net::codec::Framed; -use actix_net::framed::{FramedTransport, FramedTransportError}; -use actix_net::service::{IntoService, Service}; +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_service::{IntoService, Service}; +use actix_utils::framed::{FramedTransport, FramedTransportError}; use futures::{Future, Poll}; -use tokio_io::{AsyncRead, AsyncWrite}; use super::{Codec, Frame, Message}; diff --git a/tests/test_client.rs b/tests/test_client.rs index 3e14cd7b7..f19edda13 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -1,10 +1,4 @@ -extern crate actix; -extern crate actix_http; -extern crate actix_net; -extern crate bytes; -extern crate futures; - -use actix_net::service::NewServiceExt; +use actix_service::NewService; use bytes::Bytes; use futures::future::{self, ok}; @@ -35,6 +29,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ #[test] fn test_h1_v2() { + env_logger::init(); let mut srv = TestServer::with_factory(move || { h1::H1Service::build() .finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) diff --git a/tests/test_server.rs b/tests/test_server.rs index d169e5a98..cb9cd3f9d 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1,14 +1,8 @@ -extern crate actix; -extern crate actix_http; -extern crate actix_net; -extern crate bytes; -extern crate futures; - use std::io::{Read, Write}; use std::time::Duration; use std::{net, thread}; -use actix_net::service::NewServiceExt; +use actix_service::NewService; use bytes::Bytes; use futures::future::{self, ok}; use futures::stream::once; diff --git a/tests/test_ws.rs b/tests/test_ws.rs index f890c586e..11a3f472b 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -1,16 +1,9 @@ -extern crate actix; -extern crate actix_http; -extern crate actix_net; -extern crate actix_web; -extern crate bytes; -extern crate futures; - use std::io; -use actix_net::codec::Framed; -use actix_net::framed::IntoFramed; -use actix_net::service::NewServiceExt; -use actix_net::stream::TakeItem; +use actix_codec::Framed; +use actix_service::NewService; +use actix_utils::framed::IntoFramed; +use actix_utils::stream::TakeItem; use actix_web::ws as web_ws; use bytes::{Bytes, BytesMut}; use futures::future::{lazy, ok, Either}; @@ -79,38 +72,6 @@ fn test_simple() { }) }); - { - let url = srv.url("/"); - - let (reader, mut writer) = srv - .block_on(lazy(|| web_ws::Client::new(url).connect())) - .unwrap(); - - writer.text("text"); - let (item, reader) = srv.block_on(reader.into_future()).unwrap(); - assert_eq!(item, Some(web_ws::Message::Text("text".to_owned()))); - - writer.binary(b"text".as_ref()); - let (item, reader) = srv.block_on(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(web_ws::Message::Binary(Bytes::from_static(b"text").into())) - ); - - writer.ping("ping"); - let (item, reader) = srv.block_on(reader.into_future()).unwrap(); - assert_eq!(item, Some(web_ws::Message::Pong("ping".to_owned()))); - - writer.close(Some(web_ws::CloseCode::Normal.into())); - let (item, _) = srv.block_on(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(web_ws::Message::Close(Some( - web_ws::CloseCode::Normal.into() - ))) - ); - } - // client service let framed = srv.ws().unwrap(); let framed = srv @@ -142,5 +103,38 @@ fn test_simple() { assert_eq!( item, Some(ws::Frame::Close(Some(ws::CloseCode::Normal.into()))) - ) + ); + + { + let mut sys = actix_web::actix::System::new("test"); + let url = srv.url("/"); + + let (reader, mut writer) = sys + .block_on(lazy(|| web_ws::Client::new(url).connect())) + .unwrap(); + + writer.text("text"); + let (item, reader) = sys.block_on(reader.into_future()).unwrap(); + assert_eq!(item, Some(web_ws::Message::Text("text".to_owned()))); + + writer.binary(b"text".as_ref()); + let (item, reader) = sys.block_on(reader.into_future()).unwrap(); + assert_eq!( + item, + Some(web_ws::Message::Binary(Bytes::from_static(b"text").into())) + ); + + writer.ping("ping"); + let (item, reader) = sys.block_on(reader.into_future()).unwrap(); + assert_eq!(item, Some(web_ws::Message::Pong("ping".to_owned()))); + + writer.close(Some(web_ws::CloseCode::Normal.into())); + let (item, _) = sys.block_on(reader.into_future()).unwrap(); + assert_eq!( + item, + Some(web_ws::Message::Close(Some( + web_ws::CloseCode::Normal.into() + ))) + ); + } } From 1c60992723eff00f05832b499ed9b0fbaa954c80 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 11 Dec 2018 09:29:12 -0800 Subject: [PATCH 103/427] use released crates --- Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5afeda941..0c6d53f50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,11 +38,11 @@ ssl = ["openssl", "actix-connector/ssl"] [dependencies] actix-service = "0.1.1" -actix-codec = { git="https://github.com/actix/actix-net.git" } -actix-connector = { git="https://github.com/actix/actix-net.git" } -actix-rt = { git="https://github.com/actix/actix-net.git" } -actix-server = { git="https://github.com/actix/actix-net.git" } -actix-utils = { git="https://github.com/actix/actix-net.git" } +actix-codec = "0.1.0" +actix-connector = "0.1.0" +actix-rt = "0.1.0" +actix-server = "0.1.0" +actix-utils = "0.1.0" # actix-codec = { path="../actix-net/actix-codec/" } # actix-connector = { path="../actix-net/actix-connector/" } From b1001b80b7cf150737e830bb11f144cf09ad61a6 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 12 Dec 2018 18:39:01 -0800 Subject: [PATCH 104/427] upgrade actix-service dependency --- Cargo.toml | 2 +- src/client/connector.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0c6d53f50..d992de647 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ session = ["cookie/secure"] ssl = ["openssl", "actix-connector/ssl"] [dependencies] -actix-service = "0.1.1" +actix-service = "0.1.3" actix-codec = "0.1.0" actix-connector = "0.1.0" actix-rt = "0.1.0" diff --git a/src/client/connector.rs b/src/client/connector.rs index fdc996ffe..05caf1ed2 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -3,7 +3,7 @@ use std::{fmt, io}; use actix_codec::{AsyncRead, AsyncWrite}; use actix_connector::{Resolver, TcpConnector}; -use actix_service::Service; +use actix_service::{Service, ServiceExt}; use actix_utils::timeout::{TimeoutError, TimeoutService}; use futures::future::Either; use futures::Poll; From 67df9399df8faa7983e4831d3de2abab37ef49dd Mon Sep 17 00:00:00 2001 From: Douman Date: Sun, 16 Dec 2018 18:43:11 +0300 Subject: [PATCH 105/427] H1 decoder should ignore headers case --- src/h1/decoder.rs | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 460d2b3aa..b2c410c47 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -94,20 +94,20 @@ pub(crate) trait MessageType: Sized { } // transfer-encoding header::TRANSFER_ENCODING => { - if let Ok(s) = value.to_str() { - chunked = s.to_lowercase().contains("chunked"); + if let Ok(s) = value.to_str().map(|s| s.trim()) { + chunked = s.eq_ignore_ascii_case("chunked"); } else { return Err(ParseError::Header); } } // connection keep-alive state header::CONNECTION => { - ka = if let Ok(conn) = value.to_str() { - if conn.contains("keep-alive") { + ka = if let Ok(conn) = value.to_str().map(|conn| conn.trim()) { + if conn.eq_ignore_ascii_case("keep-alive") { Some(ConnectionType::KeepAlive) - } else if conn.contains("close") { + } else if conn.eq_ignore_ascii_case("close") { Some(ConnectionType::Close) - } else if conn.contains("upgrade") { + } else if conn.eq_ignore_ascii_case("upgrade") { Some(ConnectionType::Upgrade) } else { None @@ -120,8 +120,8 @@ pub(crate) trait MessageType: Sized { has_upgrade = true; // check content-length, some clients (dart) // sends "content-length: 0" with websocket upgrade - if let Ok(val) = value.to_str() { - if val == "websocket" { + if let Ok(val) = value.to_str().map(|val| val.trim()) { + if val.eq_ignore_ascii_case("websocket") { content_length = None; } } @@ -887,6 +887,14 @@ mod tests { let req = parse_ready!(&mut buf); assert_eq!(req.inner().head.ctype, Some(ConnectionType::Close)); + + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + connection: Close\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + assert_eq!(req.inner().head.ctype, Some(ConnectionType::Close)); } #[test] @@ -909,6 +917,15 @@ mod tests { let req = parse_ready!(&mut buf); assert_eq!(req.inner().head.ctype, Some(ConnectionType::KeepAlive)); + + let mut buf = BytesMut::from( + "GET /test HTTP/1.0\r\n\ + connection: Keep-Alive\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + assert_eq!(req.inner().head.ctype, Some(ConnectionType::KeepAlive)); + } #[test] @@ -959,6 +976,17 @@ mod tests { assert!(req.upgrade()); assert_eq!(req.inner().head.ctype, Some(ConnectionType::Upgrade)); + + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + upgrade: Websockets\r\n\ + connection: Upgrade\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + assert!(req.upgrade()); + assert_eq!(req.inner().head.ctype, Some(ConnectionType::Upgrade)); + } #[test] From cc74435b0155b11c7405e5b9b3288721c9b5edcd Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 19 Dec 2018 18:34:56 -0800 Subject: [PATCH 106/427] drop failure crate --- Cargo.toml | 13 +- src/client/error.rs | 79 ++------ src/error.rs | 401 ++++++++++++++++++----------------------- src/response.rs | 33 ---- src/ws/client/error.rs | 68 ++----- src/ws/mod.rs | 44 ++--- 6 files changed, 235 insertions(+), 403 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d992de647..a5fc597a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,12 +51,13 @@ actix-utils = "0.1.0" # actix-utils = { path="../actix-net/actix-utils/" } base64 = "0.9" +backtrace = "0.3" bitflags = "1.0" bytes = "0.4" byteorder = "1.2" cookie = { version="0.11", features=["percent-encode"] } +derive_more = "0.13" encoding = "0.2" -failure = "0.1.3" futures = "0.1" http = "0.1.8" httparse = "1.3" @@ -74,19 +75,11 @@ serde_urlencoded = "0.5.3" time = "0.1" tokio-tcp = "0.1" tokio-timer = "0.2" -trust-dns-proto = "0.5.0" -trust-dns-resolver = "0.10.0" -url = { version="1.7", features=["query_encoding"] } - -# native-tls -native-tls = { version="0.2", optional = true } +trust-dns-resolver = "0.10.1" # openssl openssl = { version="0.10", optional = true } -# rustls -rustls = { version = "^0.14", optional = true } - [dev-dependencies] actix-web = "0.7" env_logger = "0.5" diff --git a/src/client/error.rs b/src/client/error.rs index 815bc1edc..2a5df9c97 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -1,6 +1,6 @@ use std::io; -use failure::Fail; +use derive_more::{Display, From}; use trust_dns_resolver::error::ResolveError; #[cfg(feature = "ssl")] @@ -9,71 +9,52 @@ use openssl::ssl::{Error as SslError, HandshakeError}; use crate::error::{Error, ParseError}; /// A set of errors that can occur while connecting to an HTTP host -#[derive(Fail, Debug)] +#[derive(Debug, Display, From)] pub enum ConnectorError { /// Invalid URL - #[fail(display = "Invalid URL")] + #[display(fmt = "Invalid URL")] InvalidUrl(InvalidUrlKind), /// SSL feature is not enabled - #[fail(display = "SSL is not supported")] + #[display(fmt = "SSL is not supported")] SslIsNotSupported, /// SSL error #[cfg(feature = "ssl")] - #[fail(display = "{}", _0)] - SslError(#[cause] SslError), + #[display(fmt = "{}", _0)] + SslError(SslError), /// Failed to resolve the hostname - #[fail(display = "Failed resolving hostname: {}", _0)] + #[display(fmt = "Failed resolving hostname: {}", _0)] Resolver(ResolveError), /// No dns records - #[fail(display = "No dns records found for the input")] + #[display(fmt = "No dns records found for the input")] NoRecords, /// Connecting took too long - #[fail(display = "Timeout out while establishing connection")] + #[display(fmt = "Timeout out while establishing connection")] Timeout, /// Connector has been disconnected - #[fail(display = "Internal error: connector has been disconnected")] + #[display(fmt = "Internal error: connector has been disconnected")] Disconnected, /// Connection io error - #[fail(display = "{}", _0)] - IoError(io::Error), + #[display(fmt = "{}", _0)] + Io(io::Error), } -#[derive(Fail, Debug)] +#[derive(Debug, Display)] pub enum InvalidUrlKind { - #[fail(display = "Missing url scheme")] + #[display(fmt = "Missing url scheme")] MissingScheme, - #[fail(display = "Unknown url scheme")] + #[display(fmt = "Unknown url scheme")] UnknownScheme, - #[fail(display = "Missing host name")] + #[display(fmt = "Missing host name")] MissingHost, } -impl From for ConnectorError { - fn from(err: io::Error) -> ConnectorError { - ConnectorError::IoError(err) - } -} - -impl From for ConnectorError { - fn from(err: ResolveError) -> ConnectorError { - ConnectorError::Resolver(err) - } -} - -#[cfg(feature = "ssl")] -impl From for ConnectorError { - fn from(err: SslError) -> ConnectorError { - ConnectorError::SslError(err) - } -} - #[cfg(feature = "ssl")] impl From> for ConnectorError { fn from(err: HandshakeError) -> ConnectorError { @@ -90,10 +71,10 @@ impl From> for ConnectorError { } /// A set of errors that can occur during request sending and response reading -#[derive(Debug)] +#[derive(Debug, Display, From)] pub enum SendRequestError { /// Failed to connect to host - // #[fail(display = "Failed to connect to host: {}", _0)] + #[display(fmt = "Failed to connect to host: {}", _0)] Connector(ConnectorError), /// Error sending request Send(io::Error), @@ -102,27 +83,3 @@ pub enum SendRequestError { /// Error sending request body Body(Error), } - -impl From for SendRequestError { - fn from(err: io::Error) -> SendRequestError { - SendRequestError::Send(err) - } -} - -impl From for SendRequestError { - fn from(err: ConnectorError) -> SendRequestError { - SendRequestError::Connector(err) - } -} - -impl From for SendRequestError { - fn from(err: ParseError) -> SendRequestError { - SendRequestError::Response(err) - } -} - -impl From for SendRequestError { - fn from(err: Error) -> SendRequestError { - SendRequestError::Body(err) - } -} diff --git a/src/error.rs b/src/error.rs index 49602bd98..8af422fc8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,13 +1,14 @@ //! Error and Result module +use std::cell::RefCell; use std::io::Error as IoError; use std::str::Utf8Error; use std::string::FromUtf8Error; -use std::sync::Mutex; use std::{fmt, io, result}; // use actix::MailboxError; +use backtrace::Backtrace; use cookie; -use failure::{self, Backtrace, Fail}; +use derive_more::{Display, From}; use futures::Canceled; use http::uri::InvalidUri; use http::{header, Error as HttpError, StatusCode}; @@ -21,7 +22,7 @@ use tokio_timer::Error as TimerError; pub use cookie::ParseError as CookieParseError; use crate::body::Body; -use crate::response::{Response, ResponseParts}; +use crate::response::Response; /// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html) /// for actix web operations @@ -47,20 +48,6 @@ pub struct Error { } impl Error { - /// Deprecated way to reference the underlying response error. - #[deprecated( - since = "0.6.0", - note = "please use `Error::as_response_error()` instead" - )] - pub fn cause(&self) -> &ResponseError { - self.cause.as_ref() - } - - /// Returns a reference to the underlying cause of this `Error` as `Fail` - pub fn as_fail(&self) -> &Fail { - self.cause.as_fail() - } - /// Returns the reference to the underlying `ResponseError`. pub fn as_response_error(&self) -> &ResponseError { self.cause.as_ref() @@ -78,27 +65,27 @@ impl Error { } } - /// Attempts to downcast this `Error` to a particular `Fail` type by - /// reference. - /// - /// If the underlying error is not of type `T`, this will return `None`. - pub fn downcast_ref(&self) -> Option<&T> { - // in the most trivial way the cause is directly of the requested type. - if let Some(rv) = Fail::downcast_ref(self.cause.as_fail()) { - return Some(rv); - } + // /// Attempts to downcast this `Error` to a particular `Fail` type by + // /// reference. + // /// + // /// If the underlying error is not of type `T`, this will return `None`. + // pub fn downcast_ref(&self) -> Option<&T> { + // // in the most trivial way the cause is directly of the requested type. + // if let Some(rv) = Fail::downcast_ref(self.cause.as_fail()) { + // return Some(rv); + // } - // in the more complex case the error has been constructed from a failure - // error. This happens because we implement From by - // calling compat() and then storing it here. In failure this is - // represented by a failure::Error being wrapped in a failure::Compat. - // - // So we first downcast into that compat, to then further downcast through - // the failure's Error downcasting system into the original failure. - let compat: Option<&failure::Compat> = - Fail::downcast_ref(self.cause.as_fail()); - compat.and_then(|e| e.get_ref().downcast_ref()) - } + // // in the more complex case the error has been constructed from a failure + // // error. This happens because we implement From by + // // calling compat() and then storing it here. In failure this is + // // represented by a failure::Error being wrapped in a failure::Compat. + // // + // // So we first downcast into that compat, to then further downcast through + // // the failure's Error downcasting system into the original failure. + // let compat: Option<&failure::Compat> = + // Fail::downcast_ref(self.cause.as_fail()); + // compat.and_then(|e| e.get_ref().downcast_ref()) + // } /// Converts error to a response instance and set error message as response body pub fn response_with_message(self) -> Response { @@ -108,36 +95,41 @@ impl Error { } } -/// Helper trait to downcast a response error into a fail. -/// -/// This is currently not exposed because it's unclear if this is the best way -/// to achieve the downcasting on `Error` for which this is needed. -#[doc(hidden)] -pub trait InternalResponseErrorAsFail { - #[doc(hidden)] - fn as_fail(&self) -> &Fail; - #[doc(hidden)] - fn as_mut_fail(&mut self) -> &mut Fail; -} +// /// Helper trait to downcast a response error into a fail. +// /// +// /// This is currently not exposed because it's unclear if this is the best way +// /// to achieve the downcasting on `Error` for which this is needed. +// #[doc(hidden)] +// pub trait InternalResponseErrorAsFail { +// #[doc(hidden)] +// fn as_fail(&self) -> &Fail; +// #[doc(hidden)] +// fn as_mut_fail(&mut self) -> &mut Fail; +// } -#[doc(hidden)] -impl InternalResponseErrorAsFail for T { - fn as_fail(&self) -> &Fail { - self - } - fn as_mut_fail(&mut self) -> &mut Fail { - self - } -} +// #[doc(hidden)] +// impl InternalResponseErrorAsFail for T { +// fn as_fail(&self) -> &Fail { +// self +// } +// fn as_mut_fail(&mut self) -> &mut Fail { +// self +// } +// } /// Error that can be converted to `Response` -pub trait ResponseError: Fail + InternalResponseErrorAsFail { +pub trait ResponseError: fmt::Debug + fmt::Display { /// Create response for error /// /// Internal server error is generated by default. fn error_response(&self) -> Response { Response::new(StatusCode::INTERNAL_SERVER_ERROR) } + + /// Response + fn backtrace(&self) -> Option<&Backtrace> { + None + } } impl fmt::Display for Error { @@ -169,7 +161,7 @@ impl From for Response { } /// `Error` for any error that implements `ResponseError` -impl From for Error { +impl From for Error { fn from(err: T) -> Error { let backtrace = if err.backtrace().is_none() { Some(Backtrace::new()) @@ -183,17 +175,17 @@ impl From for Error { } } -/// Compatibility for `failure::Error` -impl ResponseError for failure::Compat where - T: fmt::Display + fmt::Debug + Sync + Send + 'static -{ -} +// /// Compatibility for `failure::Error` +// impl ResponseError for failure::Compat where +// T: fmt::Display + fmt::Debug + Sync + Send + 'static +// { +// } -impl From for Error { - fn from(err: failure::Error) -> Error { - err.compat().into() - } -} +// impl From for Error { +// fn from(err: failure::Error) -> Error { +// err.compat().into() +// } +// } /// `InternalServerError` for `JsonError` impl ResponseError for JsonError {} @@ -254,40 +246,40 @@ impl ResponseError for Canceled {} // impl ResponseError for MailboxError {} /// A set of errors that can occur during parsing HTTP streams -#[derive(Fail, Debug)] +#[derive(Debug, Display)] pub enum ParseError { /// An invalid `Method`, such as `GE.T`. - #[fail(display = "Invalid Method specified")] + #[display(fmt = "Invalid Method specified")] Method, /// An invalid `Uri`, such as `exam ple.domain`. - #[fail(display = "Uri error: {}", _0)] + #[display(fmt = "Uri error: {}", _0)] Uri(InvalidUri), /// An invalid `HttpVersion`, such as `HTP/1.1` - #[fail(display = "Invalid HTTP version specified")] + #[display(fmt = "Invalid HTTP version specified")] Version, /// An invalid `Header`. - #[fail(display = "Invalid Header provided")] + #[display(fmt = "Invalid Header provided")] Header, /// A message head is too large to be reasonable. - #[fail(display = "Message head is too large")] + #[display(fmt = "Message head is too large")] TooLarge, /// A message reached EOF, but is not complete. - #[fail(display = "Message is incomplete")] + #[display(fmt = "Message is incomplete")] Incomplete, /// An invalid `Status`, such as `1337 ELITE`. - #[fail(display = "Invalid Status provided")] + #[display(fmt = "Invalid Status provided")] Status, /// A timeout occurred waiting for an IO event. #[allow(dead_code)] - #[fail(display = "Timeout")] + #[display(fmt = "Timeout")] Timeout, /// An `io::Error` that occurred while trying to read or write to a network /// stream. - #[fail(display = "IO error: {}", _0)] - Io(#[cause] IoError), + #[display(fmt = "IO error: {}", _0)] + Io(IoError), /// Parsing a field as string failed - #[fail(display = "UTF8 error: {}", _0)] - Utf8(#[cause] Utf8Error), + #[display(fmt = "UTF8 error: {}", _0)] + Utf8(Utf8Error), } /// Return `BadRequest` for `ParseError` @@ -335,20 +327,20 @@ impl From for ParseError { } } -#[derive(Fail, Debug)] +#[derive(Display, Debug)] /// A set of errors that can occur during payload parsing pub enum PayloadError { /// A payload reached EOF, but is not complete. - #[fail(display = "A payload reached EOF, but is not complete.")] + #[display(fmt = "A payload reached EOF, but is not complete.")] Incomplete(Option), /// Content encoding stream corruption - #[fail(display = "Can not decode content-encoding.")] + #[display(fmt = "Can not decode content-encoding.")] EncodingCorrupted, /// A payload reached size limit. - #[fail(display = "A payload reached size limit.")] + #[display(fmt = "A payload reached size limit.")] Overflow, /// A payload length is unknown. - #[fail(display = "A payload length is unknown.")] + #[display(fmt = "A payload length is unknown.")] UnknownLength, } @@ -378,44 +370,44 @@ impl ResponseError for cookie::ParseError { } } -#[derive(Debug)] +#[derive(Debug, Display)] /// A set of errors that can occur during dispatching http requests pub enum DispatchError { /// Service error - // #[fail(display = "Application specific error: {}", _0)] + #[display(fmt = "Service specific error: {:?}", _0)] Service(E), /// An `io::Error` that occurred while trying to read or write to a network /// stream. - // #[fail(display = "IO error: {}", _0)] + #[display(fmt = "IO error: {}", _0)] Io(io::Error), /// Http request parse error. - // #[fail(display = "Parse error: {}", _0)] + #[display(fmt = "Parse error: {}", _0)] Parse(ParseError), /// The first request did not complete within the specified timeout. - // #[fail(display = "The first request did not complete within the specified timeout")] + #[display(fmt = "The first request did not complete within the specified timeout")] SlowRequestTimeout, /// Disconnect timeout. Makes sense for ssl streams. - // #[fail(display = "Connection shutdown timeout")] + #[display(fmt = "Connection shutdown timeout")] DisconnectTimeout, /// Payload is not consumed - // #[fail(display = "Task is completed but request's payload is not consumed")] + #[display(fmt = "Task is completed but request's payload is not consumed")] PayloadIsNotConsumed, /// Malformed request - // #[fail(display = "Malformed request")] + #[display(fmt = "Malformed request")] MalformedRequest, /// Internal error - // #[fail(display = "Internal error")] + #[display(fmt = "Internal error")] InternalError, /// Unknown error - // #[fail(display = "Unknown error")] + #[display(fmt = "Unknown error")] Unknown, } @@ -432,13 +424,13 @@ impl From for DispatchError { } /// A set of error that can occure during parsing content type -#[derive(Fail, PartialEq, Debug)] +#[derive(PartialEq, Debug, Display)] pub enum ContentTypeError { /// Can not parse content type - #[fail(display = "Can not parse content type")] + #[display(fmt = "Can not parse content type")] ParseError, /// Unknown content encoding - #[fail(display = "Unknown content encoding")] + #[display(fmt = "Unknown content encoding")] UnknownEncoding, } @@ -450,28 +442,26 @@ impl ResponseError for ContentTypeError { } /// A set of errors that can occur during parsing urlencoded payloads -#[derive(Fail, Debug)] +#[derive(Debug, Display, From)] pub enum UrlencodedError { /// Can not decode chunked transfer encoding - #[fail(display = "Can not decode chunked transfer encoding")] + #[display(fmt = "Can not decode chunked transfer encoding")] Chunked, /// Payload size is bigger than allowed. (default: 256kB) - #[fail( - display = "Urlencoded payload size is bigger than allowed. (default: 256kB)" - )] + #[display(fmt = "Urlencoded payload size is bigger than allowed. (default: 256kB)")] Overflow, /// Payload size is now known - #[fail(display = "Payload size is now known")] + #[display(fmt = "Payload size is now known")] UnknownLength, /// Content type error - #[fail(display = "Content type error")] + #[display(fmt = "Content type error")] ContentType, /// Parse error - #[fail(display = "Parse error")] + #[display(fmt = "Parse error")] Parse, /// Payload error - #[fail(display = "Error that occur during reading payload: {}", _0)] - Payload(#[cause] PayloadError), + #[display(fmt = "Error that occur during reading payload: {}", _0)] + Payload(PayloadError), } /// Return `BadRequest` for `UrlencodedError` @@ -485,27 +475,21 @@ impl ResponseError for UrlencodedError { } } -impl From for UrlencodedError { - fn from(err: PayloadError) -> UrlencodedError { - UrlencodedError::Payload(err) - } -} - /// A set of errors that can occur during parsing json payloads -#[derive(Fail, Debug)] +#[derive(Debug, Display, From)] pub enum JsonPayloadError { /// Payload size is bigger than allowed. (default: 256kB) - #[fail(display = "Json payload size is bigger than allowed. (default: 256kB)")] + #[display(fmt = "Json payload size is bigger than allowed. (default: 256kB)")] Overflow, /// Content type error - #[fail(display = "Content type error")] + #[display(fmt = "Content type error")] ContentType, /// Deserialize error - #[fail(display = "Json deserialize error: {}", _0)] - Deserialize(#[cause] JsonError), + #[display(fmt = "Json deserialize error: {}", _0)] + Deserialize(JsonError), /// Payload error - #[fail(display = "Error that occur during reading payload: {}", _0)] - Payload(#[cause] PayloadError), + #[display(fmt = "Error that occur during reading payload: {}", _0)] + Payload(PayloadError), } /// Return `BadRequest` for `UrlencodedError` @@ -518,19 +502,8 @@ impl ResponseError for JsonPayloadError { } } -impl From for JsonPayloadError { - fn from(err: PayloadError) -> JsonPayloadError { - JsonPayloadError::Payload(err) - } -} - -impl From for JsonPayloadError { - fn from(err: JsonError) -> JsonPayloadError { - JsonPayloadError::Deserialize(err) - } -} - /// Error type returned when reading body as lines. +#[derive(From)] pub enum ReadlinesError { /// Error when decoding a line. EncodingError, @@ -542,18 +515,6 @@ pub enum ReadlinesError { ContentTypeError(ContentTypeError), } -impl From for ReadlinesError { - fn from(err: PayloadError) -> Self { - ReadlinesError::PayloadError(err) - } -} - -impl From for ReadlinesError { - fn from(err: ContentTypeError) -> Self { - ReadlinesError::ContentTypeError(err) - } -} - /// Helper type that can wrap any error and generate custom response. /// /// In following example any `io::Error` will be converted into "BAD REQUEST" @@ -578,7 +539,7 @@ pub struct InternalError { enum InternalErrorType { Status(StatusCode), - Response(Box>>), + Response(RefCell>), } impl InternalError { @@ -593,27 +554,17 @@ impl InternalError { /// Create `InternalError` with predefined `Response`. pub fn from_response(cause: T, response: Response) -> Self { - let resp = response.into_parts(); InternalError { cause, - status: InternalErrorType::Response(Box::new(Mutex::new(Some(resp)))), + status: InternalErrorType::Response(RefCell::new(Some(response))), backtrace: Backtrace::new(), } } } -impl Fail for InternalError -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - fn backtrace(&self) -> Option<&Backtrace> { - Some(&self.backtrace) - } -} - impl fmt::Debug for InternalError where - T: Send + Sync + fmt::Debug + 'static, + T: fmt::Debug + 'static, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Debug::fmt(&self.cause, f) @@ -622,7 +573,7 @@ where impl fmt::Display for InternalError where - T: Send + Sync + fmt::Display + 'static, + T: fmt::Display + 'static, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.cause, f) @@ -631,14 +582,18 @@ where impl ResponseError for InternalError where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { + fn backtrace(&self) -> Option<&Backtrace> { + Some(&self.backtrace) + } + fn error_response(&self) -> Response { match self.status { InternalErrorType::Status(st) => Response::new(st), InternalErrorType::Response(ref resp) => { - if let Some(resp) = resp.lock().unwrap().take() { - Response::<()>::from_parts(resp) + if let Some(resp) = resp.borrow_mut().take() { + resp } else { Response::new(StatusCode::INTERNAL_SERVER_ERROR) } @@ -659,7 +614,7 @@ impl From for Error { #[allow(non_snake_case)] pub fn ErrorBadRequest(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::BAD_REQUEST).into() } @@ -669,7 +624,7 @@ where #[allow(non_snake_case)] pub fn ErrorUnauthorized(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::UNAUTHORIZED).into() } @@ -679,7 +634,7 @@ where #[allow(non_snake_case)] pub fn ErrorForbidden(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::FORBIDDEN).into() } @@ -689,7 +644,7 @@ where #[allow(non_snake_case)] pub fn ErrorNotFound(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::NOT_FOUND).into() } @@ -699,7 +654,7 @@ where #[allow(non_snake_case)] pub fn ErrorMethodNotAllowed(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::METHOD_NOT_ALLOWED).into() } @@ -709,7 +664,7 @@ where #[allow(non_snake_case)] pub fn ErrorRequestTimeout(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::REQUEST_TIMEOUT).into() } @@ -719,7 +674,7 @@ where #[allow(non_snake_case)] pub fn ErrorConflict(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::CONFLICT).into() } @@ -729,7 +684,7 @@ where #[allow(non_snake_case)] pub fn ErrorGone(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::GONE).into() } @@ -739,7 +694,7 @@ where #[allow(non_snake_case)] pub fn ErrorPreconditionFailed(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::PRECONDITION_FAILED).into() } @@ -749,7 +704,7 @@ where #[allow(non_snake_case)] pub fn ErrorExpectationFailed(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::EXPECTATION_FAILED).into() } @@ -759,7 +714,7 @@ where #[allow(non_snake_case)] pub fn ErrorInternalServerError(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::INTERNAL_SERVER_ERROR).into() } @@ -769,7 +724,7 @@ where #[allow(non_snake_case)] pub fn ErrorNotImplemented(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::NOT_IMPLEMENTED).into() } @@ -779,7 +734,7 @@ where #[allow(non_snake_case)] pub fn ErrorBadGateway(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::BAD_GATEWAY).into() } @@ -789,7 +744,7 @@ where #[allow(non_snake_case)] pub fn ErrorServiceUnavailable(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::SERVICE_UNAVAILABLE).into() } @@ -799,7 +754,7 @@ where #[allow(non_snake_case)] pub fn ErrorGatewayTimeout(err: T) -> Error where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, + T: fmt::Debug + fmt::Display + 'static, { InternalError::new(err, StatusCode::GATEWAY_TIMEOUT).into() } @@ -808,10 +763,8 @@ where mod tests { use super::*; use cookie::ParseError as CookieParseError; - use failure; use http::{Error as HttpError, StatusCode}; use httparse; - use std::env; use std::error::Error as StdError; use std::io; @@ -829,11 +782,10 @@ mod tests { } #[test] - fn test_as_fail() { + fn test_as_response() { let orig = io::Error::new(io::ErrorKind::Other, "other"); - let desc = orig.description().to_owned(); - let e = ParseError::Io(orig); - assert_eq!(format!("{}", e.cause().unwrap()), desc); + let e: Error = ParseError::Io(orig).into(); + assert_eq!(format!("{}", e.as_response_error()), "IO error: other"); } #[test] @@ -847,7 +799,7 @@ mod tests { let orig = io::Error::new(io::ErrorKind::Other, "other"); let desc = orig.description().to_owned(); let e = Error::from(orig); - assert_eq!(format!("{}", e.as_fail()), desc); + assert_eq!(format!("{}", e.as_response_error()), desc); } #[test] @@ -881,7 +833,7 @@ mod tests { ($from:expr => $error:pat) => { match ParseError::from($from) { e @ $error => { - let desc = format!("{}", e.cause().unwrap()); + let desc = format!("{}", e); assert_eq!(desc, $from.description().to_owned()); } _ => unreachable!("{:?}", $from), @@ -891,8 +843,7 @@ mod tests { #[test] fn test_from() { - from_and_cause!(io::Error::new(io::ErrorKind::Other, "other") => ParseError::Io(..)); - + // from_and_cause!(io::Error::new(io::ErrorKind::Other, "other") => ParseError::Io(..)); from!(httparse::Error::HeaderName => ParseError::Header); from!(httparse::Error::HeaderName => ParseError::Header); from!(httparse::Error::HeaderValue => ParseError::Header); @@ -903,22 +854,22 @@ mod tests { from!(httparse::Error::Version => ParseError::Version); } - #[test] - fn failure_error() { - const NAME: &str = "RUST_BACKTRACE"; - let old_tb = env::var(NAME); - env::set_var(NAME, "0"); - let error = failure::err_msg("Hello!"); - let resp: Error = error.into(); - assert_eq!( - format!("{:?}", resp), - "Compat { error: ErrorMessage { msg: \"Hello!\" } }\n\n" - ); - match old_tb { - Ok(x) => env::set_var(NAME, x), - _ => env::remove_var(NAME), - } - } + // #[test] + // fn failure_error() { + // const NAME: &str = "RUST_BACKTRACE"; + // let old_tb = env::var(NAME); + // env::set_var(NAME, "0"); + // let error = failure::err_msg("Hello!"); + // let resp: Error = error.into(); + // assert_eq!( + // format!("{:?}", resp), + // "Compat { error: ErrorMessage { msg: \"Hello!\" } }\n\n" + // ); + // match old_tb { + // Ok(x) => env::set_var(NAME, x), + // _ => env::remove_var(NAME), + // } + // } #[test] fn test_internal_error() { @@ -928,31 +879,31 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } - #[test] - fn test_error_downcasting_direct() { - #[derive(Debug, Fail)] - #[fail(display = "demo error")] - struct DemoError; + // #[test] + // fn test_error_downcasting_direct() { + // #[derive(Debug, Display)] + // #[display(fmt = "demo error")] + // struct DemoError; - impl ResponseError for DemoError {} + // impl ResponseError for DemoError {} - let err: Error = DemoError.into(); - let err_ref: &DemoError = err.downcast_ref().unwrap(); - assert_eq!(err_ref.to_string(), "demo error"); - } + // let err: Error = DemoError.into(); + // let err_ref: &DemoError = err.downcast_ref().unwrap(); + // assert_eq!(err_ref.to_string(), "demo error"); + // } - #[test] - fn test_error_downcasting_compat() { - #[derive(Debug, Fail)] - #[fail(display = "demo error")] - struct DemoError; + // #[test] + // fn test_error_downcasting_compat() { + // #[derive(Debug, Display)] + // #[display(fmt = "demo error")] + // struct DemoError; - impl ResponseError for DemoError {} + // impl ResponseError for DemoError {} - let err: Error = failure::Error::from(DemoError).into(); - let err_ref: &DemoError = err.downcast_ref().unwrap(); - assert_eq!(err_ref.to_string(), "demo error"); - } + // let err: Error = failure::Error::from(DemoError).into(); + // let err_ref: &DemoError = err.downcast_ref().unwrap(); + // assert_eq!(err_ref.to_string(), "demo error"); + // } #[test] fn test_error_helpers() { diff --git a/src/response.rs b/src/response.rs index 5cff612f7..49f2b63fb 100644 --- a/src/response.rs +++ b/src/response.rs @@ -240,17 +240,6 @@ impl Response { pub(crate) fn release(self) { ResponsePool::release(self.0); } - - pub(crate) fn into_parts(self) -> ResponseParts { - self.0.into_parts() - } - - pub(crate) fn from_parts(parts: ResponseParts) -> Response { - Response( - Box::new(InnerResponse::from_parts(parts)), - ResponseBody::Body(Body::Empty), - ) - } } impl fmt::Debug for Response { @@ -736,11 +725,6 @@ struct InnerResponse { pool: &'static ResponsePool, } -pub(crate) struct ResponseParts { - head: ResponseHead, - error: Option, -} - impl InnerResponse { #[inline] fn new(status: StatusCode, pool: &'static ResponsePool) -> InnerResponse { @@ -757,23 +741,6 @@ impl InnerResponse { error: None, } } - - /// This is for failure, we can not have Send + Sync on Streaming and Actor response - fn into_parts(self) -> ResponseParts { - ResponseParts { - head: self.head, - error: self.error, - } - } - - fn from_parts(parts: ResponseParts) -> InnerResponse { - InnerResponse { - head: parts.head, - response_size: 0, - error: parts.error, - pool: ResponsePool::pool(), - } - } } /// Internal use only! diff --git a/src/ws/client/error.rs b/src/ws/client/error.rs index 02c723755..1eccb0b95 100644 --- a/src/ws/client/error.rs +++ b/src/ws/client/error.rs @@ -2,82 +2,52 @@ use std::io; use actix_connector::ConnectorError; -use failure::Fail; +use derive_more::{Display, From}; use http::{header::HeaderValue, Error as HttpError, StatusCode}; use crate::error::ParseError; use crate::ws::ProtocolError; /// Websocket client error -#[derive(Fail, Debug)] +#[derive(Debug, Display, From)] pub enum ClientError { /// Invalid url - #[fail(display = "Invalid url")] + #[display(fmt = "Invalid url")] InvalidUrl, /// Invalid response status - #[fail(display = "Invalid response status")] + #[display(fmt = "Invalid response status")] InvalidResponseStatus(StatusCode), /// Invalid upgrade header - #[fail(display = "Invalid upgrade header")] + #[display(fmt = "Invalid upgrade header")] InvalidUpgradeHeader, /// Invalid connection header - #[fail(display = "Invalid connection header")] + #[display(fmt = "Invalid connection header")] InvalidConnectionHeader(HeaderValue), /// Missing CONNECTION header - #[fail(display = "Missing CONNECTION header")] + #[display(fmt = "Missing CONNECTION header")] MissingConnectionHeader, /// Missing SEC-WEBSOCKET-ACCEPT header - #[fail(display = "Missing SEC-WEBSOCKET-ACCEPT header")] + #[display(fmt = "Missing SEC-WEBSOCKET-ACCEPT header")] MissingWebSocketAcceptHeader, /// Invalid challenge response - #[fail(display = "Invalid challenge response")] + #[display(fmt = "Invalid challenge response")] InvalidChallengeResponse(String, HeaderValue), /// Http parsing error - #[fail(display = "Http parsing error")] - Http(#[cause] HttpError), + #[display(fmt = "Http parsing error")] + Http(HttpError), /// Response parsing error - #[fail(display = "Response parsing error: {}", _0)] - ParseError(#[cause] ParseError), + #[display(fmt = "Response parsing error: {}", _0)] + ParseError(ParseError), /// Protocol error - #[fail(display = "{}", _0)] - Protocol(#[cause] ProtocolError), + #[display(fmt = "{}", _0)] + Protocol(ProtocolError), /// Connect error - #[fail(display = "Connector error: {:?}", _0)] + #[display(fmt = "Connector error: {:?}", _0)] Connect(ConnectorError), /// IO Error - #[fail(display = "{}", _0)] - Io(#[cause] io::Error), + #[display(fmt = "{}", _0)] + Io(io::Error), /// "Disconnected" - #[fail(display = "Disconnected")] + #[display(fmt = "Disconnected")] Disconnected, } - -impl From for ClientError { - fn from(err: HttpError) -> ClientError { - ClientError::Http(err) - } -} - -impl From for ClientError { - fn from(err: ConnectorError) -> ClientError { - ClientError::Connect(err) - } -} - -impl From for ClientError { - fn from(err: ProtocolError) -> ClientError { - ClientError::Protocol(err) - } -} - -impl From for ClientError { - fn from(err: io::Error) -> ClientError { - ClientError::Io(err) - } -} - -impl From for ClientError { - fn from(err: ParseError) -> ClientError { - ClientError::ParseError(err) - } -} diff --git a/src/ws/mod.rs b/src/ws/mod.rs index 02667c964..2d629c73b 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -5,7 +5,7 @@ //! communicate with the peer. use std::io; -use failure::Fail; +use derive_more::{Display, From}; use http::{header, Method, StatusCode}; use crate::error::ResponseError; @@ -28,65 +28,59 @@ pub use self::service::VerifyWebSockets; pub use self::transport::Transport; /// Websocket protocol errors -#[derive(Fail, Debug)] +#[derive(Debug, Display, From)] pub enum ProtocolError { /// Received an unmasked frame from client - #[fail(display = "Received an unmasked frame from client")] + #[display(fmt = "Received an unmasked frame from client")] UnmaskedFrame, /// Received a masked frame from server - #[fail(display = "Received a masked frame from server")] + #[display(fmt = "Received a masked frame from server")] MaskedFrame, /// Encountered invalid opcode - #[fail(display = "Invalid opcode: {}", _0)] + #[display(fmt = "Invalid opcode: {}", _0)] InvalidOpcode(u8), /// Invalid control frame length - #[fail(display = "Invalid control frame length: {}", _0)] + #[display(fmt = "Invalid control frame length: {}", _0)] InvalidLength(usize), /// Bad web socket op code - #[fail(display = "Bad web socket op code")] + #[display(fmt = "Bad web socket op code")] BadOpCode, /// A payload reached size limit. - #[fail(display = "A payload reached size limit.")] + #[display(fmt = "A payload reached size limit.")] Overflow, /// Continuation is not supported - #[fail(display = "Continuation is not supported.")] + #[display(fmt = "Continuation is not supported.")] NoContinuation, /// Bad utf-8 encoding - #[fail(display = "Bad utf-8 encoding.")] + #[display(fmt = "Bad utf-8 encoding.")] BadEncoding, /// Io error - #[fail(display = "io error: {}", _0)] - Io(#[cause] io::Error), + #[display(fmt = "io error: {}", _0)] + Io(io::Error), } impl ResponseError for ProtocolError {} -impl From for ProtocolError { - fn from(err: io::Error) -> ProtocolError { - ProtocolError::Io(err) - } -} - /// Websocket handshake errors -#[derive(Fail, PartialEq, Debug)] +#[derive(PartialEq, Debug, Display)] pub enum HandshakeError { /// Only get method is allowed - #[fail(display = "Method not allowed")] + #[display(fmt = "Method not allowed")] GetMethodRequired, /// Upgrade header if not set to websocket - #[fail(display = "Websocket upgrade is expected")] + #[display(fmt = "Websocket upgrade is expected")] NoWebsocketUpgrade, /// Connection header is not set to upgrade - #[fail(display = "Connection upgrade is expected")] + #[display(fmt = "Connection upgrade is expected")] NoConnectionUpgrade, /// Websocket version header is not set - #[fail(display = "Websocket version header is required")] + #[display(fmt = "Websocket version header is required")] NoVersionHeader, /// Unsupported websocket version - #[fail(display = "Unsupported version")] + #[display(fmt = "Unsupported version")] UnsupportedVersion, /// Websocket key is not set or wrong - #[fail(display = "Unknown websocket key")] + #[display(fmt = "Unknown websocket key")] BadWebsocketKey, } From 42277c5c8f8a3db0fa1e0b445235154e4f87a9fc Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 26 Jan 2019 22:09:26 -0800 Subject: [PATCH 107/427] update deps --- Cargo.toml | 10 +++++----- src/h1/decoder.rs | 5 ++--- src/ws/transport.rs | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a5fc597a4..c7622fe2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ session = ["cookie/secure"] ssl = ["openssl", "actix-connector/ssl"] [dependencies] -actix-service = "0.1.3" +actix-service = "0.1.6" actix-codec = "0.1.0" actix-connector = "0.1.0" actix-rt = "0.1.0" @@ -50,7 +50,7 @@ actix-utils = "0.1.0" # actix-server = { path="../actix-net/actix-server/" } # actix-utils = { path="../actix-net/actix-utils/" } -base64 = "0.9" +base64 = "0.10" backtrace = "0.3" bitflags = "1.0" bytes = "0.4" @@ -66,7 +66,7 @@ log = "0.4" mime = "0.3" net2 = "0.2" percent-encoding = "1.0" -rand = "0.5" +rand = "0.6" serde = "1.0" serde_json = "1.0" sha1 = "0.6" @@ -75,14 +75,14 @@ serde_urlencoded = "0.5.3" time = "0.1" tokio-tcp = "0.1" tokio-timer = "0.2" -trust-dns-resolver = "0.10.1" +trust-dns-resolver = { version="0.11.0-alpha.1", default-features = false } # openssl openssl = { version="0.10", optional = true } [dev-dependencies] actix-web = "0.7" -env_logger = "0.5" +env_logger = "0.6" serde_derive = "1.0" [profile.release] diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index b2c410c47..7c17e909a 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -102,7 +102,8 @@ pub(crate) trait MessageType: Sized { } // connection keep-alive state header::CONNECTION => { - ka = if let Ok(conn) = value.to_str().map(|conn| conn.trim()) { + ka = if let Ok(conn) = value.to_str().map(|conn| conn.trim()) + { if conn.eq_ignore_ascii_case("keep-alive") { Some(ConnectionType::KeepAlive) } else if conn.eq_ignore_ascii_case("close") { @@ -925,7 +926,6 @@ mod tests { let req = parse_ready!(&mut buf); assert_eq!(req.inner().head.ctype, Some(ConnectionType::KeepAlive)); - } #[test] @@ -986,7 +986,6 @@ mod tests { assert!(req.upgrade()); assert_eq!(req.inner().head.ctype, Some(ConnectionType::Upgrade)); - } #[test] diff --git a/src/ws/transport.rs b/src/ws/transport.rs index f59ad67a7..6a4f4d227 100644 --- a/src/ws/transport.rs +++ b/src/ws/transport.rs @@ -7,7 +7,7 @@ use super::{Codec, Frame, Message}; pub struct Transport where - S: Service, + S: Service + 'static, T: AsyncRead + AsyncWrite, { inner: FramedTransport, From c3d3e8b465b7406484ee6f5d845426e033a15c60 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 27 Jan 2019 10:59:07 -0800 Subject: [PATCH 108/427] move TestServer to separate crate --- Cargo.toml | 21 ++-- src/client/pool.rs | 13 ++- src/config.rs | 11 +- src/httpmessage.rs | 5 +- src/json.rs | 2 +- src/lib.rs | 3 - src/test.rs | 227 +---------------------------------------- test-server/Cargo.toml | 64 ++++++++++++ test-server/src/lib.rs | 227 +++++++++++++++++++++++++++++++++++++++++ tests/test_client.rs | 3 +- tests/test_server.rs | 41 ++++---- tests/test_ws.rs | 5 +- 12 files changed, 345 insertions(+), 277 deletions(-) create mode 100644 test-server/Cargo.toml create mode 100644 test-server/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index c7622fe2d..426712201 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,15 +39,13 @@ ssl = ["openssl", "actix-connector/ssl"] [dependencies] actix-service = "0.1.6" actix-codec = "0.1.0" -actix-connector = "0.1.0" -actix-rt = "0.1.0" -actix-server = "0.1.0" -actix-utils = "0.1.0" +# actix-connector = "0.1.0" +# actix-utils = "0.1.0" +actix-connector = { git = "https://github.com/actix/actix-net.git" } +actix-utils = { git = "https://github.com/actix/actix-net.git" } # actix-codec = { path="../actix-net/actix-codec/" } # actix-connector = { path="../actix-net/actix-connector/" } -# actix-rt = { path="../actix-net/actix-rt/" } -# actix-server = { path="../actix-net/actix-server/" } # actix-utils = { path="../actix-net/actix-utils/" } base64 = "0.10" @@ -64,7 +62,6 @@ httparse = "1.3" indexmap = "1.0" log = "0.4" mime = "0.3" -net2 = "0.2" percent-encoding = "1.0" rand = "0.6" serde = "1.0" @@ -73,19 +70,17 @@ sha1 = "0.6" slab = "0.4" serde_urlencoded = "0.5.3" time = "0.1" -tokio-tcp = "0.1" tokio-timer = "0.2" +tokio-current-thread = "0.1" trust-dns-resolver = { version="0.11.0-alpha.1", default-features = false } # openssl openssl = { version="0.10", optional = true } [dev-dependencies] +actix-rt = "0.1.0" actix-web = "0.7" +actix-server = "0.1" +actix-http-test = { path="test-server" } env_logger = "0.6" serde_derive = "1.0" - -[profile.release] -lto = true -opt-level = 3 -codegen-units = 1 diff --git a/src/client/pool.rs b/src/client/pool.rs index 94e96899e..11828dcb8 100644 --- a/src/client/pool.rs +++ b/src/client/pool.rs @@ -5,7 +5,6 @@ use std::rc::Rc; use std::time::{Duration, Instant}; use actix_codec::{AsyncRead, AsyncWrite}; -use actix_rt::spawn; use actix_service::Service; use futures::future::{ok, Either, FutureResult}; use futures::sync::oneshot; @@ -265,7 +264,7 @@ where inner: Rc>>, fut: F, ) { - spawn(OpenWaitingConnection { + tokio_current_thread::spawn(OpenWaitingConnection { key, fut, rx: Some(rx), @@ -408,7 +407,9 @@ where || (now - conn.created) > self.conn_lifetime { if let Some(timeout) = self.disconnect_timeout { - spawn(CloseConnection::new(conn.io, timeout)) + tokio_current_thread::spawn(CloseConnection::new( + conn.io, timeout, + )) } } else { let mut io = conn.io; @@ -417,7 +418,9 @@ where Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => (), Ok(n) if n > 0 => { if let Some(timeout) = self.disconnect_timeout { - spawn(CloseConnection::new(io, timeout)) + tokio_current_thread::spawn(CloseConnection::new( + io, timeout, + )) } continue; } @@ -433,7 +436,7 @@ where fn release_close(&mut self, io: Io) { self.acquired -= 1; if let Some(timeout) = self.disconnect_timeout { - spawn(CloseConnection::new(io, timeout)) + tokio_current_thread::spawn(CloseConnection::new(io, timeout)) } } diff --git a/src/config.rs b/src/config.rs index 67c928fb7..c37601dbe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,7 +4,6 @@ use std::rc::Rc; use std::time::{Duration, Instant}; use std::{fmt, net}; -use actix_rt::spawn; use bytes::BytesMut; use futures::{future, Future}; use log::error; @@ -355,10 +354,12 @@ impl DateService { // periodic date update let s = self.clone(); - spawn(sleep(Duration::from_millis(500)).then(move |_| { - s.0.reset(); - future::ok(()) - })); + tokio_current_thread::spawn(sleep(Duration::from_millis(500)).then( + move |_| { + s.0.reset(); + future::ok(()) + }, + )); } } diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 373b7ed45..589617fc9 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -567,14 +567,15 @@ where #[cfg(test)] mod tests { - use super::*; - use crate::test::TestRequest; use encoding::all::ISO_8859_2; use encoding::Encoding; use futures::Async; use mime; use serde_derive::Deserialize; + use super::*; + use crate::test::TestRequest; + #[test] fn test_content_type() { let req = TestRequest::with_header("content-type", "text/plain").finish(); diff --git a/src/json.rs b/src/json.rs index bfecf0cc3..d06449cb0 100644 --- a/src/json.rs +++ b/src/json.rs @@ -133,12 +133,12 @@ impl Future for JsonBod #[cfg(test)] mod tests { - use super::*; use bytes::Bytes; use futures::Async; use http::header; use serde_derive::{Deserialize, Serialize}; + use super::*; use crate::test::TestRequest; impl PartialEq for JsonPayloadError { diff --git a/src/lib.rs b/src/lib.rs index 76c3a9679..5adc9236e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,9 +59,6 @@ //! * `session` - enables session support, includes `ring` crate as //! dependency //! -// #![warn(missing_docs)] -#![allow(dead_code)] - pub mod body; pub mod client; mod config; diff --git a/src/test.rs b/src/test.rs index c264ac47a..4b7e30ac3 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,29 +1,14 @@ -//! Various helpers for Actix applications to use during testing. +//! Test Various helpers for Actix applications to use during testing. use std::str::FromStr; -use std::sync::mpsc; -use std::{net, thread}; - -use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_rt::{Runtime, System}; -use actix_server::{Server, StreamServiceFactory}; -use actix_service::Service; use bytes::Bytes; use cookie::Cookie; -use futures::future::{lazy, Future}; use http::header::HeaderName; use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; -use net2::TcpBuilder; -use crate::body::MessageBody; -use crate::client::{ - ClientRequest, ClientRequestBuilder, ClientResponse, Connect, Connection, Connector, - ConnectorError, SendRequestError, -}; use crate::header::{Header, IntoHeaderValue}; use crate::payload::Payload; -use crate::request::Request; -use crate::ws; +use crate::Request; /// Test `Request` builder /// @@ -264,211 +249,3 @@ impl TestRequest { // } // } } - -/// The `TestServer` type. -/// -/// `TestServer` is very simple test server that simplify process of writing -/// integration tests cases for actix web applications. -/// -/// # Examples -/// -/// ```rust -/// # extern crate actix_web; -/// # use actix_web::*; -/// # -/// # fn my_handler(req: &HttpRequest) -> HttpResponse { -/// # HttpResponse::Ok().into() -/// # } -/// # -/// # fn main() { -/// use actix_web::test::TestServer; -/// -/// let mut srv = TestServer::new(|app| app.handler(my_handler)); -/// -/// let req = srv.get().finish().unwrap(); -/// let response = srv.execute(req.send()).unwrap(); -/// assert!(response.status().is_success()); -/// # } -/// ``` -pub struct TestServer; - -/// -pub struct TestServerRuntime { - addr: net::SocketAddr, - conn: T, - rt: Runtime, -} - -impl TestServer { - /// Start new test server with application factory - pub fn with_factory( - factory: F, - ) -> TestServerRuntime< - impl Service + Clone, - > { - let (tx, rx) = mpsc::channel(); - - // run server in separate thread - thread::spawn(move || { - let sys = System::new("actix-test-server"); - let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); - let local_addr = tcp.local_addr().unwrap(); - - Server::build() - .listen("test", tcp, factory) - .workers(1) - .disable_signals() - .start(); - - tx.send((System::current(), local_addr)).unwrap(); - sys.run(); - }); - - let (system, addr) = rx.recv().unwrap(); - System::set_current(system); - - let mut rt = Runtime::new().unwrap(); - let conn = rt - .block_on(lazy(|| Ok::<_, ()>(TestServer::new_connector()))) - .unwrap(); - - TestServerRuntime { addr, conn, rt } - } - - fn new_connector( - ) -> impl Service + Clone - { - #[cfg(feature = "ssl")] - { - use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; - - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - Connector::default().ssl(builder.build()).service() - } - #[cfg(not(feature = "ssl"))] - { - Connector::default().service() - } - } - - /// Get firat available unused address - pub fn unused_addr() -> net::SocketAddr { - let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); - let socket = TcpBuilder::new_v4().unwrap(); - socket.bind(&addr).unwrap(); - socket.reuse_address(true).unwrap(); - let tcp = socket.to_tcp_listener().unwrap(); - tcp.local_addr().unwrap() - } -} - -impl TestServerRuntime { - /// Execute future on current core - pub fn block_on(&mut self, fut: F) -> Result - where - F: Future, - { - self.rt.block_on(fut) - } - - /// Execute future on current core - pub fn execute(&mut self, fut: F) -> Result - where - F: Future, - { - self.rt.block_on(fut) - } - - /// Construct test server url - pub fn addr(&self) -> net::SocketAddr { - self.addr - } - - /// Construct test server url - pub fn url(&self, uri: &str) -> String { - if uri.starts_with('/') { - format!("http://127.0.0.1:{}{}", self.addr.port(), uri) - } else { - format!("http://127.0.0.1:{}/{}", self.addr.port(), uri) - } - } - - /// Create `GET` request - pub fn get(&self) -> ClientRequestBuilder { - ClientRequest::get(self.url("/").as_str()) - } - - /// Create `POST` request - pub fn post(&self) -> ClientRequestBuilder { - ClientRequest::post(self.url("/").as_str()) - } - - /// Create `HEAD` request - pub fn head(&self) -> ClientRequestBuilder { - ClientRequest::head(self.url("/").as_str()) - } - - /// Connect to test http server - pub fn client(&self, meth: Method, path: &str) -> ClientRequestBuilder { - ClientRequest::build() - .method(meth) - .uri(self.url(path).as_str()) - .take() - } - - /// Http connector - pub fn connector(&mut self) -> &mut T { - &mut self.conn - } - - /// Http connector - pub fn new_connector(&mut self) -> T - where - T: Clone, - { - self.conn.clone() - } - - /// Stop http server - fn stop(&mut self) { - System::current().stop(); - } -} - -impl TestServerRuntime -where - T: Service + Clone, - T::Response: Connection, -{ - /// Connect to websocket server at a given path - pub fn ws_at( - &mut self, - path: &str, - ) -> Result, ws::ClientError> { - let url = self.url(path); - self.rt - .block_on(lazy(|| ws::Client::default().call(ws::Connect::new(url)))) - } - - /// Connect to a websocket server - pub fn ws( - &mut self, - ) -> Result, ws::ClientError> { - self.ws_at("/") - } - - /// Send request and read response message - pub fn send_request( - &mut self, - req: ClientRequest, - ) -> Result { - self.rt.block_on(req.send(&mut self.conn)) - } -} - -impl Drop for TestServerRuntime { - fn drop(&mut self) { - self.stop() - } -} diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml new file mode 100644 index 000000000..5cede2dc2 --- /dev/null +++ b/test-server/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "actix-http-test" +version = "0.1.0" +authors = ["Nikolay Kim "] +description = "Actix http" +readme = "README.md" +keywords = ["http", "web", "framework", "async", "futures"] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web.git" +documentation = "https://actix.rs/api/actix-web/stable/actix_web/" +categories = ["network-programming", "asynchronous", + "web-programming::http-server", + "web-programming::websocket"] +license = "MIT/Apache-2.0" +exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] +edition = "2018" + +[package.metadata.docs.rs] +features = ["session"] + +[lib] +name = "actix_http_test" +path = "src/lib.rs" + +[features] +default = ["session"] + +# sessions feature, session require "ring" crate and c compiler +session = ["cookie/secure"] + +# openssl +ssl = ["openssl", "actix-http/ssl"] + +[dependencies] +actix-codec = "0.1" +actix-service = "0.1.6" +actix-rt = "0.1.0" +actix-server = "0.1.0" +actix-http = { path=".." } +actix-utils = { git = "https://github.com/actix/actix-net.git" } + +# actix-codec = { path="../actix-net/actix-codec/" } +# actix-rt = { path="../actix-net/actix-rt/" } +# actix-server = { path="../actix-net/actix-server/" } + +base64 = "0.10" +bytes = "0.4" +cookie = { version="0.11", features=["percent-encode"] } +futures = "0.1" +http = "0.1.8" +log = "0.4" +env_logger = "0.6" +net2 = "0.2" +serde = "1.0" +serde_json = "1.0" +sha1 = "0.6" +slab = "0.4" +serde_urlencoded = "0.5.3" +time = "0.1" +tokio-tcp = "0.1" +tokio-timer = "0.2" + +# openssl +openssl = { version="0.10", optional = true } diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs new file mode 100644 index 000000000..36c0d7d50 --- /dev/null +++ b/test-server/src/lib.rs @@ -0,0 +1,227 @@ +//! Various helpers for Actix applications to use during testing. +use std::sync::mpsc; +use std::{net, thread}; + +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_rt::{Runtime, System}; +use actix_server::{Server, StreamServiceFactory}; +use actix_service::Service; + +use futures::future::{lazy, Future}; +use http::Method; +use net2::TcpBuilder; + +use actix_http::body::MessageBody; +use actix_http::client::{ + ClientRequest, ClientRequestBuilder, ClientResponse, Connect, Connection, Connector, + ConnectorError, SendRequestError, +}; +use actix_http::ws; + +/// The `TestServer` type. +/// +/// `TestServer` is very simple test server that simplify process of writing +/// integration tests cases for actix web applications. +/// +/// # Examples +/// +/// ```rust +/// # extern crate actix_web; +/// # use actix_web::*; +/// # +/// # fn my_handler(req: &HttpRequest) -> HttpResponse { +/// # HttpResponse::Ok().into() +/// # } +/// # +/// # fn main() { +/// use actix_web::test::TestServer; +/// +/// let mut srv = TestServer::new(|app| app.handler(my_handler)); +/// +/// let req = srv.get().finish().unwrap(); +/// let response = srv.execute(req.send()).unwrap(); +/// assert!(response.status().is_success()); +/// # } +/// ``` +pub struct TestServer; + +/// +pub struct TestServerRuntime { + addr: net::SocketAddr, + conn: T, + rt: Runtime, +} + +impl TestServer { + /// Start new test server with application factory + pub fn with_factory( + factory: F, + ) -> TestServerRuntime< + impl Service + Clone, + > { + let (tx, rx) = mpsc::channel(); + + // run server in separate thread + thread::spawn(move || { + let sys = System::new("actix-test-server"); + let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); + let local_addr = tcp.local_addr().unwrap(); + + Server::build() + .listen("test", tcp, factory) + .workers(1) + .disable_signals() + .start(); + + tx.send((System::current(), local_addr)).unwrap(); + sys.run(); + }); + + let (system, addr) = rx.recv().unwrap(); + System::set_current(system); + + let mut rt = Runtime::new().unwrap(); + let conn = rt + .block_on(lazy(|| Ok::<_, ()>(TestServer::new_connector()))) + .unwrap(); + + TestServerRuntime { addr, conn, rt } + } + + fn new_connector( + ) -> impl Service + Clone + { + #[cfg(feature = "ssl")] + { + use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; + + let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); + builder.set_verify(SslVerifyMode::NONE); + Connector::default().ssl(builder.build()).service() + } + #[cfg(not(feature = "ssl"))] + { + Connector::default().service() + } + } + + /// Get firat available unused address + pub fn unused_addr() -> net::SocketAddr { + let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); + let socket = TcpBuilder::new_v4().unwrap(); + socket.bind(&addr).unwrap(); + socket.reuse_address(true).unwrap(); + let tcp = socket.to_tcp_listener().unwrap(); + tcp.local_addr().unwrap() + } +} + +impl TestServerRuntime { + /// Execute future on current core + pub fn block_on(&mut self, fut: F) -> Result + where + F: Future, + { + self.rt.block_on(fut) + } + + /// Execute future on current core + pub fn execute(&mut self, fut: F) -> Result + where + F: Future, + { + self.rt.block_on(fut) + } + + /// Construct test server url + pub fn addr(&self) -> net::SocketAddr { + self.addr + } + + /// Construct test server url + pub fn url(&self, uri: &str) -> String { + if uri.starts_with('/') { + format!("http://127.0.0.1:{}{}", self.addr.port(), uri) + } else { + format!("http://127.0.0.1:{}/{}", self.addr.port(), uri) + } + } + + /// Create `GET` request + pub fn get(&self) -> ClientRequestBuilder { + ClientRequest::get(self.url("/").as_str()) + } + + /// Create `POST` request + pub fn post(&self) -> ClientRequestBuilder { + ClientRequest::post(self.url("/").as_str()) + } + + /// Create `HEAD` request + pub fn head(&self) -> ClientRequestBuilder { + ClientRequest::head(self.url("/").as_str()) + } + + /// Connect to test http server + pub fn client(&self, meth: Method, path: &str) -> ClientRequestBuilder { + ClientRequest::build() + .method(meth) + .uri(self.url(path).as_str()) + .take() + } + + /// Http connector + pub fn connector(&mut self) -> &mut T { + &mut self.conn + } + + /// Http connector + pub fn new_connector(&mut self) -> T + where + T: Clone, + { + self.conn.clone() + } + + /// Stop http server + fn stop(&mut self) { + System::current().stop(); + } +} + +impl TestServerRuntime +where + T: Service + Clone, + T::Response: Connection, +{ + /// Connect to websocket server at a given path + pub fn ws_at( + &mut self, + path: &str, + ) -> Result, ws::ClientError> { + let url = self.url(path); + self.rt + .block_on(lazy(|| ws::Client::default().call(ws::Connect::new(url)))) + } + + /// Connect to a websocket server + pub fn ws( + &mut self, + ) -> Result, ws::ClientError> { + self.ws_at("/") + } + + /// Send request and read response message + pub fn send_request( + &mut self, + req: ClientRequest, + ) -> Result { + self.rt.block_on(req.send(&mut self.conn)) + } +} + +impl Drop for TestServerRuntime { + fn drop(&mut self) { + self.stop() + } +} diff --git a/tests/test_client.rs b/tests/test_client.rs index f19edda13..606bac22a 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -3,7 +3,8 @@ use bytes::Bytes; use futures::future::{self, ok}; use actix_http::HttpMessage; -use actix_http::{client, h1, test::TestServer, Request, Response}; +use actix_http::{client, h1, Request, Response}; +use actix_http_test::TestServer; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ diff --git a/tests/test_server.rs b/tests/test_server.rs index cb9cd3f9d..c23840ead 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -2,19 +2,20 @@ use std::io::{Read, Write}; use std::time::Duration; use std::{net, thread}; +use actix_http_test::TestServer; use actix_service::NewService; use bytes::Bytes; use futures::future::{self, ok}; use futures::stream::once; use actix_http::{ - body, client, h1, http, test, Body, Error, HttpMessage as HttpMessage2, KeepAlive, + body, client, h1, http, Body, Error, HttpMessage as HttpMessage2, KeepAlive, Request, Response, }; #[test] fn test_h1_v2() { - let mut srv = test::TestServer::with_factory(|| { + let mut srv = TestServer::with_factory(|| { h1::H1Service::build() .keep_alive(KeepAlive::Disabled) .client_timeout(1000) @@ -31,7 +32,7 @@ fn test_h1_v2() { #[test] fn test_slow_request() { - let srv = test::TestServer::with_factory(|| { + let srv = TestServer::with_factory(|| { h1::H1Service::build() .client_timeout(100) .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) @@ -47,7 +48,7 @@ fn test_slow_request() { #[test] fn test_malformed_request() { - let srv = test::TestServer::with_factory(|| { + let srv = TestServer::with_factory(|| { h1::H1Service::new(|_| future::ok::<_, ()>(Response::Ok().finish())).map(|_| ()) }); @@ -60,7 +61,7 @@ fn test_malformed_request() { #[test] fn test_keepalive() { - let srv = test::TestServer::with_factory(|| { + let srv = TestServer::with_factory(|| { h1::H1Service::build() .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) .map(|_| ()) @@ -80,7 +81,7 @@ fn test_keepalive() { #[test] fn test_keepalive_timeout() { - let srv = test::TestServer::with_factory(|| { + let srv = TestServer::with_factory(|| { h1::H1Service::build() .keep_alive(1) .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) @@ -101,7 +102,7 @@ fn test_keepalive_timeout() { #[test] fn test_keepalive_close() { - let srv = test::TestServer::with_factory(|| { + let srv = TestServer::with_factory(|| { h1::H1Service::build() .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) .map(|_| ()) @@ -121,7 +122,7 @@ fn test_keepalive_close() { #[test] fn test_keepalive_http10_default_close() { - let srv = test::TestServer::with_factory(|| { + let srv = TestServer::with_factory(|| { h1::H1Service::build() .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) .map(|_| ()) @@ -140,7 +141,7 @@ fn test_keepalive_http10_default_close() { #[test] fn test_keepalive_http10() { - let srv = test::TestServer::with_factory(|| { + let srv = TestServer::with_factory(|| { h1::H1Service::build() .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) .map(|_| ()) @@ -166,7 +167,7 @@ fn test_keepalive_http10() { #[test] fn test_keepalive_disabled() { - let srv = test::TestServer::with_factory(|| { + let srv = TestServer::with_factory(|| { h1::H1Service::build() .keep_alive(KeepAlive::Disabled) .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) @@ -191,7 +192,7 @@ fn test_content_length() { StatusCode, }; - let mut srv = test::TestServer::with_factory(|| { + let mut srv = TestServer::with_factory(|| { h1::H1Service::new(|req: Request| { let indx: usize = req.uri().path()[1..].parse().unwrap(); let statuses = [ @@ -240,7 +241,7 @@ fn test_headers() { let data = STR.repeat(10); let data2 = data.clone(); - let mut srv = test::TestServer::with_factory(move || { + let mut srv = TestServer::with_factory(move || { let data = data.clone(); h1::H1Service::new(move |_| { let mut builder = Response::Ok(); @@ -302,7 +303,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ #[test] fn test_body() { - let mut srv = test::TestServer::with_factory(|| { + let mut srv = TestServer::with_factory(|| { h1::H1Service::new(|_| future::ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) }); @@ -317,7 +318,7 @@ fn test_body() { #[test] fn test_head_empty() { - let mut srv = test::TestServer::with_factory(|| { + let mut srv = TestServer::with_factory(|| { h1::H1Service::new(|_| ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) }); @@ -340,7 +341,7 @@ fn test_head_empty() { #[test] fn test_head_binary() { - let mut srv = test::TestServer::with_factory(|| { + let mut srv = TestServer::with_factory(|| { h1::H1Service::new(|_| { ok::<_, ()>(Response::Ok().content_length(STR.len() as u64).body(STR)) }) @@ -366,7 +367,7 @@ fn test_head_binary() { #[test] fn test_head_binary2() { - let mut srv = test::TestServer::with_factory(|| { + let mut srv = TestServer::with_factory(|| { h1::H1Service::new(|_| ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) }); @@ -385,7 +386,7 @@ fn test_head_binary2() { #[test] fn test_body_length() { - let mut srv = test::TestServer::with_factory(|| { + let mut srv = TestServer::with_factory(|| { h1::H1Service::new(|_| { let body = once(Ok(Bytes::from_static(STR.as_ref()))); ok::<_, ()>( @@ -407,7 +408,7 @@ fn test_body_length() { #[test] fn test_body_chunked_explicit() { - let mut srv = test::TestServer::with_factory(|| { + let mut srv = TestServer::with_factory(|| { h1::H1Service::new(|_| { let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); ok::<_, ()>(Response::Ok().streaming(body)) @@ -428,7 +429,7 @@ fn test_body_chunked_explicit() { #[test] fn test_body_chunked_implicit() { - let mut srv = test::TestServer::with_factory(|| { + let mut srv = TestServer::with_factory(|| { h1::H1Service::new(|_| { let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); ok::<_, ()>(Response::Ok().streaming(body)) @@ -447,7 +448,7 @@ fn test_body_chunked_implicit() { #[test] fn test_response_http_error_handling() { - let mut srv = test::TestServer::with_factory(|| { + let mut srv = TestServer::with_factory(|| { h1::H1Service::new(|_| { let broken_header = Bytes::from_static(b"\0\0\0"); ok::<_, ()>( diff --git a/tests/test_ws.rs b/tests/test_ws.rs index 11a3f472b..07f857d7a 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -1,6 +1,7 @@ use std::io; use actix_codec::Framed; +use actix_http_test::TestServer; use actix_service::NewService; use actix_utils::framed::IntoFramed; use actix_utils::stream::TakeItem; @@ -9,7 +10,7 @@ use bytes::{Bytes, BytesMut}; use futures::future::{lazy, ok, Either}; use futures::{Future, Sink, Stream}; -use actix_http::{h1, test, ws, ResponseError, SendResponse, ServiceConfig}; +use actix_http::{h1, ws, ResponseError, SendResponse, ServiceConfig}; fn ws_service(req: ws::Frame) -> impl Future { match req { @@ -34,7 +35,7 @@ fn ws_service(req: ws::Frame) -> impl Future)| { From 12fb94204f4eb8923d4830c007923539d1a6b8f5 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 27 Jan 2019 11:40:26 -0800 Subject: [PATCH 109/427] use hashbrown instead of std HashMap --- Cargo.toml | 6 ++---- src/client/pool.rs | 5 +++-- src/extensions.rs | 31 ++----------------------------- test-server/Cargo.toml | 10 ---------- 4 files changed, 7 insertions(+), 45 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 426712201..b3adfa823 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,10 +44,6 @@ actix-codec = "0.1.0" actix-connector = { git = "https://github.com/actix/actix-net.git" } actix-utils = { git = "https://github.com/actix/actix-net.git" } -# actix-codec = { path="../actix-net/actix-codec/" } -# actix-connector = { path="../actix-net/actix-connector/" } -# actix-utils = { path="../actix-net/actix-utils/" } - base64 = "0.10" backtrace = "0.3" bitflags = "1.0" @@ -57,6 +53,8 @@ cookie = { version="0.11", features=["percent-encode"] } derive_more = "0.13" encoding = "0.2" futures = "0.1" +hashbrown = "0.1.8" +h2 = "0.1.16" http = "0.1.8" httparse = "1.3" indexmap = "1.0" diff --git a/src/client/pool.rs b/src/client/pool.rs index 11828dcb8..b577587d8 100644 --- a/src/client/pool.rs +++ b/src/client/pool.rs @@ -1,5 +1,5 @@ use std::cell::RefCell; -use std::collections::{HashMap, VecDeque}; +use std::collections::VecDeque; use std::io; use std::rc::Rc; use std::time::{Duration, Instant}; @@ -7,9 +7,10 @@ use std::time::{Duration, Instant}; use actix_codec::{AsyncRead, AsyncWrite}; use actix_service::Service; use futures::future::{ok, Either, FutureResult}; -use futures::sync::oneshot; use futures::task::AtomicTask; +use futures::unsync::oneshot; use futures::{Async, Future, Poll}; +use hashbrown::HashMap; use http::uri::Authority; use indexmap::IndexSet; use slab::Slab; diff --git a/src/extensions.rs b/src/extensions.rs index 430b87bda..7bb965c96 100644 --- a/src/extensions.rs +++ b/src/extensions.rs @@ -1,40 +1,13 @@ use std::any::{Any, TypeId}; -use std::collections::HashMap; use std::fmt; use std::hash::{BuildHasherDefault, Hasher}; -struct IdHasher { - id: u64, -} - -impl Default for IdHasher { - fn default() -> IdHasher { - IdHasher { id: 0 } - } -} - -impl Hasher for IdHasher { - fn write(&mut self, bytes: &[u8]) { - for &x in bytes { - self.id.wrapping_add(u64::from(x)); - } - } - - fn write_u64(&mut self, u: u64) { - self.id = u; - } - - fn finish(&self) -> u64 { - self.id - } -} - -type AnyMap = HashMap, BuildHasherDefault>; +use hashbrown::HashMap; #[derive(Default)] /// A type map of request extensions. pub struct Extensions { - map: AnyMap, + map: HashMap>, } impl Extensions { diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 5cede2dc2..851b3efe4 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -28,9 +28,6 @@ default = ["session"] # sessions feature, session require "ring" crate and c compiler session = ["cookie/secure"] -# openssl -ssl = ["openssl", "actix-http/ssl"] - [dependencies] actix-codec = "0.1" actix-service = "0.1.6" @@ -39,10 +36,6 @@ actix-server = "0.1.0" actix-http = { path=".." } actix-utils = { git = "https://github.com/actix/actix-net.git" } -# actix-codec = { path="../actix-net/actix-codec/" } -# actix-rt = { path="../actix-net/actix-rt/" } -# actix-server = { path="../actix-net/actix-server/" } - base64 = "0.10" bytes = "0.4" cookie = { version="0.11", features=["percent-encode"] } @@ -59,6 +52,3 @@ serde_urlencoded = "0.5.3" time = "0.1" tokio-tcp = "0.1" tokio-timer = "0.2" - -# openssl -openssl = { version="0.10", optional = true } From 4a388d7ad97ab85a1e42847acca73d9e8ab9baa4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 28 Jan 2019 20:41:09 -0800 Subject: [PATCH 110/427] add client http/2 support --- examples/client.rs | 33 +++ src/client/connection.rs | 138 +++++---- src/client/connector.rs | 229 ++++++-------- src/client/error.rs | 24 +- src/client/{pipeline.rs => h1proto.rs} | 106 +++++-- src/client/h2proto.rs | 151 ++++++++++ src/client/mod.rs | 5 +- src/client/pool.rs | 394 +++++++++++++++---------- src/client/request.rs | 17 +- src/client/response.rs | 2 +- src/error.rs | 5 +- src/extensions.rs | 1 - src/lib.rs | 3 + src/response.rs | 1 + test-server/src/lib.rs | 12 +- 15 files changed, 719 insertions(+), 402 deletions(-) create mode 100644 examples/client.rs rename src/client/{pipeline.rs => h1proto.rs} (67%) create mode 100644 src/client/h2proto.rs diff --git a/examples/client.rs b/examples/client.rs new file mode 100644 index 000000000..06b708e20 --- /dev/null +++ b/examples/client.rs @@ -0,0 +1,33 @@ +use actix_http::{client, Error}; +use actix_rt::System; +use bytes::BytesMut; +use futures::{future::lazy, Future, Stream}; + +fn main() -> Result<(), Error> { + std::env::set_var("RUST_LOG", "actix_http=trace"); + env_logger::init(); + + System::new("test").block_on(lazy(|| { + let mut connector = client::Connector::default().service(); + + client::ClientRequest::get("https://www.rust-lang.org/") // <- Create request builder + .header("User-Agent", "Actix-web") + .finish() + .unwrap() + .send(&mut connector) // <- Send http request + .from_err() + .and_then(|response| { + // <- server http response + println!("Response: {:?}", response); + + // read response body + response + .from_err() + .fold(BytesMut::new(), move |mut acc, chunk| { + acc.extend_from_slice(&chunk); + Ok::<_, Error>(acc) + }) + .map(|body| println!("Downloaded: {:?} bytes", body.len())) + }) + })) +} diff --git a/src/client/connection.rs b/src/client/connection.rs index ed156bf84..b192caaeb 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -1,11 +1,35 @@ -use std::{fmt, io, time}; +use std::{fmt, time}; use actix_codec::{AsyncRead, AsyncWrite}; -use futures::Poll; +use bytes::Bytes; +use futures::Future; +use h2::client::SendRequest; +use crate::body::MessageBody; +use crate::message::RequestHead; + +use super::error::SendRequestError; use super::pool::Acquired; +use super::response::ClientResponse; +use super::{h1proto, h2proto}; -pub trait Connection: AsyncRead + AsyncWrite + 'static { +pub(crate) enum ConnectionType { + H1(Io), + H2(SendRequest), +} + +pub trait RequestSender { + type Future: Future; + + /// Close connection + fn send_request( + self, + head: RequestHead, + body: B, + ) -> Self::Future; +} + +pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static { /// Close connection fn close(&mut self); @@ -16,7 +40,7 @@ pub trait Connection: AsyncRead + AsyncWrite + 'static { #[doc(hidden)] /// HTTP client connection pub struct IoConnection { - io: Option, + io: Option>, created: time::Instant, pool: Option>, } @@ -26,77 +50,83 @@ where T: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Connection {:?}", self.io) + match self.io { + Some(ConnectionType::H1(ref io)) => write!(f, "H1Connection({:?})", io), + Some(ConnectionType::H2(_)) => write!(f, "H2Connection"), + None => write!(f, "Connection(Empty)"), + } } } impl IoConnection { - pub(crate) fn new(io: T, created: time::Instant, pool: Acquired) -> Self { + pub(crate) fn new( + io: ConnectionType, + created: time::Instant, + pool: Option>, + ) -> Self { IoConnection { + pool, created, io: Some(io), - pool: Some(pool), } } - /// Raw IO stream - pub fn get_mut(&mut self) -> &mut T { - self.io.as_mut().unwrap() - } - - pub(crate) fn into_inner(self) -> (T, time::Instant) { + pub(crate) fn into_inner(self) -> (ConnectionType, time::Instant) { (self.io.unwrap(), self.created) } } -impl Connection for IoConnection { - /// Close connection - fn close(&mut self) { - if let Some(mut pool) = self.pool.take() { - if let Some(io) = self.io.take() { - pool.close(IoConnection { - io: Some(io), - created: self.created, - pool: None, - }) - } - } - } +impl RequestSender for IoConnection +where + T: AsyncRead + AsyncWrite + 'static, +{ + type Future = Box>; - /// Release this connection to the connection pool - fn release(&mut self) { - if let Some(mut pool) = self.pool.take() { - if let Some(io) = self.io.take() { - pool.release(IoConnection { - io: Some(io), - created: self.created, - pool: None, - }) - } + fn send_request( + mut self, + head: RequestHead, + body: B, + ) -> Self::Future { + match self.io.take().unwrap() { + ConnectionType::H1(io) => Box::new(h1proto::send_request( + io, + head, + body, + self.created, + self.pool, + )), + ConnectionType::H2(io) => Box::new(h2proto::send_request( + io, + head, + body, + self.created, + self.pool, + )), } } } -impl io::Read for IoConnection { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.io.as_mut().unwrap().read(buf) - } +#[allow(dead_code)] +pub(crate) enum EitherConnection { + A(IoConnection), + B(IoConnection), } -impl AsyncRead for IoConnection {} +impl RequestSender for EitherConnection +where + A: AsyncRead + AsyncWrite + 'static, + B: AsyncRead + AsyncWrite + 'static, +{ + type Future = Box>; -impl io::Write for IoConnection { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.io.as_mut().unwrap().write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.io.as_mut().unwrap().flush() - } -} - -impl AsyncWrite for IoConnection { - fn shutdown(&mut self) -> Poll<(), io::Error> { - self.io.as_mut().unwrap().shutdown() + fn send_request( + self, + head: RequestHead, + body: RB, + ) -> Self::Future { + match self { + EitherConnection::A(con) => con.send_request(head, body), + EitherConnection::B(con) => con.send_request(head, body), + } } } diff --git a/src/client/connector.rs b/src/client/connector.rs index 05caf1ed2..b573181ba 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -1,23 +1,22 @@ use std::time::Duration; -use std::{fmt, io}; use actix_codec::{AsyncRead, AsyncWrite}; use actix_connector::{Resolver, TcpConnector}; use actix_service::{Service, ServiceExt}; use actix_utils::timeout::{TimeoutError, TimeoutService}; -use futures::future::Either; -use futures::Poll; use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; use super::connect::Connect; -use super::connection::{Connection, IoConnection}; +use super::connection::RequestSender; use super::error::ConnectorError; -use super::pool::ConnectionPool; +use super::pool::{ConnectionPool, Protocol}; #[cfg(feature = "ssl")] use actix_connector::ssl::OpensslConnector; #[cfg(feature = "ssl")] use openssl::ssl::{SslConnector, SslMethod}; +#[cfg(feature = "ssl")] +const H2: &[u8] = b"h2"; #[cfg(not(feature = "ssl"))] type SslConnector = (); @@ -40,7 +39,12 @@ impl Default for Connector { let connector = { #[cfg(feature = "ssl")] { - SslConnector::builder(SslMethod::tls()).unwrap().build() + use log::error; + let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap(); + let _ = ssl + .set_alpn_protos(b"\x02h2\x08http/1.1") + .map_err(|e| error!("Can not set alpn protocol: {:?}", e)); + ssl.build() } #[cfg(not(feature = "ssl"))] { @@ -133,15 +137,17 @@ impl Connector { /// Finish configuration process and create connector service. pub fn service( self, - ) -> impl Service + Clone + ) -> impl Service + Clone { #[cfg(not(feature = "ssl"))] { let connector = TimeoutService::new( self.timeout, - self.resolver - .map_err(ConnectorError::from) - .and_then(TcpConnector::default().from_err()), + self.resolver.map_err(ConnectorError::from).and_then( + TcpConnector::default() + .from_err() + .map(|(msg, io)| (msg, io, Protocol::Http1)), + ), ) .map_err(|e| match e { TimeoutError::Service(e) => e, @@ -168,7 +174,20 @@ impl Connector { .and_then(TcpConnector::default().from_err()) .and_then( OpensslConnector::service(self.connector) - .map_err(ConnectorError::from), + .map_err(ConnectorError::from) + .map(|(msg, io)| { + let h2 = io + .get_ref() + .ssl() + .selected_alpn_protocol() + .map(|protos| protos.windows(2).any(|w| w == H2)) + .unwrap_or(false); + if h2 { + (msg, io, Protocol::Http2) + } else { + (msg, io, Protocol::Http1) + } + }), ), ) .map_err(|e| match e { @@ -178,9 +197,11 @@ impl Connector { let tcp_service = TimeoutService::new( self.timeout, - self.resolver - .map_err(ConnectorError::from) - .and_then(TcpConnector::default().from_err()), + self.resolver.map_err(ConnectorError::from).and_then( + TcpConnector::default() + .from_err() + .map(|(msg, io)| (msg, io, Protocol::Http1)), + ), ) .map_err(|e| match e { TimeoutError::Service(e) => e, @@ -209,13 +230,16 @@ impl Connector { #[cfg(not(feature = "ssl"))] mod connect_impl { + use futures::future::{err, Either, FutureResult}; + use futures::Poll; + use super::*; - use futures::future::{err, FutureResult}; + use crate::client::connection::IoConnection; pub(crate) struct InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { pub(crate) tcp_pool: ConnectionPool, } @@ -223,7 +247,8 @@ mod connect_impl { impl Clone for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service + Clone, + T: Service + + Clone, { fn clone(&self) -> Self { InnerConnector { @@ -235,7 +260,7 @@ mod connect_impl { impl Service for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { type Response = IoConnection; type Error = ConnectorError; @@ -264,17 +289,26 @@ mod connect_impl { mod connect_impl { use std::marker::PhantomData; - use futures::future::{err, FutureResult}; + use futures::future::{err, Either, FutureResult}; use futures::{Async, Future, Poll}; use super::*; + use crate::client::connection::EitherConnection; pub(crate) struct InnerConnector where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service, - T2: Service, + T1: Service< + Connect, + Response = (Connect, Io1, Protocol), + Error = ConnectorError, + >, + T2: Service< + Connect, + Response = (Connect, Io2, Protocol), + Error = ConnectorError, + >, { pub(crate) tcp_pool: ConnectionPool, pub(crate) ssl_pool: ConnectionPool, @@ -284,8 +318,16 @@ mod connect_impl { where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service + Clone, - T2: Service + Clone, + T1: Service< + Connect, + Response = (Connect, Io1, Protocol), + Error = ConnectorError, + > + Clone, + T2: Service< + Connect, + Response = (Connect, Io2, Protocol), + Error = ConnectorError, + > + Clone, { fn clone(&self) -> Self { InnerConnector { @@ -299,10 +341,18 @@ mod connect_impl { where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service, - T2: Service, + T1: Service< + Connect, + Response = (Connect, Io1, Protocol), + Error = ConnectorError, + >, + T2: Service< + Connect, + Response = (Connect, Io2, Protocol), + Error = ConnectorError, + >, { - type Response = IoEither, IoConnection>; + type Response = EitherConnection; type Error = ConnectorError; type Future = Either< FutureResult, @@ -336,7 +386,7 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseA where Io1: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { fut: as Service>::Future, _t: PhantomData, @@ -344,17 +394,17 @@ mod connect_impl { impl Future for InnerConnectorResponseA where - T: Service, + T: Service, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { - type Item = IoEither, IoConnection>; + type Item = EitherConnection; type Error = ConnectorError; fn poll(&mut self) -> Poll { match self.fut.poll()? { Async::NotReady => Ok(Async::NotReady), - Async::Ready(res) => Ok(Async::Ready(IoEither::A(res))), + Async::Ready(res) => Ok(Async::Ready(EitherConnection::A(res))), } } } @@ -362,7 +412,7 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseB where Io2: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { fut: as Service>::Future, _t: PhantomData, @@ -370,129 +420,18 @@ mod connect_impl { impl Future for InnerConnectorResponseB where - T: Service, + T: Service, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { - type Item = IoEither, IoConnection>; + type Item = EitherConnection; type Error = ConnectorError; fn poll(&mut self) -> Poll { match self.fut.poll()? { Async::NotReady => Ok(Async::NotReady), - Async::Ready(res) => Ok(Async::Ready(IoEither::B(res))), + Async::Ready(res) => Ok(Async::Ready(EitherConnection::B(res))), } } } } - -pub(crate) enum IoEither { - A(Io1), - B(Io2), -} - -impl Connection for IoEither -where - Io1: Connection, - Io2: Connection, -{ - fn close(&mut self) { - match self { - IoEither::A(ref mut io) => io.close(), - IoEither::B(ref mut io) => io.close(), - } - } - - fn release(&mut self) { - match self { - IoEither::A(ref mut io) => io.release(), - IoEither::B(ref mut io) => io.release(), - } - } -} - -impl io::Read for IoEither -where - Io1: Connection, - Io2: Connection, -{ - fn read(&mut self, buf: &mut [u8]) -> io::Result { - match self { - IoEither::A(ref mut io) => io.read(buf), - IoEither::B(ref mut io) => io.read(buf), - } - } -} - -impl AsyncRead for IoEither -where - Io1: Connection, - Io2: Connection, -{ - unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { - match self { - IoEither::A(ref io) => io.prepare_uninitialized_buffer(buf), - IoEither::B(ref io) => io.prepare_uninitialized_buffer(buf), - } - } -} - -impl AsyncWrite for IoEither -where - Io1: Connection, - Io2: Connection, -{ - fn shutdown(&mut self) -> Poll<(), io::Error> { - match self { - IoEither::A(ref mut io) => io.shutdown(), - IoEither::B(ref mut io) => io.shutdown(), - } - } - - fn poll_write(&mut self, buf: &[u8]) -> Poll { - match self { - IoEither::A(ref mut io) => io.poll_write(buf), - IoEither::B(ref mut io) => io.poll_write(buf), - } - } - - fn poll_flush(&mut self) -> Poll<(), io::Error> { - match self { - IoEither::A(ref mut io) => io.poll_flush(), - IoEither::B(ref mut io) => io.poll_flush(), - } - } -} - -impl io::Write for IoEither -where - Io1: Connection, - Io2: Connection, -{ - fn flush(&mut self) -> io::Result<()> { - match self { - IoEither::A(ref mut io) => io.flush(), - IoEither::B(ref mut io) => io.flush(), - } - } - - fn write(&mut self, buf: &[u8]) -> io::Result { - match self { - IoEither::A(ref mut io) => io.write(buf), - IoEither::B(ref mut io) => io.write(buf), - } - } -} - -impl fmt::Debug for IoEither -where - Io1: fmt::Debug, - Io2: fmt::Debug, -{ - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self { - IoEither::A(ref io) => io.fmt(fmt), - IoEither::B(ref io) => io.fmt(fmt), - } - } -} diff --git a/src/client/error.rs b/src/client/error.rs index 2a5df9c97..e27a83d85 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -6,7 +6,8 @@ use trust_dns_resolver::error::ResolveError; #[cfg(feature = "ssl")] use openssl::ssl::{Error as SslError, HandshakeError}; -use crate::error::{Error, ParseError}; +use crate::error::{Error, ParseError, ResponseError}; +use crate::response::Response; /// A set of errors that can occur while connecting to an HTTP host #[derive(Debug, Display, From)] @@ -32,6 +33,10 @@ pub enum ConnectorError { #[display(fmt = "No dns records found for the input")] NoRecords, + /// Http2 error + #[display(fmt = "{}", _0)] + H2(h2::Error), + /// Connecting took too long #[display(fmt = "Timeout out while establishing connection")] Timeout, @@ -80,6 +85,23 @@ pub enum SendRequestError { Send(io::Error), /// Error parsing response Response(ParseError), + /// Http2 error + #[display(fmt = "{}", _0)] + H2(h2::Error), /// Error sending request body Body(Error), } + +/// Convert `SendRequestError` to a server `Response` +impl ResponseError for SendRequestError { + fn error_response(&self) -> Response { + match *self { + SendRequestError::Connector(ConnectorError::Timeout) => { + Response::GatewayTimeout() + } + SendRequestError::Connector(_) => Response::BadGateway(), + _ => Response::InternalServerError(), + } + .into() + } +} diff --git a/src/client/pipeline.rs b/src/client/h1proto.rs similarity index 67% rename from src/client/pipeline.rs rename to src/client/h1proto.rs index 8d946d644..ed3c66d62 100644 --- a/src/client/pipeline.rs +++ b/src/client/h1proto.rs @@ -1,38 +1,42 @@ -use std::collections::VecDeque; +use std::{io, time}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_service::Service; use bytes::Bytes; use futures::future::{err, ok, Either}; use futures::{Async, Future, Poll, Sink, Stream}; +use super::connection::{ConnectionLifetime, ConnectionType, IoConnection}; use super::error::{ConnectorError, SendRequestError}; +use super::pool::Acquired; use super::response::ClientResponse; -use super::{Connect, Connection}; use crate::body::{BodyLength, MessageBody, PayloadStream}; use crate::error::PayloadError; use crate::h1; use crate::message::RequestHead; -pub(crate) fn send_request( +pub(crate) fn send_request( + io: T, head: RequestHead, body: B, - connector: &mut T, + created: time::Instant, + pool: Option>, ) -> impl Future where - T: Service, + T: AsyncRead + AsyncWrite + 'static, B: MessageBody, - I: Connection, { + let io = H1Connection { + io: Some(io), + created: created, + pool: pool, + }; + let len = body.length(); - connector - // connect to the host - .call(Connect::new(head.uri.clone())) + // create Framed and send reqest + Framed::new(io, h1::ClientCodec::default()) + .send((head, len).into()) .from_err() - // create Framed and send reqest - .map(|io| Framed::new(io, h1::ClientCodec::default())) - .and_then(move |framed| framed.send((head, len).into()).from_err()) // send request body .and_then(move |framed| match body.length() { BodyLength::None | BodyLength::Empty | BodyLength::Sized(0) => { @@ -64,11 +68,70 @@ where }) } +#[doc(hidden)] +/// HTTP client connection +pub struct H1Connection { + io: Option, + created: time::Instant, + pool: Option>, +} + +impl ConnectionLifetime for H1Connection { + /// Close connection + fn close(&mut self) { + if let Some(mut pool) = self.pool.take() { + if let Some(io) = self.io.take() { + pool.close(IoConnection::new( + ConnectionType::H1(io), + self.created, + None, + )); + } + } + } + + /// Release this connection to the connection pool + fn release(&mut self) { + if let Some(mut pool) = self.pool.take() { + if let Some(io) = self.io.take() { + pool.release(IoConnection::new( + ConnectionType::H1(io), + self.created, + None, + )); + } + } + } +} + +impl io::Read for H1Connection { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.io.as_mut().unwrap().read(buf) + } +} + +impl AsyncRead for H1Connection {} + +impl io::Write for H1Connection { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.io.as_mut().unwrap().write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.io.as_mut().unwrap().flush() + } +} + +impl AsyncWrite for H1Connection { + fn shutdown(&mut self) -> Poll<(), io::Error> { + self.io.as_mut().unwrap().shutdown() + } +} + /// Future responsible for sending request body to the peer -struct SendBody { +pub(crate) struct SendBody { body: Option, framed: Option>, - write_buf: VecDeque>, flushed: bool, } @@ -77,11 +140,10 @@ where I: AsyncRead + AsyncWrite + 'static, B: MessageBody, { - fn new(body: B, framed: Framed) -> Self { + pub(crate) fn new(body: B, framed: Framed) -> Self { SendBody { body: Some(body), framed: Some(framed), - write_buf: VecDeque::new(), flushed: true, } } @@ -89,7 +151,7 @@ where impl Future for SendBody where - I: Connection, + I: ConnectionLifetime, B: MessageBody, { type Item = Framed; @@ -158,15 +220,15 @@ impl Payload<()> { } } -impl Payload { - fn stream(framed: Framed) -> PayloadStream { +impl Payload { + pub fn stream(framed: Framed) -> PayloadStream { Box::new(Payload { framed: Some(framed.map_codec(|codec| codec.into_payload_codec())), }) } } -impl Stream for Payload { +impl Stream for Payload { type Item = Bytes; type Error = PayloadError; @@ -190,7 +252,7 @@ impl Stream for Payload { fn release_connection(framed: Framed, force_close: bool) where - T: Connection, + T: ConnectionLifetime, { let mut parts = framed.into_parts(); if !force_close && parts.read_buf.is_empty() && parts.write_buf.is_empty() { diff --git a/src/client/h2proto.rs b/src/client/h2proto.rs new file mode 100644 index 000000000..fe42b909c --- /dev/null +++ b/src/client/h2proto.rs @@ -0,0 +1,151 @@ +use std::cell::RefCell; +use std::time; + +use actix_codec::{AsyncRead, AsyncWrite}; +use bytes::Bytes; +use futures::future::{err, Either}; +use futures::{Async, Future, Poll, Stream}; +use h2::{client::SendRequest, SendStream}; +use http::{request::Request, Version}; + +use super::connection::{ConnectionType, IoConnection}; +use super::error::SendRequestError; +use super::pool::Acquired; +use super::response::ClientResponse; +use crate::body::{BodyLength, MessageBody}; +use crate::message::{RequestHead, ResponseHead}; + +pub(crate) fn send_request( + io: SendRequest, + head: RequestHead, + body: B, + created: time::Instant, + pool: Option>, +) -> impl Future +where + T: AsyncRead + AsyncWrite + 'static, + B: MessageBody, +{ + trace!("Sending client request: {:?} {:?}", head, body.length()); + let eof = match body.length() { + BodyLength::None | BodyLength::Empty | BodyLength::Sized(0) => true, + _ => false, + }; + + io.ready() + .map_err(SendRequestError::from) + .and_then(move |mut io| { + let mut req = Request::new(()); + *req.uri_mut() = head.uri; + *req.method_mut() = head.method; + *req.headers_mut() = head.headers; + *req.version_mut() = Version::HTTP_2; + + match io.send_request(req, eof) { + Ok((resp, send)) => { + release(io, pool, created, false); + + if !eof { + Either::A(Either::B( + SendBody { + body, + send, + buf: None, + } + .and_then(move |_| resp.map_err(SendRequestError::from)), + )) + } else { + Either::B(resp.map_err(SendRequestError::from)) + } + } + Err(e) => { + release(io, pool, created, e.is_io()); + Either::A(Either::A(err(e.into()))) + } + } + }) + .and_then(|resp| { + let (parts, body) = resp.into_parts(); + + let mut head = ResponseHead::default(); + head.version = parts.version; + head.status = parts.status; + head.headers = parts.headers; + + Ok(ClientResponse { + head, + payload: RefCell::new(Some(Box::new(body.from_err()))), + }) + }) + .from_err() +} + +struct SendBody { + body: B, + send: SendStream, + buf: Option, +} + +impl Future for SendBody { + type Item = (); + type Error = SendRequestError; + + fn poll(&mut self) -> Poll { + if self.buf.is_none() { + match self.body.poll_next() { + Ok(Async::Ready(Some(buf))) => { + self.send.reserve_capacity(buf.len()); + self.buf = Some(buf); + } + Ok(Async::Ready(None)) => { + if let Err(e) = self.send.send_data(Bytes::new(), true) { + return Err(e.into()); + } + self.send.reserve_capacity(0); + return Ok(Async::Ready(())); + } + Ok(Async::NotReady) => return Ok(Async::NotReady), + Err(e) => return Err(e.into()), + } + } + + loop { + match self.send.poll_capacity() { + Ok(Async::NotReady) => return Ok(Async::NotReady), + Ok(Async::Ready(None)) => return Ok(Async::Ready(())), + Ok(Async::Ready(Some(cap))) => { + let mut buf = self.buf.take().unwrap(); + let len = buf.len(); + let bytes = buf.split_to(std::cmp::min(cap, len)); + + if let Err(e) = self.send.send_data(bytes, false) { + return Err(e.into()); + } else { + if !buf.is_empty() { + self.send.reserve_capacity(buf.len()); + self.buf = Some(buf); + } + return self.poll(); + } + } + Err(e) => return Err(e.into()), + } + } + } +} + +// release SendRequest object +fn release( + io: SendRequest, + pool: Option>, + created: time::Instant, + close: bool, +) { + if let Some(mut pool) = pool { + if close { + pool.close(IoConnection::new(ConnectionType::H2(io), created, None)); + } else { + pool.release(IoConnection::new(ConnectionType::H2(io), created, None)); + } + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs index 76c3f8b88..c6498f371 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -3,13 +3,14 @@ mod connect; mod connection; mod connector; mod error; -mod pipeline; +mod h1proto; +mod h2proto; mod pool; mod request; mod response; pub use self::connect::Connect; -pub use self::connection::Connection; +pub use self::connection::RequestSender; pub use self::connector::Connector; pub use self::error::{ConnectorError, InvalidUrlKind, SendRequestError}; pub use self::request::{ClientRequest, ClientRequestBuilder}; diff --git a/src/client/pool.rs b/src/client/pool.rs index b577587d8..089c2627a 100644 --- a/src/client/pool.rs +++ b/src/client/pool.rs @@ -6,10 +6,12 @@ use std::time::{Duration, Instant}; use actix_codec::{AsyncRead, AsyncWrite}; use actix_service::Service; +use bytes::Bytes; use futures::future::{ok, Either, FutureResult}; use futures::task::AtomicTask; use futures::unsync::oneshot; use futures::{Async, Future, Poll}; +use h2::client::{handshake, Handshake}; use hashbrown::HashMap; use http::uri::Authority; use indexmap::IndexSet; @@ -17,9 +19,15 @@ use slab::Slab; use tokio_timer::{sleep, Delay}; use super::connect::Connect; -use super::connection::IoConnection; +use super::connection::{ConnectionType, IoConnection}; use super::error::ConnectorError; +#[derive(Clone, Copy, PartialEq)] +pub enum Protocol { + Http1, + Http2, +} + #[derive(Hash, Eq, PartialEq, Clone, Debug)] pub(crate) struct Key { authority: Authority, @@ -31,13 +39,6 @@ impl From for Key { } } -#[derive(Debug)] -struct AvailableConnection { - io: T, - used: Instant, - created: Instant, -} - /// Connections pool pub(crate) struct ConnectionPool( T, @@ -47,7 +48,7 @@ pub(crate) struct ConnectionPool( impl ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { pub(crate) fn new( connector: T, @@ -86,7 +87,7 @@ where impl Service for ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { type Response = IoConnection; type Error = ConnectorError; @@ -109,7 +110,7 @@ where Either::A(ok(IoConnection::new( io, created, - Acquired(key, Some(self.1.clone())), + Some(Acquired(key, Some(self.1.clone()))), ))) } Acquire::NotAvailable => { @@ -190,12 +191,13 @@ where { fut: F, key: Key, + h2: Option>, inner: Option>>>, } impl OpenConnection where - F: Future, + F: Future, Io: AsyncRead + AsyncWrite + 'static, { fn new(key: Key, inner: Rc>>, fut: F) -> Self { @@ -203,6 +205,7 @@ where key, fut, inner: Some(inner), + h2: None, } } } @@ -222,110 +225,165 @@ where impl Future for OpenConnection where - F: Future, + F: Future, Io: AsyncRead + AsyncWrite, { type Item = IoConnection; type Error = ConnectorError; fn poll(&mut self) -> Poll { + if let Some(ref mut h2) = self.h2 { + return match h2.poll() { + Ok(Async::Ready((snd, connection))) => { + tokio_current_thread::spawn(connection.map_err(|_| ())); + Ok(Async::Ready(IoConnection::new( + ConnectionType::H2(snd), + Instant::now(), + Some(Acquired(self.key.clone(), self.inner.clone())), + ))) + } + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(e) => Err(e.into()), + }; + } + match self.fut.poll() { Err(err) => Err(err.into()), - Ok(Async::Ready((_, io))) => { + Ok(Async::Ready((_, io, proto))) => { let _ = self.inner.take(); - Ok(Async::Ready(IoConnection::new( - io, - Instant::now(), - Acquired(self.key.clone(), self.inner.clone()), - ))) - } - Ok(Async::NotReady) => Ok(Async::NotReady), - } - } -} - -struct OpenWaitingConnection -where - Io: AsyncRead + AsyncWrite + 'static, -{ - fut: F, - key: Key, - rx: Option, ConnectorError>>>, - inner: Option>>>, -} - -impl OpenWaitingConnection -where - F: Future + 'static, - Io: AsyncRead + AsyncWrite + 'static, -{ - fn spawn( - key: Key, - rx: oneshot::Sender, ConnectorError>>, - inner: Rc>>, - fut: F, - ) { - tokio_current_thread::spawn(OpenWaitingConnection { - key, - fut, - rx: Some(rx), - inner: Some(inner), - }) - } -} - -impl Drop for OpenWaitingConnection -where - Io: AsyncRead + AsyncWrite + 'static, -{ - fn drop(&mut self) { - if let Some(inner) = self.inner.take() { - let mut inner = inner.as_ref().borrow_mut(); - inner.release(); - inner.check_availibility(); - } - } -} - -impl Future for OpenWaitingConnection -where - F: Future, - Io: AsyncRead + AsyncWrite, -{ - type Item = (); - type Error = (); - - fn poll(&mut self) -> Poll { - match self.fut.poll() { - Err(err) => { - let _ = self.inner.take(); - if let Some(rx) = self.rx.take() { - let _ = rx.send(Err(err)); - } - Err(()) - } - Ok(Async::Ready((_, io))) => { - let _ = self.inner.take(); - if let Some(rx) = self.rx.take() { - let _ = rx.send(Ok(IoConnection::new( - io, + if proto == Protocol::Http1 { + Ok(Async::Ready(IoConnection::new( + ConnectionType::H1(io), Instant::now(), - Acquired(self.key.clone(), self.inner.clone()), - ))); + Some(Acquired(self.key.clone(), self.inner.clone())), + ))) + } else { + self.h2 = Some(handshake(io)); + return self.poll(); } - Ok(Async::Ready(())) } Ok(Async::NotReady) => Ok(Async::NotReady), } } } +// struct OpenWaitingConnection +// where +// Io: AsyncRead + AsyncWrite + 'static, +// { +// fut: F, +// key: Key, +// h2: Option>, +// rx: Option, ConnectorError>>>, +// inner: Option>>>, +// } + +// impl OpenWaitingConnection +// where +// F: Future + 'static, +// Io: AsyncRead + AsyncWrite + 'static, +// { +// fn spawn( +// key: Key, +// rx: oneshot::Sender, ConnectorError>>, +// inner: Rc>>, +// fut: F, +// ) { +// tokio_current_thread::spawn(OpenWaitingConnection { +// key, +// fut, +// h2: None, +// rx: Some(rx), +// inner: Some(inner), +// }) +// } +// } + +// impl Drop for OpenWaitingConnection +// where +// Io: AsyncRead + AsyncWrite + 'static, +// { +// fn drop(&mut self) { +// if let Some(inner) = self.inner.take() { +// let mut inner = inner.as_ref().borrow_mut(); +// inner.release(); +// inner.check_availibility(); +// } +// } +// } + +// impl Future for OpenWaitingConnection +// where +// F: Future, +// Io: AsyncRead + AsyncWrite, +// { +// type Item = (); +// type Error = (); + +// fn poll(&mut self) -> Poll { +// if let Some(ref mut h2) = self.h2 { +// return match h2.poll() { +// Ok(Async::Ready((snd, connection))) => { +// tokio_current_thread::spawn(connection.map_err(|_| ())); +// let _ = self.rx.take().unwrap().send(Ok(IoConnection::new( +// ConnectionType::H2(snd), +// Instant::now(), +// Some(Acquired(self.key.clone(), self.inner.clone())), +// ))); +// Ok(Async::Ready(())) +// } +// Ok(Async::NotReady) => Ok(Async::NotReady), +// Err(e) => { +// let _ = self.inner.take(); +// if let Some(rx) = self.rx.take() { +// let _ = rx.send(Err(e.into())); +// } + +// Err(()) +// } +// }; +// } + +// match self.fut.poll() { +// Err(err) => { +// let _ = self.inner.take(); +// if let Some(rx) = self.rx.take() { +// let _ = rx.send(Err(err)); +// } +// Err(()) +// } +// Ok(Async::Ready((_, io, proto))) => { +// let _ = self.inner.take(); +// if proto == Protocol::Http1 { +// let _ = self.rx.take().unwrap().send(Ok(IoConnection::new( +// ConnectionType::H1(io), +// Instant::now(), +// Some(Acquired(self.key.clone(), self.inner.clone())), +// ))); +// } else { +// self.h2 = Some(handshake(io)); +// return self.poll(); +// } +// Ok(Async::Ready(())) +// } +// Ok(Async::NotReady) => Ok(Async::NotReady), +// } +// } +// } + enum Acquire { - Acquired(T, Instant), + Acquired(ConnectionType, Instant), Available, NotAvailable, } +// #[derive(Debug)] +struct AvailableConnection { + io: ConnectionType, + used: Instant, + created: Instant, +} + pub(crate) struct Inner { conn_lifetime: Duration, conn_keep_alive: Duration, @@ -355,7 +413,7 @@ impl Inner { self.waiters_queue.remove(&(key.clone(), token)); } - fn release_conn(&mut self, key: &Key, io: Io, created: Instant) { + fn release_conn(&mut self, key: &Key, io: ConnectionType, created: Instant) { self.acquired -= 1; self.available .entry(key.clone()) @@ -408,24 +466,30 @@ where || (now - conn.created) > self.conn_lifetime { if let Some(timeout) = self.disconnect_timeout { - tokio_current_thread::spawn(CloseConnection::new( - conn.io, timeout, - )) + if let ConnectionType::H1(io) = conn.io { + tokio_current_thread::spawn(CloseConnection::new( + io, timeout, + )) + } } } else { let mut io = conn.io; let mut buf = [0; 2]; - match io.read(&mut buf) { - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => (), - Ok(n) if n > 0 => { - if let Some(timeout) = self.disconnect_timeout { - tokio_current_thread::spawn(CloseConnection::new( - io, timeout, - )) + if let ConnectionType::H1(ref mut s) = io { + match s.read(&mut buf) { + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => (), + Ok(n) if n > 0 => { + if let Some(timeout) = self.disconnect_timeout { + if let ConnectionType::H1(io) = io { + tokio_current_thread::spawn( + CloseConnection::new(io, timeout), + ) + } + } + continue; } - continue; + Ok(_) | Err(_) => continue, } - Ok(_) | Err(_) => continue, } return Acquire::Acquired(io, conn.created); } @@ -434,10 +498,12 @@ where Acquire::Available } - fn release_close(&mut self, io: Io) { + fn release_close(&mut self, io: ConnectionType) { self.acquired -= 1; if let Some(timeout) = self.disconnect_timeout { - tokio_current_thread::spawn(CloseConnection::new(io, timeout)) + if let ConnectionType::H1(io) = io { + tokio_current_thread::spawn(CloseConnection::new(io, timeout)) + } } } @@ -448,65 +514,65 @@ where } } -struct ConnectorPoolSupport -where - Io: AsyncRead + AsyncWrite + 'static, -{ - connector: T, - inner: Rc>>, -} +// struct ConnectorPoolSupport +// where +// Io: AsyncRead + AsyncWrite + 'static, +// { +// connector: T, +// inner: Rc>>, +// } -impl Future for ConnectorPoolSupport -where - Io: AsyncRead + AsyncWrite + 'static, - T: Service, - T::Future: 'static, -{ - type Item = (); - type Error = (); +// impl Future for ConnectorPoolSupport +// where +// Io: AsyncRead + AsyncWrite + 'static, +// T: Service, +// T::Future: 'static, +// { +// type Item = (); +// type Error = (); - fn poll(&mut self) -> Poll { - let mut inner = self.inner.as_ref().borrow_mut(); - inner.task.register(); +// fn poll(&mut self) -> Poll { +// let mut inner = self.inner.as_ref().borrow_mut(); +// inner.task.register(); - // check waiters - loop { - let (key, token) = { - if let Some((key, token)) = inner.waiters_queue.get_index(0) { - (key.clone(), *token) - } else { - break; - } - }; - match inner.acquire(&key) { - Acquire::NotAvailable => break, - Acquire::Acquired(io, created) => { - let (_, tx) = inner.waiters.remove(token); - if let Err(conn) = tx.send(Ok(IoConnection::new( - io, - created, - Acquired(key.clone(), Some(self.inner.clone())), - ))) { - let (io, created) = conn.unwrap().into_inner(); - inner.release_conn(&key, io, created); - } - } - Acquire::Available => { - let (connect, tx) = inner.waiters.remove(token); - OpenWaitingConnection::spawn( - key.clone(), - tx, - self.inner.clone(), - self.connector.call(connect), - ); - } - } - let _ = inner.waiters_queue.swap_remove_index(0); - } +// // check waiters +// loop { +// let (key, token) = { +// if let Some((key, token)) = inner.waiters_queue.get_index(0) { +// (key.clone(), *token) +// } else { +// break; +// } +// }; +// match inner.acquire(&key) { +// Acquire::NotAvailable => break, +// Acquire::Acquired(io, created) => { +// let (_, tx) = inner.waiters.remove(token); +// if let Err(conn) = tx.send(Ok(IoConnection::new( +// io, +// created, +// Some(Acquired(key.clone(), Some(self.inner.clone()))), +// ))) { +// let (io, created) = conn.unwrap().into_inner(); +// inner.release_conn(&key, io, created); +// } +// } +// Acquire::Available => { +// let (connect, tx) = inner.waiters.remove(token); +// OpenWaitingConnection::spawn( +// key.clone(), +// tx, +// self.inner.clone(), +// self.connector.call(connect), +// ); +// } +// } +// let _ = inner.waiters_queue.swap_remove_index(0); +// } - Ok(Async::NotReady) - } -} +// Ok(Async::NotReady) +// } +// } struct CloseConnection { io: T, diff --git a/src/client/request.rs b/src/client/request.rs index fbb1e840b..a2233a2f1 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -17,8 +17,9 @@ use crate::http::{ }; use crate::message::{ConnectionType, Head, RequestHead}; +use super::connection::RequestSender; use super::response::ClientResponse; -use super::{pipeline, Connect, Connection, ConnectorError, SendRequestError}; +use super::{Connect, ConnectorError, SendRequestError}; /// An HTTP Client Request /// @@ -37,7 +38,6 @@ use super::{pipeline, Connect, Connection, ConnectorError, SendRequestError}; /// .map_err(|_| ()) /// .and_then(|response| { // <- server http response /// println!("Response: {:?}", response); -/// # actix_rt::System::current().stop(); /// Ok(()) /// }) /// })); @@ -175,10 +175,18 @@ where connector: &mut T, ) -> impl Future where + B: 'static, T: Service, - I: Connection, + I: RequestSender, { - pipeline::send_request(self.head, self.body, connector) + let Self { head, body } = self; + + connector + // connect to the host + .call(Connect::new(head.uri.clone())) + .from_err() + // send request + .and_then(move |connection| connection.send_request(head, body)) } } @@ -273,7 +281,6 @@ impl ClientRequestBuilder { /// .unwrap(); /// } /// ``` - #[doc(hidden)] pub fn set(&mut self, hdr: H) -> &mut Self { if let Some(parts) = parts(&mut self.head, &self.err) { match hdr.try_into() { diff --git a/src/client/response.rs b/src/client/response.rs index 6bfdfc321..005c0875b 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -10,7 +10,7 @@ use crate::error::PayloadError; use crate::httpmessage::HttpMessage; use crate::message::{Head, ResponseHead}; -use super::pipeline::Payload; +use super::h1proto::Payload; /// Client Response pub struct ClientResponse { diff --git a/src/error.rs b/src/error.rs index 8af422fc8..5470534f7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -327,7 +327,7 @@ impl From for ParseError { } } -#[derive(Display, Debug)] +#[derive(Display, Debug, From)] /// A set of errors that can occur during payload parsing pub enum PayloadError { /// A payload reached EOF, but is not complete. @@ -342,6 +342,9 @@ pub enum PayloadError { /// A payload length is unknown. #[display(fmt = "A payload length is unknown.")] UnknownLength, + /// Http2 payload error + #[display(fmt = "{}", _0)] + H2Payload(h2::Error), } impl From for PayloadError { diff --git a/src/extensions.rs b/src/extensions.rs index 7bb965c96..f7805641b 100644 --- a/src/extensions.rs +++ b/src/extensions.rs @@ -1,6 +1,5 @@ use std::any::{Any, TypeId}; use std::fmt; -use std::hash::{BuildHasherDefault, Hasher}; use hashbrown::HashMap; diff --git a/src/lib.rs b/src/lib.rs index 5adc9236e..0dbaee6aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,9 @@ //! * `session` - enables session support, includes `ring` crate as //! dependency //! +#[macro_use] +extern crate log; + pub mod body; pub mod client; mod config; diff --git a/src/response.rs b/src/response.rs index 49f2b63fb..5e1c0d076 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] //! Http response use std::cell::RefCell; use std::collections::VecDeque; diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 36c0d7d50..cd74d456b 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -13,8 +13,8 @@ use net2::TcpBuilder; use actix_http::body::MessageBody; use actix_http::client::{ - ClientRequest, ClientRequestBuilder, ClientResponse, Connect, Connection, Connector, - ConnectorError, SendRequestError, + ClientRequest, ClientRequestBuilder, ClientResponse, Connect, Connector, + ConnectorError, RequestSender, SendRequestError, }; use actix_http::ws; @@ -57,7 +57,7 @@ impl TestServer { pub fn with_factory( factory: F, ) -> TestServerRuntime< - impl Service + Clone, + impl Service + Clone, > { let (tx, rx) = mpsc::channel(); @@ -89,7 +89,7 @@ impl TestServer { } fn new_connector( - ) -> impl Service + Clone + ) -> impl Service + Clone { #[cfg(feature = "ssl")] { @@ -192,7 +192,7 @@ impl TestServerRuntime { impl TestServerRuntime where T: Service + Clone, - T::Response: Connection, + T::Response: RequestSender, { /// Connect to websocket server at a given path pub fn ws_at( @@ -212,7 +212,7 @@ where } /// Send request and read response message - pub fn send_request( + pub fn send_request( &mut self, req: ClientRequest, ) -> Result { From 4217894d48d6572b24c329e2e9166a39f31b004b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 29 Jan 2019 10:14:00 -0800 Subject: [PATCH 111/427] cleaup warnings --- .travis.yml | 2 +- Cargo.toml | 6 +-- README.md | 7 ++- src/body.rs | 2 +- src/client/connector.rs | 4 +- src/client/error.rs | 8 +-- src/client/h1proto.rs | 4 +- src/client/h2proto.rs | 38 +++++++------- src/client/pool.rs | 4 +- src/client/response.rs | 1 + src/error.rs | 4 +- src/h1/decoder.rs | 81 +++--------------------------- src/h1/dispatcher.rs | 106 +++++++++++++++++++++++++++++++++++----- src/h1/encoder.rs | 8 +-- src/h1/service.rs | 1 + src/lib.rs | 6 +++ src/payload.rs | 16 +++--- src/response.rs | 4 +- src/service.rs | 4 +- src/test.rs | 3 +- src/ws/mask.rs | 6 +-- 21 files changed, 166 insertions(+), 149 deletions(-) diff --git a/.travis.yml b/.travis.yml index feae30596..fd7dfd4e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ script: - | if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install -f cargo-tarpaulin - RUST_BACKTRACE=1 cargo tarpaulin --out Xml + RUST_BACKTRACE=1 cargo tarpaulin --features="ssl" --out Xml bash <(curl -s https://codecov.io/bash) echo "Uploaded code coverage" fi diff --git a/Cargo.toml b/Cargo.toml index b3adfa823..af19cacaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,8 @@ description = "Actix http" readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -documentation = "https://actix.rs/api/actix-web/stable/actix_web/" +repository = "https://github.com/actix/actix-http.git" +documentation = "https://actix.rs/api/actix-http/stable/actix_http/" categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] @@ -20,7 +20,7 @@ features = ["session"] [badges] travis-ci = { repository = "actix/actix-web", branch = "master" } -appveyor = { repository = "fafhrd91/actix-web-hdy9d" } +appveyor = { repository = "actix/actix-http-hdy9d" } codecov = { repository = "actix/actix-web", branch = "master", service = "github" } [lib] diff --git a/README.md b/README.md index be8160968..4f9e44b90 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ -# Actix http [![Build Status](https://travis-ci.org/fafhrd91/actix-http.svg?branch=master)](https://travis-ci.org/fafhrd91/actix-http) [![Build status](https://ci.appveyor.com/api/projects/status/bwq6923pblqg55gk/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-http/branch/master) [![codecov](https://codecov.io/gh/fafhrd91/actix-http/branch/master/graph/badge.svg)](https://codecov.io/gh/fafhrd91/actix-http) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# Actix http [![Build Status](https://travis-ci.org/actix/actix-http.svg?branch=master)](https://travis-ci.org/actix/actix-http) [![Build status](https://ci.appveyor.com/api/projects/status/bwq6923pblqg55gk/branch/master?svg=true)](https://ci.appveyor.com/project/actix/actix-http/branch/master) [![codecov](https://codecov.io/gh/actix/actix-http/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-http) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Actix http ## Documentation & community resources * [User Guide](https://actix.rs/docs/) -* [API Documentation (Development)](https://actix.rs/actix-http/actix_http/) -* [API Documentation (Releases)](https://actix.rs/api/actix-http/stable/actix_http/) +* [API Documentation](https://docs.rs/actix-http/) * [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-http](https://crates.io/crates/actix-web) +* Cargo package: [actix-http](https://crates.io/crates/actix-http) * Minimum supported Rust version: 1.26 or later ## Example diff --git a/src/body.rs b/src/body.rs index 4b71e9bb9..12e2d0345 100644 --- a/src/body.rs +++ b/src/body.rs @@ -158,7 +158,7 @@ impl From<&'static str> for Body { impl From<&'static [u8]> for Body { fn from(s: &'static [u8]) -> Body { - Body::Bytes(Bytes::from_static(s.as_ref())) + Body::Bytes(Bytes::from_static(s)) } } diff --git a/src/client/connector.rs b/src/client/connector.rs index b573181ba..5b5356c75 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -47,9 +47,7 @@ impl Default for Connector { ssl.build() } #[cfg(not(feature = "ssl"))] - { - () - } + {} }; Connector { diff --git a/src/client/error.rs b/src/client/error.rs index e27a83d85..6c91ff976 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -65,12 +65,8 @@ impl From> for ConnectorError { fn from(err: HandshakeError) -> ConnectorError { match err { HandshakeError::SetupFailure(stack) => SslError::from(stack).into(), - HandshakeError::Failure(stream) => { - SslError::from(stream.into_error()).into() - } - HandshakeError::WouldBlock(stream) => { - SslError::from(stream.into_error()).into() - } + HandshakeError::Failure(stream) => stream.into_error().into(), + HandshakeError::WouldBlock(stream) => stream.into_error().into(), } } } diff --git a/src/client/h1proto.rs b/src/client/h1proto.rs index ed3c66d62..86fc10b84 100644 --- a/src/client/h1proto.rs +++ b/src/client/h1proto.rs @@ -26,9 +26,9 @@ where B: MessageBody, { let io = H1Connection { + created, + pool, io: Some(io), - created: created, - pool: pool, }; let len = body.length(); diff --git a/src/client/h2proto.rs b/src/client/h2proto.rs index fe42b909c..e3d5be0b0 100644 --- a/src/client/h2proto.rs +++ b/src/client/h2proto.rs @@ -91,25 +91,25 @@ impl Future for SendBody { type Error = SendRequestError; fn poll(&mut self) -> Poll { - if self.buf.is_none() { - match self.body.poll_next() { - Ok(Async::Ready(Some(buf))) => { - self.send.reserve_capacity(buf.len()); - self.buf = Some(buf); - } - Ok(Async::Ready(None)) => { - if let Err(e) = self.send.send_data(Bytes::new(), true) { - return Err(e.into()); - } - self.send.reserve_capacity(0); - return Ok(Async::Ready(())); - } - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(e) => return Err(e.into()), - } - } - loop { + if self.buf.is_none() { + match self.body.poll_next() { + Ok(Async::Ready(Some(buf))) => { + self.send.reserve_capacity(buf.len()); + self.buf = Some(buf); + } + Ok(Async::Ready(None)) => { + if let Err(e) = self.send.send_data(Bytes::new(), true) { + return Err(e.into()); + } + self.send.reserve_capacity(0); + return Ok(Async::Ready(())); + } + Ok(Async::NotReady) => return Ok(Async::NotReady), + Err(e) => return Err(e.into()), + } + } + match self.send.poll_capacity() { Ok(Async::NotReady) => return Ok(Async::NotReady), Ok(Async::Ready(None)) => return Ok(Async::Ready(())), @@ -125,7 +125,7 @@ impl Future for SendBody { self.send.reserve_capacity(buf.len()); self.buf = Some(buf); } - return self.poll(); + continue; } } Err(e) => return Err(e.into()), diff --git a/src/client/pool.rs b/src/client/pool.rs index 089c2627a..425e89395 100644 --- a/src/client/pool.rs +++ b/src/client/pool.rs @@ -248,7 +248,7 @@ where } match self.fut.poll() { - Err(err) => Err(err.into()), + Err(err) => Err(err), Ok(Async::Ready((_, io, proto))) => { let _ = self.inner.take(); if proto == Protocol::Http1 { @@ -259,7 +259,7 @@ where ))) } else { self.h2 = Some(handshake(io)); - return self.poll(); + self.poll() } } Ok(Async::NotReady) => Ok(Async::NotReady), diff --git a/src/client/response.rs b/src/client/response.rs index 005c0875b..9010a3c56 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -13,6 +13,7 @@ use crate::message::{Head, ResponseHead}; use super::h1proto::Payload; /// Client Response +#[derive(Default)] pub struct ClientResponse { pub(crate) head: ResponseHead, pub(crate) payload: RefCell>, diff --git a/src/error.rs b/src/error.rs index 5470534f7..43bf7cfb8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -837,7 +837,7 @@ mod tests { match ParseError::from($from) { e @ $error => { let desc = format!("{}", e); - assert_eq!(desc, $from.description().to_owned()); + assert_eq!(desc, format!("IO error: {}", $from.description())); } _ => unreachable!("{:?}", $from), } @@ -846,7 +846,7 @@ mod tests { #[test] fn test_from() { - // from_and_cause!(io::Error::new(io::ErrorKind::Other, "other") => ParseError::Io(..)); + from_and_cause!(io::Error::new(io::ErrorKind::Other, "other") => ParseError::Io(..)); from!(httparse::Error::HeaderName => ParseError::Header); from!(httparse::Error::HeaderName => ParseError::Header); from!(httparse::Error::HeaderValue => ParseError::Header); diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 7c17e909a..480864787 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -273,16 +273,14 @@ impl MessageType for ClientResponse { // message payload let decoder = if let PayloadLength::Payload(pl) = len { pl + } else if status == StatusCode::SWITCHING_PROTOCOLS { + // switching protocol or connect + PayloadType::Stream(PayloadDecoder::eof()) + } else if src.len() >= MAX_BUFFER_SIZE { + error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); + return Err(ParseError::TooLarge); } else { - if status == StatusCode::SWITCHING_PROTOCOLS { - // switching protocol or connect - PayloadType::Stream(PayloadDecoder::eof()) - } else if src.len() >= MAX_BUFFER_SIZE { - error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); - return Err(ParseError::TooLarge); - } else { - PayloadType::None - } + PayloadType::None }; msg.head.status = status; @@ -670,71 +668,6 @@ mod tests { }}; } - struct Buffer { - buf: Bytes, - err: Option, - } - - impl Buffer { - fn new(data: &'static str) -> Buffer { - Buffer { - buf: Bytes::from(data), - err: None, - } - } - } - - impl AsyncRead for Buffer {} - impl io::Read for Buffer { - fn read(&mut self, dst: &mut [u8]) -> Result { - if self.buf.is_empty() { - if self.err.is_some() { - Err(self.err.take().unwrap()) - } else { - Err(io::Error::new(io::ErrorKind::WouldBlock, "")) - } - } else { - let size = cmp::min(self.buf.len(), dst.len()); - let b = self.buf.split_to(size); - dst[..size].copy_from_slice(&b); - Ok(size) - } - } - } - - impl io::Write for Buffer { - fn write(&mut self, buf: &[u8]) -> io::Result { - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } - } - impl AsyncWrite for Buffer { - fn shutdown(&mut self) -> Poll<(), io::Error> { - Ok(Async::Ready(())) - } - fn write_buf(&mut self, _: &mut B) -> Poll { - Ok(Async::NotReady) - } - } - - // #[test] - // fn test_req_parse_err() { - // let mut sys = System::new("test"); - // let _ = sys.block_on(future::lazy(|| { - // let buf = Buffer::new("GET /test HTTP/1\r\n\r\n"); - // let readbuf = BytesMut::new(); - - // let mut h1 = Dispatcher::new(buf, |req| ok(Response::Ok().finish())); - // assert!(h1.poll_io().is_ok()); - // assert!(h1.poll_io().is_ok()); - // assert!(h1.flags.contains(Flags::READ_DISCONNECTED)); - // assert_eq!(h1.tasks.len(), 1); - // future::ok::<_, ()>(()) - // })); - // } - #[test] fn test_parse() { let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 569b1deeb..f66955af1 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -312,7 +312,7 @@ where Message::Item(req) => { match self.framed.get_codec().message_type() { MessageType::Payload => { - let (ps, pl) = Payload::new(false); + let (ps, pl) = Payload::create(false); *req.inner.payload.borrow_mut() = Some(pl); self.payload = Some(ps); } @@ -417,10 +417,10 @@ where // start shutdown timer if let Some(deadline) = self.config.client_disconnect_timer() { - self.ka_timer.as_mut().map(|timer| { + if let Some(timer) = self.ka_timer.as_mut() { timer.reset(deadline); let _ = timer.poll(); - }); + } } else { return Ok(()); } @@ -439,17 +439,14 @@ where self.state = State::None; } } else if let Some(deadline) = self.config.keep_alive_expire() { - self.ka_timer.as_mut().map(|timer| { + if let Some(timer) = self.ka_timer.as_mut() { timer.reset(deadline); let _ = timer.poll(); - }); + } } - } else { - let expire = self.ka_expire; - self.ka_timer.as_mut().map(|timer| { - timer.reset(expire); - let _ = timer.poll(); - }); + } else if let Some(timer) = self.ka_timer.as_mut() { + timer.reset(self.ka_expire); + let _ = timer.poll(); } } Async::NotReady => (), @@ -526,3 +523,90 @@ where } } } + +#[cfg(test)] +mod tests { + use std::{cmp, io}; + + use actix_codec::{AsyncRead, AsyncWrite}; + use actix_service::IntoService; + use bytes::{Buf, Bytes, BytesMut}; + use futures::future::{lazy, ok}; + + use super::*; + use crate::error::Error; + + struct Buffer { + buf: Bytes, + err: Option, + } + + impl Buffer { + fn new(data: &'static str) -> Buffer { + Buffer { + buf: Bytes::from(data), + err: None, + } + } + } + + impl AsyncRead for Buffer {} + impl io::Read for Buffer { + fn read(&mut self, dst: &mut [u8]) -> Result { + if self.buf.is_empty() { + if self.err.is_some() { + Err(self.err.take().unwrap()) + } else { + Err(io::Error::new(io::ErrorKind::WouldBlock, "")) + } + } else { + let size = cmp::min(self.buf.len(), dst.len()); + let b = self.buf.split_to(size); + dst[..size].copy_from_slice(&b); + Ok(size) + } + } + } + + impl io::Write for Buffer { + fn write(&mut self, buf: &[u8]) -> io::Result { + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + impl AsyncWrite for Buffer { + fn shutdown(&mut self) -> Poll<(), io::Error> { + Ok(Async::Ready(())) + } + fn write_buf(&mut self, _: &mut B) -> Poll { + Ok(Async::NotReady) + } + } + + #[test] + fn test_req_parse_err() { + let mut sys = actix_rt::System::new("test"); + let _ = sys.block_on(lazy(|| { + let buf = Buffer::new("GET /test HTTP/1\r\n\r\n"); + let readbuf = BytesMut::new(); + + let mut h1 = Dispatcher::new( + buf, + ServiceConfig::default(), + (|req| ok::<_, Error>(Response::Ok().finish())).into_service(), + ); + assert!(h1.poll().is_ok()); + assert!(h1.poll().is_ok()); + assert!(h1 + .inner + .as_ref() + .unwrap() + .flags + .contains(Flags::DISCONNECTED)); + // assert_eq!(h1.tasks.len(), 1); + ok::<_, ()>(()) + })); + } +} diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index 92456520a..32c8f9c48 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -104,10 +104,10 @@ pub(crate) trait MessageType: Sized { let mut remaining = dst.remaining_mut(); let mut buf = unsafe { &mut *(dst.bytes_mut() as *mut [u8]) }; for (key, value) in self.headers() { - match key { - &CONNECTION => continue, - &TRANSFER_ENCODING | &CONTENT_LENGTH if skip_len => continue, - &DATE => { + match *key { + CONNECTION => continue, + TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue, + DATE => { has_date = true; } _ => (), diff --git a/src/h1/service.rs b/src/h1/service.rs index 7c2589cc8..d8f63a323 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -272,6 +272,7 @@ where } /// `NewService` implementation for `OneRequestService` service +#[derive(Default)] pub struct OneRequest { config: ServiceConfig, _t: PhantomData, diff --git a/src/lib.rs b/src/lib.rs index 0dbaee6aa..442637251 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,12 @@ //! * `session` - enables session support, includes `ring` crate as //! dependency //! +#![allow( + clippy::type_complexity, + clippy::new_without_default, + clippy::new_without_default_derive +)] + #[macro_use] extern crate log; diff --git a/src/payload.rs b/src/payload.rs index ea266f70b..6665a0e41 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -42,7 +42,7 @@ impl Payload { /// * `PayloadSender` - *Sender* side of the stream /// /// * `Payload` - *Receiver* side of the stream - pub fn new(eof: bool) -> (PayloadSender, Payload) { + pub fn create(eof: bool) -> (PayloadSender, Payload) { let shared = Rc::new(RefCell::new(Inner::new(eof))); ( @@ -540,7 +540,7 @@ mod tests { Runtime::new() .unwrap() .block_on(lazy(|| { - let (_, payload) = Payload::new(false); + let (_, payload) = Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(payload.len, 0); @@ -557,7 +557,7 @@ mod tests { Runtime::new() .unwrap() .block_on(lazy(|| { - let (mut sender, payload) = Payload::new(false); + let (mut sender, payload) = Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); @@ -582,7 +582,7 @@ mod tests { Runtime::new() .unwrap() .block_on(lazy(|| { - let (mut sender, payload) = Payload::new(false); + let (mut sender, payload) = Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); @@ -600,7 +600,7 @@ mod tests { Runtime::new() .unwrap() .block_on(lazy(|| { - let (mut sender, payload) = Payload::new(false); + let (mut sender, payload) = Payload::create(false); let mut payload = PayloadBuffer::new(payload); sender.feed_data(Bytes::from("line1")); @@ -629,7 +629,7 @@ mod tests { Runtime::new() .unwrap() .block_on(lazy(|| { - let (mut sender, payload) = Payload::new(false); + let (mut sender, payload) = Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(Async::NotReady, payload.read_exact(2).ok().unwrap()); @@ -663,7 +663,7 @@ mod tests { Runtime::new() .unwrap() .block_on(lazy(|| { - let (mut sender, payload) = Payload::new(false); + let (mut sender, payload) = Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(Async::NotReady, payload.read_until(b"ne").ok().unwrap()); @@ -697,7 +697,7 @@ mod tests { Runtime::new() .unwrap() .block_on(lazy(|| { - let (_, mut payload) = Payload::new(false); + let (_, mut payload) = Payload::create(false); payload.unread_data(Bytes::from("data")); assert!(!payload.is_empty()); diff --git a/src/response.rs b/src/response.rs index 5e1c0d076..a4b65f2b4 100644 --- a/src/response.rs +++ b/src/response.rs @@ -109,7 +109,7 @@ impl Response { /// Constructs a response with body #[inline] pub fn with_body(status: StatusCode, body: B) -> Response { - ResponsePool::with_body(status, body.into()) + ResponsePool::with_body(status, body) } /// The source `error` for this response @@ -644,7 +644,7 @@ impl ResponseBuilder { } #[inline] -#[cfg_attr(feature = "cargo-clippy", allow(borrowed_box))] +#[allow(clippy::borrowed_box)] fn parts<'a>( parts: &'a mut Option>, err: &Option, diff --git a/src/service.rs b/src/service.rs index f98234e76..57828187d 100644 --- a/src/service.rs +++ b/src/service.rs @@ -85,7 +85,7 @@ where fn poll(&mut self) -> Poll { if let Some(res) = self.res.take() { - if let Err(_) = self.framed.as_mut().unwrap().force_send(res) { + if self.framed.as_mut().unwrap().force_send(res).is_err() { return Err((self.err.take().unwrap(), self.framed.take().unwrap())); } } @@ -232,6 +232,6 @@ where break; } } - return Ok(Async::Ready(self.framed.take().unwrap())); + Ok(Async::Ready(self.framed.take().unwrap())) } } diff --git a/src/test.rs b/src/test.rs index 4b7e30ac3..a26f31e59 100644 --- a/src/test.rs +++ b/src/test.rs @@ -144,9 +144,8 @@ impl TestRequest { uri, version, headers, - _cookies: _, payload, - prefix: _, + .. } = self; let mut req = Request::new(); diff --git a/src/ws/mask.rs b/src/ws/mask.rs index e9bfb3d56..157375417 100644 --- a/src/ws/mask.rs +++ b/src/ws/mask.rs @@ -1,5 +1,5 @@ //! This is code from [Tungstenite project](https://github.com/snapview/tungstenite-rs) -#![cfg_attr(feature = "cargo-clippy", allow(cast_ptr_alignment))] +#![allow(clippy::cast_ptr_alignment)] use std::ptr::copy_nonoverlapping; use std::slice; @@ -19,7 +19,7 @@ impl<'a> ShortSlice<'a> { /// Faster version of `apply_mask()` which operates on 8-byte blocks. #[inline] -#[cfg_attr(feature = "cargo-clippy", allow(cast_lossless))] +#[allow(clippy::cast_lossless)] pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) { // Extend the mask to 64 bits let mut mask_u64 = ((mask_u32 as u64) << 32) | (mask_u32 as u64); @@ -50,7 +50,7 @@ pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) { // TODO: copy_nonoverlapping here compiles to call memcpy. While it is not so // inefficient, it could be done better. The compiler does not understand that // a `ShortSlice` must be smaller than a u64. -#[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))] +#[allow(clippy::needless_pass_by_value)] fn xor_short(buf: ShortSlice, mask: u64) { // Unsafe: we know that a `ShortSlice` fits in a u64 unsafe { From 9a4eb5a848bce9ede1452cba385e2083abdae7f0 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 29 Jan 2019 10:17:38 -0800 Subject: [PATCH 112/427] update readme --- CHANGES.md | 2 +- Cargo.toml | 6 +++--- README.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c3c38011c..74fa4a22e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,5 @@ # Changes -## [0.1.0] - 2018-09-x +## [0.1.0] - 2019-01-x * Initial impl diff --git a/Cargo.toml b/Cargo.toml index af19cacaf..01aa1a0cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,9 +19,9 @@ edition = "2018" features = ["session"] [badges] -travis-ci = { repository = "actix/actix-web", branch = "master" } -appveyor = { repository = "actix/actix-http-hdy9d" } -codecov = { repository = "actix/actix-web", branch = "master", service = "github" } +travis-ci = { repository = "actix/actix-http", branch = "master" } +appveyor = { repository = "fafhrd91/actix-http-b1qsn" } +codecov = { repository = "actix/actix-http", branch = "master", service = "github" } [lib] name = "actix_http" diff --git a/README.md b/README.md index 4f9e44b90..a5a255672 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Actix http [![Build Status](https://travis-ci.org/actix/actix-http.svg?branch=master)](https://travis-ci.org/actix/actix-http) [![Build status](https://ci.appveyor.com/api/projects/status/bwq6923pblqg55gk/branch/master?svg=true)](https://ci.appveyor.com/project/actix/actix-http/branch/master) [![codecov](https://codecov.io/gh/actix/actix-http/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-http) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# Actix http [![Build Status](https://travis-ci.org/actix/actix-http.svg?branch=master)](https://travis-ci.org/actix/actix-http) [![Build status](https://ci.appveyor.com/api/projects/status/i51p65u2bukstgwg/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-http-b1qsn/branch/master) [![codecov](https://codecov.io/gh/actix/actix-http/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-http) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Actix http From 3e6bdbd9eee2b965eb97dea30e0e45c71ceffce6 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 29 Jan 2019 10:34:27 -0800 Subject: [PATCH 113/427] rename trait --- Cargo.toml | 2 +- src/client/connection.rs | 6 +++--- src/client/connector.rs | 4 ++-- src/client/mod.rs | 2 +- src/client/request.rs | 4 ++-- src/h1/decoder.rs | 5 +---- test-server/src/lib.rs | 10 +++++----- 7 files changed, 15 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 01aa1a0cc..88a9ba88f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,7 @@ serde_urlencoded = "0.5.3" time = "0.1" tokio-timer = "0.2" tokio-current-thread = "0.1" -trust-dns-resolver = { version="0.11.0-alpha.1", default-features = false } +trust-dns-resolver = { version="0.11.0-alpha.2", default-features = false } # openssl openssl = { version="0.10", optional = true } diff --git a/src/client/connection.rs b/src/client/connection.rs index b192caaeb..683738e28 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -18,7 +18,7 @@ pub(crate) enum ConnectionType { H2(SendRequest), } -pub trait RequestSender { +pub trait Connection { type Future: Future; /// Close connection @@ -76,7 +76,7 @@ impl IoConnection { } } -impl RequestSender for IoConnection +impl Connection for IoConnection where T: AsyncRead + AsyncWrite + 'static, { @@ -112,7 +112,7 @@ pub(crate) enum EitherConnection { B(IoConnection), } -impl RequestSender for EitherConnection +impl Connection for EitherConnection where A: AsyncRead + AsyncWrite + 'static, B: AsyncRead + AsyncWrite + 'static, diff --git a/src/client/connector.rs b/src/client/connector.rs index 5b5356c75..05e24e51c 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -7,7 +7,7 @@ use actix_utils::timeout::{TimeoutError, TimeoutService}; use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; use super::connect::Connect; -use super::connection::RequestSender; +use super::connection::Connection; use super::error::ConnectorError; use super::pool::{ConnectionPool, Protocol}; @@ -135,7 +135,7 @@ impl Connector { /// Finish configuration process and create connector service. pub fn service( self, - ) -> impl Service + Clone + ) -> impl Service + Clone { #[cfg(not(feature = "ssl"))] { diff --git a/src/client/mod.rs b/src/client/mod.rs index c6498f371..8d041827f 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -10,7 +10,7 @@ mod request; mod response; pub use self::connect::Connect; -pub use self::connection::RequestSender; +pub use self::connection::Connection; pub use self::connector::Connector; pub use self::error::{ConnectorError, InvalidUrlKind, SendRequestError}; pub use self::request::{ClientRequest, ClientRequestBuilder}; diff --git a/src/client/request.rs b/src/client/request.rs index a2233a2f1..b62ebaf3c 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -17,7 +17,7 @@ use crate::http::{ }; use crate::message::{ConnectionType, Head, RequestHead}; -use super::connection::RequestSender; +use super::connection::Connection; use super::response::ClientResponse; use super::{Connect, ConnectorError, SendRequestError}; @@ -177,7 +177,7 @@ where where B: 'static, T: Service, - I: RequestSender, + I: Connection, { let Self { head, body } = self; diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 480864787..74e1fb68c 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -604,10 +604,7 @@ impl ChunkedState { #[cfg(test)] mod tests { - use std::{cmp, io}; - - use actix_codec::{AsyncRead, AsyncWrite}; - use bytes::{Buf, Bytes, BytesMut}; + use bytes::{Bytes, BytesMut}; use http::{Method, Version}; use super::*; diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index cd74d456b..1ef452044 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -13,8 +13,8 @@ use net2::TcpBuilder; use actix_http::body::MessageBody; use actix_http::client::{ - ClientRequest, ClientRequestBuilder, ClientResponse, Connect, Connector, - ConnectorError, RequestSender, SendRequestError, + ClientRequest, ClientRequestBuilder, ClientResponse, Connect, Connection, Connector, + ConnectorError, SendRequestError, }; use actix_http::ws; @@ -57,7 +57,7 @@ impl TestServer { pub fn with_factory( factory: F, ) -> TestServerRuntime< - impl Service + Clone, + impl Service + Clone, > { let (tx, rx) = mpsc::channel(); @@ -89,7 +89,7 @@ impl TestServer { } fn new_connector( - ) -> impl Service + Clone + ) -> impl Service + Clone { #[cfg(feature = "ssl")] { @@ -192,7 +192,7 @@ impl TestServerRuntime { impl TestServerRuntime where T: Service + Clone, - T::Response: RequestSender, + T::Response: Connection, { /// Connect to websocket server at a given path pub fn ws_at( From 76866f054f16231d9e271dd5bac85abc9698ec1b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 30 Jan 2019 10:29:15 -0800 Subject: [PATCH 114/427] move service to submodule; update travis config --- .travis.yml | 10 ++++++++++ src/service/mod.rs | 3 +++ src/{service.rs => service/senderror.rs} | 0 3 files changed, 13 insertions(+) create mode 100644 src/service/mod.rs rename src/{service.rs => service/senderror.rs} (100%) diff --git a/.travis.yml b/.travis.yml index fd7dfd4e5..b7b43895e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,16 @@ matrix: allow_failures: - rust: nightly +env: + global: + - RUSTFLAGS="-C link-dead-code" + - OPENSSL_VERSION=openssl-1.0.2 + +before_install: + - sudo add-apt-repository -y ppa:0k53d-karl-f830m/openssl + - sudo apt-get update -qq + - sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev + before_script: - export PATH=$PATH:~/.cargo/bin diff --git a/src/service/mod.rs b/src/service/mod.rs new file mode 100644 index 000000000..83a40bd12 --- /dev/null +++ b/src/service/mod.rs @@ -0,0 +1,3 @@ +mod senderror; + +pub use self::senderror::{SendError, SendResponse}; diff --git a/src/service.rs b/src/service/senderror.rs similarity index 100% rename from src/service.rs rename to src/service/senderror.rs From 3269e3572295496ea5fd9f2115f243088c54b623 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 1 Feb 2019 20:18:44 -0800 Subject: [PATCH 115/427] migrate to actix-service 0.2 --- Cargo.toml | 10 +- src/client/connector.rs | 74 +++++++--- src/client/pool.rs | 15 +- src/client/request.rs | 2 +- src/h1/dispatcher.rs | 14 +- src/h1/service.rs | 30 ++-- src/h2/mod.rs | 20 +++ src/h2/service.rs | 310 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/service/senderror.rs | 12 +- src/ws/client/service.rs | 11 +- src/ws/frame.rs | 2 +- src/ws/service.rs | 6 +- src/ws/transport.rs | 10 +- test-server/Cargo.toml | 6 +- test-server/src/lib.rs | 12 +- 16 files changed, 460 insertions(+), 75 deletions(-) create mode 100644 src/h2/mod.rs create mode 100644 src/h2/service.rs diff --git a/Cargo.toml b/Cargo.toml index 88a9ba88f..f41147819 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,12 +37,10 @@ session = ["cookie/secure"] ssl = ["openssl", "actix-connector/ssl"] [dependencies] -actix-service = "0.1.6" +actix-service = "0.2.0" actix-codec = "0.1.0" -# actix-connector = "0.1.0" -# actix-utils = "0.1.0" -actix-connector = { git = "https://github.com/actix/actix-net.git" } -actix-utils = { git = "https://github.com/actix/actix-net.git" } +actix-connector = "0.2.0" +actix-utils = "0.2.0" base64 = "0.10" backtrace = "0.3" @@ -78,7 +76,7 @@ openssl = { version="0.10", optional = true } [dev-dependencies] actix-rt = "0.1.0" actix-web = "0.7" -actix-server = "0.1" +actix-server = "0.2" actix-http-test = { path="test-server" } env_logger = "0.6" serde_derive = "1.0" diff --git a/src/client/connector.rs b/src/client/connector.rs index 05e24e51c..fea9b9c0a 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -135,8 +135,11 @@ impl Connector { /// Finish configuration process and create connector service. pub fn service( self, - ) -> impl Service + Clone - { + ) -> impl Service< + Request = Connect, + Response = impl Connection, + Error = ConnectorError, + > + Clone { #[cfg(not(feature = "ssl"))] { let connector = TimeoutService::new( @@ -237,7 +240,11 @@ mod connect_impl { pub(crate) struct InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io, Protocol), + Error = ConnectorError, + >, { pub(crate) tcp_pool: ConnectionPool, } @@ -245,8 +252,11 @@ mod connect_impl { impl Clone for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service - + Clone, + T: Service< + Request = Connect, + Response = (Connect, Io, Protocol), + Error = ConnectorError, + > + Clone, { fn clone(&self) -> Self { InnerConnector { @@ -255,15 +265,20 @@ mod connect_impl { } } - impl Service for InnerConnector + impl Service for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io, Protocol), + Error = ConnectorError, + >, { + type Request = Connect; type Response = IoConnection; type Error = ConnectorError; type Future = Either< - as Service>::Future, + as Service>::Future, FutureResult, ConnectorError>, >; @@ -298,12 +313,12 @@ mod connect_impl { Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, T1: Service< - Connect, + Request = Connect, Response = (Connect, Io1, Protocol), Error = ConnectorError, >, T2: Service< - Connect, + Request = Connect, Response = (Connect, Io2, Protocol), Error = ConnectorError, >, @@ -317,12 +332,12 @@ mod connect_impl { Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, T1: Service< - Connect, + Request = Connect, Response = (Connect, Io1, Protocol), Error = ConnectorError, > + Clone, T2: Service< - Connect, + Request = Connect, Response = (Connect, Io2, Protocol), Error = ConnectorError, > + Clone, @@ -335,21 +350,22 @@ mod connect_impl { } } - impl Service for InnerConnector + impl Service for InnerConnector where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, T1: Service< - Connect, + Request = Connect, Response = (Connect, Io1, Protocol), Error = ConnectorError, >, T2: Service< - Connect, + Request = Connect, Response = (Connect, Io2, Protocol), Error = ConnectorError, >, { + type Request = Connect; type Response = EitherConnection; type Error = ConnectorError; type Future = Either< @@ -384,15 +400,23 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseA where Io1: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io1, Protocol), + Error = ConnectorError, + >, { - fut: as Service>::Future, + fut: as Service>::Future, _t: PhantomData, } impl Future for InnerConnectorResponseA where - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io1, Protocol), + Error = ConnectorError, + >, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { @@ -410,15 +434,23 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseB where Io2: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io2, Protocol), + Error = ConnectorError, + >, { - fut: as Service>::Future, + fut: as Service>::Future, _t: PhantomData, } impl Future for InnerConnectorResponseB where - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io2, Protocol), + Error = ConnectorError, + >, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { diff --git a/src/client/pool.rs b/src/client/pool.rs index 425e89395..188980cb3 100644 --- a/src/client/pool.rs +++ b/src/client/pool.rs @@ -48,7 +48,11 @@ pub(crate) struct ConnectionPool( impl ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io, Protocol), + Error = ConnectorError, + >, { pub(crate) fn new( connector: T, @@ -84,11 +88,16 @@ where } } -impl Service for ConnectionPool +impl Service for ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io, Protocol), + Error = ConnectorError, + >, { + type Request = Connect; type Response = IoConnection; type Error = ConnectorError; type Future = Either< diff --git a/src/client/request.rs b/src/client/request.rs index b62ebaf3c..b80f0e6dc 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -176,7 +176,7 @@ where ) -> impl Future where B: 'static, - T: Service, + T: Service, I: Connection, { let Self { head, body } = self; diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index f66955af1..7780223f2 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -35,14 +35,14 @@ bitflags! { } /// Dispatcher for HTTP/1.1 protocol -pub struct Dispatcher, B: MessageBody> +pub struct Dispatcher where S::Error: Debug, { inner: Option>, } -struct InnerDispatcher, B: MessageBody> +struct InnerDispatcher where S::Error: Debug, { @@ -66,13 +66,13 @@ enum DispatcherMessage { Error(Response<()>), } -enum State, B: MessageBody> { +enum State { None, ServiceCall(S::Future), SendPayload(ResponseBody), } -impl, B: MessageBody> State { +impl State { fn is_empty(&self) -> bool { if let State::None = self { true @@ -85,7 +85,7 @@ impl, B: MessageBody> State { impl Dispatcher where T: AsyncRead + AsyncWrite, - S: Service>, + S: Service>, S::Error: Debug, B: MessageBody, { @@ -139,7 +139,7 @@ where impl InnerDispatcher where T: AsyncRead + AsyncWrite, - S: Service>, + S: Service>, S::Error: Debug, B: MessageBody, { @@ -459,7 +459,7 @@ where impl Future for Dispatcher where T: AsyncRead + AsyncWrite, - S: Service>, + S: Service>, S::Error: Debug, B: MessageBody, { diff --git a/src/h1/service.rs b/src/h1/service.rs index d8f63a323..c35d18714 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -27,13 +27,13 @@ pub struct H1Service { impl H1Service where - S: NewService> + Clone, + S: NewService> + Clone, S::Service: Clone, S::Error: Debug, B: MessageBody, { /// Create new `HttpService` instance. - pub fn new>(service: F) -> Self { + pub fn new>(service: F) -> Self { let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0); H1Service { @@ -49,14 +49,15 @@ where } } -impl NewService for H1Service +impl NewService for H1Service where T: AsyncRead + AsyncWrite, - S: NewService> + Clone, + S: NewService> + Clone, S::Service: Clone, S::Error: Debug, B: MessageBody, { + type Request = T; type Response = H1ServiceResult; type Error = DispatchError; type InitError = S::InitError; @@ -88,7 +89,7 @@ pub struct H1ServiceBuilder { impl H1ServiceBuilder where - S: NewService, + S: NewService, S::Service: Clone, S::Error: Debug, { @@ -187,7 +188,7 @@ where pub fn finish(self, service: F) -> H1Service where B: MessageBody, - F: IntoNewService, + F: IntoNewService, { let cfg = ServiceConfig::new( self.keep_alive, @@ -203,7 +204,7 @@ where } #[doc(hidden)] -pub struct H1ServiceResponse, B> { +pub struct H1ServiceResponse { fut: S::Future, cfg: Option, _t: PhantomData<(T, B)>, @@ -212,7 +213,7 @@ pub struct H1ServiceResponse, B> { impl Future for H1ServiceResponse where T: AsyncRead + AsyncWrite, - S: NewService>, + S: NewService>, S::Service: Clone, S::Error: Debug, B: MessageBody, @@ -238,7 +239,7 @@ pub struct H1ServiceHandler { impl H1ServiceHandler where - S: Service> + Clone, + S: Service> + Clone, S::Error: Debug, B: MessageBody, { @@ -251,13 +252,14 @@ where } } -impl Service for H1ServiceHandler +impl Service for H1ServiceHandler where T: AsyncRead + AsyncWrite, - S: Service> + Clone, + S: Service> + Clone, S::Error: Debug, B: MessageBody, { + type Request = T; type Response = H1ServiceResult; type Error = DispatchError; type Future = Dispatcher; @@ -291,10 +293,11 @@ where } } -impl NewService for OneRequest +impl NewService for OneRequest where T: AsyncRead + AsyncWrite, { + type Request = T; type Response = (Request, Framed); type Error = ParseError; type InitError = (); @@ -316,10 +319,11 @@ pub struct OneRequestService { _t: PhantomData, } -impl Service for OneRequestService +impl Service for OneRequestService where T: AsyncRead + AsyncWrite, { + type Request = T; type Response = (Request, Framed); type Error = ParseError; type Future = OneRequestServiceResponse; diff --git a/src/h2/mod.rs b/src/h2/mod.rs new file mode 100644 index 000000000..4a54ec9fe --- /dev/null +++ b/src/h2/mod.rs @@ -0,0 +1,20 @@ +use std::fmt; + +mod service; + +/// H1 service response type +pub enum H2ServiceResult { + Disconnected, + Shutdown(T), +} + +impl fmt::Debug for H2ServiceResult { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + H2ServiceResult::Disconnected => write!(f, "H2ServiceResult::Disconnected"), + H2ServiceResult::Shutdown(ref v) => { + write!(f, "H2ServiceResult::Shutdown({:?})", v) + } + } + } +} diff --git a/src/h2/service.rs b/src/h2/service.rs new file mode 100644 index 000000000..827f84488 --- /dev/null +++ b/src/h2/service.rs @@ -0,0 +1,310 @@ +use std::fmt::Debug; +use std::marker::PhantomData; +use std::net; + +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_service::{IntoNewService, NewService, Service}; +use bytes::Bytes; +use futures::future::{ok, FutureResult}; +use futures::{try_ready, Async, Future, Poll, Stream}; +use h2::server::{self, Connection, Handshake}; +use log::error; + +use crate::body::MessageBody; +use crate::config::{KeepAlive, ServiceConfig}; +use crate::error::{DispatchError, ParseError}; +use crate::request::Request; +use crate::response::Response; + +// use super::dispatcher::Dispatcher; +use super::H2ServiceResult; + +/// `NewService` implementation for HTTP2 transport +pub struct H2Service { + srv: S, + cfg: ServiceConfig, + _t: PhantomData<(T, B)>, +} + +impl H2Service +where + S: NewService> + Clone, + S::Service: Clone, + S::Error: Debug, + B: MessageBody, +{ + /// Create new `HttpService` instance. + pub fn new>(service: F) -> Self { + let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0); + + H2Service { + cfg, + srv: service.into_new_service(), + _t: PhantomData, + } + } + + /// Create builder for `HttpService` instance. + pub fn build() -> H2ServiceBuilder { + H2ServiceBuilder::new() + } +} + +impl NewService for H2Service +where + T: AsyncRead + AsyncWrite, + S: NewService> + Clone, + S::Service: Clone, + S::Error: Debug, + B: MessageBody, +{ + type Request = T; + type Response = H2ServiceResult; + type Error = (); //DispatchError; + type InitError = S::InitError; + type Service = H2ServiceHandler; + type Future = H2ServiceResponse; + + fn new_service(&self) -> Self::Future { + H2ServiceResponse { + fut: self.srv.new_service(), + cfg: Some(self.cfg.clone()), + _t: PhantomData, + } + } +} + +/// A http/2 new service builder +/// +/// This type can be used to construct an instance of `ServiceConfig` through a +/// builder-like pattern. +pub struct H2ServiceBuilder { + keep_alive: KeepAlive, + client_timeout: u64, + client_disconnect: u64, + host: String, + addr: net::SocketAddr, + secure: bool, + _t: PhantomData<(T, S)>, +} + +impl H2ServiceBuilder +where + S: NewService, + S::Service: Clone, + S::Error: Debug, +{ + /// Create instance of `H2ServiceBuilder` + pub fn new() -> H2ServiceBuilder { + H2ServiceBuilder { + keep_alive: KeepAlive::Timeout(5), + client_timeout: 5000, + client_disconnect: 0, + secure: false, + host: "localhost".to_owned(), + addr: "127.0.0.1:8080".parse().unwrap(), + _t: PhantomData, + } + } + + /// Enable secure flag for current server. + /// This flags also enables `client disconnect timeout`. + /// + /// By default this flag is set to false. + pub fn secure(mut self) -> Self { + self.secure = true; + if self.client_disconnect == 0 { + self.client_disconnect = 3000; + } + self + } + + /// Set server keep-alive setting. + /// + /// By default keep alive is set to a 5 seconds. + pub fn keep_alive>(mut self, val: U) -> Self { + self.keep_alive = val.into(); + self + } + + /// Set server client timeout in milliseconds for first request. + /// + /// Defines a timeout for reading client request header. If a client does not transmit + /// the entire set headers within this time, the request is terminated with + /// the 408 (Request Time-out) error. + /// + /// To disable timeout set value to 0. + /// + /// By default client timeout is set to 5000 milliseconds. + pub fn client_timeout(mut self, val: u64) -> Self { + self.client_timeout = val; + self + } + + /// Set server connection disconnect timeout in milliseconds. + /// + /// Defines a timeout for disconnect connection. If a disconnect procedure does not complete + /// within this time, the request get dropped. This timeout affects secure connections. + /// + /// To disable timeout set value to 0. + /// + /// By default disconnect timeout is set to 3000 milliseconds. + pub fn client_disconnect(mut self, val: u64) -> Self { + self.client_disconnect = val; + self + } + + /// Set server host name. + /// + /// Host name is used by application router aa a hostname for url + /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. + /// html#method.host) documentation for more information. + /// + /// By default host name is set to a "localhost" value. + pub fn server_hostname(mut self, val: &str) -> Self { + self.host = val.to_owned(); + self + } + + /// Set server ip address. + /// + /// Host name is used by application router aa a hostname for url + /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. + /// html#method.host) documentation for more information. + /// + /// By default server address is set to a "127.0.0.1:8080" + pub fn server_address(mut self, addr: U) -> Self { + match addr.to_socket_addrs() { + Err(err) => error!("Can not convert to SocketAddr: {}", err), + Ok(mut addrs) => { + if let Some(addr) = addrs.next() { + self.addr = addr; + } + } + } + self + } + + /// Finish service configuration and create `H1Service` instance. + pub fn finish(self, service: F) -> H2Service + where + B: MessageBody, + F: IntoNewService, + { + let cfg = ServiceConfig::new( + self.keep_alive, + self.client_timeout, + self.client_disconnect, + ); + H2Service { + cfg, + srv: service.into_new_service(), + _t: PhantomData, + } + } +} + +#[doc(hidden)] +pub struct H2ServiceResponse { + fut: S::Future, + cfg: Option, + _t: PhantomData<(T, B)>, +} + +impl Future for H2ServiceResponse +where + T: AsyncRead + AsyncWrite, + S: NewService>, + S::Service: Clone, + S::Error: Debug, + B: MessageBody, +{ + type Item = H2ServiceHandler; + type Error = S::InitError; + + fn poll(&mut self) -> Poll { + let service = try_ready!(self.fut.poll()); + Ok(Async::Ready(H2ServiceHandler::new( + self.cfg.take().unwrap(), + service, + ))) + } +} + +/// `Service` implementation for http/2 transport +pub struct H2ServiceHandler { + srv: S, + cfg: ServiceConfig, + _t: PhantomData<(T, B)>, +} + +impl H2ServiceHandler +where + S: Service> + Clone, + S::Error: Debug, + B: MessageBody, +{ + fn new(cfg: ServiceConfig, srv: S) -> H2ServiceHandler { + H2ServiceHandler { + srv, + cfg, + _t: PhantomData, + } + } +} + +impl Service for H2ServiceHandler +where + T: AsyncRead + AsyncWrite, + S: Service> + Clone, + S::Error: Debug, + B: MessageBody, +{ + type Request = T; + type Response = H2ServiceResult; + type Error = (); // DispatchError; + type Future = H2ServiceHandlerResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.srv.poll_ready().map_err(|_| ()) + } + + fn call(&mut self, req: T) -> Self::Future { + H2ServiceHandlerResponse { + state: State::Handshake(server::handshake(req)), + _t: PhantomData, + } + } +} + +enum State { + Handshake(Handshake), + Connection(Connection), + Empty, +} + +pub struct H2ServiceHandlerResponse +where + T: AsyncRead + AsyncWrite, + S: Service> + Clone, + S::Error: Debug, + B: MessageBody, +{ + state: State, + _t: PhantomData, +} + +impl Future for H2ServiceHandlerResponse +where + T: AsyncRead + AsyncWrite, + S: Service> + Clone, + S::Error: Debug, + B: MessageBody, +{ + type Item = H2ServiceResult; + type Error = (); + + fn poll(&mut self) -> Poll { + unimplemented!() + } +} diff --git a/src/lib.rs b/src/lib.rs index 442637251..cdb4f0382 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,7 @@ mod service; pub mod error; pub mod h1; +pub mod h2; pub mod test; pub mod ws; diff --git a/src/service/senderror.rs b/src/service/senderror.rs index 57828187d..b469a61e6 100644 --- a/src/service/senderror.rs +++ b/src/service/senderror.rs @@ -22,11 +22,12 @@ where } } -impl NewService)>> for SendError +impl NewService for SendError where T: AsyncRead + AsyncWrite, E: ResponseError, { + type Request = Result)>; type Response = R; type Error = (E, Framed); type InitError = (); @@ -38,11 +39,12 @@ where } } -impl Service)>> for SendError +impl Service for SendError where T: AsyncRead + AsyncWrite, E: ResponseError, { + type Request = Result)>; type Response = R; type Error = (E, Framed); type Future = Either)>, SendErrorFut>; @@ -128,11 +130,12 @@ where } } -impl NewService<(Response, Framed)> for SendResponse +impl NewService for SendResponse where T: AsyncRead + AsyncWrite, B: MessageBody, { + type Request = (Response, Framed); type Response = Framed; type Error = Error; type InitError = (); @@ -144,11 +147,12 @@ where } } -impl Service<(Response, Framed)> for SendResponse +impl Service for SendResponse where T: AsyncRead + AsyncWrite, B: MessageBody, { + type Request = (Response, Framed); type Response = Framed; type Error = Error; type Future = SendResponseFut; diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index c48b6e0c1..586873d19 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -26,7 +26,7 @@ pub type DefaultClient = Client; /// WebSocket's client pub struct Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite, { connector: T, @@ -34,7 +34,7 @@ where impl Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite, { /// Create new websocket's client factory @@ -51,7 +51,7 @@ impl Default for Client { impl Clone for Client where - T: Service + Clone, + T: Service + Clone, T::Response: AsyncRead + AsyncWrite, { fn clone(&self) -> Self { @@ -61,12 +61,13 @@ where } } -impl Service for Client +impl Service for Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite + 'static, T::Future: 'static, { + type Request = Connect; type Response = Framed; type Error = ClientError; type Future = Either< diff --git a/src/ws/frame.rs b/src/ws/frame.rs index 32ad4ef4f..d4c15627f 100644 --- a/src/ws/frame.rs +++ b/src/ws/frame.rs @@ -117,7 +117,7 @@ impl Parser { // control frames must have length <= 125 match opcode { OpCode::Ping | OpCode::Pong if length > 125 => { - return Err(ProtocolError::InvalidLength(length)) + return Err(ProtocolError::InvalidLength(length)); } OpCode::Close if length > 125 => { debug!("Received close frame with payload length exceeding 125. Morphing to protocol close frame."); diff --git a/src/ws/service.rs b/src/ws/service.rs index 8189b1955..137d41d43 100644 --- a/src/ws/service.rs +++ b/src/ws/service.rs @@ -20,7 +20,8 @@ impl Default for VerifyWebSockets { } } -impl NewService<(Request, Framed)> for VerifyWebSockets { +impl NewService for VerifyWebSockets { + type Request = (Request, Framed); type Response = (Request, Framed); type Error = (HandshakeError, Framed); type InitError = (); @@ -32,7 +33,8 @@ impl NewService<(Request, Framed)> for VerifyWebSockets { } } -impl Service<(Request, Framed)> for VerifyWebSockets { +impl Service for VerifyWebSockets { + type Request = (Request, Framed); type Response = (Request, Framed); type Error = (HandshakeError, Framed); type Future = FutureResult; diff --git a/src/ws/transport.rs b/src/ws/transport.rs index 6a4f4d227..da7782be5 100644 --- a/src/ws/transport.rs +++ b/src/ws/transport.rs @@ -7,7 +7,7 @@ use super::{Codec, Frame, Message}; pub struct Transport where - S: Service + 'static, + S: Service + 'static, T: AsyncRead + AsyncWrite, { inner: FramedTransport, @@ -16,17 +16,17 @@ where impl Transport where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Future: 'static, S::Error: 'static, { - pub fn new>(io: T, service: F) -> Self { + pub fn new>(io: T, service: F) -> Self { Transport { inner: FramedTransport::new(Framed::new(io, Codec::new()), service), } } - pub fn with>(framed: Framed, service: F) -> Self { + pub fn with>(framed: Framed, service: F) -> Self { Transport { inner: FramedTransport::new(framed, service), } @@ -36,7 +36,7 @@ where impl Future for Transport where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Future: 'static, S::Error: 'static, { diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 851b3efe4..81a3d909c 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -30,11 +30,11 @@ session = ["cookie/secure"] [dependencies] actix-codec = "0.1" -actix-service = "0.1.6" +actix-service = "0.2.0" actix-rt = "0.1.0" -actix-server = "0.1.0" +actix-server = "0.2.0" +actix-utils = "0.2.0" actix-http = { path=".." } -actix-utils = { git = "https://github.com/actix/actix-net.git" } base64 = "0.10" bytes = "0.4" diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 1ef452044..8083ebb15 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -57,7 +57,8 @@ impl TestServer { pub fn with_factory( factory: F, ) -> TestServerRuntime< - impl Service + Clone, + impl Service + + Clone, > { let (tx, rx) = mpsc::channel(); @@ -89,8 +90,11 @@ impl TestServer { } fn new_connector( - ) -> impl Service + Clone - { + ) -> impl Service< + Request = Connect, + Response = impl Connection, + Error = ConnectorError, + > + Clone { #[cfg(feature = "ssl")] { use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; @@ -191,7 +195,7 @@ impl TestServerRuntime { impl TestServerRuntime where - T: Service + Clone, + T: Service + Clone, T::Response: Connection, { /// Connect to websocket server at a given path From e70c7f2a5d7d5dac8e66fa7c1e180a58f09bad14 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 1 Feb 2019 20:22:43 -0800 Subject: [PATCH 116/427] upgrade derive-more --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f41147819..a039b4557 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ bitflags = "1.0" bytes = "0.4" byteorder = "1.2" cookie = { version="0.11", features=["percent-encode"] } -derive_more = "0.13" +derive_more = "0.14" encoding = "0.2" futures = "0.1" hashbrown = "0.1.8" From c9bb2116feb8ac6c6c40a7f8f63e03dff8c973d5 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Feb 2019 10:50:29 -0800 Subject: [PATCH 117/427] update actix-utils --- Cargo.toml | 4 ++++ src/client/connector.rs | 14 +++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a039b4557..ff19a4f96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,3 +80,7 @@ actix-server = "0.2" actix-http-test = { path="test-server" } env_logger = "0.6" serde_derive = "1.0" + +[patch.crates-io] +actix-utils = { git = "https://github.com/actix/actix-net.git" } +actix-service = { git = "https://github.com/actix/actix-net.git" } diff --git a/src/client/connector.rs b/src/client/connector.rs index fea9b9c0a..8e3c4b5ae 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -2,7 +2,7 @@ use std::time::Duration; use actix_codec::{AsyncRead, AsyncWrite}; use actix_connector::{Resolver, TcpConnector}; -use actix_service::{Service, ServiceExt}; +use actix_service::{Apply, Service, ServiceExt}; use actix_utils::timeout::{TimeoutError, TimeoutService}; use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; @@ -142,8 +142,8 @@ impl Connector { > + Clone { #[cfg(not(feature = "ssl"))] { - let connector = TimeoutService::new( - self.timeout, + let connector = Apply::new( + TimeoutService::new(self.timeout), self.resolver.map_err(ConnectorError::from).and_then( TcpConnector::default() .from_err() @@ -167,8 +167,8 @@ impl Connector { } #[cfg(feature = "ssl")] { - let ssl_service = TimeoutService::new( - self.timeout, + let ssl_service = Apply::new( + TimeoutService::new(self.timeout), self.resolver .clone() .map_err(ConnectorError::from) @@ -196,8 +196,8 @@ impl Connector { TimeoutError::Timeout => ConnectorError::Timeout, }); - let tcp_service = TimeoutService::new( - self.timeout, + let tcp_service = Apply::new( + TimeoutService::new(self.timeout), self.resolver.map_err(ConnectorError::from).and_then( TcpConnector::default() .from_err() From ef5b54a48151f457a8ca47c6683b44feb3dd8525 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Feb 2019 14:05:44 -0800 Subject: [PATCH 118/427] use released service crate --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ff19a4f96..37e6a066c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ session = ["cookie/secure"] ssl = ["openssl", "actix-connector/ssl"] [dependencies] -actix-service = "0.2.0" +actix-service = "0.2.1" actix-codec = "0.1.0" actix-connector = "0.2.0" actix-utils = "0.2.0" @@ -83,4 +83,3 @@ serde_derive = "1.0" [patch.crates-io] actix-utils = { git = "https://github.com/actix/actix-net.git" } -actix-service = { git = "https://github.com/actix/actix-net.git" } From 55a29d37782443ccf7485caf3c7a7dd270bd60f7 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Feb 2019 11:44:15 -0800 Subject: [PATCH 119/427] add h2 server support --- .travis.yml | 2 +- Cargo.toml | 7 +- src/body.rs | 21 +++ src/client/connector.rs | 11 +- src/client/h2proto.rs | 57 +++++-- src/client/response.rs | 2 +- src/config.rs | 4 + src/error.rs | 10 ++ src/h1/dispatcher.rs | 4 +- src/h2/dispatcher.rs | 325 ++++++++++++++++++++++++++++++++++++++++ src/h2/mod.rs | 42 ++++++ src/h2/service.rs | 136 +++++++++++------ src/httpmessage.rs | 20 +-- src/json.rs | 2 +- src/message.rs | 4 - src/request.rs | 73 ++++++--- src/test.rs | 21 +-- test-server/Cargo.toml | 5 + test-server/src/lib.rs | 25 +++- tests/cert.pem | 43 ++---- tests/key.pem | 79 ++++------ tests/test_server.rs | 85 ++++++++++- 22 files changed, 774 insertions(+), 204 deletions(-) create mode 100644 src/h2/dispatcher.rs diff --git a/.travis.yml b/.travis.yml index b7b43895e..c9c9db14f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,7 @@ script: - | if [[ "$TRAVIS_RUST_VERSION" != "nightly" ]]; then cargo clean - cargo test + cargo test --features="ssl" fi - | if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then diff --git a/Cargo.toml b/Cargo.toml index 37e6a066c..bbb31c161 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,13 +34,13 @@ default = ["session"] session = ["cookie/secure"] # openssl -ssl = ["openssl", "actix-connector/ssl"] +ssl = ["openssl", "actix-connector/ssl", "actix-server/ssl", "actix-http-test/ssl"] [dependencies] actix-service = "0.2.1" actix-codec = "0.1.0" actix-connector = "0.2.0" -actix-utils = "0.2.0" +actix-utils = "0.2.1" base64 = "0.10" backtrace = "0.3" @@ -80,6 +80,3 @@ actix-server = "0.2" actix-http-test = { path="test-server" } env_logger = "0.6" serde_derive = "1.0" - -[patch.crates-io] -actix-utils = { git = "https://github.com/actix/actix-net.git" } diff --git a/src/body.rs b/src/body.rs index 12e2d0345..1c54d4ce7 100644 --- a/src/body.rs +++ b/src/body.rs @@ -20,6 +20,18 @@ pub enum BodyLength { Stream, } +impl BodyLength { + pub fn is_eof(&self) -> bool { + match self { + BodyLength::None + | BodyLength::Empty + | BodyLength::Sized(0) + | BodyLength::Sized64(0) => true, + _ => false, + } + } +} + /// Type that provides this trait can be streamed to a peer. pub trait MessageBody { fn length(&self) -> BodyLength; @@ -42,6 +54,15 @@ pub enum ResponseBody { Other(Body), } +impl ResponseBody { + pub fn into_body(self) -> ResponseBody { + match self { + ResponseBody::Body(b) => ResponseBody::Other(b), + ResponseBody::Other(b) => ResponseBody::Other(b), + } + } +} + impl ResponseBody { pub fn as_ref(&self) -> Option<&B> { if let ResponseBody::Body(ref b) = self { diff --git a/src/client/connector.rs b/src/client/connector.rs index 8e3c4b5ae..32ba50121 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -12,11 +12,7 @@ use super::error::ConnectorError; use super::pool::{ConnectionPool, Protocol}; #[cfg(feature = "ssl")] -use actix_connector::ssl::OpensslConnector; -#[cfg(feature = "ssl")] -use openssl::ssl::{SslConnector, SslMethod}; -#[cfg(feature = "ssl")] -const H2: &[u8] = b"h2"; +use openssl::ssl::SslConnector; #[cfg(not(feature = "ssl"))] type SslConnector = (); @@ -40,6 +36,8 @@ impl Default for Connector { #[cfg(feature = "ssl")] { use log::error; + use openssl::ssl::{SslConnector, SslMethod}; + let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap(); let _ = ssl .set_alpn_protos(b"\x02h2\x08http/1.1") @@ -167,6 +165,9 @@ impl Connector { } #[cfg(feature = "ssl")] { + const H2: &[u8] = b"h2"; + use actix_connector::ssl::OpensslConnector; + let ssl_service = Apply::new( TimeoutService::new(self.timeout), self.resolver diff --git a/src/client/h2proto.rs b/src/client/h2proto.rs index e3d5be0b0..ecd18cf82 100644 --- a/src/client/h2proto.rs +++ b/src/client/h2proto.rs @@ -4,16 +4,19 @@ use std::time; use actix_codec::{AsyncRead, AsyncWrite}; use bytes::Bytes; use futures::future::{err, Either}; -use futures::{Async, Future, Poll, Stream}; +use futures::{Async, Future, Poll}; use h2::{client::SendRequest, SendStream}; -use http::{request::Request, Version}; +use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; +use http::{request::Request, HttpTryFrom, Version}; + +use crate::body::{BodyLength, MessageBody}; +use crate::h2::Payload; +use crate::message::{RequestHead, ResponseHead}; use super::connection::{ConnectionType, IoConnection}; use super::error::SendRequestError; use super::pool::Acquired; use super::response::ClientResponse; -use crate::body::{BodyLength, MessageBody}; -use crate::message::{RequestHead, ResponseHead}; pub(crate) fn send_request( io: SendRequest, @@ -27,7 +30,8 @@ where B: MessageBody, { trace!("Sending client request: {:?} {:?}", head, body.length()); - let eof = match body.length() { + let length = body.length(); + let eof = match length { BodyLength::None | BodyLength::Empty | BodyLength::Sized(0) => true, _ => false, }; @@ -38,11 +42,44 @@ where let mut req = Request::new(()); *req.uri_mut() = head.uri; *req.method_mut() = head.method; - *req.headers_mut() = head.headers; *req.version_mut() = Version::HTTP_2; + let mut skip_len = true; + let mut has_date = false; + + // Content length + let _ = match length { + BodyLength::Chunked | BodyLength::None => None, + BodyLength::Stream => { + skip_len = false; + None + } + BodyLength::Empty => req + .headers_mut() + .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), + BodyLength::Sized(len) => req.headers_mut().insert( + CONTENT_LENGTH, + HeaderValue::try_from(format!("{}", len)).unwrap(), + ), + BodyLength::Sized64(len) => req.headers_mut().insert( + CONTENT_LENGTH, + HeaderValue::try_from(format!("{}", len)).unwrap(), + ), + }; + + // copy headers + for (key, value) in head.headers.iter() { + match *key { + CONNECTION | TRANSFER_ENCODING => continue, // http2 specific + CONTENT_LENGTH if skip_len => continue, + DATE => has_date = true, + _ => (), + } + req.headers_mut().append(key, value.clone()); + } + match io.send_request(req, eof) { - Ok((resp, send)) => { + Ok((res, send)) => { release(io, pool, created, false); if !eof { @@ -52,10 +89,10 @@ where send, buf: None, } - .and_then(move |_| resp.map_err(SendRequestError::from)), + .and_then(move |_| res.map_err(SendRequestError::from)), )) } else { - Either::B(resp.map_err(SendRequestError::from)) + Either::B(res.map_err(SendRequestError::from)) } } Err(e) => { @@ -74,7 +111,7 @@ where Ok(ClientResponse { head, - payload: RefCell::new(Some(Box::new(body.from_err()))), + payload: RefCell::new(Some(Box::new(Payload::new(body)))), }) }) .from_err() diff --git a/src/client/response.rs b/src/client/response.rs index 9010a3c56..6224d3cb5 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -27,7 +27,7 @@ impl HttpMessage for ClientResponse { } #[inline] - fn payload(&self) -> Self::Stream { + fn payload(self) -> Self::Stream { if let Some(payload) = self.payload.borrow_mut().take() { payload } else { diff --git a/src/config.rs b/src/config.rs index c37601dbe..960f13706 100644 --- a/src/config.rs +++ b/src/config.rs @@ -171,6 +171,10 @@ impl ServiceConfig { buf[35..].copy_from_slice(b"\r\n\r\n"); dst.extend_from_slice(&buf); } + + pub(crate) fn set_date_header(&self, dst: &mut BytesMut) { + dst.extend_from_slice(&self.0.timer.date().bytes); + } } /// A service config builder diff --git a/src/error.rs b/src/error.rs index 43bf7cfb8..03224b558 100644 --- a/src/error.rs +++ b/src/error.rs @@ -389,6 +389,10 @@ pub enum DispatchError { #[display(fmt = "Parse error: {}", _0)] Parse(ParseError), + /// Http/2 error + #[display(fmt = "{}", _0)] + H2(h2::Error), + /// The first request did not complete within the specified timeout. #[display(fmt = "The first request did not complete within the specified timeout")] SlowRequestTimeout, @@ -426,6 +430,12 @@ impl From for DispatchError { } } +impl From for DispatchError { + fn from(err: h2::Error) -> Self { + DispatchError::H2(err) + } +} + /// A set of error that can occure during parsing content type #[derive(PartialEq, Debug, Display)] pub enum ContentTypeError { diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 7780223f2..1295dfddf 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -309,11 +309,11 @@ where self.flags.insert(Flags::STARTED); match msg { - Message::Item(req) => { + Message::Item(mut req) => { match self.framed.get_codec().message_type() { MessageType::Payload => { let (ps, pl) = Payload::create(false); - *req.inner.payload.borrow_mut() = Some(pl); + req = req.set_payload(pl); self.payload = Some(ps); } MessageType::Stream => { diff --git a/src/h2/dispatcher.rs b/src/h2/dispatcher.rs new file mode 100644 index 000000000..2994d0a3d --- /dev/null +++ b/src/h2/dispatcher.rs @@ -0,0 +1,325 @@ +use std::collections::VecDeque; +use std::marker::PhantomData; +use std::time::Instant; +use std::{fmt, mem}; + +use actix_codec::{AsyncRead, AsyncWrite}; +use actix_service::Service; +use bitflags::bitflags; +use bytes::{Bytes, BytesMut}; +use futures::{try_ready, Async, Future, Poll, Sink, Stream}; +use h2::server::{Connection, SendResponse}; +use h2::{RecvStream, SendStream}; +use http::header::{ + HeaderValue, ACCEPT_ENCODING, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, +}; +use http::HttpTryFrom; +use log::{debug, error, trace}; +use tokio_timer::Delay; + +use crate::body::{Body, BodyLength, MessageBody, ResponseBody}; +use crate::config::ServiceConfig; +use crate::error::{DispatchError, Error, ParseError, PayloadError, ResponseError}; +use crate::message::ResponseHead; +use crate::request::Request; +use crate::response::Response; + +use super::{H2ServiceResult, Payload}; + +const CHUNK_SIZE: usize = 16_384; + +bitflags! { + struct Flags: u8 { + const DISCONNECTED = 0b0000_0001; + const SHUTDOWN = 0b0000_0010; + } +} + +/// Dispatcher for HTTP/2 protocol +pub struct Dispatcher { + flags: Flags, + service: S, + connection: Connection, + config: ServiceConfig, + ka_expire: Instant, + ka_timer: Option, + _t: PhantomData, +} + +impl Dispatcher +where + T: AsyncRead + AsyncWrite, + S: Service, Response = Response> + 'static, + S::Error: Into + fmt::Debug, + B: MessageBody + 'static, +{ + pub fn new( + service: S, + connection: Connection, + config: ServiceConfig, + timeout: Option, + ) -> Self { + let keepalive = config.keep_alive_enabled(); + // let flags = if keepalive { + // Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED + // } else { + // Flags::empty() + // }; + + // keep-alive timer + let (ka_expire, ka_timer) = if let Some(delay) = timeout { + (delay.deadline(), Some(delay)) + } else if let Some(delay) = config.keep_alive_timer() { + (delay.deadline(), Some(delay)) + } else { + (config.now(), None) + }; + + Dispatcher { + service, + config, + ka_expire, + ka_timer, + connection, + flags: Flags::empty(), + _t: PhantomData, + } + } +} + +impl Future for Dispatcher +where + T: AsyncRead + AsyncWrite, + S: Service, Response = Response> + 'static, + S::Error: Into + fmt::Debug, + B: MessageBody + 'static, +{ + type Item = (); + type Error = DispatchError<()>; + + #[inline] + fn poll(&mut self) -> Poll { + loop { + match self.connection.poll()? { + Async::Ready(None) => { + self.flags.insert(Flags::DISCONNECTED); + } + Async::Ready(Some((req, res))) => { + // update keep-alive expire + if self.ka_timer.is_some() { + if let Some(expire) = self.config.keep_alive_expire() { + self.ka_expire = expire; + } + } + + let (parts, body) = req.into_parts(); + let mut req = Request::with_payload(Payload::new(body)); + + let head = &mut req.inner_mut().head; + head.uri = parts.uri; + head.method = parts.method; + head.version = parts.version; + head.headers = parts.headers; + tokio_current_thread::spawn(ServiceResponse:: { + state: ServiceResponseState::ServiceCall( + self.service.call(req), + Some(res), + ), + config: self.config.clone(), + buffer: None, + }) + } + Async::NotReady => return Ok(Async::NotReady), + } + } + } +} + +struct ServiceResponse { + state: ServiceResponseState, + config: ServiceConfig, + buffer: Option, +} + +enum ServiceResponseState { + ServiceCall(S::Future, Option>), + SendPayload(SendStream, ResponseBody), +} + +impl ServiceResponse +where + S: Service, Response = Response> + 'static, + S::Error: Into + fmt::Debug, + B: MessageBody + 'static, +{ + fn prepare_response( + &self, + head: &ResponseHead, + length: &mut BodyLength, + ) -> http::Response<()> { + let mut has_date = false; + let mut skip_len = length != &BodyLength::Stream; + + let mut res = http::Response::new(()); + *res.status_mut() = head.status; + *res.version_mut() = http::Version::HTTP_2; + + // Content length + match head.status { + http::StatusCode::NO_CONTENT + | http::StatusCode::CONTINUE + | http::StatusCode::PROCESSING => *length = BodyLength::None, + http::StatusCode::SWITCHING_PROTOCOLS => { + skip_len = true; + *length = BodyLength::Stream; + } + _ => (), + } + let _ = match length { + BodyLength::Chunked | BodyLength::None | BodyLength::Stream => None, + BodyLength::Empty => res + .headers_mut() + .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), + BodyLength::Sized(len) => res.headers_mut().insert( + CONTENT_LENGTH, + HeaderValue::try_from(format!("{}", len)).unwrap(), + ), + BodyLength::Sized64(len) => res.headers_mut().insert( + CONTENT_LENGTH, + HeaderValue::try_from(format!("{}", len)).unwrap(), + ), + }; + + // copy headers + for (key, value) in head.headers.iter() { + match *key { + CONNECTION | TRANSFER_ENCODING => continue, // http2 specific + CONTENT_LENGTH if skip_len => continue, + DATE => has_date = true, + _ => (), + } + res.headers_mut().append(key, value.clone()); + } + + // set date header + if !has_date { + let mut bytes = BytesMut::with_capacity(29); + self.config.set_date_header(&mut bytes); + res.headers_mut() + .insert(DATE, HeaderValue::try_from(bytes.freeze()).unwrap()); + } + + res + } +} + +impl Future for ServiceResponse +where + S: Service, Response = Response> + 'static, + S::Error: Into + fmt::Debug, + B: MessageBody + 'static, +{ + type Item = (); + type Error = (); + + fn poll(&mut self) -> Poll { + match self.state { + ServiceResponseState::ServiceCall(ref mut call, ref mut send) => { + match call.poll() { + Ok(Async::Ready(res)) => { + let (res, body) = res.replace_body(()); + + let mut send = send.take().unwrap(); + let mut length = body.length(); + let h2_res = self.prepare_response(res.head(), &mut length); + + let stream = send + .send_response(h2_res, length.is_eof()) + .map_err(|e| { + trace!("Error sending h2 response: {:?}", e); + })?; + + if length.is_eof() { + Ok(Async::Ready(())) + } else { + self.state = ServiceResponseState::SendPayload(stream, body); + self.poll() + } + } + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(e) => { + let res: Response = e.into().into(); + let (res, body) = res.replace_body(()); + + let mut send = send.take().unwrap(); + let mut length = body.length(); + let h2_res = self.prepare_response(res.head(), &mut length); + + let stream = send + .send_response(h2_res, length.is_eof()) + .map_err(|e| { + trace!("Error sending h2 response: {:?}", e); + })?; + + if length.is_eof() { + Ok(Async::Ready(())) + } else { + self.state = ServiceResponseState::SendPayload( + stream, + body.into_body(), + ); + self.poll() + } + } + } + } + ServiceResponseState::SendPayload(ref mut stream, ref mut body) => loop { + loop { + if let Some(ref mut buffer) = self.buffer { + match stream.poll_capacity().map_err(|e| warn!("{:?}", e))? { + Async::NotReady => return Ok(Async::NotReady), + Async::Ready(None) => return Ok(Async::Ready(())), + Async::Ready(Some(cap)) => { + let len = buffer.len(); + let bytes = buffer.split_to(std::cmp::min(cap, len)); + + if let Err(e) = stream.send_data(bytes, false) { + warn!("{:?}", e); + return Err(()); + } else if !buffer.is_empty() { + let cap = std::cmp::min(buffer.len(), CHUNK_SIZE); + stream.reserve_capacity(cap); + } else { + self.buffer.take(); + } + } + } + } else { + match body.poll_next() { + Ok(Async::NotReady) => return Ok(Async::NotReady), + Ok(Async::Ready(None)) => { + if let Err(e) = stream.send_data(Bytes::new(), true) { + warn!("{:?}", e); + return Err(()); + } else { + return Ok(Async::Ready(())); + } + } + Ok(Async::Ready(Some(chunk))) => { + stream.reserve_capacity(std::cmp::min( + chunk.len(), + CHUNK_SIZE, + )); + self.buffer = Some(chunk); + } + Err(e) => { + error!("Response payload stream error: {:?}", e); + return Err(()); + } + } + } + } + }, + } + } +} diff --git a/src/h2/mod.rs b/src/h2/mod.rs index 4a54ec9fe..55e057607 100644 --- a/src/h2/mod.rs +++ b/src/h2/mod.rs @@ -1,7 +1,17 @@ +#![allow(dead_code, unused_imports)] + use std::fmt; +use bytes::Bytes; +use futures::{Async, Poll, Stream}; +use h2::RecvStream; + +mod dispatcher; mod service; +pub use self::service::H2Service; +use crate::error::PayloadError; + /// H1 service response type pub enum H2ServiceResult { Disconnected, @@ -18,3 +28,35 @@ impl fmt::Debug for H2ServiceResult { } } } + +/// H2 receive stream +pub struct Payload { + pl: RecvStream, +} + +impl Payload { + pub(crate) fn new(pl: RecvStream) -> Self { + Self { pl } + } +} + +impl Stream for Payload { + type Item = Bytes; + type Error = PayloadError; + + fn poll(&mut self) -> Poll, Self::Error> { + match self.pl.poll() { + Ok(Async::Ready(Some(chunk))) => { + let len = chunk.len(); + if let Err(err) = self.pl.release_capacity().release_capacity(len) { + Err(err.into()) + } else { + Ok(Async::Ready(Some(chunk))) + } + } + Ok(Async::Ready(None)) => Ok(Async::Ready(None)), + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(err) => Err(err.into()), + } + } +} diff --git a/src/h2/service.rs b/src/h2/service.rs index 827f84488..b598b0a6d 100644 --- a/src/h2/service.rs +++ b/src/h2/service.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; use std::marker::PhantomData; -use std::net; +use std::{io, net}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_service::{IntoNewService, NewService, Service}; @@ -8,16 +8,17 @@ use bytes::Bytes; use futures::future::{ok, FutureResult}; use futures::{try_ready, Async, Future, Poll, Stream}; use h2::server::{self, Connection, Handshake}; +use h2::RecvStream; use log::error; use crate::body::MessageBody; use crate::config::{KeepAlive, ServiceConfig}; -use crate::error::{DispatchError, ParseError}; +use crate::error::{DispatchError, Error, ParseError, ResponseError}; use crate::request::Request; use crate::response::Response; -// use super::dispatcher::Dispatcher; -use super::H2ServiceResult; +use super::dispatcher::Dispatcher; +use super::{H2ServiceResult, Payload}; /// `NewService` implementation for HTTP2 transport pub struct H2Service { @@ -28,10 +29,10 @@ pub struct H2Service { impl H2Service where - S: NewService> + Clone, - S::Service: Clone, - S::Error: Debug, - B: MessageBody, + S: NewService, Response = Response> + Clone, + S::Service: Clone + 'static, + S::Error: Into + Debug + 'static, + B: MessageBody + 'static, { /// Create new `HttpService` instance. pub fn new>(service: F) -> Self { @@ -53,14 +54,14 @@ where impl NewService for H2Service where T: AsyncRead + AsyncWrite, - S: NewService> + Clone, - S::Service: Clone, - S::Error: Debug, - B: MessageBody, + S: NewService, Response = Response> + Clone, + S::Service: Clone + 'static, + S::Error: Into + Debug, + B: MessageBody + 'static, { type Request = T; - type Response = H2ServiceResult; - type Error = (); //DispatchError; + type Response = (); + type Error = DispatchError<()>; type InitError = S::InitError; type Service = H2ServiceHandler; type Future = H2ServiceResponse; @@ -90,9 +91,9 @@ pub struct H2ServiceBuilder { impl H2ServiceBuilder where - S: NewService, - S::Service: Clone, - S::Error: Debug, + S: NewService>, + S::Service: Clone + 'static, + S::Error: Into + Debug + 'static, { /// Create instance of `H2ServiceBuilder` pub fn new() -> H2ServiceBuilder { @@ -185,6 +186,25 @@ where self } + // #[cfg(feature = "ssl")] + // /// Configure alpn protocols for SslAcceptorBuilder. + // pub fn configure_openssl( + // builder: &mut openssl::ssl::SslAcceptorBuilder, + // ) -> io::Result<()> { + // let protos: &[u8] = b"\x02h2"; + // builder.set_alpn_select_callback(|_, protos| { + // const H2: &[u8] = b"\x02h2"; + // if protos.windows(3).any(|window| window == H2) { + // Ok(b"h2") + // } else { + // Err(openssl::ssl::AlpnError::NOACK) + // } + // }); + // builder.set_alpn_protos(&protos)?; + + // Ok(()) + // } + /// Finish service configuration and create `H1Service` instance. pub fn finish(self, service: F) -> H2Service where @@ -214,10 +234,10 @@ pub struct H2ServiceResponse { impl Future for H2ServiceResponse where T: AsyncRead + AsyncWrite, - S: NewService>, - S::Service: Clone, - S::Error: Debug, - B: MessageBody, + S: NewService, Response = Response>, + S::Service: Clone + 'static, + S::Error: Into + Debug, + B: MessageBody + 'static, { type Item = H2ServiceHandler; type Error = S::InitError; @@ -240,9 +260,9 @@ pub struct H2ServiceHandler { impl H2ServiceHandler where - S: Service> + Clone, - S::Error: Debug, - B: MessageBody, + S: Service, Response = Response> + Clone + 'static, + S::Error: Into + Debug, + B: MessageBody + 'static, { fn new(cfg: ServiceConfig, srv: S) -> H2ServiceHandler { H2ServiceHandler { @@ -256,55 +276,79 @@ where impl Service for H2ServiceHandler where T: AsyncRead + AsyncWrite, - S: Service> + Clone, - S::Error: Debug, - B: MessageBody, + S: Service, Response = Response> + Clone + 'static, + S::Error: Into + Debug, + B: MessageBody + 'static, { type Request = T; - type Response = H2ServiceResult; - type Error = (); // DispatchError; + type Response = (); + type Error = DispatchError<()>; type Future = H2ServiceHandlerResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.srv.poll_ready().map_err(|_| ()) + self.srv.poll_ready().map_err(|e| { + error!("Service readiness error: {:?}", e); + DispatchError::Service(()) + }) } fn call(&mut self, req: T) -> Self::Future { H2ServiceHandlerResponse { - state: State::Handshake(server::handshake(req)), - _t: PhantomData, + state: State::Handshake( + Some(self.srv.clone()), + Some(self.cfg.clone()), + server::handshake(req), + ), } } } -enum State { - Handshake(Handshake), - Connection(Connection), - Empty, +enum State { + Incoming(Dispatcher), + Handshake(Option, Option, Handshake), } pub struct H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite, - S: Service> + Clone, - S::Error: Debug, - B: MessageBody, + S: Service, Response = Response> + Clone + 'static, + S::Error: Into + Debug, + B: MessageBody + 'static, { - state: State, - _t: PhantomData, + state: State, } impl Future for H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite, - S: Service> + Clone, - S::Error: Debug, + S: Service, Response = Response> + Clone, + S::Error: Into + Debug, B: MessageBody, { - type Item = H2ServiceResult; - type Error = (); + type Item = (); + type Error = DispatchError<()>; fn poll(&mut self) -> Poll { - unimplemented!() + match self.state { + State::Incoming(ref mut disp) => disp.poll(), + State::Handshake(ref mut srv, ref mut config, ref mut handshake) => { + match handshake.poll() { + Ok(Async::Ready(conn)) => { + self.state = State::Incoming(Dispatcher::new( + srv.take().unwrap(), + conn, + config.take().unwrap(), + None, + )); + self.poll() + } + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(err) => { + trace!("H2 handshake error: {}", err); + return Err(err.into()); + } + } + } + } } } diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 589617fc9..c50de2e94 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -25,7 +25,7 @@ pub trait HttpMessage: Sized { fn headers(&self) -> &HeaderMap; /// Message payload stream - fn payload(&self) -> Self::Stream; + fn payload(self) -> Self::Stream; #[doc(hidden)] /// Get a header @@ -128,7 +128,7 @@ pub trait HttpMessage: Sized { /// } /// # fn main() {} /// ``` - fn body(&self) -> MessageBody { + fn body(self) -> MessageBody { MessageBody::new(self) } @@ -162,7 +162,7 @@ pub trait HttpMessage: Sized { /// } /// # fn main() {} /// ``` - fn urlencoded(&self) -> UrlEncoded { + fn urlencoded(self) -> UrlEncoded { UrlEncoded::new(self) } @@ -198,12 +198,12 @@ pub trait HttpMessage: Sized { /// } /// # fn main() {} /// ``` - fn json(&self) -> JsonBody { + fn json(self) -> JsonBody { JsonBody::new(self) } /// Return stream of lines. - fn readlines(&self) -> Readlines { + fn readlines(self) -> Readlines { Readlines::new(self) } } @@ -220,7 +220,7 @@ pub struct Readlines { impl Readlines { /// Create a new stream to read request line by line. - fn new(req: &T) -> Self { + fn new(req: T) -> Self { let encoding = match req.encoding() { Ok(enc) => enc, Err(err) => return Self::err(req, err.into()), @@ -242,7 +242,7 @@ impl Readlines { self } - fn err(req: &T, err: ReadlinesError) -> Self { + fn err(req: T, err: ReadlinesError) -> Self { Readlines { stream: req.payload(), buff: BytesMut::new(), @@ -362,7 +362,7 @@ pub struct MessageBody { impl MessageBody { /// Create `MessageBody` for request. - pub fn new(req: &T) -> MessageBody { + pub fn new(req: T) -> MessageBody { let mut len = None; if let Some(l) = req.headers().get(header::CONTENT_LENGTH) { if let Ok(s) = l.to_str() { @@ -457,7 +457,7 @@ pub struct UrlEncoded { impl UrlEncoded { /// Create a new future to URL encode a request - pub fn new(req: &T) -> UrlEncoded { + pub fn new(req: T) -> UrlEncoded { // check content type if req.content_type().to_lowercase() != "application/x-www-form-urlencoded" { return Self::err(UrlencodedError::ContentType); @@ -800,7 +800,7 @@ mod tests { Contrary to popular belief, Lorem Ipsum is not simply random text.", )) .finish(); - let mut r = Readlines::new(&req); + let mut r = Readlines::new(req); match r.poll().ok().unwrap() { Async::Ready(Some(s)) => assert_eq!( s, diff --git a/src/json.rs b/src/json.rs index d06449cb0..fc1ab4d25 100644 --- a/src/json.rs +++ b/src/json.rs @@ -50,7 +50,7 @@ pub struct JsonBody { impl JsonBody { /// Create `JsonBody` for request. - pub fn new(req: &T) -> Self { + pub fn new(req: T) -> Self { // check content-type let json = if let Ok(Some(mime)) = req.mime_type() { mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) diff --git a/src/message.rs b/src/message.rs index 31d61f63d..a73392221 100644 --- a/src/message.rs +++ b/src/message.rs @@ -5,7 +5,6 @@ use std::rc::Rc; use http::{HeaderMap, Method, StatusCode, Uri, Version}; use crate::extensions::Extensions; -use crate::payload::Payload; /// Represents various types of connection #[derive(Copy, Clone, PartialEq, Debug)] @@ -149,7 +148,6 @@ impl ResponseHead { pub struct Message { pub head: T, pub extensions: RefCell, - pub payload: RefCell>, pub(crate) pool: &'static MessagePool, } @@ -159,7 +157,6 @@ impl Message { pub fn reset(&mut self) { self.head.clear(); self.extensions.borrow_mut().clear(); - *self.payload.borrow_mut() = None; } } @@ -168,7 +165,6 @@ impl Default for Message { Message { pool: T::pool(), head: T::default(), - payload: RefCell::new(None), extensions: RefCell::new(Extensions::new()), } } diff --git a/src/request.rs b/src/request.rs index 60ddee19b..b60a772e1 100644 --- a/src/request.rs +++ b/src/request.rs @@ -2,43 +2,76 @@ use std::cell::{Ref, RefMut}; use std::fmt; use std::rc::Rc; +use bytes::Bytes; +use futures::Stream; use http::{header, HeaderMap, Method, Uri, Version}; +use crate::error::PayloadError; use crate::extensions::Extensions; use crate::httpmessage::HttpMessage; use crate::message::{Message, MessagePool, RequestHead}; use crate::payload::Payload; /// Request -pub struct Request { +pub struct Request

    { + pub(crate) payload: Option

    , pub(crate) inner: Rc>, } -impl HttpMessage for Request { - type Stream = Payload; +impl

    HttpMessage for Request

    +where + P: Stream, +{ + type Stream = P; fn headers(&self) -> &HeaderMap { &self.inner.head.headers } #[inline] - fn payload(&self) -> Payload { - if let Some(payload) = self.inner.payload.borrow_mut().take() { - payload - } else { - Payload::empty() + fn payload(mut self) -> P { + self.payload.take().unwrap() + } +} + +impl Request { + /// Create new Request instance + pub fn new() -> Request { + Request { + payload: Some(Payload::empty()), + inner: MessagePool::get_message(), } } } -impl Request { +impl Request { /// Create new Request instance - pub fn new() -> Request { + pub fn with_payload(payload: Payload) -> Request { Request { + payload: Some(payload), inner: MessagePool::get_message(), } } + /// Create new Request instance + pub fn set_payload

    (self, payload: P) -> Request

    { + Request { + payload: Some(payload), + inner: self.inner.clone(), + } + } + + /// Take request's payload + pub fn take_payload(mut self) -> (Payload, Request<()>) { + ( + self.payload.take().unwrap(), + Request { + payload: Some(()), + inner: self.inner.clone(), + }, + ) + } + // /// Create new Request instance with pool // pub(crate) fn with_pool(pool: &'static MessagePool) -> Request { // Request { @@ -143,17 +176,17 @@ impl Request { self.inner().head.method == Method::CONNECT } - #[doc(hidden)] - /// Note: this method should be called only as part of clone operation - /// of wrapper type. - pub fn clone_request(&self) -> Self { - Request { - inner: self.inner.clone(), - } - } + // #[doc(hidden)] + // /// Note: this method should be called only as part of clone operation + // /// of wrapper type. + // pub fn clone_request(&self) -> Self { + // Request { + // inner: self.inner.clone(), + // } + // } } -impl Drop for Request { +impl Drop for Request { fn drop(&mut self) { if Rc::strong_count(&self.inner) == 1 { self.inner.pool.release(self.inner.clone()); @@ -161,7 +194,7 @@ impl Drop for Request { } } -impl fmt::Debug for Request { +impl fmt::Debug for Request { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!( f, diff --git a/src/test.rs b/src/test.rs index a26f31e59..852dd3c0b 100644 --- a/src/test.rs +++ b/src/test.rs @@ -148,15 +148,18 @@ impl TestRequest { .. } = self; - let mut req = Request::new(); - { - let inner = req.inner_mut(); - inner.head.uri = uri; - inner.head.method = method; - inner.head.version = version; - inner.head.headers = headers; - *inner.payload.borrow_mut() = payload; - } + let mut req = if let Some(pl) = payload { + Request::with_payload(pl) + } else { + Request::with_payload(Payload::empty()) + }; + + let inner = req.inner_mut(); + inner.head.uri = uri; + inner.head.method = method; + inner.head.version = version; + inner.head.headers = headers; + // req.set_cookies(cookies); req } diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 81a3d909c..9c71a25c0 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -28,6 +28,9 @@ default = ["session"] # sessions feature, session require "ring" crate and c compiler session = ["cookie/secure"] +# openssl +ssl = ["openssl", "actix-http/ssl", "actix-server/ssl"] + [dependencies] actix-codec = "0.1" actix-service = "0.2.0" @@ -52,3 +55,5 @@ serde_urlencoded = "0.5.3" time = "0.1" tokio-tcp = "0.1" tokio-timer = "0.2" + +openssl = { version="0.10", optional = true } diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 8083ebb15..3d6d917e5 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -3,6 +3,12 @@ use std::sync::mpsc; use std::{net, thread}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_http::body::MessageBody; +use actix_http::client::{ + ClientRequest, ClientRequestBuilder, ClientResponse, Connect, Connection, Connector, + ConnectorError, SendRequestError, +}; +use actix_http::ws; use actix_rt::{Runtime, System}; use actix_server::{Server, StreamServiceFactory}; use actix_service::Service; @@ -11,13 +17,6 @@ use futures::future::{lazy, Future}; use http::Method; use net2::TcpBuilder; -use actix_http::body::MessageBody; -use actix_http::client::{ - ClientRequest, ClientRequestBuilder, ClientResponse, Connect, Connection, Connector, - ConnectorError, SendRequestError, -}; -use actix_http::ws; - /// The `TestServer` type. /// /// `TestServer` is very simple test server that simplify process of writing @@ -101,6 +100,9 @@ impl TestServer { let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); builder.set_verify(SslVerifyMode::NONE); + let _ = builder + .set_alpn_protos(b"\x02h2\x08http/1.1") + .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); Connector::default().ssl(builder.build()).service() } #[cfg(not(feature = "ssl"))] @@ -151,6 +153,15 @@ impl TestServerRuntime { } } + /// Construct test https server url + pub fn surl(&self, uri: &str) -> String { + if uri.starts_with('/') { + format!("https://127.0.0.1:{}{}", self.addr.port(), uri) + } else { + format!("https://127.0.0.1:{}/{}", self.addr.port(), uri) + } + } + /// Create `GET` request pub fn get(&self) -> ClientRequestBuilder { ClientRequest::get(self.url("/").as_str()) diff --git a/tests/cert.pem b/tests/cert.pem index db04fbfae..5e195d98d 100644 --- a/tests/cert.pem +++ b/tests/cert.pem @@ -1,31 +1,16 @@ -----BEGIN CERTIFICATE----- -MIIFXTCCA0WgAwIBAgIJAJ3tqfd0MLLNMA0GCSqGSIb3DQEBCwUAMGExCzAJBgNV -BAYTAlVTMQswCQYDVQQIDAJDRjELMAkGA1UEBwwCU0YxEDAOBgNVBAoMB0NvbXBh -bnkxDDAKBgNVBAsMA09yZzEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tMB4XDTE4 -MDcyOTE4MDgzNFoXDTE5MDcyOTE4MDgzNFowYTELMAkGA1UEBhMCVVMxCzAJBgNV -BAgMAkNGMQswCQYDVQQHDAJTRjEQMA4GA1UECgwHQ29tcGFueTEMMAoGA1UECwwD -T3JnMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUA -A4ICDwAwggIKAoICAQDZbMgDYilVH1Nv0QWEhOXG6ETmtjZrdLqrNg3NBWBIWCDF -cQ+fyTWxARx6vkF8A/3zpJyTcfQW8HgG38jw/A61QKaHBxzwq0HlNwY9Hh+Neeuk -L4wgrlQ0uTC7IEMrOJjNN0GPyRQVfVbGa8QcSCpOg85l8GCxLvVwkBH/M5atoMtJ -EzniNfK+gtk3hOL2tBqBCu9NDjhXPnJwNDLtTG1tQaHUJW/r281Wvv9I46H83DkU -05lYtauh0bKh5znCH2KpFmBGqJNRzou3tXZFZzZfaCPBJPZR8j5TjoinehpDtkPh -4CSio0PF2eIFkDKRUbdz/327HgEARJMXx+w1yHpS2JwHFgy5O76i68/Smx8j3DDA -2WIkOYAJFRMH0CBHKdsvUDOGpCgN+xv3whl+N806nCfC4vCkwA+FuB3ko11logng -dvr+y0jIUSU4THF3dMDEXYayF3+WrUlw0cBnUNJdXky85ZP81aBfBsjNSBDx4iL4 -e4NhfZRS5oHpHy1t3nYfuttS/oet+Ke5KUpaqNJguSIoeTBSmgzDzL1TJxFLOzUT -2c/A9M69FdvSY0JB4EJX0W9K01Vd0JRNPwsY+/zvFIPama3suKOUTqYcsbwxx9xa -TMDr26cIQcgUAUOKZO43sQGWNzXX3FYVNwczKhkB8UX6hOrBJsEYiau4LGdokQID -AQABoxgwFjAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggIB -AIX+Qb4QRBxHl5X2UjRyLfWVkimtGlwI8P+eJZL3DrHBH/TpqAaCvTf0EbRC32nm -ASDMwIghaMvyrW40QN6V/CWRRi25cXUfsIZr1iHAHK0eZJV8SWooYtt4iNrcUs3g -4OTvDxhNmDyNwV9AXhJsBKf80dCW6/84jItqVAj20/OO4Rkd2tEeI8NomiYBc6a1 -hgwvv02myYF5hG/xZ9YSqeroBCZHwGYoJJnSpMPqJsxbCVnx2/U9FzGwcRmNHFCe -0g7EJZd3//8Plza6nkTBjJ/V7JnLqMU+ltx4mAgZO8rfzIr84qZdt0YN33VJQhYq -seuMySxrsuaAoxAmm8IoK9cW4IPzx1JveBQiroNlq5YJGf2UW7BTc3gz6c2tINZi -7ailBVdhlMnDXAf3/9xiiVlRAHOxgZh/7sRrKU7kDEHM4fGoc0YyZBTQKndPYMwO -3Bd82rlQ4sd46XYutTrB+mBYClVrJs+OzbNedTsR61DVNKKsRG4mNPyKSAIgOfM5 -XmSvCMPN5JK9U0DsNIV2/SnVsmcklQczT35FLTxl9ntx8ys7ZYK+SppD7XuLfWMq -GT9YMWhlpw0aRDg/aayeeOcnsNBhzAFMcOpQj1t6Fgv4+zbS9BM2bT0hbX86xjkr -E6wWgkuCslMgQlEJ+TM5RhYrI5/rVZQhvmgcob/9gPZv +MIICljCCAX4CCQDFdWu66640QjANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJ1 +czAeFw0xOTAyMDQyMzEyNTBaFw0yMDAyMDQyMzEyNTBaMA0xCzAJBgNVBAYTAnVz +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzZUXMnS5X8HWxTvHAc82 +Q2d32fiPQGtD+fp3OV90l6RC9jgMdH4yTVUgX5mYYcW0k89RaP8g61H6b76F9gcd +yZ1idqKI1AU9aeBUPV8wkrouhR/6Omv8fA7yr9tVmNo53jPN7WyKoBoU0r7Yj9Ez +g3qjv/808Jlgby3EhduruyyfdvSt5ZFXnOz2D3SF9DS4yrM2jSw4ZTuoVMfZ8vZe +FVzLo/+sV8qokU6wBTEOAmZQ7e/zZV4qAoH2Z3Vj/uD1Zr/MXYyh81RdXpDqIXwV +Z29LEOa2eTGFEdvfG+tdvvuIvSdF3+WbLrwn2ECfwJ8zmKyTauPRV4pj7ks+wkBI +EQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB6dmuWBOpFfDdu0mdsDb8XnJY1svjH +4kbztXhjQJ/WuhCUIwvXFyz9dqQCq+TbJUbUEzZJEfaq1uaI3iB5wd35ArSoAGJA +k0lonzyeSM+cmNOe/5BPqWhd1qPwbsfgMoCCkZUoTT5Rvw6yt00XIqZzMqrsvRBX +hAcUW3zBtFQNP6aQqsMdn4ClZE0WHf+LzWy2NQh+Sf46tSYBHELfdUawgR789PB4 +/gNjAeklq06JmE/3gELijwaijVIuUsMC9ua//ITk4YIFpqanPtka+7BpfTegPGNs +HCj1g7Jot97oQMuvDOJeso91aiSA+gutepCClZICT8LxNRkY3ZlXYp92 -----END CERTIFICATE----- diff --git a/tests/key.pem b/tests/key.pem index aac387c64..50ded0ce0 100644 --- a/tests/key.pem +++ b/tests/key.pem @@ -1,51 +1,28 @@ ------BEGIN RSA PRIVATE KEY----- -MIIJKAIBAAKCAgEA2WzIA2IpVR9Tb9EFhITlxuhE5rY2a3S6qzYNzQVgSFggxXEP -n8k1sQEcer5BfAP986Sck3H0FvB4Bt/I8PwOtUCmhwcc8KtB5TcGPR4fjXnrpC+M -IK5UNLkwuyBDKziYzTdBj8kUFX1WxmvEHEgqToPOZfBgsS71cJAR/zOWraDLSRM5 -4jXyvoLZN4Ti9rQagQrvTQ44Vz5ycDQy7UxtbUGh1CVv69vNVr7/SOOh/Nw5FNOZ -WLWrodGyoec5wh9iqRZgRqiTUc6Lt7V2RWc2X2gjwST2UfI+U46Ip3oaQ7ZD4eAk -oqNDxdniBZAykVG3c/99ux4BAESTF8fsNch6UticBxYMuTu+ouvP0psfI9wwwNli -JDmACRUTB9AgRynbL1AzhqQoDfsb98IZfjfNOpwnwuLwpMAPhbgd5KNdZaIJ4Hb6 -/stIyFElOExxd3TAxF2Gshd/lq1JcNHAZ1DSXV5MvOWT/NWgXwbIzUgQ8eIi+HuD -YX2UUuaB6R8tbd52H7rbUv6HrfinuSlKWqjSYLkiKHkwUpoMw8y9UycRSzs1E9nP -wPTOvRXb0mNCQeBCV9FvStNVXdCUTT8LGPv87xSD2pmt7LijlE6mHLG8McfcWkzA -69unCEHIFAFDimTuN7EBljc119xWFTcHMyoZAfFF+oTqwSbBGImruCxnaJECAwEA -AQKCAgAME3aoeXNCPxMrSri7u4Xnnk71YXl0Tm9vwvjRQlMusXZggP8VKN/KjP0/ -9AE/GhmoxqPLrLCZ9ZE1EIjgmZ9Xgde9+C8rTtfCG2RFUL7/5J2p6NonlocmxoJm -YkxYwjP6ce86RTjQWL3RF3s09u0inz9/efJk5O7M6bOWMQ9VZXDlBiRY5BYvbqUR -6FeSzD4MnMbdyMRoVBeXE88gTvZk8xhB6DJnLzYgc0tKiRoeKT0iYv5JZw25VyRM -ycLzfTrFmXCPfB1ylb483d9Ly4fBlM8nkx37PzEnAuukIawDxsPOb9yZC+hfvNJI -7NFiMN+3maEqG2iC00w4Lep4skHY7eHUEUMl+Wjr+koAy2YGLWAwHZQTm7iXn9Ab -L6adL53zyCKelRuEQOzbeosJAqS+5fpMK0ekXyoFIuskj7bWuIoCX7K/kg6q5IW+ -vC2FrlsrbQ79GztWLVmHFO1I4J9M5r666YS0qdh8c+2yyRl4FmSiHfGxb3eOKpxQ -b6uI97iZlkxPF9LYUCSc7wq0V2gGz+6LnGvTHlHrOfVXqw/5pLAKhXqxvnroDTwz -0Ay/xFF6ei/NSxBY5t8ztGCBm45wCU3l8pW0X6dXqwUipw5b4MRy1VFRu6rqlmbL -OPSCuLxqyqsigiEYsBgS/icvXz9DWmCQMPd2XM9YhsHvUq+R4QKCAQEA98EuMMXI -6UKIt1kK2t/3OeJRyDd4iv/fCMUAnuPjLBvFE4cXD/SbqCxcQYqb+pue3PYkiTIC -71rN8OQAc5yKhzmmnCE5N26br/0pG4pwEjIr6mt8kZHmemOCNEzvhhT83nfKmV0g -9lNtuGEQMiwmZrpUOF51JOMC39bzcVjYX2Cmvb7cFbIq3lR0zwM+aZpQ4P8LHCIu -bgHmwbdlkLyIULJcQmHIbo6nPFB3ZZE4mqmjwY+rA6Fh9rgBa8OFCfTtrgeYXrNb -IgZQ5U8GoYRPNC2ot0vpTinraboa/cgm6oG4M7FW1POCJTl+/ktHEnKuO5oroSga -/BSg7hCNFVaOhwKCAQEA4Kkys0HtwEbV5mY/NnvUD5KwfXX7BxoXc9lZ6seVoLEc -KjgPYxqYRVrC7dB2YDwwp3qcRTi/uBAgFNm3iYlDzI4xS5SeaudUWjglj7BSgXE2 -iOEa7EwcvVPluLaTgiWjlzUKeUCNNHWSeQOt+paBOT+IgwRVemGVpAgkqQzNh/nP -tl3p9aNtgzEm1qVlPclY/XUCtf3bcOR+z1f1b4jBdn0leu5OhnxkC+Htik+2fTXD -jt6JGrMkanN25YzsjnD3Sn+v6SO26H99wnYx5oMSdmb8SlWRrKtfJHnihphjG/YY -l1cyorV6M/asSgXNQfGJm4OuJi0I4/FL2wLUHnU+JwKCAQEAzh4WipcRthYXXcoj -gMKRkMOb3GFh1OpYqJgVExtudNTJmZxq8GhFU51MR27Eo7LycMwKy2UjEfTOnplh -Us2qZiPtW7k8O8S2m6yXlYUQBeNdq9IuuYDTaYD94vsazscJNSAeGodjE+uGvb1q -1wLqE87yoE7dUInYa1cOA3+xy2/CaNuviBFJHtzOrSb6tqqenQEyQf6h9/12+DTW -t5pSIiixHrzxHiFqOoCLRKGToQB+71rSINwTf0nITNpGBWmSj5VcC3VV3TG5/XxI -fPlxV2yhD5WFDPVNGBGvwPDSh4jSMZdZMSNBZCy4XWFNSKjGEWoK4DFYed3DoSt9 -5IG1YwKCAQA63ntHl64KJUWlkwNbboU583FF3uWBjee5VqoGKHhf3CkKMxhtGqnt -+oN7t5VdUEhbinhqdx1dyPPvIsHCS3K1pkjqii4cyzNCVNYa2dQ00Qq+QWZBpwwc -3GAkz8rFXsGIPMDa1vxpU6mnBjzPniKMcsZ9tmQDppCEpBGfLpio2eAA5IkK8eEf -cIDB3CM0Vo94EvI76CJZabaE9IJ+0HIJb2+jz9BJ00yQBIqvJIYoNy9gP5Xjpi+T -qV/tdMkD5jwWjHD3AYHLWKUGkNwwkAYFeqT/gX6jpWBP+ZRPOp011X3KInJFSpKU -DT5GQ1Dux7EMTCwVGtXqjO8Ym5wjwwsfAoIBAEcxlhIW1G6BiNfnWbNPWBdh3v/K -5Ln98Rcrz8UIbWyl7qNPjYb13C1KmifVG1Rym9vWMO3KuG5atK3Mz2yLVRtmWAVc -fxzR57zz9MZFDun66xo+Z1wN3fVxQB4CYpOEI4Lb9ioX4v85hm3D6RpFukNtRQEc -Gfr4scTjJX4jFWDp0h6ffMb8mY+quvZoJ0TJqV9L9Yj6Ksdvqez/bdSraev97bHQ -4gbQxaTZ6WjaD4HjpPQefMdWp97Metg0ZQSS8b8EzmNFgyJ3XcjirzwliKTAQtn6 -I2sd0NCIooelrKRD8EJoDUwxoOctY7R97wpZ7/wEHU45cBCbRV3H4JILS5c= ------END RSA PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDNlRcydLlfwdbF +O8cBzzZDZ3fZ+I9Aa0P5+nc5X3SXpEL2OAx0fjJNVSBfmZhhxbSTz1Fo/yDrUfpv +voX2Bx3JnWJ2oojUBT1p4FQ9XzCSui6FH/o6a/x8DvKv21WY2jneM83tbIqgGhTS +vtiP0TODeqO//zTwmWBvLcSF26u7LJ929K3lkVec7PYPdIX0NLjKszaNLDhlO6hU +x9ny9l4VXMuj/6xXyqiRTrAFMQ4CZlDt7/NlXioCgfZndWP+4PVmv8xdjKHzVF1e +kOohfBVnb0sQ5rZ5MYUR298b612++4i9J0Xf5ZsuvCfYQJ/AnzOYrJNq49FXimPu +Sz7CQEgRAgMBAAECggEBALC547EaKmko5wmyM4dYq9sRzTPxuqO0EkGIkIkfh8j8 +ChxDXmGeQnu8HBJSpW4XWP5fkCpkd9YTKOh6rgorX+37f7NgUaOBxaOIlqITfFwF +9Qu3y5IBVpEHAJUwRcsaffiILBRX5GtxQElSijRHsLLr8GySZN4X25B3laNEjcJe +NWJrDaxOn0m0MMGRvBpM8PaZu1Mn9NWxt04b/fteVLdN4TAcuY9TgvVZBq92S2FM +qvZcnJCQckNOuMOptVdP45qPkerKUohpOcqBfIiWFaalC378jE3Dm68p7slt3R6y +I1wVqCI4+MZfM3CtKcYJV0fdqklJCvXORvRiT8OZKakCgYEA5YnhgXOu4CO4DR1T +Lacv716DPyHeKVa6TbHhUhWw4bLwNLUsEL98jeU9SZ6VH8enBlDm5pCsp2i//t9n +8hoykN4L0rS4EyAGENouTRkLhtHfjTAKTKDK8cNvEaS8NOBJWrI0DTiHtFbCRBvI +zRx5VhrB5H4DDbqn7QV9g+GBKvMCgYEA5Ug3bN0RNUi3KDoIRcnWc06HsX307su7 +tB4cGqXJqVOJCrkk5sefGF502+W8m3Ldjaakr+Q9BoOdZX6boZnFtVetT8Hyzk1C +Rkiyz3GcwovOkQK//UmljsuRjgHF+PuQGX5ol4YlJtXU21k5bCsi1Tmyp7IufiGV +AQRMVZVbeesCgYA/QBZGwKTgmJcf7gO8ocRAto999wwr4f0maazIHLgICXHNZFsH +JmzhANk5jxxSjIaG5AYsZJNe8ittxQv0l6l1Z+pkHm5Wvs1NGYIGtq8JcI2kbyd3 +ZBtoMU1K1FUUUPWFq3NSbVBfrkSL1ggoFP+ObYMePmcDAntBgfDLRXl9ZwKBgQCt +/dh5l2UIn27Gawt+EkXX6L8WVTQ6xoZhj/vZyPe4tDip14gGTXQQ5RUfDj7LZCZ2 +6P/OrpAU0mnt7F8kCfI7xBY0EUU1gvGJLn/q5heElt2hs4mIJ4woSZjiP7xBTn2y +qveqDNVCnEBUWGg4Cp/7WTaXBaM8ejV9uQpIY/gwEwKBgQCCYnd9fD8L4nGyOLoD +eUzMV7G8TZfinlxCNMVXfpn4Z8OaYHOk5NiujHK55w4ghx06NQw038qhnX0ogbjU +caWOwCIbrYgx2fwYuOZbJFXdjWlpjIK3RFOcbNCNgCRLT6Lgz4uZYZ9RVftADvMi +zR1QsLWnIvARbTtOPfZqizT2gQ== +-----END PRIVATE KEY----- diff --git a/tests/test_server.rs b/tests/test_server.rs index c23840ead..9fa27e71b 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -5,16 +5,16 @@ use std::{net, thread}; use actix_http_test::TestServer; use actix_service::NewService; use bytes::Bytes; -use futures::future::{self, ok}; +use futures::future::{self, ok, Future}; use futures::stream::once; use actix_http::{ - body, client, h1, http, Body, Error, HttpMessage as HttpMessage2, KeepAlive, + body, client, h1, h2, http, Body, Error, HttpMessage as HttpMessage2, KeepAlive, Request, Response, }; #[test] -fn test_h1_v2() { +fn test_h1() { let mut srv = TestServer::with_factory(|| { h1::H1Service::build() .keep_alive(KeepAlive::Disabled) @@ -30,6 +30,85 @@ fn test_h1_v2() { assert!(response.status().is_success()); } +#[cfg(feature = "ssl")] +fn ssl_acceptor() -> std::io::Result> { + use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; + // load ssl keys + let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); + builder + .set_private_key_file("tests/key.pem", SslFiletype::PEM) + .unwrap(); + builder + .set_certificate_chain_file("tests/cert.pem") + .unwrap(); + builder.set_alpn_select_callback(|_, protos| { + const H2: &[u8] = b"\x02h2"; + if protos.windows(3).any(|window| window == H2) { + Ok(b"h2") + } else { + Err(openssl::ssl::AlpnError::NOACK) + } + }); + builder.set_alpn_protos(b"\x02h2")?; + Ok(actix_server::ssl::OpensslAcceptor::new(builder.build())) +} + +#[cfg(feature = "ssl")] +#[test] +fn test_h2() -> std::io::Result<()> { + let openssl = ssl_acceptor()?; + let mut srv = TestServer::with_factory(move || { + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)) + .and_then( + h2::H2Service::build() + .finish(|_| future::ok::<_, Error>(Response::Ok().finish())) + .map_err(|_| ()), + ) + }); + + let req = client::ClientRequest::get(srv.surl("/")).finish().unwrap(); + let response = srv.send_request(req).unwrap(); + println!("RES: {:?}", response); + assert!(response.status().is_success()); + Ok(()) +} + +#[cfg(feature = "ssl")] +#[test] +fn test_h2_body() -> std::io::Result<()> { + // std::env::set_var("RUST_LOG", "actix_http=trace"); + // env_logger::init(); + + let data = "HELLOWORLD".to_owned().repeat(64 * 1024); + let openssl = ssl_acceptor()?; + let mut srv = TestServer::with_factory(move || { + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)) + .and_then( + h2::H2Service::build() + .finish(|req: Request<_>| { + req.body() + .limit(1024 * 1024) + .and_then(|body| Ok(Response::Ok().body(body))) + }) + .map_err(|_| ()), + ) + }); + + let req = client::ClientRequest::get(srv.surl("/")) + .body(data.clone()) + .unwrap(); + let response = srv.send_request(req).unwrap(); + assert!(response.status().is_success()); + + let body = srv.block_on(response.body().limit(1024 * 1024)).unwrap(); + assert_eq!(&body, data.as_bytes()); + Ok(()) +} + #[test] fn test_slow_request() { let srv = TestServer::with_factory(|| { From fcace161c7bd2fdf81b59b0b8ccefd16219105af Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Feb 2019 12:22:40 -0800 Subject: [PATCH 120/427] fix manifest features --- Cargo.toml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bbb31c161..8cc74446e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ default = ["session"] session = ["cookie/secure"] # openssl -ssl = ["openssl", "actix-connector/ssl", "actix-server/ssl", "actix-http-test/ssl"] +ssl = ["openssl"] [dependencies] actix-service = "0.2.1" @@ -76,7 +76,10 @@ openssl = { version="0.10", optional = true } [dev-dependencies] actix-rt = "0.1.0" actix-web = "0.7" -actix-server = "0.2" -actix-http-test = { path="test-server" } +actix-server = { version="0.2", features=["ssl"] } +actix-connector = { version="0.2.0", features=["ssl"] } +actix-utils = "0.2.1" +actix-http-test = { path="test-server", features=["ssl"] } env_logger = "0.6" serde_derive = "1.0" +openssl = { version="0.10" } From cd83553db7a04f60d54f09ddc0e56c7e89a5fcc0 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Feb 2019 11:06:05 -0800 Subject: [PATCH 121/427] simplify payload api; add missing http error helper functions --- Cargo.toml | 1 - examples/echo.rs | 4 +- examples/echo2.rs | 4 +- src/client/h1proto.rs | 21 +-- src/client/h2proto.rs | 3 +- src/client/request.rs | 19 +-- src/client/response.rs | 22 ++- src/error.rs | 314 +++++++++++++++++++++++++++++++++++++++- src/h1/dispatcher.rs | 2 +- src/h1/mod.rs | 2 + src/{ => h1}/payload.rs | 0 src/httpcodes.rs | 1 + src/httpmessage.rs | 137 ++++++++++-------- src/json.rs | 12 +- src/lib.rs | 2 - src/request.rs | 11 +- src/service/mod.rs | 2 + src/test.rs | 2 +- tests/test_client.rs | 4 +- tests/test_server.rs | 20 +-- tests/test_ws.rs | 34 ----- 21 files changed, 442 insertions(+), 175 deletions(-) rename src/{ => h1}/payload.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 8cc74446e..23938f2fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,6 @@ openssl = { version="0.10", optional = true } [dev-dependencies] actix-rt = "0.1.0" -actix-web = "0.7" actix-server = { version="0.2", features=["ssl"] } actix-connector = { version="0.2.0", features=["ssl"] } actix-utils = "0.2.1" diff --git a/examples/echo.rs b/examples/echo.rs index 3bfb04d7c..03d5b4706 100644 --- a/examples/echo.rs +++ b/examples/echo.rs @@ -18,8 +18,8 @@ fn main() { .client_timeout(1000) .client_disconnect(1000) .server_hostname("localhost") - .finish(|_req: Request| { - _req.body().limit(512).and_then(|bytes: Bytes| { + .finish(|mut req: Request| { + req.body().limit(512).and_then(|bytes: Bytes| { info!("request body: {:?}", bytes); let mut res = Response::Ok(); res.header("x-head", HeaderValue::from_static("dummy value!")); diff --git a/examples/echo2.rs b/examples/echo2.rs index 0e2bc9d52..2fd9cbcff 100644 --- a/examples/echo2.rs +++ b/examples/echo2.rs @@ -8,8 +8,8 @@ use futures::Future; use log::info; use std::env; -fn handle_request(_req: Request) -> impl Future { - _req.body().limit(512).from_err().and_then(|bytes: Bytes| { +fn handle_request(mut req: Request) -> impl Future { + req.body().limit(512).from_err().and_then(|bytes: Bytes| { info!("request body: {:?}", bytes); let mut res = Response::Ok(); res.header("x-head", HeaderValue::from_static("dummy value!")); diff --git a/src/client/h1proto.rs b/src/client/h1proto.rs index 86fc10b84..59a03ef48 100644 --- a/src/client/h1proto.rs +++ b/src/client/h1proto.rs @@ -50,14 +50,14 @@ where .into_future() .map_err(|(e, _)| SendRequestError::from(e)) .and_then(|(item, framed)| { - if let Some(res) = item { + if let Some(mut res) = item { match framed.get_codec().message_type() { h1::MessageType::None => { let force_close = !framed.get_codec().keepalive(); release_connection(framed, force_close) } _ => { - *res.payload.borrow_mut() = Some(Payload::stream(framed)) + res.set_payload(Payload::stream(framed)); } } ok(res) @@ -199,27 +199,10 @@ where } } -struct EmptyPayload; - -impl Stream for EmptyPayload { - type Item = Bytes; - type Error = PayloadError; - - fn poll(&mut self) -> Poll, Self::Error> { - Ok(Async::Ready(None)) - } -} - pub(crate) struct Payload { framed: Option>, } -impl Payload<()> { - pub fn empty() -> PayloadStream { - Box::new(EmptyPayload) - } -} - impl Payload { pub fn stream(framed: Framed) -> PayloadStream { Box::new(Payload { diff --git a/src/client/h2proto.rs b/src/client/h2proto.rs index ecd18cf82..f2f18d935 100644 --- a/src/client/h2proto.rs +++ b/src/client/h2proto.rs @@ -1,4 +1,3 @@ -use std::cell::RefCell; use std::time; use actix_codec::{AsyncRead, AsyncWrite}; @@ -111,7 +110,7 @@ where Ok(ClientResponse { head, - payload: RefCell::new(Some(Box::new(Payload::new(body)))), + payload: Some(Box::new(Payload::new(body))), }) }) .from_err() diff --git a/src/client/request.rs b/src/client/request.rs index b80f0e6dc..7e971756d 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -268,10 +268,9 @@ impl ClientRequestBuilder { /// /// ```rust /// # extern crate mime; - /// # extern crate actix_web; - /// # use actix_web::client::*; + /// # extern crate actix_http; /// # - /// use actix_web::{client, http}; + /// use actix_http::{client, http}; /// /// fn main() { /// let req = client::ClientRequest::build() @@ -299,16 +298,14 @@ impl ClientRequestBuilder { /// To override header use `set_header()` method. /// /// ```rust - /// # extern crate http; - /// # extern crate actix_web; - /// # use actix_web::client::*; + /// # extern crate actix_http; /// # - /// use http::header; + /// use actix_http::{client, http}; /// /// fn main() { - /// let req = ClientRequest::build() + /// let req = client::ClientRequest::build() /// .header("X-TEST", "value") - /// .header(header::CONTENT_TYPE, "application/json") + /// .header(http::header::CONTENT_TYPE, "application/json") /// .finish() /// .unwrap(); /// } @@ -427,8 +424,8 @@ impl ClientRequestBuilder { /// Set a cookie /// /// ```rust - /// # extern crate actix_web; - /// use actix_web::{client, http}; + /// # extern crate actix_http; + /// use actix_http::{client, http}; /// /// fn main() { /// let req = client::ClientRequest::build() diff --git a/src/client/response.rs b/src/client/response.rs index 6224d3cb5..65c59f2a6 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -1,4 +1,3 @@ -use std::cell::RefCell; use std::fmt; use bytes::Bytes; @@ -10,13 +9,11 @@ use crate::error::PayloadError; use crate::httpmessage::HttpMessage; use crate::message::{Head, ResponseHead}; -use super::h1proto::Payload; - /// Client Response #[derive(Default)] pub struct ClientResponse { pub(crate) head: ResponseHead, - pub(crate) payload: RefCell>, + pub(crate) payload: Option, } impl HttpMessage for ClientResponse { @@ -27,12 +24,8 @@ impl HttpMessage for ClientResponse { } #[inline] - fn payload(self) -> Self::Stream { - if let Some(payload) = self.payload.borrow_mut().take() { - payload - } else { - Payload::empty() - } + fn payload(&mut self) -> Option { + self.payload.take() } } @@ -41,7 +34,7 @@ impl ClientResponse { pub fn new() -> ClientResponse { ClientResponse { head: ResponseHead::default(), - payload: RefCell::new(None), + payload: None, } } @@ -84,6 +77,11 @@ impl ClientResponse { pub fn keep_alive(&self) -> bool { self.head().keep_alive() } + + /// Set response payload + pub fn set_payload(&mut self, payload: PayloadStream) { + self.payload = Some(payload); + } } impl Stream for ClientResponse { @@ -91,7 +89,7 @@ impl Stream for ClientResponse { type Error = PayloadError; fn poll(&mut self) -> Poll, Self::Error> { - if let Some(ref mut payload) = &mut *self.payload.borrow_mut() { + if let Some(ref mut payload) = self.payload { payload.poll() } else { Ok(Async::Ready(None)) diff --git a/src/error.rs b/src/error.rs index 03224b558..f71b429fd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -344,7 +344,7 @@ pub enum PayloadError { UnknownLength, /// Http2 payload error #[display(fmt = "{}", _0)] - H2Payload(h2::Error), + Http2Payload(h2::Error), } impl From for PayloadError { @@ -642,6 +642,16 @@ where InternalError::new(err, StatusCode::UNAUTHORIZED).into() } +/// Helper function that creates wrapper of any error and generate +/// *PAYMENT_REQUIRED* response. +#[allow(non_snake_case)] +pub fn ErrorPaymentRequired(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::PAYMENT_REQUIRED).into() +} + /// Helper function that creates wrapper of any error and generate *FORBIDDEN* /// response. #[allow(non_snake_case)] @@ -672,6 +682,26 @@ where InternalError::new(err, StatusCode::METHOD_NOT_ALLOWED).into() } +/// Helper function that creates wrapper of any error and generate *NOT +/// ACCEPTABLE* response. +#[allow(non_snake_case)] +pub fn ErrorNotAcceptable(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::NOT_ACCEPTABLE).into() +} + +/// Helper function that creates wrapper of any error and generate *PROXY +/// AUTHENTICATION REQUIRED* response. +#[allow(non_snake_case)] +pub fn ErrorProxyAuthenticationRequired(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::PROXY_AUTHENTICATION_REQUIRED).into() +} + /// Helper function that creates wrapper of any error and generate *REQUEST /// TIMEOUT* response. #[allow(non_snake_case)] @@ -702,6 +732,116 @@ where InternalError::new(err, StatusCode::GONE).into() } +/// Helper function that creates wrapper of any error and generate *LENGTH +/// REQUIRED* response. +#[allow(non_snake_case)] +pub fn ErrorLengthRequired(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::LENGTH_REQUIRED).into() +} + +/// Helper function that creates wrapper of any error and generate +/// *PAYLOAD TOO LARGE* response. +#[allow(non_snake_case)] +pub fn ErrorPayloadTooLarge(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::PAYLOAD_TOO_LARGE).into() +} + +/// Helper function that creates wrapper of any error and generate +/// *URI TOO LONG* response. +#[allow(non_snake_case)] +pub fn ErrorUriTooLong(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::URI_TOO_LONG).into() +} + +/// Helper function that creates wrapper of any error and generate +/// *UNSUPPORTED MEDIA TYPE* response. +#[allow(non_snake_case)] +pub fn ErrorUnsupportedMediaType(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::UNSUPPORTED_MEDIA_TYPE).into() +} + +/// Helper function that creates wrapper of any error and generate +/// *RANGE NOT SATISFIABLE* response. +#[allow(non_snake_case)] +pub fn ErrorRangeNotSatisfiable(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::RANGE_NOT_SATISFIABLE).into() +} + +/// Helper function that creates wrapper of any error and generate +/// *IM A TEAPOT* response. +#[allow(non_snake_case)] +pub fn ErrorImATeapot(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::IM_A_TEAPOT).into() +} + +/// Helper function that creates wrapper of any error and generate +/// *MISDIRECTED REQUEST* response. +#[allow(non_snake_case)] +pub fn ErrorMisdirectedRequest(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::MISDIRECTED_REQUEST).into() +} + +/// Helper function that creates wrapper of any error and generate +/// *UNPROCESSABLE ENTITY* response. +#[allow(non_snake_case)] +pub fn ErrorUnprocessableEntity(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::UNPROCESSABLE_ENTITY).into() +} + +/// Helper function that creates wrapper of any error and generate +/// *LOCKED* response. +#[allow(non_snake_case)] +pub fn ErrorLocked(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::LOCKED).into() +} + +/// Helper function that creates wrapper of any error and generate +/// *FAILED DEPENDENCY* response. +#[allow(non_snake_case)] +pub fn ErrorFailedDependency(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::FAILED_DEPENDENCY).into() +} + +/// Helper function that creates wrapper of any error and generate +/// *UPGRADE REQUIRED* response. +#[allow(non_snake_case)] +pub fn ErrorUpgradeRequired(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::UPGRADE_REQUIRED).into() +} + /// Helper function that creates wrapper of any error and generate /// *PRECONDITION FAILED* response. #[allow(non_snake_case)] @@ -712,6 +852,46 @@ where InternalError::new(err, StatusCode::PRECONDITION_FAILED).into() } +/// Helper function that creates wrapper of any error and generate +/// *PRECONDITION REQUIRED* response. +#[allow(non_snake_case)] +pub fn ErrorPreconditionRequired(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::PRECONDITION_REQUIRED).into() +} + +/// Helper function that creates wrapper of any error and generate +/// *TOO MANY REQUESTS* response. +#[allow(non_snake_case)] +pub fn ErrorTooManyRequests(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::TOO_MANY_REQUESTS).into() +} + +/// Helper function that creates wrapper of any error and generate +/// *REQUEST HEADER FIELDS TOO LARGE* response. +#[allow(non_snake_case)] +pub fn ErrorRequestHeaderFieldsTooLarge(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE).into() +} + +/// Helper function that creates wrapper of any error and generate +/// *UNAVAILABLE FOR LEGAL REASONS* response. +#[allow(non_snake_case)] +pub fn ErrorUnavailableForLegalReasons(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS).into() +} + /// Helper function that creates wrapper of any error and generate /// *EXPECTATION FAILED* response. #[allow(non_snake_case)] @@ -772,6 +952,66 @@ where InternalError::new(err, StatusCode::GATEWAY_TIMEOUT).into() } +/// Helper function that creates wrapper of any error and +/// generate *HTTP VERSION NOT SUPPORTED* response. +#[allow(non_snake_case)] +pub fn ErrorHttpVersionNotSupported(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::HTTP_VERSION_NOT_SUPPORTED).into() +} + +/// Helper function that creates wrapper of any error and +/// generate *VARIANT ALSO NEGOTIATES* response. +#[allow(non_snake_case)] +pub fn ErrorVariantAlsoNegotiates(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::VARIANT_ALSO_NEGOTIATES).into() +} + +/// Helper function that creates wrapper of any error and +/// generate *INSUFFICIENT STORAGE* response. +#[allow(non_snake_case)] +pub fn ErrorInsufficientStorage(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::INSUFFICIENT_STORAGE).into() +} + +/// Helper function that creates wrapper of any error and +/// generate *LOOP DETECTED* response. +#[allow(non_snake_case)] +pub fn ErrorLoopDetected(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::LOOP_DETECTED).into() +} + +/// Helper function that creates wrapper of any error and +/// generate *NOT EXTENDED* response. +#[allow(non_snake_case)] +pub fn ErrorNotExtended(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::NOT_EXTENDED).into() +} + +/// Helper function that creates wrapper of any error and +/// generate *NETWORK AUTHENTICATION REQUIRED* response. +#[allow(non_snake_case)] +pub fn ErrorNetworkAuthenticationRequired(err: T) -> Error +where + T: fmt::Debug + fmt::Display + 'static, +{ + InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into() +} + #[cfg(test)] mod tests { use super::*; @@ -926,6 +1166,9 @@ mod tests { let r: Response = ErrorUnauthorized("err").into(); assert_eq!(r.status(), StatusCode::UNAUTHORIZED); + let r: Response = ErrorPaymentRequired("err").into(); + assert_eq!(r.status(), StatusCode::PAYMENT_REQUIRED); + let r: Response = ErrorForbidden("err").into(); assert_eq!(r.status(), StatusCode::FORBIDDEN); @@ -935,6 +1178,12 @@ mod tests { let r: Response = ErrorMethodNotAllowed("err").into(); assert_eq!(r.status(), StatusCode::METHOD_NOT_ALLOWED); + let r: Response = ErrorNotAcceptable("err").into(); + assert_eq!(r.status(), StatusCode::NOT_ACCEPTABLE); + + let r: Response = ErrorProxyAuthenticationRequired("err").into(); + assert_eq!(r.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED); + let r: Response = ErrorRequestTimeout("err").into(); assert_eq!(r.status(), StatusCode::REQUEST_TIMEOUT); @@ -944,12 +1193,57 @@ mod tests { let r: Response = ErrorGone("err").into(); assert_eq!(r.status(), StatusCode::GONE); + let r: Response = ErrorLengthRequired("err").into(); + assert_eq!(r.status(), StatusCode::LENGTH_REQUIRED); + let r: Response = ErrorPreconditionFailed("err").into(); assert_eq!(r.status(), StatusCode::PRECONDITION_FAILED); + let r: Response = ErrorPayloadTooLarge("err").into(); + assert_eq!(r.status(), StatusCode::PAYLOAD_TOO_LARGE); + + let r: Response = ErrorUriTooLong("err").into(); + assert_eq!(r.status(), StatusCode::URI_TOO_LONG); + + let r: Response = ErrorUnsupportedMediaType("err").into(); + assert_eq!(r.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); + + let r: Response = ErrorRangeNotSatisfiable("err").into(); + assert_eq!(r.status(), StatusCode::RANGE_NOT_SATISFIABLE); + let r: Response = ErrorExpectationFailed("err").into(); assert_eq!(r.status(), StatusCode::EXPECTATION_FAILED); + let r: Response = ErrorImATeapot("err").into(); + assert_eq!(r.status(), StatusCode::IM_A_TEAPOT); + + let r: Response = ErrorMisdirectedRequest("err").into(); + assert_eq!(r.status(), StatusCode::MISDIRECTED_REQUEST); + + let r: Response = ErrorUnprocessableEntity("err").into(); + assert_eq!(r.status(), StatusCode::UNPROCESSABLE_ENTITY); + + let r: Response = ErrorLocked("err").into(); + assert_eq!(r.status(), StatusCode::LOCKED); + + let r: Response = ErrorFailedDependency("err").into(); + assert_eq!(r.status(), StatusCode::FAILED_DEPENDENCY); + + let r: Response = ErrorUpgradeRequired("err").into(); + assert_eq!(r.status(), StatusCode::UPGRADE_REQUIRED); + + let r: Response = ErrorPreconditionRequired("err").into(); + assert_eq!(r.status(), StatusCode::PRECONDITION_REQUIRED); + + let r: Response = ErrorTooManyRequests("err").into(); + assert_eq!(r.status(), StatusCode::TOO_MANY_REQUESTS); + + let r: Response = ErrorRequestHeaderFieldsTooLarge("err").into(); + assert_eq!(r.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE); + + let r: Response = ErrorUnavailableForLegalReasons("err").into(); + assert_eq!(r.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS); + let r: Response = ErrorInternalServerError("err").into(); assert_eq!(r.status(), StatusCode::INTERNAL_SERVER_ERROR); @@ -964,5 +1258,23 @@ mod tests { let r: Response = ErrorGatewayTimeout("err").into(); assert_eq!(r.status(), StatusCode::GATEWAY_TIMEOUT); + + let r: Response = ErrorHttpVersionNotSupported("err").into(); + assert_eq!(r.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED); + + let r: Response = ErrorVariantAlsoNegotiates("err").into(); + assert_eq!(r.status(), StatusCode::VARIANT_ALSO_NEGOTIATES); + + let r: Response = ErrorInsufficientStorage("err").into(); + assert_eq!(r.status(), StatusCode::INSUFFICIENT_STORAGE); + + let r: Response = ErrorLoopDetected("err").into(); + assert_eq!(r.status(), StatusCode::LOOP_DETECTED); + + let r: Response = ErrorNotExtended("err").into(); + assert_eq!(r.status(), StatusCode::NOT_EXTENDED); + + let r: Response = ErrorNetworkAuthenticationRequired("err").into(); + assert_eq!(r.status(), StatusCode::NETWORK_AUTHENTICATION_REQUIRED); } } diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 1295dfddf..0a0ed04e2 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -14,11 +14,11 @@ use crate::body::{Body, BodyLength, MessageBody, ResponseBody}; use crate::config::ServiceConfig; use crate::error::DispatchError; use crate::error::{ParseError, PayloadError}; -use crate::payload::{Payload, PayloadSender, PayloadStatus, PayloadWriter}; use crate::request::Request; use crate::response::Response; use super::codec::Codec; +use super::payload::{Payload, PayloadSender, PayloadStatus, PayloadWriter}; use super::{H1ServiceResult, Message, MessageType}; const MAX_PIPELINED_MESSAGES: usize = 16; diff --git a/src/h1/mod.rs b/src/h1/mod.rs index a375b60d3..9054c2665 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -9,11 +9,13 @@ mod codec; mod decoder; mod dispatcher; mod encoder; +mod payload; mod service; pub use self::client::{ClientCodec, ClientPayloadCodec}; pub use self::codec::Codec; pub use self::dispatcher::Dispatcher; +pub use self::payload::{Payload, PayloadBuffer}; pub use self::service::{H1Service, H1ServiceHandler, OneRequest}; use crate::request::Request; diff --git a/src/payload.rs b/src/h1/payload.rs similarity index 100% rename from src/payload.rs rename to src/h1/payload.rs diff --git a/src/httpcodes.rs b/src/httpcodes.rs index 7806bd806..5dfeefa9e 100644 --- a/src/httpcodes.rs +++ b/src/httpcodes.rs @@ -54,6 +54,7 @@ impl Response { STATIC_RESP!(Gone, StatusCode::GONE); STATIC_RESP!(LengthRequired, StatusCode::LENGTH_REQUIRED); STATIC_RESP!(PreconditionFailed, StatusCode::PRECONDITION_FAILED); + STATIC_RESP!(PreconditionRequired, StatusCode::PRECONDITION_REQUIRED); STATIC_RESP!(PayloadTooLarge, StatusCode::PAYLOAD_TOO_LARGE); STATIC_RESP!(UriTooLong, StatusCode::URI_TOO_LONG); STATIC_RESP!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE); diff --git a/src/httpmessage.rs b/src/httpmessage.rs index c50de2e94..39aa1b689 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -25,7 +25,7 @@ pub trait HttpMessage: Sized { fn headers(&self) -> &HeaderMap; /// Message payload stream - fn payload(self) -> Self::Stream; + fn payload(&mut self) -> Option; #[doc(hidden)] /// Get a header @@ -128,7 +128,7 @@ pub trait HttpMessage: Sized { /// } /// # fn main() {} /// ``` - fn body(self) -> MessageBody { + fn body(&mut self) -> MessageBody { MessageBody::new(self) } @@ -162,7 +162,7 @@ pub trait HttpMessage: Sized { /// } /// # fn main() {} /// ``` - fn urlencoded(self) -> UrlEncoded { + fn urlencoded(&mut self) -> UrlEncoded { UrlEncoded::new(self) } @@ -198,19 +198,19 @@ pub trait HttpMessage: Sized { /// } /// # fn main() {} /// ``` - fn json(self) -> JsonBody { + fn json(&mut self) -> JsonBody { JsonBody::new(self) } /// Return stream of lines. - fn readlines(self) -> Readlines { + fn readlines(&mut self) -> Readlines { Readlines::new(self) } } /// Stream to read request line by line. pub struct Readlines { - stream: T::Stream, + stream: Option, buff: BytesMut, limit: usize, checked_buff: bool, @@ -220,7 +220,7 @@ pub struct Readlines { impl Readlines { /// Create a new stream to read request line by line. - fn new(req: T) -> Self { + fn new(req: &mut T) -> Self { let encoding = match req.encoding() { Ok(enc) => enc, Err(err) => return Self::err(req, err.into()), @@ -242,7 +242,7 @@ impl Readlines { self } - fn err(req: T, err: ReadlinesError) -> Self { + fn err(req: &mut T, err: ReadlinesError) -> Self { Readlines { stream: req.payload(), buff: BytesMut::new(), @@ -292,61 +292,65 @@ impl Stream for Readlines { self.checked_buff = true; } // poll req for more bytes - match self.stream.poll() { - Ok(Async::Ready(Some(mut bytes))) => { - // check if there is a newline in bytes - let mut found: Option = None; - for (ind, b) in bytes.iter().enumerate() { - if *b == b'\n' { - found = Some(ind); - break; + if let Some(ref mut stream) = self.stream { + match stream.poll() { + Ok(Async::Ready(Some(mut bytes))) => { + // check if there is a newline in bytes + let mut found: Option = None; + for (ind, b) in bytes.iter().enumerate() { + if *b == b'\n' { + found = Some(ind); + break; + } } + if let Some(ind) = found { + // check if line is longer than limit + if ind + 1 > self.limit { + return Err(ReadlinesError::LimitOverflow); + } + let enc: *const Encoding = self.encoding as *const Encoding; + let line = if enc == UTF_8 { + str::from_utf8(&bytes.split_to(ind + 1)) + .map_err(|_| ReadlinesError::EncodingError)? + .to_owned() + } else { + self.encoding + .decode(&bytes.split_to(ind + 1), DecoderTrap::Strict) + .map_err(|_| ReadlinesError::EncodingError)? + }; + // extend buffer with rest of the bytes; + self.buff.extend_from_slice(&bytes); + self.checked_buff = false; + return Ok(Async::Ready(Some(line))); + } + self.buff.extend_from_slice(&bytes); + Ok(Async::NotReady) } - if let Some(ind) = found { - // check if line is longer than limit - if ind + 1 > self.limit { + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(None)) => { + if self.buff.is_empty() { + return Ok(Async::Ready(None)); + } + if self.buff.len() > self.limit { return Err(ReadlinesError::LimitOverflow); } let enc: *const Encoding = self.encoding as *const Encoding; let line = if enc == UTF_8 { - str::from_utf8(&bytes.split_to(ind + 1)) + str::from_utf8(&self.buff) .map_err(|_| ReadlinesError::EncodingError)? .to_owned() } else { self.encoding - .decode(&bytes.split_to(ind + 1), DecoderTrap::Strict) + .decode(&self.buff, DecoderTrap::Strict) .map_err(|_| ReadlinesError::EncodingError)? }; - // extend buffer with rest of the bytes; - self.buff.extend_from_slice(&bytes); - self.checked_buff = false; - return Ok(Async::Ready(Some(line))); + self.buff.clear(); + Ok(Async::Ready(Some(line))) } - self.buff.extend_from_slice(&bytes); - Ok(Async::NotReady) + Err(e) => Err(ReadlinesError::from(e)), } - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(None)) => { - if self.buff.is_empty() { - return Ok(Async::Ready(None)); - } - if self.buff.len() > self.limit { - return Err(ReadlinesError::LimitOverflow); - } - let enc: *const Encoding = self.encoding as *const Encoding; - let line = if enc == UTF_8 { - str::from_utf8(&self.buff) - .map_err(|_| ReadlinesError::EncodingError)? - .to_owned() - } else { - self.encoding - .decode(&self.buff, DecoderTrap::Strict) - .map_err(|_| ReadlinesError::EncodingError)? - }; - self.buff.clear(); - Ok(Async::Ready(Some(line))) - } - Err(e) => Err(ReadlinesError::from(e)), + } else { + Ok(Async::Ready(None)) } } } @@ -362,7 +366,7 @@ pub struct MessageBody { impl MessageBody { /// Create `MessageBody` for request. - pub fn new(req: T) -> MessageBody { + pub fn new(req: &mut T) -> MessageBody { let mut len = None; if let Some(l) = req.headers().get(header::CONTENT_LENGTH) { if let Ok(s) = l.to_str() { @@ -379,7 +383,7 @@ impl MessageBody { MessageBody { limit: 262_144, length: len, - stream: Some(req.payload()), + stream: req.payload(), fut: None, err: None, } @@ -424,6 +428,10 @@ where } } + if self.stream.is_none() { + return Ok(Async::Ready(Bytes::new())); + } + // future let limit = self.limit; self.fut = Some(Box::new( @@ -457,7 +465,7 @@ pub struct UrlEncoded { impl UrlEncoded { /// Create a new future to URL encode a request - pub fn new(req: T) -> UrlEncoded { + pub fn new(req: &mut T) -> UrlEncoded { // check content type if req.content_type().to_lowercase() != "application/x-www-form-urlencoded" { return Self::err(UrlencodedError::ContentType); @@ -482,7 +490,7 @@ impl UrlEncoded { UrlEncoded { encoding, - stream: Some(req.payload()), + stream: req.payload(), limit: 262_144, length: len, fut: None, @@ -694,7 +702,7 @@ mod tests { #[test] fn test_urlencoded_error() { - let req = TestRequest::with_header( + let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) @@ -705,7 +713,7 @@ mod tests { UrlencodedError::UnknownLength ); - let req = TestRequest::with_header( + let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) @@ -716,7 +724,7 @@ mod tests { UrlencodedError::Overflow ); - let req = TestRequest::with_header(header::CONTENT_TYPE, "text/plain") + let mut req = TestRequest::with_header(header::CONTENT_TYPE, "text/plain") .header(header::CONTENT_LENGTH, "10") .finish(); assert_eq!( @@ -727,7 +735,7 @@ mod tests { #[test] fn test_urlencoded() { - let req = TestRequest::with_header( + let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) @@ -743,7 +751,7 @@ mod tests { }) ); - let req = TestRequest::with_header( + let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8", ) @@ -762,19 +770,20 @@ mod tests { #[test] fn test_message_body() { - let req = TestRequest::with_header(header::CONTENT_LENGTH, "xxxx").finish(); + let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "xxxx").finish(); match req.body().poll().err().unwrap() { PayloadError::UnknownLength => (), _ => unreachable!("error"), } - let req = TestRequest::with_header(header::CONTENT_LENGTH, "1000000").finish(); + let mut req = + TestRequest::with_header(header::CONTENT_LENGTH, "1000000").finish(); match req.body().poll().err().unwrap() { PayloadError::Overflow => (), _ => unreachable!("error"), } - let req = TestRequest::default() + let mut req = TestRequest::default() .set_payload(Bytes::from_static(b"test")) .finish(); match req.body().poll().ok().unwrap() { @@ -782,7 +791,7 @@ mod tests { _ => unreachable!("error"), } - let req = TestRequest::default() + let mut req = TestRequest::default() .set_payload(Bytes::from_static(b"11111111111111")) .finish(); match req.body().limit(5).poll().err().unwrap() { @@ -793,14 +802,14 @@ mod tests { #[test] fn test_readlines() { - let req = TestRequest::default() + let mut req = TestRequest::default() .set_payload(Bytes::from_static( b"Lorem Ipsum is simply dummy text of the printing and typesetting\n\ industry. Lorem Ipsum has been the industry's standard dummy\n\ Contrary to popular belief, Lorem Ipsum is not simply random text.", )) .finish(); - let mut r = Readlines::new(req); + let mut r = Readlines::new(&mut req); match r.poll().ok().unwrap() { Async::Ready(Some(s)) => assert_eq!( s, diff --git a/src/json.rs b/src/json.rs index fc1ab4d25..6cd1e87ab 100644 --- a/src/json.rs +++ b/src/json.rs @@ -50,7 +50,7 @@ pub struct JsonBody { impl JsonBody { /// Create `JsonBody` for request. - pub fn new(req: T) -> Self { + pub fn new(req: &mut T) -> Self { // check content-type let json = if let Ok(Some(mime)) = req.mime_type() { mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) @@ -79,7 +79,7 @@ impl JsonBody { JsonBody { limit: 262_144, length: len, - stream: Some(req.payload()), + stream: req.payload(), fut: None, err: None, } @@ -164,11 +164,11 @@ mod tests { #[test] fn test_json_body() { - let req = TestRequest::default().finish(); + let mut req = TestRequest::default().finish(); let mut json = req.json::(); assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); - let req = TestRequest::default() + let mut req = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/text"), @@ -177,7 +177,7 @@ mod tests { let mut json = req.json::(); assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); - let req = TestRequest::default() + let mut req = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), @@ -190,7 +190,7 @@ mod tests { let mut json = req.json::().limit(100); assert_eq!(json.poll().err().unwrap(), JsonPayloadError::Overflow); - let req = TestRequest::default() + let mut req = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), diff --git a/src/lib.rs b/src/lib.rs index cdb4f0382..a3ca52eda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,7 +78,6 @@ mod httpcodes; mod httpmessage; mod json; mod message; -mod payload; mod request; mod response; mod service; @@ -111,7 +110,6 @@ pub mod dev { pub use crate::httpmessage::{MessageBody, Readlines, UrlEncoded}; pub use crate::json::JsonBody; - pub use crate::payload::{Payload, PayloadBuffer}; pub use crate::response::ResponseBuilder; } diff --git a/src/request.rs b/src/request.rs index b60a772e1..4df95d450 100644 --- a/src/request.rs +++ b/src/request.rs @@ -10,7 +10,8 @@ use crate::error::PayloadError; use crate::extensions::Extensions; use crate::httpmessage::HttpMessage; use crate::message::{Message, MessagePool, RequestHead}; -use crate::payload::Payload; + +use crate::h1::Payload; /// Request pub struct Request

    { @@ -29,8 +30,8 @@ where } #[inline] - fn payload(mut self) -> P { - self.payload.take().unwrap() + fn payload(&mut self) -> Option

    { + self.payload.take() } } @@ -62,9 +63,9 @@ impl Request { } /// Take request's payload - pub fn take_payload(mut self) -> (Payload, Request<()>) { + pub fn take_payload(mut self) -> (Option, Request<()>) { ( - self.payload.take().unwrap(), + self.payload.take(), Request { payload: Some(()), inner: self.inner.clone(), diff --git a/src/service/mod.rs b/src/service/mod.rs index 83a40bd12..3939ab99c 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -1,3 +1,5 @@ +use h2::RecvStream; + mod senderror; pub use self::senderror::{SendError, SendResponse}; diff --git a/src/test.rs b/src/test.rs index 852dd3c0b..c68bbf8e5 100644 --- a/src/test.rs +++ b/src/test.rs @@ -6,8 +6,8 @@ use cookie::Cookie; use http::header::HeaderName; use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; +use crate::h1::Payload; use crate::header::{Header, IntoHeaderValue}; -use crate::payload::Payload; use crate::Request; /// Test `Request` builder diff --git a/tests/test_client.rs b/tests/test_client.rs index 606bac22a..6f502b0af 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -47,7 +47,7 @@ fn test_h1_v2() { assert!(repr.contains("ClientRequest")); assert!(repr.contains("x-test")); - let response = srv.block_on(request.send(&mut connector)).unwrap(); + let mut response = srv.block_on(request.send(&mut connector)).unwrap(); assert!(response.status().is_success()); // read response @@ -55,7 +55,7 @@ fn test_h1_v2() { assert_eq!(bytes, Bytes::from_static(STR.as_ref())); let request = srv.post().finish().unwrap(); - let response = srv.block_on(request.send(&mut connector)).unwrap(); + let mut response = srv.block_on(request.send(&mut connector)).unwrap(); assert!(response.status().is_success()); // read response diff --git a/tests/test_server.rs b/tests/test_server.rs index 9fa27e71b..53db38403 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -89,7 +89,7 @@ fn test_h2_body() -> std::io::Result<()> { .map_err(|e| println!("Openssl error: {}", e)) .and_then( h2::H2Service::build() - .finish(|req: Request<_>| { + .finish(|mut req: Request<_>| { req.body() .limit(1024 * 1024) .and_then(|body| Ok(Response::Ok().body(body))) @@ -101,7 +101,7 @@ fn test_h2_body() -> std::io::Result<()> { let req = client::ClientRequest::get(srv.surl("/")) .body(data.clone()) .unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); let body = srv.block_on(response.body().limit(1024 * 1024)).unwrap(); @@ -350,7 +350,7 @@ fn test_headers() { let req = srv.get().finish().unwrap(); - let response = srv.block_on(req.send(&mut connector)).unwrap(); + let mut response = srv.block_on(req.send(&mut connector)).unwrap(); assert!(response.status().is_success()); // read response @@ -387,7 +387,7 @@ fn test_body() { }); let req = srv.get().finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response @@ -402,7 +402,7 @@ fn test_head_empty() { }); let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); { @@ -428,7 +428,7 @@ fn test_head_binary() { }); let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); { @@ -477,7 +477,7 @@ fn test_body_length() { }); let req = srv.get().finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response @@ -496,7 +496,7 @@ fn test_body_chunked_explicit() { }); let req = srv.get().finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response @@ -517,7 +517,7 @@ fn test_body_chunked_implicit() { }); let req = srv.get().finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response @@ -540,7 +540,7 @@ fn test_response_http_error_handling() { }); let req = srv.get().finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response diff --git a/tests/test_ws.rs b/tests/test_ws.rs index 07f857d7a..bf5a3c41e 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -5,7 +5,6 @@ use actix_http_test::TestServer; use actix_service::NewService; use actix_utils::framed::IntoFramed; use actix_utils::stream::TakeItem; -use actix_web::ws as web_ws; use bytes::{Bytes, BytesMut}; use futures::future::{lazy, ok, Either}; use futures::{Future, Sink, Stream}; @@ -105,37 +104,4 @@ fn test_simple() { item, Some(ws::Frame::Close(Some(ws::CloseCode::Normal.into()))) ); - - { - let mut sys = actix_web::actix::System::new("test"); - let url = srv.url("/"); - - let (reader, mut writer) = sys - .block_on(lazy(|| web_ws::Client::new(url).connect())) - .unwrap(); - - writer.text("text"); - let (item, reader) = sys.block_on(reader.into_future()).unwrap(); - assert_eq!(item, Some(web_ws::Message::Text("text".to_owned()))); - - writer.binary(b"text".as_ref()); - let (item, reader) = sys.block_on(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(web_ws::Message::Binary(Bytes::from_static(b"text").into())) - ); - - writer.ping("ping"); - let (item, reader) = sys.block_on(reader.into_future()).unwrap(); - assert_eq!(item, Some(web_ws::Message::Pong("ping".to_owned()))); - - writer.close(Some(web_ws::CloseCode::Normal.into())); - let (item, _) = sys.block_on(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(web_ws::Message::Close(Some( - web_ws::CloseCode::Normal.into() - ))) - ); - } } From c4596b0bd6ce2758a79a93e11f2df77dc1f0f94a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Feb 2019 13:24:24 -0800 Subject: [PATCH 122/427] add headers from actix-web --- Cargo.toml | 3 + src/header.rs | 155 ---- src/header/common/accept.rs | 160 ++++ src/header/common/accept_charset.rs | 69 ++ src/header/common/accept_encoding.rs | 72 ++ src/header/common/accept_language.rs | 75 ++ src/header/common/allow.rs | 85 +++ src/header/common/cache_control.rs | 257 +++++++ src/header/common/content_disposition.rs | 918 +++++++++++++++++++++++ src/header/common/content_language.rs | 65 ++ src/header/common/content_range.rs | 208 +++++ src/header/common/content_type.rs | 122 +++ src/header/common/date.rs | 42 ++ src/header/common/etag.rs | 96 +++ src/header/common/expires.rs | 39 + src/header/common/if_match.rs | 70 ++ src/header/common/if_modified_since.rs | 39 + src/header/common/if_none_match.rs | 92 +++ src/header/common/if_range.rs | 116 +++ src/header/common/if_unmodified_since.rs | 40 + src/header/common/last_modified.rs | 38 + src/header/common/mod.rs | 352 +++++++++ src/header/common/range.rs | 434 +++++++++++ src/header/mod.rs | 465 ++++++++++++ src/header/shared/charset.rs | 153 ++++ src/header/shared/encoding.rs | 58 ++ src/header/shared/entity.rs | 265 +++++++ src/header/shared/httpdate.rs | 118 +++ src/header/shared/mod.rs | 14 + src/header/shared/quality_item.rs | 291 +++++++ tests/test_ws.rs | 2 +- 31 files changed, 4757 insertions(+), 156 deletions(-) delete mode 100644 src/header.rs create mode 100644 src/header/common/accept.rs create mode 100644 src/header/common/accept_charset.rs create mode 100644 src/header/common/accept_encoding.rs create mode 100644 src/header/common/accept_language.rs create mode 100644 src/header/common/allow.rs create mode 100644 src/header/common/cache_control.rs create mode 100644 src/header/common/content_disposition.rs create mode 100644 src/header/common/content_language.rs create mode 100644 src/header/common/content_range.rs create mode 100644 src/header/common/content_type.rs create mode 100644 src/header/common/date.rs create mode 100644 src/header/common/etag.rs create mode 100644 src/header/common/expires.rs create mode 100644 src/header/common/if_match.rs create mode 100644 src/header/common/if_modified_since.rs create mode 100644 src/header/common/if_none_match.rs create mode 100644 src/header/common/if_range.rs create mode 100644 src/header/common/if_unmodified_since.rs create mode 100644 src/header/common/last_modified.rs create mode 100644 src/header/common/mod.rs create mode 100644 src/header/common/range.rs create mode 100644 src/header/mod.rs create mode 100644 src/header/shared/charset.rs create mode 100644 src/header/shared/encoding.rs create mode 100644 src/header/shared/entity.rs create mode 100644 src/header/shared/httpdate.rs create mode 100644 src/header/shared/mod.rs create mode 100644 src/header/shared/quality_item.rs diff --git a/Cargo.toml b/Cargo.toml index 23938f2fa..ea246762e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,10 +56,13 @@ h2 = "0.1.16" http = "0.1.8" httparse = "1.3" indexmap = "1.0" +lazy_static = "1.0" +language-tags = "0.2" log = "0.4" mime = "0.3" percent-encoding = "1.0" rand = "0.6" +regex = "1.0" serde = "1.0" serde_json = "1.0" sha1 = "0.6" diff --git a/src/header.rs b/src/header.rs deleted file mode 100644 index 6276dd4f4..000000000 --- a/src/header.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! Various http headers - -use bytes::Bytes; -pub use http::header::*; -use http::Error as HttpError; -use mime::Mime; - -use crate::error::ParseError; -use crate::httpmessage::HttpMessage; - -#[doc(hidden)] -/// A trait for any object that will represent a header field and value. -pub trait Header -where - Self: IntoHeaderValue, -{ - /// Returns the name of the header field - fn name() -> HeaderName; - - /// Parse a header - fn parse(msg: &T) -> Result; -} - -#[doc(hidden)] -/// A trait for any object that can be Converted to a `HeaderValue` -pub trait IntoHeaderValue: Sized { - /// The type returned in the event of a conversion error. - type Error: Into; - - /// Try to convert value to a Header value. - fn try_into(self) -> Result; -} - -impl IntoHeaderValue for HeaderValue { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - Ok(self) - } -} - -impl<'a> IntoHeaderValue for &'a str { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - self.parse() - } -} - -impl<'a> IntoHeaderValue for &'a [u8] { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_bytes(self) - } -} - -impl IntoHeaderValue for Bytes { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(self) - } -} - -impl IntoHeaderValue for Vec { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(Bytes::from(self)) - } -} - -impl IntoHeaderValue for String { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(Bytes::from(self)) - } -} - -impl IntoHeaderValue for Mime { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(Bytes::from(format!("{}", self))) - } -} - -/// Represents supported types of content encodings -#[derive(Copy, Clone, PartialEq, Debug)] -pub enum ContentEncoding { - /// Automatically select encoding based on encoding negotiation - Auto, - /// A format using the Brotli algorithm - Br, - /// A format using the zlib structure with deflate algorithm - Deflate, - /// Gzip algorithm - Gzip, - /// Indicates the identity function (i.e. no compression, nor modification) - Identity, -} - -impl ContentEncoding { - #[inline] - /// Is the content compressed? - pub fn is_compression(self) -> bool { - match self { - ContentEncoding::Identity | ContentEncoding::Auto => false, - _ => true, - } - } - - #[inline] - /// Convert content encoding to string - pub fn as_str(self) -> &'static str { - match self { - ContentEncoding::Br => "br", - ContentEncoding::Gzip => "gzip", - ContentEncoding::Deflate => "deflate", - ContentEncoding::Identity | ContentEncoding::Auto => "identity", - } - } - - #[inline] - /// default quality value - pub fn quality(self) -> f64 { - match self { - ContentEncoding::Br => 1.1, - ContentEncoding::Gzip => 1.0, - ContentEncoding::Deflate => 0.9, - ContentEncoding::Identity | ContentEncoding::Auto => 0.1, - } - } -} - -// TODO: remove memory allocation -impl<'a> From<&'a str> for ContentEncoding { - fn from(s: &'a str) -> ContentEncoding { - match AsRef::::as_ref(&s.trim().to_lowercase()) { - "br" => ContentEncoding::Br, - "gzip" => ContentEncoding::Gzip, - "deflate" => ContentEncoding::Deflate, - _ => ContentEncoding::Identity, - } - } -} diff --git a/src/header/common/accept.rs b/src/header/common/accept.rs new file mode 100644 index 000000000..d52eba241 --- /dev/null +++ b/src/header/common/accept.rs @@ -0,0 +1,160 @@ +use mime::Mime; + +use crate::header::{qitem, QualityItem}; +use crate::http::header; + +header! { + /// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2) + /// + /// The `Accept` header field can be used by user agents to specify + /// response media types that are acceptable. Accept header fields can + /// be used to indicate that the request is specifically limited to a + /// small set of desired types, as in the case of a request for an + /// in-line image + /// + /// # ABNF + /// + /// ```text + /// Accept = #( media-range [ accept-params ] ) + /// + /// media-range = ( "*/*" + /// / ( type "/" "*" ) + /// / ( type "/" subtype ) + /// ) *( OWS ";" OWS parameter ) + /// accept-params = weight *( accept-ext ) + /// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ] + /// ``` + /// + /// # Example values + /// * `audio/*; q=0.2, audio/basic` + /// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c` + /// + /// # Examples + /// ```rust + /// # extern crate actix_http; + /// extern crate mime; + /// use actix_http::Response; + /// use actix_http::http::header::{Accept, qitem}; + /// + /// # fn main() { + /// let mut builder = Response::Ok(); + /// + /// builder.set( + /// Accept(vec![ + /// qitem(mime::TEXT_HTML), + /// ]) + /// ); + /// # } + /// ``` + /// + /// ```rust + /// # extern crate actix_http; + /// extern crate mime; + /// use actix_http::Response; + /// use actix_http::http::header::{Accept, qitem}; + /// + /// # fn main() { + /// let mut builder = Response::Ok(); + /// + /// builder.set( + /// Accept(vec![ + /// qitem(mime::APPLICATION_JSON), + /// ]) + /// ); + /// # } + /// ``` + /// + /// ```rust + /// # extern crate actix_http; + /// extern crate mime; + /// use actix_http::Response; + /// use actix_http::http::header::{Accept, QualityItem, q, qitem}; + /// + /// # fn main() { + /// let mut builder = Response::Ok(); + /// + /// builder.set( + /// Accept(vec![ + /// qitem(mime::TEXT_HTML), + /// qitem("application/xhtml+xml".parse().unwrap()), + /// QualityItem::new( + /// mime::TEXT_XML, + /// q(900) + /// ), + /// qitem("image/webp".parse().unwrap()), + /// QualityItem::new( + /// mime::STAR_STAR, + /// q(800) + /// ), + /// ]) + /// ); + /// # } + /// ``` + (Accept, header::ACCEPT) => (QualityItem)+ + + test_accept { + // Tests from the RFC + test_header!( + test1, + vec![b"audio/*; q=0.2, audio/basic"], + Some(HeaderField(vec![ + QualityItem::new("audio/*".parse().unwrap(), q(200)), + qitem("audio/basic".parse().unwrap()), + ]))); + test_header!( + test2, + vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], + Some(HeaderField(vec![ + QualityItem::new(mime::TEXT_PLAIN, q(500)), + qitem(mime::TEXT_HTML), + QualityItem::new( + "text/x-dvi".parse().unwrap(), + q(800)), + qitem("text/x-c".parse().unwrap()), + ]))); + // Custom tests + test_header!( + test3, + vec![b"text/plain; charset=utf-8"], + Some(Accept(vec![ + qitem(mime::TEXT_PLAIN_UTF_8), + ]))); + test_header!( + test4, + vec![b"text/plain; charset=utf-8; q=0.5"], + Some(Accept(vec![ + QualityItem::new(mime::TEXT_PLAIN_UTF_8, + q(500)), + ]))); + + #[test] + fn test_fuzzing1() { + use crate::test::TestRequest; + let req = TestRequest::with_header(crate::header::ACCEPT, "chunk#;e").finish(); + let header = Accept::parse(&req); + assert!(header.is_ok()); + } + } +} + +impl Accept { + /// A constructor to easily create `Accept: */*`. + pub fn star() -> Accept { + Accept(vec![qitem(mime::STAR_STAR)]) + } + + /// A constructor to easily create `Accept: application/json`. + pub fn json() -> Accept { + Accept(vec![qitem(mime::APPLICATION_JSON)]) + } + + /// A constructor to easily create `Accept: text/*`. + pub fn text() -> Accept { + Accept(vec![qitem(mime::TEXT_STAR)]) + } + + /// A constructor to easily create `Accept: image/*`. + pub fn image() -> Accept { + Accept(vec![qitem(mime::IMAGE_STAR)]) + } +} diff --git a/src/header/common/accept_charset.rs b/src/header/common/accept_charset.rs new file mode 100644 index 000000000..117e2015d --- /dev/null +++ b/src/header/common/accept_charset.rs @@ -0,0 +1,69 @@ +use crate::header::{Charset, QualityItem, ACCEPT_CHARSET}; + +header! { + /// `Accept-Charset` header, defined in + /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.3) + /// + /// The `Accept-Charset` header field can be sent by a user agent to + /// indicate what charsets are acceptable in textual response content. + /// This field allows user agents capable of understanding more + /// comprehensive or special-purpose charsets to signal that capability + /// to an origin server that is capable of representing information in + /// those charsets. + /// + /// # ABNF + /// + /// ```text + /// Accept-Charset = 1#( ( charset / "*" ) [ weight ] ) + /// ``` + /// + /// # Example values + /// * `iso-8859-5, unicode-1-1;q=0.8` + /// + /// # Examples + /// ```rust + /// # extern crate actix_http; + /// use actix_http::Response; + /// use actix_http::http::header::{AcceptCharset, Charset, qitem}; + /// + /// # fn main() { + /// let mut builder = Response::Ok(); + /// builder.set( + /// AcceptCharset(vec![qitem(Charset::Us_Ascii)]) + /// ); + /// # } + /// ``` + /// ```rust + /// # extern crate actix_http; + /// use actix_http::Response; + /// use actix_http::http::header::{AcceptCharset, Charset, q, QualityItem}; + /// + /// # fn main() { + /// let mut builder = Response::Ok(); + /// builder.set( + /// AcceptCharset(vec![ + /// QualityItem::new(Charset::Us_Ascii, q(900)), + /// QualityItem::new(Charset::Iso_8859_10, q(200)), + /// ]) + /// ); + /// # } + /// ``` + /// ```rust + /// # extern crate actix_http; + /// use actix_http::Response; + /// use actix_http::http::header::{AcceptCharset, Charset, qitem}; + /// + /// # fn main() { + /// let mut builder = Response::Ok(); + /// builder.set( + /// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))]) + /// ); + /// # } + /// ``` + (AcceptCharset, ACCEPT_CHARSET) => (QualityItem)+ + + test_accept_charset { + /// Test case from RFC + test_header!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); + } +} diff --git a/src/header/common/accept_encoding.rs b/src/header/common/accept_encoding.rs new file mode 100644 index 000000000..c90f529bc --- /dev/null +++ b/src/header/common/accept_encoding.rs @@ -0,0 +1,72 @@ +use header::{Encoding, QualityItem}; + +header! { + /// `Accept-Encoding` header, defined in + /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.4) + /// + /// The `Accept-Encoding` header field can be used by user agents to + /// indicate what response content-codings are + /// acceptable in the response. An `identity` token is used as a synonym + /// for "no encoding" in order to communicate when no encoding is + /// preferred. + /// + /// # ABNF + /// + /// ```text + /// Accept-Encoding = #( codings [ weight ] ) + /// codings = content-coding / "identity" / "*" + /// ``` + /// + /// # Example values + /// * `compress, gzip` + /// * `` + /// * `*` + /// * `compress;q=0.5, gzip;q=1` + /// * `gzip;q=1.0, identity; q=0.5, *;q=0` + /// + /// # Examples + /// ``` + /// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem}; + /// + /// let mut headers = Headers::new(); + /// headers.set( + /// AcceptEncoding(vec![qitem(Encoding::Chunked)]) + /// ); + /// ``` + /// ``` + /// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem}; + /// + /// let mut headers = Headers::new(); + /// headers.set( + /// AcceptEncoding(vec![ + /// qitem(Encoding::Chunked), + /// qitem(Encoding::Gzip), + /// qitem(Encoding::Deflate), + /// ]) + /// ); + /// ``` + /// ``` + /// use hyper::header::{Headers, AcceptEncoding, Encoding, QualityItem, q, qitem}; + /// + /// let mut headers = Headers::new(); + /// headers.set( + /// AcceptEncoding(vec![ + /// qitem(Encoding::Chunked), + /// QualityItem::new(Encoding::Gzip, q(600)), + /// QualityItem::new(Encoding::EncodingExt("*".to_owned()), q(0)), + /// ]) + /// ); + /// ``` + (AcceptEncoding, "Accept-Encoding") => (QualityItem)* + + test_accept_encoding { + // From the RFC + test_header!(test1, vec![b"compress, gzip"]); + test_header!(test2, vec![b""], Some(AcceptEncoding(vec![]))); + test_header!(test3, vec![b"*"]); + // Note: Removed quality 1 from gzip + test_header!(test4, vec![b"compress;q=0.5, gzip"]); + // Note: Removed quality 1 from gzip + test_header!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); + } +} diff --git a/src/header/common/accept_language.rs b/src/header/common/accept_language.rs new file mode 100644 index 000000000..55879b57f --- /dev/null +++ b/src/header/common/accept_language.rs @@ -0,0 +1,75 @@ +use crate::header::{QualityItem, ACCEPT_LANGUAGE}; +use language_tags::LanguageTag; + +header! { + /// `Accept-Language` header, defined in + /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.5) + /// + /// The `Accept-Language` header field can be used by user agents to + /// indicate the set of natural languages that are preferred in the + /// response. + /// + /// # ABNF + /// + /// ```text + /// Accept-Language = 1#( language-range [ weight ] ) + /// language-range = + /// ``` + /// + /// # Example values + /// * `da, en-gb;q=0.8, en;q=0.7` + /// * `en-us;q=1.0, en;q=0.5, fr` + /// + /// # Examples + /// + /// ```rust + /// # extern crate actix_http; + /// # extern crate language_tags; + /// use actix_http::Response; + /// use actix_http::http::header::{AcceptLanguage, LanguageTag, qitem}; + /// + /// # fn main() { + /// let mut builder = Response::Ok(); + /// let mut langtag: LanguageTag = Default::default(); + /// langtag.language = Some("en".to_owned()); + /// langtag.region = Some("US".to_owned()); + /// builder.set( + /// AcceptLanguage(vec![ + /// qitem(langtag), + /// ]) + /// ); + /// # } + /// ``` + /// + /// ```rust + /// # extern crate actix_http; + /// # #[macro_use] extern crate language_tags; + /// use actix_http::Response; + /// use actix_http::http::header::{AcceptLanguage, QualityItem, q, qitem}; + /// # + /// # fn main() { + /// let mut builder = Response::Ok(); + /// builder.set( + /// AcceptLanguage(vec![ + /// qitem(langtag!(da)), + /// QualityItem::new(langtag!(en;;;GB), q(800)), + /// QualityItem::new(langtag!(en), q(700)), + /// ]) + /// ); + /// # } + /// ``` + (AcceptLanguage, ACCEPT_LANGUAGE) => (QualityItem)+ + + test_accept_language { + // From the RFC + test_header!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]); + // Own test + test_header!( + test2, vec![b"en-US, en; q=0.5, fr"], + Some(AcceptLanguage(vec![ + qitem("en-US".parse().unwrap()), + QualityItem::new("en".parse().unwrap(), q(500)), + qitem("fr".parse().unwrap()), + ]))); + } +} diff --git a/src/header/common/allow.rs b/src/header/common/allow.rs new file mode 100644 index 000000000..432cc00d5 --- /dev/null +++ b/src/header/common/allow.rs @@ -0,0 +1,85 @@ +use http::Method; +use http::header; + +header! { + /// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1) + /// + /// The `Allow` header field lists the set of methods advertised as + /// supported by the target resource. The purpose of this field is + /// strictly to inform the recipient of valid request methods associated + /// with the resource. + /// + /// # ABNF + /// + /// ```text + /// Allow = #method + /// ``` + /// + /// # Example values + /// * `GET, HEAD, PUT` + /// * `OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH, fOObAr` + /// * `` + /// + /// # Examples + /// + /// ```rust + /// # extern crate http; + /// # extern crate actix_http; + /// use actix_http::Response; + /// use actix_http::http::header::Allow; + /// use http::Method; + /// + /// # fn main() { + /// let mut builder = Response::Ok(); + /// builder.set( + /// Allow(vec![Method::GET]) + /// ); + /// # } + /// ``` + /// + /// ```rust + /// # extern crate http; + /// # extern crate actix_http; + /// use actix_http::Response; + /// use actix_http::http::header::Allow; + /// use http::Method; + /// + /// # fn main() { + /// let mut builder = Response::Ok(); + /// builder.set( + /// Allow(vec![ + /// Method::GET, + /// Method::POST, + /// Method::PATCH, + /// ]) + /// ); + /// # } + /// ``` + (Allow, header::ALLOW) => (Method)* + + test_allow { + // From the RFC + test_header!( + test1, + vec![b"GET, HEAD, PUT"], + Some(HeaderField(vec![Method::GET, Method::HEAD, Method::PUT]))); + // Own tests + test_header!( + test2, + vec![b"OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH"], + Some(HeaderField(vec![ + Method::OPTIONS, + Method::GET, + Method::PUT, + Method::POST, + Method::DELETE, + Method::HEAD, + Method::TRACE, + Method::CONNECT, + Method::PATCH]))); + test_header!( + test3, + vec![b""], + Some(HeaderField(Vec::::new()))); + } +} diff --git a/src/header/common/cache_control.rs b/src/header/common/cache_control.rs new file mode 100644 index 000000000..0b79ea7c0 --- /dev/null +++ b/src/header/common/cache_control.rs @@ -0,0 +1,257 @@ +use std::fmt::{self, Write}; +use std::str::FromStr; + +use http::header; + +use crate::header::{ + fmt_comma_delimited, from_comma_delimited, Header, IntoHeaderValue, Writer, +}; + +/// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2) +/// +/// The `Cache-Control` header field is used to specify directives for +/// caches along the request/response chain. Such cache directives are +/// unidirectional in that the presence of a directive in a request does +/// not imply that the same directive is to be given in the response. +/// +/// # ABNF +/// +/// ```text +/// Cache-Control = 1#cache-directive +/// cache-directive = token [ "=" ( token / quoted-string ) ] +/// ``` +/// +/// # Example values +/// +/// * `no-cache` +/// * `private, community="UCI"` +/// * `max-age=30` +/// +/// # Examples +/// ```rust +/// use actix_http::Response; +/// use actix_http::http::header::{CacheControl, CacheDirective}; +/// +/// let mut builder = Response::Ok(); +/// builder.set(CacheControl(vec![CacheDirective::MaxAge(86400u32)])); +/// ``` +/// +/// ```rust +/// use actix_http::Response; +/// use actix_http::http::header::{CacheControl, CacheDirective}; +/// +/// let mut builder = Response::Ok(); +/// builder.set(CacheControl(vec![ +/// CacheDirective::NoCache, +/// CacheDirective::Private, +/// CacheDirective::MaxAge(360u32), +/// CacheDirective::Extension("foo".to_owned(), Some("bar".to_owned())), +/// ])); +/// ``` +#[derive(PartialEq, Clone, Debug)] +pub struct CacheControl(pub Vec); + +__hyper__deref!(CacheControl => Vec); + +//TODO: this could just be the header! macro +impl Header for CacheControl { + fn name() -> header::HeaderName { + header::CACHE_CONTROL + } + + #[inline] + fn parse(msg: &T) -> Result + where + T: crate::HttpMessage, + { + let directives = from_comma_delimited(msg.headers().get_all(Self::name()))?; + if !directives.is_empty() { + Ok(CacheControl(directives)) + } else { + Err(crate::error::ParseError::Header) + } + } +} + +impl fmt::Display for CacheControl { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt_comma_delimited(f, &self[..]) + } +} + +impl IntoHeaderValue for CacheControl { + type Error = header::InvalidHeaderValueBytes; + + fn try_into(self) -> Result { + let mut writer = Writer::new(); + let _ = write!(&mut writer, "{}", self); + header::HeaderValue::from_shared(writer.take()) + } +} + +/// `CacheControl` contains a list of these directives. +#[derive(PartialEq, Clone, Debug)] +pub enum CacheDirective { + /// "no-cache" + NoCache, + /// "no-store" + NoStore, + /// "no-transform" + NoTransform, + /// "only-if-cached" + OnlyIfCached, + + // request directives + /// "max-age=delta" + MaxAge(u32), + /// "max-stale=delta" + MaxStale(u32), + /// "min-fresh=delta" + MinFresh(u32), + + // response directives + /// "must-revalidate" + MustRevalidate, + /// "public" + Public, + /// "private" + Private, + /// "proxy-revalidate" + ProxyRevalidate, + /// "s-maxage=delta" + SMaxAge(u32), + + /// Extension directives. Optionally include an argument. + Extension(String, Option), +} + +impl fmt::Display for CacheDirective { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::CacheDirective::*; + fmt::Display::fmt( + match *self { + NoCache => "no-cache", + NoStore => "no-store", + NoTransform => "no-transform", + OnlyIfCached => "only-if-cached", + + MaxAge(secs) => return write!(f, "max-age={}", secs), + MaxStale(secs) => return write!(f, "max-stale={}", secs), + MinFresh(secs) => return write!(f, "min-fresh={}", secs), + + MustRevalidate => "must-revalidate", + Public => "public", + Private => "private", + ProxyRevalidate => "proxy-revalidate", + SMaxAge(secs) => return write!(f, "s-maxage={}", secs), + + Extension(ref name, None) => &name[..], + Extension(ref name, Some(ref arg)) => { + return write!(f, "{}={}", name, arg); + } + }, + f, + ) + } +} + +impl FromStr for CacheDirective { + type Err = Option<::Err>; + fn from_str(s: &str) -> Result::Err>> { + use self::CacheDirective::*; + match s { + "no-cache" => Ok(NoCache), + "no-store" => Ok(NoStore), + "no-transform" => Ok(NoTransform), + "only-if-cached" => Ok(OnlyIfCached), + "must-revalidate" => Ok(MustRevalidate), + "public" => Ok(Public), + "private" => Ok(Private), + "proxy-revalidate" => Ok(ProxyRevalidate), + "" => Err(None), + _ => match s.find('=') { + Some(idx) if idx + 1 < s.len() => { + match (&s[..idx], (&s[idx + 1..]).trim_matches('"')) { + ("max-age", secs) => secs.parse().map(MaxAge).map_err(Some), + ("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some), + ("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some), + ("s-maxage", secs) => secs.parse().map(SMaxAge).map_err(Some), + (left, right) => { + Ok(Extension(left.to_owned(), Some(right.to_owned()))) + } + } + } + Some(_) => Err(None), + None => Ok(Extension(s.to_owned(), None)), + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::header::Header; + use crate::test::TestRequest; + + #[test] + fn test_parse_multiple_headers() { + let req = TestRequest::with_header(header::CACHE_CONTROL, "no-cache, private") + .finish(); + let cache = Header::parse(&req); + assert_eq!( + cache.ok(), + Some(CacheControl(vec![ + CacheDirective::NoCache, + CacheDirective::Private, + ])) + ) + } + + #[test] + fn test_parse_argument() { + let req = + TestRequest::with_header(header::CACHE_CONTROL, "max-age=100, private") + .finish(); + let cache = Header::parse(&req); + assert_eq!( + cache.ok(), + Some(CacheControl(vec![ + CacheDirective::MaxAge(100), + CacheDirective::Private, + ])) + ) + } + + #[test] + fn test_parse_quote_form() { + let req = + TestRequest::with_header(header::CACHE_CONTROL, "max-age=\"200\"").finish(); + let cache = Header::parse(&req); + assert_eq!( + cache.ok(), + Some(CacheControl(vec![CacheDirective::MaxAge(200)])) + ) + } + + #[test] + fn test_parse_extension() { + let req = + TestRequest::with_header(header::CACHE_CONTROL, "foo, bar=baz").finish(); + let cache = Header::parse(&req); + assert_eq!( + cache.ok(), + Some(CacheControl(vec![ + CacheDirective::Extension("foo".to_owned(), None), + CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned())), + ])) + ) + } + + #[test] + fn test_parse_bad_syntax() { + let req = TestRequest::with_header(header::CACHE_CONTROL, "foo=").finish(); + let cache: Result = Header::parse(&req); + assert_eq!(cache.ok(), None) + } +} diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs new file mode 100644 index 000000000..e04f9c89f --- /dev/null +++ b/src/header/common/content_disposition.rs @@ -0,0 +1,918 @@ +// # References +// +// "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt +// "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt +// "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt +// Browser conformance tests at: http://greenbytes.de/tech/tc2231/ +// IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml + +use lazy_static::lazy_static; +use regex::Regex; +use std::fmt::{self, Write}; + +use crate::header::{self, ExtendedValue, Header, IntoHeaderValue, Writer}; + +/// Split at the index of the first `needle` if it exists or at the end. +fn split_once(haystack: &str, needle: char) -> (&str, &str) { + haystack.find(needle).map_or_else( + || (haystack, ""), + |sc| { + let (first, last) = haystack.split_at(sc); + (first, last.split_at(1).1) + }, + ) +} + +/// Split at the index of the first `needle` if it exists or at the end, trim the right of the +/// first part and the left of the last part. +fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) { + let (first, last) = split_once(haystack, needle); + (first.trim_right(), last.trim_left()) +} + +/// The implied disposition of the content of the HTTP body. +#[derive(Clone, Debug, PartialEq)] +pub enum DispositionType { + /// Inline implies default processing + Inline, + /// Attachment implies that the recipient should prompt the user to save the response locally, + /// rather than process it normally (as per its media type). + Attachment, + /// Used in *multipart/form-data* as defined in + /// [RFC7578](https://tools.ietf.org/html/rfc7578) to carry the field name and the file name. + FormData, + /// Extension type. Should be handled by recipients the same way as Attachment + Ext(String), +} + +impl<'a> From<&'a str> for DispositionType { + fn from(origin: &'a str) -> DispositionType { + if origin.eq_ignore_ascii_case("inline") { + DispositionType::Inline + } else if origin.eq_ignore_ascii_case("attachment") { + DispositionType::Attachment + } else if origin.eq_ignore_ascii_case("form-data") { + DispositionType::FormData + } else { + DispositionType::Ext(origin.to_owned()) + } + } +} + +/// Parameter in [`ContentDisposition`]. +/// +/// # Examples +/// ``` +/// use actix_http::http::header::DispositionParam; +/// +/// let param = DispositionParam::Filename(String::from("sample.txt")); +/// assert!(param.is_filename()); +/// assert_eq!(param.as_filename().unwrap(), "sample.txt"); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub enum DispositionParam { + /// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from + /// the form. + Name(String), + /// A plain file name. + Filename(String), + /// An extended file name. It must not exist for `ContentType::Formdata` according to + /// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2). + FilenameExt(ExtendedValue), + /// An unrecognized regular parameter as defined in + /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *reg-parameter*, in + /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *token "=" value*. Recipients should + /// ignore unrecognizable parameters. + Unknown(String, String), + /// An unrecognized extended paramater as defined in + /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *ext-parameter*, in + /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *ext-token "=" ext-value*. The single + /// trailling asterisk is not included. Recipients should ignore unrecognizable parameters. + UnknownExt(String, ExtendedValue), +} + +impl DispositionParam { + /// Returns `true` if the paramater is [`Name`](DispositionParam::Name). + #[inline] + pub fn is_name(&self) -> bool { + self.as_name().is_some() + } + + /// Returns `true` if the paramater is [`Filename`](DispositionParam::Filename). + #[inline] + pub fn is_filename(&self) -> bool { + self.as_filename().is_some() + } + + /// Returns `true` if the paramater is [`FilenameExt`](DispositionParam::FilenameExt). + #[inline] + pub fn is_filename_ext(&self) -> bool { + self.as_filename_ext().is_some() + } + + /// Returns `true` if the paramater is [`Unknown`](DispositionParam::Unknown) and the `name` + #[inline] + /// matches. + pub fn is_unknown>(&self, name: T) -> bool { + self.as_unknown(name).is_some() + } + + /// Returns `true` if the paramater is [`UnknownExt`](DispositionParam::UnknownExt) and the + /// `name` matches. + #[inline] + pub fn is_unknown_ext>(&self, name: T) -> bool { + self.as_unknown_ext(name).is_some() + } + + /// Returns the name if applicable. + #[inline] + pub fn as_name(&self) -> Option<&str> { + match self { + DispositionParam::Name(ref name) => Some(name.as_str()), + _ => None, + } + } + + /// Returns the filename if applicable. + #[inline] + pub fn as_filename(&self) -> Option<&str> { + match self { + DispositionParam::Filename(ref filename) => Some(filename.as_str()), + _ => None, + } + } + + /// Returns the filename* if applicable. + #[inline] + pub fn as_filename_ext(&self) -> Option<&ExtendedValue> { + match self { + DispositionParam::FilenameExt(ref value) => Some(value), + _ => None, + } + } + + /// Returns the value of the unrecognized regular parameter if it is + /// [`Unknown`](DispositionParam::Unknown) and the `name` matches. + #[inline] + pub fn as_unknown>(&self, name: T) -> Option<&str> { + match self { + DispositionParam::Unknown(ref ext_name, ref value) + if ext_name.eq_ignore_ascii_case(name.as_ref()) => + { + Some(value.as_str()) + } + _ => None, + } + } + + /// Returns the value of the unrecognized extended parameter if it is + /// [`Unknown`](DispositionParam::Unknown) and the `name` matches. + #[inline] + pub fn as_unknown_ext>(&self, name: T) -> Option<&ExtendedValue> { + match self { + DispositionParam::UnknownExt(ref ext_name, ref value) + if ext_name.eq_ignore_ascii_case(name.as_ref()) => + { + Some(value) + } + _ => None, + } + } +} + +/// A *Content-Disposition* header. It is compatible to be used either as +/// [a response header for the main body](https://mdn.io/Content-Disposition#As_a_response_header_for_the_main_body) +/// as (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266), or as +/// [a header for a multipart body](https://mdn.io/Content-Disposition#As_a_header_for_a_multipart_body) +/// as (re)defined in [RFC7587](https://tools.ietf.org/html/rfc7578). +/// +/// In a regular HTTP response, the *Content-Disposition* response header is a header indicating if +/// the content is expected to be displayed *inline* in the browser, that is, as a Web page or as +/// part of a Web page, or as an attachment, that is downloaded and saved locally, and also can be +/// used to attach additional metadata, such as the filename to use when saving the response payload +/// locally. +/// +/// In a *multipart/form-data* body, the HTTP *Content-Disposition* general header is a header that +/// can be used on the subpart of a multipart body to give information about the field it applies to. +/// The subpart is delimited by the boundary defined in the *Content-Type* header. Used on the body +/// itself, *Content-Disposition* has no effect. +/// +/// # ABNF + +/// ```text +/// content-disposition = "Content-Disposition" ":" +/// disposition-type *( ";" disposition-parm ) +/// +/// disposition-type = "inline" | "attachment" | disp-ext-type +/// ; case-insensitive +/// +/// disp-ext-type = token +/// +/// disposition-parm = filename-parm | disp-ext-parm +/// +/// filename-parm = "filename" "=" value +/// | "filename*" "=" ext-value +/// +/// disp-ext-parm = token "=" value +/// | ext-token "=" ext-value +/// +/// ext-token = +/// ``` +/// +/// **Note**: filename* [must not](https://tools.ietf.org/html/rfc7578#section-4.2) be used within +/// *multipart/form-data*. +/// +/// # Example +/// +/// ``` +/// use actix_http::http::header::{ +/// Charset, ContentDisposition, DispositionParam, DispositionType, +/// ExtendedValue, +/// }; +/// +/// let cd1 = ContentDisposition { +/// disposition: DispositionType::Attachment, +/// parameters: vec![DispositionParam::FilenameExt(ExtendedValue { +/// charset: Charset::Iso_8859_1, // The character set for the bytes of the filename +/// language_tag: None, // The optional language tag (see `language-tag` crate) +/// value: b"\xa9 Copyright 1989.txt".to_vec(), // the actual bytes of the filename +/// })], +/// }; +/// assert!(cd1.is_attachment()); +/// assert!(cd1.get_filename_ext().is_some()); +/// +/// let cd2 = ContentDisposition { +/// disposition: DispositionType::FormData, +/// parameters: vec![ +/// DispositionParam::Name(String::from("file")), +/// DispositionParam::Filename(String::from("bill.odt")), +/// ], +/// }; +/// assert_eq!(cd2.get_name(), Some("file")); // field name +/// assert_eq!(cd2.get_filename(), Some("bill.odt")); +/// ``` +/// +/// # WARN +/// If "filename" parameter is supplied, do not use the file name blindly, check and possibly +/// change to match local file system conventions if applicable, and do not use directory path +/// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3) +/// . +#[derive(Clone, Debug, PartialEq)] +pub struct ContentDisposition { + /// The disposition type + pub disposition: DispositionType, + /// Disposition parameters + pub parameters: Vec, +} + +impl ContentDisposition { + /// Parse a raw Content-Disposition header value. + pub fn from_raw(hv: &header::HeaderValue) -> Result { + // `header::from_one_raw_str` invokes `hv.to_str` which assumes `hv` contains only visible + // ASCII characters. So `hv.as_bytes` is necessary here. + let hv = String::from_utf8(hv.as_bytes().to_vec()) + .map_err(|_| crate::error::ParseError::Header)?; + let (disp_type, mut left) = split_once_and_trim(hv.as_str().trim(), ';'); + if disp_type.is_empty() { + return Err(crate::error::ParseError::Header); + } + let mut cd = ContentDisposition { + disposition: disp_type.into(), + parameters: Vec::new(), + }; + + while !left.is_empty() { + let (param_name, new_left) = split_once_and_trim(left, '='); + if param_name.is_empty() || param_name == "*" || new_left.is_empty() { + return Err(crate::error::ParseError::Header); + } + left = new_left; + if param_name.ends_with('*') { + // extended parameters + let param_name = ¶m_name[..param_name.len() - 1]; // trim asterisk + let (ext_value, new_left) = split_once_and_trim(left, ';'); + left = new_left; + let ext_value = header::parse_extended_value(ext_value)?; + + let param = if param_name.eq_ignore_ascii_case("filename") { + DispositionParam::FilenameExt(ext_value) + } else { + DispositionParam::UnknownExt(param_name.to_owned(), ext_value) + }; + cd.parameters.push(param); + } else { + // regular parameters + let value = if left.starts_with('\"') { + // quoted-string: defined in RFC6266 -> RFC2616 Section 3.6 + let mut escaping = false; + let mut quoted_string = vec![]; + let mut end = None; + // search for closing quote + for (i, &c) in left.as_bytes().iter().skip(1).enumerate() { + if escaping { + escaping = false; + quoted_string.push(c); + } else if c == 0x5c { + // backslash + escaping = true; + } else if c == 0x22 { + // double quote + end = Some(i + 1); // cuz skipped 1 for the leading quote + break; + } else { + quoted_string.push(c); + } + } + left = &left[end.ok_or(crate::error::ParseError::Header)? + 1..]; + left = split_once(left, ';').1.trim_left(); + // In fact, it should not be Err if the above code is correct. + String::from_utf8(quoted_string) + .map_err(|_| crate::error::ParseError::Header)? + } else { + // token: won't contains semicolon according to RFC 2616 Section 2.2 + let (token, new_left) = split_once_and_trim(left, ';'); + left = new_left; + token.to_owned() + }; + if value.is_empty() { + return Err(crate::error::ParseError::Header); + } + + let param = if param_name.eq_ignore_ascii_case("name") { + DispositionParam::Name(value) + } else if param_name.eq_ignore_ascii_case("filename") { + DispositionParam::Filename(value) + } else { + DispositionParam::Unknown(param_name.to_owned(), value) + }; + cd.parameters.push(param); + } + } + + Ok(cd) + } + + /// Returns `true` if it is [`Inline`](DispositionType::Inline). + pub fn is_inline(&self) -> bool { + match self.disposition { + DispositionType::Inline => true, + _ => false, + } + } + + /// Returns `true` if it is [`Attachment`](DispositionType::Attachment). + pub fn is_attachment(&self) -> bool { + match self.disposition { + DispositionType::Attachment => true, + _ => false, + } + } + + /// Returns `true` if it is [`FormData`](DispositionType::FormData). + pub fn is_form_data(&self) -> bool { + match self.disposition { + DispositionType::FormData => true, + _ => false, + } + } + + /// Returns `true` if it is [`Ext`](DispositionType::Ext) and the `disp_type` matches. + pub fn is_ext>(&self, disp_type: T) -> bool { + match self.disposition { + DispositionType::Ext(ref t) + if t.eq_ignore_ascii_case(disp_type.as_ref()) => + { + true + } + _ => false, + } + } + + /// Return the value of *name* if exists. + pub fn get_name(&self) -> Option<&str> { + self.parameters.iter().filter_map(|p| p.as_name()).nth(0) + } + + /// Return the value of *filename* if exists. + pub fn get_filename(&self) -> Option<&str> { + self.parameters + .iter() + .filter_map(|p| p.as_filename()) + .nth(0) + } + + /// Return the value of *filename\** if exists. + pub fn get_filename_ext(&self) -> Option<&ExtendedValue> { + self.parameters + .iter() + .filter_map(|p| p.as_filename_ext()) + .nth(0) + } + + /// Return the value of the parameter which the `name` matches. + pub fn get_unknown>(&self, name: T) -> Option<&str> { + let name = name.as_ref(); + self.parameters + .iter() + .filter_map(|p| p.as_unknown(name)) + .nth(0) + } + + /// Return the value of the extended parameter which the `name` matches. + pub fn get_unknown_ext>(&self, name: T) -> Option<&ExtendedValue> { + let name = name.as_ref(); + self.parameters + .iter() + .filter_map(|p| p.as_unknown_ext(name)) + .nth(0) + } +} + +impl IntoHeaderValue for ContentDisposition { + type Error = header::InvalidHeaderValueBytes; + + fn try_into(self) -> Result { + let mut writer = Writer::new(); + let _ = write!(&mut writer, "{}", self); + header::HeaderValue::from_shared(writer.take()) + } +} + +impl Header for ContentDisposition { + fn name() -> header::HeaderName { + header::CONTENT_DISPOSITION + } + + fn parse(msg: &T) -> Result { + if let Some(h) = msg.headers().get(Self::name()) { + Self::from_raw(&h) + } else { + Err(crate::error::ParseError::Header) + } + } +} + +impl fmt::Display for DispositionType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DispositionType::Inline => write!(f, "inline"), + DispositionType::Attachment => write!(f, "attachment"), + DispositionType::FormData => write!(f, "form-data"), + DispositionType::Ext(ref s) => write!(f, "{}", s), + } + } +} + +impl fmt::Display for DispositionParam { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // All ASCII control charaters (0-30, 127) excepting horizontal tab, double quote, and + // backslash should be escaped in quoted-string (i.e. "foobar"). + // Ref: RFC6266 S4.1 -> RFC2616 S2.2; RFC 7578 S4.2 -> RFC2183 S2 -> ... . + lazy_static! { + static ref RE: Regex = Regex::new("[\x01-\x08\x10\x1F\x7F\"\\\\]").unwrap(); + } + match self { + DispositionParam::Name(ref value) => write!(f, "name={}", value), + DispositionParam::Filename(ref value) => { + write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref()) + } + DispositionParam::Unknown(ref name, ref value) => write!( + f, + "{}=\"{}\"", + name, + &RE.replace_all(value, "\\$0").as_ref() + ), + DispositionParam::FilenameExt(ref ext_value) => { + write!(f, "filename*={}", ext_value) + } + DispositionParam::UnknownExt(ref name, ref ext_value) => { + write!(f, "{}*={}", name, ext_value) + } + } + } +} + +impl fmt::Display for ContentDisposition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.disposition)?; + self.parameters + .iter() + .map(|param| write!(f, "; {}", param)) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::{ContentDisposition, DispositionParam, DispositionType}; + use crate::header::shared::Charset; + use crate::header::{ExtendedValue, HeaderValue}; + + #[test] + fn test_from_raw_basic() { + assert!(ContentDisposition::from_raw(&HeaderValue::from_static("")).is_err()); + + let a = HeaderValue::from_static( + "form-data; dummy=3; name=upload; filename=\"sample.png\"", + ); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::FormData, + parameters: vec![ + DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), + DispositionParam::Name("upload".to_owned()), + DispositionParam::Filename("sample.png".to_owned()), + ], + }; + assert_eq!(a, b); + + let a = HeaderValue::from_static("attachment; filename=\"image.jpg\""); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::Attachment, + parameters: vec![DispositionParam::Filename("image.jpg".to_owned())], + }; + assert_eq!(a, b); + + let a = HeaderValue::from_static("inline; filename=image.jpg"); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::Inline, + parameters: vec![DispositionParam::Filename("image.jpg".to_owned())], + }; + assert_eq!(a, b); + + let a = HeaderValue::from_static( + "attachment; creation-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"", + ); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::Attachment, + parameters: vec![DispositionParam::Unknown( + String::from("creation-date"), + "Wed, 12 Feb 1997 16:29:51 -0500".to_owned(), + )], + }; + assert_eq!(a, b); + } + + #[test] + fn test_from_raw_extended() { + let a = HeaderValue::from_static( + "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates", + ); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::Attachment, + parameters: vec![DispositionParam::FilenameExt(ExtendedValue { + charset: Charset::Ext(String::from("UTF-8")), + language_tag: None, + value: vec![ + 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, + b'r', b'a', b't', b'e', b's', + ], + })], + }; + assert_eq!(a, b); + + let a = HeaderValue::from_static( + "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates", + ); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::Attachment, + parameters: vec![DispositionParam::FilenameExt(ExtendedValue { + charset: Charset::Ext(String::from("UTF-8")), + language_tag: None, + value: vec![ + 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, + b'r', b'a', b't', b'e', b's', + ], + })], + }; + assert_eq!(a, b); + } + + #[test] + fn test_from_raw_extra_whitespace() { + let a = HeaderValue::from_static( + "form-data ; du-mmy= 3 ; name =upload ; filename = \"sample.png\" ; ", + ); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::FormData, + parameters: vec![ + DispositionParam::Unknown("du-mmy".to_owned(), "3".to_owned()), + DispositionParam::Name("upload".to_owned()), + DispositionParam::Filename("sample.png".to_owned()), + ], + }; + assert_eq!(a, b); + } + + #[test] + fn test_from_raw_unordered() { + let a = HeaderValue::from_static( + "form-data; dummy=3; filename=\"sample.png\" ; name=upload;", + // Actually, a trailling semolocon is not compliant. But it is fine to accept. + ); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::FormData, + parameters: vec![ + DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), + DispositionParam::Filename("sample.png".to_owned()), + DispositionParam::Name("upload".to_owned()), + ], + }; + assert_eq!(a, b); + + let a = HeaderValue::from_str( + "attachment; filename*=iso-8859-1''foo-%E4.html; filename=\"foo-ä.html\"", + ) + .unwrap(); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::Attachment, + parameters: vec![ + DispositionParam::FilenameExt(ExtendedValue { + charset: Charset::Iso_8859_1, + language_tag: None, + value: b"foo-\xe4.html".to_vec(), + }), + DispositionParam::Filename("foo-ä.html".to_owned()), + ], + }; + assert_eq!(a, b); + } + + #[test] + fn test_from_raw_only_disp() { + let a = ContentDisposition::from_raw(&HeaderValue::from_static("attachment")) + .unwrap(); + let b = ContentDisposition { + disposition: DispositionType::Attachment, + parameters: vec![], + }; + assert_eq!(a, b); + + let a = + ContentDisposition::from_raw(&HeaderValue::from_static("inline ;")).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::Inline, + parameters: vec![], + }; + assert_eq!(a, b); + + let a = ContentDisposition::from_raw(&HeaderValue::from_static( + "unknown-disp-param", + )) + .unwrap(); + let b = ContentDisposition { + disposition: DispositionType::Ext(String::from("unknown-disp-param")), + parameters: vec![], + }; + assert_eq!(a, b); + } + + #[test] + fn from_raw_with_mixed_case() { + let a = HeaderValue::from_str( + "InLInE; fIlenAME*=iso-8859-1''foo-%E4.html; filEName=\"foo-ä.html\"", + ) + .unwrap(); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::Inline, + parameters: vec![ + DispositionParam::FilenameExt(ExtendedValue { + charset: Charset::Iso_8859_1, + language_tag: None, + value: b"foo-\xe4.html".to_vec(), + }), + DispositionParam::Filename("foo-ä.html".to_owned()), + ], + }; + assert_eq!(a, b); + } + + #[test] + fn from_raw_with_unicode() { + /* RFC7578 Section 4.2: + Some commonly deployed systems use multipart/form-data with file names directly encoded + including octets outside the US-ASCII range. The encoding used for the file names is + typically UTF-8, although HTML forms will use the charset associated with the form. + + Mainstream browsers like Firefox (gecko) and Chrome use UTF-8 directly as above. + (And now, only UTF-8 is handled by this implementation.) + */ + let a = + HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"") + .unwrap(); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::FormData, + parameters: vec![ + DispositionParam::Name(String::from("upload")), + DispositionParam::Filename(String::from("文件.webp")), + ], + }; + assert_eq!(a, b); + + let a = + HeaderValue::from_str("form-data; name=upload; filename=\"余固知謇謇之為患兮,å¿è€Œä¸èƒ½èˆä¹Ÿ.pptx\"").unwrap(); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::FormData, + parameters: vec![ + DispositionParam::Name(String::from("upload")), + DispositionParam::Filename(String::from( + "余固知謇謇之為患兮,å¿è€Œä¸èƒ½èˆä¹Ÿ.pptx", + )), + ], + }; + assert_eq!(a, b); + } + + #[test] + fn test_from_raw_escape() { + let a = HeaderValue::from_static( + "form-data; dummy=3; name=upload; filename=\"s\\amp\\\"le.png\"", + ); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::FormData, + parameters: vec![ + DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), + DispositionParam::Name("upload".to_owned()), + DispositionParam::Filename( + ['s', 'a', 'm', 'p', '\"', 'l', 'e', '.', 'p', 'n', 'g'] + .iter() + .collect(), + ), + ], + }; + assert_eq!(a, b); + } + + #[test] + fn test_from_raw_semicolon() { + let a = + HeaderValue::from_static("form-data; filename=\"A semicolon here;.pdf\""); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::FormData, + parameters: vec![DispositionParam::Filename(String::from( + "A semicolon here;.pdf", + ))], + }; + assert_eq!(a, b); + } + + #[test] + fn test_from_raw_uncessary_percent_decode() { + let a = HeaderValue::from_static( + "form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"", // Should not be decoded! + ); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::FormData, + parameters: vec![ + DispositionParam::Name("photo".to_owned()), + DispositionParam::Filename(String::from("%74%65%73%74%2e%70%6e%67")), + ], + }; + assert_eq!(a, b); + + let a = HeaderValue::from_static( + "form-data; name=photo; filename=\"%74%65%73%74.png\"", + ); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::FormData, + parameters: vec![ + DispositionParam::Name("photo".to_owned()), + DispositionParam::Filename(String::from("%74%65%73%74.png")), + ], + }; + assert_eq!(a, b); + } + + #[test] + fn test_from_raw_param_value_missing() { + let a = HeaderValue::from_static("form-data; name=upload ; filename="); + assert!(ContentDisposition::from_raw(&a).is_err()); + + let a = HeaderValue::from_static("attachment; dummy=; filename=invoice.pdf"); + assert!(ContentDisposition::from_raw(&a).is_err()); + + let a = HeaderValue::from_static("inline; filename= "); + assert!(ContentDisposition::from_raw(&a).is_err()); + } + + #[test] + fn test_from_raw_param_name_missing() { + let a = HeaderValue::from_static("inline; =\"test.txt\""); + assert!(ContentDisposition::from_raw(&a).is_err()); + + let a = HeaderValue::from_static("inline; =diary.odt"); + assert!(ContentDisposition::from_raw(&a).is_err()); + + let a = HeaderValue::from_static("inline; ="); + assert!(ContentDisposition::from_raw(&a).is_err()); + } + + #[test] + fn test_display_extended() { + let as_string = + "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates"; + let a = HeaderValue::from_static(as_string); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let display_rendered = format!("{}", a); + assert_eq!(as_string, display_rendered); + + let a = HeaderValue::from_static("attachment; filename=colourful.csv"); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let display_rendered = format!("{}", a); + assert_eq!( + "attachment; filename=\"colourful.csv\"".to_owned(), + display_rendered + ); + } + + #[test] + fn test_display_quote() { + let as_string = "form-data; name=upload; filename=\"Quote\\\"here.png\""; + as_string + .find(['\\', '\"'].iter().collect::().as_str()) + .unwrap(); // ensure `\"` is there + let a = HeaderValue::from_static(as_string); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let display_rendered = format!("{}", a); + assert_eq!(as_string, display_rendered); + } + + #[test] + fn test_display_space_tab() { + let as_string = "form-data; name=upload; filename=\"Space here.png\""; + let a = HeaderValue::from_static(as_string); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let display_rendered = format!("{}", a); + assert_eq!(as_string, display_rendered); + + let a: ContentDisposition = ContentDisposition { + disposition: DispositionType::Inline, + parameters: vec![DispositionParam::Filename(String::from("Tab\there.png"))], + }; + let display_rendered = format!("{}", a); + assert_eq!("inline; filename=\"Tab\x09here.png\"", display_rendered); + } + + #[test] + fn test_display_control_characters() { + /* let a = "attachment; filename=\"carriage\rreturn.png\""; + let a = HeaderValue::from_static(a); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); + let display_rendered = format!("{}", a); + assert_eq!( + "attachment; filename=\"carriage\\\rreturn.png\"", + display_rendered + );*/ + // No way to create a HeaderValue containing a carriage return. + + let a: ContentDisposition = ContentDisposition { + disposition: DispositionType::Inline, + parameters: vec![DispositionParam::Filename(String::from("bell\x07.png"))], + }; + let display_rendered = format!("{}", a); + assert_eq!("inline; filename=\"bell\\\x07.png\"", display_rendered); + } + + #[test] + fn test_param_methods() { + let param = DispositionParam::Filename(String::from("sample.txt")); + assert!(param.is_filename()); + assert_eq!(param.as_filename().unwrap(), "sample.txt"); + + let param = DispositionParam::Unknown(String::from("foo"), String::from("bar")); + assert!(param.is_unknown("foo")); + assert_eq!(param.as_unknown("fOo"), Some("bar")); + } + + #[test] + fn test_disposition_methods() { + let cd = ContentDisposition { + disposition: DispositionType::FormData, + parameters: vec![ + DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), + DispositionParam::Name("upload".to_owned()), + DispositionParam::Filename("sample.png".to_owned()), + ], + }; + assert_eq!(cd.get_name(), Some("upload")); + assert_eq!(cd.get_unknown("dummy"), Some("3")); + assert_eq!(cd.get_filename(), Some("sample.png")); + assert_eq!(cd.get_unknown_ext("dummy"), None); + assert_eq!(cd.get_unknown("duMMy"), Some("3")); + } +} diff --git a/src/header/common/content_language.rs b/src/header/common/content_language.rs new file mode 100644 index 000000000..838981a39 --- /dev/null +++ b/src/header/common/content_language.rs @@ -0,0 +1,65 @@ +use crate::header::{QualityItem, CONTENT_LANGUAGE}; +use language_tags::LanguageTag; + +header! { + /// `Content-Language` header, defined in + /// [RFC7231](https://tools.ietf.org/html/rfc7231#section-3.1.3.2) + /// + /// The `Content-Language` header field describes the natural language(s) + /// of the intended audience for the representation. Note that this + /// might not be equivalent to all the languages used within the + /// representation. + /// + /// # ABNF + /// + /// ```text + /// Content-Language = 1#language-tag + /// ``` + /// + /// # Example values + /// + /// * `da` + /// * `mi, en` + /// + /// # Examples + /// + /// ```rust + /// # extern crate actix_http; + /// # #[macro_use] extern crate language_tags; + /// use actix_http::Response; + /// # use actix_http::http::header::{ContentLanguage, qitem}; + /// # + /// # fn main() { + /// let mut builder = Response::Ok(); + /// builder.set( + /// ContentLanguage(vec![ + /// qitem(langtag!(en)), + /// ]) + /// ); + /// # } + /// ``` + /// + /// ```rust + /// # extern crate actix_http; + /// # #[macro_use] extern crate language_tags; + /// use actix_http::Response; + /// # use actix_http::http::header::{ContentLanguage, qitem}; + /// # + /// # fn main() { + /// + /// let mut builder = Response::Ok(); + /// builder.set( + /// ContentLanguage(vec![ + /// qitem(langtag!(da)), + /// qitem(langtag!(en;;;GB)), + /// ]) + /// ); + /// # } + /// ``` + (ContentLanguage, CONTENT_LANGUAGE) => (QualityItem)+ + + test_content_language { + test_header!(test1, vec![b"da"]); + test_header!(test2, vec![b"mi, en"]); + } +} diff --git a/src/header/common/content_range.rs b/src/header/common/content_range.rs new file mode 100644 index 000000000..cc7f27548 --- /dev/null +++ b/src/header/common/content_range.rs @@ -0,0 +1,208 @@ +use std::fmt::{self, Display, Write}; +use std::str::FromStr; + +use crate::error::ParseError; +use crate::header::{ + HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, Writer, CONTENT_RANGE, +}; + +header! { + /// `Content-Range` header, defined in + /// [RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2) + (ContentRange, CONTENT_RANGE) => [ContentRangeSpec] + + test_content_range { + test_header!(test_bytes, + vec![b"bytes 0-499/500"], + Some(ContentRange(ContentRangeSpec::Bytes { + range: Some((0, 499)), + instance_length: Some(500) + }))); + + test_header!(test_bytes_unknown_len, + vec![b"bytes 0-499/*"], + Some(ContentRange(ContentRangeSpec::Bytes { + range: Some((0, 499)), + instance_length: None + }))); + + test_header!(test_bytes_unknown_range, + vec![b"bytes */500"], + Some(ContentRange(ContentRangeSpec::Bytes { + range: None, + instance_length: Some(500) + }))); + + test_header!(test_unregistered, + vec![b"seconds 1-2"], + Some(ContentRange(ContentRangeSpec::Unregistered { + unit: "seconds".to_owned(), + resp: "1-2".to_owned() + }))); + + test_header!(test_no_len, + vec![b"bytes 0-499"], + None::); + + test_header!(test_only_unit, + vec![b"bytes"], + None::); + + test_header!(test_end_less_than_start, + vec![b"bytes 499-0/500"], + None::); + + test_header!(test_blank, + vec![b""], + None::); + + test_header!(test_bytes_many_spaces, + vec![b"bytes 1-2/500 3"], + None::); + + test_header!(test_bytes_many_slashes, + vec![b"bytes 1-2/500/600"], + None::); + + test_header!(test_bytes_many_dashes, + vec![b"bytes 1-2-3/500"], + None::); + + } +} + +/// Content-Range, described in [RFC7233](https://tools.ietf.org/html/rfc7233#section-4.2) +/// +/// # ABNF +/// +/// ```text +/// Content-Range = byte-content-range +/// / other-content-range +/// +/// byte-content-range = bytes-unit SP +/// ( byte-range-resp / unsatisfied-range ) +/// +/// byte-range-resp = byte-range "/" ( complete-length / "*" ) +/// byte-range = first-byte-pos "-" last-byte-pos +/// unsatisfied-range = "*/" complete-length +/// +/// complete-length = 1*DIGIT +/// +/// other-content-range = other-range-unit SP other-range-resp +/// other-range-resp = *CHAR +/// ``` +#[derive(PartialEq, Clone, Debug)] +pub enum ContentRangeSpec { + /// Byte range + Bytes { + /// First and last bytes of the range, omitted if request could not be + /// satisfied + range: Option<(u64, u64)>, + + /// Total length of the instance, can be omitted if unknown + instance_length: Option, + }, + + /// Custom range, with unit not registered at IANA + Unregistered { + /// other-range-unit + unit: String, + + /// other-range-resp + resp: String, + }, +} + +fn split_in_two(s: &str, separator: char) -> Option<(&str, &str)> { + let mut iter = s.splitn(2, separator); + match (iter.next(), iter.next()) { + (Some(a), Some(b)) => Some((a, b)), + _ => None, + } +} + +impl FromStr for ContentRangeSpec { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let res = match split_in_two(s, ' ') { + Some(("bytes", resp)) => { + let (range, instance_length) = + split_in_two(resp, '/').ok_or(ParseError::Header)?; + + let instance_length = if instance_length == "*" { + None + } else { + Some(instance_length.parse().map_err(|_| ParseError::Header)?) + }; + + let range = if range == "*" { + None + } else { + let (first_byte, last_byte) = + split_in_two(range, '-').ok_or(ParseError::Header)?; + let first_byte = + first_byte.parse().map_err(|_| ParseError::Header)?; + let last_byte = last_byte.parse().map_err(|_| ParseError::Header)?; + if last_byte < first_byte { + return Err(ParseError::Header); + } + Some((first_byte, last_byte)) + }; + + ContentRangeSpec::Bytes { + range, + instance_length, + } + } + Some((unit, resp)) => ContentRangeSpec::Unregistered { + unit: unit.to_owned(), + resp: resp.to_owned(), + }, + _ => return Err(ParseError::Header), + }; + Ok(res) + } +} + +impl Display for ContentRangeSpec { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ContentRangeSpec::Bytes { + range, + instance_length, + } => { + f.write_str("bytes ")?; + match range { + Some((first_byte, last_byte)) => { + write!(f, "{}-{}", first_byte, last_byte)?; + } + None => { + f.write_str("*")?; + } + }; + f.write_str("/")?; + if let Some(v) = instance_length { + write!(f, "{}", v) + } else { + f.write_str("*") + } + } + ContentRangeSpec::Unregistered { ref unit, ref resp } => { + f.write_str(unit)?; + f.write_str(" ")?; + f.write_str(resp) + } + } + } +} + +impl IntoHeaderValue for ContentRangeSpec { + type Error = InvalidHeaderValueBytes; + + fn try_into(self) -> Result { + let mut writer = Writer::new(); + let _ = write!(&mut writer, "{}", self); + HeaderValue::from_shared(writer.take()) + } +} diff --git a/src/header/common/content_type.rs b/src/header/common/content_type.rs new file mode 100644 index 000000000..a0baa5637 --- /dev/null +++ b/src/header/common/content_type.rs @@ -0,0 +1,122 @@ +use crate::header::CONTENT_TYPE; +use mime::Mime; + +header! { + /// `Content-Type` header, defined in + /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-3.1.1.5) + /// + /// The `Content-Type` header field indicates the media type of the + /// associated representation: either the representation enclosed in the + /// message payload or the selected representation, as determined by the + /// message semantics. The indicated media type defines both the data + /// format and how that data is intended to be processed by a recipient, + /// within the scope of the received message semantics, after any content + /// codings indicated by Content-Encoding are decoded. + /// + /// Although the `mime` crate allows the mime options to be any slice, this crate + /// forces the use of Vec. This is to make sure the same header can't have more than 1 type. If + /// this is an issue, it's possible to implement `Header` on a custom struct. + /// + /// # ABNF + /// + /// ```text + /// Content-Type = media-type + /// ``` + /// + /// # Example values + /// + /// * `text/html; charset=utf-8` + /// * `application/json` + /// + /// # Examples + /// + /// ```rust + /// use actix_http::Response; + /// use actix_http::http::header::ContentType; + /// + /// # fn main() { + /// let mut builder = Response::Ok(); + /// builder.set( + /// ContentType::json() + /// ); + /// # } + /// ``` + /// + /// ```rust + /// # extern crate mime; + /// # extern crate actix_http; + /// use mime::TEXT_HTML; + /// use actix_http::Response; + /// use actix_http::http::header::ContentType; + /// + /// # fn main() { + /// let mut builder = Response::Ok(); + /// builder.set( + /// ContentType(TEXT_HTML) + /// ); + /// # } + /// ``` + (ContentType, CONTENT_TYPE) => [Mime] + + test_content_type { + test_header!( + test1, + vec![b"text/html"], + Some(HeaderField(mime::TEXT_HTML))); + } +} + +impl ContentType { + /// A constructor to easily create a `Content-Type: application/json` + /// header. + #[inline] + pub fn json() -> ContentType { + ContentType(mime::APPLICATION_JSON) + } + + /// A constructor to easily create a `Content-Type: text/plain; + /// charset=utf-8` header. + #[inline] + pub fn plaintext() -> ContentType { + ContentType(mime::TEXT_PLAIN_UTF_8) + } + + /// A constructor to easily create a `Content-Type: text/html` header. + #[inline] + pub fn html() -> ContentType { + ContentType(mime::TEXT_HTML) + } + + /// A constructor to easily create a `Content-Type: text/xml` header. + #[inline] + pub fn xml() -> ContentType { + ContentType(mime::TEXT_XML) + } + + /// A constructor to easily create a `Content-Type: + /// application/www-form-url-encoded` header. + #[inline] + pub fn form_url_encoded() -> ContentType { + ContentType(mime::APPLICATION_WWW_FORM_URLENCODED) + } + /// A constructor to easily create a `Content-Type: image/jpeg` header. + #[inline] + pub fn jpeg() -> ContentType { + ContentType(mime::IMAGE_JPEG) + } + + /// A constructor to easily create a `Content-Type: image/png` header. + #[inline] + pub fn png() -> ContentType { + ContentType(mime::IMAGE_PNG) + } + + /// A constructor to easily create a `Content-Type: + /// application/octet-stream` header. + #[inline] + pub fn octet_stream() -> ContentType { + ContentType(mime::APPLICATION_OCTET_STREAM) + } +} + +impl Eq for ContentType {} diff --git a/src/header/common/date.rs b/src/header/common/date.rs new file mode 100644 index 000000000..784100e8d --- /dev/null +++ b/src/header/common/date.rs @@ -0,0 +1,42 @@ +use crate::header::{HttpDate, DATE}; +use std::time::SystemTime; + +header! { + /// `Date` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.1.1.2) + /// + /// The `Date` header field represents the date and time at which the + /// message was originated. + /// + /// # ABNF + /// + /// ```text + /// Date = HTTP-date + /// ``` + /// + /// # Example values + /// + /// * `Tue, 15 Nov 1994 08:12:31 GMT` + /// + /// # Example + /// + /// ```rust + /// use actix_http::Response; + /// use actix_http::http::header::Date; + /// use std::time::SystemTime; + /// + /// let mut builder = Response::Ok(); + /// builder.set(Date(SystemTime::now().into())); + /// ``` + (Date, DATE) => [HttpDate] + + test_date { + test_header!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]); + } +} + +impl Date { + /// Create a date instance set to the current system time + pub fn now() -> Date { + Date(SystemTime::now().into()) + } +} diff --git a/src/header/common/etag.rs b/src/header/common/etag.rs new file mode 100644 index 000000000..325b91cbf --- /dev/null +++ b/src/header/common/etag.rs @@ -0,0 +1,96 @@ +use crate::header::{EntityTag, ETAG}; + +header! { + /// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3) + /// + /// The `ETag` header field in a response provides the current entity-tag + /// for the selected representation, as determined at the conclusion of + /// handling the request. An entity-tag is an opaque validator for + /// differentiating between multiple representations of the same + /// resource, regardless of whether those multiple representations are + /// due to resource state changes over time, content negotiation + /// resulting in multiple representations being valid at the same time, + /// or both. An entity-tag consists of an opaque quoted string, possibly + /// prefixed by a weakness indicator. + /// + /// # ABNF + /// + /// ```text + /// ETag = entity-tag + /// ``` + /// + /// # Example values + /// + /// * `"xyzzy"` + /// * `W/"xyzzy"` + /// * `""` + /// + /// # Examples + /// + /// ```rust + /// use actix_http::Response; + /// use actix_http::http::header::{ETag, EntityTag}; + /// + /// let mut builder = Response::Ok(); + /// builder.set(ETag(EntityTag::new(false, "xyzzy".to_owned()))); + /// ``` + /// + /// ```rust + /// use actix_http::Response; + /// use actix_http::http::header::{ETag, EntityTag}; + /// + /// let mut builder = Response::Ok(); + /// builder.set(ETag(EntityTag::new(true, "xyzzy".to_owned()))); + /// ``` + (ETag, ETAG) => [EntityTag] + + test_etag { + // From the RFC + test_header!(test1, + vec![b"\"xyzzy\""], + Some(ETag(EntityTag::new(false, "xyzzy".to_owned())))); + test_header!(test2, + vec![b"W/\"xyzzy\""], + Some(ETag(EntityTag::new(true, "xyzzy".to_owned())))); + test_header!(test3, + vec![b"\"\""], + Some(ETag(EntityTag::new(false, "".to_owned())))); + // Own tests + test_header!(test4, + vec![b"\"foobar\""], + Some(ETag(EntityTag::new(false, "foobar".to_owned())))); + test_header!(test5, + vec![b"\"\""], + Some(ETag(EntityTag::new(false, "".to_owned())))); + test_header!(test6, + vec![b"W/\"weak-etag\""], + Some(ETag(EntityTag::new(true, "weak-etag".to_owned())))); + test_header!(test7, + vec![b"W/\"\x65\x62\""], + Some(ETag(EntityTag::new(true, "\u{0065}\u{0062}".to_owned())))); + test_header!(test8, + vec![b"W/\"\""], + Some(ETag(EntityTag::new(true, "".to_owned())))); + test_header!(test9, + vec![b"no-dquotes"], + None::); + test_header!(test10, + vec![b"w/\"the-first-w-is-case-sensitive\""], + None::); + test_header!(test11, + vec![b""], + None::); + test_header!(test12, + vec![b"\"unmatched-dquotes1"], + None::); + test_header!(test13, + vec![b"unmatched-dquotes2\""], + None::); + test_header!(test14, + vec![b"matched-\"dquotes\""], + None::); + test_header!(test15, + vec![b"\""], + None::); + } +} diff --git a/src/header/common/expires.rs b/src/header/common/expires.rs new file mode 100644 index 000000000..3b9a7873d --- /dev/null +++ b/src/header/common/expires.rs @@ -0,0 +1,39 @@ +use crate::header::{HttpDate, EXPIRES}; + +header! { + /// `Expires` header, defined in [RFC7234](http://tools.ietf.org/html/rfc7234#section-5.3) + /// + /// The `Expires` header field gives the date/time after which the + /// response is considered stale. + /// + /// The presence of an Expires field does not imply that the original + /// resource will change or cease to exist at, before, or after that + /// time. + /// + /// # ABNF + /// + /// ```text + /// Expires = HTTP-date + /// ``` + /// + /// # Example values + /// * `Thu, 01 Dec 1994 16:00:00 GMT` + /// + /// # Example + /// + /// ```rust + /// use actix_http::Response; + /// use actix_http::http::header::Expires; + /// use std::time::{SystemTime, Duration}; + /// + /// let mut builder = Response::Ok(); + /// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24); + /// builder.set(Expires(expiration.into())); + /// ``` + (Expires, EXPIRES) => [HttpDate] + + test_expires { + // Test case from RFC + test_header!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]); + } +} diff --git a/src/header/common/if_match.rs b/src/header/common/if_match.rs new file mode 100644 index 000000000..7e0e9a7e0 --- /dev/null +++ b/src/header/common/if_match.rs @@ -0,0 +1,70 @@ +use crate::header::{EntityTag, IF_MATCH}; + +header! { + /// `If-Match` header, defined in + /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.1) + /// + /// The `If-Match` header field makes the request method conditional on + /// the recipient origin server either having at least one current + /// representation of the target resource, when the field-value is "*", + /// or having a current representation of the target resource that has an + /// entity-tag matching a member of the list of entity-tags provided in + /// the field-value. + /// + /// An origin server MUST use the strong comparison function when + /// comparing entity-tags for `If-Match`, since the client + /// intends this precondition to prevent the method from being applied if + /// there have been any changes to the representation data. + /// + /// # ABNF + /// + /// ```text + /// If-Match = "*" / 1#entity-tag + /// ``` + /// + /// # Example values + /// + /// * `"xyzzy"` + /// * "xyzzy", "r2d2xxxx", "c3piozzzz" + /// + /// # Examples + /// + /// ```rust + /// use actix_http::Response; + /// use actix_http::http::header::IfMatch; + /// + /// let mut builder = Response::Ok(); + /// builder.set(IfMatch::Any); + /// ``` + /// + /// ```rust + /// use actix_http::Response; + /// use actix_http::http::header::{IfMatch, EntityTag}; + /// + /// let mut builder = Response::Ok(); + /// builder.set( + /// IfMatch::Items(vec![ + /// EntityTag::new(false, "xyzzy".to_owned()), + /// EntityTag::new(false, "foobar".to_owned()), + /// EntityTag::new(false, "bazquux".to_owned()), + /// ]) + /// ); + /// ``` + (IfMatch, IF_MATCH) => {Any / (EntityTag)+} + + test_if_match { + test_header!( + test1, + vec![b"\"xyzzy\""], + Some(HeaderField::Items( + vec![EntityTag::new(false, "xyzzy".to_owned())]))); + test_header!( + test2, + vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""], + Some(HeaderField::Items( + vec![EntityTag::new(false, "xyzzy".to_owned()), + EntityTag::new(false, "r2d2xxxx".to_owned()), + EntityTag::new(false, "c3piozzzz".to_owned())]))); + test_header!(test3, vec![b"*"], Some(IfMatch::Any)); + } +} diff --git a/src/header/common/if_modified_since.rs b/src/header/common/if_modified_since.rs new file mode 100644 index 000000000..39aca595d --- /dev/null +++ b/src/header/common/if_modified_since.rs @@ -0,0 +1,39 @@ +use crate::header::{HttpDate, IF_MODIFIED_SINCE}; + +header! { + /// `If-Modified-Since` header, defined in + /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.3) + /// + /// The `If-Modified-Since` header field makes a GET or HEAD request + /// method conditional on the selected representation's modification date + /// being more recent than the date provided in the field-value. + /// Transfer of the selected representation's data is avoided if that + /// data has not changed. + /// + /// # ABNF + /// + /// ```text + /// If-Unmodified-Since = HTTP-date + /// ``` + /// + /// # Example values + /// * `Sat, 29 Oct 1994 19:43:31 GMT` + /// + /// # Example + /// + /// ```rust + /// use actix_http::Response; + /// use actix_http::http::header::IfModifiedSince; + /// use std::time::{SystemTime, Duration}; + /// + /// let mut builder = Response::Ok(); + /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); + /// builder.set(IfModifiedSince(modified.into())); + /// ``` + (IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate] + + test_if_modified_since { + // Test case from RFC + test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + } +} diff --git a/src/header/common/if_none_match.rs b/src/header/common/if_none_match.rs new file mode 100644 index 000000000..7f6ccb137 --- /dev/null +++ b/src/header/common/if_none_match.rs @@ -0,0 +1,92 @@ +use crate::header::{EntityTag, IF_NONE_MATCH}; + +header! { + /// `If-None-Match` header, defined in + /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.2) + /// + /// The `If-None-Match` header field makes the request method conditional + /// on a recipient cache or origin server either not having any current + /// representation of the target resource, when the field-value is "*", + /// or having a selected representation with an entity-tag that does not + /// match any of those listed in the field-value. + /// + /// A recipient MUST use the weak comparison function when comparing + /// entity-tags for If-None-Match (Section 2.3.2), since weak entity-tags + /// can be used for cache validation even if there have been changes to + /// the representation data. + /// + /// # ABNF + /// + /// ```text + /// If-None-Match = "*" / 1#entity-tag + /// ``` + /// + /// # Example values + /// + /// * `"xyzzy"` + /// * `W/"xyzzy"` + /// * `"xyzzy", "r2d2xxxx", "c3piozzzz"` + /// * `W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"` + /// * `*` + /// + /// # Examples + /// + /// ```rust + /// use actix_http::Response; + /// use actix_http::http::header::IfNoneMatch; + /// + /// let mut builder = Response::Ok(); + /// builder.set(IfNoneMatch::Any); + /// ``` + /// + /// ```rust + /// use actix_http::Response; + /// use actix_http::http::header::{IfNoneMatch, EntityTag}; + /// + /// let mut builder = Response::Ok(); + /// builder.set( + /// IfNoneMatch::Items(vec![ + /// EntityTag::new(false, "xyzzy".to_owned()), + /// EntityTag::new(false, "foobar".to_owned()), + /// EntityTag::new(false, "bazquux".to_owned()), + /// ]) + /// ); + /// ``` + (IfNoneMatch, IF_NONE_MATCH) => {Any / (EntityTag)+} + + test_if_none_match { + test_header!(test1, vec![b"\"xyzzy\""]); + test_header!(test2, vec![b"W/\"xyzzy\""]); + test_header!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); + test_header!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]); + test_header!(test5, vec![b"*"]); + } +} + +#[cfg(test)] +mod tests { + use super::IfNoneMatch; + use crate::header::{EntityTag, Header, IF_NONE_MATCH}; + use crate::test::TestRequest; + + #[test] + fn test_if_none_match() { + let mut if_none_match: Result; + + let req = TestRequest::with_header(IF_NONE_MATCH, "*").finish(); + if_none_match = Header::parse(&req); + assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any)); + + let req = + TestRequest::with_header(IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..]) + .finish(); + + if_none_match = Header::parse(&req); + let mut entities: Vec = Vec::new(); + let foobar_etag = EntityTag::new(false, "foobar".to_owned()); + let weak_etag = EntityTag::new(true, "weak-etag".to_owned()); + entities.push(foobar_etag); + entities.push(weak_etag); + assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities))); + } +} diff --git a/src/header/common/if_range.rs b/src/header/common/if_range.rs new file mode 100644 index 000000000..2140ccbb3 --- /dev/null +++ b/src/header/common/if_range.rs @@ -0,0 +1,116 @@ +use std::fmt::{self, Display, Write}; + +use crate::error::ParseError; +use crate::header::{ + self, from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, + IntoHeaderValue, InvalidHeaderValueBytes, Writer, +}; +use crate::httpmessage::HttpMessage; + +/// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2) +/// +/// If a client has a partial copy of a representation and wishes to have +/// an up-to-date copy of the entire representation, it could use the +/// Range header field with a conditional GET (using either or both of +/// If-Unmodified-Since and If-Match.) However, if the precondition +/// fails because the representation has been modified, the client would +/// then have to make a second request to obtain the entire current +/// representation. +/// +/// The `If-Range` header field allows a client to \"short-circuit\" the +/// second request. Informally, its meaning is as follows: if the +/// representation is unchanged, send me the part(s) that I am requesting +/// in Range; otherwise, send me the entire representation. +/// +/// # ABNF +/// +/// ```text +/// If-Range = entity-tag / HTTP-date +/// ``` +/// +/// # Example values +/// +/// * `Sat, 29 Oct 1994 19:43:31 GMT` +/// * `\"xyzzy\"` +/// +/// # Examples +/// +/// ```rust +/// use actix_http::Response; +/// use actix_http::http::header::{EntityTag, IfRange}; +/// +/// let mut builder = Response::Ok(); +/// builder.set(IfRange::EntityTag(EntityTag::new( +/// false, +/// "xyzzy".to_owned(), +/// ))); +/// ``` +/// +/// ```rust +/// use actix_http::Response; +/// use actix_http::http::header::IfRange; +/// use std::time::{Duration, SystemTime}; +/// +/// let mut builder = Response::Ok(); +/// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24); +/// builder.set(IfRange::Date(fetched.into())); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub enum IfRange { + /// The entity-tag the client has of the resource + EntityTag(EntityTag), + /// The date when the client retrieved the resource + Date(HttpDate), +} + +impl Header for IfRange { + fn name() -> HeaderName { + header::IF_RANGE + } + #[inline] + fn parse(msg: &T) -> Result + where + T: HttpMessage, + { + let etag: Result = + from_one_raw_str(msg.headers().get(header::IF_RANGE)); + if let Ok(etag) = etag { + return Ok(IfRange::EntityTag(etag)); + } + let date: Result = + from_one_raw_str(msg.headers().get(header::IF_RANGE)); + if let Ok(date) = date { + return Ok(IfRange::Date(date)); + } + Err(ParseError::Header) + } +} + +impl Display for IfRange { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + IfRange::EntityTag(ref x) => Display::fmt(x, f), + IfRange::Date(ref x) => Display::fmt(x, f), + } + } +} + +impl IntoHeaderValue for IfRange { + type Error = InvalidHeaderValueBytes; + + fn try_into(self) -> Result { + let mut writer = Writer::new(); + let _ = write!(&mut writer, "{}", self); + HeaderValue::from_shared(writer.take()) + } +} + +#[cfg(test)] +mod test_if_range { + use super::IfRange as HeaderField; + use crate::header::*; + use std::str; + test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + test_header!(test2, vec![b"\"xyzzy\""]); + test_header!(test3, vec![b"this-is-invalid"], None::); +} diff --git a/src/header/common/if_unmodified_since.rs b/src/header/common/if_unmodified_since.rs new file mode 100644 index 000000000..d6c099e64 --- /dev/null +++ b/src/header/common/if_unmodified_since.rs @@ -0,0 +1,40 @@ +use crate::header::{HttpDate, IF_UNMODIFIED_SINCE}; + +header! { + /// `If-Unmodified-Since` header, defined in + /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.4) + /// + /// The `If-Unmodified-Since` header field makes the request method + /// conditional on the selected representation's last modification date + /// being earlier than or equal to the date provided in the field-value. + /// This field accomplishes the same purpose as If-Match for cases where + /// the user agent does not have an entity-tag for the representation. + /// + /// # ABNF + /// + /// ```text + /// If-Unmodified-Since = HTTP-date + /// ``` + /// + /// # Example values + /// + /// * `Sat, 29 Oct 1994 19:43:31 GMT` + /// + /// # Example + /// + /// ```rust + /// use actix_http::Response; + /// use actix_http::http::header::IfUnmodifiedSince; + /// use std::time::{SystemTime, Duration}; + /// + /// let mut builder = Response::Ok(); + /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); + /// builder.set(IfUnmodifiedSince(modified.into())); + /// ``` + (IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate] + + test_if_unmodified_since { + // Test case from RFC + test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + } +} diff --git a/src/header/common/last_modified.rs b/src/header/common/last_modified.rs new file mode 100644 index 000000000..cc888ccb0 --- /dev/null +++ b/src/header/common/last_modified.rs @@ -0,0 +1,38 @@ +use crate::header::{HttpDate, LAST_MODIFIED}; + +header! { + /// `Last-Modified` header, defined in + /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2) + /// + /// The `Last-Modified` header field in a response provides a timestamp + /// indicating the date and time at which the origin server believes the + /// selected representation was last modified, as determined at the + /// conclusion of handling the request. + /// + /// # ABNF + /// + /// ```text + /// Expires = HTTP-date + /// ``` + /// + /// # Example values + /// + /// * `Sat, 29 Oct 1994 19:43:31 GMT` + /// + /// # Example + /// + /// ```rust + /// use actix_http::Response; + /// use actix_http::http::header::LastModified; + /// use std::time::{SystemTime, Duration}; + /// + /// let mut builder = Response::Ok(); + /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); + /// builder.set(LastModified(modified.into())); + /// ``` + (LastModified, LAST_MODIFIED) => [HttpDate] + + test_last_modified { + // Test case from RFC + test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);} +} diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs new file mode 100644 index 000000000..adc7484a9 --- /dev/null +++ b/src/header/common/mod.rs @@ -0,0 +1,352 @@ +//! A Collection of Header implementations for common HTTP Headers. +//! +//! ## Mime +//! +//! Several header fields use MIME values for their contents. Keeping with the +//! strongly-typed theme, the [mime](https://docs.rs/mime) crate +//! is used, such as `ContentType(pub Mime)`. +#![cfg_attr(rustfmt, rustfmt_skip)] + +pub use self::accept_charset::AcceptCharset; +//pub use self::accept_encoding::AcceptEncoding; +pub use self::accept_language::AcceptLanguage; +pub use self::accept::Accept; +pub use self::allow::Allow; +pub use self::cache_control::{CacheControl, CacheDirective}; +pub use self::content_disposition::{ContentDisposition, DispositionType, DispositionParam}; +pub use self::content_language::ContentLanguage; +pub use self::content_range::{ContentRange, ContentRangeSpec}; +pub use self::content_type::ContentType; +pub use self::date::Date; +pub use self::etag::ETag; +pub use self::expires::Expires; +pub use self::if_match::IfMatch; +pub use self::if_modified_since::IfModifiedSince; +pub use self::if_none_match::IfNoneMatch; +pub use self::if_range::IfRange; +pub use self::if_unmodified_since::IfUnmodifiedSince; +pub use self::last_modified::LastModified; +//pub use self::range::{Range, ByteRangeSpec}; + +#[doc(hidden)] +#[macro_export] +macro_rules! __hyper__deref { + ($from:ty => $to:ty) => { + impl ::std::ops::Deref for $from { + type Target = $to; + + #[inline] + fn deref(&self) -> &$to { + &self.0 + } + } + + impl ::std::ops::DerefMut for $from { + #[inline] + fn deref_mut(&mut self) -> &mut $to { + &mut self.0 + } + } + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __hyper__tm { + ($id:ident, $tm:ident{$($tf:item)*}) => { + #[allow(unused_imports)] + #[cfg(test)] + mod $tm{ + use std::str; + use http::Method; + use mime::*; + use $crate::header::*; + use super::$id as HeaderField; + $($tf)* + } + + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! test_header { + ($id:ident, $raw:expr) => { + #[test] + fn $id() { + use $crate::test; + use super::*; + + let raw = $raw; + let a: Vec> = raw.iter().map(|x| x.to_vec()).collect(); + let mut req = test::TestRequest::default(); + for item in a { + req = req.header(HeaderField::name(), item); + } + let req = req.finish(); + let value = HeaderField::parse(&req); + let result = format!("{}", value.unwrap()); + let expected = String::from_utf8(raw[0].to_vec()).unwrap(); + let result_cmp: Vec = result + .to_ascii_lowercase() + .split(' ') + .map(|x| x.to_owned()) + .collect(); + let expected_cmp: Vec = expected + .to_ascii_lowercase() + .split(' ') + .map(|x| x.to_owned()) + .collect(); + assert_eq!(result_cmp.concat(), expected_cmp.concat()); + } + }; + ($id:ident, $raw:expr, $typed:expr) => { + #[test] + fn $id() { + use $crate::test; + + let a: Vec> = $raw.iter().map(|x| x.to_vec()).collect(); + let mut req = test::TestRequest::default(); + for item in a { + req = req.header(HeaderField::name(), item); + } + let req = req.finish(); + let val = HeaderField::parse(&req); + let typed: Option = $typed; + // Test parsing + assert_eq!(val.ok(), typed); + // Test formatting + if typed.is_some() { + let raw = &($raw)[..]; + let mut iter = raw.iter().map(|b|str::from_utf8(&b[..]).unwrap()); + let mut joined = String::new(); + joined.push_str(iter.next().unwrap()); + for s in iter { + joined.push_str(", "); + joined.push_str(s); + } + assert_eq!(format!("{}", typed.unwrap()), joined); + } + } + } +} + +#[macro_export] +macro_rules! header { + // $a:meta: Attributes associated with the header item (usually docs) + // $id:ident: Identifier of the header + // $n:expr: Lowercase name of the header + // $nn:expr: Nice name of the header + + // List header, zero or more items + ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)*) => { + $(#[$a])* + #[derive(Clone, Debug, PartialEq)] + pub struct $id(pub Vec<$item>); + __hyper__deref!($id => Vec<$item>); + impl $crate::http::header::Header for $id { + #[inline] + fn name() -> $crate::http::header::HeaderName { + $name + } + #[inline] + fn parse(msg: &T) -> Result + where T: $crate::HttpMessage + { + $crate::http::header::from_comma_delimited( + msg.headers().get_all(Self::name())).map($id) + } + } + impl std::fmt::Display for $id { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter) -> ::std::fmt::Result { + $crate::http::header::fmt_comma_delimited(f, &self.0[..]) + } + } + impl $crate::http::header::IntoHeaderValue for $id { + type Error = $crate::http::header::InvalidHeaderValueBytes; + + fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { + use std::fmt::Write; + let mut writer = $crate::http::header::Writer::new(); + let _ = write!(&mut writer, "{}", self); + $crate::http::header::HeaderValue::from_shared(writer.take()) + } + } + }; + // List header, one or more items + ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)+) => { + $(#[$a])* + #[derive(Clone, Debug, PartialEq)] + pub struct $id(pub Vec<$item>); + __hyper__deref!($id => Vec<$item>); + impl $crate::http::header::Header for $id { + #[inline] + fn name() -> $crate::http::header::HeaderName { + $name + } + #[inline] + fn parse(msg: &T) -> Result + where T: $crate::HttpMessage + { + $crate::http::header::from_comma_delimited( + msg.headers().get_all(Self::name())).map($id) + } + } + impl std::fmt::Display for $id { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + $crate::http::header::fmt_comma_delimited(f, &self.0[..]) + } + } + impl $crate::http::header::IntoHeaderValue for $id { + type Error = $crate::http::header::InvalidHeaderValueBytes; + + fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { + use std::fmt::Write; + let mut writer = $crate::http::header::Writer::new(); + let _ = write!(&mut writer, "{}", self); + $crate::http::header::HeaderValue::from_shared(writer.take()) + } + } + }; + // Single value header + ($(#[$a:meta])*($id:ident, $name:expr) => [$value:ty]) => { + $(#[$a])* + #[derive(Clone, Debug, PartialEq)] + pub struct $id(pub $value); + __hyper__deref!($id => $value); + impl $crate::http::header::Header for $id { + #[inline] + fn name() -> $crate::http::header::HeaderName { + $name + } + #[inline] + fn parse(msg: &T) -> Result + where T: $crate::HttpMessage + { + $crate::http::header::from_one_raw_str( + msg.headers().get(Self::name())).map($id) + } + } + impl std::fmt::Display for $id { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } + } + impl $crate::http::header::IntoHeaderValue for $id { + type Error = $crate::http::header::InvalidHeaderValueBytes; + + fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { + self.0.try_into() + } + } + }; + // List header, one or more items with "*" option + ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { + $(#[$a])* + #[derive(Clone, Debug, PartialEq)] + pub enum $id { + /// Any value is a match + Any, + /// Only the listed items are a match + Items(Vec<$item>), + } + impl $crate::http::header::Header for $id { + #[inline] + fn name() -> $crate::http::header::HeaderName { + $name + } + #[inline] + fn parse(msg: &T) -> Result + where T: $crate::HttpMessage + { + let any = msg.headers().get(Self::name()).and_then(|hdr| { + hdr.to_str().ok().and_then(|hdr| Some(hdr.trim() == "*"))}); + + if let Some(true) = any { + Ok($id::Any) + } else { + Ok($id::Items( + $crate::http::header::from_comma_delimited( + msg.headers().get_all(Self::name()))?)) + } + } + } + impl std::fmt::Display for $id { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + $id::Any => f.write_str("*"), + $id::Items(ref fields) => $crate::http::header::fmt_comma_delimited( + f, &fields[..]) + } + } + } + impl $crate::http::header::IntoHeaderValue for $id { + type Error = $crate::http::header::InvalidHeaderValueBytes; + + fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { + use std::fmt::Write; + let mut writer = $crate::http::header::Writer::new(); + let _ = write!(&mut writer, "{}", self); + $crate::http::header::HeaderValue::from_shared(writer.take()) + } + } + }; + + // optional test module + ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => { + header! { + $(#[$a])* + ($id, $name) => ($item)* + } + + __hyper__tm! { $id, $tm { $($tf)* }} + }; + ($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => { + header! { + $(#[$a])* + ($id, $n) => ($item)+ + } + + __hyper__tm! { $id, $tm { $($tf)* }} + }; + ($(#[$a:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => { + header! { + $(#[$a])* ($id, $name) => [$item] + } + + __hyper__tm! { $id, $tm { $($tf)* }} + }; + ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => { + header! { + $(#[$a])* + ($id, $name) => {Any / ($item)+} + } + + __hyper__tm! { $id, $tm { $($tf)* }} + }; +} + + +mod accept_charset; +//mod accept_encoding; +mod accept_language; +mod accept; +mod allow; +mod cache_control; +mod content_disposition; +mod content_language; +mod content_range; +mod content_type; +mod date; +mod etag; +mod expires; +mod if_match; +mod if_modified_since; +mod if_none_match; +mod if_range; +mod if_unmodified_since; +mod last_modified; diff --git a/src/header/common/range.rs b/src/header/common/range.rs new file mode 100644 index 000000000..71718fc7a --- /dev/null +++ b/src/header/common/range.rs @@ -0,0 +1,434 @@ +use std::fmt::{self, Display}; +use std::str::FromStr; + +use header::parsing::from_one_raw_str; +use header::{Header, Raw}; + +/// `Range` header, defined in [RFC7233](https://tools.ietf.org/html/rfc7233#section-3.1) +/// +/// The "Range" header field on a GET request modifies the method +/// semantics to request transfer of only one or more subranges of the +/// selected representation data, rather than the entire selected +/// representation data. +/// +/// # ABNF +/// +/// ```text +/// Range = byte-ranges-specifier / other-ranges-specifier +/// other-ranges-specifier = other-range-unit "=" other-range-set +/// other-range-set = 1*VCHAR +/// +/// bytes-unit = "bytes" +/// +/// byte-ranges-specifier = bytes-unit "=" byte-range-set +/// byte-range-set = 1#(byte-range-spec / suffix-byte-range-spec) +/// byte-range-spec = first-byte-pos "-" [last-byte-pos] +/// first-byte-pos = 1*DIGIT +/// last-byte-pos = 1*DIGIT +/// ``` +/// +/// # Example values +/// +/// * `bytes=1000-` +/// * `bytes=-2000` +/// * `bytes=0-1,30-40` +/// * `bytes=0-10,20-90,-100` +/// * `custom_unit=0-123` +/// * `custom_unit=xxx-yyy` +/// +/// # Examples +/// +/// ``` +/// use hyper::header::{Headers, Range, ByteRangeSpec}; +/// +/// let mut headers = Headers::new(); +/// headers.set(Range::Bytes( +/// vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::AllFrom(200)] +/// )); +/// +/// headers.clear(); +/// headers.set(Range::Unregistered("letters".to_owned(), "a-f".to_owned())); +/// ``` +/// +/// ``` +/// use hyper::header::{Headers, Range}; +/// +/// let mut headers = Headers::new(); +/// headers.set(Range::bytes(1, 100)); +/// +/// headers.clear(); +/// headers.set(Range::bytes_multi(vec![(1, 100), (200, 300)])); +/// ``` +#[derive(PartialEq, Clone, Debug)] +pub enum Range { + /// Byte range + Bytes(Vec), + /// Custom range, with unit not registered at IANA + /// (`other-range-unit`: String , `other-range-set`: String) + Unregistered(String, String), +} + +/// Each `Range::Bytes` header can contain one or more `ByteRangeSpecs`. +/// Each `ByteRangeSpec` defines a range of bytes to fetch +#[derive(PartialEq, Clone, Debug)] +pub enum ByteRangeSpec { + /// Get all bytes between x and y ("x-y") + FromTo(u64, u64), + /// Get all bytes starting from x ("x-") + AllFrom(u64), + /// Get last x bytes ("-x") + Last(u64), +} + +impl ByteRangeSpec { + /// Given the full length of the entity, attempt to normalize the byte range + /// into an satisfiable end-inclusive (from, to) range. + /// + /// The resulting range is guaranteed to be a satisfiable range within the + /// bounds of `0 <= from <= to < full_length`. + /// + /// If the byte range is deemed unsatisfiable, `None` is returned. + /// An unsatisfiable range is generally cause for a server to either reject + /// the client request with a `416 Range Not Satisfiable` status code, or to + /// simply ignore the range header and serve the full entity using a `200 + /// OK` status code. + /// + /// This function closely follows [RFC 7233][1] section 2.1. + /// As such, it considers ranges to be satisfiable if they meet the + /// following conditions: + /// + /// > If a valid byte-range-set includes at least one byte-range-spec with + /// a first-byte-pos that is less than the current length of the + /// representation, or at least one suffix-byte-range-spec with a + /// non-zero suffix-length, then the byte-range-set is satisfiable. + /// Otherwise, the byte-range-set is unsatisfiable. + /// + /// The function also computes remainder ranges based on the RFC: + /// + /// > If the last-byte-pos value is + /// absent, or if the value is greater than or equal to the current + /// length of the representation data, the byte range is interpreted as + /// the remainder of the representation (i.e., the server replaces the + /// value of last-byte-pos with a value that is one less than the current + /// length of the selected representation). + /// + /// [1]: https://tools.ietf.org/html/rfc7233 + pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> { + // If the full length is zero, there is no satisfiable end-inclusive range. + if full_length == 0 { + return None; + } + match self { + &ByteRangeSpec::FromTo(from, to) => { + if from < full_length && from <= to { + Some((from, ::std::cmp::min(to, full_length - 1))) + } else { + None + } + } + &ByteRangeSpec::AllFrom(from) => { + if from < full_length { + Some((from, full_length - 1)) + } else { + None + } + } + &ByteRangeSpec::Last(last) => { + if last > 0 { + // From the RFC: If the selected representation is shorter + // than the specified suffix-length, + // the entire representation is used. + if last > full_length { + Some((0, full_length - 1)) + } else { + Some((full_length - last, full_length - 1)) + } + } else { + None + } + } + } + } +} + +impl Range { + /// Get the most common byte range header ("bytes=from-to") + pub fn bytes(from: u64, to: u64) -> Range { + Range::Bytes(vec![ByteRangeSpec::FromTo(from, to)]) + } + + /// Get byte range header with multiple subranges + /// ("bytes=from1-to1,from2-to2,fromX-toX") + pub fn bytes_multi(ranges: Vec<(u64, u64)>) -> Range { + Range::Bytes( + ranges + .iter() + .map(|r| ByteRangeSpec::FromTo(r.0, r.1)) + .collect(), + ) + } +} + +impl fmt::Display for ByteRangeSpec { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ByteRangeSpec::FromTo(from, to) => write!(f, "{}-{}", from, to), + ByteRangeSpec::Last(pos) => write!(f, "-{}", pos), + ByteRangeSpec::AllFrom(pos) => write!(f, "{}-", pos), + } + } +} + +impl fmt::Display for Range { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Range::Bytes(ref ranges) => { + try!(write!(f, "bytes=")); + + for (i, range) in ranges.iter().enumerate() { + if i != 0 { + try!(f.write_str(",")); + } + try!(Display::fmt(range, f)); + } + Ok(()) + } + Range::Unregistered(ref unit, ref range_str) => { + write!(f, "{}={}", unit, range_str) + } + } + } +} + +impl FromStr for Range { + type Err = ::Error; + + fn from_str(s: &str) -> ::Result { + let mut iter = s.splitn(2, '='); + + match (iter.next(), iter.next()) { + (Some("bytes"), Some(ranges)) => { + let ranges = from_comma_delimited(ranges); + if ranges.is_empty() { + return Err(::Error::Header); + } + Ok(Range::Bytes(ranges)) + } + (Some(unit), Some(range_str)) if unit != "" && range_str != "" => Ok( + Range::Unregistered(unit.to_owned(), range_str.to_owned()), + ), + _ => Err(::Error::Header), + } + } +} + +impl FromStr for ByteRangeSpec { + type Err = ::Error; + + fn from_str(s: &str) -> ::Result { + let mut parts = s.splitn(2, '-'); + + match (parts.next(), parts.next()) { + (Some(""), Some(end)) => end.parse() + .or(Err(::Error::Header)) + .map(ByteRangeSpec::Last), + (Some(start), Some("")) => start + .parse() + .or(Err(::Error::Header)) + .map(ByteRangeSpec::AllFrom), + (Some(start), Some(end)) => match (start.parse(), end.parse()) { + (Ok(start), Ok(end)) if start <= end => { + Ok(ByteRangeSpec::FromTo(start, end)) + } + _ => Err(::Error::Header), + }, + _ => Err(::Error::Header), + } + } +} + +fn from_comma_delimited(s: &str) -> Vec { + s.split(',') + .filter_map(|x| match x.trim() { + "" => None, + y => Some(y), + }) + .filter_map(|x| x.parse().ok()) + .collect() +} + +impl Header for Range { + fn header_name() -> &'static str { + static NAME: &'static str = "Range"; + NAME + } + + fn parse_header(raw: &Raw) -> ::Result { + from_one_raw_str(raw) + } + + fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result { + f.fmt_line(self) + } +} + +#[test] +fn test_parse_bytes_range_valid() { + let r: Range = Header::parse_header(&"bytes=1-100".into()).unwrap(); + let r2: Range = Header::parse_header(&"bytes=1-100,-".into()).unwrap(); + let r3 = Range::bytes(1, 100); + assert_eq!(r, r2); + assert_eq!(r2, r3); + + let r: Range = Header::parse_header(&"bytes=1-100,200-".into()).unwrap(); + let r2: Range = + Header::parse_header(&"bytes= 1-100 , 101-xxx, 200- ".into()).unwrap(); + let r3 = Range::Bytes(vec![ + ByteRangeSpec::FromTo(1, 100), + ByteRangeSpec::AllFrom(200), + ]); + assert_eq!(r, r2); + assert_eq!(r2, r3); + + let r: Range = Header::parse_header(&"bytes=1-100,-100".into()).unwrap(); + let r2: Range = Header::parse_header(&"bytes=1-100, ,,-100".into()).unwrap(); + let r3 = Range::Bytes(vec![ + ByteRangeSpec::FromTo(1, 100), + ByteRangeSpec::Last(100), + ]); + assert_eq!(r, r2); + assert_eq!(r2, r3); + + let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); + let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); + assert_eq!(r, r2); +} + +#[test] +fn test_parse_unregistered_range_valid() { + let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); + let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); + assert_eq!(r, r2); + + let r: Range = Header::parse_header(&"custom=abcd".into()).unwrap(); + let r2 = Range::Unregistered("custom".to_owned(), "abcd".to_owned()); + assert_eq!(r, r2); + + let r: Range = Header::parse_header(&"custom=xxx-yyy".into()).unwrap(); + let r2 = Range::Unregistered("custom".to_owned(), "xxx-yyy".to_owned()); + assert_eq!(r, r2); +} + +#[test] +fn test_parse_invalid() { + let r: ::Result = Header::parse_header(&"bytes=1-a,-".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"bytes=1-2-3".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"abc".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"bytes=1-100=".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"bytes=".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"custom=".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"=1-100".into()); + assert_eq!(r.ok(), None); +} + +#[test] +fn test_fmt() { + use header::Headers; + + let mut headers = Headers::new(); + + headers.set(Range::Bytes(vec![ + ByteRangeSpec::FromTo(0, 1000), + ByteRangeSpec::AllFrom(2000), + ])); + assert_eq!(&headers.to_string(), "Range: bytes=0-1000,2000-\r\n"); + + headers.clear(); + headers.set(Range::Bytes(vec![])); + + assert_eq!(&headers.to_string(), "Range: bytes=\r\n"); + + headers.clear(); + headers.set(Range::Unregistered( + "custom".to_owned(), + "1-xxx".to_owned(), + )); + + assert_eq!(&headers.to_string(), "Range: custom=1-xxx\r\n"); +} + +#[test] +fn test_byte_range_spec_to_satisfiable_range() { + assert_eq!( + Some((0, 0)), + ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(3) + ); + assert_eq!( + Some((1, 2)), + ByteRangeSpec::FromTo(1, 2).to_satisfiable_range(3) + ); + assert_eq!( + Some((1, 2)), + ByteRangeSpec::FromTo(1, 5).to_satisfiable_range(3) + ); + assert_eq!( + None, + ByteRangeSpec::FromTo(3, 3).to_satisfiable_range(3) + ); + assert_eq!( + None, + ByteRangeSpec::FromTo(2, 1).to_satisfiable_range(3) + ); + assert_eq!( + None, + ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(0) + ); + + assert_eq!( + Some((0, 2)), + ByteRangeSpec::AllFrom(0).to_satisfiable_range(3) + ); + assert_eq!( + Some((2, 2)), + ByteRangeSpec::AllFrom(2).to_satisfiable_range(3) + ); + assert_eq!( + None, + ByteRangeSpec::AllFrom(3).to_satisfiable_range(3) + ); + assert_eq!( + None, + ByteRangeSpec::AllFrom(5).to_satisfiable_range(3) + ); + assert_eq!( + None, + ByteRangeSpec::AllFrom(0).to_satisfiable_range(0) + ); + + assert_eq!( + Some((1, 2)), + ByteRangeSpec::Last(2).to_satisfiable_range(3) + ); + assert_eq!( + Some((2, 2)), + ByteRangeSpec::Last(1).to_satisfiable_range(3) + ); + assert_eq!( + Some((0, 2)), + ByteRangeSpec::Last(5).to_satisfiable_range(3) + ); + assert_eq!(None, ByteRangeSpec::Last(0).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::Last(2).to_satisfiable_range(0)); +} diff --git a/src/header/mod.rs b/src/header/mod.rs new file mode 100644 index 000000000..1ef1bd198 --- /dev/null +++ b/src/header/mod.rs @@ -0,0 +1,465 @@ +//! Various http headers +// This is mostly copy of [hyper](https://github.com/hyperium/hyper/tree/master/src/header) + +use std::{fmt, str::FromStr}; + +use bytes::{Bytes, BytesMut}; +use http::header::GetAll; +use http::Error as HttpError; +use mime::Mime; + +pub use http::header::*; + +use crate::error::ParseError; +use crate::httpmessage::HttpMessage; + +mod common; +mod shared; +#[doc(hidden)] +pub use self::common::*; +#[doc(hidden)] +pub use self::shared::*; + +#[doc(hidden)] +/// A trait for any object that will represent a header field and value. +pub trait Header +where + Self: IntoHeaderValue, +{ + /// Returns the name of the header field + fn name() -> HeaderName; + + /// Parse a header + fn parse(msg: &T) -> Result; +} + +#[doc(hidden)] +/// A trait for any object that can be Converted to a `HeaderValue` +pub trait IntoHeaderValue: Sized { + /// The type returned in the event of a conversion error. + type Error: Into; + + /// Try to convert value to a Header value. + fn try_into(self) -> Result; +} + +impl IntoHeaderValue for HeaderValue { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + Ok(self) + } +} + +impl<'a> IntoHeaderValue for &'a str { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + self.parse() + } +} + +impl<'a> IntoHeaderValue for &'a [u8] { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::from_bytes(self) + } +} + +impl IntoHeaderValue for Bytes { + type Error = InvalidHeaderValueBytes; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::from_shared(self) + } +} + +impl IntoHeaderValue for Vec { + type Error = InvalidHeaderValueBytes; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::from_shared(Bytes::from(self)) + } +} + +impl IntoHeaderValue for String { + type Error = InvalidHeaderValueBytes; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::from_shared(Bytes::from(self)) + } +} + +impl IntoHeaderValue for Mime { + type Error = InvalidHeaderValueBytes; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::from_shared(Bytes::from(format!("{}", self))) + } +} + +/// Represents supported types of content encodings +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum ContentEncoding { + /// Automatically select encoding based on encoding negotiation + Auto, + /// A format using the Brotli algorithm + Br, + /// A format using the zlib structure with deflate algorithm + Deflate, + /// Gzip algorithm + Gzip, + /// Indicates the identity function (i.e. no compression, nor modification) + Identity, +} + +impl ContentEncoding { + #[inline] + /// Is the content compressed? + pub fn is_compression(self) -> bool { + match self { + ContentEncoding::Identity | ContentEncoding::Auto => false, + _ => true, + } + } + + #[inline] + /// Convert content encoding to string + pub fn as_str(self) -> &'static str { + match self { + ContentEncoding::Br => "br", + ContentEncoding::Gzip => "gzip", + ContentEncoding::Deflate => "deflate", + ContentEncoding::Identity | ContentEncoding::Auto => "identity", + } + } + + #[inline] + /// default quality value + pub fn quality(self) -> f64 { + match self { + ContentEncoding::Br => 1.1, + ContentEncoding::Gzip => 1.0, + ContentEncoding::Deflate => 0.9, + ContentEncoding::Identity | ContentEncoding::Auto => 0.1, + } + } +} + +impl<'a> From<&'a str> for ContentEncoding { + fn from(s: &'a str) -> ContentEncoding { + let s = s.trim(); + + if s.eq_ignore_ascii_case("br") { + ContentEncoding::Br + } else if s.eq_ignore_ascii_case("gzip") { + ContentEncoding::Gzip + } else if s.eq_ignore_ascii_case("deflate") { + ContentEncoding::Deflate + } else { + ContentEncoding::Identity + } + } +} + +#[doc(hidden)] +pub(crate) struct Writer { + buf: BytesMut, +} + +impl Writer { + fn new() -> Writer { + Writer { + buf: BytesMut::new(), + } + } + fn take(&mut self) -> Bytes { + self.buf.take().freeze() + } +} + +impl fmt::Write for Writer { + #[inline] + fn write_str(&mut self, s: &str) -> fmt::Result { + self.buf.extend_from_slice(s.as_bytes()); + Ok(()) + } + + #[inline] + fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result { + fmt::write(self, args) + } +} + +#[inline] +#[doc(hidden)] +/// Reads a comma-delimited raw header into a Vec. +pub fn from_comma_delimited( + all: GetAll, +) -> Result, ParseError> { + let mut result = Vec::new(); + for h in all { + let s = h.to_str().map_err(|_| ParseError::Header)?; + result.extend( + s.split(',') + .filter_map(|x| match x.trim() { + "" => None, + y => Some(y), + }) + .filter_map(|x| x.trim().parse().ok()), + ) + } + Ok(result) +} + +#[inline] +#[doc(hidden)] +/// Reads a single string when parsing a header. +pub fn from_one_raw_str(val: Option<&HeaderValue>) -> Result { + if let Some(line) = val { + let line = line.to_str().map_err(|_| ParseError::Header)?; + if !line.is_empty() { + return T::from_str(line).or(Err(ParseError::Header)); + } + } + Err(ParseError::Header) +} + +#[inline] +#[doc(hidden)] +/// Format an array into a comma-delimited string. +pub fn fmt_comma_delimited(f: &mut fmt::Formatter, parts: &[T]) -> fmt::Result +where + T: fmt::Display, +{ + let mut iter = parts.iter(); + if let Some(part) = iter.next() { + fmt::Display::fmt(part, f)?; + } + for part in iter { + f.write_str(", ")?; + fmt::Display::fmt(part, f)?; + } + Ok(()) +} + +// From hyper v0.11.27 src/header/parsing.rs + +/// The value part of an extended parameter consisting of three parts: +/// the REQUIRED character set name (`charset`), the OPTIONAL language information (`language_tag`), +/// and a character sequence representing the actual value (`value`), separated by single quote +/// characters. It is defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). +#[derive(Clone, Debug, PartialEq)] +pub struct ExtendedValue { + /// The character set that is used to encode the `value` to a string. + pub charset: Charset, + /// The human language details of the `value`, if available. + pub language_tag: Option, + /// The parameter value, as expressed in octets. + pub value: Vec, +} + +/// Parses extended header parameter values (`ext-value`), as defined in +/// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). +/// +/// Extended values are denoted by parameter names that end with `*`. +/// +/// ## ABNF +/// +/// ```text +/// ext-value = charset "'" [ language ] "'" value-chars +/// ; like RFC 2231's +/// ; (see [RFC2231], Section 7) +/// +/// charset = "UTF-8" / "ISO-8859-1" / mime-charset +/// +/// mime-charset = 1*mime-charsetc +/// mime-charsetc = ALPHA / DIGIT +/// / "!" / "#" / "$" / "%" / "&" +/// / "+" / "-" / "^" / "_" / "`" +/// / "{" / "}" / "~" +/// ; as in Section 2.3 of [RFC2978] +/// ; except that the single quote is not included +/// ; SHOULD be registered in the IANA charset registry +/// +/// language = +/// +/// value-chars = *( pct-encoded / attr-char ) +/// +/// pct-encoded = "%" HEXDIG HEXDIG +/// ; see [RFC3986], Section 2.1 +/// +/// attr-char = ALPHA / DIGIT +/// / "!" / "#" / "$" / "&" / "+" / "-" / "." +/// / "^" / "_" / "`" / "|" / "~" +/// ; token except ( "*" / "'" / "%" ) +/// ``` +pub fn parse_extended_value( + val: &str, +) -> Result { + // Break into three pieces separated by the single-quote character + let mut parts = val.splitn(3, '\''); + + // Interpret the first piece as a Charset + let charset: Charset = match parts.next() { + None => return Err(crate::error::ParseError::Header), + Some(n) => FromStr::from_str(n).map_err(|_| crate::error::ParseError::Header)?, + }; + + // Interpret the second piece as a language tag + let language_tag: Option = match parts.next() { + None => return Err(crate::error::ParseError::Header), + Some("") => None, + Some(s) => match s.parse() { + Ok(lt) => Some(lt), + Err(_) => return Err(crate::error::ParseError::Header), + }, + }; + + // Interpret the third piece as a sequence of value characters + let value: Vec = match parts.next() { + None => return Err(crate::error::ParseError::Header), + Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(), + }; + + Ok(ExtendedValue { + value, + charset, + language_tag, + }) +} + +impl fmt::Display for ExtendedValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let encoded_value = percent_encoding::percent_encode( + &self.value[..], + self::percent_encoding_http::HTTP_VALUE, + ); + if let Some(ref lang) = self.language_tag { + write!(f, "{}'{}'{}", self.charset, lang, encoded_value) + } else { + write!(f, "{}''{}", self.charset, encoded_value) + } + } +} + +/// Percent encode a sequence of bytes with a character set defined in +/// [https://tools.ietf.org/html/rfc5987#section-3.2][url] +/// +/// [url]: https://tools.ietf.org/html/rfc5987#section-3.2 +pub fn http_percent_encode(f: &mut fmt::Formatter, bytes: &[u8]) -> fmt::Result { + let encoded = + percent_encoding::percent_encode(bytes, self::percent_encoding_http::HTTP_VALUE); + fmt::Display::fmt(&encoded, f) +} + +mod percent_encoding_http { + use percent_encoding::{self, define_encode_set}; + + // internal module because macro is hard-coded to make a public item + // but we don't want to public export this item + define_encode_set! { + // This encode set is used for HTTP header values and is defined at + // https://tools.ietf.org/html/rfc5987#section-3.2 + pub HTTP_VALUE = [percent_encoding::SIMPLE_ENCODE_SET] | { + ' ', '"', '%', '\'', '(', ')', '*', ',', '/', ':', ';', '<', '-', '>', '?', + '[', '\\', ']', '{', '}' + } + } +} + +#[cfg(test)] +mod tests { + use super::shared::Charset; + use super::{parse_extended_value, ExtendedValue}; + use language_tags::LanguageTag; + + #[test] + fn test_parse_extended_value_with_encoding_and_language_tag() { + let expected_language_tag = "en".parse::().unwrap(); + // RFC 5987, Section 3.2.2 + // Extended notation, using the Unicode character U+00A3 (POUND SIGN) + let result = parse_extended_value("iso-8859-1'en'%A3%20rates"); + assert!(result.is_ok()); + let extended_value = result.unwrap(); + assert_eq!(Charset::Iso_8859_1, extended_value.charset); + assert!(extended_value.language_tag.is_some()); + assert_eq!(expected_language_tag, extended_value.language_tag.unwrap()); + assert_eq!( + vec![163, b' ', b'r', b'a', b't', b'e', b's'], + extended_value.value + ); + } + + #[test] + fn test_parse_extended_value_with_encoding() { + // RFC 5987, Section 3.2.2 + // Extended notation, using the Unicode characters U+00A3 (POUND SIGN) + // and U+20AC (EURO SIGN) + let result = parse_extended_value("UTF-8''%c2%a3%20and%20%e2%82%ac%20rates"); + assert!(result.is_ok()); + let extended_value = result.unwrap(); + assert_eq!(Charset::Ext("UTF-8".to_string()), extended_value.charset); + assert!(extended_value.language_tag.is_none()); + assert_eq!( + vec![ + 194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', + b't', b'e', b's', + ], + extended_value.value + ); + } + + #[test] + fn test_parse_extended_value_missing_language_tag_and_encoding() { + // From: https://greenbytes.de/tech/tc2231/#attwithfn2231quot2 + let result = parse_extended_value("foo%20bar.html"); + assert!(result.is_err()); + } + + #[test] + fn test_parse_extended_value_partially_formatted() { + let result = parse_extended_value("UTF-8'missing third part"); + assert!(result.is_err()); + } + + #[test] + fn test_parse_extended_value_partially_formatted_blank() { + let result = parse_extended_value("blank second part'"); + assert!(result.is_err()); + } + + #[test] + fn test_fmt_extended_value_with_encoding_and_language_tag() { + let extended_value = ExtendedValue { + charset: Charset::Iso_8859_1, + language_tag: Some("en".parse().expect("Could not parse language tag")), + value: vec![163, b' ', b'r', b'a', b't', b'e', b's'], + }; + assert_eq!("ISO-8859-1'en'%A3%20rates", format!("{}", extended_value)); + } + + #[test] + fn test_fmt_extended_value_with_encoding() { + let extended_value = ExtendedValue { + charset: Charset::Ext("UTF-8".to_string()), + language_tag: None, + value: vec![ + 194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', + b't', b'e', b's', + ], + }; + assert_eq!( + "UTF-8''%C2%A3%20and%20%E2%82%AC%20rates", + format!("{}", extended_value) + ); + } +} diff --git a/src/header/shared/charset.rs b/src/header/shared/charset.rs new file mode 100644 index 000000000..ec3fe3854 --- /dev/null +++ b/src/header/shared/charset.rs @@ -0,0 +1,153 @@ +use std::fmt::{self, Display}; +use std::str::FromStr; + +use self::Charset::*; + +/// A Mime charset. +/// +/// The string representation is normalized to upper case. +/// +/// See [http://www.iana.org/assignments/character-sets/character-sets.xhtml][url]. +/// +/// [url]: http://www.iana.org/assignments/character-sets/character-sets.xhtml +#[derive(Clone, Debug, PartialEq)] +#[allow(non_camel_case_types)] +pub enum Charset { + /// US ASCII + Us_Ascii, + /// ISO-8859-1 + Iso_8859_1, + /// ISO-8859-2 + Iso_8859_2, + /// ISO-8859-3 + Iso_8859_3, + /// ISO-8859-4 + Iso_8859_4, + /// ISO-8859-5 + Iso_8859_5, + /// ISO-8859-6 + Iso_8859_6, + /// ISO-8859-7 + Iso_8859_7, + /// ISO-8859-8 + Iso_8859_8, + /// ISO-8859-9 + Iso_8859_9, + /// ISO-8859-10 + Iso_8859_10, + /// Shift_JIS + Shift_Jis, + /// EUC-JP + Euc_Jp, + /// ISO-2022-KR + Iso_2022_Kr, + /// EUC-KR + Euc_Kr, + /// ISO-2022-JP + Iso_2022_Jp, + /// ISO-2022-JP-2 + Iso_2022_Jp_2, + /// ISO-8859-6-E + Iso_8859_6_E, + /// ISO-8859-6-I + Iso_8859_6_I, + /// ISO-8859-8-E + Iso_8859_8_E, + /// ISO-8859-8-I + Iso_8859_8_I, + /// GB2312 + Gb2312, + /// Big5 + Big5, + /// KOI8-R + Koi8_R, + /// An arbitrary charset specified as a string + Ext(String), +} + +impl Charset { + fn label(&self) -> &str { + match *self { + Us_Ascii => "US-ASCII", + Iso_8859_1 => "ISO-8859-1", + Iso_8859_2 => "ISO-8859-2", + Iso_8859_3 => "ISO-8859-3", + Iso_8859_4 => "ISO-8859-4", + Iso_8859_5 => "ISO-8859-5", + Iso_8859_6 => "ISO-8859-6", + Iso_8859_7 => "ISO-8859-7", + Iso_8859_8 => "ISO-8859-8", + Iso_8859_9 => "ISO-8859-9", + Iso_8859_10 => "ISO-8859-10", + Shift_Jis => "Shift-JIS", + Euc_Jp => "EUC-JP", + Iso_2022_Kr => "ISO-2022-KR", + Euc_Kr => "EUC-KR", + Iso_2022_Jp => "ISO-2022-JP", + Iso_2022_Jp_2 => "ISO-2022-JP-2", + Iso_8859_6_E => "ISO-8859-6-E", + Iso_8859_6_I => "ISO-8859-6-I", + Iso_8859_8_E => "ISO-8859-8-E", + Iso_8859_8_I => "ISO-8859-8-I", + Gb2312 => "GB2312", + Big5 => "big5", + Koi8_R => "KOI8-R", + Ext(ref s) => s, + } + } +} + +impl Display for Charset { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.label()) + } +} + +impl FromStr for Charset { + type Err = crate::Error; + + fn from_str(s: &str) -> crate::Result { + Ok(match s.to_ascii_uppercase().as_ref() { + "US-ASCII" => Us_Ascii, + "ISO-8859-1" => Iso_8859_1, + "ISO-8859-2" => Iso_8859_2, + "ISO-8859-3" => Iso_8859_3, + "ISO-8859-4" => Iso_8859_4, + "ISO-8859-5" => Iso_8859_5, + "ISO-8859-6" => Iso_8859_6, + "ISO-8859-7" => Iso_8859_7, + "ISO-8859-8" => Iso_8859_8, + "ISO-8859-9" => Iso_8859_9, + "ISO-8859-10" => Iso_8859_10, + "SHIFT-JIS" => Shift_Jis, + "EUC-JP" => Euc_Jp, + "ISO-2022-KR" => Iso_2022_Kr, + "EUC-KR" => Euc_Kr, + "ISO-2022-JP" => Iso_2022_Jp, + "ISO-2022-JP-2" => Iso_2022_Jp_2, + "ISO-8859-6-E" => Iso_8859_6_E, + "ISO-8859-6-I" => Iso_8859_6_I, + "ISO-8859-8-E" => Iso_8859_8_E, + "ISO-8859-8-I" => Iso_8859_8_I, + "GB2312" => Gb2312, + "big5" => Big5, + "KOI8-R" => Koi8_R, + s => Ext(s.to_owned()), + }) + } +} + +#[test] +fn test_parse() { + assert_eq!(Us_Ascii, "us-ascii".parse().unwrap()); + assert_eq!(Us_Ascii, "US-Ascii".parse().unwrap()); + assert_eq!(Us_Ascii, "US-ASCII".parse().unwrap()); + assert_eq!(Shift_Jis, "Shift-JIS".parse().unwrap()); + assert_eq!(Ext("ABCD".to_owned()), "abcd".parse().unwrap()); +} + +#[test] +fn test_display() { + assert_eq!("US-ASCII", format!("{}", Us_Ascii)); + assert_eq!("ABCD", format!("{}", Ext("ABCD".to_owned()))); +} diff --git a/src/header/shared/encoding.rs b/src/header/shared/encoding.rs new file mode 100644 index 000000000..af7404828 --- /dev/null +++ b/src/header/shared/encoding.rs @@ -0,0 +1,58 @@ +use std::{fmt, str}; + +pub use self::Encoding::{ + Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, +}; + +/// A value to represent an encoding used in `Transfer-Encoding` +/// or `Accept-Encoding` header. +#[derive(Clone, PartialEq, Debug)] +pub enum Encoding { + /// The `chunked` encoding. + Chunked, + /// The `br` encoding. + Brotli, + /// The `gzip` encoding. + Gzip, + /// The `deflate` encoding. + Deflate, + /// The `compress` encoding. + Compress, + /// The `identity` encoding. + Identity, + /// The `trailers` encoding. + Trailers, + /// Some other encoding that is less common, can be any String. + EncodingExt(String), +} + +impl fmt::Display for Encoding { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match *self { + Chunked => "chunked", + Brotli => "br", + Gzip => "gzip", + Deflate => "deflate", + Compress => "compress", + Identity => "identity", + Trailers => "trailers", + EncodingExt(ref s) => s.as_ref(), + }) + } +} + +impl str::FromStr for Encoding { + type Err = crate::error::ParseError; + fn from_str(s: &str) -> Result { + match s { + "chunked" => Ok(Chunked), + "br" => Ok(Brotli), + "deflate" => Ok(Deflate), + "gzip" => Ok(Gzip), + "compress" => Ok(Compress), + "identity" => Ok(Identity), + "trailers" => Ok(Trailers), + _ => Ok(EncodingExt(s.to_owned())), + } + } +} diff --git a/src/header/shared/entity.rs b/src/header/shared/entity.rs new file mode 100644 index 000000000..da02dc193 --- /dev/null +++ b/src/header/shared/entity.rs @@ -0,0 +1,265 @@ +use std::fmt::{self, Display, Write}; +use std::str::FromStr; + +use crate::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, Writer}; + +/// check that each char in the slice is either: +/// 1. `%x21`, or +/// 2. in the range `%x23` to `%x7E`, or +/// 3. above `%x80` +fn check_slice_validity(slice: &str) -> bool { + slice + .bytes() + .all(|c| c == b'\x21' || (c >= b'\x23' && c <= b'\x7e') | (c >= b'\x80')) +} + +/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3) +/// +/// An entity tag consists of a string enclosed by two literal double quotes. +/// Preceding the first double quote is an optional weakness indicator, +/// which always looks like `W/`. Examples for valid tags are `"xyzzy"` and +/// `W/"xyzzy"`. +/// +/// # ABNF +/// +/// ```text +/// entity-tag = [ weak ] opaque-tag +/// weak = %x57.2F ; "W/", case-sensitive +/// opaque-tag = DQUOTE *etagc DQUOTE +/// etagc = %x21 / %x23-7E / obs-text +/// ; VCHAR except double quotes, plus obs-text +/// ``` +/// +/// # Comparison +/// To check if two entity tags are equivalent in an application always use the +/// `strong_eq` or `weak_eq` methods based on the context of the Tag. Only use +/// `==` to check if two tags are identical. +/// +/// The example below shows the results for a set of entity-tag pairs and +/// both the weak and strong comparison function results: +/// +/// | `ETag 1`| `ETag 2`| Strong Comparison | Weak Comparison | +/// |---------|---------|-------------------|-----------------| +/// | `W/"1"` | `W/"1"` | no match | match | +/// | `W/"1"` | `W/"2"` | no match | no match | +/// | `W/"1"` | `"1"` | no match | match | +/// | `"1"` | `"1"` | match | match | +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EntityTag { + /// Weakness indicator for the tag + pub weak: bool, + /// The opaque string in between the DQUOTEs + tag: String, +} + +impl EntityTag { + /// Constructs a new EntityTag. + /// # Panics + /// If the tag contains invalid characters. + pub fn new(weak: bool, tag: String) -> EntityTag { + assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); + EntityTag { weak, tag } + } + + /// Constructs a new weak EntityTag. + /// # Panics + /// If the tag contains invalid characters. + pub fn weak(tag: String) -> EntityTag { + EntityTag::new(true, tag) + } + + /// Constructs a new strong EntityTag. + /// # Panics + /// If the tag contains invalid characters. + pub fn strong(tag: String) -> EntityTag { + EntityTag::new(false, tag) + } + + /// Get the tag. + pub fn tag(&self) -> &str { + self.tag.as_ref() + } + + /// Set the tag. + /// # Panics + /// If the tag contains invalid characters. + pub fn set_tag(&mut self, tag: String) { + assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); + self.tag = tag + } + + /// For strong comparison two entity-tags are equivalent if both are not + /// weak and their opaque-tags match character-by-character. + pub fn strong_eq(&self, other: &EntityTag) -> bool { + !self.weak && !other.weak && self.tag == other.tag + } + + /// For weak comparison two entity-tags are equivalent if their + /// opaque-tags match character-by-character, regardless of either or + /// both being tagged as "weak". + pub fn weak_eq(&self, other: &EntityTag) -> bool { + self.tag == other.tag + } + + /// The inverse of `EntityTag.strong_eq()`. + pub fn strong_ne(&self, other: &EntityTag) -> bool { + !self.strong_eq(other) + } + + /// The inverse of `EntityTag.weak_eq()`. + pub fn weak_ne(&self, other: &EntityTag) -> bool { + !self.weak_eq(other) + } +} + +impl Display for EntityTag { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.weak { + write!(f, "W/\"{}\"", self.tag) + } else { + write!(f, "\"{}\"", self.tag) + } + } +} + +impl FromStr for EntityTag { + type Err = crate::error::ParseError; + + fn from_str(s: &str) -> Result { + let length: usize = s.len(); + let slice = &s[..]; + // Early exits if it doesn't terminate in a DQUOTE. + if !slice.ends_with('"') || slice.len() < 2 { + return Err(crate::error::ParseError::Header); + } + // The etag is weak if its first char is not a DQUOTE. + if slice.len() >= 2 + && slice.starts_with('"') + && check_slice_validity(&slice[1..length - 1]) + { + // No need to check if the last char is a DQUOTE, + // we already did that above. + return Ok(EntityTag { + weak: false, + tag: slice[1..length - 1].to_owned(), + }); + } else if slice.len() >= 4 + && slice.starts_with("W/\"") + && check_slice_validity(&slice[3..length - 1]) + { + return Ok(EntityTag { + weak: true, + tag: slice[3..length - 1].to_owned(), + }); + } + Err(crate::error::ParseError::Header) + } +} + +impl IntoHeaderValue for EntityTag { + type Error = InvalidHeaderValueBytes; + + fn try_into(self) -> Result { + let mut wrt = Writer::new(); + write!(wrt, "{}", self).unwrap(); + HeaderValue::from_shared(wrt.take()) + } +} + +#[cfg(test)] +mod tests { + use super::EntityTag; + + #[test] + fn test_etag_parse_success() { + // Expected success + assert_eq!( + "\"foobar\"".parse::().unwrap(), + EntityTag::strong("foobar".to_owned()) + ); + assert_eq!( + "\"\"".parse::().unwrap(), + EntityTag::strong("".to_owned()) + ); + assert_eq!( + "W/\"weaktag\"".parse::().unwrap(), + EntityTag::weak("weaktag".to_owned()) + ); + assert_eq!( + "W/\"\x65\x62\"".parse::().unwrap(), + EntityTag::weak("\x65\x62".to_owned()) + ); + assert_eq!( + "W/\"\"".parse::().unwrap(), + EntityTag::weak("".to_owned()) + ); + } + + #[test] + fn test_etag_parse_failures() { + // Expected failures + assert!("no-dquotes".parse::().is_err()); + assert!("w/\"the-first-w-is-case-sensitive\"" + .parse::() + .is_err()); + assert!("".parse::().is_err()); + assert!("\"unmatched-dquotes1".parse::().is_err()); + assert!("unmatched-dquotes2\"".parse::().is_err()); + assert!("matched-\"dquotes\"".parse::().is_err()); + } + + #[test] + fn test_etag_fmt() { + assert_eq!( + format!("{}", EntityTag::strong("foobar".to_owned())), + "\"foobar\"" + ); + assert_eq!(format!("{}", EntityTag::strong("".to_owned())), "\"\""); + assert_eq!( + format!("{}", EntityTag::weak("weak-etag".to_owned())), + "W/\"weak-etag\"" + ); + assert_eq!( + format!("{}", EntityTag::weak("\u{0065}".to_owned())), + "W/\"\x65\"" + ); + assert_eq!(format!("{}", EntityTag::weak("".to_owned())), "W/\"\""); + } + + #[test] + fn test_cmp() { + // | ETag 1 | ETag 2 | Strong Comparison | Weak Comparison | + // |---------|---------|-------------------|-----------------| + // | `W/"1"` | `W/"1"` | no match | match | + // | `W/"1"` | `W/"2"` | no match | no match | + // | `W/"1"` | `"1"` | no match | match | + // | `"1"` | `"1"` | match | match | + let mut etag1 = EntityTag::weak("1".to_owned()); + let mut etag2 = EntityTag::weak("1".to_owned()); + assert!(!etag1.strong_eq(&etag2)); + assert!(etag1.weak_eq(&etag2)); + assert!(etag1.strong_ne(&etag2)); + assert!(!etag1.weak_ne(&etag2)); + + etag1 = EntityTag::weak("1".to_owned()); + etag2 = EntityTag::weak("2".to_owned()); + assert!(!etag1.strong_eq(&etag2)); + assert!(!etag1.weak_eq(&etag2)); + assert!(etag1.strong_ne(&etag2)); + assert!(etag1.weak_ne(&etag2)); + + etag1 = EntityTag::weak("1".to_owned()); + etag2 = EntityTag::strong("1".to_owned()); + assert!(!etag1.strong_eq(&etag2)); + assert!(etag1.weak_eq(&etag2)); + assert!(etag1.strong_ne(&etag2)); + assert!(!etag1.weak_ne(&etag2)); + + etag1 = EntityTag::strong("1".to_owned()); + etag2 = EntityTag::strong("1".to_owned()); + assert!(etag1.strong_eq(&etag2)); + assert!(etag1.weak_eq(&etag2)); + assert!(!etag1.strong_ne(&etag2)); + assert!(!etag1.weak_ne(&etag2)); + } +} diff --git a/src/header/shared/httpdate.rs b/src/header/shared/httpdate.rs new file mode 100644 index 000000000..350f77bbe --- /dev/null +++ b/src/header/shared/httpdate.rs @@ -0,0 +1,118 @@ +use std::fmt::{self, Display}; +use std::io::Write; +use std::str::FromStr; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use bytes::{BufMut, BytesMut}; +use http::header::{HeaderValue, InvalidHeaderValueBytes}; + +use crate::error::ParseError; +use crate::header::IntoHeaderValue; + +/// A timestamp with HTTP formatting and parsing +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct HttpDate(time::Tm); + +impl FromStr for HttpDate { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + match time::strptime(s, "%a, %d %b %Y %T %Z") + .or_else(|_| time::strptime(s, "%A, %d-%b-%y %T %Z")) + .or_else(|_| time::strptime(s, "%c")) + { + Ok(t) => Ok(HttpDate(t)), + Err(_) => Err(ParseError::Header), + } + } +} + +impl Display for HttpDate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0.to_utc().rfc822(), f) + } +} + +impl From for HttpDate { + fn from(tm: time::Tm) -> HttpDate { + HttpDate(tm) + } +} + +impl From for HttpDate { + fn from(sys: SystemTime) -> HttpDate { + let tmspec = match sys.duration_since(UNIX_EPOCH) { + Ok(dur) => { + time::Timespec::new(dur.as_secs() as i64, dur.subsec_nanos() as i32) + } + Err(err) => { + let neg = err.duration(); + time::Timespec::new( + -(neg.as_secs() as i64), + -(neg.subsec_nanos() as i32), + ) + } + }; + HttpDate(time::at_utc(tmspec)) + } +} + +impl IntoHeaderValue for HttpDate { + type Error = InvalidHeaderValueBytes; + + fn try_into(self) -> Result { + let mut wrt = BytesMut::with_capacity(29).writer(); + write!(wrt, "{}", self.0.rfc822()).unwrap(); + HeaderValue::from_shared(wrt.get_mut().take().freeze()) + } +} + +impl From for SystemTime { + fn from(date: HttpDate) -> SystemTime { + let spec = date.0.to_timespec(); + if spec.sec >= 0 { + UNIX_EPOCH + Duration::new(spec.sec as u64, spec.nsec as u32) + } else { + UNIX_EPOCH - Duration::new(spec.sec as u64, spec.nsec as u32) + } + } +} + +#[cfg(test)] +mod tests { + use super::HttpDate; + use time::Tm; + + const NOV_07: HttpDate = HttpDate(Tm { + tm_nsec: 0, + tm_sec: 37, + tm_min: 48, + tm_hour: 8, + tm_mday: 7, + tm_mon: 10, + tm_year: 94, + tm_wday: 0, + tm_isdst: 0, + tm_yday: 0, + tm_utcoff: 0, + }); + + #[test] + fn test_date() { + assert_eq!( + "Sun, 07 Nov 1994 08:48:37 GMT".parse::().unwrap(), + NOV_07 + ); + assert_eq!( + "Sunday, 07-Nov-94 08:48:37 GMT" + .parse::() + .unwrap(), + NOV_07 + ); + assert_eq!( + "Sun Nov 7 08:48:37 1994".parse::().unwrap(), + NOV_07 + ); + assert!("this-is-no-date".parse::().is_err()); + } +} diff --git a/src/header/shared/mod.rs b/src/header/shared/mod.rs new file mode 100644 index 000000000..f2bc91634 --- /dev/null +++ b/src/header/shared/mod.rs @@ -0,0 +1,14 @@ +//! Copied for `hyper::header::shared`; + +pub use self::charset::Charset; +pub use self::encoding::Encoding; +pub use self::entity::EntityTag; +pub use self::httpdate::HttpDate; +pub use self::quality_item::{q, qitem, Quality, QualityItem}; +pub use language_tags::LanguageTag; + +mod charset; +mod encoding; +mod entity; +mod httpdate; +mod quality_item; diff --git a/src/header/shared/quality_item.rs b/src/header/shared/quality_item.rs new file mode 100644 index 000000000..07c206581 --- /dev/null +++ b/src/header/shared/quality_item.rs @@ -0,0 +1,291 @@ +use std::{cmp, fmt, str}; + +use self::internal::IntoQuality; + +/// Represents a quality used in quality values. +/// +/// Can be created with the `q` function. +/// +/// # Implementation notes +/// +/// The quality value is defined as a number between 0 and 1 with three decimal +/// places. This means there are 1001 possible values. Since floating point +/// numbers are not exact and the smallest floating point data type (`f32`) +/// consumes four bytes, hyper uses an `u16` value to store the +/// quality internally. For performance reasons you may set quality directly to +/// a value between 0 and 1000 e.g. `Quality(532)` matches the quality +/// `q=0.532`. +/// +/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1) +/// gives more information on quality values in HTTP header fields. +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Quality(u16); + +impl Default for Quality { + fn default() -> Quality { + Quality(1000) + } +} + +/// Represents an item with a quality value as defined in +/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.1). +#[derive(Clone, PartialEq, Debug)] +pub struct QualityItem { + /// The actual contents of the field. + pub item: T, + /// The quality (client or server preference) for the value. + pub quality: Quality, +} + +impl QualityItem { + /// Creates a new `QualityItem` from an item and a quality. + /// The item can be of any type. + /// The quality should be a value in the range [0, 1]. + pub fn new(item: T, quality: Quality) -> QualityItem { + QualityItem { item, quality } + } +} + +impl cmp::PartialOrd for QualityItem { + fn partial_cmp(&self, other: &QualityItem) -> Option { + self.quality.partial_cmp(&other.quality) + } +} + +impl fmt::Display for QualityItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.item, f)?; + match self.quality.0 { + 1000 => Ok(()), + 0 => f.write_str("; q=0"), + x => write!(f, "; q=0.{}", format!("{:03}", x).trim_right_matches('0')), + } + } +} + +impl str::FromStr for QualityItem { + type Err = crate::error::ParseError; + + fn from_str(s: &str) -> Result, crate::error::ParseError> { + if !s.is_ascii() { + return Err(crate::error::ParseError::Header); + } + // Set defaults used if parsing fails. + let mut raw_item = s; + let mut quality = 1f32; + + let parts: Vec<&str> = s.rsplitn(2, ';').map(|x| x.trim()).collect(); + if parts.len() == 2 { + if parts[0].len() < 2 { + return Err(crate::error::ParseError::Header); + } + let start = &parts[0][0..2]; + if start == "q=" || start == "Q=" { + let q_part = &parts[0][2..parts[0].len()]; + if q_part.len() > 5 { + return Err(crate::error::ParseError::Header); + } + match q_part.parse::() { + Ok(q_value) => { + if 0f32 <= q_value && q_value <= 1f32 { + quality = q_value; + raw_item = parts[1]; + } else { + return Err(crate::error::ParseError::Header); + } + } + Err(_) => return Err(crate::error::ParseError::Header), + } + } + } + match raw_item.parse::() { + // we already checked above that the quality is within range + Ok(item) => Ok(QualityItem::new(item, from_f32(quality))), + Err(_) => Err(crate::error::ParseError::Header), + } + } +} + +#[inline] +fn from_f32(f: f32) -> Quality { + // this function is only used internally. A check that `f` is within range + // should be done before calling this method. Just in case, this + // debug_assert should catch if we were forgetful + debug_assert!( + f >= 0f32 && f <= 1f32, + "q value must be between 0.0 and 1.0" + ); + Quality((f * 1000f32) as u16) +} + +/// Convenience function to wrap a value in a `QualityItem` +/// Sets `q` to the default 1.0 +pub fn qitem(item: T) -> QualityItem { + QualityItem::new(item, Default::default()) +} + +/// Convenience function to create a `Quality` from a float or integer. +/// +/// Implemented for `u16` and `f32`. Panics if value is out of range. +pub fn q(val: T) -> Quality { + val.into_quality() +} + +mod internal { + use super::Quality; + + // TryFrom is probably better, but it's not stable. For now, we want to + // keep the functionality of the `q` function, while allowing it to be + // generic over `f32` and `u16`. + // + // `q` would panic before, so keep that behavior. `TryFrom` can be + // introduced later for a non-panicking conversion. + + pub trait IntoQuality: Sealed + Sized { + fn into_quality(self) -> Quality; + } + + impl IntoQuality for f32 { + fn into_quality(self) -> Quality { + assert!( + self >= 0f32 && self <= 1f32, + "float must be between 0.0 and 1.0" + ); + super::from_f32(self) + } + } + + impl IntoQuality for u16 { + fn into_quality(self) -> Quality { + assert!(self <= 1000, "u16 must be between 0 and 1000"); + Quality(self) + } + } + + pub trait Sealed {} + impl Sealed for u16 {} + impl Sealed for f32 {} +} + +#[cfg(test)] +mod tests { + use super::super::encoding::*; + use super::*; + + #[test] + fn test_quality_item_fmt_q_1() { + let x = qitem(Chunked); + assert_eq!(format!("{}", x), "chunked"); + } + #[test] + fn test_quality_item_fmt_q_0001() { + let x = QualityItem::new(Chunked, Quality(1)); + assert_eq!(format!("{}", x), "chunked; q=0.001"); + } + #[test] + fn test_quality_item_fmt_q_05() { + // Custom value + let x = QualityItem { + item: EncodingExt("identity".to_owned()), + quality: Quality(500), + }; + assert_eq!(format!("{}", x), "identity; q=0.5"); + } + + #[test] + fn test_quality_item_fmt_q_0() { + // Custom value + let x = QualityItem { + item: EncodingExt("identity".to_owned()), + quality: Quality(0), + }; + assert_eq!(x.to_string(), "identity; q=0"); + } + + #[test] + fn test_quality_item_from_str1() { + let x: Result, _> = "chunked".parse(); + assert_eq!( + x.unwrap(), + QualityItem { + item: Chunked, + quality: Quality(1000), + } + ); + } + #[test] + fn test_quality_item_from_str2() { + let x: Result, _> = "chunked; q=1".parse(); + assert_eq!( + x.unwrap(), + QualityItem { + item: Chunked, + quality: Quality(1000), + } + ); + } + #[test] + fn test_quality_item_from_str3() { + let x: Result, _> = "gzip; q=0.5".parse(); + assert_eq!( + x.unwrap(), + QualityItem { + item: Gzip, + quality: Quality(500), + } + ); + } + #[test] + fn test_quality_item_from_str4() { + let x: Result, _> = "gzip; q=0.273".parse(); + assert_eq!( + x.unwrap(), + QualityItem { + item: Gzip, + quality: Quality(273), + } + ); + } + #[test] + fn test_quality_item_from_str5() { + let x: Result, _> = "gzip; q=0.2739999".parse(); + assert!(x.is_err()); + } + #[test] + fn test_quality_item_from_str6() { + let x: Result, _> = "gzip; q=2".parse(); + assert!(x.is_err()); + } + #[test] + fn test_quality_item_ordering() { + let x: QualityItem = "gzip; q=0.5".parse().ok().unwrap(); + let y: QualityItem = "gzip; q=0.273".parse().ok().unwrap(); + let comparision_result: bool = x.gt(&y); + assert!(comparision_result) + } + + #[test] + fn test_quality() { + assert_eq!(q(0.5), Quality(500)); + } + + #[test] + #[should_panic] // FIXME - 32-bit msvc unwinding broken + #[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)] + fn test_quality_invalid() { + q(-1.0); + } + + #[test] + #[should_panic] // FIXME - 32-bit msvc unwinding broken + #[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)] + fn test_quality_invalid2() { + q(2.0); + } + + #[test] + fn test_fuzzing_bugs() { + assert!("99999;".parse::>().is_err()); + assert!("\x0d;;;=\u{d6aa}==".parse::>().is_err()) + } +} diff --git a/tests/test_ws.rs b/tests/test_ws.rs index bf5a3c41e..e5a54c7c1 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -6,7 +6,7 @@ use actix_service::NewService; use actix_utils::framed::IntoFramed; use actix_utils::stream::TakeItem; use bytes::{Bytes, BytesMut}; -use futures::future::{lazy, ok, Either}; +use futures::future::{ok, Either}; use futures::{Future, Sink, Stream}; use actix_http::{h1, ws, ResponseError, SendResponse, ServiceConfig}; From 7d49a07f91156253a371000d24fa9e4291cbce46 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Feb 2019 13:39:15 -0800 Subject: [PATCH 123/427] add h1/h2 payload --- src/lib.rs | 1 + src/payload.rs | 32 ++++++++++++++++++++++++++++++++ src/request.rs | 12 +++++++----- src/service/mod.rs | 2 -- src/test.rs | 8 ++++---- 5 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 src/payload.rs diff --git a/src/lib.rs b/src/lib.rs index a3ca52eda..715823930 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,7 @@ mod service; pub mod error; pub mod h1; pub mod h2; +pub mod payload; pub mod test; pub mod ws; diff --git a/src/payload.rs b/src/payload.rs new file mode 100644 index 000000000..ede1281ec --- /dev/null +++ b/src/payload.rs @@ -0,0 +1,32 @@ +use bytes::Bytes; +use derive_more::From; +use futures::{Poll, Stream}; +use h2::RecvStream; + +use crate::error::PayloadError; + +#[derive(From)] +pub enum Payload { + H1(crate::h1::Payload), + H2(crate::h2::Payload), + Dyn(Box>), +} + +impl From for Payload { + fn from(v: RecvStream) -> Self { + Payload::H2(crate::h2::Payload::new(v)) + } +} + +impl Stream for Payload { + type Item = Bytes; + type Error = PayloadError; + + fn poll(&mut self) -> Poll, Self::Error> { + match self { + Payload::H1(ref mut pl) => pl.poll(), + Payload::H2(ref mut pl) => pl.poll(), + Payload::Dyn(ref mut pl) => pl.poll(), + } + } +} diff --git a/src/request.rs b/src/request.rs index 4df95d450..f6be69ddb 100644 --- a/src/request.rs +++ b/src/request.rs @@ -10,8 +10,7 @@ use crate::error::PayloadError; use crate::extensions::Extensions; use crate::httpmessage::HttpMessage; use crate::message::{Message, MessagePool, RequestHead}; - -use crate::h1::Payload; +use crate::payload::Payload; /// Request pub struct Request

    { @@ -39,7 +38,7 @@ impl Request { /// Create new Request instance pub fn new() -> Request { Request { - payload: Some(Payload::empty()), + payload: None, inner: MessagePool::get_message(), } } @@ -55,9 +54,12 @@ impl Request { } /// Create new Request instance - pub fn set_payload

    (self, payload: P) -> Request

    { + pub fn set_payload(self, payload: I) -> Request

    + where + I: Into

    , + { Request { - payload: Some(payload), + payload: Some(payload.into()), inner: self.inner.clone(), } } diff --git a/src/service/mod.rs b/src/service/mod.rs index 3939ab99c..83a40bd12 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -1,5 +1,3 @@ -use h2::RecvStream; - mod senderror; pub use self::senderror::{SendError, SendResponse}; diff --git a/src/test.rs b/src/test.rs index c68bbf8e5..cd160e60a 100644 --- a/src/test.rs +++ b/src/test.rs @@ -6,8 +6,8 @@ use cookie::Cookie; use http::header::HeaderName; use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; -use crate::h1::Payload; use crate::header::{Header, IntoHeaderValue}; +use crate::payload::Payload; use crate::Request; /// Test `Request` builder @@ -125,9 +125,9 @@ impl TestRequest { /// Set request payload pub fn set_payload>(mut self, data: B) -> Self { - let mut payload = Payload::empty(); + let mut payload = crate::h1::Payload::empty(); payload.unread_data(data.into()); - self.payload = Some(payload); + self.payload = Some(payload.into()); self } @@ -151,7 +151,7 @@ impl TestRequest { let mut req = if let Some(pl) = payload { Request::with_payload(pl) } else { - Request::with_payload(Payload::empty()) + Request::with_payload(crate::h1::Payload::empty().into()) }; let inner = req.inner_mut(); From 5575ee7d2d0ce5281c5980a9b6c8769e2d5c9d85 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Feb 2019 13:41:50 -0800 Subject: [PATCH 124/427] use same payload type for h1 and h2 --- src/h1/service.rs | 3 ++- src/h2/dispatcher.rs | 5 +++-- src/h2/service.rs | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/h1/service.rs b/src/h1/service.rs index c35d18714..fbc0a2f0f 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -11,6 +11,7 @@ use log::error; use crate::body::MessageBody; use crate::config::{KeepAlive, ServiceConfig}; use crate::error::{DispatchError, ParseError}; +use crate::payload::Payload; use crate::request::Request; use crate::response::Response; @@ -27,7 +28,7 @@ pub struct H1Service { impl H1Service where - S: NewService> + Clone, + S: NewService, Response = Response> + Clone, S::Service: Clone, S::Error: Debug, B: MessageBody, diff --git a/src/h2/dispatcher.rs b/src/h2/dispatcher.rs index 2994d0a3d..301777a82 100644 --- a/src/h2/dispatcher.rs +++ b/src/h2/dispatcher.rs @@ -21,10 +21,11 @@ use crate::body::{Body, BodyLength, MessageBody, ResponseBody}; use crate::config::ServiceConfig; use crate::error::{DispatchError, Error, ParseError, PayloadError, ResponseError}; use crate::message::ResponseHead; +use crate::payload::Payload; use crate::request::Request; use crate::response::Response; -use super::{H2ServiceResult, Payload}; +use super::H2ServiceResult; const CHUNK_SIZE: usize = 16_384; @@ -113,7 +114,7 @@ where } let (parts, body) = req.into_parts(); - let mut req = Request::with_payload(Payload::new(body)); + let mut req = Request::with_payload(body.into()); let head = &mut req.inner_mut().head; head.uri = parts.uri; diff --git a/src/h2/service.rs b/src/h2/service.rs index b598b0a6d..5759b55e2 100644 --- a/src/h2/service.rs +++ b/src/h2/service.rs @@ -14,11 +14,12 @@ use log::error; use crate::body::MessageBody; use crate::config::{KeepAlive, ServiceConfig}; use crate::error::{DispatchError, Error, ParseError, ResponseError}; +use crate::payload::Payload; use crate::request::Request; use crate::response::Response; use super::dispatcher::Dispatcher; -use super::{H2ServiceResult, Payload}; +use super::H2ServiceResult; /// `NewService` implementation for HTTP2 transport pub struct H2Service { From 2a6e4dc7ab0ea8fca3df3ebbb7e1905e4a7daa42 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Feb 2019 19:26:12 -0800 Subject: [PATCH 125/427] use non mutable self for HttpMessage::payload() for ergonomic reasons --- src/client/h2proto.rs | 3 ++- src/client/response.rs | 13 +++++++------ src/httpmessage.rs | 2 +- src/lib.rs | 2 +- src/request.rs | 18 +++++++++--------- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/client/h2proto.rs b/src/client/h2proto.rs index f2f18d935..ecd18cf82 100644 --- a/src/client/h2proto.rs +++ b/src/client/h2proto.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::time; use actix_codec::{AsyncRead, AsyncWrite}; @@ -110,7 +111,7 @@ where Ok(ClientResponse { head, - payload: Some(Box::new(Payload::new(body))), + payload: RefCell::new(Some(Box::new(Payload::new(body)))), }) }) .from_err() diff --git a/src/client/response.rs b/src/client/response.rs index 65c59f2a6..7a83d825d 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::fmt; use bytes::Bytes; @@ -13,7 +14,7 @@ use crate::message::{Head, ResponseHead}; #[derive(Default)] pub struct ClientResponse { pub(crate) head: ResponseHead, - pub(crate) payload: Option, + pub(crate) payload: RefCell>, } impl HttpMessage for ClientResponse { @@ -24,8 +25,8 @@ impl HttpMessage for ClientResponse { } #[inline] - fn payload(&mut self) -> Option { - self.payload.take() + fn payload(&self) -> Option { + self.payload.borrow_mut().take() } } @@ -34,7 +35,7 @@ impl ClientResponse { pub fn new() -> ClientResponse { ClientResponse { head: ResponseHead::default(), - payload: None, + payload: RefCell::new(None), } } @@ -80,7 +81,7 @@ impl ClientResponse { /// Set response payload pub fn set_payload(&mut self, payload: PayloadStream) { - self.payload = Some(payload); + *self.payload.get_mut() = Some(payload); } } @@ -89,7 +90,7 @@ impl Stream for ClientResponse { type Error = PayloadError; fn poll(&mut self) -> Poll, Self::Error> { - if let Some(ref mut payload) = self.payload { + if let Some(ref mut payload) = self.payload.get_mut() { payload.poll() } else { Ok(Async::Ready(None)) diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 39aa1b689..47fc57d6c 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -25,7 +25,7 @@ pub trait HttpMessage: Sized { fn headers(&self) -> &HeaderMap; /// Message payload stream - fn payload(&mut self) -> Option; + fn payload(&self) -> Option; #[doc(hidden)] /// Get a header diff --git a/src/lib.rs b/src/lib.rs index 715823930..f34da84c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,7 +75,7 @@ mod extensions; mod header; mod helpers; mod httpcodes; -mod httpmessage; +pub mod httpmessage; mod json; mod message; mod request; diff --git a/src/request.rs b/src/request.rs index f6be69ddb..519cc38ed 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,4 +1,4 @@ -use std::cell::{Ref, RefMut}; +use std::cell::{Ref, RefCell, RefMut}; use std::fmt; use std::rc::Rc; @@ -14,7 +14,7 @@ use crate::payload::Payload; /// Request pub struct Request

    { - pub(crate) payload: Option

    , + pub(crate) payload: RefCell>, pub(crate) inner: Rc>, } @@ -29,8 +29,8 @@ where } #[inline] - fn payload(&mut self) -> Option

    { - self.payload.take() + fn payload(&self) -> Option

    { + self.payload.borrow_mut().take() } } @@ -38,7 +38,7 @@ impl Request { /// Create new Request instance pub fn new() -> Request { Request { - payload: None, + payload: RefCell::new(None), inner: MessagePool::get_message(), } } @@ -48,7 +48,7 @@ impl Request { /// Create new Request instance pub fn with_payload(payload: Payload) -> Request { Request { - payload: Some(payload), + payload: RefCell::new(Some(payload.into())), inner: MessagePool::get_message(), } } @@ -59,7 +59,7 @@ impl Request { I: Into

    , { Request { - payload: Some(payload.into()), + payload: RefCell::new(Some(payload.into())), inner: self.inner.clone(), } } @@ -67,9 +67,9 @@ impl Request { /// Take request's payload pub fn take_payload(mut self) -> (Option, Request<()>) { ( - self.payload.take(), + self.payload.get_mut().take(), Request { - payload: Some(()), + payload: RefCell::new(None), inner: self.inner.clone(), }, ) From a7a2d4cf5c82320fc742e479a6e3a02f86f68090 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Feb 2019 19:53:48 -0800 Subject: [PATCH 126/427] fix warns --- examples/echo.rs | 2 +- examples/echo2.rs | 2 +- src/httpmessage.rs | 43 +++++++++++++++++++++---------------------- src/json.rs | 10 +++++----- tests/test_client.rs | 4 ++-- tests/test_server.rs | 20 ++++++++++---------- 6 files changed, 40 insertions(+), 41 deletions(-) diff --git a/examples/echo.rs b/examples/echo.rs index 03d5b4706..5b68024f1 100644 --- a/examples/echo.rs +++ b/examples/echo.rs @@ -18,7 +18,7 @@ fn main() { .client_timeout(1000) .client_disconnect(1000) .server_hostname("localhost") - .finish(|mut req: Request| { + .finish(|req: Request| { req.body().limit(512).and_then(|bytes: Bytes| { info!("request body: {:?}", bytes); let mut res = Response::Ok(); diff --git a/examples/echo2.rs b/examples/echo2.rs index 2fd9cbcff..daaafa087 100644 --- a/examples/echo2.rs +++ b/examples/echo2.rs @@ -8,7 +8,7 @@ use futures::Future; use log::info; use std::env; -fn handle_request(mut req: Request) -> impl Future { +fn handle_request(req: Request) -> impl Future { req.body().limit(512).from_err().and_then(|bytes: Bytes| { info!("request body: {:?}", bytes); let mut res = Response::Ok(); diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 47fc57d6c..f071cd7bc 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -128,7 +128,7 @@ pub trait HttpMessage: Sized { /// } /// # fn main() {} /// ``` - fn body(&mut self) -> MessageBody { + fn body(&self) -> MessageBody { MessageBody::new(self) } @@ -162,7 +162,7 @@ pub trait HttpMessage: Sized { /// } /// # fn main() {} /// ``` - fn urlencoded(&mut self) -> UrlEncoded { + fn urlencoded(&self) -> UrlEncoded { UrlEncoded::new(self) } @@ -198,12 +198,12 @@ pub trait HttpMessage: Sized { /// } /// # fn main() {} /// ``` - fn json(&mut self) -> JsonBody { + fn json(&self) -> JsonBody { JsonBody::new(self) } /// Return stream of lines. - fn readlines(&mut self) -> Readlines { + fn readlines(&self) -> Readlines { Readlines::new(self) } } @@ -220,10 +220,10 @@ pub struct Readlines { impl Readlines { /// Create a new stream to read request line by line. - fn new(req: &mut T) -> Self { + fn new(req: &T) -> Self { let encoding = match req.encoding() { Ok(enc) => enc, - Err(err) => return Self::err(req, err.into()), + Err(err) => return Self::err(err.into()), }; Readlines { @@ -242,9 +242,9 @@ impl Readlines { self } - fn err(req: &mut T, err: ReadlinesError) -> Self { + fn err(err: ReadlinesError) -> Self { Readlines { - stream: req.payload(), + stream: None, buff: BytesMut::new(), limit: 262_144, checked_buff: true, @@ -366,7 +366,7 @@ pub struct MessageBody { impl MessageBody { /// Create `MessageBody` for request. - pub fn new(req: &mut T) -> MessageBody { + pub fn new(req: &T) -> MessageBody { let mut len = None; if let Some(l) = req.headers().get(header::CONTENT_LENGTH) { if let Ok(s) = l.to_str() { @@ -465,7 +465,7 @@ pub struct UrlEncoded { impl UrlEncoded { /// Create a new future to URL encode a request - pub fn new(req: &mut T) -> UrlEncoded { + pub fn new(req: &T) -> UrlEncoded { // check content type if req.content_type().to_lowercase() != "application/x-www-form-urlencoded" { return Self::err(UrlencodedError::ContentType); @@ -702,7 +702,7 @@ mod tests { #[test] fn test_urlencoded_error() { - let mut req = TestRequest::with_header( + let req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) @@ -713,7 +713,7 @@ mod tests { UrlencodedError::UnknownLength ); - let mut req = TestRequest::with_header( + let req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) @@ -724,7 +724,7 @@ mod tests { UrlencodedError::Overflow ); - let mut req = TestRequest::with_header(header::CONTENT_TYPE, "text/plain") + let req = TestRequest::with_header(header::CONTENT_TYPE, "text/plain") .header(header::CONTENT_LENGTH, "10") .finish(); assert_eq!( @@ -735,7 +735,7 @@ mod tests { #[test] fn test_urlencoded() { - let mut req = TestRequest::with_header( + let req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) @@ -751,7 +751,7 @@ mod tests { }) ); - let mut req = TestRequest::with_header( + let req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8", ) @@ -770,20 +770,19 @@ mod tests { #[test] fn test_message_body() { - let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "xxxx").finish(); + let req = TestRequest::with_header(header::CONTENT_LENGTH, "xxxx").finish(); match req.body().poll().err().unwrap() { PayloadError::UnknownLength => (), _ => unreachable!("error"), } - let mut req = - TestRequest::with_header(header::CONTENT_LENGTH, "1000000").finish(); + let req = TestRequest::with_header(header::CONTENT_LENGTH, "1000000").finish(); match req.body().poll().err().unwrap() { PayloadError::Overflow => (), _ => unreachable!("error"), } - let mut req = TestRequest::default() + let req = TestRequest::default() .set_payload(Bytes::from_static(b"test")) .finish(); match req.body().poll().ok().unwrap() { @@ -791,7 +790,7 @@ mod tests { _ => unreachable!("error"), } - let mut req = TestRequest::default() + let req = TestRequest::default() .set_payload(Bytes::from_static(b"11111111111111")) .finish(); match req.body().limit(5).poll().err().unwrap() { @@ -802,14 +801,14 @@ mod tests { #[test] fn test_readlines() { - let mut req = TestRequest::default() + let req = TestRequest::default() .set_payload(Bytes::from_static( b"Lorem Ipsum is simply dummy text of the printing and typesetting\n\ industry. Lorem Ipsum has been the industry's standard dummy\n\ Contrary to popular belief, Lorem Ipsum is not simply random text.", )) .finish(); - let mut r = Readlines::new(&mut req); + let mut r = Readlines::new(&req); match r.poll().ok().unwrap() { Async::Ready(Some(s)) => assert_eq!( s, diff --git a/src/json.rs b/src/json.rs index 6cd1e87ab..f750f545d 100644 --- a/src/json.rs +++ b/src/json.rs @@ -50,7 +50,7 @@ pub struct JsonBody { impl JsonBody { /// Create `JsonBody` for request. - pub fn new(req: &mut T) -> Self { + pub fn new(req: &T) -> Self { // check content-type let json = if let Ok(Some(mime)) = req.mime_type() { mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) @@ -164,11 +164,11 @@ mod tests { #[test] fn test_json_body() { - let mut req = TestRequest::default().finish(); + let req = TestRequest::default().finish(); let mut json = req.json::(); assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); - let mut req = TestRequest::default() + let req = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/text"), @@ -177,7 +177,7 @@ mod tests { let mut json = req.json::(); assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); - let mut req = TestRequest::default() + let req = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), @@ -190,7 +190,7 @@ mod tests { let mut json = req.json::().limit(100); assert_eq!(json.poll().err().unwrap(), JsonPayloadError::Overflow); - let mut req = TestRequest::default() + let req = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), diff --git a/tests/test_client.rs b/tests/test_client.rs index 6f502b0af..606bac22a 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -47,7 +47,7 @@ fn test_h1_v2() { assert!(repr.contains("ClientRequest")); assert!(repr.contains("x-test")); - let mut response = srv.block_on(request.send(&mut connector)).unwrap(); + let response = srv.block_on(request.send(&mut connector)).unwrap(); assert!(response.status().is_success()); // read response @@ -55,7 +55,7 @@ fn test_h1_v2() { assert_eq!(bytes, Bytes::from_static(STR.as_ref())); let request = srv.post().finish().unwrap(); - let mut response = srv.block_on(request.send(&mut connector)).unwrap(); + let response = srv.block_on(request.send(&mut connector)).unwrap(); assert!(response.status().is_success()); // read response diff --git a/tests/test_server.rs b/tests/test_server.rs index 53db38403..9fa27e71b 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -89,7 +89,7 @@ fn test_h2_body() -> std::io::Result<()> { .map_err(|e| println!("Openssl error: {}", e)) .and_then( h2::H2Service::build() - .finish(|mut req: Request<_>| { + .finish(|req: Request<_>| { req.body() .limit(1024 * 1024) .and_then(|body| Ok(Response::Ok().body(body))) @@ -101,7 +101,7 @@ fn test_h2_body() -> std::io::Result<()> { let req = client::ClientRequest::get(srv.surl("/")) .body(data.clone()) .unwrap(); - let mut response = srv.send_request(req).unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); let body = srv.block_on(response.body().limit(1024 * 1024)).unwrap(); @@ -350,7 +350,7 @@ fn test_headers() { let req = srv.get().finish().unwrap(); - let mut response = srv.block_on(req.send(&mut connector)).unwrap(); + let response = srv.block_on(req.send(&mut connector)).unwrap(); assert!(response.status().is_success()); // read response @@ -387,7 +387,7 @@ fn test_body() { }); let req = srv.get().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response @@ -402,7 +402,7 @@ fn test_head_empty() { }); let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); { @@ -428,7 +428,7 @@ fn test_head_binary() { }); let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); { @@ -477,7 +477,7 @@ fn test_body_length() { }); let req = srv.get().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response @@ -496,7 +496,7 @@ fn test_body_chunked_explicit() { }); let req = srv.get().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response @@ -517,7 +517,7 @@ fn test_body_chunked_implicit() { }); let req = srv.get().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response @@ -540,7 +540,7 @@ fn test_response_http_error_handling() { }); let req = srv.get().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let response = srv.send_request(req).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response From b0e36fdcf9c3eb43606b3980fd12bf53bb8a0cd7 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Feb 2019 21:16:46 -0800 Subject: [PATCH 127/427] simplify Message api --- src/h1/codec.rs | 8 +- src/h1/decoder.rs | 39 +++--- src/h2/dispatcher.rs | 2 +- src/message.rs | 104 ++++++++++---- src/request.rs | 95 +++++-------- src/response.rs | 321 +++++++++++++++---------------------------- src/test.rs | 10 +- 7 files changed, 243 insertions(+), 336 deletions(-) diff --git a/src/h1/codec.rs b/src/h1/codec.rs index fbc8b4a58..23feda505 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -115,10 +115,10 @@ impl Decoder for Codec { None => None, }) } else if let Some((req, payload)) = self.decoder.decode(src)? { - self.flags - .set(Flags::HEAD, req.inner.head.method == Method::HEAD); - self.version = req.inner().head.version; - self.ctype = req.inner().head.connection_type(); + let head = req.head(); + self.flags.set(Flags::HEAD, head.method == Method::HEAD); + self.version = head.version; + self.ctype = head.connection_type(); if self.ctype == ConnectionType::KeepAlive && !self.flags.contains(Flags::KEEPALIVE_ENABLED) { diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 74e1fb68c..80bca94c5 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -159,7 +159,7 @@ pub(crate) trait MessageType: Sized { impl MessageType for Request { fn set_connection_type(&mut self, ctype: Option) { - self.inner_mut().head.ctype = ctype; + self.head_mut().ctype = ctype; } fn headers_mut(&mut self) -> &mut HeaderMap { @@ -218,12 +218,10 @@ impl MessageType for Request { } }; - { - let inner = msg.inner_mut(); - inner.head.uri = uri; - inner.head.method = method; - inner.head.version = ver; - } + let head = msg.head_mut(); + head.uri = uri; + head.method = method; + head.version = ver; Ok(Some((msg, decoder))) } @@ -817,7 +815,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert_eq!(req.inner().head.ctype, Some(ConnectionType::Close)); + assert_eq!(req.head().ctype, Some(ConnectionType::Close)); let mut buf = BytesMut::from( "GET /test HTTP/1.1\r\n\ @@ -825,7 +823,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert_eq!(req.inner().head.ctype, Some(ConnectionType::Close)); + assert_eq!(req.head().ctype, Some(ConnectionType::Close)); } #[test] @@ -836,7 +834,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert_eq!(req.inner().head.ctype, Some(ConnectionType::Close)); + assert_eq!(req.head().ctype, Some(ConnectionType::Close)); } #[test] @@ -847,7 +845,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert_eq!(req.inner().head.ctype, Some(ConnectionType::KeepAlive)); + assert_eq!(req.head().ctype, Some(ConnectionType::KeepAlive)); let mut buf = BytesMut::from( "GET /test HTTP/1.0\r\n\ @@ -855,7 +853,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert_eq!(req.inner().head.ctype, Some(ConnectionType::KeepAlive)); + assert_eq!(req.head().ctype, Some(ConnectionType::KeepAlive)); } #[test] @@ -866,7 +864,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert_eq!(req.inner().head.ctype, Some(ConnectionType::KeepAlive)); + assert_eq!(req.head().ctype, Some(ConnectionType::KeepAlive)); } #[test] @@ -877,7 +875,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert_eq!(req.inner().head.connection_type(), ConnectionType::Close); + assert_eq!(req.head().connection_type(), ConnectionType::Close); } #[test] @@ -888,11 +886,8 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert_eq!(req.inner().head.ctype, None); - assert_eq!( - req.inner().head.connection_type(), - ConnectionType::KeepAlive - ); + assert_eq!(req.head().ctype, None); + assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); } #[test] @@ -905,7 +900,7 @@ mod tests { let req = parse_ready!(&mut buf); assert!(req.upgrade()); - assert_eq!(req.inner().head.ctype, Some(ConnectionType::Upgrade)); + assert_eq!(req.head().ctype, Some(ConnectionType::Upgrade)); let mut buf = BytesMut::from( "GET /test HTTP/1.1\r\n\ @@ -915,7 +910,7 @@ mod tests { let req = parse_ready!(&mut buf); assert!(req.upgrade()); - assert_eq!(req.inner().head.ctype, Some(ConnectionType::Upgrade)); + assert_eq!(req.head().ctype, Some(ConnectionType::Upgrade)); } #[test] @@ -1013,7 +1008,7 @@ mod tests { ); let mut reader = MessageDecoder::::default(); let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); - assert_eq!(req.inner().head.ctype, Some(ConnectionType::Upgrade)); + assert_eq!(req.head().ctype, Some(ConnectionType::Upgrade)); assert!(req.upgrade()); assert!(pl.is_unhandled()); } diff --git a/src/h2/dispatcher.rs b/src/h2/dispatcher.rs index 301777a82..001acc560 100644 --- a/src/h2/dispatcher.rs +++ b/src/h2/dispatcher.rs @@ -116,7 +116,7 @@ where let (parts, body) = req.into_parts(); let mut req = Request::with_payload(body.into()); - let head = &mut req.inner_mut().head; + let head = &mut req.head_mut(); head.uri = parts.uri; head.method = parts.method; head.version = parts.version; diff --git a/src/message.rs b/src/message.rs index a73392221..08edeef38 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,4 +1,4 @@ -use std::cell::RefCell; +use std::cell::{Ref, RefCell, RefMut}; use std::collections::VecDeque; use std::rc::Rc; @@ -146,12 +146,59 @@ impl ResponseHead { } pub struct Message { - pub head: T, - pub extensions: RefCell, - pub(crate) pool: &'static MessagePool, + inner: Rc>, + pool: &'static MessagePool, } impl Message { + /// Get new message from the pool of objects + pub fn new() -> Self { + T::pool().get_message() + } + + /// Message extensions + #[inline] + pub fn extensions(&self) -> Ref { + self.inner.as_ref().extensions.borrow() + } + + /// Mutable reference to a the message's extensions + #[inline] + pub fn extensions_mut(&self) -> RefMut { + self.inner.as_ref().extensions.borrow_mut() + } +} + +impl std::ops::Deref for Message { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner.as_ref().head + } +} + +impl std::ops::DerefMut for Message { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut Rc::get_mut(&mut self.inner) + .expect("Multiple copies exist") + .head + } +} + +impl Drop for Message { + fn drop(&mut self) { + if Rc::strong_count(&self.inner) == 1 { + self.pool.release(self.inner.clone()); + } + } +} + +struct MessageInner { + head: T, + extensions: RefCell, +} + +impl MessageInner { #[inline] /// Reset request instance pub fn reset(&mut self) { @@ -160,10 +207,9 @@ impl Message { } } -impl Default for Message { +impl Default for MessageInner { fn default() -> Self { - Message { - pool: T::pool(), + MessageInner { head: T::default(), extensions: RefCell::new(Extensions::new()), } @@ -172,41 +218,39 @@ impl Default for Message { #[doc(hidden)] /// Request's objects pool -pub struct MessagePool(RefCell>>>); +pub struct MessagePool(RefCell>>>); thread_local!(static REQUEST_POOL: &'static MessagePool = MessagePool::::create()); thread_local!(static RESPONSE_POOL: &'static MessagePool = MessagePool::::create()); -impl MessagePool { - /// Get default request's pool - pub fn pool() -> &'static MessagePool { - REQUEST_POOL.with(|p| *p) - } - - /// Get Request object - #[inline] - pub fn get_message() -> Rc> { - REQUEST_POOL.with(|pool| { - if let Some(mut msg) = pool.0.borrow_mut().pop_front() { - if let Some(r) = Rc::get_mut(&mut msg) { - r.reset(); - } - return msg; - } - Rc::new(Message::default()) - }) - } -} - impl MessagePool { fn create() -> &'static MessagePool { let pool = MessagePool(RefCell::new(VecDeque::with_capacity(128))); Box::leak(Box::new(pool)) } + /// Get message from the pool + #[inline] + fn get_message(&'static self) -> Message { + if let Some(mut msg) = self.0.borrow_mut().pop_front() { + if let Some(r) = Rc::get_mut(&mut msg) { + r.reset(); + } + Message { + inner: msg, + pool: self, + } + } else { + Message { + inner: Rc::new(MessageInner::default()), + pool: self, + } + } + } + #[inline] /// Release request instance - pub(crate) fn release(&self, msg: Rc>) { + fn release(&self, msg: Rc>) { let v = &mut self.0.borrow_mut(); if v.len() < 128 { v.push_front(msg); diff --git a/src/request.rs b/src/request.rs index 519cc38ed..0064de4e0 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,6 +1,5 @@ use std::cell::{Ref, RefCell, RefMut}; use std::fmt; -use std::rc::Rc; use bytes::Bytes; use futures::Stream; @@ -9,13 +8,13 @@ use http::{header, HeaderMap, Method, Uri, Version}; use crate::error::PayloadError; use crate::extensions::Extensions; use crate::httpmessage::HttpMessage; -use crate::message::{Message, MessagePool, RequestHead}; +use crate::message::{Message, RequestHead}; use crate::payload::Payload; /// Request pub struct Request

    { pub(crate) payload: RefCell>, - pub(crate) inner: Rc>, + pub(crate) inner: Message, } impl

    HttpMessage for Request

    @@ -25,7 +24,7 @@ where type Stream = P; fn headers(&self) -> &HeaderMap { - &self.inner.head.headers + &self.head().headers } #[inline] @@ -34,12 +33,21 @@ where } } +impl From> for Request { + fn from(msg: Message) -> Self { + Request { + payload: RefCell::new(None), + inner: msg, + } + } +} + impl Request { /// Create new Request instance pub fn new() -> Request { Request { payload: RefCell::new(None), - inner: MessagePool::get_message(), + inner: Message::new(), } } } @@ -49,7 +57,7 @@ impl Request { pub fn with_payload(payload: Payload) -> Request { Request { payload: RefCell::new(Some(payload.into())), - inner: MessagePool::get_message(), + inner: Message::new(), } } @@ -60,123 +68,90 @@ impl Request { { Request { payload: RefCell::new(Some(payload.into())), - inner: self.inner.clone(), + inner: self.inner, } } - /// Take request's payload - pub fn take_payload(mut self) -> (Option, Request<()>) { - ( - self.payload.get_mut().take(), - Request { - payload: RefCell::new(None), - inner: self.inner.clone(), - }, - ) - } - - // /// Create new Request instance with pool - // pub(crate) fn with_pool(pool: &'static MessagePool) -> Request { - // Request { - // inner: Rc::new(Message { - // pool, - // url: Url::default(), - // head: RequestHead::default(), - // status: StatusCode::OK, - // flags: Cell::new(MessageFlags::empty()), - // payload: RefCell::new(None), - // extensions: RefCell::new(Extensions::new()), - // }), - // } - // } - - #[inline] - #[doc(hidden)] - pub fn inner(&self) -> &Message { - self.inner.as_ref() - } - - #[inline] - #[doc(hidden)] - pub fn inner_mut(&mut self) -> &mut Message { - Rc::get_mut(&mut self.inner).expect("Multiple copies exist") + /// Split request into request head and payload + pub fn into_parts(mut self) -> (Message, Option) { + (self.inner, self.payload.get_mut().take()) } #[inline] /// Http message part of the request pub fn head(&self) -> &RequestHead { - &self.inner.as_ref().head + &*self.inner } #[inline] #[doc(hidden)] /// Mutable reference to a http message part of the request pub fn head_mut(&mut self) -> &mut RequestHead { - &mut self.inner_mut().head + &mut *self.inner } /// Request's uri. #[inline] pub fn uri(&self) -> &Uri { - &self.inner().head.uri + &self.head().uri } /// Mutable reference to the request's uri. #[inline] pub fn uri_mut(&mut self) -> &mut Uri { - &mut self.inner_mut().head.uri + &mut self.head_mut().uri } /// Read the Request method. #[inline] pub fn method(&self) -> &Method { - &self.inner().head.method + &self.head().method } /// Read the Request Version. #[inline] pub fn version(&self) -> Version { - self.inner().head.version + self.head().version } /// The target path of this Request. #[inline] pub fn path(&self) -> &str { - self.inner().head.uri.path() + self.head().uri.path() } #[inline] /// Returns Request's headers. pub fn headers(&self) -> &HeaderMap { - &self.inner().head.headers + &self.head().headers } #[inline] /// Returns mutable Request's headers. pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.inner_mut().head.headers + &mut self.head_mut().headers } /// Request extensions #[inline] pub fn extensions(&self) -> Ref { - self.inner().extensions.borrow() + self.inner.extensions() } /// Mutable reference to a the request's extensions #[inline] pub fn extensions_mut(&self) -> RefMut { - self.inner().extensions.borrow_mut() + self.inner.extensions_mut() } /// Check if request requires connection upgrade pub fn upgrade(&self) -> bool { - if let Some(conn) = self.inner().head.headers.get(header::CONNECTION) { + if let Some(conn) = self.head().headers.get(header::CONNECTION) { if let Ok(s) = conn.to_str() { return s.to_lowercase().contains("upgrade"); } } - self.inner().head.method == Method::CONNECT + self.head().method == Method::CONNECT } // #[doc(hidden)] @@ -189,14 +164,6 @@ impl Request { // } } -impl Drop for Request { - fn drop(&mut self) { - if Rc::strong_count(&self.inner) == 1 { - self.inner.pool.release(self.inner.clone()); - } - } -} - impl fmt::Debug for Request { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!( diff --git a/src/response.rs b/src/response.rs index a4b65f2b4..d84100fa2 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,7 +1,4 @@ -#![allow(dead_code)] //! Http response -use std::cell::RefCell; -use std::collections::VecDeque; use std::io::Write; use std::{fmt, str}; @@ -9,26 +6,27 @@ use bytes::{BufMut, Bytes, BytesMut}; use cookie::{Cookie, CookieJar}; use futures::Stream; use http::header::{self, HeaderName, HeaderValue}; -use http::{Error as HttpError, HeaderMap, HttpTryFrom, StatusCode, Version}; +use http::{Error as HttpError, HeaderMap, HttpTryFrom, StatusCode}; use serde::Serialize; use serde_json; use crate::body::{Body, BodyStream, MessageBody, ResponseBody}; use crate::error::Error; use crate::header::{Header, IntoHeaderValue}; -use crate::message::{ConnectionType, Head, ResponseHead}; - -/// max write buffer size 64k -pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; +use crate::message::{ConnectionType, Head, Message, ResponseHead}; /// An HTTP Response -pub struct Response(Box, ResponseBody); +pub struct Response { + head: Message, + body: ResponseBody, + error: Option, +} impl Response { /// Create http response builder with specific status. #[inline] pub fn build(status: StatusCode) -> ResponseBuilder { - ResponsePool::get(status) + ResponseBuilder::new(status) } /// Create http response builder @@ -40,14 +38,21 @@ impl Response { /// Constructs a response #[inline] pub fn new(status: StatusCode) -> Response { - ResponsePool::with_body(status, Body::Empty) + let mut head: Message = Message::new(); + head.status = status; + + Response { + head, + body: ResponseBody::Body(Body::Empty), + error: None, + } } /// Constructs an error response #[inline] pub fn from_error(error: Error) -> Response { let mut resp = error.as_response_error().error_response(); - resp.get_mut().error = Some(error); + resp.error = Some(error); resp } @@ -67,7 +72,7 @@ impl Response { } ResponseBuilder { - response: Some(self.0), + head: Some(self.head), err: None, cookies: jar, } @@ -75,90 +80,85 @@ impl Response { /// Convert response to response with body pub fn into_body(self) -> Response { - let b = match self.1 { + let b = match self.body { ResponseBody::Body(b) => b, ResponseBody::Other(b) => b, }; - Response(self.0, ResponseBody::Other(b)) + Response { + head: self.head, + error: self.error, + body: ResponseBody::Other(b), + } } } impl Response { - #[inline] - fn get_ref(&self) -> &InnerResponse { - self.0.as_ref() - } - - #[inline] - fn get_mut(&mut self) -> &mut InnerResponse { - self.0.as_mut() - } - #[inline] /// Http message part of the response pub fn head(&self) -> &ResponseHead { - &self.0.as_ref().head + &*self.head } #[inline] /// Mutable reference to a http message part of the response pub fn head_mut(&mut self) -> &mut ResponseHead { - &mut self.0.as_mut().head + &mut *self.head } /// Constructs a response with body #[inline] pub fn with_body(status: StatusCode, body: B) -> Response { - ResponsePool::with_body(status, body) + let mut head: Message = Message::new(); + head.status = status; + Response { + head, + body: ResponseBody::Body(body), + error: None, + } } /// The source `error` for this response #[inline] pub fn error(&self) -> Option<&Error> { - self.get_ref().error.as_ref() + self.error.as_ref() } /// Get the response status code #[inline] pub fn status(&self) -> StatusCode { - self.get_ref().head.status + self.head.status } /// Set the `StatusCode` for this response #[inline] pub fn status_mut(&mut self) -> &mut StatusCode { - &mut self.get_mut().head.status + &mut self.head.status } /// Get the headers from the response #[inline] pub fn headers(&self) -> &HeaderMap { - &self.get_ref().head.headers + &self.head.headers } /// Get a mutable reference to the headers #[inline] pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.get_mut().head.headers + &mut self.head.headers } /// Get an iterator for the cookies set by this response #[inline] pub fn cookies(&self) -> CookieIter { CookieIter { - iter: self - .get_ref() - .head - .headers - .get_all(header::SET_COOKIE) - .iter(), + iter: self.head.headers.get_all(header::SET_COOKIE).iter(), } } /// Add a cookie to this response #[inline] pub fn add_cookie(&mut self, cookie: &Cookie) -> Result<(), HttpError> { - let h = &mut self.get_mut().head.headers; + let h = &mut self.head.headers; HeaderValue::from_str(&cookie.to_string()) .map(|c| { h.append(header::SET_COOKIE, c); @@ -170,7 +170,7 @@ impl Response { /// the number of cookies removed. #[inline] pub fn del_cookie(&mut self, name: &str) -> usize { - let h = &mut self.get_mut().head.headers; + let h = &mut self.head.headers; let vals: Vec = h .get_all(header::SET_COOKIE) .iter() @@ -196,28 +196,36 @@ impl Response { /// Connection upgrade status #[inline] pub fn upgrade(&self) -> bool { - self.get_ref().head.upgrade() + self.head.upgrade() } /// Keep-alive status for this connection pub fn keep_alive(&self) -> bool { - self.get_ref().head.keep_alive() + self.head.keep_alive() } /// Get body os this response #[inline] - pub(crate) fn body(&self) -> &ResponseBody { - &self.1 + pub fn body(&self) -> &ResponseBody { + &self.body } /// Set a body pub(crate) fn set_body(self, body: B2) -> Response { - Response(self.0, ResponseBody::Body(body)) + Response { + head: self.head, + body: ResponseBody::Body(body), + error: None, + } } /// Drop request's body pub(crate) fn drop_body(self) -> Response<()> { - Response(self.0, ResponseBody::Body(())) + Response { + head: self.head, + body: ResponseBody::Body(()), + error: None, + } } /// Set a body and return previous body value @@ -225,21 +233,14 @@ impl Response { self, body: B2, ) -> (Response, ResponseBody) { - (Response(self.0, ResponseBody::Body(body)), self.1) - } - - /// Size of response in bytes, excluding HTTP headers - pub fn response_size(&self) -> u64 { - self.get_ref().response_size - } - - /// Set response size - pub(crate) fn set_response_size(&mut self, size: u64) { - self.get_mut().response_size = size; - } - - pub(crate) fn release(self) { - ResponsePool::release(self.0); + ( + Response { + head: self.head, + body: ResponseBody::Body(body), + error: self.error, + }, + self.body, + ) } } @@ -248,15 +249,15 @@ impl fmt::Debug for Response { let res = writeln!( f, "\nResponse {:?} {}{}", - self.get_ref().head.version, - self.get_ref().head.status, - self.get_ref().head.reason.unwrap_or(""), + self.head.version, + self.head.status, + self.head.reason.unwrap_or(""), ); let _ = writeln!(f, " headers:"); - for (key, val) in self.get_ref().head.headers.iter() { + for (key, val) in self.head.headers.iter() { let _ = writeln!(f, " {:?}: {:?}", key, val); } - let _ = writeln!(f, " body: {:?}", self.1.length()); + let _ = writeln!(f, " body: {:?}", self.body.length()); res } } @@ -284,17 +285,29 @@ impl<'a> Iterator for CookieIter<'a> { /// This type can be used to construct an instance of `Response` through a /// builder-like pattern. pub struct ResponseBuilder { - response: Option>, + head: Option>, err: Option, cookies: Option, } impl ResponseBuilder { + /// Create response builder + pub fn new(status: StatusCode) -> Self { + let mut head: Message = Message::new(); + head.status = status; + + ResponseBuilder { + head: Some(head), + err: None, + cookies: None, + } + } + /// Set HTTP status code of this response. #[inline] pub fn status(&mut self, status: StatusCode) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.head.status = status; + if let Some(parts) = parts(&mut self.head, &self.err) { + parts.status = status; } self } @@ -316,10 +329,10 @@ impl ResponseBuilder { /// ``` #[doc(hidden)] pub fn set(&mut self, hdr: H) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { + if let Some(parts) = parts(&mut self.head, &self.err) { match hdr.try_into() { Ok(value) => { - parts.head.headers.append(H::name(), value); + parts.headers.append(H::name(), value); } Err(e) => self.err = Some(e.into()), } @@ -346,11 +359,11 @@ impl ResponseBuilder { HeaderName: HttpTryFrom, V: IntoHeaderValue, { - if let Some(parts) = parts(&mut self.response, &self.err) { + if let Some(parts) = parts(&mut self.head, &self.err) { match HeaderName::try_from(key) { Ok(key) => match value.try_into() { Ok(value) => { - parts.head.headers.append(key, value); + parts.headers.append(key, value); } Err(e) => self.err = Some(e.into()), }, @@ -379,11 +392,11 @@ impl ResponseBuilder { HeaderName: HttpTryFrom, V: IntoHeaderValue, { - if let Some(parts) = parts(&mut self.response, &self.err) { + if let Some(parts) = parts(&mut self.head, &self.err) { match HeaderName::try_from(key) { Ok(key) => match value.try_into() { Ok(value) => { - parts.head.headers.insert(key, value); + parts.headers.insert(key, value); } Err(e) => self.err = Some(e.into()), }, @@ -396,8 +409,8 @@ impl ResponseBuilder { /// Set the custom reason for the response. #[inline] pub fn reason(&mut self, reason: &'static str) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.head.reason = Some(reason); + if let Some(parts) = parts(&mut self.head, &self.err) { + parts.reason = Some(reason); } self } @@ -405,8 +418,8 @@ impl ResponseBuilder { /// Set connection type to KeepAlive #[inline] pub fn keep_alive(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.head.set_connection_type(ConnectionType::KeepAlive); + if let Some(parts) = parts(&mut self.head, &self.err) { + parts.set_connection_type(ConnectionType::KeepAlive); } self } @@ -417,8 +430,8 @@ impl ResponseBuilder { where V: IntoHeaderValue, { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.head.set_connection_type(ConnectionType::Upgrade); + if let Some(parts) = parts(&mut self.head, &self.err) { + parts.set_connection_type(ConnectionType::Upgrade); } self.set_header(header::UPGRADE, value) } @@ -426,8 +439,8 @@ impl ResponseBuilder { /// Force close connection, even if it is marked as keep-alive #[inline] pub fn force_close(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.head.set_connection_type(ConnectionType::Close); + if let Some(parts) = parts(&mut self.head, &self.err) { + parts.set_connection_type(ConnectionType::Close); } self } @@ -438,10 +451,10 @@ impl ResponseBuilder { where HeaderValue: HttpTryFrom, { - if let Some(parts) = parts(&mut self.response, &self.err) { + if let Some(parts) = parts(&mut self.head, &self.err) { match HeaderValue::try_from(value) { Ok(value) => { - parts.head.headers.insert(header::CONTENT_TYPE, value); + parts.headers.insert(header::CONTENT_TYPE, value); } Err(e) => self.err = Some(e.into()), }; @@ -540,20 +553,6 @@ impl ResponseBuilder { self } - // /// Set write buffer capacity - // /// - // /// This parameter makes sense only for streaming response - // /// or actor. If write buffer reaches specified capacity, stream or actor - // /// get paused. - // /// - // /// Default write buffer capacity is 64kb - // pub fn write_buffer_capacity(&mut self, cap: usize) -> &mut Self { - // if let Some(parts) = parts(&mut self.response, &self.err) { - // parts.write_capacity = cap; - // } - // self - // } - /// Set a body and generate `Response`. /// /// `ResponseBuilder` can not be used after this call. @@ -569,19 +568,23 @@ impl ResponseBuilder { return Response::from(Error::from(e)).into_body(); } - let mut response = self.response.take().expect("cannot reuse response builder"); + let mut response = self.head.take().expect("cannot reuse response builder"); if let Some(ref jar) = self.cookies { for cookie in jar.delta() { match HeaderValue::from_str(&cookie.to_string()) { Ok(val) => { - let _ = response.head.headers.append(header::SET_COOKIE, val); + let _ = response.headers.append(header::SET_COOKIE, val); } Err(e) => return Response::from(Error::from(e)).into_body(), }; } } - Response(response, ResponseBody::Body(body)) + Response { + head: response, + body: ResponseBody::Body(body), + error: None, + } } #[inline] @@ -609,9 +612,8 @@ impl ResponseBuilder { pub fn json2(&mut self, value: &T) -> Response { match serde_json::to_string(value) { Ok(body) => { - let contains = if let Some(parts) = parts(&mut self.response, &self.err) - { - parts.head.headers.contains_key(header::CONTENT_TYPE) + let contains = if let Some(parts) = parts(&mut self.head, &self.err) { + parts.headers.contains_key(header::CONTENT_TYPE) } else { true }; @@ -636,7 +638,7 @@ impl ResponseBuilder { /// This method construct new `ResponseBuilder` pub fn take(&mut self) -> ResponseBuilder { ResponseBuilder { - response: self.response.take(), + head: self.head.take(), err: self.err.take(), cookies: self.cookies.take(), } @@ -646,9 +648,9 @@ impl ResponseBuilder { #[inline] #[allow(clippy::borrowed_box)] fn parts<'a>( - parts: &'a mut Option>, + parts: &'a mut Option>, err: &Option, -) -> Option<&'a mut Box> { +) -> Option<&'a mut Message> { if err.is_some() { return None; } @@ -719,107 +721,6 @@ impl From for Response { } } -struct InnerResponse { - head: ResponseHead, - response_size: u64, - error: Option, - pool: &'static ResponsePool, -} - -impl InnerResponse { - #[inline] - fn new(status: StatusCode, pool: &'static ResponsePool) -> InnerResponse { - InnerResponse { - head: ResponseHead { - status, - version: Version::default(), - headers: HeaderMap::with_capacity(16), - reason: None, - ctype: None, - }, - pool, - response_size: 0, - error: None, - } - } -} - -/// Internal use only! -pub(crate) struct ResponsePool(RefCell>>); - -thread_local!(static POOL: &'static ResponsePool = ResponsePool::pool()); - -impl ResponsePool { - fn pool() -> &'static ResponsePool { - let pool = ResponsePool(RefCell::new(VecDeque::with_capacity(128))); - Box::leak(Box::new(pool)) - } - - pub fn get_pool() -> &'static ResponsePool { - POOL.with(|p| *p) - } - - #[inline] - pub fn get_builder( - pool: &'static ResponsePool, - status: StatusCode, - ) -> ResponseBuilder { - if let Some(mut msg) = pool.0.borrow_mut().pop_front() { - msg.head.status = status; - ResponseBuilder { - response: Some(msg), - err: None, - cookies: None, - } - } else { - let msg = Box::new(InnerResponse::new(status, pool)); - ResponseBuilder { - response: Some(msg), - err: None, - cookies: None, - } - } - } - - #[inline] - pub fn get_response( - pool: &'static ResponsePool, - status: StatusCode, - body: B, - ) -> Response { - if let Some(mut msg) = pool.0.borrow_mut().pop_front() { - msg.head.status = status; - Response(msg, ResponseBody::Body(body)) - } else { - Response( - Box::new(InnerResponse::new(status, pool)), - ResponseBody::Body(body), - ) - } - } - - #[inline] - fn get(status: StatusCode) -> ResponseBuilder { - POOL.with(|pool| ResponsePool::get_builder(pool, status)) - } - - #[inline] - fn with_body(status: StatusCode, body: B) -> Response { - POOL.with(|pool| ResponsePool::get_response(pool, status, body)) - } - - #[inline] - fn release(mut inner: Box) { - let mut p = inner.pool.0.borrow_mut(); - if p.len() < 128 { - inner.head.clear(); - inner.response_size = 0; - inner.error = None; - p.push_front(inner); - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/test.rs b/src/test.rs index cd160e60a..b0e308728 100644 --- a/src/test.rs +++ b/src/test.rs @@ -154,11 +154,11 @@ impl TestRequest { Request::with_payload(crate::h1::Payload::empty().into()) }; - let inner = req.inner_mut(); - inner.head.uri = uri; - inner.head.method = method; - inner.head.version = version; - inner.head.headers = headers; + let head = req.head_mut(); + head.uri = uri; + head.method = method; + head.version = version; + head.headers = headers; // req.set_cookies(cookies); req From ed7ca7fe079ef6e9ee419bafe54e517657846041 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Feb 2019 21:50:20 -0800 Subject: [PATCH 128/427] make Message clonable and expose as public --- Cargo.toml | 2 +- src/lib.rs | 1 + src/message.rs | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ea246762e..f940ae86c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ documentation = "https://actix.rs/api/actix-http/stable/actix_http/" categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] -license = "MIT/Apache-2.0" +license = "Apache-2.0" exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] edition = "2018" diff --git a/src/lib.rs b/src/lib.rs index f34da84c1..932b54852 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,6 +94,7 @@ pub use self::config::{KeepAlive, ServiceConfig, ServiceConfigBuilder}; pub use self::error::{Error, ResponseError, Result}; pub use self::extensions::Extensions; pub use self::httpmessage::HttpMessage; +pub use self::message::{Message, RequestHead, ResponseHead}; pub use self::request::Request; pub use self::response::Response; pub use self::service::{SendError, SendResponse}; diff --git a/src/message.rs b/src/message.rs index 08edeef38..12da9eaf1 100644 --- a/src/message.rs +++ b/src/message.rs @@ -169,6 +169,15 @@ impl Message { } } +impl Clone for Message { + fn clone(&self) -> Self { + Message { + inner: self.inner.clone(), + pool: self.pool, + } + } +} + impl std::ops::Deref for Message { type Target = T; From f3ed1b601ea6833fa20efae4d47f0251e6543b3a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Feb 2019 08:44:22 -0800 Subject: [PATCH 129/427] Change service response to Into --- src/h1/dispatcher.rs | 13 ++++++++----- src/h1/service.rs | 15 ++++++++++----- src/h2/dispatcher.rs | 14 +++++++++----- src/h2/service.rs | 21 ++++++++++++++------- 4 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 0a0ed04e2..667e674c5 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -85,8 +85,9 @@ impl State { impl Dispatcher where T: AsyncRead + AsyncWrite, - S: Service>, + S: Service, S::Error: Debug, + S::Response: Into>, B: MessageBody, { /// Create http/1 dispatcher. @@ -139,8 +140,9 @@ where impl InnerDispatcher where T: AsyncRead + AsyncWrite, - S: Service>, + S: Service, S::Error: Debug, + S::Response: Into>, B: MessageBody, { fn can_read(&self) -> bool { @@ -224,7 +226,7 @@ where State::ServiceCall(mut fut) => { match fut.poll().map_err(DispatchError::Service)? { Async::Ready(res) => { - let (res, body) = res.replace_body(()); + let (res, body) = res.into().replace_body(()); Some(self.send_response(res, body)?) } Async::NotReady => { @@ -287,7 +289,7 @@ where let mut task = self.service.call(req); match task.poll().map_err(DispatchError::Service)? { Async::Ready(res) => { - let (res, body) = res.replace_body(()); + let (res, body) = res.into().replace_body(()); self.send_response(res, body) } Async::NotReady => Ok(State::ServiceCall(task)), @@ -459,8 +461,9 @@ where impl Future for Dispatcher where T: AsyncRead + AsyncWrite, - S: Service>, + S: Service, S::Error: Debug, + S::Response: Into>, B: MessageBody, { type Item = H1ServiceResult; diff --git a/src/h1/service.rs b/src/h1/service.rs index fbc0a2f0f..02a4b15d6 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -28,9 +28,10 @@ pub struct H1Service { impl H1Service where - S: NewService, Response = Response> + Clone, + S: NewService> + Clone, S::Service: Clone, S::Error: Debug, + S::Response: Into>, B: MessageBody, { /// Create new `HttpService` instance. @@ -53,9 +54,10 @@ where impl NewService for H1Service where T: AsyncRead + AsyncWrite, - S: NewService> + Clone, + S: NewService + Clone, S::Service: Clone, S::Error: Debug, + S::Response: Into>, B: MessageBody, { type Request = T; @@ -214,9 +216,10 @@ pub struct H1ServiceResponse { impl Future for H1ServiceResponse where T: AsyncRead + AsyncWrite, - S: NewService>, + S: NewService, S::Service: Clone, S::Error: Debug, + S::Response: Into>, B: MessageBody, { type Item = H1ServiceHandler; @@ -240,8 +243,9 @@ pub struct H1ServiceHandler { impl H1ServiceHandler where - S: Service> + Clone, + S: Service + Clone, S::Error: Debug, + S::Response: Into>, B: MessageBody, { fn new(cfg: ServiceConfig, srv: S) -> H1ServiceHandler { @@ -256,8 +260,9 @@ where impl Service for H1ServiceHandler where T: AsyncRead + AsyncWrite, - S: Service> + Clone, + S: Service + Clone, S::Error: Debug, + S::Response: Into>, B: MessageBody, { type Request = T; diff --git a/src/h2/dispatcher.rs b/src/h2/dispatcher.rs index 001acc560..b7339fa1d 100644 --- a/src/h2/dispatcher.rs +++ b/src/h2/dispatcher.rs @@ -50,8 +50,9 @@ pub struct Dispatcher { impl Dispatcher where T: AsyncRead + AsyncWrite, - S: Service, Response = Response> + 'static, + S: Service> + 'static, S::Error: Into + fmt::Debug, + S::Response: Into>, B: MessageBody + 'static, { pub fn new( @@ -91,8 +92,9 @@ where impl Future for Dispatcher where T: AsyncRead + AsyncWrite, - S: Service, Response = Response> + 'static, + S: Service> + 'static, S::Error: Into + fmt::Debug, + S::Response: Into>, B: MessageBody + 'static, { type Item = (); @@ -149,8 +151,9 @@ enum ServiceResponseState { impl ServiceResponse where - S: Service, Response = Response> + 'static, + S: Service> + 'static, S::Error: Into + fmt::Debug, + S::Response: Into>, B: MessageBody + 'static, { fn prepare_response( @@ -216,8 +219,9 @@ where impl Future for ServiceResponse where - S: Service, Response = Response> + 'static, + S: Service> + 'static, S::Error: Into + fmt::Debug, + S::Response: Into>, B: MessageBody + 'static, { type Item = (); @@ -228,7 +232,7 @@ where ServiceResponseState::ServiceCall(ref mut call, ref mut send) => { match call.poll() { Ok(Async::Ready(res)) => { - let (res, body) = res.replace_body(()); + let (res, body) = res.into().replace_body(()); let mut send = send.take().unwrap(); let mut length = body.length(); diff --git a/src/h2/service.rs b/src/h2/service.rs index 5759b55e2..a1526375f 100644 --- a/src/h2/service.rs +++ b/src/h2/service.rs @@ -30,9 +30,10 @@ pub struct H2Service { impl H2Service where - S: NewService, Response = Response> + Clone, + S: NewService> + Clone, S::Service: Clone + 'static, S::Error: Into + Debug + 'static, + S::Response: Into>, B: MessageBody + 'static, { /// Create new `HttpService` instance. @@ -55,9 +56,10 @@ where impl NewService for H2Service where T: AsyncRead + AsyncWrite, - S: NewService, Response = Response> + Clone, + S: NewService> + Clone, S::Service: Clone + 'static, S::Error: Into + Debug, + S::Response: Into>, B: MessageBody + 'static, { type Request = T; @@ -235,8 +237,9 @@ pub struct H2ServiceResponse { impl Future for H2ServiceResponse where T: AsyncRead + AsyncWrite, - S: NewService, Response = Response>, + S: NewService>, S::Service: Clone + 'static, + S::Response: Into>, S::Error: Into + Debug, B: MessageBody + 'static, { @@ -261,8 +264,9 @@ pub struct H2ServiceHandler { impl H2ServiceHandler where - S: Service, Response = Response> + Clone + 'static, + S: Service> + Clone + 'static, S::Error: Into + Debug, + S::Response: Into>, B: MessageBody + 'static, { fn new(cfg: ServiceConfig, srv: S) -> H2ServiceHandler { @@ -277,8 +281,9 @@ where impl Service for H2ServiceHandler where T: AsyncRead + AsyncWrite, - S: Service, Response = Response> + Clone + 'static, + S: Service> + Clone + 'static, S::Error: Into + Debug, + S::Response: Into>, B: MessageBody + 'static, { type Request = T; @@ -312,8 +317,9 @@ enum State { pub struct H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite, - S: Service, Response = Response> + Clone + 'static, + S: Service> + Clone + 'static, S::Error: Into + Debug, + S::Response: Into>, B: MessageBody + 'static, { state: State, @@ -322,8 +328,9 @@ where impl Future for H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite, - S: Service, Response = Response> + Clone, + S: Service> + Clone, S::Error: Into + Debug, + S::Response: Into>, B: MessageBody, { type Item = (); From 6a343fae06c90aacd7c843d28c742a52cd7bc9f6 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Feb 2019 10:33:49 -0800 Subject: [PATCH 130/427] simplify Message type --- src/message.rs | 76 +++++++++++++++++++------------------------------- 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/src/message.rs b/src/message.rs index 12da9eaf1..7fb45bcc5 100644 --- a/src/message.rs +++ b/src/message.rs @@ -45,6 +45,7 @@ pub struct RequestHead { pub version: Version, pub headers: HeaderMap, pub ctype: Option, + pub extensions: RefCell, } impl Default for RequestHead { @@ -55,6 +56,7 @@ impl Default for RequestHead { version: Version::HTTP_11, headers: HeaderMap::with_capacity(16), ctype: None, + extensions: RefCell::new(Extensions::new()), } } } @@ -63,6 +65,7 @@ impl Head for RequestHead { fn clear(&mut self) { self.ctype = None; self.headers.clear(); + self.extensions.borrow_mut().clear(); } fn set_connection_type(&mut self, ctype: ConnectionType) { @@ -84,6 +87,20 @@ impl Head for RequestHead { } } +impl RequestHead { + /// Message extensions + #[inline] + pub fn extensions(&self) -> Ref { + self.extensions.borrow() + } + + /// Mutable reference to a the message's extensions + #[inline] + pub fn extensions_mut(&self) -> RefMut { + self.extensions.borrow_mut() + } +} + #[derive(Debug)] pub struct ResponseHead { pub version: Version, @@ -146,7 +163,7 @@ impl ResponseHead { } pub struct Message { - inner: Rc>, + head: Rc, pool: &'static MessagePool, } @@ -155,24 +172,12 @@ impl Message { pub fn new() -> Self { T::pool().get_message() } - - /// Message extensions - #[inline] - pub fn extensions(&self) -> Ref { - self.inner.as_ref().extensions.borrow() - } - - /// Mutable reference to a the message's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut { - self.inner.as_ref().extensions.borrow_mut() - } } impl Clone for Message { fn clone(&self) -> Self { Message { - inner: self.inner.clone(), + head: self.head.clone(), pool: self.pool, } } @@ -182,52 +187,27 @@ impl std::ops::Deref for Message { type Target = T; fn deref(&self) -> &Self::Target { - &self.inner.as_ref().head + &self.head.as_ref() } } impl std::ops::DerefMut for Message { fn deref_mut(&mut self) -> &mut Self::Target { - &mut Rc::get_mut(&mut self.inner) - .expect("Multiple copies exist") - .head + Rc::get_mut(&mut self.head).expect("Multiple copies exist") } } impl Drop for Message { fn drop(&mut self) { - if Rc::strong_count(&self.inner) == 1 { - self.pool.release(self.inner.clone()); - } - } -} - -struct MessageInner { - head: T, - extensions: RefCell, -} - -impl MessageInner { - #[inline] - /// Reset request instance - pub fn reset(&mut self) { - self.head.clear(); - self.extensions.borrow_mut().clear(); - } -} - -impl Default for MessageInner { - fn default() -> Self { - MessageInner { - head: T::default(), - extensions: RefCell::new(Extensions::new()), + if Rc::strong_count(&self.head) == 1 { + self.pool.release(self.head.clone()); } } } #[doc(hidden)] /// Request's objects pool -pub struct MessagePool(RefCell>>>); +pub struct MessagePool(RefCell>>); thread_local!(static REQUEST_POOL: &'static MessagePool = MessagePool::::create()); thread_local!(static RESPONSE_POOL: &'static MessagePool = MessagePool::::create()); @@ -243,15 +223,15 @@ impl MessagePool { fn get_message(&'static self) -> Message { if let Some(mut msg) = self.0.borrow_mut().pop_front() { if let Some(r) = Rc::get_mut(&mut msg) { - r.reset(); + r.clear(); } Message { - inner: msg, + head: msg, pool: self, } } else { Message { - inner: Rc::new(MessageInner::default()), + head: Rc::new(T::default()), pool: self, } } @@ -259,7 +239,7 @@ impl MessagePool { #[inline] /// Release request instance - fn release(&self, msg: Rc>) { + fn release(&self, msg: Rc) { let v = &mut self.0.borrow_mut(); if v.len() < 128 { v.push_front(msg); From a66d8589c2fd0048680d68945a47ab20fd3f3938 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Feb 2019 10:45:35 -0800 Subject: [PATCH 131/427] add Extensions::contains method --- src/extensions.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/extensions.rs b/src/extensions.rs index f7805641b..148e4c18e 100644 --- a/src/extensions.rs +++ b/src/extensions.rs @@ -26,6 +26,11 @@ impl Extensions { self.map.insert(TypeId::of::(), Box::new(val)); } + /// Check if container contains entry + pub fn contains(&self) -> bool { + self.map.get(&TypeId::of::()).is_some() + } + /// Get a reference to a type previously inserted on this `Extensions`. pub fn get(&self) -> Option<&T> { self.map From 1af149b9e61090556b3df98e21e11930dc9c337d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Feb 2019 20:27:39 -0800 Subject: [PATCH 132/427] remove Clone constraint from handler service --- src/h1/dispatcher.rs | 15 ++++++++------- src/h1/service.rs | 22 +++++++++++----------- src/h2/dispatcher.rs | 7 ++++--- src/h2/service.rs | 35 ++++++++++++++++++++--------------- src/request.rs | 9 --------- 5 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 667e674c5..06d4312a4 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -5,6 +5,7 @@ use std::time::Instant; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_service::Service; +use actix_utils::cloneable::CloneableService; use bitflags::bitflags; use futures::{try_ready, Async, Future, Poll, Sink, Stream}; use log::{debug, error, trace}; @@ -35,18 +36,18 @@ bitflags! { } /// Dispatcher for HTTP/1.1 protocol -pub struct Dispatcher +pub struct Dispatcher where S::Error: Debug, { inner: Option>, } -struct InnerDispatcher +struct InnerDispatcher where S::Error: Debug, { - service: S, + service: CloneableService, flags: Flags, framed: Framed, error: Option>, @@ -85,13 +86,13 @@ impl State { impl Dispatcher where T: AsyncRead + AsyncWrite, - S: Service, + S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody, { /// Create http/1 dispatcher. - pub fn new(stream: T, config: ServiceConfig, service: S) -> Self { + pub fn new(stream: T, config: ServiceConfig, service: CloneableService) -> Self { Dispatcher::with_timeout(stream, config, None, service) } @@ -100,7 +101,7 @@ where stream: T, config: ServiceConfig, timeout: Option, - service: S, + service: CloneableService, ) -> Self { let keepalive = config.keep_alive_enabled(); let flags = if keepalive { @@ -140,7 +141,7 @@ where impl InnerDispatcher where T: AsyncRead + AsyncWrite, - S: Service, + S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody, diff --git a/src/h1/service.rs b/src/h1/service.rs index 02a4b15d6..169a80ebc 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -4,6 +4,7 @@ use std::net; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_service::{IntoNewService, NewService, Service}; +use actix_utils::cloneable::CloneableService; use futures::future::{ok, FutureResult}; use futures::{try_ready, Async, Future, Poll, Stream}; use log::error; @@ -28,10 +29,10 @@ pub struct H1Service { impl H1Service where - S: NewService> + Clone, - S::Service: Clone, + S: NewService>, S::Error: Debug, S::Response: Into>, + S::Service: 'static, B: MessageBody, { /// Create new `HttpService` instance. @@ -54,10 +55,10 @@ where impl NewService for H1Service where T: AsyncRead + AsyncWrite, - S: NewService + Clone, - S::Service: Clone, + S: NewService, S::Error: Debug, S::Response: Into>, + S::Service: 'static, B: MessageBody, { type Request = T; @@ -93,7 +94,6 @@ pub struct H1ServiceBuilder { impl H1ServiceBuilder where S: NewService, - S::Service: Clone, S::Error: Debug, { /// Create instance of `ServiceConfigBuilder` @@ -217,7 +217,7 @@ impl Future for H1ServiceResponse where T: AsyncRead + AsyncWrite, S: NewService, - S::Service: Clone, + S::Service: 'static, S::Error: Debug, S::Response: Into>, B: MessageBody, @@ -235,22 +235,22 @@ where } /// `Service` implementation for HTTP1 transport -pub struct H1ServiceHandler { - srv: S, +pub struct H1ServiceHandler { + srv: CloneableService, cfg: ServiceConfig, _t: PhantomData<(T, B)>, } impl H1ServiceHandler where - S: Service + Clone, + S: Service, S::Error: Debug, S::Response: Into>, B: MessageBody, { fn new(cfg: ServiceConfig, srv: S) -> H1ServiceHandler { H1ServiceHandler { - srv, + srv: CloneableService::new(srv), cfg, _t: PhantomData, } @@ -260,7 +260,7 @@ where impl Service for H1ServiceHandler where T: AsyncRead + AsyncWrite, - S: Service + Clone, + S: Service, S::Error: Debug, S::Response: Into>, B: MessageBody, diff --git a/src/h2/dispatcher.rs b/src/h2/dispatcher.rs index b7339fa1d..5a8c4b855 100644 --- a/src/h2/dispatcher.rs +++ b/src/h2/dispatcher.rs @@ -5,6 +5,7 @@ use std::{fmt, mem}; use actix_codec::{AsyncRead, AsyncWrite}; use actix_service::Service; +use actix_utils::cloneable::CloneableService; use bitflags::bitflags; use bytes::{Bytes, BytesMut}; use futures::{try_ready, Async, Future, Poll, Sink, Stream}; @@ -37,9 +38,9 @@ bitflags! { } /// Dispatcher for HTTP/2 protocol -pub struct Dispatcher { +pub struct Dispatcher { flags: Flags, - service: S, + service: CloneableService, connection: Connection, config: ServiceConfig, ka_expire: Instant, @@ -56,7 +57,7 @@ where B: MessageBody + 'static, { pub fn new( - service: S, + service: CloneableService, connection: Connection, config: ServiceConfig, timeout: Option, diff --git a/src/h2/service.rs b/src/h2/service.rs index a1526375f..16b7e4956 100644 --- a/src/h2/service.rs +++ b/src/h2/service.rs @@ -4,6 +4,7 @@ use std::{io, net}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_service::{IntoNewService, NewService, Service}; +use actix_utils::cloneable::CloneableService; use bytes::Bytes; use futures::future::{ok, FutureResult}; use futures::{try_ready, Async, Future, Poll, Stream}; @@ -30,8 +31,8 @@ pub struct H2Service { impl H2Service where - S: NewService> + Clone, - S::Service: Clone + 'static, + S: NewService>, + S::Service: 'static, S::Error: Into + Debug + 'static, S::Response: Into>, B: MessageBody + 'static, @@ -56,8 +57,8 @@ where impl NewService for H2Service where T: AsyncRead + AsyncWrite, - S: NewService> + Clone, - S::Service: Clone + 'static, + S: NewService>, + S::Service: 'static, S::Error: Into + Debug, S::Response: Into>, B: MessageBody + 'static, @@ -95,7 +96,7 @@ pub struct H2ServiceBuilder { impl H2ServiceBuilder where S: NewService>, - S::Service: Clone + 'static, + S::Service: 'static, S::Error: Into + Debug + 'static, { /// Create instance of `H2ServiceBuilder` @@ -238,7 +239,7 @@ impl Future for H2ServiceResponse where T: AsyncRead + AsyncWrite, S: NewService>, - S::Service: Clone + 'static, + S::Service: 'static, S::Response: Into>, S::Error: Into + Debug, B: MessageBody + 'static, @@ -256,23 +257,23 @@ where } /// `Service` implementation for http/2 transport -pub struct H2ServiceHandler { - srv: S, +pub struct H2ServiceHandler { + srv: CloneableService, cfg: ServiceConfig, _t: PhantomData<(T, B)>, } impl H2ServiceHandler where - S: Service> + Clone + 'static, + S: Service> + 'static, S::Error: Into + Debug, S::Response: Into>, B: MessageBody + 'static, { fn new(cfg: ServiceConfig, srv: S) -> H2ServiceHandler { H2ServiceHandler { - srv, cfg, + srv: CloneableService::new(srv), _t: PhantomData, } } @@ -281,7 +282,7 @@ where impl Service for H2ServiceHandler where T: AsyncRead + AsyncWrite, - S: Service> + Clone + 'static, + S: Service> + 'static, S::Error: Into + Debug, S::Response: Into>, B: MessageBody + 'static, @@ -309,15 +310,19 @@ where } } -enum State { +enum State { Incoming(Dispatcher), - Handshake(Option, Option, Handshake), + Handshake( + Option>, + Option, + Handshake, + ), } pub struct H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite, - S: Service> + Clone + 'static, + S: Service> + 'static, S::Error: Into + Debug, S::Response: Into>, B: MessageBody + 'static, @@ -328,7 +333,7 @@ where impl Future for H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite, - S: Service> + Clone, + S: Service> + 'static, S::Error: Into + Debug, S::Response: Into>, B: MessageBody, diff --git a/src/request.rs b/src/request.rs index 0064de4e0..b9c35c7ac 100644 --- a/src/request.rs +++ b/src/request.rs @@ -153,15 +153,6 @@ impl Request { } self.head().method == Method::CONNECT } - - // #[doc(hidden)] - // /// Note: this method should be called only as part of clone operation - // /// of wrapper type. - // pub fn clone_request(&self) -> Self { - // Request { - // inner: self.inner.clone(), - // } - // } } impl fmt::Debug for Request { From e178db7f74bffad6fa562044c507083c634a3956 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Feb 2019 21:32:44 -0800 Subject: [PATCH 133/427] fix test --- src/h1/dispatcher.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 06d4312a4..6a1762747 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -599,7 +599,9 @@ mod tests { let mut h1 = Dispatcher::new( buf, ServiceConfig::default(), - (|req| ok::<_, Error>(Response::Ok().finish())).into_service(), + CloneableService::new( + (|req| ok::<_, Error>(Response::Ok().finish())).into_service(), + ), ); assert!(h1.poll().is_ok()); assert!(h1.poll().is_ok()); From f9724fa0ec8f1482fa911f24a0ce6c3a37931385 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 11 Feb 2019 09:54:41 -0800 Subject: [PATCH 134/427] add ErrorResponse impl for TimeoutError --- Cargo.toml | 3 +-- src/error.rs | 11 +++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f940ae86c..5bcaea7c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ ssl = ["openssl"] actix-service = "0.2.1" actix-codec = "0.1.0" actix-connector = "0.2.0" -actix-utils = "0.2.1" +actix-utils = "0.2.2" base64 = "0.10" backtrace = "0.3" @@ -80,7 +80,6 @@ openssl = { version="0.10", optional = true } actix-rt = "0.1.0" actix-server = { version="0.2", features=["ssl"] } actix-connector = { version="0.2.0", features=["ssl"] } -actix-utils = "0.2.1" actix-http-test = { path="test-server", features=["ssl"] } env_logger = "0.6" serde_derive = "1.0" diff --git a/src/error.rs b/src/error.rs index f71b429fd..cd5cabaa6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,6 +6,7 @@ use std::string::FromUtf8Error; use std::{fmt, io, result}; // use actix::MailboxError; +use actix_utils::timeout::TimeoutError; use backtrace::Backtrace; use cookie; use derive_more::{Display, From}; @@ -187,6 +188,16 @@ impl From for Error { // } // } +/// Return `GATEWAY_TIMEOUT` for `TimeoutError` +impl ResponseError for TimeoutError { + fn error_response(&self) -> Response { + match self { + TimeoutError::Service(e) => e.error_response(), + TimeoutError::Timeout => Response::new(StatusCode::GATEWAY_TIMEOUT), + } + } +} + /// `InternalServerError` for `JsonError` impl ResponseError for JsonError {} From 32021532c398b550ba09e639a334d09bc60e41b1 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 12 Feb 2019 09:55:29 -0800 Subject: [PATCH 135/427] export Payload type --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 932b54852..2b9d8c619 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,7 @@ mod httpcodes; pub mod httpmessage; mod json; mod message; +mod payload; mod request; mod response; mod service; @@ -85,7 +86,6 @@ mod service; pub mod error; pub mod h1; pub mod h2; -pub mod payload; pub mod test; pub mod ws; @@ -95,6 +95,7 @@ pub use self::error::{Error, ResponseError, Result}; pub use self::extensions::Extensions; pub use self::httpmessage::HttpMessage; pub use self::message::{Message, RequestHead, ResponseHead}; +pub use self::payload::Payload; pub use self::request::Request; pub use self::response::Response; pub use self::service::{SendError, SendResponse}; From a41459bf698ca13c56822da1fcf7137c1a452931 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 12 Feb 2019 11:07:42 -0800 Subject: [PATCH 136/427] make payload generic --- src/body.rs | 5 +- src/client/h1proto.rs | 5 +- src/client/h2proto.rs | 3 +- src/client/response.rs | 25 ++++----- src/h1/dispatcher.rs | 2 +- src/h1/service.rs | 4 +- src/httpmessage.rs | 117 +++++++++++++++++++---------------------- src/json.rs | 10 ++-- src/payload.rs | 41 ++++++++++++--- src/request.rs | 38 ++++++------- 10 files changed, 127 insertions(+), 123 deletions(-) diff --git a/src/body.rs b/src/body.rs index 1c54d4ce7..d3e63f9c9 100644 --- a/src/body.rs +++ b/src/body.rs @@ -4,10 +4,7 @@ use std::{fmt, mem}; use bytes::{Bytes, BytesMut}; use futures::{Async, Poll, Stream}; -use crate::error::{Error, PayloadError}; - -/// Type represent streaming payload -pub type PayloadStream = Box>; +use crate::error::Error; #[derive(Debug, PartialEq, Copy, Clone)] /// Different type of body diff --git a/src/client/h1proto.rs b/src/client/h1proto.rs index 59a03ef48..d16491385 100644 --- a/src/client/h1proto.rs +++ b/src/client/h1proto.rs @@ -9,10 +9,11 @@ use super::connection::{ConnectionLifetime, ConnectionType, IoConnection}; use super::error::{ConnectorError, SendRequestError}; use super::pool::Acquired; use super::response::ClientResponse; -use crate::body::{BodyLength, MessageBody, PayloadStream}; +use crate::body::{BodyLength, MessageBody}; use crate::error::PayloadError; use crate::h1; use crate::message::RequestHead; +use crate::payload::PayloadStream; pub(crate) fn send_request( io: T, @@ -57,7 +58,7 @@ where release_connection(framed, force_close) } _ => { - res.set_payload(Payload::stream(framed)); + res.set_payload(Payload::stream(framed).into()); } } ok(res) diff --git a/src/client/h2proto.rs b/src/client/h2proto.rs index ecd18cf82..697d30a41 100644 --- a/src/client/h2proto.rs +++ b/src/client/h2proto.rs @@ -10,7 +10,6 @@ use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCOD use http::{request::Request, HttpTryFrom, Version}; use crate::body::{BodyLength, MessageBody}; -use crate::h2::Payload; use crate::message::{RequestHead, ResponseHead}; use super::connection::{ConnectionType, IoConnection}; @@ -111,7 +110,7 @@ where Ok(ClientResponse { head, - payload: RefCell::new(Some(Box::new(Payload::new(body)))), + payload: RefCell::new(body.into()), }) }) .from_err() diff --git a/src/client/response.rs b/src/client/response.rs index 7a83d825d..f19e2d17a 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -1,20 +1,19 @@ use std::cell::RefCell; -use std::fmt; +use std::{fmt, mem}; use bytes::Bytes; -use futures::{Async, Poll, Stream}; +use futures::{Poll, Stream}; use http::{HeaderMap, StatusCode, Version}; -use crate::body::PayloadStream; use crate::error::PayloadError; use crate::httpmessage::HttpMessage; use crate::message::{Head, ResponseHead}; +use crate::payload::{Payload, PayloadStream}; /// Client Response -#[derive(Default)] pub struct ClientResponse { pub(crate) head: ResponseHead, - pub(crate) payload: RefCell>, + pub(crate) payload: RefCell, } impl HttpMessage for ClientResponse { @@ -25,8 +24,8 @@ impl HttpMessage for ClientResponse { } #[inline] - fn payload(&self) -> Option { - self.payload.borrow_mut().take() + fn payload(&self) -> Payload { + mem::replace(&mut *self.payload.borrow_mut(), Payload::None) } } @@ -35,7 +34,7 @@ impl ClientResponse { pub fn new() -> ClientResponse { ClientResponse { head: ResponseHead::default(), - payload: RefCell::new(None), + payload: RefCell::new(Payload::None), } } @@ -80,8 +79,8 @@ impl ClientResponse { } /// Set response payload - pub fn set_payload(&mut self, payload: PayloadStream) { - *self.payload.get_mut() = Some(payload); + pub fn set_payload(&mut self, payload: Payload) { + *self.payload.get_mut() = payload; } } @@ -90,11 +89,7 @@ impl Stream for ClientResponse { type Error = PayloadError; fn poll(&mut self) -> Poll, Self::Error> { - if let Some(ref mut payload) = self.payload.get_mut() { - payload.poll() - } else { - Ok(Async::Ready(None)) - } + self.payload.get_mut().poll() } } diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 6a1762747..c242333b9 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -316,7 +316,7 @@ where match self.framed.get_codec().message_type() { MessageType::Payload => { let (ps, pl) = Payload::create(false); - req = req.set_payload(pl); + req = req.set_payload(crate::Payload::H1(pl)); self.payload = Some(ps); } MessageType::Stream => { diff --git a/src/h1/service.rs b/src/h1/service.rs index 169a80ebc..381864498 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -12,7 +12,7 @@ use log::error; use crate::body::MessageBody; use crate::config::{KeepAlive, ServiceConfig}; use crate::error::{DispatchError, ParseError}; -use crate::payload::Payload; +use crate::payload::PayloadStream; use crate::request::Request; use crate::response::Response; @@ -29,7 +29,7 @@ pub struct H1Service { impl H1Service where - S: NewService>, + S: NewService>, S::Error: Debug, S::Response: Into>, S::Service: 'static, diff --git a/src/httpmessage.rs b/src/httpmessage.rs index f071cd7bc..57ad4bf95 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -1,3 +1,5 @@ +use std::{mem, str}; + use bytes::{Bytes, BytesMut}; use encoding::all::UTF_8; use encoding::label::encoding_from_whatwg_label; @@ -8,13 +10,13 @@ use http::{header, HeaderMap}; use mime::Mime; use serde::de::DeserializeOwned; use serde_urlencoded; -use std::str; use crate::error::{ ContentTypeError, ParseError, PayloadError, ReadlinesError, UrlencodedError, }; use crate::header::Header; use crate::json::JsonBody; +use crate::payload::Payload; /// Trait that implements general purpose operations on http messages pub trait HttpMessage: Sized { @@ -25,7 +27,7 @@ pub trait HttpMessage: Sized { fn headers(&self) -> &HeaderMap; /// Message payload stream - fn payload(&self) -> Option; + fn payload(&self) -> Payload; #[doc(hidden)] /// Get a header @@ -210,7 +212,7 @@ pub trait HttpMessage: Sized { /// Stream to read request line by line. pub struct Readlines { - stream: Option, + stream: Payload, buff: BytesMut, limit: usize, checked_buff: bool, @@ -244,7 +246,7 @@ impl Readlines { fn err(err: ReadlinesError) -> Self { Readlines { - stream: None, + stream: Payload::None, buff: BytesMut::new(), limit: 262_144, checked_buff: true, @@ -292,65 +294,61 @@ impl Stream for Readlines { self.checked_buff = true; } // poll req for more bytes - if let Some(ref mut stream) = self.stream { - match stream.poll() { - Ok(Async::Ready(Some(mut bytes))) => { - // check if there is a newline in bytes - let mut found: Option = None; - for (ind, b) in bytes.iter().enumerate() { - if *b == b'\n' { - found = Some(ind); - break; - } + match self.stream.poll() { + Ok(Async::Ready(Some(mut bytes))) => { + // check if there is a newline in bytes + let mut found: Option = None; + for (ind, b) in bytes.iter().enumerate() { + if *b == b'\n' { + found = Some(ind); + break; } - if let Some(ind) = found { - // check if line is longer than limit - if ind + 1 > self.limit { - return Err(ReadlinesError::LimitOverflow); - } - let enc: *const Encoding = self.encoding as *const Encoding; - let line = if enc == UTF_8 { - str::from_utf8(&bytes.split_to(ind + 1)) - .map_err(|_| ReadlinesError::EncodingError)? - .to_owned() - } else { - self.encoding - .decode(&bytes.split_to(ind + 1), DecoderTrap::Strict) - .map_err(|_| ReadlinesError::EncodingError)? - }; - // extend buffer with rest of the bytes; - self.buff.extend_from_slice(&bytes); - self.checked_buff = false; - return Ok(Async::Ready(Some(line))); - } - self.buff.extend_from_slice(&bytes); - Ok(Async::NotReady) } - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(None)) => { - if self.buff.is_empty() { - return Ok(Async::Ready(None)); - } - if self.buff.len() > self.limit { + if let Some(ind) = found { + // check if line is longer than limit + if ind + 1 > self.limit { return Err(ReadlinesError::LimitOverflow); } let enc: *const Encoding = self.encoding as *const Encoding; let line = if enc == UTF_8 { - str::from_utf8(&self.buff) + str::from_utf8(&bytes.split_to(ind + 1)) .map_err(|_| ReadlinesError::EncodingError)? .to_owned() } else { self.encoding - .decode(&self.buff, DecoderTrap::Strict) + .decode(&bytes.split_to(ind + 1), DecoderTrap::Strict) .map_err(|_| ReadlinesError::EncodingError)? }; - self.buff.clear(); - Ok(Async::Ready(Some(line))) + // extend buffer with rest of the bytes; + self.buff.extend_from_slice(&bytes); + self.checked_buff = false; + return Ok(Async::Ready(Some(line))); } - Err(e) => Err(ReadlinesError::from(e)), + self.buff.extend_from_slice(&bytes); + Ok(Async::NotReady) } - } else { - Ok(Async::Ready(None)) + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(None)) => { + if self.buff.is_empty() { + return Ok(Async::Ready(None)); + } + if self.buff.len() > self.limit { + return Err(ReadlinesError::LimitOverflow); + } + let enc: *const Encoding = self.encoding as *const Encoding; + let line = if enc == UTF_8 { + str::from_utf8(&self.buff) + .map_err(|_| ReadlinesError::EncodingError)? + .to_owned() + } else { + self.encoding + .decode(&self.buff, DecoderTrap::Strict) + .map_err(|_| ReadlinesError::EncodingError)? + }; + self.buff.clear(); + Ok(Async::Ready(Some(line))) + } + Err(e) => Err(ReadlinesError::from(e)), } } } @@ -359,7 +357,7 @@ impl Stream for Readlines { pub struct MessageBody { limit: usize, length: Option, - stream: Option, + stream: Payload, err: Option, fut: Option>>, } @@ -397,7 +395,7 @@ impl MessageBody { fn err(e: PayloadError) -> Self { MessageBody { - stream: None, + stream: Payload::None, limit: 262_144, fut: None, err: Some(e), @@ -428,16 +426,10 @@ where } } - if self.stream.is_none() { - return Ok(Async::Ready(Bytes::new())); - } - // future let limit = self.limit; self.fut = Some(Box::new( - self.stream - .take() - .expect("Can not be used second time") + mem::replace(&mut self.stream, Payload::None) .from_err() .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { if (body.len() + chunk.len()) > limit { @@ -455,7 +447,7 @@ where /// Future that resolves to a parsed urlencoded values. pub struct UrlEncoded { - stream: Option, + stream: Payload, limit: usize, length: Option, encoding: EncodingRef, @@ -500,7 +492,7 @@ impl UrlEncoded { fn err(e: UrlencodedError) -> Self { UrlEncoded { - stream: None, + stream: Payload::None, limit: 262_144, fut: None, err: Some(e), @@ -543,10 +535,7 @@ where // future let encoding = self.encoding; - let fut = self - .stream - .take() - .expect("UrlEncoded could not be used second time") + let fut = mem::replace(&mut self.stream, Payload::None) .from_err() .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { if (body.len() + chunk.len()) > limit { diff --git a/src/json.rs b/src/json.rs index f750f545d..573fde411 100644 --- a/src/json.rs +++ b/src/json.rs @@ -8,6 +8,7 @@ use serde_json; use crate::error::JsonPayloadError; use crate::httpmessage::HttpMessage; +use crate::payload::Payload; /// Request payload json parser that resolves to a deserialized `T` value. /// @@ -43,7 +44,7 @@ use crate::httpmessage::HttpMessage; pub struct JsonBody { limit: usize, length: Option, - stream: Option, + stream: Payload, err: Option, fut: Option>>, } @@ -61,7 +62,7 @@ impl JsonBody { return JsonBody { limit: 262_144, length: None, - stream: None, + stream: Payload::None, fut: None, err: Some(JsonPayloadError::ContentType), }; @@ -112,10 +113,7 @@ impl Future for JsonBod } } - let fut = self - .stream - .take() - .expect("JsonBody could not be used second time") + let fut = std::mem::replace(&mut self.stream, Payload::None) .from_err() .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { if (body.len() + chunk.len()) > limit { diff --git a/src/payload.rs b/src/payload.rs index ede1281ec..21e415313 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -1,32 +1,57 @@ use bytes::Bytes; -use derive_more::From; -use futures::{Poll, Stream}; +use futures::{Async, Poll, Stream}; use h2::RecvStream; use crate::error::PayloadError; -#[derive(From)] -pub enum Payload { +/// Type represent boxed payload +pub type PayloadStream = Box>; + +/// Type represent streaming payload +pub enum Payload { + None, H1(crate::h1::Payload), H2(crate::h2::Payload), - Dyn(Box>), + Stream(S), } -impl From for Payload { +impl From for Payload { fn from(v: RecvStream) -> Self { Payload::H2(crate::h2::Payload::new(v)) } } -impl Stream for Payload { +impl From for Payload { + fn from(pl: crate::h1::Payload) -> Self { + Payload::H1(pl) + } +} + +impl From for Payload { + fn from(pl: crate::h2::Payload) -> Self { + Payload::H2(pl) + } +} + +impl From for Payload { + fn from(pl: PayloadStream) -> Self { + Payload::Stream(pl) + } +} + +impl Stream for Payload +where + S: Stream, +{ type Item = Bytes; type Error = PayloadError; fn poll(&mut self) -> Poll, Self::Error> { match self { + Payload::None => Ok(Async::Ready(None)), Payload::H1(ref mut pl) => pl.poll(), Payload::H2(ref mut pl) => pl.poll(), - Payload::Dyn(ref mut pl) => pl.poll(), + Payload::Stream(ref mut pl) => pl.poll(), } } } diff --git a/src/request.rs b/src/request.rs index b9c35c7ac..388fe7543 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,5 +1,5 @@ use std::cell::{Ref, RefCell, RefMut}; -use std::fmt; +use std::{fmt, mem}; use bytes::Bytes; use futures::Stream; @@ -9,11 +9,11 @@ use crate::error::PayloadError; use crate::extensions::Extensions; use crate::httpmessage::HttpMessage; use crate::message::{Message, RequestHead}; -use crate::payload::Payload; +use crate::payload::{Payload, PayloadStream}; /// Request -pub struct Request

    { - pub(crate) payload: RefCell>, +pub struct Request

    { + pub(crate) payload: RefCell>, pub(crate) inner: Message, } @@ -28,53 +28,53 @@ where } #[inline] - fn payload(&self) -> Option

    { - self.payload.borrow_mut().take() + fn payload(&self) -> Payload { + mem::replace(&mut *self.payload.borrow_mut(), Payload::None) } } -impl From> for Request { +impl

    From> for Request

    { fn from(msg: Message) -> Self { Request { - payload: RefCell::new(None), + payload: RefCell::new(Payload::None), inner: msg, } } } -impl Request { +impl Request { /// Create new Request instance - pub fn new() -> Request { + pub fn new() -> Request { Request { - payload: RefCell::new(None), + payload: RefCell::new(Payload::None), inner: Message::new(), } } } -impl Request { +impl

    Request

    { /// Create new Request instance - pub fn with_payload(payload: Payload) -> Request { + pub fn with_payload(payload: Payload

    ) -> Request

    { Request { - payload: RefCell::new(Some(payload.into())), + payload: RefCell::new(payload), inner: Message::new(), } } /// Create new Request instance - pub fn set_payload(self, payload: I) -> Request

    + pub fn set_payload(self, payload: I) -> Request where - I: Into

    , + I: Into>, { Request { - payload: RefCell::new(Some(payload.into())), + payload: RefCell::new(payload.into()), inner: self.inner, } } /// Split request into request head and payload - pub fn into_parts(mut self) -> (Message, Option) { - (self.inner, self.payload.get_mut().take()) + pub fn into_parts(self) -> (Message, Payload

    ) { + (self.inner, self.payload.into_inner()) } #[inline] From 8d4ce0c956f6a81d0b8aec50ad220b352f12e5c0 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 12 Feb 2019 11:09:58 -0800 Subject: [PATCH 137/427] export PayloadStream --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 2b9d8c619..4e6e37955 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,7 +95,7 @@ pub use self::error::{Error, ResponseError, Result}; pub use self::extensions::Extensions; pub use self::httpmessage::HttpMessage; pub use self::message::{Message, RequestHead, ResponseHead}; -pub use self::payload::Payload; +pub use self::payload::{Payload, PayloadStream}; pub use self::request::Request; pub use self::response::Response; pub use self::service::{SendError, SendResponse}; From 118606262be9dcfe64b9870308cd2dccb26111d3 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 13 Feb 2019 13:52:11 -0800 Subject: [PATCH 138/427] refactor payload handling --- examples/echo.rs | 2 +- examples/echo2.rs | 2 +- src/client/h1proto.rs | 5 ++- src/client/h2proto.rs | 7 ++-- src/client/response.rs | 22 +++++----- src/h1/dispatcher.rs | 4 +- src/h1/service.rs | 3 +- src/httpmessage.rs | 95 ++++++++++++++++++++++++++++-------------- src/json.rs | 31 +++++++++----- src/message.rs | 25 ++++++++++- src/payload.rs | 20 ++++----- src/request.rs | 71 ++++++++++++++++--------------- tests/test_client.rs | 4 +- tests/test_server.rs | 20 ++++----- 14 files changed, 186 insertions(+), 125 deletions(-) diff --git a/examples/echo.rs b/examples/echo.rs index 5b68024f1..03d5b4706 100644 --- a/examples/echo.rs +++ b/examples/echo.rs @@ -18,7 +18,7 @@ fn main() { .client_timeout(1000) .client_disconnect(1000) .server_hostname("localhost") - .finish(|req: Request| { + .finish(|mut req: Request| { req.body().limit(512).and_then(|bytes: Bytes| { info!("request body: {:?}", bytes); let mut res = Response::Ok(); diff --git a/examples/echo2.rs b/examples/echo2.rs index daaafa087..2fd9cbcff 100644 --- a/examples/echo2.rs +++ b/examples/echo2.rs @@ -8,7 +8,7 @@ use futures::Future; use log::info; use std::env; -fn handle_request(req: Request) -> impl Future { +fn handle_request(mut req: Request) -> impl Future { req.body().limit(512).from_err().and_then(|bytes: Bytes| { info!("request body: {:?}", bytes); let mut res = Response::Ok(); diff --git a/src/client/h1proto.rs b/src/client/h1proto.rs index d16491385..3329fcfec 100644 --- a/src/client/h1proto.rs +++ b/src/client/h1proto.rs @@ -13,7 +13,6 @@ use crate::body::{BodyLength, MessageBody}; use crate::error::PayloadError; use crate::h1; use crate::message::RequestHead; -use crate::payload::PayloadStream; pub(crate) fn send_request( io: T, @@ -205,7 +204,9 @@ pub(crate) struct Payload { } impl Payload { - pub fn stream(framed: Framed) -> PayloadStream { + pub fn stream( + framed: Framed, + ) -> Box> { Box::new(Payload { framed: Some(framed.map_codec(|codec| codec.into_payload_codec())), }) diff --git a/src/client/h2proto.rs b/src/client/h2proto.rs index 697d30a41..8804d13f2 100644 --- a/src/client/h2proto.rs +++ b/src/client/h2proto.rs @@ -1,4 +1,3 @@ -use std::cell::RefCell; use std::time; use actix_codec::{AsyncRead, AsyncWrite}; @@ -10,7 +9,7 @@ use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCOD use http::{request::Request, HttpTryFrom, Version}; use crate::body::{BodyLength, MessageBody}; -use crate::message::{RequestHead, ResponseHead}; +use crate::message::{Message, RequestHead, ResponseHead}; use super::connection::{ConnectionType, IoConnection}; use super::error::SendRequestError; @@ -103,14 +102,14 @@ where .and_then(|resp| { let (parts, body) = resp.into_parts(); - let mut head = ResponseHead::default(); + let mut head: Message = Message::new(); head.version = parts.version; head.status = parts.status; head.headers = parts.headers; Ok(ClientResponse { head, - payload: RefCell::new(body.into()), + payload: body.into(), }) }) .from_err() diff --git a/src/client/response.rs b/src/client/response.rs index f19e2d17a..104d28ed7 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -1,5 +1,4 @@ -use std::cell::RefCell; -use std::{fmt, mem}; +use std::fmt; use bytes::Bytes; use futures::{Poll, Stream}; @@ -7,13 +6,13 @@ use http::{HeaderMap, StatusCode, Version}; use crate::error::PayloadError; use crate::httpmessage::HttpMessage; -use crate::message::{Head, ResponseHead}; +use crate::message::{Head, Message, ResponseHead}; use crate::payload::{Payload, PayloadStream}; /// Client Response pub struct ClientResponse { - pub(crate) head: ResponseHead, - pub(crate) payload: RefCell, + pub(crate) head: Message, + pub(crate) payload: Payload, } impl HttpMessage for ClientResponse { @@ -23,9 +22,8 @@ impl HttpMessage for ClientResponse { &self.head.headers } - #[inline] - fn payload(&self) -> Payload { - mem::replace(&mut *self.payload.borrow_mut(), Payload::None) + fn take_payload(&mut self) -> Payload { + std::mem::replace(&mut self.payload, Payload::None) } } @@ -33,8 +31,8 @@ impl ClientResponse { /// Create new Request instance pub fn new() -> ClientResponse { ClientResponse { - head: ResponseHead::default(), - payload: RefCell::new(Payload::None), + head: Message::new(), + payload: Payload::None, } } @@ -80,7 +78,7 @@ impl ClientResponse { /// Set response payload pub fn set_payload(&mut self, payload: Payload) { - *self.payload.get_mut() = payload; + self.payload = payload; } } @@ -89,7 +87,7 @@ impl Stream for ClientResponse { type Error = PayloadError; fn poll(&mut self) -> Poll, Self::Error> { - self.payload.get_mut().poll() + self.payload.poll() } } diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index c242333b9..22d7ea865 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -316,7 +316,9 @@ where match self.framed.get_codec().message_type() { MessageType::Payload => { let (ps, pl) = Payload::create(false); - req = req.set_payload(crate::Payload::H1(pl)); + let (req1, _) = + req.replace_payload(crate::Payload::H1(pl)); + req = req1; self.payload = Some(ps); } MessageType::Stream => { diff --git a/src/h1/service.rs b/src/h1/service.rs index 381864498..cb8dae54d 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -12,7 +12,6 @@ use log::error; use crate::body::MessageBody; use crate::config::{KeepAlive, ServiceConfig}; use crate::error::{DispatchError, ParseError}; -use crate::payload::PayloadStream; use crate::request::Request; use crate::response::Response; @@ -29,7 +28,7 @@ pub struct H1Service { impl H1Service where - S: NewService>, + S: NewService, S::Error: Debug, S::Response: Into>, S::Service: 'static, diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 57ad4bf95..79f29a723 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -1,4 +1,4 @@ -use std::{mem, str}; +use std::str; use bytes::{Bytes, BytesMut}; use encoding::all::UTF_8; @@ -21,13 +21,13 @@ use crate::payload::Payload; /// Trait that implements general purpose operations on http messages pub trait HttpMessage: Sized { /// Type of message payload stream - type Stream: Stream + Sized; + type Stream; /// Read the message headers. fn headers(&self) -> &HeaderMap; /// Message payload stream - fn payload(&self) -> Payload; + fn take_payload(&mut self) -> Payload; #[doc(hidden)] /// Get a header @@ -130,7 +130,10 @@ pub trait HttpMessage: Sized { /// } /// # fn main() {} /// ``` - fn body(&self) -> MessageBody { + fn body(&mut self) -> MessageBody + where + Self::Stream: Stream + Sized, + { MessageBody::new(self) } @@ -164,7 +167,10 @@ pub trait HttpMessage: Sized { /// } /// # fn main() {} /// ``` - fn urlencoded(&self) -> UrlEncoded { + fn urlencoded(&mut self) -> UrlEncoded + where + Self::Stream: Stream, + { UrlEncoded::new(self) } @@ -200,12 +206,18 @@ pub trait HttpMessage: Sized { /// } /// # fn main() {} /// ``` - fn json(&self) -> JsonBody { + fn json(&mut self) -> JsonBody + where + Self::Stream: Stream + 'static, + { JsonBody::new(self) } /// Return stream of lines. - fn readlines(&self) -> Readlines { + fn readlines(&mut self) -> Readlines + where + Self::Stream: Stream + 'static, + { Readlines::new(self) } } @@ -220,16 +232,20 @@ pub struct Readlines { err: Option, } -impl Readlines { +impl Readlines +where + T: HttpMessage, + T::Stream: Stream, +{ /// Create a new stream to read request line by line. - fn new(req: &T) -> Self { + fn new(req: &mut T) -> Self { let encoding = match req.encoding() { Ok(enc) => enc, Err(err) => return Self::err(err.into()), }; Readlines { - stream: req.payload(), + stream: req.take_payload(), buff: BytesMut::with_capacity(262_144), limit: 262_144, checked_buff: true, @@ -256,7 +272,11 @@ impl Readlines { } } -impl Stream for Readlines { +impl Stream for Readlines +where + T: HttpMessage, + T::Stream: Stream, +{ type Item = String; type Error = ReadlinesError; @@ -362,9 +382,13 @@ pub struct MessageBody { fut: Option>>, } -impl MessageBody { +impl MessageBody +where + T: HttpMessage, + T::Stream: Stream, +{ /// Create `MessageBody` for request. - pub fn new(req: &T) -> MessageBody { + pub fn new(req: &mut T) -> MessageBody { let mut len = None; if let Some(l) = req.headers().get(header::CONTENT_LENGTH) { if let Ok(s) = l.to_str() { @@ -379,9 +403,9 @@ impl MessageBody { } MessageBody { + stream: req.take_payload(), limit: 262_144, length: len, - stream: req.payload(), fut: None, err: None, } @@ -406,7 +430,8 @@ impl MessageBody { impl Future for MessageBody where - T: HttpMessage + 'static, + T: HttpMessage, + T::Stream: Stream + 'static, { type Item = Bytes; type Error = PayloadError; @@ -429,7 +454,7 @@ where // future let limit = self.limit; self.fut = Some(Box::new( - mem::replace(&mut self.stream, Payload::None) + std::mem::replace(&mut self.stream, Payload::None) .from_err() .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { if (body.len() + chunk.len()) > limit { @@ -455,9 +480,13 @@ pub struct UrlEncoded { fut: Option>>, } -impl UrlEncoded { +impl UrlEncoded +where + T: HttpMessage, + T::Stream: Stream, +{ /// Create a new future to URL encode a request - pub fn new(req: &T) -> UrlEncoded { + pub fn new(req: &mut T) -> UrlEncoded { // check content type if req.content_type().to_lowercase() != "application/x-www-form-urlencoded" { return Self::err(UrlencodedError::ContentType); @@ -482,7 +511,7 @@ impl UrlEncoded { UrlEncoded { encoding, - stream: req.payload(), + stream: req.take_payload(), limit: 262_144, length: len, fut: None, @@ -510,7 +539,8 @@ impl UrlEncoded { impl Future for UrlEncoded where - T: HttpMessage + 'static, + T: HttpMessage, + T::Stream: Stream + 'static, U: DeserializeOwned + 'static, { type Item = U; @@ -535,7 +565,7 @@ where // future let encoding = self.encoding; - let fut = mem::replace(&mut self.stream, Payload::None) + let fut = std::mem::replace(&mut self.stream, Payload::None) .from_err() .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { if (body.len() + chunk.len()) > limit { @@ -691,7 +721,7 @@ mod tests { #[test] fn test_urlencoded_error() { - let req = TestRequest::with_header( + let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) @@ -702,7 +732,7 @@ mod tests { UrlencodedError::UnknownLength ); - let req = TestRequest::with_header( + let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) @@ -713,7 +743,7 @@ mod tests { UrlencodedError::Overflow ); - let req = TestRequest::with_header(header::CONTENT_TYPE, "text/plain") + let mut req = TestRequest::with_header(header::CONTENT_TYPE, "text/plain") .header(header::CONTENT_LENGTH, "10") .finish(); assert_eq!( @@ -724,7 +754,7 @@ mod tests { #[test] fn test_urlencoded() { - let req = TestRequest::with_header( + let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) @@ -740,7 +770,7 @@ mod tests { }) ); - let req = TestRequest::with_header( + let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8", ) @@ -759,19 +789,20 @@ mod tests { #[test] fn test_message_body() { - let req = TestRequest::with_header(header::CONTENT_LENGTH, "xxxx").finish(); + let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "xxxx").finish(); match req.body().poll().err().unwrap() { PayloadError::UnknownLength => (), _ => unreachable!("error"), } - let req = TestRequest::with_header(header::CONTENT_LENGTH, "1000000").finish(); + let mut req = + TestRequest::with_header(header::CONTENT_LENGTH, "1000000").finish(); match req.body().poll().err().unwrap() { PayloadError::Overflow => (), _ => unreachable!("error"), } - let req = TestRequest::default() + let mut req = TestRequest::default() .set_payload(Bytes::from_static(b"test")) .finish(); match req.body().poll().ok().unwrap() { @@ -779,7 +810,7 @@ mod tests { _ => unreachable!("error"), } - let req = TestRequest::default() + let mut req = TestRequest::default() .set_payload(Bytes::from_static(b"11111111111111")) .finish(); match req.body().limit(5).poll().err().unwrap() { @@ -790,14 +821,14 @@ mod tests { #[test] fn test_readlines() { - let req = TestRequest::default() + let mut req = TestRequest::default() .set_payload(Bytes::from_static( b"Lorem Ipsum is simply dummy text of the printing and typesetting\n\ industry. Lorem Ipsum has been the industry's standard dummy\n\ Contrary to popular belief, Lorem Ipsum is not simply random text.", )) .finish(); - let mut r = Readlines::new(&req); + let mut r = Readlines::new(&mut req); match r.poll().ok().unwrap() { Async::Ready(Some(s)) => assert_eq!( s, diff --git a/src/json.rs b/src/json.rs index 573fde411..026ecb871 100644 --- a/src/json.rs +++ b/src/json.rs @@ -2,11 +2,12 @@ use bytes::BytesMut; use futures::{Future, Poll, Stream}; use http::header::CONTENT_LENGTH; +use bytes::Bytes; use mime; use serde::de::DeserializeOwned; use serde_json; -use crate::error::JsonPayloadError; +use crate::error::{JsonPayloadError, PayloadError}; use crate::httpmessage::HttpMessage; use crate::payload::Payload; @@ -41,7 +42,7 @@ use crate::payload::Payload; /// } /// # fn main() {} /// ``` -pub struct JsonBody { +pub struct JsonBody { limit: usize, length: Option, stream: Payload, @@ -49,9 +50,14 @@ pub struct JsonBody { fut: Option>>, } -impl JsonBody { +impl JsonBody +where + T: HttpMessage, + T::Stream: Stream + 'static, + U: DeserializeOwned + 'static, +{ /// Create `JsonBody` for request. - pub fn new(req: &T) -> Self { + pub fn new(req: &mut T) -> Self { // check content-type let json = if let Ok(Some(mime)) = req.mime_type() { mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) @@ -80,7 +86,7 @@ impl JsonBody { JsonBody { limit: 262_144, length: len, - stream: req.payload(), + stream: req.take_payload(), fut: None, err: None, } @@ -93,7 +99,12 @@ impl JsonBody { } } -impl Future for JsonBody { +impl Future for JsonBody +where + T: HttpMessage, + T::Stream: Stream + 'static, + U: DeserializeOwned + 'static, +{ type Item = U; type Error = JsonPayloadError; @@ -162,11 +173,11 @@ mod tests { #[test] fn test_json_body() { - let req = TestRequest::default().finish(); + let mut req = TestRequest::default().finish(); let mut json = req.json::(); assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); - let req = TestRequest::default() + let mut req = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/text"), @@ -175,7 +186,7 @@ mod tests { let mut json = req.json::(); assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); - let req = TestRequest::default() + let mut req = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), @@ -188,7 +199,7 @@ mod tests { let mut json = req.json::().limit(100); assert_eq!(json.poll().err().unwrap(), JsonPayloadError::Overflow); - let req = TestRequest::default() + let mut req = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), diff --git a/src/message.rs b/src/message.rs index 7fb45bcc5..812f099e4 100644 --- a/src/message.rs +++ b/src/message.rs @@ -2,9 +2,8 @@ use std::cell::{Ref, RefCell, RefMut}; use std::collections::VecDeque; use std::rc::Rc; -use http::{HeaderMap, Method, StatusCode, Uri, Version}; - use crate::extensions::Extensions; +use crate::http::{HeaderMap, Method, StatusCode, Uri, Version}; /// Represents various types of connection #[derive(Copy, Clone, PartialEq, Debug)] @@ -21,6 +20,12 @@ pub enum ConnectionType { pub trait Head: Default + 'static { fn clear(&mut self); + /// Read the message headers. + fn headers(&self) -> &HeaderMap; + + /// Mutable reference to the message headers. + fn headers_mut(&mut self) -> &mut HeaderMap; + /// Connection type fn connection_type(&self) -> ConnectionType; @@ -68,6 +73,14 @@ impl Head for RequestHead { self.extensions.borrow_mut().clear(); } + fn headers(&self) -> &HeaderMap { + &self.headers + } + + fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.headers + } + fn set_connection_type(&mut self, ctype: ConnectionType) { self.ctype = Some(ctype) } @@ -129,6 +142,14 @@ impl Head for ResponseHead { self.headers.clear(); } + fn headers(&self) -> &HeaderMap { + &self.headers + } + + fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.headers + } + fn set_connection_type(&mut self, ctype: ConnectionType) { self.ctype = Some(ctype) } diff --git a/src/payload.rs b/src/payload.rs index 21e415313..bc40fe807 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -15,21 +15,21 @@ pub enum Payload { Stream(S), } -impl From for Payload { - fn from(v: RecvStream) -> Self { - Payload::H2(crate::h2::Payload::new(v)) - } -} - impl From for Payload { - fn from(pl: crate::h1::Payload) -> Self { - Payload::H1(pl) + fn from(v: crate::h1::Payload) -> Self { + Payload::H1(v) } } impl From for Payload { - fn from(pl: crate::h2::Payload) -> Self { - Payload::H2(pl) + fn from(v: crate::h2::Payload) -> Self { + Payload::H2(v) + } +} + +impl From for Payload { + fn from(v: RecvStream) -> Self { + Payload::H2(crate::h2::Payload::new(v)) } } diff --git a/src/request.rs b/src/request.rs index 388fe7543..e1b893f95 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,11 +1,8 @@ -use std::cell::{Ref, RefCell, RefMut}; -use std::{fmt, mem}; +use std::cell::{Ref, RefMut}; +use std::fmt; -use bytes::Bytes; -use futures::Stream; use http::{header, HeaderMap, Method, Uri, Version}; -use crate::error::PayloadError; use crate::extensions::Extensions; use crate::httpmessage::HttpMessage; use crate::message::{Message, RequestHead}; @@ -13,31 +10,27 @@ use crate::payload::{Payload, PayloadStream}; /// Request pub struct Request

    { - pub(crate) payload: RefCell>, - pub(crate) inner: Message, + pub(crate) payload: Payload

    , + pub(crate) head: Message, } -impl

    HttpMessage for Request

    -where - P: Stream, -{ +impl

    HttpMessage for Request

    { type Stream = P; fn headers(&self) -> &HeaderMap { &self.head().headers } - #[inline] - fn payload(&self) -> Payload { - mem::replace(&mut *self.payload.borrow_mut(), Payload::None) + fn take_payload(&mut self) -> Payload

    { + std::mem::replace(&mut self.payload, Payload::None) } } -impl

    From> for Request

    { - fn from(msg: Message) -> Self { +impl From> for Request { + fn from(head: Message) -> Self { Request { - payload: RefCell::new(Payload::None), - inner: msg, + head, + payload: Payload::None, } } } @@ -46,8 +39,8 @@ impl Request { /// Create new Request instance pub fn new() -> Request { Request { - payload: RefCell::new(Payload::None), - inner: Message::new(), + head: Message::new(), + payload: Payload::None, } } } @@ -56,38 +49,44 @@ impl

    Request

    { /// Create new Request instance pub fn with_payload(payload: Payload

    ) -> Request

    { Request { - payload: RefCell::new(payload), - inner: Message::new(), + payload, + head: Message::new(), } } /// Create new Request instance - pub fn set_payload(self, payload: I) -> Request - where - I: Into>, - { - Request { - payload: RefCell::new(payload.into()), - inner: self.inner, - } + pub fn replace_payload(self, payload: Payload) -> (Request, Payload

    ) { + let pl = self.payload; + ( + Request { + payload, + head: self.head, + }, + pl, + ) + } + + /// Get request's payload + pub fn take_payload(&mut self) -> Payload

    { + std::mem::replace(&mut self.payload, Payload::None) } /// Split request into request head and payload pub fn into_parts(self) -> (Message, Payload

    ) { - (self.inner, self.payload.into_inner()) + (self.head, self.payload) } #[inline] /// Http message part of the request pub fn head(&self) -> &RequestHead { - &*self.inner + &*self.head } #[inline] #[doc(hidden)] /// Mutable reference to a http message part of the request pub fn head_mut(&mut self) -> &mut RequestHead { - &mut *self.inner + &mut *self.head } /// Request's uri. @@ -135,13 +134,13 @@ impl

    Request

    { /// Request extensions #[inline] pub fn extensions(&self) -> Ref { - self.inner.extensions() + self.head.extensions() } /// Mutable reference to a the request's extensions #[inline] pub fn extensions_mut(&self) -> RefMut { - self.inner.extensions_mut() + self.head.extensions_mut() } /// Check if request requires connection upgrade @@ -155,7 +154,7 @@ impl

    Request

    { } } -impl fmt::Debug for Request { +impl

    fmt::Debug for Request

    { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!( f, diff --git a/tests/test_client.rs b/tests/test_client.rs index 606bac22a..6f502b0af 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -47,7 +47,7 @@ fn test_h1_v2() { assert!(repr.contains("ClientRequest")); assert!(repr.contains("x-test")); - let response = srv.block_on(request.send(&mut connector)).unwrap(); + let mut response = srv.block_on(request.send(&mut connector)).unwrap(); assert!(response.status().is_success()); // read response @@ -55,7 +55,7 @@ fn test_h1_v2() { assert_eq!(bytes, Bytes::from_static(STR.as_ref())); let request = srv.post().finish().unwrap(); - let response = srv.block_on(request.send(&mut connector)).unwrap(); + let mut response = srv.block_on(request.send(&mut connector)).unwrap(); assert!(response.status().is_success()); // read response diff --git a/tests/test_server.rs b/tests/test_server.rs index 9fa27e71b..53db38403 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -89,7 +89,7 @@ fn test_h2_body() -> std::io::Result<()> { .map_err(|e| println!("Openssl error: {}", e)) .and_then( h2::H2Service::build() - .finish(|req: Request<_>| { + .finish(|mut req: Request<_>| { req.body() .limit(1024 * 1024) .and_then(|body| Ok(Response::Ok().body(body))) @@ -101,7 +101,7 @@ fn test_h2_body() -> std::io::Result<()> { let req = client::ClientRequest::get(srv.surl("/")) .body(data.clone()) .unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); let body = srv.block_on(response.body().limit(1024 * 1024)).unwrap(); @@ -350,7 +350,7 @@ fn test_headers() { let req = srv.get().finish().unwrap(); - let response = srv.block_on(req.send(&mut connector)).unwrap(); + let mut response = srv.block_on(req.send(&mut connector)).unwrap(); assert!(response.status().is_success()); // read response @@ -387,7 +387,7 @@ fn test_body() { }); let req = srv.get().finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response @@ -402,7 +402,7 @@ fn test_head_empty() { }); let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); { @@ -428,7 +428,7 @@ fn test_head_binary() { }); let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); { @@ -477,7 +477,7 @@ fn test_body_length() { }); let req = srv.get().finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response @@ -496,7 +496,7 @@ fn test_body_chunked_explicit() { }); let req = srv.get().finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response @@ -517,7 +517,7 @@ fn test_body_chunked_implicit() { }); let req = srv.get().finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); // read response @@ -540,7 +540,7 @@ fn test_response_http_error_handling() { }); let req = srv.get().finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let mut response = srv.send_request(req).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response From e6e83ea57e82b59ad504420b23e2d00950756ce8 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 18 Feb 2019 17:01:35 -0800 Subject: [PATCH 139/427] add Response::map_body --- src/lib.rs | 3 +-- src/response.rs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4e6e37955..8750b24ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,12 +89,11 @@ pub mod h2; pub mod test; pub mod ws; -pub use self::body::{Body, MessageBody}; pub use self::config::{KeepAlive, ServiceConfig, ServiceConfigBuilder}; pub use self::error::{Error, ResponseError, Result}; pub use self::extensions::Extensions; pub use self::httpmessage::HttpMessage; -pub use self::message::{Message, RequestHead, ResponseHead}; +pub use self::message::{Head, Message, RequestHead, ResponseHead}; pub use self::payload::{Payload, PayloadStream}; pub use self::request::Request; pub use self::response::Response; diff --git a/src/response.rs b/src/response.rs index d84100fa2..0295758b2 100644 --- a/src/response.rs +++ b/src/response.rs @@ -242,6 +242,20 @@ impl Response { self.body, ) } + + /// Set a body and return previous body value + pub fn map_body(mut self, f: F) -> Response + where + F: FnOnce(&mut ResponseHead, ResponseBody) -> ResponseBody, + { + let body = f(&mut self.head, self.body); + + Response { + head: self.head, + body: body, + error: self.error, + } + } } impl fmt::Debug for Response { From 037c3da1729d7fd61e5edb2cf89a29463a5263c2 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 18 Feb 2019 18:40:40 -0800 Subject: [PATCH 140/427] enable ssl for connector --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5bcaea7c3..e4dd6c58c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ default = ["session"] session = ["cookie/secure"] # openssl -ssl = ["openssl"] +ssl = ["openssl", "actix-connector/ssl"] [dependencies] actix-service = "0.2.1" From d180b2a1e357d353187a9f67d5b382f63ba77bd0 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 18 Feb 2019 18:46:30 -0800 Subject: [PATCH 141/427] update tests --- test-server/src/lib.rs | 2 +- tests/test_client.rs | 6 +++--- tests/test_server.rs | 47 +++++++++++++++++++++--------------------- tests/test_ws.rs | 2 +- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 3d6d917e5..a13e86cf8 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -53,7 +53,7 @@ pub struct TestServerRuntime { impl TestServer { /// Start new test server with application factory - pub fn with_factory( + pub fn new( factory: F, ) -> TestServerRuntime< impl Service diff --git a/tests/test_client.rs b/tests/test_client.rs index 6f502b0af..f44c45cbc 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -31,7 +31,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ #[test] fn test_h1_v2() { env_logger::init(); - let mut srv = TestServer::with_factory(move || { + let mut srv = TestServer::new(move || { h1::H1Service::build() .finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) .map(|_| ()) @@ -65,7 +65,7 @@ fn test_h1_v2() { #[test] fn test_connection_close() { - let mut srv = TestServer::with_factory(move || { + let mut srv = TestServer::new(move || { h1::H1Service::build() .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .map(|_| ()) @@ -79,7 +79,7 @@ fn test_connection_close() { #[test] fn test_with_query_parameter() { - let mut srv = TestServer::with_factory(move || { + let mut srv = TestServer::new(move || { h1::H1Service::build() .finish(|req: Request| { if req.uri().query().unwrap().contains("qp=") { diff --git a/tests/test_server.rs b/tests/test_server.rs index 53db38403..dc1ebcfbc 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -8,14 +8,15 @@ use bytes::Bytes; use futures::future::{self, ok, Future}; use futures::stream::once; +use actix_http::body::Body; use actix_http::{ - body, client, h1, h2, http, Body, Error, HttpMessage as HttpMessage2, KeepAlive, - Request, Response, + body, client, h1, h2, http, Error, HttpMessage as HttpMessage2, KeepAlive, Request, + Response, }; #[test] fn test_h1() { - let mut srv = TestServer::with_factory(|| { + let mut srv = TestServer::new(|| { h1::H1Service::build() .keep_alive(KeepAlive::Disabled) .client_timeout(1000) @@ -57,7 +58,7 @@ fn ssl_acceptor() -> std::io::Result> { #[test] fn test_h2() -> std::io::Result<()> { let openssl = ssl_acceptor()?; - let mut srv = TestServer::with_factory(move || { + let mut srv = TestServer::new(move || { openssl .clone() .map_err(|e| println!("Openssl error: {}", e)) @@ -83,7 +84,7 @@ fn test_h2_body() -> std::io::Result<()> { let data = "HELLOWORLD".to_owned().repeat(64 * 1024); let openssl = ssl_acceptor()?; - let mut srv = TestServer::with_factory(move || { + let mut srv = TestServer::new(move || { openssl .clone() .map_err(|e| println!("Openssl error: {}", e)) @@ -111,7 +112,7 @@ fn test_h2_body() -> std::io::Result<()> { #[test] fn test_slow_request() { - let srv = TestServer::with_factory(|| { + let srv = TestServer::new(|| { h1::H1Service::build() .client_timeout(100) .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) @@ -127,7 +128,7 @@ fn test_slow_request() { #[test] fn test_malformed_request() { - let srv = TestServer::with_factory(|| { + let srv = TestServer::new(|| { h1::H1Service::new(|_| future::ok::<_, ()>(Response::Ok().finish())).map(|_| ()) }); @@ -140,7 +141,7 @@ fn test_malformed_request() { #[test] fn test_keepalive() { - let srv = TestServer::with_factory(|| { + let srv = TestServer::new(|| { h1::H1Service::build() .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) .map(|_| ()) @@ -160,7 +161,7 @@ fn test_keepalive() { #[test] fn test_keepalive_timeout() { - let srv = TestServer::with_factory(|| { + let srv = TestServer::new(|| { h1::H1Service::build() .keep_alive(1) .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) @@ -181,7 +182,7 @@ fn test_keepalive_timeout() { #[test] fn test_keepalive_close() { - let srv = TestServer::with_factory(|| { + let srv = TestServer::new(|| { h1::H1Service::build() .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) .map(|_| ()) @@ -201,7 +202,7 @@ fn test_keepalive_close() { #[test] fn test_keepalive_http10_default_close() { - let srv = TestServer::with_factory(|| { + let srv = TestServer::new(|| { h1::H1Service::build() .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) .map(|_| ()) @@ -220,7 +221,7 @@ fn test_keepalive_http10_default_close() { #[test] fn test_keepalive_http10() { - let srv = TestServer::with_factory(|| { + let srv = TestServer::new(|| { h1::H1Service::build() .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) .map(|_| ()) @@ -246,7 +247,7 @@ fn test_keepalive_http10() { #[test] fn test_keepalive_disabled() { - let srv = TestServer::with_factory(|| { + let srv = TestServer::new(|| { h1::H1Service::build() .keep_alive(KeepAlive::Disabled) .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) @@ -271,7 +272,7 @@ fn test_content_length() { StatusCode, }; - let mut srv = TestServer::with_factory(|| { + let mut srv = TestServer::new(|| { h1::H1Service::new(|req: Request| { let indx: usize = req.uri().path()[1..].parse().unwrap(); let statuses = [ @@ -320,7 +321,7 @@ fn test_headers() { let data = STR.repeat(10); let data2 = data.clone(); - let mut srv = TestServer::with_factory(move || { + let mut srv = TestServer::new(move || { let data = data.clone(); h1::H1Service::new(move |_| { let mut builder = Response::Ok(); @@ -382,7 +383,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ #[test] fn test_body() { - let mut srv = TestServer::with_factory(|| { + let mut srv = TestServer::new(|| { h1::H1Service::new(|_| future::ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) }); @@ -397,7 +398,7 @@ fn test_body() { #[test] fn test_head_empty() { - let mut srv = TestServer::with_factory(|| { + let mut srv = TestServer::new(|| { h1::H1Service::new(|_| ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) }); @@ -420,7 +421,7 @@ fn test_head_empty() { #[test] fn test_head_binary() { - let mut srv = TestServer::with_factory(|| { + let mut srv = TestServer::new(|| { h1::H1Service::new(|_| { ok::<_, ()>(Response::Ok().content_length(STR.len() as u64).body(STR)) }) @@ -446,7 +447,7 @@ fn test_head_binary() { #[test] fn test_head_binary2() { - let mut srv = TestServer::with_factory(|| { + let mut srv = TestServer::new(|| { h1::H1Service::new(|_| ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) }); @@ -465,7 +466,7 @@ fn test_head_binary2() { #[test] fn test_body_length() { - let mut srv = TestServer::with_factory(|| { + let mut srv = TestServer::new(|| { h1::H1Service::new(|_| { let body = once(Ok(Bytes::from_static(STR.as_ref()))); ok::<_, ()>( @@ -487,7 +488,7 @@ fn test_body_length() { #[test] fn test_body_chunked_explicit() { - let mut srv = TestServer::with_factory(|| { + let mut srv = TestServer::new(|| { h1::H1Service::new(|_| { let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); ok::<_, ()>(Response::Ok().streaming(body)) @@ -508,7 +509,7 @@ fn test_body_chunked_explicit() { #[test] fn test_body_chunked_implicit() { - let mut srv = TestServer::with_factory(|| { + let mut srv = TestServer::new(|| { h1::H1Service::new(|_| { let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); ok::<_, ()>(Response::Ok().streaming(body)) @@ -527,7 +528,7 @@ fn test_body_chunked_implicit() { #[test] fn test_response_http_error_handling() { - let mut srv = TestServer::with_factory(|| { + let mut srv = TestServer::new(|| { h1::H1Service::new(|_| { let broken_header = Bytes::from_static(b"\0\0\0"); ok::<_, ()>( diff --git a/tests/test_ws.rs b/tests/test_ws.rs index e5a54c7c1..4111ca3db 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -34,7 +34,7 @@ fn ws_service(req: ws::Frame) -> impl Future)| { From 842da939dc490a255a9f1a8b4d3d28b3dcc6647a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 18 Feb 2019 20:24:50 -0800 Subject: [PATCH 142/427] fix chunked transfer encoding handling --- src/body.rs | 3 +-- src/client/h2proto.rs | 2 +- src/h1/encoder.rs | 27 ++++++++++++++++++++++----- src/h2/dispatcher.rs | 2 +- src/message.rs | 6 ++++++ src/response.rs | 9 +++++++++ 6 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/body.rs b/src/body.rs index d3e63f9c9..1f218c4ba 100644 --- a/src/body.rs +++ b/src/body.rs @@ -13,7 +13,6 @@ pub enum BodyLength { Empty, Sized(usize), Sized64(u64), - Chunked, Stream, } @@ -331,7 +330,7 @@ where E: Into, { fn length(&self) -> BodyLength { - BodyLength::Chunked + BodyLength::Stream } fn poll_next(&mut self) -> Poll, Error> { diff --git a/src/client/h2proto.rs b/src/client/h2proto.rs index 8804d13f2..617c21b60 100644 --- a/src/client/h2proto.rs +++ b/src/client/h2proto.rs @@ -47,7 +47,7 @@ where // Content length let _ = match length { - BodyLength::Chunked | BodyLength::None => None, + BodyLength::None => None, BodyLength::Stream => { skip_len = false; None diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index 32c8f9c48..7627f6979 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -45,6 +45,8 @@ pub(crate) trait MessageType: Sized { fn headers(&self) -> &HeaderMap; + fn chunked(&self) -> bool; + fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()>; fn encode_headers( @@ -71,8 +73,10 @@ pub(crate) trait MessageType: Sized { } } match length { - BodyLength::Chunked => { - dst.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") + BodyLength::Stream => { + if self.chunked() { + dst.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") + } } BodyLength::Empty => { dst.extend_from_slice(b"\r\ncontent-length: 0\r\n"); @@ -83,7 +87,7 @@ pub(crate) trait MessageType: Sized { write!(dst.writer(), "{}", len)?; dst.extend_from_slice(b"\r\n"); } - BodyLength::None | BodyLength::Stream => dst.extend_from_slice(b"\r\n"), + BodyLength::None => dst.extend_from_slice(b"\r\n"), } // Connection @@ -159,6 +163,10 @@ impl MessageType for Response<()> { Some(self.head().status) } + fn chunked(&self) -> bool { + !self.head().no_chunking + } + fn connection_type(&self) -> Option { self.head().ctype } @@ -188,6 +196,10 @@ impl MessageType for RequestHead { self.ctype } + fn chunked(&self) -> bool { + !self.no_chunking + } + fn headers(&self) -> &HeaderMap { &self.headers } @@ -236,8 +248,13 @@ impl MessageEncoder { BodyLength::Empty => TransferEncoding::empty(), BodyLength::Sized(len) => TransferEncoding::length(len as u64), BodyLength::Sized64(len) => TransferEncoding::length(len), - BodyLength::Chunked => TransferEncoding::chunked(), - BodyLength::Stream => TransferEncoding::eof(), + BodyLength::Stream => { + if message.chunked() { + TransferEncoding::chunked() + } else { + TransferEncoding::eof() + } + } BodyLength::None => TransferEncoding::empty(), }; } else { diff --git a/src/h2/dispatcher.rs b/src/h2/dispatcher.rs index 5a8c4b855..ea8756d27 100644 --- a/src/h2/dispatcher.rs +++ b/src/h2/dispatcher.rs @@ -181,7 +181,7 @@ where _ => (), } let _ = match length { - BodyLength::Chunked | BodyLength::None | BodyLength::Stream => None, + BodyLength::None | BodyLength::Stream => None, BodyLength::Empty => res .headers_mut() .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), diff --git a/src/message.rs b/src/message.rs index 812f099e4..3a1ac1306 100644 --- a/src/message.rs +++ b/src/message.rs @@ -36,6 +36,7 @@ pub trait Head: Default + 'static { self.connection_type() == ConnectionType::Upgrade } + /// Check if keep-alive is enabled fn keep_alive(&self) -> bool { self.connection_type() == ConnectionType::KeepAlive } @@ -50,6 +51,7 @@ pub struct RequestHead { pub version: Version, pub headers: HeaderMap, pub ctype: Option, + pub no_chunking: bool, pub extensions: RefCell, } @@ -61,6 +63,7 @@ impl Default for RequestHead { version: Version::HTTP_11, headers: HeaderMap::with_capacity(16), ctype: None, + no_chunking: false, extensions: RefCell::new(Extensions::new()), } } @@ -120,6 +123,7 @@ pub struct ResponseHead { pub status: StatusCode, pub headers: HeaderMap, pub reason: Option<&'static str>, + pub no_chunking: bool, pub(crate) ctype: Option, } @@ -130,6 +134,7 @@ impl Default for ResponseHead { status: StatusCode::OK, headers: HeaderMap::with_capacity(16), reason: None, + no_chunking: false, ctype: None, } } @@ -139,6 +144,7 @@ impl Head for ResponseHead { fn clear(&mut self) { self.ctype = None; self.reason = None; + self.no_chunking = false; self.headers.clear(); } diff --git a/src/response.rs b/src/response.rs index 0295758b2..2c627d53d 100644 --- a/src/response.rs +++ b/src/response.rs @@ -459,6 +459,15 @@ impl ResponseBuilder { self } + /// Disable chunked transfer encoding for HTTP/1.1 streaming responses. + #[inline] + pub fn no_chunking(&mut self) -> &mut Self { + if let Some(parts) = parts(&mut self.head, &self.err) { + parts.no_chunking = true; + } + self + } + /// Set response content type #[inline] pub fn content_type(&mut self, value: V) -> &mut Self From c8713d045cf5969df0daa3c81b14b272a4492fc6 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 18 Feb 2019 21:41:38 -0800 Subject: [PATCH 143/427] poll payload again if framed object get flushed during same iteration --- src/h1/dispatcher.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 22d7ea865..9ae8cd2a1 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -167,7 +167,7 @@ where } /// Flush stream - fn poll_flush(&mut self) -> Poll<(), DispatchError> { + fn poll_flush(&mut self) -> Poll> { if !self.framed.is_write_buf_empty() { match self.framed.poll_complete() { Ok(Async::NotReady) => Ok(Async::NotReady), @@ -180,11 +180,11 @@ where if self.payload.is_some() && self.state.is_empty() { return Err(DispatchError::PayloadIsNotConsumed); } - Ok(Async::Ready(())) + Ok(Async::Ready(true)) } } } else { - Ok(Async::Ready(())) + Ok(Async::Ready(false)) } } @@ -482,8 +482,12 @@ where } else { inner.poll_keepalive()?; inner.poll_request()?; - inner.poll_response()?; - inner.poll_flush()?; + loop { + inner.poll_response()?; + if let Async::Ready(false) = inner.poll_flush()? { + break; + } + } if inner.flags.contains(Flags::DISCONNECTED) { return Ok(Async::Ready(H1ServiceResult::Disconnected)); From 781f1a3fef466a820bdcf4f841616f0a8c9fa55f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 18 Feb 2019 22:20:00 -0800 Subject: [PATCH 144/427] do not skip content length is no chunking is selected --- src/h1/encoder.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index 7627f6979..d4f54d247 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -57,6 +57,7 @@ pub(crate) trait MessageType: Sized { ctype: ConnectionType, config: &ServiceConfig, ) -> io::Result<()> { + let chunked = self.chunked(); let mut skip_len = length != BodyLength::Stream; // Content length @@ -74,8 +75,10 @@ pub(crate) trait MessageType: Sized { } match length { BodyLength::Stream => { - if self.chunked() { + if chunked { dst.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") + } else { + skip_len = false; } } BodyLength::Empty => { From 7f749ac9cccf01ad63f82648dcbfef5d15d715ff Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 18 Feb 2019 22:34:22 -0800 Subject: [PATCH 145/427] add missing end of line --- src/h1/encoder.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index d4f54d247..9fe5ba69a 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -79,6 +79,7 @@ pub(crate) trait MessageType: Sized { dst.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") } else { skip_len = false; + dst.extend_from_slice(b"\r\n"); } } BodyLength::Empty => { From 60a8da5c05e71a20e214fc6bc392197a2ce70eb0 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 20 Feb 2019 21:02:23 -0800 Subject: [PATCH 146/427] remove Response constraint --- src/response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/response.rs b/src/response.rs index 2c627d53d..8d21d6b3f 100644 --- a/src/response.rs +++ b/src/response.rs @@ -16,7 +16,7 @@ use crate::header::{Header, IntoHeaderValue}; use crate::message::{ConnectionType, Head, Message, ResponseHead}; /// An HTTP Response -pub struct Response { +pub struct Response { head: Message, body: ResponseBody, error: Option, From 2f89b12f4faa178e71b2465106ef8454d5c44bcf Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 20 Feb 2019 21:05:37 -0800 Subject: [PATCH 147/427] remove more response containts --- src/response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/response.rs b/src/response.rs index 8d21d6b3f..aab881062 100644 --- a/src/response.rs +++ b/src/response.rs @@ -92,7 +92,7 @@ impl Response { } } -impl Response { +impl Response { #[inline] /// Http message part of the response pub fn head(&self) -> &ResponseHead { From b80ee71785c467958c41d5bd0be55e9b13034491 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 22 Feb 2019 14:21:35 -0800 Subject: [PATCH 148/427] use new new service api --- Cargo.toml | 20 +++++++++++++++----- src/h1/service.rs | 6 +++--- src/h2/service.rs | 4 ++-- src/service/senderror.rs | 4 ++-- src/ws/service.rs | 2 +- test-server/Cargo.toml | 10 +++++++--- tests/test_server.rs | 2 -- 7 files changed, 30 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e4dd6c58c..edf572af4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,10 +37,18 @@ session = ["cookie/secure"] ssl = ["openssl", "actix-connector/ssl"] [dependencies] -actix-service = "0.2.1" +#actix-service = "0.2.1" actix-codec = "0.1.0" -actix-connector = "0.2.0" -actix-utils = "0.2.2" +#actix-connector = "0.2.0" +#actix-utils = "0.2.2" + +actix-service = { git = "https://github.com/actix/actix-net" } +actix-connector = { git = "https://github.com/actix/actix-net" } +actix-utils = { git = "https://github.com/actix/actix-net" } + +#actix-service = { path = "../actix-net/actix-service" } +#actix-connector = { path = "../actix-net/actix-connector" } +#actix-utils = { path = "../actix-net/actix-utils" } base64 = "0.10" backtrace = "0.3" @@ -78,8 +86,10 @@ openssl = { version="0.10", optional = true } [dev-dependencies] actix-rt = "0.1.0" -actix-server = { version="0.2", features=["ssl"] } -actix-connector = { version="0.2.0", features=["ssl"] } +actix-server = { git = "https://github.com/actix/actix-net", features=["ssl"] } +#actix-server = { path = "../actix-net/actix-server", features=["ssl"] } +#actix-connector = { path = "../actix-net/actix-connector", features=["ssl"] } +actix-connector = { git = "https://github.com/actix/actix-net", features=["ssl"] } actix-http-test = { path="test-server", features=["ssl"] } env_logger = "0.6" serde_derive = "1.0" diff --git a/src/h1/service.rs b/src/h1/service.rs index cb8dae54d..4beb4c9e4 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -67,9 +67,9 @@ where type Service = H1ServiceHandler; type Future = H1ServiceResponse; - fn new_service(&self) -> Self::Future { + fn new_service(&self, _: &()) -> Self::Future { H1ServiceResponse { - fut: self.srv.new_service(), + fut: self.srv.new_service(&()), cfg: Some(self.cfg.clone()), _t: PhantomData, } @@ -309,7 +309,7 @@ where type Service = OneRequestService; type Future = FutureResult; - fn new_service(&self) -> Self::Future { + fn new_service(&self, _: &()) -> Self::Future { ok(OneRequestService { config: self.config.clone(), _t: PhantomData, diff --git a/src/h2/service.rs b/src/h2/service.rs index 16b7e4956..fcfc0be22 100644 --- a/src/h2/service.rs +++ b/src/h2/service.rs @@ -70,9 +70,9 @@ where type Service = H2ServiceHandler; type Future = H2ServiceResponse; - fn new_service(&self) -> Self::Future { + fn new_service(&self, _: &()) -> Self::Future { H2ServiceResponse { - fut: self.srv.new_service(), + fut: self.srv.new_service(&()), cfg: Some(self.cfg.clone()), _t: PhantomData, } diff --git a/src/service/senderror.rs b/src/service/senderror.rs index b469a61e6..44d362593 100644 --- a/src/service/senderror.rs +++ b/src/service/senderror.rs @@ -34,7 +34,7 @@ where type Service = SendError; type Future = FutureResult; - fn new_service(&self) -> Self::Future { + fn new_service(&self, _: &()) -> Self::Future { ok(SendError(PhantomData)) } } @@ -142,7 +142,7 @@ where type Service = SendResponse; type Future = FutureResult; - fn new_service(&self) -> Self::Future { + fn new_service(&self, _: &()) -> Self::Future { ok(SendResponse(PhantomData)) } } diff --git a/src/ws/service.rs b/src/ws/service.rs index 137d41d43..f3b066053 100644 --- a/src/ws/service.rs +++ b/src/ws/service.rs @@ -28,7 +28,7 @@ impl NewService for VerifyWebSockets { type Service = VerifyWebSockets; type Future = FutureResult; - fn new_service(&self) -> Self::Future { + fn new_service(&self, _: &()) -> Self::Future { ok(VerifyWebSockets { _t: PhantomData }) } } diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 9c71a25c0..f5e8afd22 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -33,12 +33,16 @@ ssl = ["openssl", "actix-http/ssl", "actix-server/ssl"] [dependencies] actix-codec = "0.1" -actix-service = "0.2.0" actix-rt = "0.1.0" -actix-server = "0.2.0" -actix-utils = "0.2.0" actix-http = { path=".." } +#actix-service = "0.2.0" +#actix-server = "0.2.0" +#actix-utils = "0.2.0" +actix-service = { git = "https://github.com/actix/actix-net" } +actix-server = { git = "https://github.com/actix/actix-net" } +actix-utils = { git = "https://github.com/actix/actix-net" } + base64 = "0.10" bytes = "0.4" cookie = { version="0.11", features=["percent-encode"] } diff --git a/tests/test_server.rs b/tests/test_server.rs index dc1ebcfbc..fd848b82b 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -514,7 +514,6 @@ fn test_body_chunked_implicit() { let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); ok::<_, ()>(Response::Ok().streaming(body)) }) - .map(|_| ()) }); let req = srv.get().finish().unwrap(); @@ -537,7 +536,6 @@ fn test_response_http_error_handling() { .body(STR), ) }) - .map(|_| ()) }); let req = srv.get().finish().unwrap(); From 38c86d4683ba661e29be84aeb0a2e845d326e697 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 1 Mar 2019 20:33:31 -0800 Subject: [PATCH 149/427] update tarpaulin travis config --- .travis.yml | 42 +++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/.travis.yml b/.travis.yml index c9c9db14f..b97272588 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,12 @@ language: rust sudo: required dist: trusty +addons: + apt: + packages: + - libssl-dev -cache: - cargo: true - apt: true +cache: cargo matrix: include: @@ -14,32 +16,22 @@ matrix: allow_failures: - rust: nightly -env: - global: - - RUSTFLAGS="-C link-dead-code" - - OPENSSL_VERSION=openssl-1.0.2 - -before_install: - - sudo add-apt-repository -y ppa:0k53d-karl-f830m/openssl - - sudo apt-get update -qq - - sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev - -before_script: - - export PATH=$PATH:~/.cargo/bin +before_cache: | + if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then + RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin -f + fi script: - - | - if [[ "$TRAVIS_RUST_VERSION" != "nightly" ]]; then - cargo clean - cargo test --features="ssl" - fi - - | - if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then - RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install -f cargo-tarpaulin - RUST_BACKTRACE=1 cargo tarpaulin --features="ssl" --out Xml +- cargo clean +- cargo build --features="ssl" +- cargo test --features="ssl" + +after_success: | + if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then + cargo tarpaulin --features="ssl" --out Xml bash <(curl -s https://codecov.io/bash) echo "Uploaded code coverage" - fi + fi # Upload docs #after_success: From 650474ca39b0613a14bd2b4d67084b9f6c92a31c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 1 Mar 2019 21:02:56 -0800 Subject: [PATCH 150/427] choose openssl version for travis --- .travis.yml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index b97272588..4f35c6564 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,10 @@ language: rust sudo: required dist: trusty -addons: - apt: - packages: - - libssl-dev -cache: cargo +cache: + cargo: true + apt: true matrix: include: @@ -16,6 +14,16 @@ matrix: allow_failures: - rust: nightly +env: + global: + - RUSTFLAGS="-C link-dead-code" + - OPENSSL_VERSION=openssl-1.0.2 + +before_install: + - sudo add-apt-repository -y ppa:0k53d-karl-f830m/openssl + - sudo apt-get update -qq + - sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev + before_cache: | if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin -f From 5fff07402efe32c180ffba8415e9c39a00b40d25 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 1 Mar 2019 21:36:37 -0800 Subject: [PATCH 151/427] downgrade tarpaulin --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4f35c6564..283437ce6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ before_install: before_cache: | if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then - RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin -f + RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin -f --version 0.6.7 fi script: From 2d7293aaf8fa7e81cec3efc2a50ffbd79f9b1de9 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 1 Mar 2019 22:51:32 -0800 Subject: [PATCH 152/427] copy actix-web2 --- CHANGES.md | 832 +------- Cargo.toml | 122 +- build.rs | 16 - examples/basic.rs | 54 + rustfmt.toml | 3 - src/app.rs | 648 ++++++ src/application.rs | 416 ++-- src/blocking.rs | 74 + src/body.rs | 391 ---- src/client/connector.rs | 1340 ------------ src/client/mod.rs | 120 -- src/client/parser.rs | 238 --- src/client/pipeline.rs | 553 ----- src/client/request.rs | 783 ------- src/client/response.rs | 124 -- src/client/writer.rs | 412 ---- src/context.rs | 294 --- src/de.rs | 455 ----- src/error.rs | 1426 ------------- src/extensions.rs | 114 -- src/extractor.rs | 1280 ++++++------ src/filter.rs | 327 +++ src/framed_app.rs | 240 +++ src/framed_handler.rs | 379 ++++ src/framed_route.rs | 448 ++++ src/fs.rs | 2354 +++++++++++----------- src/handler.rs | 872 ++++---- src/header/common/accept.rs | 159 -- src/header/common/accept_charset.rs | 69 - src/header/common/accept_encoding.rs | 72 - src/header/common/accept_language.rs | 75 - src/header/common/allow.rs | 85 - src/header/common/cache_control.rs | 254 --- src/header/common/content_disposition.rs | 914 --------- src/header/common/content_language.rs | 65 - src/header/common/content_range.rs | 210 -- src/header/common/content_type.rs | 122 -- src/header/common/date.rs | 42 - src/header/common/etag.rs | 96 - src/header/common/expires.rs | 39 - src/header/common/if_match.rs | 70 - src/header/common/if_modified_since.rs | 39 - src/header/common/if_none_match.rs | 92 - src/header/common/if_range.rs | 115 -- src/header/common/if_unmodified_since.rs | 40 - src/header/common/last_modified.rs | 38 - src/header/common/mod.rs | 350 ---- src/header/common/range.rs | 434 ---- src/header/mod.rs | 471 ----- src/header/shared/charset.rs | 152 -- src/header/shared/encoding.rs | 59 - src/header/shared/entity.rs | 266 --- src/header/shared/httpdate.rs | 119 -- src/header/shared/mod.rs | 14 - src/header/shared/quality_item.rs | 294 --- src/helpers.rs | 709 ++----- src/httpcodes.rs | 84 - src/httpmessage.rs | 855 -------- src/httprequest.rs | 545 ----- src/httpresponse.rs | 1458 -------------- src/info.rs | 78 +- src/json.rs | 519 ----- src/lib.rs | 309 +-- src/middleware/compress.rs | 443 ++++ src/middleware/cors.rs | 1227 ----------- src/middleware/csrf.rs | 275 --- src/middleware/defaultheaders.rs | 147 +- src/middleware/errhandlers.rs | 141 -- src/middleware/identity.rs | 399 ---- src/middleware/logger.rs | 384 ---- src/middleware/mod.rs | 113 +- src/middleware/session.rs | 618 ------ src/multipart.rs | 815 -------- src/param.rs | 334 --- src/payload.rs | 715 ------- src/pipeline.rs | 869 -------- src/pred.rs | 328 --- src/request.rs | 174 ++ src/resource.rs | 513 +++-- src/responder.rs | 259 +++ src/route.rs | 896 ++++---- src/router.rs | 1247 ------------ src/scope.rs | 1236 ------------ src/server/acceptor.rs | 383 ---- src/server/builder.rs | 134 -- src/server/channel.rs | 300 --- src/server/error.rs | 108 - src/server/h1.rs | 1353 ------------- src/server/h1decoder.rs | 541 ----- src/server/h1writer.rs | 364 ---- src/server/h2.rs | 472 ----- src/server/h2writer.rs | 268 --- src/server/handler.rs | 208 -- src/server/helpers.rs | 208 -- src/server/http.rs | 579 ------ src/server/incoming.rs | 69 - src/server/input.rs | 288 --- src/server/message.rs | 284 --- src/server/mod.rs | 370 ---- src/server/output.rs | 760 ------- src/server/service.rs | 272 --- src/server/settings.rs | 503 ----- src/server/ssl/mod.rs | 12 - src/server/ssl/nativetls.rs | 34 - src/server/ssl/openssl.rs | 87 - src/server/ssl/rustls.rs | 87 - src/service.rs | 155 ++ src/state.rs | 120 ++ src/test.rs | 668 +----- src/uri.rs | 177 -- src/with.rs | 383 ---- src/ws/client.rs | 602 ------ src/ws/context.rs | 341 ---- src/ws/frame.rs | 538 ----- src/ws/mask.rs | 152 -- src/ws/mod.rs | 477 ----- src/ws/proto.rs | 318 --- tests/identity.pfx | Bin 5549 -> 0 bytes tests/test space.binary | 1 - tests/test.binary | 1 - tests/test.png | Bin 168 -> 0 bytes tests/test_client.rs | 508 ----- tests/test_custom_pipeline.rs | 81 - tests/test_handlers.rs | 677 ------- tests/test_middleware.rs | 1055 ---------- tests/test_server.rs | 1942 +++++++----------- tests/test_ws.rs | 395 ---- 127 files changed, 7554 insertions(+), 43481 deletions(-) delete mode 100644 build.rs create mode 100644 examples/basic.rs create mode 100644 src/app.rs create mode 100644 src/blocking.rs delete mode 100644 src/body.rs delete mode 100644 src/client/connector.rs delete mode 100644 src/client/mod.rs delete mode 100644 src/client/parser.rs delete mode 100644 src/client/pipeline.rs delete mode 100644 src/client/request.rs delete mode 100644 src/client/response.rs delete mode 100644 src/client/writer.rs delete mode 100644 src/context.rs delete mode 100644 src/de.rs delete mode 100644 src/error.rs delete mode 100644 src/extensions.rs create mode 100644 src/filter.rs create mode 100644 src/framed_app.rs create mode 100644 src/framed_handler.rs create mode 100644 src/framed_route.rs delete mode 100644 src/header/common/accept.rs delete mode 100644 src/header/common/accept_charset.rs delete mode 100644 src/header/common/accept_encoding.rs delete mode 100644 src/header/common/accept_language.rs delete mode 100644 src/header/common/allow.rs delete mode 100644 src/header/common/cache_control.rs delete mode 100644 src/header/common/content_disposition.rs delete mode 100644 src/header/common/content_language.rs delete mode 100644 src/header/common/content_range.rs delete mode 100644 src/header/common/content_type.rs delete mode 100644 src/header/common/date.rs delete mode 100644 src/header/common/etag.rs delete mode 100644 src/header/common/expires.rs delete mode 100644 src/header/common/if_match.rs delete mode 100644 src/header/common/if_modified_since.rs delete mode 100644 src/header/common/if_none_match.rs delete mode 100644 src/header/common/if_range.rs delete mode 100644 src/header/common/if_unmodified_since.rs delete mode 100644 src/header/common/last_modified.rs delete mode 100644 src/header/common/mod.rs delete mode 100644 src/header/common/range.rs delete mode 100644 src/header/mod.rs delete mode 100644 src/header/shared/charset.rs delete mode 100644 src/header/shared/encoding.rs delete mode 100644 src/header/shared/entity.rs delete mode 100644 src/header/shared/httpdate.rs delete mode 100644 src/header/shared/mod.rs delete mode 100644 src/header/shared/quality_item.rs delete mode 100644 src/httpcodes.rs delete mode 100644 src/httpmessage.rs delete mode 100644 src/httprequest.rs delete mode 100644 src/httpresponse.rs delete mode 100644 src/json.rs create mode 100644 src/middleware/compress.rs delete mode 100644 src/middleware/cors.rs delete mode 100644 src/middleware/csrf.rs delete mode 100644 src/middleware/errhandlers.rs delete mode 100644 src/middleware/identity.rs delete mode 100644 src/middleware/logger.rs delete mode 100644 src/middleware/session.rs delete mode 100644 src/multipart.rs delete mode 100644 src/param.rs delete mode 100644 src/payload.rs delete mode 100644 src/pipeline.rs delete mode 100644 src/pred.rs create mode 100644 src/request.rs create mode 100644 src/responder.rs delete mode 100644 src/router.rs delete mode 100644 src/scope.rs delete mode 100644 src/server/acceptor.rs delete mode 100644 src/server/builder.rs delete mode 100644 src/server/channel.rs delete mode 100644 src/server/error.rs delete mode 100644 src/server/h1.rs delete mode 100644 src/server/h1decoder.rs delete mode 100644 src/server/h1writer.rs delete mode 100644 src/server/h2.rs delete mode 100644 src/server/h2writer.rs delete mode 100644 src/server/handler.rs delete mode 100644 src/server/helpers.rs delete mode 100644 src/server/http.rs delete mode 100644 src/server/incoming.rs delete mode 100644 src/server/input.rs delete mode 100644 src/server/message.rs delete mode 100644 src/server/mod.rs delete mode 100644 src/server/output.rs delete mode 100644 src/server/service.rs delete mode 100644 src/server/settings.rs delete mode 100644 src/server/ssl/mod.rs delete mode 100644 src/server/ssl/nativetls.rs delete mode 100644 src/server/ssl/openssl.rs delete mode 100644 src/server/ssl/rustls.rs create mode 100644 src/service.rs create mode 100644 src/state.rs delete mode 100644 src/uri.rs delete mode 100644 src/with.rs delete mode 100644 src/ws/client.rs delete mode 100644 src/ws/context.rs delete mode 100644 src/ws/frame.rs delete mode 100644 src/ws/mask.rs delete mode 100644 src/ws/mod.rs delete mode 100644 src/ws/proto.rs delete mode 100644 tests/identity.pfx delete mode 100644 tests/test space.binary delete mode 100644 tests/test.binary delete mode 100644 tests/test.png delete mode 100644 tests/test_client.rs delete mode 100644 tests/test_custom_pipeline.rs delete mode 100644 tests/test_handlers.rs delete mode 100644 tests/test_middleware.rs delete mode 100644 tests/test_ws.rs diff --git a/CHANGES.md b/CHANGES.md index d1d838f43..b93e282ac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,833 +1,5 @@ # Changes -## [x.x.xx] - xxxx-xx-xx +## [0.1.0] - 2018-10-x -### Added - -* Add `from_file` and `from_file_with_config` to `NamedFile` to allow sending files without a known path. #670 - -* Add `insert` and `remove` methods to `HttpResponseBuilder` - -### Fixed - -* Ignored the `If-Modified-Since` if `If-None-Match` is specified. #680 - -## [0.7.18] - 2019-01-10 - -### Added - -* Add `with_cookie` for `TestRequest` to allow users to customize request cookie. #647 - -* Add `cookie` method for `TestRequest` to allow users to add cookie dynamically. - -### Fixed - -* StaticFiles decode special characters in request's path - -* Fix test server listener leak #654 - -## [0.7.17] - 2018-12-25 - -### Added - -* Support for custom content types in `JsonConfig`. #637 - -* Send `HTTP/1.1 100 Continue` if request contains `expect: continue` header #634 - -### Fixed - -* HTTP1 decoder should perform case-insentive comparison for client requests (e.g. `Keep-Alive`). #631 - -* Access-Control-Allow-Origin header should only a return a single, matching origin. #603 - -## [0.7.16] - 2018-12-11 - -### Added - -* Implement `FromRequest` extractor for `Either` - -* Implement `ResponseError` for `SendError` - - -## [0.7.15] - 2018-12-05 - -### Changed - -* `ClientConnector::resolver` now accepts `Into` instead of `Addr`. It enables user to implement own resolver. - -* `QueryConfig` and `PathConfig` are made public. - -* `AsyncResult::async` is changed to `AsyncResult::future` as `async` is reserved keyword in 2018 edition. - -### Added - -* By default, `Path` extractor now percent decode all characters. This behaviour can be disabled - with `PathConfig::default().disable_decoding()` - - -## [0.7.14] - 2018-11-14 - -### Added - -* Add method to configure custom error handler to `Query` and `Path` extractors. - -* Add method to configure `SameSite` option in `CookieIdentityPolicy`. - -* By default, `Path` extractor now percent decode all characters. This behaviour can be disabled - with `PathConfig::default().disable_decoding()` - - -### Fixed - -* Fix websockets connection drop if request contains "content-length" header #567 - -* Fix keep-alive timer reset - -* HttpServer now treats streaming bodies the same for HTTP/1.x protocols. #549 - -* Set nodelay for socket #560 - - -## [0.7.13] - 2018-10-14 - -### Fixed - -* Fixed rustls support - -* HttpServer not sending streamed request body on HTTP/2 requests #544 - - -## [0.7.12] - 2018-10-10 - -### Changed - -* Set min version for actix - -* Set min version for actix-net - - -## [0.7.11] - 2018-10-09 - -### Fixed - -* Fixed 204 responses for http/2 - - -## [0.7.10] - 2018-10-09 - -### Fixed - -* Fixed panic during graceful shutdown - - -## [0.7.9] - 2018-10-09 - -### Added - -* Added client shutdown timeout setting - -* Added slow request timeout setting - -* Respond with 408 response on slow request timeout #523 - - -### Fixed - -* HTTP1 decoding errors are reported to the client. #512 - -* Correctly compose multiple allowed origins in CORS. #517 - -* Websocket server finished() isn't called if client disconnects #511 - -* Responses with the following codes: 100, 101, 102, 204 -- are sent without Content-Length header. #521 - -* Correct usage of `no_http2` flag in `bind_*` methods. #519 - - -## [0.7.8] - 2018-09-17 - -### Added - -* Use server `Keep-Alive` setting as slow request timeout #439 - -### Changed - -* Use 5 seconds keep-alive timer by default. - -### Fixed - -* Fixed wrong error message for i16 type #510 - - -## [0.7.7] - 2018-09-11 - -### Fixed - -* Fix linked list of HttpChannels #504 - -* Fix requests to TestServer fail #508 - - -## [0.7.6] - 2018-09-07 - -### Fixed - -* Fix system_exit in HttpServer #501 - -* Fix parsing of route param containin regexes with repetition #500 - -### Changes - -* Unhide `SessionBackend` and `SessionImpl` traits #455 - - -## [0.7.5] - 2018-09-04 - -### Added - -* Added the ability to pass a custom `TlsConnector`. - -* Allow to register handlers on scope level #465 - - -### Fixed - -* Handle socket read disconnect - -* Handling scoped paths without leading slashes #460 - - -### Changed - -* Read client response until eof if connection header set to close #464 - - -## [0.7.4] - 2018-08-23 - -### Added - -* Added `HttpServer::maxconn()` and `HttpServer::maxconnrate()`, - accept backpressure #250 - -* Allow to customize connection handshake process via `HttpServer::listen_with()` - and `HttpServer::bind_with()` methods - -* Support making client connections via `tokio-uds`'s `UnixStream` when "uds" feature is enabled #472 - -### Changed - -* It is allowed to use function with up to 10 parameters for handler with `extractor parameters`. - `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple - even for handler with one parameter. - -* native-tls - 0.2 - -* `Content-Disposition` is re-worked. Its parser is now more robust and handles quoted content better. See #461 - -### Fixed - -* Use zlib instead of raw deflate for decoding and encoding payloads with - `Content-Encoding: deflate`. - -* Fixed headers formating for CORS Middleware Access-Control-Expose-Headers #436 - -* Fix adding multiple response headers #446 - -* Client includes port in HOST header when it is not default(e.g. not 80 and 443). #448 - -* Panic during access without routing being set #452 - -* Fixed http/2 error handling - -### Deprecated - -* `HttpServer::no_http2()` is deprecated, use `OpensslAcceptor::with_flags()` or - `RustlsAcceptor::with_flags()` instead - -* `HttpServer::listen_tls()`, `HttpServer::listen_ssl()`, `HttpServer::listen_rustls()` have been - deprecated in favor of `HttpServer::listen_with()` with specific `acceptor`. - -* `HttpServer::bind_tls()`, `HttpServer::bind_ssl()`, `HttpServer::bind_rustls()` have been - deprecated in favor of `HttpServer::bind_with()` with specific `acceptor`. - - -## [0.7.3] - 2018-08-01 - -### Added - -* Support HTTP/2 with rustls #36 - -* Allow TestServer to open a websocket on any URL (TestServer::ws_at()) #433 - -### Fixed - -* Fixed failure 0.1.2 compatibility - -* Do not override HOST header for client request #428 - -* Gz streaming, use `flate2::write::GzDecoder` #228 - -* HttpRequest::url_for is not working with scopes #429 - -* Fixed headers' formating for CORS Middleware `Access-Control-Expose-Headers` header value to HTTP/1.1 & HTTP/2 spec-compliant format #436 - - -## [0.7.2] - 2018-07-26 - -### Added - -* Add implementation of `FromRequest` for `Option` and `Result` - -* Allow to handle application prefix, i.e. allow to handle `/app` path - for application with `/app` prefix. - Check [`App::prefix()`](https://actix.rs/actix-web/actix_web/struct.App.html#method.prefix) - api doc. - -* Add `CookieSessionBackend::http_only` method to set `HttpOnly` directive of cookies - -### Changed - -* Upgrade to cookie 0.11 - -* Removed the timestamp from the default logger middleware - -### Fixed - -* Missing response header "content-encoding" #421 - -* Fix stream draining for http/2 connections #290 - - -## [0.7.1] - 2018-07-21 - -### Fixed - -* Fixed default_resource 'not yet implemented' panic #410 - - -## [0.7.0] - 2018-07-21 - -### Added - -* Add `fs::StaticFileConfig` to provide means of customizing static - file services. It allows to map `mime` to `Content-Disposition`, - specify whether to use `ETag` and `Last-Modified` and allowed methods. - -* Add `.has_prefixed_resource()` method to `router::ResourceInfo` - for route matching with prefix awareness - -* Add `HttpMessage::readlines()` for reading line by line. - -* Add `ClientRequestBuilder::form()` for sending `application/x-www-form-urlencoded` requests. - -* Add method to configure custom error handler to `Form` extractor. - -* Add methods to `HttpResponse` to retrieve, add, and delete cookies - -* Add `.set_content_type()` and `.set_content_disposition()` methods - to `fs::NamedFile` to allow overriding the values inferred by default - -* Add `fs::file_extension_to_mime()` helper function to get the MIME - type for a file extension - -* Add `.content_disposition()` method to parse Content-Disposition of - multipart fields - -* Re-export `actix::prelude::*` as `actix_web::actix` module. - -* `HttpRequest::url_for_static()` for a named route with no variables segments - -* Propagation of the application's default resource to scopes that haven't set a default resource. - - -### Changed - -* Min rustc version is 1.26 - -* Use tokio instead of tokio-core - -* `CookieSessionBackend` sets percent encoded cookies for outgoing HTTP messages. - -* Became possible to use enums with query extractor. - Issue [#371](https://github.com/actix/actix-web/issues/371). - [Example](https://github.com/actix/actix-web/blob/master/tests/test_handlers.rs#L94-L134) - -* `HttpResponse::into_builder()` now moves cookies into the builder - instead of dropping them - -* For safety and performance reasons `Handler::handle()` uses `&self` instead of `&mut self` - -* `Handler::handle()` uses `&HttpRequest` instead of `HttpRequest` - -* Added header `User-Agent: Actix-web/` to default headers when building a request - -* port `Extensions` type from http create, we don't need `Send + Sync` - -* `HttpRequest::query()` returns `Ref>` - -* `HttpRequest::cookies()` returns `Ref>>` - -* `StaticFiles::new()` returns `Result, Error>` instead of `StaticFiles` - -* `StaticFiles` uses the default handler if the file does not exist - - -### Removed - -* Remove `Route::with2()` and `Route::with3()` use tuple of extractors instead. - -* Remove `HttpMessage::range()` - - -## [0.6.15] - 2018-07-11 - -### Fixed - -* Fix h2 compatibility #352 - -* Fix duplicate tail of StaticFiles with index_file. #344 - - -## [0.6.14] - 2018-06-21 - -### Added - -* Allow to disable masking for websockets client - -### Fixed - -* SendRequest execution fails with the "internal error: entered unreachable code" #329 - - -## [0.6.13] - 2018-06-11 - -* http/2 end-of-frame is not set if body is empty bytes #307 - -* InternalError can trigger memory unsafety #301 - - -## [0.6.12] - 2018-06-08 - -### Added - -* Add `Host` filter #287 - -* Allow to filter applications - -* Improved failure interoperability with downcasting #285 - -* Allow to use custom resolver for `ClientConnector` - - -## [0.6.11] - 2018-06-05 - -* Support chunked encoding for UrlEncoded body #262 - -* `HttpRequest::url_for()` for a named route with no variables segments #265 - -* `Middleware::response()` is not invoked if error result was returned by another `Middleware::start()` #255 - -* CORS: Do not validate Origin header on non-OPTION requests #271 - -* Fix multipart upload "Incomplete" error #282 - - -## [0.6.10] - 2018-05-24 - -### Added - -* Allow to use path without trailing slashes for scope registration #241 - -* Allow to set encoding for exact NamedFile #239 - -### Fixed - -* `TestServer::post()` actually sends `GET` request #240 - - -## 0.6.9 (2018-05-22) - -* Drop connection if request's payload is not fully consumed #236 - -* Fix streaming response with body compression - - -## 0.6.8 (2018-05-20) - -* Fix scope resource path extractor #234 - -* Re-use tcp listener on pause/resume - - -## 0.6.7 (2018-05-17) - -* Fix compilation with --no-default-features - - -## 0.6.6 (2018-05-17) - -* Panic during middleware execution #226 - -* Add support for listen_tls/listen_ssl #224 - -* Implement extractor for `Session` - -* Ranges header support for NamedFile #60 - - -## 0.6.5 (2018-05-15) - -* Fix error handling during request decoding #222 - - -## 0.6.4 (2018-05-11) - -* Fix segfault in ServerSettings::get_response_builder() - - -## 0.6.3 (2018-05-10) - -* Add `Router::with_async()` method for async handler registration. - -* Added error response functions for 501,502,503,504 - -* Fix client request timeout handling - - -## 0.6.2 (2018-05-09) - -* WsWriter trait is optional. - - -## 0.6.1 (2018-05-08) - -* Fix http/2 payload streaming #215 - -* Fix connector's default `keep-alive` and `lifetime` settings #212 - -* Send `ErrorNotFound` instead of `ErrorBadRequest` when path extractor fails #214 - -* Allow to exclude certain endpoints from logging #211 - - -## 0.6.0 (2018-05-08) - -* Add route scopes #202 - -* Allow to use ssl and non-ssl connections at the same time #206 - -* Websocket CloseCode Empty/Status is ambiguous #193 - -* Add Content-Disposition to NamedFile #204 - -* Allow to access Error's backtrace object - -* Allow to override files listing renderer for `StaticFiles` #203 - -* Various extractor usability improvements #207 - - -## 0.5.6 (2018-04-24) - -* Make flate2 crate optional #200 - - -## 0.5.5 (2018-04-24) - -* Fix panic when Websocket is closed with no error code #191 - -* Allow to use rust backend for flate2 crate #199 - -## 0.5.4 (2018-04-19) - -* Add identity service middleware - -* Middleware response() is not invoked if there was an error in async handler #187 - -* Use Display formatting for InternalError Display implementation #188 - - -## 0.5.3 (2018-04-18) - -* Impossible to quote slashes in path parameters #182 - - -## 0.5.2 (2018-04-16) - -* Allow to configure StaticFiles's CpuPool, via static method or env variable - -* Add support for custom handling of Json extractor errors #181 - -* Fix StaticFiles does not support percent encoded paths #177 - -* Fix Client Request with custom Body Stream halting on certain size requests #176 - - -## 0.5.1 (2018-04-12) - -* Client connector provides stats, `ClientConnector::stats()` - -* Fix end-of-stream handling in parse_payload #173 - -* Fix StaticFiles generate a lot of threads #174 - - -## 0.5.0 (2018-04-10) - -* Type-safe path/query/form parameter handling, using serde #70 - -* HttpResponse builder's methods `.body()`, `.finish()`, `.json()` - return `HttpResponse` instead of `Result` - -* Use more ergonomic `actix_web::Error` instead of `http::Error` for `ClientRequestBuilder::body()` - -* Added `signed` and `private` `CookieSessionBackend`s - -* Added `HttpRequest::resource()`, returns current matched resource - -* Added `ErrorHandlers` middleware - -* Fix router cannot parse Non-ASCII characters in URL #137 - -* Fix client connection pooling - -* Fix long client urls #129 - -* Fix panic on invalid URL characters #130 - -* Fix logger request duration calculation #152 - -* Fix prefix and static file serving #168 - - -## 0.4.10 (2018-03-20) - -* Use `Error` instead of `InternalError` for `error::ErrorXXXX` methods - -* Allow to set client request timeout - -* Allow to set client websocket handshake timeout - -* Refactor `TestServer` configuration - -* Fix server websockets big payloads support - -* Fix http/2 date header generation - - -## 0.4.9 (2018-03-16) - -* Allow to disable http/2 support - -* Wake payload reading task when data is available - -* Fix server keep-alive handling - -* Send Query Parameters in client requests #120 - -* Move brotli encoding to a feature - -* Add option of default handler for `StaticFiles` handler #57 - -* Add basic client connection pooling - - -## 0.4.8 (2018-03-12) - -* Allow to set read buffer capacity for server request - -* Handle WouldBlock error for socket accept call - - -## 0.4.7 (2018-03-11) - -* Fix panic on unknown content encoding - -* Fix connection get closed too early - -* Fix streaming response handling for http/2 - -* Better sleep on error support - - -## 0.4.6 (2018-03-10) - -* Fix client cookie handling - -* Fix json content type detection - -* Fix CORS middleware #117 - -* Optimize websockets stream support - - -## 0.4.5 (2018-03-07) - -* Fix compression #103 and #104 - -* Fix client cookie handling #111 - -* Non-blocking processing of a `NamedFile` - -* Enable compression support for `NamedFile` - -* Better support for `NamedFile` type - -* Add `ResponseError` impl for `SendRequestError`. This improves ergonomics of the client. - -* Add native-tls support for client - -* Allow client connection timeout to be set #108 - -* Allow to use std::net::TcpListener for HttpServer - -* Handle panics in worker threads - - -## 0.4.4 (2018-03-04) - -* Allow to use Arc> as response/request body - -* Fix handling of requests with an encoded body with a length > 8192 #93 - -## 0.4.3 (2018-03-03) - -* Fix request body read bug - -* Fix segmentation fault #79 - -* Set reuse address before bind #90 - - -## 0.4.2 (2018-03-02) - -* Better naming for websockets implementation - -* Add `Pattern::with_prefix()`, make it more usable outside of actix - -* Add csrf middleware for filter for cross-site request forgery #89 - -* Fix disconnect on idle connections - - -## 0.4.1 (2018-03-01) - -* Rename `Route::p()` to `Route::filter()` - -* Better naming for http codes - -* Fix payload parse in situation when socket data is not ready. - -* Fix Session mutable borrow lifetime #87 - - -## 0.4.0 (2018-02-28) - -* Actix 0.5 compatibility - -* Fix request json/urlencoded loaders - -* Simplify HttpServer type definition - -* Added HttpRequest::encoding() method - -* Added HttpRequest::mime_type() method - -* Added HttpRequest::uri_mut(), allows to modify request uri - -* Added StaticFiles::index_file() - -* Added http client - -* Added websocket client - -* Added TestServer::ws(), test websockets client - -* Added TestServer http client support - -* Allow to override content encoding on application level - - -## 0.3.3 (2018-01-25) - -* Stop processing any events after context stop - -* Re-enable write back-pressure for h1 connections - -* Refactor HttpServer::start_ssl() method - -* Upgrade openssl to 0.10 - - -## 0.3.2 (2018-01-21) - -* Fix HEAD requests handling - -* Log request processing errors - -* Always enable content encoding if encoding explicitly selected - -* Allow multiple Applications on a single server with different state #49 - -* CORS middleware: allowed_headers is defaulting to None #50 - - -## 0.3.1 (2018-01-13) - -* Fix directory entry path #47 - -* Do not enable chunked encoding for HTTP/1.0 - -* Allow explicitly disable chunked encoding - - -## 0.3.0 (2018-01-12) - -* HTTP/2 Support - -* Refactor streaming responses - -* Refactor error handling - -* Asynchronous middlewares - -* Refactor logger middleware - -* Content compression/decompression (br, gzip, deflate) - -* Server multi-threading - -* Graceful shutdown support - - -## 0.2.1 (2017-11-03) - -* Allow to start tls server with `HttpServer::serve_tls` - -* Export `Frame` enum - -* Add conversion impl from `HttpResponse` and `BinaryBody` to a `Frame` - - -## 0.2.0 (2017-10-30) - -* Do not use `http::Uri` as it can not parse some valid paths - -* Refactor response `Body` - -* Refactor `RouteRecognizer` usability - -* Refactor `HttpContext::write` - -* Refactor `Payload` stream - -* Re-use `BinaryBody` for `Frame::Payload` - -* Stop http actor on `write_eof` - -* Fix disconnection handling. - - -## 0.1.0 (2017-10-23) - -* First release +* Initial impl diff --git a/Cargo.toml b/Cargo.toml index bd3cb306c..6a61c7801 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "0.7.18" +version = "0.1.0" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" @@ -10,44 +10,21 @@ repository = "https://github.com/actix/actix-web.git" documentation = "https://actix.rs/api/actix-web/stable/actix_web/" categories = ["network-programming", "asynchronous", "web-programming::http-server", - "web-programming::http-client", "web-programming::websocket"] license = "MIT/Apache-2.0" exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] -build = "build.rs" - -[package.metadata.docs.rs] -features = ["tls", "ssl", "rust-tls", "session", "brotli", "flate2-c"] +edition = "2018" [badges] -travis-ci = { repository = "actix/actix-web", branch = "master" } -appveyor = { repository = "fafhrd91/actix-web-hdy9d" } -codecov = { repository = "actix/actix-web", branch = "master", service = "github" } +travis-ci = { repository = "actix/actix-web2", branch = "master" } +codecov = { repository = "actix/actix-web2", branch = "master", service = "github" } [lib] name = "actix_web" path = "src/lib.rs" [features] -default = ["session", "brotli", "flate2-c", "cell"] - -# tls -tls = ["native-tls", "tokio-tls", "actix-net/tls"] - -# openssl -ssl = ["openssl", "tokio-openssl", "actix-net/ssl"] - -# deprecated, use "ssl" -alpn = ["openssl", "tokio-openssl", "actix-net/ssl"] - -# rustls -rust-tls = ["rustls", "tokio-rustls", "webpki", "webpki-roots", "actix-net/rust-tls"] - -# unix sockets -uds = ["tokio-uds"] - -# sessions feature, session require "ring" crate and c compiler -session = ["cookie/secure"] +default = ["brotli", "flate2-c"] # brotli encoding, requires c compiler brotli = ["brotli2"] @@ -58,81 +35,54 @@ flate2-c = ["flate2/miniz-sys"] # rust backend for flate2 crate flate2-rust = ["flate2/rust_backend"] -cell = ["actix-net/cell"] - [dependencies] -actix = "0.7.9" -actix-net = "0.2.6" +actix-codec = "0.1.0" +#actix-service = "0.2.1" +#actix-server = "0.2.1" +#actix-utils = "0.2.1" +actix-service = { git = "https://github.com/actix/actix-net.git" } +actix-server = { git = "https://github.com/actix/actix-net.git" } +actix-utils = { git = "https://github.com/actix/actix-net.git" } -v_htmlescape = "0.4" -base64 = "0.10" -bitflags = "1.0" -failure = "^0.1.2" -h2 = "0.1" -http = "^0.1.14" -httparse = "1.3" +actix-rt = "0.1.0" +actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-router = { git = "https://github.com/actix/actix-net.git" } + +bytes = "0.4" +futures = "0.1" +derive_more = "0.14" log = "0.4" +lazy_static = "1.2" mime = "0.3" mime_guess = "2.0.0-alpha" -num_cpus = "1.0" +num_cpus = "1.10" percent-encoding = "1.0" -rand = "0.6" -regex = "1.0" +cookie = { version="0.11", features=["percent-encode"] } +v_htmlescape = "0.4" serde = "1.0" serde_json = "1.0" -sha1 = "0.6" -smallvec = "0.6" -time = "0.1" encoding = "0.2" -language-tags = "0.2" -lazy_static = "1.0" -lazycell = "1.0.0" -parking_lot = "0.7" serde_urlencoded = "^0.5.3" -url = { version="1.7", features=["query_encoding"] } -cookie = { version="0.11", features=["percent-encode"] } +parking_lot = "0.7" +hashbrown = "0.1" +regex = "1" +time = "0.1" +threadpool = "1.7" + +# compression brotli2 = { version="^0.3.2", optional = true } flate2 = { version="^1.0.2", optional = true, default-features = false } -# io -mio = "^0.6.13" -net2 = "0.2" -bytes = "0.4" -byteorder = "1.2" -futures = "0.1" -futures-cpupool = "0.1" -slab = "0.4" -tokio = "0.1" -tokio-io = "0.1" -tokio-tcp = "0.1" -tokio-timer = "0.2.8" -tokio-reactor = "0.1" -tokio-current-thread = "0.1" - -# native-tls -native-tls = { version="0.2", optional = true } -tokio-tls = { version="0.2", optional = true } - -# openssl -openssl = { version="0.10", optional = true } -tokio-openssl = { version="0.2", optional = true } - -#rustls -rustls = { version = "0.14", optional = true } -tokio-rustls = { version = "0.8", optional = true } -webpki = { version = "0.18", optional = true } -webpki-roots = { version = "0.15", optional = true } - -# unix sockets -tokio-uds = { version="0.2", optional = true } - [dev-dependencies] +actix-rt = "0.1.0" +#actix-server = { version="0.2", features=["ssl"] } +actix-server = { git = "https://github.com/actix/actix-net.git", features=["ssl"] } +actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +rand = "0.6" env_logger = "0.6" serde_derive = "1.0" -[build-dependencies] -version_check = "0.1" - [profile.release] lto = true opt-level = 3 diff --git a/build.rs b/build.rs deleted file mode 100644 index c8457944c..000000000 --- a/build.rs +++ /dev/null @@ -1,16 +0,0 @@ -extern crate version_check; - -fn main() { - match version_check::is_min_version("1.26.0") { - Some((true, _)) => println!("cargo:rustc-cfg=actix_impl_trait"), - _ => (), - }; - match version_check::is_nightly() { - Some(true) => { - println!("cargo:rustc-cfg=actix_nightly"); - println!("cargo:rustc-cfg=actix_impl_trait"); - } - Some(false) => (), - None => (), - }; -} diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 000000000..9f4701ebc --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,54 @@ +use futures::IntoFuture; + +use actix_http::{h1, http::Method, Response}; +use actix_server::Server; +use actix_web2::{middleware, App, Error, HttpRequest, Resource}; + +fn index(req: HttpRequest) -> &'static str { + println!("REQ: {:?}", req); + "Hello world!\r\n" +} + +fn index_async(req: HttpRequest) -> impl IntoFuture { + println!("REQ: {:?}", req); + Ok("Hello world!\r\n") +} + +fn no_params() -> &'static str { + "Hello world!\r\n" +} + +fn main() { + ::std::env::set_var("RUST_LOG", "actix_server=info,actix_web2=info"); + env_logger::init(); + let sys = actix_rt::System::new("hello-world"); + + Server::build() + .bind("test", "127.0.0.1:8080", || { + h1::H1Service::new( + App::new() + .middleware( + middleware::DefaultHeaders::new().header("X-Version", "0.2"), + ) + .middleware(middleware::Compress::default()) + .resource("/resource1/index.html", |r| r.get(index)) + .service( + "/resource2/index.html", + Resource::new() + .middleware( + middleware::DefaultHeaders::new() + .header("X-Version-R2", "0.3"), + ) + .default_resource(|r| r.to(|| Response::MethodNotAllowed())) + .method(Method::GET, |r| r.to_async(index_async)), + ) + .service("/test1.html", Resource::new().to(|| "Test\r\n")) + .service("/", Resource::new().to(no_params)), + ) + }) + .unwrap() + .workers(1) + .start(); + + let _ = sys.run(); +} diff --git a/rustfmt.toml b/rustfmt.toml index 4fff285e7..94bd11d51 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,5 +1,2 @@ max_width = 89 reorder_imports = true -#wrap_comments = true -fn_args_density = "Compressed" -#use_small_heuristics = false diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 000000000..6ed9b1440 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,648 @@ +use std::cell::RefCell; +use std::marker::PhantomData; +use std::rc::Rc; + +use actix_http::body::{Body, MessageBody}; +use actix_http::{Extensions, PayloadStream, Request, Response}; +use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; +use actix_service::{ + AndThenNewService, ApplyNewService, IntoNewService, IntoNewTransform, NewService, + NewTransform, Service, +}; +use futures::future::{ok, Either, FutureResult}; +use futures::{Async, Future, IntoFuture, Poll}; + +use crate::helpers::{ + BoxedHttpNewService, BoxedHttpService, DefaultNewService, HttpDefaultNewService, +}; +use crate::resource::Resource; +use crate::service::{ServiceRequest, ServiceResponse}; +use crate::state::{State, StateFactory, StateFactoryResult}; + +type BoxedResponse = Box>; + +pub trait HttpServiceFactory { + type Factory: NewService; + + fn rdef(&self) -> &ResourceDef; + + fn create(self) -> Self::Factory; +} + +/// Application builder +pub struct App { + services: Vec<( + ResourceDef, + BoxedHttpNewService, ServiceResponse>, + )>, + default: Option, ServiceResponse>>>, + defaults: Vec< + Rc< + RefCell< + Option, ServiceResponse>>>, + >, + >, + >, + endpoint: T, + factory_ref: Rc>>>, + extensions: Extensions, + state: Vec>, + _t: PhantomData<(P, B)>, +} + +impl App> { + /// Create application with empty state. Application can + /// be configured with a builder-like pattern. + pub fn new() -> Self { + App::create() + } +} + +impl Default for App> { + fn default() -> Self { + App::new() + } +} + +impl App> { + /// Create application with specified state. Application can be + /// configured with a builder-like pattern. + /// + /// State is shared with all resources within same application and + /// could be accessed with `HttpRequest::state()` method. + /// + /// **Note**: http server accepts an application factory rather than + /// an application instance. Http server constructs an application + /// instance for each thread, thus application state must be constructed + /// multiple times. If you want to share state between different + /// threads, a shared object should be used, e.g. `Arc`. Application + /// state does not need to be `Send` or `Sync`. + pub fn state(mut self, state: S) -> Self { + self.state.push(Box::new(State::new(state))); + self + } + + /// Set application state. This function is + /// similar to `.state()` but it accepts state factory. State get + /// constructed asynchronously during application initialization. + pub fn state_factory(mut self, state: F) -> Self + where + F: Fn() -> Out + 'static, + Out: IntoFuture + 'static, + Out::Error: std::fmt::Debug, + { + self.state.push(Box::new(State::new(state))); + self + } + + fn create() -> Self { + let fref = Rc::new(RefCell::new(None)); + App { + services: Vec::new(), + default: None, + defaults: Vec::new(), + endpoint: AppEntry::new(fref.clone()), + factory_ref: fref, + extensions: Extensions::new(), + state: Vec::new(), + _t: PhantomData, + } + } +} + +// /// Application router builder +// pub struct AppRouter { +// services: Vec<( +// ResourceDef, +// BoxedHttpNewService, Response>, +// )>, +// default: Option, Response>>>, +// defaults: +// Vec, Response>>>>>>, +// state: AppState, +// endpoint: T, +// factory_ref: Rc>>>, +// extensions: Extensions, +// _t: PhantomData

    , +// } + +impl App +where + P: 'static, + B: MessageBody, + T: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + /// Configure resource for a specific path. + /// + /// Resources may have variable path segments. For example, a + /// resource with the path `/a/{name}/c` would match all incoming + /// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. + /// + /// A variable segment is specified in the form `{identifier}`, + /// where the identifier can be used later in a request handler to + /// access the matched value for that segment. This is done by + /// looking up the identifier in the `Params` object returned by + /// `HttpRequest.match_info()` method. + /// + /// By default, each segment matches the regular expression `[^{}/]+`. + /// + /// You can also specify a custom regex in the form `{identifier:regex}`: + /// + /// For instance, to route `GET`-requests on any route matching + /// `/users/{userid}/{friend}` and store `userid` and `friend` in + /// the exposed `Params` object: + /// + /// ```rust,ignore + /// # extern crate actix_web; + /// use actix_web::{http, App, HttpResponse}; + /// + /// fn main() { + /// let app = App::new().resource("/users/{userid}/{friend}", |r| { + /// r.get(|r| r.to(|_| HttpResponse::Ok())); + /// r.head(|r| r.to(|_| HttpResponse::MethodNotAllowed())) + /// }); + /// } + /// ``` + pub fn resource(mut self, path: &str, f: F) -> Self + where + F: FnOnce(Resource

    ) -> Resource, + U: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, + { + let rdef = ResourceDef::new(path); + let resource = f(Resource::new()); + self.defaults.push(resource.get_default()); + self.services.push(( + rdef, + Box::new(HttpNewService::new(resource.into_new_service())), + )); + self + } + + /// Default resource to be used if no matching route could be found. + /// + /// Default resource works with resources only and does not work with + /// custom services. + pub fn default_resource(mut self, f: F) -> Self + where + F: FnOnce(Resource

    ) -> R, + R: IntoNewService, + U: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + > + 'static, + { + // create and configure default resource + self.default = Some(Rc::new(Box::new(DefaultNewService::new( + f(Resource::new()).into_new_service(), + )))); + + self + } + + /// Register resource handler service. + pub fn service(mut self, rdef: R, factory: F) -> Self + where + R: Into, + F: IntoNewService, + U: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + > + 'static, + { + self.services.push(( + rdef.into(), + Box::new(HttpNewService::new(factory.into_new_service())), + )); + self + } + + /// Register a middleware. + pub fn middleware( + self, + mw: F, + ) -> App< + P, + B1, + impl NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, + > + where + M: NewTransform< + T::Service, + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, + B1: MessageBody, + F: IntoNewTransform, + { + let endpoint = ApplyNewService::new(mw, self.endpoint); + App { + endpoint, + state: self.state, + services: self.services, + default: self.default, + defaults: Vec::new(), + factory_ref: self.factory_ref, + extensions: Extensions::new(), + _t: PhantomData, + } + } + + /// Register an external resource. + /// + /// External resources are useful for URL generation purposes only + /// and are never considered for matching at request time. Calls to + /// `HttpRequest::url_for()` will work as expected. + /// + /// ```rust,ignore + /// # extern crate actix_web; + /// use actix_web::{App, HttpRequest, HttpResponse, Result}; + /// + /// fn index(req: &HttpRequest) -> Result { + /// let url = req.url_for("youtube", &["oHg5SJYRHA0"])?; + /// assert_eq!(url.as_str(), "https://youtube.com/watch/oHg5SJYRHA0"); + /// Ok(HttpResponse::Ok().into()) + /// } + /// + /// fn main() { + /// let app = App::new() + /// .resource("/index.html", |r| r.get().f(index)) + /// .external_resource("youtube", "https://youtube.com/watch/{video_id}") + /// .finish(); + /// } + /// ``` + pub fn external_resource(self, _name: N, _url: U) -> Self + where + N: AsRef, + U: AsRef, + { + // self.parts + // .as_mut() + // .expect("Use after finish") + // .router + // .register_external(name.as_ref(), ResourceDef::external(url.as_ref())); + self + } +} + +impl + IntoNewService, T, ()>> for App +where + T: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + fn into_new_service(self) -> AndThenNewService, T, ()> { + // update resource default service + if self.default.is_some() { + for default in &self.defaults { + if default.borrow_mut().is_none() { + *default.borrow_mut() = self.default.clone(); + } + } + } + + // set factory + *self.factory_ref.borrow_mut() = Some(AppFactory { + services: Rc::new(self.services), + }); + + AppStateFactory { + state: self.state, + extensions: Rc::new(RefCell::new(Rc::new(self.extensions))), + _t: PhantomData, + } + .and_then(self.endpoint) + } +} + +/// Service factory to convert `Request` to a `ServiceRequest` +pub struct AppStateFactory

    { + state: Vec>, + extensions: Rc>>, + _t: PhantomData

    , +} + +impl NewService for AppStateFactory

    { + type Request = Request

    ; + type Response = ServiceRequest

    ; + type Error = (); + type InitError = (); + type Service = AppStateService

    ; + type Future = AppStateFactoryResult

    ; + + fn new_service(&self, _: &()) -> Self::Future { + AppStateFactoryResult { + state: self.state.iter().map(|s| s.construct()).collect(), + extensions: self.extensions.clone(), + _t: PhantomData, + } + } +} + +#[doc(hidden)] +pub struct AppStateFactoryResult

    { + state: Vec>, + extensions: Rc>>, + _t: PhantomData

    , +} + +impl

    Future for AppStateFactoryResult

    { + type Item = AppStateService

    ; + type Error = (); + + fn poll(&mut self) -> Poll { + if let Some(extensions) = Rc::get_mut(&mut *self.extensions.borrow_mut()) { + let mut idx = 0; + while idx < self.state.len() { + if let Async::Ready(_) = self.state[idx].poll_result(extensions)? { + self.state.remove(idx); + } else { + idx += 1; + } + } + if !self.state.is_empty() { + return Ok(Async::NotReady); + } + } else { + log::warn!("Multiple copies of app extensions exists"); + } + + Ok(Async::Ready(AppStateService { + extensions: self.extensions.borrow().clone(), + _t: PhantomData, + })) + } +} + +/// Service to convert `Request` to a `ServiceRequest` +pub struct AppStateService

    { + extensions: Rc, + _t: PhantomData

    , +} + +impl

    Service for AppStateService

    { + type Request = Request

    ; + type Response = ServiceRequest

    ; + type Error = (); + type Future = FutureResult; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: Request

    ) -> Self::Future { + ok(ServiceRequest::new( + Path::new(Url::new(req.uri().clone())), + req, + self.extensions.clone(), + )) + } +} + +pub struct AppFactory

    { + services: Rc< + Vec<( + ResourceDef, + BoxedHttpNewService, ServiceResponse>, + )>, + >, +} + +impl

    NewService for AppFactory

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = AppService

    ; + type Future = CreateAppService

    ; + + fn new_service(&self, _: &()) -> Self::Future { + CreateAppService { + fut: self + .services + .iter() + .map(|(path, service)| { + CreateAppServiceItem::Future( + Some(path.clone()), + service.new_service(&()), + ) + }) + .collect(), + } + } +} + +type HttpServiceFut

    = + Box, ServiceResponse>, Error = ()>>; + +/// Create app service +#[doc(hidden)] +pub struct CreateAppService

    { + fut: Vec>, +} + +enum CreateAppServiceItem

    { + Future(Option, HttpServiceFut

    ), + Service( + ResourceDef, + BoxedHttpService, ServiceResponse>, + ), +} + +impl

    Future for CreateAppService

    { + type Item = AppService

    ; + type Error = (); + + fn poll(&mut self) -> Poll { + let mut done = true; + + // poll http services + for item in &mut self.fut { + let res = match item { + CreateAppServiceItem::Future(ref mut path, ref mut fut) => { + match fut.poll()? { + Async::Ready(service) => Some((path.take().unwrap(), service)), + Async::NotReady => { + done = false; + None + } + } + } + CreateAppServiceItem::Service(_, _) => continue, + }; + + if let Some((path, service)) = res { + *item = CreateAppServiceItem::Service(path, service); + } + } + + if done { + let router = self + .fut + .drain(..) + .fold(Router::build(), |mut router, item| { + match item { + CreateAppServiceItem::Service(path, service) => { + router.rdef(path, service) + } + CreateAppServiceItem::Future(_, _) => unreachable!(), + } + router + }); + Ok(Async::Ready(AppService { + router: router.finish(), + ready: None, + })) + } else { + Ok(Async::NotReady) + } + } +} + +pub struct AppService

    { + router: Router, ServiceResponse>>, + ready: Option<(ServiceRequest

    , ResourceInfo)>, +} + +impl

    Service for AppService

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type Future = Either>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + if self.ready.is_none() { + Ok(Async::Ready(())) + } else { + Ok(Async::NotReady) + } + } + + fn call(&mut self, mut req: ServiceRequest

    ) -> Self::Future { + if let Some((srv, _info)) = self.router.recognize_mut(req.match_info_mut()) { + Either::A(srv.call(req)) + } else { + let req = req.into_request(); + Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) + } + } +} + +pub struct AppServiceResponse(Box>); + +impl Future for AppServiceResponse { + type Item = ServiceResponse; + type Error = (); + + fn poll(&mut self) -> Poll { + self.0.poll().map_err(|_| panic!()) + } +} + +struct HttpNewService>>(T); + +impl HttpNewService +where + T: NewService, Response = ServiceResponse, Error = ()>, + T::Future: 'static, + ::Future: 'static, +{ + pub fn new(service: T) -> Self { + HttpNewService(service) + } +} + +impl NewService for HttpNewService +where + T: NewService, Response = ServiceResponse, Error = ()>, + T::Future: 'static, + T::Service: 'static, + ::Future: 'static, +{ + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = BoxedHttpService, Self::Response>; + type Future = Box>; + + fn new_service(&self, _: &()) -> Self::Future { + Box::new(self.0.new_service(&()).map_err(|_| ()).and_then(|service| { + let service: BoxedHttpService<_, _> = Box::new(HttpServiceWrapper { + service, + _t: PhantomData, + }); + Ok(service) + })) + } +} + +struct HttpServiceWrapper { + service: T, + _t: PhantomData<(P,)>, +} + +impl Service for HttpServiceWrapper +where + T::Future: 'static, + T: Service, Response = ServiceResponse, Error = ()>, +{ + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type Future = BoxedResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready().map_err(|_| ()) + } + + fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { + Box::new(self.service.call(req)) + } +} + +#[doc(hidden)] +pub struct AppEntry

    { + factory: Rc>>>, +} + +impl

    AppEntry

    { + fn new(factory: Rc>>>) -> Self { + AppEntry { factory } + } +} + +impl

    NewService for AppEntry

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = AppService

    ; + type Future = CreateAppService

    ; + + fn new_service(&self, _: &()) -> Self::Future { + self.factory.borrow_mut().as_mut().unwrap().new_service(&()) + } +} diff --git a/src/application.rs b/src/application.rs index d8a6cbe7b..6ca4ce285 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,17 +1,21 @@ use std::rc::Rc; +use actix_http::http::ContentEncoding; +use actix_http::{Error, Request, Response}; +use actix_service::Service; +use futures::{Async, Future, Poll}; + use handler::{AsyncResult, FromRequest, Handler, Responder, WrapHandler}; -use header::ContentEncoding; use http::Method; use httprequest::HttpRequest; use httpresponse::HttpResponse; -use middleware::Middleware; -use pipeline::{Pipeline, PipelineHandler}; +// use middleware::Middleware; +// use pipeline::{Pipeline, PipelineHandler}; use pred::Predicate; use resource::Resource; use router::{ResourceDef, Router}; -use scope::Scope; -use server::{HttpHandler, HttpHandlerTask, IntoHttpHandler, Request}; +// use scope::Scope; +// use server::{HttpHandler, HttpHandlerTask, IntoHttpHandler, Request}; use with::WithFactory; /// Application @@ -21,7 +25,7 @@ pub struct HttpApplication { prefix_len: usize, inner: Rc>, filters: Option>>>, - middlewares: Rc>>>, + // middlewares: Rc>>>, } #[doc(hidden)] @@ -30,16 +34,16 @@ pub struct Inner { encoding: ContentEncoding, } -impl PipelineHandler for Inner { - #[inline] - fn encoding(&self) -> ContentEncoding { - self.encoding - } +// impl PipelineHandler for Inner { +// #[inline] +// fn encoding(&self) -> ContentEncoding { +// self.encoding +// } - fn handle(&self, req: &HttpRequest) -> AsyncResult { - self.router.handle(req) - } -} +// fn handle(&self, req: &HttpRequest) -> AsyncResult { +// self.router.handle(req) +// } +// } impl HttpApplication { #[cfg(test)] @@ -54,10 +58,18 @@ impl HttpApplication { } } -impl HttpHandler for HttpApplication { - type Task = Pipeline>; +impl Service for HttpApplication { + // type Task = Pipeline>; + type Request = actix_http::Request; + type Response = actix_http::Response; + type Error = Error; + type Future = Box>; - fn handle(&self, msg: Request) -> Result>, Request> { + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, msg: actix_http::Request) -> Self::Future { let m = { if self.prefix_len == 0 { true @@ -70,11 +82,12 @@ impl HttpHandler for HttpApplication { }; if m { if let Some(ref filters) = self.filters { - for filter in filters { - if !filter.check(&msg, &self.state) { - return Err(msg); - } - } + //for filter in filters { + // if !filter.check(&msg, &self.state) { + //return Err(msg); + unimplemented!() + // } + //} } let info = self @@ -83,10 +96,12 @@ impl HttpHandler for HttpApplication { .recognize(&msg, &self.state, self.prefix_len); let inner = Rc::clone(&self.inner); - let req = HttpRequest::new(msg, Rc::clone(&self.state), info); - Ok(Pipeline::new(req, Rc::clone(&self.middlewares), inner)) + // let req = HttpRequest::new(msg, Rc::clone(&self.state), info); + // Ok(Pipeline::new(req, inner)) + unimplemented!() } else { - Err(msg) + // Err(msg) + unimplemented!() } } } @@ -96,7 +111,7 @@ struct ApplicationParts { prefix: String, router: Router, encoding: ContentEncoding, - middlewares: Vec>>, + // middlewares: Vec>>, filters: Vec>>, } @@ -106,55 +121,10 @@ pub struct App { parts: Option>, } -impl App<()> { - /// Create application with empty state. Application can - /// be configured with a builder-like pattern. - pub fn new() -> App<()> { - App::with_state(()) - } -} - -impl Default for App<()> { - fn default() -> Self { - App::new() - } -} - impl App where S: 'static, { - /// Create application with specified state. Application can be - /// configured with a builder-like pattern. - /// - /// State is shared with all resources within same application and - /// could be accessed with `HttpRequest::state()` method. - /// - /// **Note**: http server accepts an application factory rather than - /// an application instance. Http server constructs an application - /// instance for each thread, thus application state must be constructed - /// multiple times. If you want to share state between different - /// threads, a shared object should be used, e.g. `Arc`. Application - /// state does not need to be `Send` or `Sync`. - pub fn with_state(state: S) -> App { - App { - parts: Some(ApplicationParts { - state, - prefix: "".to_owned(), - router: Router::new(ResourceDef::prefix("")), - middlewares: Vec::new(), - filters: Vec::new(), - encoding: ContentEncoding::Auto, - }), - } - } - - /// Get reference to the application state - pub fn state(&self) -> &S { - let parts = self.parts.as_ref().expect("Use after finish"); - &parts.state - } - /// Set application prefix. /// /// Only requests that match the application's prefix get @@ -205,26 +175,6 @@ where self } - /// Add match predicate to application. - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// # fn main() { - /// App::new() - /// .filter(pred::Host("www.rust-lang.org")) - /// .resource("/path", |r| r.f(|_| HttpResponse::Ok())) - /// # .finish(); - /// # } - /// ``` - pub fn filter + 'static>(mut self, p: T) -> App { - { - let parts = self.parts.as_mut().expect("Use after finish"); - parts.filters.push(Box::new(p)); - } - self - } - /// Configure route for a specific path. /// /// This is a simplified version of the `App::resource()` method. @@ -263,42 +213,42 @@ where self } - /// Configure scope for common root path. - /// - /// Scopes collect multiple paths under a common path prefix. - /// Scope path can contain variable path segments as resources. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().scope("/{project_id}", |scope| { - /// scope - /// .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - /// .resource("/path2", |r| r.f(|_| HttpResponse::Ok())) - /// .resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed())) - /// }); - /// } - /// ``` - /// - /// In the above example, three routes get added: - /// * /{project_id}/path1 - /// * /{project_id}/path2 - /// * /{project_id}/path3 - /// - pub fn scope(mut self, path: &str, f: F) -> App - where - F: FnOnce(Scope) -> Scope, - { - let scope = f(Scope::new(path)); - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_scope(scope); - self - } + // /// Configure scope for common root path. + // /// + // /// Scopes collect multiple paths under a common path prefix. + // /// Scope path can contain variable path segments as resources. + // /// + // /// ```rust + // /// # extern crate actix_web; + // /// use actix_web::{http, App, HttpRequest, HttpResponse}; + // /// + // /// fn main() { + // /// let app = App::new().scope("/{project_id}", |scope| { + // /// scope + // /// .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) + // /// .resource("/path2", |r| r.f(|_| HttpResponse::Ok())) + // /// .resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed())) + // /// }); + // /// } + // /// ``` + // /// + // /// In the above example, three routes get added: + // /// * /{project_id}/path1 + // /// * /{project_id}/path2 + // /// * /{project_id}/path3 + // /// + // pub fn scope(mut self, path: &str, f: F) -> App + // where + // F: FnOnce(Scope) -> Scope, + // { + // let scope = f(Scope::new(path)); + // self.parts + // .as_mut() + // .expect("Use after finish") + // .router + // .register_scope(scope); + // self + // } /// Configure resource for a specific path. /// @@ -377,51 +327,6 @@ where self } - /// Set default content encoding. `ContentEncoding::Auto` is set by default. - pub fn default_encoding(mut self, encoding: ContentEncoding) -> App { - { - let parts = self.parts.as_mut().expect("Use after finish"); - parts.encoding = encoding; - } - self - } - - /// Register an external resource. - /// - /// External resources are useful for URL generation purposes only - /// and are never considered for matching at request time. Calls to - /// `HttpRequest::url_for()` will work as expected. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest, HttpResponse, Result}; - /// - /// fn index(req: &HttpRequest) -> Result { - /// let url = req.url_for("youtube", &["oHg5SJYRHA0"])?; - /// assert_eq!(url.as_str(), "https://youtube.com/watch/oHg5SJYRHA0"); - /// Ok(HttpResponse::Ok().into()) - /// } - /// - /// fn main() { - /// let app = App::new() - /// .resource("/index.html", |r| r.get().f(index)) - /// .external_resource("youtube", "https://youtube.com/watch/{video_id}") - /// .finish(); - /// } - /// ``` - pub fn external_resource(mut self, name: T, url: U) -> App - where - T: AsRef, - U: AsRef, - { - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_external(name.as_ref(), ResourceDef::external(url.as_ref())); - self - } - /// Configure handler for specific path prefix. /// /// A path prefix consists of valid path segments, i.e for the @@ -458,15 +363,15 @@ where self } - /// Register a middleware. - pub fn middleware>(mut self, mw: M) -> App { - self.parts - .as_mut() - .expect("Use after finish") - .middlewares - .push(Box::new(mw)); - self - } + // /// Register a middleware. + // pub fn middleware>(mut self, mw: M) -> App { + // self.parts + // .as_mut() + // .expect("Use after finish") + // .middlewares + // .push(Box::new(mw)); + // self + // } /// Run external configuration as part of the application building /// process @@ -521,93 +426,93 @@ where inner, filters, state: Rc::new(parts.state), - middlewares: Rc::new(parts.middlewares), + // middlewares: Rc::new(parts.middlewares), prefix: prefix.to_owned(), prefix_len: prefix.len(), } } - /// Convenience method for creating `Box` instances. - /// - /// This method is useful if you need to register multiple - /// application instances with different state. - /// - /// ```rust - /// # use std::thread; - /// # extern crate actix_web; - /// use actix_web::{server, App, HttpResponse}; - /// - /// struct State1; - /// - /// struct State2; - /// - /// fn main() { - /// # thread::spawn(|| { - /// server::new(|| { - /// vec![ - /// App::with_state(State1) - /// .prefix("/app1") - /// .resource("/", |r| r.f(|r| HttpResponse::Ok())) - /// .boxed(), - /// App::with_state(State2) - /// .prefix("/app2") - /// .resource("/", |r| r.f(|r| HttpResponse::Ok())) - /// .boxed(), - /// ] - /// }).bind("127.0.0.1:8080") - /// .unwrap() - /// .run() - /// # }); - /// } - /// ``` - pub fn boxed(mut self) -> Box>> { - Box::new(BoxedApplication { app: self.finish() }) - } + // /// Convenience method for creating `Box` instances. + // /// + // /// This method is useful if you need to register multiple + // /// application instances with different state. + // /// + // /// ```rust + // /// # use std::thread; + // /// # extern crate actix_web; + // /// use actix_web::{server, App, HttpResponse}; + // /// + // /// struct State1; + // /// + // /// struct State2; + // /// + // /// fn main() { + // /// # thread::spawn(|| { + // /// server::new(|| { + // /// vec![ + // /// App::with_state(State1) + // /// .prefix("/app1") + // /// .resource("/", |r| r.f(|r| HttpResponse::Ok())) + // /// .boxed(), + // /// App::with_state(State2) + // /// .prefix("/app2") + // /// .resource("/", |r| r.f(|r| HttpResponse::Ok())) + // /// .boxed(), + // /// ] + // /// }).bind("127.0.0.1:8080") + // /// .unwrap() + // /// .run() + // /// # }); + // /// } + // /// ``` + // pub fn boxed(mut self) -> Box>> { + // Box::new(BoxedApplication { app: self.finish() }) + // } } -struct BoxedApplication { - app: HttpApplication, -} +// struct BoxedApplication { +// app: HttpApplication, +// } -impl HttpHandler for BoxedApplication { - type Task = Box; +// impl HttpHandler for BoxedApplication { +// type Task = Box; - fn handle(&self, req: Request) -> Result { - self.app.handle(req).map(|t| { - let task: Self::Task = Box::new(t); - task - }) - } -} +// fn handle(&self, req: Request) -> Result { +// self.app.handle(req).map(|t| { +// let task: Self::Task = Box::new(t); +// task +// }) +// } +// } -impl IntoHttpHandler for App { - type Handler = HttpApplication; +// impl IntoHttpHandler for App { +// type Handler = HttpApplication; - fn into_handler(mut self) -> HttpApplication { - self.finish() - } -} +// fn into_handler(mut self) -> HttpApplication { +// self.finish() +// } +// } -impl<'a, S: 'static> IntoHttpHandler for &'a mut App { - type Handler = HttpApplication; +// impl<'a, S: 'static> IntoHttpHandler for &'a mut App { +// type Handler = HttpApplication; - fn into_handler(self) -> HttpApplication { - self.finish() - } -} +// fn into_handler(self) -> HttpApplication { +// self.finish() +// } +// } -#[doc(hidden)] -impl Iterator for App { - type Item = HttpApplication; +// #[doc(hidden)] +// impl Iterator for App { +// type Item = HttpApplication; - fn next(&mut self) -> Option { - if self.parts.is_some() { - Some(self.finish()) - } else { - None - } - } -} +// fn next(&mut self) -> Option { +// if self.parts.is_some() { +// Some(self.finish()) +// } else { +// None +// } +// } +// } #[cfg(test)] mod tests { @@ -773,7 +678,8 @@ mod tests { .route("/test", Method::GET, |_: HttpRequest| HttpResponse::Ok()) .route("/test", Method::POST, |_: HttpRequest| { HttpResponse::Created() - }).finish(); + }) + .finish(); let req = TestRequest::with_uri("/test").method(Method::GET).request(); let resp = app.run(req); diff --git a/src/blocking.rs b/src/blocking.rs new file mode 100644 index 000000000..fc2624f6d --- /dev/null +++ b/src/blocking.rs @@ -0,0 +1,74 @@ +//! Thread pool for blocking operations + +use futures::sync::oneshot; +use futures::{Async, Future, Poll}; +use parking_lot::Mutex; +use threadpool::ThreadPool; + +/// Env variable for default cpu pool size +const ENV_CPU_POOL_VAR: &str = "ACTIX_CPU_POOL"; + +lazy_static::lazy_static! { + pub(crate) static ref DEFAULT_POOL: Mutex = { + let default = match std::env::var(ENV_CPU_POOL_VAR) { + Ok(val) => { + if let Ok(val) = val.parse() { + val + } else { + log::error!("Can not parse ACTIX_CPU_POOL value"); + num_cpus::get() * 5 + } + } + Err(_) => num_cpus::get() * 5, + }; + Mutex::new( + threadpool::Builder::new() + .thread_name("actix-web".to_owned()) + .num_threads(8) + .build(), + ) + }; +} + +thread_local! { + static POOL: ThreadPool = { + DEFAULT_POOL.lock().clone() + }; +} + +pub enum BlockingError { + Error(E), + Canceled, +} + +/// Execute blocking function on a thread pool, returns future that resolves +/// to result of the function execution. +pub fn run(f: F) -> CpuFuture +where + F: FnOnce() -> Result, +{ + let (tx, rx) = oneshot::channel(); + POOL.with(move |pool| { + let _ = tx.send(f()); + }); + + CpuFuture { rx } +} + +pub struct CpuFuture { + rx: oneshot::Receiver>, +} + +impl Future for CpuFuture { + type Item = I; + type Error = BlockingError; + + fn poll(&mut self) -> Poll { + let res = + futures::try_ready!(self.rx.poll().map_err(|_| BlockingError::Canceled)); + match res { + Ok(val) => Ok(Async::Ready(val)), + Err(err) => Err(BlockingError::Error(err)), + } + } +} diff --git a/src/body.rs b/src/body.rs deleted file mode 100644 index 5487dbba4..000000000 --- a/src/body.rs +++ /dev/null @@ -1,391 +0,0 @@ -use bytes::{Bytes, BytesMut}; -use futures::Stream; -use std::borrow::Cow; -use std::sync::Arc; -use std::{fmt, mem}; - -use context::ActorHttpContext; -use error::Error; -use handler::Responder; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; - -/// Type represent streaming body -pub type BodyStream = Box>; - -/// Represents various types of http message body. -pub enum Body { - /// Empty response. `Content-Length` header is set to `0` - Empty, - /// Specific response body. - Binary(Binary), - /// Unspecified streaming response. Developer is responsible for setting - /// right `Content-Length` or `Transfer-Encoding` headers. - Streaming(BodyStream), - /// Special body type for actor response. - Actor(Box), -} - -/// Represents various types of binary body. -/// `Content-Length` header is set to length of the body. -#[derive(Debug, PartialEq)] -pub enum Binary { - /// Bytes body - Bytes(Bytes), - /// Static slice - Slice(&'static [u8]), - /// Shared string body - #[doc(hidden)] - SharedString(Arc), - /// Shared vec body - SharedVec(Arc>), -} - -impl Body { - /// Does this body streaming. - #[inline] - pub fn is_streaming(&self) -> bool { - match *self { - Body::Streaming(_) | Body::Actor(_) => true, - _ => false, - } - } - - /// Is this binary body. - #[inline] - pub fn is_binary(&self) -> bool { - match *self { - Body::Binary(_) => true, - _ => false, - } - } - - /// Is this binary empy. - #[inline] - pub fn is_empty(&self) -> bool { - match *self { - Body::Empty => true, - _ => false, - } - } - - /// Create body from slice (copy) - pub fn from_slice(s: &[u8]) -> Body { - Body::Binary(Binary::Bytes(Bytes::from(s))) - } - - /// Is this binary body. - #[inline] - pub(crate) fn binary(self) -> Binary { - match self { - Body::Binary(b) => b, - _ => panic!(), - } - } -} - -impl PartialEq for Body { - fn eq(&self, other: &Body) -> bool { - match *self { - Body::Empty => match *other { - Body::Empty => true, - _ => false, - }, - Body::Binary(ref b) => match *other { - Body::Binary(ref b2) => b == b2, - _ => false, - }, - Body::Streaming(_) | Body::Actor(_) => false, - } - } -} - -impl fmt::Debug for Body { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Body::Empty => write!(f, "Body::Empty"), - Body::Binary(ref b) => write!(f, "Body::Binary({:?})", b), - Body::Streaming(_) => write!(f, "Body::Streaming(_)"), - Body::Actor(_) => write!(f, "Body::Actor(_)"), - } - } -} - -impl From for Body -where - T: Into, -{ - fn from(b: T) -> Body { - Body::Binary(b.into()) - } -} - -impl From> for Body { - fn from(ctx: Box) -> Body { - Body::Actor(ctx) - } -} - -impl Binary { - #[inline] - /// Returns `true` if body is empty - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - #[inline] - /// Length of body in bytes - pub fn len(&self) -> usize { - match *self { - Binary::Bytes(ref bytes) => bytes.len(), - Binary::Slice(slice) => slice.len(), - Binary::SharedString(ref s) => s.len(), - Binary::SharedVec(ref s) => s.len(), - } - } - - /// Create binary body from slice - pub fn from_slice(s: &[u8]) -> Binary { - Binary::Bytes(Bytes::from(s)) - } - - /// Convert Binary to a Bytes instance - pub fn take(&mut self) -> Bytes { - mem::replace(self, Binary::Slice(b"")).into() - } -} - -impl Clone for Binary { - fn clone(&self) -> Binary { - match *self { - Binary::Bytes(ref bytes) => Binary::Bytes(bytes.clone()), - Binary::Slice(slice) => Binary::Bytes(Bytes::from(slice)), - Binary::SharedString(ref s) => Binary::SharedString(s.clone()), - Binary::SharedVec(ref s) => Binary::SharedVec(s.clone()), - } - } -} - -impl Into for Binary { - fn into(self) -> Bytes { - match self { - Binary::Bytes(bytes) => bytes, - Binary::Slice(slice) => Bytes::from(slice), - Binary::SharedString(s) => Bytes::from(s.as_str()), - Binary::SharedVec(s) => Bytes::from(AsRef::<[u8]>::as_ref(s.as_ref())), - } - } -} - -impl From<&'static str> for Binary { - fn from(s: &'static str) -> Binary { - Binary::Slice(s.as_ref()) - } -} - -impl From<&'static [u8]> for Binary { - fn from(s: &'static [u8]) -> Binary { - Binary::Slice(s) - } -} - -impl From> for Binary { - fn from(vec: Vec) -> Binary { - Binary::Bytes(Bytes::from(vec)) - } -} - -impl From> for Binary { - fn from(b: Cow<'static, [u8]>) -> Binary { - match b { - Cow::Borrowed(s) => Binary::Slice(s), - Cow::Owned(vec) => Binary::Bytes(Bytes::from(vec)), - } - } -} - -impl From for Binary { - fn from(s: String) -> Binary { - Binary::Bytes(Bytes::from(s)) - } -} - -impl From> for Binary { - fn from(s: Cow<'static, str>) -> Binary { - match s { - Cow::Borrowed(s) => Binary::Slice(s.as_ref()), - Cow::Owned(s) => Binary::Bytes(Bytes::from(s)), - } - } -} - -impl<'a> From<&'a String> for Binary { - fn from(s: &'a String) -> Binary { - Binary::Bytes(Bytes::from(AsRef::<[u8]>::as_ref(&s))) - } -} - -impl From for Binary { - fn from(s: Bytes) -> Binary { - Binary::Bytes(s) - } -} - -impl From for Binary { - fn from(s: BytesMut) -> Binary { - Binary::Bytes(s.freeze()) - } -} - -impl From> for Binary { - fn from(body: Arc) -> Binary { - Binary::SharedString(body) - } -} - -impl<'a> From<&'a Arc> for Binary { - fn from(body: &'a Arc) -> Binary { - Binary::SharedString(Arc::clone(body)) - } -} - -impl From>> for Binary { - fn from(body: Arc>) -> Binary { - Binary::SharedVec(body) - } -} - -impl<'a> From<&'a Arc>> for Binary { - fn from(body: &'a Arc>) -> Binary { - Binary::SharedVec(Arc::clone(body)) - } -} - -impl AsRef<[u8]> for Binary { - #[inline] - fn as_ref(&self) -> &[u8] { - match *self { - Binary::Bytes(ref bytes) => bytes.as_ref(), - Binary::Slice(slice) => slice, - Binary::SharedString(ref s) => s.as_bytes(), - Binary::SharedVec(ref s) => s.as_ref().as_ref(), - } - } -} - -impl Responder for Binary { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(HttpResponse::build_from(req) - .content_type("application/octet-stream") - .body(self)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_body_is_streaming() { - assert_eq!(Body::Empty.is_streaming(), false); - assert_eq!(Body::Binary(Binary::from("")).is_streaming(), false); - } - - #[test] - fn test_is_empty() { - assert_eq!(Binary::from("").is_empty(), true); - assert_eq!(Binary::from("test").is_empty(), false); - } - - #[test] - fn test_static_str() { - assert_eq!(Binary::from("test").len(), 4); - assert_eq!(Binary::from("test").as_ref(), b"test"); - } - - #[test] - fn test_cow_str() { - let cow: Cow<'static, str> = Cow::Borrowed("test"); - assert_eq!(Binary::from(cow.clone()).len(), 4); - assert_eq!(Binary::from(cow.clone()).as_ref(), b"test"); - let cow: Cow<'static, str> = Cow::Owned("test".to_owned()); - assert_eq!(Binary::from(cow.clone()).len(), 4); - assert_eq!(Binary::from(cow.clone()).as_ref(), b"test"); - } - - #[test] - fn test_static_bytes() { - assert_eq!(Binary::from(b"test".as_ref()).len(), 4); - assert_eq!(Binary::from(b"test".as_ref()).as_ref(), b"test"); - assert_eq!(Binary::from_slice(b"test".as_ref()).len(), 4); - assert_eq!(Binary::from_slice(b"test".as_ref()).as_ref(), b"test"); - } - - #[test] - fn test_vec() { - assert_eq!(Binary::from(Vec::from("test")).len(), 4); - assert_eq!(Binary::from(Vec::from("test")).as_ref(), b"test"); - } - - #[test] - fn test_bytes() { - assert_eq!(Binary::from(Bytes::from("test")).len(), 4); - assert_eq!(Binary::from(Bytes::from("test")).as_ref(), b"test"); - } - - #[test] - fn test_cow_bytes() { - let cow: Cow<'static, [u8]> = Cow::Borrowed(b"test"); - assert_eq!(Binary::from(cow.clone()).len(), 4); - assert_eq!(Binary::from(cow.clone()).as_ref(), b"test"); - let cow: Cow<'static, [u8]> = Cow::Owned(Vec::from("test")); - assert_eq!(Binary::from(cow.clone()).len(), 4); - assert_eq!(Binary::from(cow.clone()).as_ref(), b"test"); - } - - #[test] - fn test_arc_string() { - let b = Arc::new("test".to_owned()); - assert_eq!(Binary::from(b.clone()).len(), 4); - assert_eq!(Binary::from(b.clone()).as_ref(), b"test"); - assert_eq!(Binary::from(&b).len(), 4); - assert_eq!(Binary::from(&b).as_ref(), b"test"); - } - - #[test] - fn test_string() { - let b = "test".to_owned(); - assert_eq!(Binary::from(b.clone()).len(), 4); - assert_eq!(Binary::from(b.clone()).as_ref(), b"test"); - assert_eq!(Binary::from(&b).len(), 4); - assert_eq!(Binary::from(&b).as_ref(), b"test"); - } - - #[test] - fn test_shared_vec() { - let b = Arc::new(Vec::from(&b"test"[..])); - assert_eq!(Binary::from(b.clone()).len(), 4); - assert_eq!(Binary::from(b.clone()).as_ref(), &b"test"[..]); - assert_eq!(Binary::from(&b).len(), 4); - assert_eq!(Binary::from(&b).as_ref(), &b"test"[..]); - } - - #[test] - fn test_bytes_mut() { - let b = BytesMut::from("test"); - assert_eq!(Binary::from(b.clone()).len(), 4); - assert_eq!(Binary::from(b).as_ref(), b"test"); - } - - #[test] - fn test_binary_into() { - let bytes = Bytes::from_static(b"test"); - let b: Bytes = Binary::from("test").into(); - assert_eq!(b, bytes); - let b: Bytes = Binary::from(bytes.clone()).into(); - assert_eq!(b, bytes); - } -} diff --git a/src/client/connector.rs b/src/client/connector.rs deleted file mode 100644 index 1d0623023..000000000 --- a/src/client/connector.rs +++ /dev/null @@ -1,1340 +0,0 @@ -use std::collections::{HashMap, VecDeque}; -use std::net::Shutdown; -use std::time::{Duration, Instant}; -use std::{fmt, io, mem, time}; - -use actix_inner::actors::resolver::{Connect as ResolveConnect, Resolver, ResolverError}; -use actix_inner::{ - fut, Actor, ActorFuture, ActorResponse, AsyncContext, Context, - ContextFutureSpawner, Handler, Message, Recipient, StreamHandler, Supervised, - SystemService, WrapFuture, -}; - -use futures::sync::{mpsc, oneshot}; -use futures::{Async, Future, Poll}; -use http::{Error as HttpError, HttpTryFrom, Uri}; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_timer::Delay; - -#[cfg(any(feature = "alpn", feature = "ssl"))] -use { - openssl::ssl::{Error as SslError, SslConnector, SslMethod}, - tokio_openssl::SslConnectorExt, -}; - -#[cfg(all( - feature = "tls", - not(any(feature = "alpn", feature = "ssl", feature = "rust-tls")) -))] -use { - native_tls::{Error as SslError, TlsConnector as NativeTlsConnector}, - tokio_tls::TlsConnector as SslConnector, -}; - -#[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "tls", feature = "ssl")) -))] -use { - rustls::ClientConfig, std::io::Error as SslError, std::sync::Arc, - tokio_rustls::TlsConnector as SslConnector, webpki::DNSNameRef, webpki_roots, -}; - -#[cfg(not(any( - feature = "alpn", - feature = "ssl", - feature = "tls", - feature = "rust-tls" -)))] -type SslConnector = (); - -use server::IoStream; -use {HAS_OPENSSL, HAS_RUSTLS, HAS_TLS}; - -/// Client connector usage stats -#[derive(Default, Message)] -pub struct ClientConnectorStats { - /// Number of waited-on connections - pub waits: usize, - /// Size of the wait queue - pub wait_queue: usize, - /// Number of reused connections - pub reused: usize, - /// Number of opened connections - pub opened: usize, - /// Number of closed connections - pub closed: usize, - /// Number of connections with errors - pub errors: usize, - /// Number of connection timeouts - pub timeouts: usize, -} - -#[derive(Debug)] -/// `Connect` type represents a message that can be sent to -/// `ClientConnector` with a connection request. -pub struct Connect { - pub(crate) uri: Uri, - pub(crate) wait_timeout: Duration, - pub(crate) conn_timeout: Duration, -} - -impl Connect { - /// Create `Connect` message for specified `Uri` - pub fn new(uri: U) -> Result - where - Uri: HttpTryFrom, - { - Ok(Connect { - uri: Uri::try_from(uri).map_err(|e| e.into())?, - wait_timeout: Duration::from_secs(5), - conn_timeout: Duration::from_secs(1), - }) - } - - /// Connection timeout, i.e. max time to connect to remote host. - /// Set to 1 second by default. - pub fn conn_timeout(mut self, timeout: Duration) -> Self { - self.conn_timeout = timeout; - self - } - - /// If connection pool limits are enabled, wait time indicates - /// max time to wait for a connection to become available. - /// Set to 5 seconds by default. - pub fn wait_timeout(mut self, timeout: Duration) -> Self { - self.wait_timeout = timeout; - self - } -} - -impl Message for Connect { - type Result = Result; -} - -/// Pause connection process for `ClientConnector` -/// -/// All connect requests enter wait state during connector pause. -pub struct Pause { - time: Option, -} - -impl Pause { - /// Create message with pause duration parameter - pub fn new(time: Duration) -> Pause { - Pause { time: Some(time) } - } -} - -impl Default for Pause { - fn default() -> Pause { - Pause { time: None } - } -} - -impl Message for Pause { - type Result = (); -} - -/// Resume connection process for `ClientConnector` -#[derive(Message)] -pub struct Resume; - -/// A set of errors that can occur while connecting to an HTTP host -#[derive(Fail, Debug)] -pub enum ClientConnectorError { - /// Invalid URL - #[fail(display = "Invalid URL")] - InvalidUrl, - - /// SSL feature is not enabled - #[fail(display = "SSL is not supported")] - SslIsNotSupported, - - /// SSL error - #[cfg(any( - feature = "tls", - feature = "alpn", - feature = "ssl", - feature = "rust-tls", - ))] - #[fail(display = "{}", _0)] - SslError(#[cause] SslError), - - /// Resolver error - #[fail(display = "{}", _0)] - Resolver(#[cause] ResolverError), - - /// Connection took too long - #[fail(display = "Timeout while establishing connection")] - Timeout, - - /// Connector has been disconnected - #[fail(display = "Internal error: connector has been disconnected")] - Disconnected, - - /// Connection IO error - #[fail(display = "{}", _0)] - IoError(#[cause] io::Error), -} - -impl From for ClientConnectorError { - fn from(err: ResolverError) -> ClientConnectorError { - match err { - ResolverError::Timeout => ClientConnectorError::Timeout, - _ => ClientConnectorError::Resolver(err), - } - } -} - -struct Waiter { - tx: oneshot::Sender>, - wait: Instant, - conn_timeout: Duration, -} - -enum Paused { - No, - Yes, - Timeout(Instant, Delay), -} - -impl Paused { - fn is_paused(&self) -> bool { - match *self { - Paused::No => false, - _ => true, - } - } -} - -/// `ClientConnector` type is responsible for transport layer of a -/// client connection. -pub struct ClientConnector { - #[allow(dead_code)] - connector: SslConnector, - - stats: ClientConnectorStats, - subscriber: Option>, - - acq_tx: mpsc::UnboundedSender, - acq_rx: Option>, - - resolver: Option>, - conn_lifetime: Duration, - conn_keep_alive: Duration, - limit: usize, - limit_per_host: usize, - acquired: usize, - acquired_per_host: HashMap, - available: HashMap>, - to_close: Vec, - waiters: Option>>, - wait_timeout: Option<(Instant, Delay)>, - paused: Paused, -} - -impl Actor for ClientConnector { - type Context = Context; - - fn started(&mut self, ctx: &mut Self::Context) { - if self.resolver.is_none() { - self.resolver = Some(Resolver::from_registry().recipient()) - } - self.collect_periodic(ctx); - ctx.add_stream(self.acq_rx.take().unwrap()); - ctx.spawn(Maintenance); - } -} - -impl Supervised for ClientConnector {} - -impl SystemService for ClientConnector {} - -impl Default for ClientConnector { - fn default() -> ClientConnector { - let connector = { - #[cfg(all(any(feature = "alpn", feature = "ssl")))] - { - SslConnector::builder(SslMethod::tls()).unwrap().build() - } - - #[cfg(all( - feature = "tls", - not(any(feature = "alpn", feature = "ssl", feature = "rust-tls")) - ))] - { - NativeTlsConnector::builder().build().unwrap().into() - } - - #[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "tls", feature = "ssl")) - ))] - { - let mut config = ClientConfig::new(); - config - .root_store - .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); - SslConnector::from(Arc::new(config)) - } - - #[cfg_attr(rustfmt, rustfmt_skip)] - #[cfg(not(any( - feature = "alpn", feature = "ssl", feature = "tls", feature = "rust-tls")))] - { - () - } - }; - - #[cfg_attr(feature = "cargo-clippy", allow(let_unit_value))] - ClientConnector::with_connector_impl(connector) - } -} - -impl ClientConnector { - #[cfg(any(feature = "alpn", feature = "ssl"))] - /// Create `ClientConnector` actor with custom `SslConnector` instance. - /// - /// By default `ClientConnector` uses very a simple SSL configuration. - /// With `with_connector` method it is possible to use a custom - /// `SslConnector` object. - /// - /// ```rust,ignore - /// # #![cfg(feature="alpn")] - /// # extern crate actix_web; - /// # extern crate futures; - /// # use futures::{future, Future}; - /// # use std::io::Write; - /// # use actix_web::actix::Actor; - /// extern crate openssl; - /// use actix_web::{actix, client::ClientConnector, client::Connect}; - /// - /// use openssl::ssl::{SslConnector, SslMethod}; - /// - /// fn main() { - /// actix::run(|| { - /// // Start `ClientConnector` with custom `SslConnector` - /// let ssl_conn = SslConnector::builder(SslMethod::tls()).unwrap().build(); - /// let conn = ClientConnector::with_connector(ssl_conn).start(); - /// - /// conn.send( - /// Connect::new("https://www.rust-lang.org").unwrap()) // <- connect to host - /// .map_err(|_| ()) - /// .and_then(|res| { - /// if let Ok(mut stream) = res { - /// stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); - /// } - /// # actix::System::current().stop(); - /// Ok(()) - /// }) - /// }); - /// } - /// ``` - pub fn with_connector(connector: SslConnector) -> ClientConnector { - // keep level of indirection for docstrings matching featureflags - Self::with_connector_impl(connector) - } - - #[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "ssl", feature = "tls")) - ))] - /// Create `ClientConnector` actor with custom `SslConnector` instance. - /// - /// By default `ClientConnector` uses very a simple SSL configuration. - /// With `with_connector` method it is possible to use a custom - /// `SslConnector` object. - /// - /// ```rust - /// # #![cfg(feature = "rust-tls")] - /// # extern crate actix_web; - /// # extern crate futures; - /// # use futures::{future, Future}; - /// # use std::io::Write; - /// # use actix_web::actix::Actor; - /// extern crate rustls; - /// extern crate webpki_roots; - /// use actix_web::{actix, client::ClientConnector, client::Connect}; - /// - /// use rustls::ClientConfig; - /// use std::sync::Arc; - /// - /// fn main() { - /// actix::run(|| { - /// // Start `ClientConnector` with custom `ClientConfig` - /// let mut config = ClientConfig::new(); - /// config - /// .root_store - /// .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); - /// let conn = ClientConnector::with_connector(config).start(); - /// - /// conn.send( - /// Connect::new("https://www.rust-lang.org").unwrap()) // <- connect to host - /// .map_err(|_| ()) - /// .and_then(|res| { - /// if let Ok(mut stream) = res { - /// stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); - /// } - /// # actix::System::current().stop(); - /// Ok(()) - /// }) - /// }); - /// } - /// ``` - pub fn with_connector(connector: ClientConfig) -> ClientConnector { - // keep level of indirection for docstrings matching featureflags - Self::with_connector_impl(SslConnector::from(Arc::new(connector))) - } - - #[cfg(all( - feature = "tls", - not(any(feature = "ssl", feature = "alpn", feature = "rust-tls")) - ))] - /// Create `ClientConnector` actor with custom `SslConnector` instance. - /// - /// By default `ClientConnector` uses very a simple SSL configuration. - /// With `with_connector` method it is possible to use a custom - /// `SslConnector` object. - /// - /// ```rust - /// # #![cfg(feature = "tls")] - /// # extern crate actix_web; - /// # extern crate futures; - /// # use futures::{future, Future}; - /// # use std::io::Write; - /// # use actix_web::actix::Actor; - /// extern crate native_tls; - /// extern crate webpki_roots; - /// use native_tls::TlsConnector; - /// use actix_web::{actix, client::ClientConnector, client::Connect}; - /// - /// fn main() { - /// actix::run(|| { - /// let connector = TlsConnector::new().unwrap(); - /// let conn = ClientConnector::with_connector(connector.into()).start(); - /// - /// conn.send( - /// Connect::new("https://www.rust-lang.org").unwrap()) // <- connect to host - /// .map_err(|_| ()) - /// .and_then(|res| { - /// if let Ok(mut stream) = res { - /// stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); - /// } - /// # actix::System::current().stop(); - /// Ok(()) - /// }) - /// }); - /// } - /// ``` - pub fn with_connector(connector: SslConnector) -> ClientConnector { - // keep level of indirection for docstrings matching featureflags - Self::with_connector_impl(connector) - } - - #[inline] - fn with_connector_impl(connector: SslConnector) -> ClientConnector { - let (tx, rx) = mpsc::unbounded(); - - ClientConnector { - connector, - stats: ClientConnectorStats::default(), - subscriber: None, - acq_tx: tx, - acq_rx: Some(rx), - resolver: None, - conn_lifetime: Duration::from_secs(75), - conn_keep_alive: Duration::from_secs(15), - limit: 100, - limit_per_host: 0, - acquired: 0, - acquired_per_host: HashMap::new(), - available: HashMap::new(), - to_close: Vec::new(), - waiters: Some(HashMap::new()), - wait_timeout: None, - paused: Paused::No, - } - } - - /// Set total number of simultaneous connections. - /// - /// If limit is 0, the connector has no limit. - /// The default limit size is 100. - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - /// Set total number of simultaneous connections to the same endpoint. - /// - /// Endpoints are the same if they have equal (host, port, ssl) triplets. - /// If limit is 0, the connector has no limit. The default limit size is 0. - pub fn limit_per_host(mut self, limit: usize) -> Self { - self.limit_per_host = limit; - self - } - - /// Set keep-alive period for opened connection. - /// - /// Keep-alive period is the period between connection usage. If - /// the delay between repeated usages of the same connection - /// exceeds this period, the connection is closed. - /// Default keep-alive period is 15 seconds. - pub fn conn_keep_alive(mut self, dur: Duration) -> Self { - self.conn_keep_alive = dur; - self - } - - /// Set max lifetime period for connection. - /// - /// Connection lifetime is max lifetime of any opened connection - /// until it is closed regardless of keep-alive period. - /// Default lifetime period is 75 seconds. - pub fn conn_lifetime(mut self, dur: Duration) -> Self { - self.conn_lifetime = dur; - self - } - - /// Subscribe for connector stats. Only one subscriber is supported. - pub fn stats(mut self, subs: Recipient) -> Self { - self.subscriber = Some(subs); - self - } - - /// Use custom resolver actor - /// - /// By default actix's Resolver is used. - pub fn resolver>>(mut self, addr: A) -> Self { - self.resolver = Some(addr.into()); - self - } - - fn acquire(&mut self, key: &Key) -> Acquire { - // check limits - if self.limit > 0 { - if self.acquired >= self.limit { - return Acquire::NotAvailable; - } - if self.limit_per_host > 0 { - if let Some(per_host) = self.acquired_per_host.get(key) { - if *per_host >= self.limit_per_host { - return Acquire::NotAvailable; - } - } - } - } else if self.limit_per_host > 0 { - if let Some(per_host) = self.acquired_per_host.get(key) { - if *per_host >= self.limit_per_host { - return Acquire::NotAvailable; - } - } - } - - self.reserve(key); - - // check if open connection is available - // cleanup stale connections at the same time - if let Some(ref mut connections) = self.available.get_mut(key) { - let now = Instant::now(); - while let Some(conn) = connections.pop_back() { - // check if it still usable - if (now - conn.0) > self.conn_keep_alive - || (now - conn.1.ts) > self.conn_lifetime - { - self.stats.closed += 1; - self.to_close.push(conn.1); - } else { - let mut conn = conn.1; - let mut buf = [0; 2]; - match conn.stream().read(&mut buf) { - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => (), - Ok(n) if n > 0 => { - self.stats.closed += 1; - self.to_close.push(conn); - continue; - } - Ok(_) | Err(_) => continue, - } - return Acquire::Acquired(conn); - } - } - } - Acquire::Available - } - - fn reserve(&mut self, key: &Key) { - self.acquired += 1; - let per_host = if let Some(per_host) = self.acquired_per_host.get(key) { - *per_host - } else { - 0 - }; - self.acquired_per_host.insert(key.clone(), per_host + 1); - } - - fn release_key(&mut self, key: &Key) { - if self.acquired > 0 { - self.acquired -= 1; - } - let per_host = if let Some(per_host) = self.acquired_per_host.get(key) { - *per_host - } else { - return; - }; - if per_host > 1 { - self.acquired_per_host.insert(key.clone(), per_host - 1); - } else { - self.acquired_per_host.remove(key); - } - } - - fn collect_periodic(&mut self, ctx: &mut Context) { - // check connections for shutdown - let mut idx = 0; - while idx < self.to_close.len() { - match AsyncWrite::shutdown(&mut self.to_close[idx]) { - Ok(Async::NotReady) => idx += 1, - _ => { - self.to_close.swap_remove(idx); - } - } - } - - // re-schedule next collect period - ctx.run_later(Duration::from_secs(1), |act, ctx| act.collect_periodic(ctx)); - - // send stats - let mut stats = mem::replace(&mut self.stats, ClientConnectorStats::default()); - if let Some(ref mut subscr) = self.subscriber { - if let Some(ref waiters) = self.waiters { - for w in waiters.values() { - stats.wait_queue += w.len(); - } - } - let _ = subscr.do_send(stats); - } - } - - // TODO: waiters should be sorted by deadline. maybe timewheel? - fn collect_waiters(&mut self) { - let now = Instant::now(); - let mut next = None; - - for waiters in self.waiters.as_mut().unwrap().values_mut() { - let mut idx = 0; - while idx < waiters.len() { - let wait = waiters[idx].wait; - if wait <= now { - self.stats.timeouts += 1; - let waiter = waiters.swap_remove_back(idx).unwrap(); - let _ = waiter.tx.send(Err(ClientConnectorError::Timeout)); - } else { - if let Some(n) = next { - if wait < n { - next = Some(wait); - } - } else { - next = Some(wait); - } - idx += 1; - } - } - } - - if next.is_some() { - self.install_wait_timeout(next.unwrap()); - } - } - - fn install_wait_timeout(&mut self, time: Instant) { - if let Some(ref mut wait) = self.wait_timeout { - if wait.0 < time { - return; - } - } - - let mut timeout = Delay::new(time); - let _ = timeout.poll(); - self.wait_timeout = Some((time, timeout)); - } - - fn wait_for( - &mut self, key: Key, wait: Duration, conn_timeout: Duration, - ) -> oneshot::Receiver> { - // connection is not available, wait - let (tx, rx) = oneshot::channel(); - - let wait = Instant::now() + wait; - self.install_wait_timeout(wait); - - let waiter = Waiter { - tx, - wait, - conn_timeout, - }; - self.waiters - .as_mut() - .unwrap() - .entry(key) - .or_insert_with(VecDeque::new) - .push_back(waiter); - rx - } - - fn check_availibility(&mut self, ctx: &mut Context) { - // check waiters - let mut act_waiters = self.waiters.take().unwrap(); - - for (key, ref mut waiters) in &mut act_waiters { - while let Some(waiter) = waiters.pop_front() { - if waiter.tx.is_canceled() { - continue; - } - - match self.acquire(key) { - Acquire::Acquired(mut conn) => { - // use existing connection - self.stats.reused += 1; - conn.pool = - Some(AcquiredConn(key.clone(), Some(self.acq_tx.clone()))); - let _ = waiter.tx.send(Ok(conn)); - } - Acquire::NotAvailable => { - waiters.push_front(waiter); - break; - } - Acquire::Available => { - // create new connection - self.connect_waiter(&key, waiter, ctx); - } - } - } - } - - self.waiters = Some(act_waiters); - } - - fn connect_waiter(&mut self, key: &Key, waiter: Waiter, ctx: &mut Context) { - let key = key.clone(); - let conn = AcquiredConn(key.clone(), Some(self.acq_tx.clone())); - - let key2 = key.clone(); - fut::WrapFuture::::actfuture( - self.resolver.as_ref().unwrap().send( - ResolveConnect::host_and_port(&conn.0.host, conn.0.port) - .timeout(waiter.conn_timeout), - ), - ).map_err(move |_, act, _| { - act.release_key(&key2); - () - }).and_then(move |res, act, _| { - #[cfg(any(feature = "alpn", feature = "ssl"))] - match res { - Err(err) => { - let _ = waiter.tx.send(Err(err.into())); - fut::Either::B(fut::err(())) - } - Ok(stream) => { - act.stats.opened += 1; - if conn.0.ssl { - fut::Either::A( - act.connector - .connect_async(&key.host, stream) - .into_actor(act) - .then(move |res, _, _| { - match res { - Err(e) => { - let _ = waiter.tx.send(Err( - ClientConnectorError::SslError(e), - )); - } - Ok(stream) => { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - } - } - fut::ok(()) - }), - ) - } else { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - fut::Either::B(fut::ok(())) - } - } - } - - #[cfg(all(feature = "tls", not(any(feature = "alpn", feature = "ssl"))))] - match res { - Err(err) => { - let _ = waiter.tx.send(Err(err.into())); - fut::Either::B(fut::err(())) - } - Ok(stream) => { - act.stats.opened += 1; - if conn.0.ssl { - fut::Either::A( - act.connector - .connect(&conn.0.host, stream) - .into_actor(act) - .then(move |res, _, _| { - match res { - Err(e) => { - let _ = waiter.tx.send(Err( - ClientConnectorError::SslError(e), - )); - } - Ok(stream) => { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - } - } - fut::ok(()) - }), - ) - } else { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - fut::Either::B(fut::ok(())) - } - } - } - - #[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "ssl", feature = "tls")) - ))] - match res { - Err(err) => { - let _ = waiter.tx.send(Err(err.into())); - fut::Either::B(fut::err(())) - } - Ok(stream) => { - act.stats.opened += 1; - if conn.0.ssl { - let host = DNSNameRef::try_from_ascii_str(&key.host).unwrap(); - fut::Either::A( - act.connector - .connect(host, stream) - .into_actor(act) - .then(move |res, _, _| { - match res { - Err(e) => { - let _ = waiter.tx.send(Err( - ClientConnectorError::SslError(e), - )); - } - Ok(stream) => { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - } - } - fut::ok(()) - }), - ) - } else { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - fut::Either::B(fut::ok(())) - } - } - } - - #[cfg(not(any( - feature = "alpn", - feature = "ssl", - feature = "tls", - feature = "rust-tls" - )))] - match res { - Err(err) => { - let _ = waiter.tx.send(Err(err.into())); - fut::err(()) - } - Ok(stream) => { - act.stats.opened += 1; - if conn.0.ssl { - let _ = - waiter.tx.send(Err(ClientConnectorError::SslIsNotSupported)); - } else { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - }; - fut::ok(()) - } - } - }).spawn(ctx); - } -} - -impl Handler for ClientConnector { - type Result = (); - - fn handle(&mut self, msg: Pause, _: &mut Self::Context) { - if let Some(time) = msg.time { - let when = Instant::now() + time; - let mut timeout = Delay::new(when); - let _ = timeout.poll(); - self.paused = Paused::Timeout(when, timeout); - } else { - self.paused = Paused::Yes; - } - } -} - -impl Handler for ClientConnector { - type Result = (); - - fn handle(&mut self, _: Resume, _: &mut Self::Context) { - self.paused = Paused::No; - } -} - -impl Handler for ClientConnector { - type Result = ActorResponse; - - fn handle(&mut self, msg: Connect, ctx: &mut Self::Context) -> Self::Result { - let uri = &msg.uri; - let wait_timeout = msg.wait_timeout; - let conn_timeout = msg.conn_timeout; - - // host name is required - if uri.host().is_none() { - return ActorResponse::reply(Err(ClientConnectorError::InvalidUrl)); - } - - // supported protocols - let proto = match uri.scheme_part() { - Some(scheme) => match Protocol::from(scheme.as_str()) { - Some(proto) => proto, - None => { - return ActorResponse::reply(Err(ClientConnectorError::InvalidUrl)) - } - }, - None => return ActorResponse::reply(Err(ClientConnectorError::InvalidUrl)), - }; - - // check ssl availability - if proto.is_secure() && !HAS_OPENSSL && !HAS_TLS && !HAS_RUSTLS { - return ActorResponse::reply(Err(ClientConnectorError::SslIsNotSupported)); - } - - let host = uri.host().unwrap().to_owned(); - let port = uri.port_part().map(|port| port.as_u16()).unwrap_or_else(|| proto.port()); - let key = Key { - host, - port, - ssl: proto.is_secure(), - }; - - // check pause state - if self.paused.is_paused() { - let rx = self.wait_for(key.clone(), wait_timeout, conn_timeout); - self.stats.waits += 1; - return ActorResponse::async( - rx.map_err(|_| ClientConnectorError::Disconnected) - .into_actor(self) - .and_then(move |res, act, ctx| match res { - Ok(conn) => fut::ok(conn), - Err(err) => { - match err { - ClientConnectorError::Timeout => (), - _ => { - act.release_key(&key); - } - } - act.stats.errors += 1; - act.check_availibility(ctx); - fut::err(err) - } - }), - ); - } - - // do not re-use websockets connection - if !proto.is_http() { - let (tx, rx) = oneshot::channel(); - let wait = Instant::now() + wait_timeout; - let waiter = Waiter { - tx, - wait, - conn_timeout, - }; - self.connect_waiter(&key, waiter, ctx); - - return ActorResponse::async( - rx.map_err(|_| ClientConnectorError::Disconnected) - .into_actor(self) - .and_then(move |res, act, ctx| match res { - Ok(conn) => fut::ok(conn), - Err(err) => { - act.stats.errors += 1; - act.release_key(&key); - act.check_availibility(ctx); - fut::err(err) - } - }), - ); - } - - // acquire connection - match self.acquire(&key) { - Acquire::Acquired(mut conn) => { - // use existing connection - conn.pool = Some(AcquiredConn(key, Some(self.acq_tx.clone()))); - self.stats.reused += 1; - ActorResponse::async(fut::ok(conn)) - } - Acquire::NotAvailable => { - // connection is not available, wait - let rx = self.wait_for(key.clone(), wait_timeout, conn_timeout); - self.stats.waits += 1; - - ActorResponse::async( - rx.map_err(|_| ClientConnectorError::Disconnected) - .into_actor(self) - .and_then(move |res, act, ctx| match res { - Ok(conn) => fut::ok(conn), - Err(err) => { - match err { - ClientConnectorError::Timeout => (), - _ => { - act.release_key(&key); - } - } - act.stats.errors += 1; - act.check_availibility(ctx); - fut::err(err) - } - }), - ) - } - Acquire::Available => { - let (tx, rx) = oneshot::channel(); - let wait = Instant::now() + wait_timeout; - let waiter = Waiter { - tx, - wait, - conn_timeout, - }; - self.connect_waiter(&key, waiter, ctx); - - ActorResponse::async( - rx.map_err(|_| ClientConnectorError::Disconnected) - .into_actor(self) - .and_then(move |res, act, ctx| match res { - Ok(conn) => fut::ok(conn), - Err(err) => { - act.stats.errors += 1; - act.release_key(&key); - act.check_availibility(ctx); - fut::err(err) - } - }), - ) - } - } - } -} - -impl StreamHandler for ClientConnector { - fn handle(&mut self, msg: AcquiredConnOperation, ctx: &mut Context) { - match msg { - AcquiredConnOperation::Close(conn) => { - self.release_key(&conn.key); - self.to_close.push(conn); - self.stats.closed += 1; - } - AcquiredConnOperation::Release(conn) => { - self.release_key(&conn.key); - if (Instant::now() - conn.ts) < self.conn_lifetime { - self.available - .entry(conn.key.clone()) - .or_insert_with(VecDeque::new) - .push_back(Conn(Instant::now(), conn)); - } else { - self.to_close.push(conn); - self.stats.closed += 1; - } - } - AcquiredConnOperation::ReleaseKey(key) => { - // closed - self.stats.closed += 1; - self.release_key(&key); - } - } - - self.check_availibility(ctx); - } -} - -struct Maintenance; - -impl fut::ActorFuture for Maintenance { - type Item = (); - type Error = (); - type Actor = ClientConnector; - - fn poll( - &mut self, act: &mut ClientConnector, ctx: &mut Context, - ) -> Poll { - // check pause duration - if let Paused::Timeout(inst, _) = act.paused { - if inst <= Instant::now() { - act.paused = Paused::No; - } - } - - // collect wait timers - act.collect_waiters(); - - // check waiters - act.check_availibility(ctx); - - Ok(Async::NotReady) - } -} - -#[derive(PartialEq, Hash, Debug, Clone, Copy)] -enum Protocol { - Http, - Https, - Ws, - Wss, -} - -impl Protocol { - fn from(s: &str) -> Option { - match s { - "http" => Some(Protocol::Http), - "https" => Some(Protocol::Https), - "ws" => Some(Protocol::Ws), - "wss" => Some(Protocol::Wss), - _ => None, - } - } - - fn is_http(self) -> bool { - match self { - Protocol::Https | Protocol::Http => true, - _ => false, - } - } - - fn is_secure(self) -> bool { - match self { - Protocol::Https | Protocol::Wss => true, - _ => false, - } - } - - fn port(self) -> u16 { - match self { - Protocol::Http | Protocol::Ws => 80, - Protocol::Https | Protocol::Wss => 443, - } - } -} - -#[derive(Hash, Eq, PartialEq, Clone, Debug)] -struct Key { - host: String, - port: u16, - ssl: bool, -} - -impl Key { - fn empty() -> Key { - Key { - host: String::new(), - port: 0, - ssl: false, - } - } -} - -#[derive(Debug)] -struct Conn(Instant, Connection); - -enum Acquire { - Acquired(Connection), - Available, - NotAvailable, -} - -enum AcquiredConnOperation { - Close(Connection), - Release(Connection), - ReleaseKey(Key), -} - -struct AcquiredConn(Key, Option>); - -impl AcquiredConn { - fn close(&mut self, conn: Connection) { - if let Some(tx) = self.1.take() { - let _ = tx.unbounded_send(AcquiredConnOperation::Close(conn)); - } - } - fn release(&mut self, conn: Connection) { - if let Some(tx) = self.1.take() { - let _ = tx.unbounded_send(AcquiredConnOperation::Release(conn)); - } - } -} - -impl Drop for AcquiredConn { - fn drop(&mut self) { - if let Some(tx) = self.1.take() { - let _ = tx.unbounded_send(AcquiredConnOperation::ReleaseKey(self.0.clone())); - } - } -} - -/// HTTP client connection -pub struct Connection { - key: Key, - stream: Box, - pool: Option, - ts: Instant, -} - -impl fmt::Debug for Connection { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Connection {}:{}", self.key.host, self.key.port) - } -} - -impl Connection { - fn new(key: Key, pool: Option, stream: Box) -> Self { - Connection { - key, - stream, - pool, - ts: Instant::now(), - } - } - - /// Raw IO stream - pub fn stream(&mut self) -> &mut IoStream { - &mut *self.stream - } - - /// Create a new connection from an IO Stream - /// - /// The stream can be a `UnixStream` if the Unix-only "uds" feature is enabled. - /// - /// See also `ClientRequestBuilder::with_connection()`. - pub fn from_stream(io: T) -> Connection { - Connection::new(Key::empty(), None, Box::new(io)) - } - - /// Close connection - pub fn close(mut self) { - if let Some(mut pool) = self.pool.take() { - pool.close(self) - } - } - - /// Release this connection to the connection pool - pub fn release(mut self) { - if let Some(mut pool) = self.pool.take() { - pool.release(self) - } - } -} - -impl IoStream for Connection { - fn shutdown(&mut self, how: Shutdown) -> io::Result<()> { - IoStream::shutdown(&mut *self.stream, how) - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - IoStream::set_nodelay(&mut *self.stream, nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - IoStream::set_linger(&mut *self.stream, dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - IoStream::set_keepalive(&mut *self.stream, dur) - } -} - -impl io::Read for Connection { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.stream.read(buf) - } -} - -impl AsyncRead for Connection {} - -impl io::Write for Connection { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.stream.write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.stream.flush() - } -} - -impl AsyncWrite for Connection { - fn shutdown(&mut self) -> Poll<(), io::Error> { - self.stream.shutdown() - } -} - -#[cfg(feature = "tls")] -use tokio_tls::TlsStream; - -#[cfg(feature = "tls")] -/// This is temp solution untile actix-net migration -impl IoStream for TlsStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = self.get_mut().shutdown(); - Ok(()) - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().get_mut().set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_keepalive(dur) - } -} diff --git a/src/client/mod.rs b/src/client/mod.rs deleted file mode 100644 index 5321e4b05..000000000 --- a/src/client/mod.rs +++ /dev/null @@ -1,120 +0,0 @@ -//! Http client api -//! -//! ```rust -//! # extern crate actix_web; -//! # extern crate actix; -//! # extern crate futures; -//! # extern crate tokio; -//! # use std::process; -//! use actix_web::client; -//! use futures::Future; -//! -//! fn main() { -//! actix::run( -//! || client::get("http://www.rust-lang.org") // <- Create request builder -//! .header("User-Agent", "Actix-web") -//! .finish().unwrap() -//! .send() // <- Send http request -//! .map_err(|_| ()) -//! .and_then(|response| { // <- server http response -//! println!("Response: {:?}", response); -//! # actix::System::current().stop(); -//! Ok(()) -//! }) -//! ); -//! } -//! ``` -mod connector; -mod parser; -mod pipeline; -mod request; -mod response; -mod writer; - -pub use self::connector::{ - ClientConnector, ClientConnectorError, ClientConnectorStats, Connect, Connection, - Pause, Resume, -}; -pub(crate) use self::parser::{HttpResponseParser, HttpResponseParserError}; -pub(crate) use self::pipeline::Pipeline; -pub use self::pipeline::{SendRequest, SendRequestError}; -pub use self::request::{ClientRequest, ClientRequestBuilder}; -pub use self::response::ClientResponse; -pub(crate) use self::writer::HttpClientWriter; - -use error::ResponseError; -use http::Method; -use httpresponse::HttpResponse; - -/// Convert `SendRequestError` to a `HttpResponse` -impl ResponseError for SendRequestError { - fn error_response(&self) -> HttpResponse { - match *self { - SendRequestError::Timeout => HttpResponse::GatewayTimeout(), - SendRequestError::Connector(_) => HttpResponse::BadGateway(), - _ => HttpResponse::InternalServerError(), - }.into() - } -} - -/// Create request builder for `GET` requests -/// -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate actix; -/// # extern crate futures; -/// # extern crate tokio; -/// # extern crate env_logger; -/// # use std::process; -/// use actix_web::client; -/// use futures::Future; -/// -/// fn main() { -/// actix::run( -/// || client::get("http://www.rust-lang.org") // <- Create request builder -/// .header("User-Agent", "Actix-web") -/// .finish().unwrap() -/// .send() // <- Send http request -/// .map_err(|_| ()) -/// .and_then(|response| { // <- server http response -/// println!("Response: {:?}", response); -/// # actix::System::current().stop(); -/// Ok(()) -/// }), -/// ); -/// } -/// ``` -pub fn get>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::GET).uri(uri); - builder -} - -/// Create request builder for `HEAD` requests -pub fn head>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::HEAD).uri(uri); - builder -} - -/// Create request builder for `POST` requests -pub fn post>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::POST).uri(uri); - builder -} - -/// Create request builder for `PUT` requests -pub fn put>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::PUT).uri(uri); - builder -} - -/// Create request builder for `DELETE` requests -pub fn delete>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::DELETE).uri(uri); - builder -} diff --git a/src/client/parser.rs b/src/client/parser.rs deleted file mode 100644 index 92a7abe13..000000000 --- a/src/client/parser.rs +++ /dev/null @@ -1,238 +0,0 @@ -use std::mem; - -use bytes::{Bytes, BytesMut}; -use futures::{Async, Poll}; -use http::header::{self, HeaderName, HeaderValue}; -use http::{HeaderMap, StatusCode, Version}; -use httparse; - -use error::{ParseError, PayloadError}; - -use server::h1decoder::{EncodingDecoder, HeaderIndex}; -use server::IoStream; - -use super::response::ClientMessage; -use super::ClientResponse; - -const MAX_BUFFER_SIZE: usize = 131_072; -const MAX_HEADERS: usize = 96; - -#[derive(Default)] -pub struct HttpResponseParser { - decoder: Option, - eof: bool, // indicate that we read payload until stream eof -} - -#[derive(Debug, Fail)] -pub enum HttpResponseParserError { - /// Server disconnected - #[fail(display = "Server disconnected")] - Disconnect, - #[fail(display = "{}", _0)] - Error(#[cause] ParseError), -} - -impl HttpResponseParser { - pub fn parse( - &mut self, io: &mut T, buf: &mut BytesMut, - ) -> Poll - where - T: IoStream, - { - loop { - // Don't call parser until we have data to parse. - if !buf.is_empty() { - match HttpResponseParser::parse_message(buf) - .map_err(HttpResponseParserError::Error)? - { - Async::Ready((msg, info)) => { - if let Some((decoder, eof)) = info { - self.eof = eof; - self.decoder = Some(decoder); - } else { - self.eof = false; - self.decoder = None; - } - return Ok(Async::Ready(msg)); - } - Async::NotReady => { - if buf.len() >= MAX_BUFFER_SIZE { - return Err(HttpResponseParserError::Error( - ParseError::TooLarge, - )); - } - // Parser needs more data. - } - } - } - // Read some more data into the buffer for the parser. - match io.read_available(buf) { - Ok(Async::Ready((false, true))) => { - return Err(HttpResponseParserError::Disconnect) - } - Ok(Async::Ready(_)) => (), - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(err) => return Err(HttpResponseParserError::Error(err.into())), - } - } - } - - pub fn parse_payload( - &mut self, io: &mut T, buf: &mut BytesMut, - ) -> Poll, PayloadError> - where - T: IoStream, - { - if self.decoder.is_some() { - loop { - // read payload - let (not_ready, stream_finished) = match io.read_available(buf) { - Ok(Async::Ready((_, true))) => (false, true), - Ok(Async::Ready((_, false))) => (false, false), - Ok(Async::NotReady) => (true, false), - Err(err) => return Err(err.into()), - }; - - match self.decoder.as_mut().unwrap().decode(buf) { - Ok(Async::Ready(Some(b))) => return Ok(Async::Ready(Some(b))), - Ok(Async::Ready(None)) => { - self.decoder.take(); - return Ok(Async::Ready(None)); - } - Ok(Async::NotReady) => { - if not_ready { - return Ok(Async::NotReady); - } - if stream_finished { - // read untile eof? - if self.eof { - return Ok(Async::Ready(None)); - } else { - return Err(PayloadError::Incomplete); - } - } - } - Err(err) => return Err(err.into()), - } - } - } else { - Ok(Async::Ready(None)) - } - } - - fn parse_message( - buf: &mut BytesMut, - ) -> Poll<(ClientResponse, Option<(EncodingDecoder, bool)>), ParseError> { - // Unsafe: we read only this data only after httparse parses headers into. - // performance bump for pipeline benchmarks. - let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { mem::uninitialized() }; - - let (len, version, status, headers_len) = { - let mut parsed: [httparse::Header; MAX_HEADERS] = - unsafe { mem::uninitialized() }; - - let mut resp = httparse::Response::new(&mut parsed); - match resp.parse(buf)? { - httparse::Status::Complete(len) => { - let version = if resp.version.unwrap_or(1) == 1 { - Version::HTTP_11 - } else { - Version::HTTP_10 - }; - HeaderIndex::record(buf, resp.headers, &mut headers); - let status = StatusCode::from_u16(resp.code.unwrap()) - .map_err(|_| ParseError::Status)?; - - (len, version, status, resp.headers.len()) - } - httparse::Status::Partial => return Ok(Async::NotReady), - } - }; - - let slice = buf.split_to(len).freeze(); - - // convert headers - let mut hdrs = HeaderMap::new(); - for idx in headers[..headers_len].iter() { - if let Ok(name) = HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]) { - // Unsafe: httparse check header value for valid utf-8 - let value = unsafe { - HeaderValue::from_shared_unchecked( - slice.slice(idx.value.0, idx.value.1), - ) - }; - hdrs.append(name, value); - } else { - return Err(ParseError::Header); - } - } - - let decoder = if status == StatusCode::SWITCHING_PROTOCOLS { - Some((EncodingDecoder::eof(), true)) - } else if let Some(len) = hdrs.get(header::CONTENT_LENGTH) { - // Content-Length - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - Some((EncodingDecoder::length(len), false)) - } else { - debug!("illegal Content-Length: {:?}", len); - return Err(ParseError::Header); - } - } else { - debug!("illegal Content-Length: {:?}", len); - return Err(ParseError::Header); - } - } else if chunked(&hdrs)? { - // Chunked encoding - Some((EncodingDecoder::chunked(), false)) - } else if let Some(value) = hdrs.get(header::CONNECTION) { - let close = if let Ok(s) = value.to_str() { - s == "close" - } else { - false - }; - if close { - Some((EncodingDecoder::eof(), true)) - } else { - None - } - } else { - None - }; - - if let Some(decoder) = decoder { - Ok(Async::Ready(( - ClientResponse::new(ClientMessage { - status, - version, - headers: hdrs, - cookies: None, - }), - Some(decoder), - ))) - } else { - Ok(Async::Ready(( - ClientResponse::new(ClientMessage { - status, - version, - headers: hdrs, - cookies: None, - }), - None, - ))) - } - } -} - -/// Check if request has chunked transfer encoding -pub fn chunked(headers: &HeaderMap) -> Result { - if let Some(encodings) = headers.get(header::TRANSFER_ENCODING) { - if let Ok(s) = encodings.to_str() { - Ok(s.to_lowercase().contains("chunked")) - } else { - Err(ParseError::Header) - } - } else { - Ok(false) - } -} diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs deleted file mode 100644 index 1dbd2e171..000000000 --- a/src/client/pipeline.rs +++ /dev/null @@ -1,553 +0,0 @@ -use bytes::{Bytes, BytesMut}; -use futures::sync::oneshot; -use futures::{Async, Future, Poll, Stream}; -use http::header::CONTENT_ENCODING; -use std::time::{Duration, Instant}; -use std::{io, mem}; -use tokio_timer::Delay; - -use actix_inner::dev::Request; -use actix::{Addr, SystemService}; - -use super::{ - ClientConnector, ClientConnectorError, ClientRequest, ClientResponse, Connect, - Connection, HttpClientWriter, HttpResponseParser, HttpResponseParserError, -}; -use body::{Body, BodyStream}; -use context::{ActorHttpContext, Frame}; -use error::Error; -use error::PayloadError; -use header::ContentEncoding; -use http::{Method, Uri}; -use httpmessage::HttpMessage; -use server::input::PayloadStream; -use server::WriterState; - -/// A set of errors that can occur during request sending and response reading -#[derive(Fail, Debug)] -pub enum SendRequestError { - /// Response took too long - #[fail(display = "Timeout while waiting for response")] - Timeout, - /// Failed to connect to host - #[fail(display = "Failed to connect to host: {}", _0)] - Connector(#[cause] ClientConnectorError), - /// Error parsing response - #[fail(display = "{}", _0)] - ParseError(#[cause] HttpResponseParserError), - /// Error reading response payload - #[fail(display = "Error reading response payload: {}", _0)] - Io(#[cause] io::Error), -} - -impl From for SendRequestError { - fn from(err: io::Error) -> SendRequestError { - SendRequestError::Io(err) - } -} - -impl From for SendRequestError { - fn from(err: ClientConnectorError) -> SendRequestError { - match err { - ClientConnectorError::Timeout => SendRequestError::Timeout, - _ => SendRequestError::Connector(err), - } - } -} - -enum State { - New, - Connect(Request), - Connection(Connection), - Send(Box), - None, -} - -/// `SendRequest` is a `Future` which represents an asynchronous -/// request sending process. -#[must_use = "SendRequest does nothing unless polled"] -pub struct SendRequest { - req: ClientRequest, - state: State, - conn: Option>, - conn_timeout: Duration, - wait_timeout: Duration, - timeout: Option, -} - -impl SendRequest { - pub(crate) fn new(req: ClientRequest) -> SendRequest { - SendRequest { - req, - conn: None, - state: State::New, - timeout: None, - wait_timeout: Duration::from_secs(5), - conn_timeout: Duration::from_secs(1), - } - } - - pub(crate) fn with_connector( - req: ClientRequest, conn: Addr, - ) -> SendRequest { - SendRequest { - req, - conn: Some(conn), - state: State::New, - timeout: None, - wait_timeout: Duration::from_secs(5), - conn_timeout: Duration::from_secs(1), - } - } - - pub(crate) fn with_connection(req: ClientRequest, conn: Connection) -> SendRequest { - SendRequest { - req, - state: State::Connection(conn), - conn: None, - timeout: None, - wait_timeout: Duration::from_secs(5), - conn_timeout: Duration::from_secs(1), - } - } - - /// Set request timeout - /// - /// Request timeout is the total time before a response must be received. - /// Default value is 5 seconds. - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = Some(timeout); - self - } - - /// Set connection timeout - /// - /// Connection timeout includes resolving hostname and actual connection to - /// the host. - /// Default value is 1 second. - pub fn conn_timeout(mut self, timeout: Duration) -> Self { - self.conn_timeout = timeout; - self - } - - /// Set wait timeout - /// - /// If connections pool limits are enabled, wait time indicates max time - /// to wait for available connection. Default value is 5 seconds. - pub fn wait_timeout(mut self, timeout: Duration) -> Self { - self.wait_timeout = timeout; - self - } -} - -impl Future for SendRequest { - type Item = ClientResponse; - type Error = SendRequestError; - - fn poll(&mut self) -> Poll { - loop { - let state = mem::replace(&mut self.state, State::None); - - match state { - State::New => { - let conn = if let Some(conn) = self.conn.take() { - conn - } else { - ClientConnector::from_registry() - }; - self.state = State::Connect(conn.send(Connect { - uri: self.req.uri().clone(), - wait_timeout: self.wait_timeout, - conn_timeout: self.conn_timeout, - })) - } - State::Connect(mut conn) => match conn.poll() { - Ok(Async::NotReady) => { - self.state = State::Connect(conn); - return Ok(Async::NotReady); - } - Ok(Async::Ready(result)) => match result { - Ok(stream) => self.state = State::Connection(stream), - Err(err) => return Err(err.into()), - }, - Err(_) => { - return Err(SendRequestError::Connector( - ClientConnectorError::Disconnected, - )); - } - }, - State::Connection(conn) => { - let mut writer = HttpClientWriter::new(); - writer.start(&mut self.req)?; - - let body = match self.req.replace_body(Body::Empty) { - Body::Streaming(stream) => IoBody::Payload(stream), - Body::Actor(ctx) => IoBody::Actor(ctx), - _ => IoBody::Done, - }; - - let timeout = self - .timeout - .take() - .unwrap_or_else(|| Duration::from_secs(5)); - - let pl = Box::new(Pipeline { - body, - writer, - conn: Some(conn), - parser: Some(HttpResponseParser::default()), - parser_buf: BytesMut::new(), - disconnected: false, - body_completed: false, - drain: None, - decompress: None, - should_decompress: self.req.response_decompress(), - write_state: RunningState::Running, - timeout: Some(Delay::new(Instant::now() + timeout)), - meth: self.req.method().clone(), - path: self.req.uri().clone(), - }); - self.state = State::Send(pl); - } - State::Send(mut pl) => { - pl.poll_timeout()?; - pl.poll_write().map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("{}", e).as_str()) - })?; - - match pl.parse() { - Ok(Async::Ready(mut resp)) => { - if self.req.method() == Method::HEAD { - pl.parser.take(); - } - resp.set_pipeline(pl); - return Ok(Async::Ready(resp)); - } - Ok(Async::NotReady) => { - self.state = State::Send(pl); - return Ok(Async::NotReady); - } - Err(err) => { - return Err(SendRequestError::ParseError(err)); - } - } - } - State::None => unreachable!(), - } - } - } -} - -pub struct Pipeline { - body: IoBody, - body_completed: bool, - conn: Option, - writer: HttpClientWriter, - parser: Option, - parser_buf: BytesMut, - disconnected: bool, - drain: Option>, - decompress: Option, - should_decompress: bool, - write_state: RunningState, - timeout: Option, - meth: Method, - path: Uri, -} - -enum IoBody { - Payload(BodyStream), - Actor(Box), - Done, -} - -#[derive(Debug, PartialEq)] -enum RunningState { - Running, - Paused, - Done, -} - -impl RunningState { - #[inline] - fn pause(&mut self) { - if *self != RunningState::Done { - *self = RunningState::Paused - } - } - #[inline] - fn resume(&mut self) { - if *self != RunningState::Done { - *self = RunningState::Running - } - } -} - -impl Pipeline { - fn release_conn(&mut self) { - if let Some(conn) = self.conn.take() { - if self.meth == Method::HEAD { - conn.close() - } else { - conn.release() - } - } - } - - #[inline] - fn parse(&mut self) -> Poll { - if let Some(ref mut conn) = self.conn { - match self - .parser - .as_mut() - .unwrap() - .parse(conn, &mut self.parser_buf) - { - Ok(Async::Ready(resp)) => { - // check content-encoding - if self.should_decompress { - if let Some(enc) = resp.headers().get(CONTENT_ENCODING) { - if let Ok(enc) = enc.to_str() { - match ContentEncoding::from(enc) { - ContentEncoding::Auto - | ContentEncoding::Identity => (), - enc => { - self.decompress = Some(PayloadStream::new(enc)) - } - } - } - } - } - - Ok(Async::Ready(resp)) - } - val => val, - } - } else { - Ok(Async::NotReady) - } - } - - #[inline] - pub(crate) fn poll(&mut self) -> Poll, PayloadError> { - if self.conn.is_none() { - return Ok(Async::Ready(None)); - } - let mut need_run = false; - - // need write? - match self - .poll_write() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{}", e)))? - { - Async::NotReady => need_run = true, - Async::Ready(_) => { - self.poll_timeout().map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("{}", e)) - })?; - } - } - - // need read? - if self.parser.is_some() { - let conn: &mut Connection = self.conn.as_mut().unwrap(); - - loop { - match self - .parser - .as_mut() - .unwrap() - .parse_payload(conn, &mut self.parser_buf)? - { - Async::Ready(Some(b)) => { - if let Some(ref mut decompress) = self.decompress { - match decompress.feed_data(b) { - Ok(Some(b)) => return Ok(Async::Ready(Some(b))), - Ok(None) => return Ok(Async::NotReady), - Err(ref err) - if err.kind() == io::ErrorKind::WouldBlock => - { - continue - } - Err(err) => return Err(err.into()), - } - } else { - return Ok(Async::Ready(Some(b))); - } - } - Async::Ready(None) => { - let _ = self.parser.take(); - break; - } - Async::NotReady => return Ok(Async::NotReady), - } - } - } - - // eof - if let Some(mut decompress) = self.decompress.take() { - let res = decompress.feed_eof(); - if let Some(b) = res? { - self.release_conn(); - return Ok(Async::Ready(Some(b))); - } - } - - if need_run { - Ok(Async::NotReady) - } else { - self.release_conn(); - Ok(Async::Ready(None)) - } - } - - fn poll_timeout(&mut self) -> Result<(), SendRequestError> { - if self.timeout.is_some() { - match self.timeout.as_mut().unwrap().poll() { - Ok(Async::Ready(())) => return Err(SendRequestError::Timeout), - Ok(Async::NotReady) => (), - Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e).into()), - } - } - Ok(()) - } - - #[inline] - fn poll_write(&mut self) -> Poll<(), Error> { - if self.write_state == RunningState::Done || self.conn.is_none() { - return Ok(Async::Ready(())); - } - - let mut done = false; - if self.drain.is_none() && self.write_state != RunningState::Paused { - 'outter: loop { - let result = match mem::replace(&mut self.body, IoBody::Done) { - IoBody::Payload(mut body) => match body.poll()? { - Async::Ready(None) => { - self.writer.write_eof()?; - self.body_completed = true; - break; - } - Async::Ready(Some(chunk)) => { - self.body = IoBody::Payload(body); - self.writer.write(chunk.as_ref())? - } - Async::NotReady => { - done = true; - self.body = IoBody::Payload(body); - break; - } - }, - IoBody::Actor(mut ctx) => { - if self.disconnected { - ctx.disconnected(); - } - match ctx.poll()? { - Async::Ready(Some(vec)) => { - if vec.is_empty() { - self.body = IoBody::Actor(ctx); - break; - } - let mut res = None; - for frame in vec { - match frame { - Frame::Chunk(None) => { - self.body_completed = true; - self.writer.write_eof()?; - break 'outter; - } - Frame::Chunk(Some(chunk)) => { - res = - Some(self.writer.write(chunk.as_ref())?) - } - Frame::Drain(fut) => self.drain = Some(fut), - } - } - self.body = IoBody::Actor(ctx); - if self.drain.is_some() { - self.write_state.resume(); - break; - } - res.unwrap() - } - Async::Ready(None) => { - done = true; - break; - } - Async::NotReady => { - done = true; - self.body = IoBody::Actor(ctx); - break; - } - } - } - IoBody::Done => { - self.body_completed = true; - done = true; - break; - } - }; - - match result { - WriterState::Pause => { - self.write_state.pause(); - break; - } - WriterState::Done => self.write_state.resume(), - } - } - } - - // flush io but only if we need to - match self - .writer - .poll_completed(self.conn.as_mut().unwrap(), false) - { - Ok(Async::Ready(_)) => { - if self.disconnected - || (self.body_completed && self.writer.is_completed()) - { - self.write_state = RunningState::Done; - } else { - self.write_state.resume(); - } - - // resolve drain futures - if let Some(tx) = self.drain.take() { - let _ = tx.send(()); - } - // restart io processing - if !done || self.write_state == RunningState::Done { - self.poll_write() - } else { - Ok(Async::NotReady) - } - } - Ok(Async::NotReady) => Ok(Async::NotReady), - Err(err) => Err(err.into()), - } - } -} - -impl Drop for Pipeline { - fn drop(&mut self) { - if let Some(conn) = self.conn.take() { - debug!( - "Client http transaction is not completed, dropping connection: {:?} {:?}", - self.meth, - self.path, - ); - conn.close() - } - } -} - -/// Future that resolves to a complete request body. -impl Stream for Box { - type Item = Bytes; - type Error = PayloadError; - - fn poll(&mut self) -> Poll, Self::Error> { - Pipeline::poll(self) - } -} diff --git a/src/client/request.rs b/src/client/request.rs deleted file mode 100644 index ad08ad135..000000000 --- a/src/client/request.rs +++ /dev/null @@ -1,783 +0,0 @@ -use std::fmt::Write as FmtWrite; -use std::io::Write; -use std::time::Duration; -use std::{fmt, mem}; - -use actix::Addr; -use bytes::{BufMut, Bytes, BytesMut}; -use cookie::{Cookie, CookieJar}; -use futures::Stream; -use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; -use serde::Serialize; -use serde_json; -use serde_urlencoded; -use url::Url; - -use super::connector::{ClientConnector, Connection}; -use super::pipeline::SendRequest; -use body::Body; -use error::Error; -use header::{ContentEncoding, Header, IntoHeaderValue}; -use http::header::{self, HeaderName, HeaderValue}; -use http::{uri, Error as HttpError, HeaderMap, HttpTryFrom, Method, Uri, Version}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; - -/// An HTTP Client Request -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate actix; -/// # extern crate futures; -/// # extern crate tokio; -/// # use futures::Future; -/// # use std::process; -/// use actix_web::client; -/// -/// fn main() { -/// actix::run( -/// || client::ClientRequest::get("http://www.rust-lang.org") // <- Create request builder -/// .header("User-Agent", "Actix-web") -/// .finish().unwrap() -/// .send() // <- Send http request -/// .map_err(|_| ()) -/// .and_then(|response| { // <- server http response -/// println!("Response: {:?}", response); -/// # actix::System::current().stop(); -/// Ok(()) -/// }), -/// ); -/// } -/// ``` -pub struct ClientRequest { - uri: Uri, - method: Method, - version: Version, - headers: HeaderMap, - body: Body, - chunked: bool, - upgrade: bool, - timeout: Option, - encoding: ContentEncoding, - response_decompress: bool, - buffer_capacity: usize, - conn: ConnectionType, -} - -enum ConnectionType { - Default, - Connector(Addr), - Connection(Connection), -} - -impl Default for ClientRequest { - fn default() -> ClientRequest { - ClientRequest { - uri: Uri::default(), - method: Method::default(), - version: Version::HTTP_11, - headers: HeaderMap::with_capacity(16), - body: Body::Empty, - chunked: false, - upgrade: false, - timeout: None, - encoding: ContentEncoding::Auto, - response_decompress: true, - buffer_capacity: 32_768, - conn: ConnectionType::Default, - } - } -} - -impl ClientRequest { - /// Create request builder for `GET` request - pub fn get>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::GET).uri(uri); - builder - } - - /// Create request builder for `HEAD` request - pub fn head>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::HEAD).uri(uri); - builder - } - - /// Create request builder for `POST` request - pub fn post>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::POST).uri(uri); - builder - } - - /// Create request builder for `PUT` request - pub fn put>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::PUT).uri(uri); - builder - } - - /// Create request builder for `DELETE` request - pub fn delete>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::DELETE).uri(uri); - builder - } -} - -impl ClientRequest { - /// Create client request builder - pub fn build() -> ClientRequestBuilder { - ClientRequestBuilder { - request: Some(ClientRequest::default()), - err: None, - cookies: None, - default_headers: true, - } - } - - /// Create client request builder - pub fn build_from>(source: T) -> ClientRequestBuilder { - source.into() - } - - /// Get the request URI - #[inline] - pub fn uri(&self) -> &Uri { - &self.uri - } - - /// Set client request URI - #[inline] - pub fn set_uri(&mut self, uri: Uri) { - self.uri = uri - } - - /// Get the request method - #[inline] - pub fn method(&self) -> &Method { - &self.method - } - - /// Set HTTP `Method` for the request - #[inline] - pub fn set_method(&mut self, method: Method) { - self.method = method - } - - /// Get HTTP version for the request - #[inline] - pub fn version(&self) -> Version { - self.version - } - - /// Set http `Version` for the request - #[inline] - pub fn set_version(&mut self, version: Version) { - self.version = version - } - - /// Get the headers from the request - #[inline] - pub fn headers(&self) -> &HeaderMap { - &self.headers - } - - /// Get a mutable reference to the headers - #[inline] - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.headers - } - - /// is chunked encoding enabled - #[inline] - pub fn chunked(&self) -> bool { - self.chunked - } - - /// is upgrade request - #[inline] - pub fn upgrade(&self) -> bool { - self.upgrade - } - - /// Content encoding - #[inline] - pub fn content_encoding(&self) -> ContentEncoding { - self.encoding - } - - /// Decompress response payload - #[inline] - pub fn response_decompress(&self) -> bool { - self.response_decompress - } - - /// Requested write buffer capacity - pub fn write_buffer_capacity(&self) -> usize { - self.buffer_capacity - } - - /// Get body of this response - #[inline] - pub fn body(&self) -> &Body { - &self.body - } - - /// Set a body - pub fn set_body>(&mut self, body: B) { - self.body = body.into(); - } - - /// Extract body, replace it with `Empty` - pub(crate) fn replace_body(&mut self, body: Body) -> Body { - mem::replace(&mut self.body, body) - } - - /// Send request - /// - /// This method returns a future that resolves to a ClientResponse - pub fn send(mut self) -> SendRequest { - let timeout = self.timeout.take(); - let send = match mem::replace(&mut self.conn, ConnectionType::Default) { - ConnectionType::Default => SendRequest::new(self), - ConnectionType::Connector(conn) => SendRequest::with_connector(self, conn), - ConnectionType::Connection(conn) => SendRequest::with_connection(self, conn), - }; - if let Some(timeout) = timeout { - send.timeout(timeout) - } else { - send - } - } -} - -impl fmt::Debug for ClientRequest { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!( - f, - "\nClientRequest {:?} {}:{}", - self.version, self.method, self.uri - )?; - writeln!(f, " headers:")?; - for (key, val) in self.headers.iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -/// An HTTP Client request builder -/// -/// This type can be used to construct an instance of `ClientRequest` through a -/// builder-like pattern. -pub struct ClientRequestBuilder { - request: Option, - err: Option, - cookies: Option, - default_headers: bool, -} - -impl ClientRequestBuilder { - /// Set HTTP URI of request. - #[inline] - pub fn uri>(&mut self, uri: U) -> &mut Self { - match Url::parse(uri.as_ref()) { - Ok(url) => self._uri(url.as_str()), - Err(_) => self._uri(uri.as_ref()), - } - } - - fn _uri(&mut self, url: &str) -> &mut Self { - match Uri::try_from(url) { - Ok(uri) => { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.uri = uri; - } - } - Err(e) => self.err = Some(e.into()), - } - self - } - - /// Set HTTP method of this request. - #[inline] - pub fn method(&mut self, method: Method) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.method = method; - } - self - } - - /// Set HTTP method of this request. - #[inline] - pub fn get_method(&mut self) -> &Method { - let parts = self.request.as_ref().expect("cannot reuse request builder"); - &parts.method - } - - /// Set HTTP version of this request. - /// - /// By default requests's HTTP version depends on network stream - #[inline] - pub fn version(&mut self, version: Version) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.version = version; - } - self - } - - /// Set a header. - /// - /// ```rust - /// # extern crate mime; - /// # extern crate actix_web; - /// # use actix_web::client::*; - /// # - /// use actix_web::{client, http}; - /// - /// fn main() { - /// let req = client::ClientRequest::build() - /// .set(http::header::Date::now()) - /// .set(http::header::ContentType(mime::TEXT_HTML)) - /// .finish() - /// .unwrap(); - /// } - /// ``` - #[doc(hidden)] - pub fn set(&mut self, hdr: H) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - match hdr.try_into() { - Ok(value) => { - parts.headers.insert(H::name(), value); - } - Err(e) => self.err = Some(e.into()), - } - } - self - } - - /// Append a header. - /// - /// Header gets appended to existing header. - /// To override header use `set_header()` method. - /// - /// ```rust - /// # extern crate http; - /// # extern crate actix_web; - /// # use actix_web::client::*; - /// # - /// use http::header; - /// - /// fn main() { - /// let req = ClientRequest::build() - /// .header("X-TEST", "value") - /// .header(header::CONTENT_TYPE, "application/json") - /// .finish() - /// .unwrap(); - /// } - /// ``` - pub fn header(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.request, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - parts.headers.append(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set a header. - pub fn set_header(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.request, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - parts.headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set a header only if it is not yet set. - pub fn set_header_if_none(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.request, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => if !parts.headers.contains_key(&key) { - match value.try_into() { - Ok(value) => { - parts.headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - } - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set content encoding. - /// - /// By default `ContentEncoding::Identity` is used. - #[inline] - pub fn content_encoding(&mut self, enc: ContentEncoding) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.encoding = enc; - } - self - } - - /// Enables automatic chunked transfer encoding - #[inline] - pub fn chunked(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.chunked = true; - } - self - } - - /// Enable connection upgrade - #[inline] - pub fn upgrade(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.upgrade = true; - } - self - } - - /// Set request's content type - #[inline] - pub fn content_type(&mut self, value: V) -> &mut Self - where - HeaderValue: HttpTryFrom, - { - if let Some(parts) = parts(&mut self.request, &self.err) { - match HeaderValue::try_from(value) { - Ok(value) => { - parts.headers.insert(header::CONTENT_TYPE, value); - } - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set content length - #[inline] - pub fn content_length(&mut self, len: u64) -> &mut Self { - let mut wrt = BytesMut::new().writer(); - let _ = write!(wrt, "{}", len); - self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze()) - } - - /// Set a cookie - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{client, http}; - /// - /// fn main() { - /// let req = client::ClientRequest::build() - /// .cookie( - /// http::Cookie::build("name", "value") - /// .domain("www.rust-lang.org") - /// .path("/") - /// .secure(true) - /// .http_only(true) - /// .finish(), - /// ) - /// .finish() - /// .unwrap(); - /// } - /// ``` - pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { - if self.cookies.is_none() { - let mut jar = CookieJar::new(); - jar.add(cookie.into_owned()); - self.cookies = Some(jar) - } else { - self.cookies.as_mut().unwrap().add(cookie.into_owned()); - } - self - } - - /// Do not add default request headers. - /// By default `Accept-Encoding` and `User-Agent` headers are set. - pub fn no_default_headers(&mut self) -> &mut Self { - self.default_headers = false; - self - } - - /// Disable automatic decompress response body - pub fn disable_decompress(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.response_decompress = false; - } - self - } - - /// Set write buffer capacity - /// - /// Default buffer capacity is 32kb - pub fn write_buffer_capacity(&mut self, cap: usize) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.buffer_capacity = cap; - } - self - } - - /// Set request timeout - /// - /// Request timeout is a total time before response should be received. - /// Default value is 5 seconds. - pub fn timeout(&mut self, timeout: Duration) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.timeout = Some(timeout); - } - self - } - - /// Send request using custom connector - pub fn with_connector(&mut self, conn: Addr) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.conn = ConnectionType::Connector(conn); - } - self - } - - /// Send request using existing `Connection` - pub fn with_connection(&mut self, conn: Connection) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.conn = ConnectionType::Connection(conn); - } - self - } - - /// This method calls provided closure with builder reference if - /// value is `true`. - pub fn if_true(&mut self, value: bool, f: F) -> &mut Self - where - F: FnOnce(&mut ClientRequestBuilder), - { - if value { - f(self); - } - self - } - - /// This method calls provided closure with builder reference if - /// value is `Some`. - pub fn if_some(&mut self, value: Option, f: F) -> &mut Self - where - F: FnOnce(T, &mut ClientRequestBuilder), - { - if let Some(val) = value { - f(val, self); - } - self - } - - /// Set a body and generate `ClientRequest`. - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn body>(&mut self, body: B) -> Result { - if let Some(e) = self.err.take() { - return Err(e.into()); - } - - if self.default_headers { - // enable br only for https - let https = if let Some(parts) = parts(&mut self.request, &self.err) { - parts - .uri - .scheme_part() - .map(|s| s == &uri::Scheme::HTTPS) - .unwrap_or(true) - } else { - true - }; - - if https { - self.set_header_if_none(header::ACCEPT_ENCODING, "br, gzip, deflate"); - } else { - self.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate"); - } - - // set request host header - if let Some(parts) = parts(&mut self.request, &self.err) { - if let Some(host) = parts.uri.host() { - if !parts.headers.contains_key(header::HOST) { - let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); - - let _ = match parts.uri.port_part().map(|port| port.as_u16()) { - None | Some(80) | Some(443) => write!(wrt, "{}", host), - Some(port) => write!(wrt, "{}:{}", host, port), - }; - - match wrt.get_mut().take().freeze().try_into() { - Ok(value) => { - parts.headers.insert(header::HOST, value); - } - Err(e) => self.err = Some(e.into()), - } - } - } - } - - // user agent - self.set_header_if_none( - header::USER_AGENT, - concat!("actix-web/", env!("CARGO_PKG_VERSION")), - ); - } - - let mut request = self.request.take().expect("cannot reuse request builder"); - - // set cookies - if let Some(ref mut jar) = self.cookies { - let mut cookie = String::new(); - for c in jar.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); - let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - request.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); - } - request.body = body.into(); - Ok(request) - } - - /// Set a JSON body and generate `ClientRequest` - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn json(&mut self, value: T) -> Result { - let body = serde_json::to_string(&value)?; - - let contains = if let Some(parts) = parts(&mut self.request, &self.err) { - parts.headers.contains_key(header::CONTENT_TYPE) - } else { - true - }; - if !contains { - self.header(header::CONTENT_TYPE, "application/json"); - } - - self.body(body) - } - - /// Set a urlencoded body and generate `ClientRequest` - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn form(&mut self, value: T) -> Result { - let body = serde_urlencoded::to_string(&value)?; - - let contains = if let Some(parts) = parts(&mut self.request, &self.err) { - parts.headers.contains_key(header::CONTENT_TYPE) - } else { - true - }; - if !contains { - self.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded"); - } - - self.body(body) - } - - /// Set a streaming body and generate `ClientRequest`. - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn streaming(&mut self, stream: S) -> Result - where - S: Stream + 'static, - E: Into, - { - self.body(Body::Streaming(Box::new(stream.map_err(|e| e.into())))) - } - - /// Set an empty body and generate `ClientRequest` - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn finish(&mut self) -> Result { - self.body(Body::Empty) - } - - /// This method construct new `ClientRequestBuilder` - pub fn take(&mut self) -> ClientRequestBuilder { - ClientRequestBuilder { - request: self.request.take(), - err: self.err.take(), - cookies: self.cookies.take(), - default_headers: self.default_headers, - } - } -} - -#[inline] -fn parts<'a>( - parts: &'a mut Option, err: &Option, -) -> Option<&'a mut ClientRequest> { - if err.is_some() { - return None; - } - parts.as_mut() -} - -impl fmt::Debug for ClientRequestBuilder { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(ref parts) = self.request { - writeln!( - f, - "\nClientRequestBuilder {:?} {}:{}", - parts.version, parts.method, parts.uri - )?; - writeln!(f, " headers:")?; - for (key, val) in parts.headers.iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } else { - write!(f, "ClientRequestBuilder(Consumed)") - } - } -} - -/// Create `ClientRequestBuilder` from `HttpRequest` -/// -/// It is useful for proxy requests. This implementation -/// copies all request headers and the method. -impl<'a, S: 'static> From<&'a HttpRequest> for ClientRequestBuilder { - fn from(req: &'a HttpRequest) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - for (key, value) in req.headers() { - builder.header(key.clone(), value.clone()); - } - builder.method(req.method().clone()); - builder - } -} diff --git a/src/client/response.rs b/src/client/response.rs deleted file mode 100644 index 5f1f42649..000000000 --- a/src/client/response.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::cell::RefCell; -use std::{fmt, str}; - -use cookie::Cookie; -use http::header::{self, HeaderValue}; -use http::{HeaderMap, StatusCode, Version}; - -use error::CookieParseError; -use httpmessage::HttpMessage; - -use super::pipeline::Pipeline; - -pub(crate) struct ClientMessage { - pub status: StatusCode, - pub version: Version, - pub headers: HeaderMap, - pub cookies: Option>>, -} - -impl Default for ClientMessage { - fn default() -> ClientMessage { - ClientMessage { - status: StatusCode::OK, - version: Version::HTTP_11, - headers: HeaderMap::with_capacity(16), - cookies: None, - } - } -} - -/// An HTTP Client response -pub struct ClientResponse(ClientMessage, RefCell>>); - -impl HttpMessage for ClientResponse { - type Stream = Box; - - /// Get the headers from the response. - #[inline] - fn headers(&self) -> &HeaderMap { - &self.0.headers - } - - #[inline] - fn payload(&self) -> Box { - self.1 - .borrow_mut() - .take() - .expect("Payload is already consumed.") - } -} - -impl ClientResponse { - pub(crate) fn new(msg: ClientMessage) -> ClientResponse { - ClientResponse(msg, RefCell::new(None)) - } - - pub(crate) fn set_pipeline(&mut self, pl: Box) { - *self.1.borrow_mut() = Some(pl); - } - - /// Get the HTTP version of this response. - #[inline] - pub fn version(&self) -> Version { - self.0.version - } - - /// Get the status from the server. - #[inline] - pub fn status(&self) -> StatusCode { - self.0.status - } - - /// Load response cookies. - pub fn cookies(&self) -> Result>, CookieParseError> { - let mut cookies = Vec::new(); - for val in self.0.headers.get_all(header::SET_COOKIE).iter() { - let s = str::from_utf8(val.as_bytes()).map_err(CookieParseError::from)?; - cookies.push(Cookie::parse_encoded(s)?.into_owned()); - } - Ok(cookies) - } - - /// Return request cookie. - pub fn cookie(&self, name: &str) -> Option { - if let Ok(cookies) = self.cookies() { - for cookie in cookies { - if cookie.name() == name { - return Some(cookie); - } - } - } - None - } -} - -impl fmt::Debug for ClientResponse { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status())?; - writeln!(f, " headers:")?; - for (key, val) in self.headers().iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_debug() { - let mut resp = ClientResponse::new(ClientMessage::default()); - resp.0 - .headers - .insert(header::COOKIE, HeaderValue::from_static("cookie1=value1")); - resp.0 - .headers - .insert(header::COOKIE, HeaderValue::from_static("cookie2=value2")); - - let dbg = format!("{:?}", resp); - assert!(dbg.contains("ClientResponse")); - } -} diff --git a/src/client/writer.rs b/src/client/writer.rs deleted file mode 100644 index 321753bbf..000000000 --- a/src/client/writer.rs +++ /dev/null @@ -1,412 +0,0 @@ -#![cfg_attr( - feature = "cargo-clippy", - allow(redundant_field_names) -)] - -use std::cell::RefCell; -use std::fmt::Write as FmtWrite; -use std::io::{self, Write}; - -#[cfg(feature = "brotli")] -use brotli2::write::BrotliEncoder; -use bytes::{BufMut, BytesMut}; -#[cfg(feature = "flate2")] -use flate2::write::{GzEncoder, ZlibEncoder}; -#[cfg(feature = "flate2")] -use flate2::Compression; -use futures::{Async, Poll}; -use http::header::{ - HeaderValue, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, -}; -use http::{HttpTryFrom, Version}; -use time::{self, Duration}; -use tokio_io::AsyncWrite; - -use body::{Binary, Body}; -use header::ContentEncoding; -use server::output::{ContentEncoder, Output, TransferEncoding}; -use server::WriterState; - -use client::ClientRequest; - -const AVERAGE_HEADER_SIZE: usize = 30; - -bitflags! { - struct Flags: u8 { - const STARTED = 0b0000_0001; - const UPGRADE = 0b0000_0010; - const KEEPALIVE = 0b0000_0100; - const DISCONNECTED = 0b0000_1000; - } -} - -pub(crate) struct HttpClientWriter { - flags: Flags, - written: u64, - headers_size: u32, - buffer: Output, - buffer_capacity: usize, -} - -impl HttpClientWriter { - pub fn new() -> HttpClientWriter { - HttpClientWriter { - flags: Flags::empty(), - written: 0, - headers_size: 0, - buffer_capacity: 0, - buffer: Output::Buffer(BytesMut::new()), - } - } - - pub fn disconnected(&mut self) { - self.buffer.take(); - } - - pub fn is_completed(&self) -> bool { - self.buffer.is_empty() - } - - // pub fn keepalive(&self) -> bool { - // self.flags.contains(Flags::KEEPALIVE) && - // !self.flags.contains(Flags::UPGRADE) } - - fn write_to_stream( - &mut self, stream: &mut T, - ) -> io::Result { - while !self.buffer.is_empty() { - match stream.write(self.buffer.as_ref().as_ref()) { - Ok(0) => { - self.disconnected(); - return Ok(WriterState::Done); - } - Ok(n) => { - let _ = self.buffer.split_to(n); - } - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { - if self.buffer.len() > self.buffer_capacity { - return Ok(WriterState::Pause); - } else { - return Ok(WriterState::Done); - } - } - Err(err) => return Err(err), - } - } - Ok(WriterState::Done) - } -} - -pub struct Writer<'a>(pub &'a mut BytesMut); - -impl<'a> io::Write for Writer<'a> { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.extend_from_slice(buf); - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -impl HttpClientWriter { - pub fn start(&mut self, msg: &mut ClientRequest) -> io::Result<()> { - // prepare task - self.buffer = content_encoder(self.buffer.take(), msg); - self.flags.insert(Flags::STARTED); - if msg.upgrade() { - self.flags.insert(Flags::UPGRADE); - } - - // render message - { - // output buffer - let buffer = self.buffer.as_mut(); - - // status line - writeln!( - Writer(buffer), - "{} {} {:?}\r", - msg.method(), - msg.uri() - .path_and_query() - .map(|u| u.as_str()) - .unwrap_or("/"), - msg.version() - ).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - - // write headers - if let Body::Binary(ref bytes) = *msg.body() { - buffer.reserve(msg.headers().len() * AVERAGE_HEADER_SIZE + bytes.len()); - } else { - buffer.reserve(msg.headers().len() * AVERAGE_HEADER_SIZE); - } - - for (key, value) in msg.headers() { - let v = value.as_ref(); - let k = key.as_str().as_bytes(); - buffer.reserve(k.len() + v.len() + 4); - buffer.put_slice(k); - buffer.put_slice(b": "); - buffer.put_slice(v); - buffer.put_slice(b"\r\n"); - } - - // set date header - if !msg.headers().contains_key(DATE) { - buffer.extend_from_slice(b"date: "); - set_date(buffer); - buffer.extend_from_slice(b"\r\n\r\n"); - } else { - buffer.extend_from_slice(b"\r\n"); - } - } - self.headers_size = self.buffer.len() as u32; - - if msg.body().is_binary() { - if let Body::Binary(bytes) = msg.replace_body(Body::Empty) { - self.written += bytes.len() as u64; - self.buffer.write(bytes.as_ref())?; - } - } else { - self.buffer_capacity = msg.write_buffer_capacity(); - } - Ok(()) - } - - pub fn write(&mut self, payload: &[u8]) -> io::Result { - self.written += payload.len() as u64; - if !self.flags.contains(Flags::DISCONNECTED) { - self.buffer.write(payload)?; - } - - if self.buffer.len() > self.buffer_capacity { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - pub fn write_eof(&mut self) -> io::Result<()> { - if self.buffer.write_eof()? { - Ok(()) - } else { - Err(io::Error::new( - io::ErrorKind::Other, - "Last payload item, but eof is not reached", - )) - } - } - - #[inline] - pub fn poll_completed( - &mut self, stream: &mut T, shutdown: bool, - ) -> Poll<(), io::Error> { - match self.write_to_stream(stream) { - Ok(WriterState::Done) => { - if shutdown { - stream.shutdown() - } else { - Ok(Async::Ready(())) - } - } - Ok(WriterState::Pause) => Ok(Async::NotReady), - Err(err) => Err(err), - } - } -} - -fn content_encoder(buf: BytesMut, req: &mut ClientRequest) -> Output { - let version = req.version(); - let mut body = req.replace_body(Body::Empty); - let mut encoding = req.content_encoding(); - - let transfer = match body { - Body::Empty => { - req.headers_mut().remove(CONTENT_LENGTH); - return Output::Empty(buf); - } - Body::Binary(ref mut bytes) => { - #[cfg(any(feature = "flate2", feature = "brotli"))] - { - if encoding.is_compression() { - let mut tmp = BytesMut::new(); - let mut transfer = TransferEncoding::eof(tmp); - let mut enc = match encoding { - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => ContentEncoder::Deflate( - ZlibEncoder::new(transfer, Compression::default()), - ), - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => ContentEncoder::Gzip(GzEncoder::new( - transfer, - Compression::default(), - )), - #[cfg(feature = "brotli")] - ContentEncoding::Br => { - ContentEncoder::Br(BrotliEncoder::new(transfer, 5)) - } - ContentEncoding::Auto | ContentEncoding::Identity => { - unreachable!() - } - }; - // TODO return error! - let _ = enc.write(bytes.as_ref()); - let _ = enc.write_eof(); - *bytes = Binary::from(enc.buf_mut().take()); - - req.headers_mut().insert( - CONTENT_ENCODING, - HeaderValue::from_static(encoding.as_str()), - ); - encoding = ContentEncoding::Identity; - } - let mut b = BytesMut::new(); - let _ = write!(b, "{}", bytes.len()); - req.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::try_from(b.freeze()).unwrap()); - TransferEncoding::eof(buf) - } - #[cfg(not(any(feature = "flate2", feature = "brotli")))] - { - let mut b = BytesMut::new(); - let _ = write!(b, "{}", bytes.len()); - req.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::try_from(b.freeze()).unwrap()); - TransferEncoding::eof(buf) - } - } - Body::Streaming(_) | Body::Actor(_) => { - if req.upgrade() { - if version == Version::HTTP_2 { - error!("Connection upgrade is forbidden for HTTP/2"); - } else { - req.headers_mut() - .insert(CONNECTION, HeaderValue::from_static("upgrade")); - } - if encoding != ContentEncoding::Identity { - encoding = ContentEncoding::Identity; - req.headers_mut().remove(CONTENT_ENCODING); - } - TransferEncoding::eof(buf) - } else { - streaming_encoding(buf, version, req) - } - } - }; - - if encoding.is_compression() { - req.headers_mut().insert( - CONTENT_ENCODING, - HeaderValue::from_static(encoding.as_str()), - ); - } - - req.replace_body(body); - let enc = match encoding { - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => { - ContentEncoder::Deflate(ZlibEncoder::new(transfer, Compression::default())) - } - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => { - ContentEncoder::Gzip(GzEncoder::new(transfer, Compression::default())) - } - #[cfg(feature = "brotli")] - ContentEncoding::Br => ContentEncoder::Br(BrotliEncoder::new(transfer, 5)), - ContentEncoding::Identity | ContentEncoding::Auto => return Output::TE(transfer), - }; - Output::Encoder(enc) -} - -fn streaming_encoding( - buf: BytesMut, version: Version, req: &mut ClientRequest, -) -> TransferEncoding { - if req.chunked() { - // Enable transfer encoding - req.headers_mut().remove(CONTENT_LENGTH); - if version == Version::HTTP_2 { - req.headers_mut().remove(TRANSFER_ENCODING); - TransferEncoding::eof(buf) - } else { - req.headers_mut() - .insert(TRANSFER_ENCODING, HeaderValue::from_static("chunked")); - TransferEncoding::chunked(buf) - } - } else { - // if Content-Length is specified, then use it as length hint - let (len, chunked) = if let Some(len) = req.headers().get(CONTENT_LENGTH) { - // Content-Length - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - (Some(len), false) - } else { - error!("illegal Content-Length: {:?}", len); - (None, false) - } - } else { - error!("illegal Content-Length: {:?}", len); - (None, false) - } - } else { - (None, true) - }; - - if !chunked { - if let Some(len) = len { - TransferEncoding::length(len, buf) - } else { - TransferEncoding::eof(buf) - } - } else { - // Enable transfer encoding - match version { - Version::HTTP_11 => { - req.headers_mut() - .insert(TRANSFER_ENCODING, HeaderValue::from_static("chunked")); - TransferEncoding::chunked(buf) - } - _ => { - req.headers_mut().remove(TRANSFER_ENCODING); - TransferEncoding::eof(buf) - } - } - } - } -} - -// "Sun, 06 Nov 1994 08:49:37 GMT".len() -pub const DATE_VALUE_LENGTH: usize = 29; - -fn set_date(dst: &mut BytesMut) { - CACHED.with(|cache| { - let mut cache = cache.borrow_mut(); - let now = time::get_time(); - if now > cache.next_update { - cache.update(now); - } - dst.extend_from_slice(cache.buffer()); - }) -} - -struct CachedDate { - bytes: [u8; DATE_VALUE_LENGTH], - next_update: time::Timespec, -} - -thread_local!(static CACHED: RefCell = RefCell::new(CachedDate { - bytes: [0; DATE_VALUE_LENGTH], - next_update: time::Timespec::new(0, 0), -})); - -impl CachedDate { - fn buffer(&self) -> &[u8] { - &self.bytes[..] - } - - fn update(&mut self, now: time::Timespec) { - write!(&mut self.bytes[..], "{}", time::at_utc(now).rfc822()).unwrap(); - self.next_update = now + Duration::seconds(1); - self.next_update.nsec = 0; - } -} diff --git a/src/context.rs b/src/context.rs deleted file mode 100644 index 71a5af2d8..000000000 --- a/src/context.rs +++ /dev/null @@ -1,294 +0,0 @@ -extern crate actix; - -use futures::sync::oneshot; -use futures::sync::oneshot::Sender; -use futures::{Async, Future, Poll}; -use smallvec::SmallVec; -use std::marker::PhantomData; - -use self::actix::dev::{ - AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope, -}; -use self::actix::fut::ActorFuture; -use self::actix::{ - Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message, SpawnHandle, -}; - -use body::{Binary, Body}; -use error::{Error, ErrorInternalServerError}; -use httprequest::HttpRequest; - -pub trait ActorHttpContext: 'static { - fn disconnected(&mut self); - fn poll(&mut self) -> Poll>, Error>; -} - -#[derive(Debug)] -pub enum Frame { - Chunk(Option), - Drain(oneshot::Sender<()>), -} - -impl Frame { - pub fn len(&self) -> usize { - match *self { - Frame::Chunk(Some(ref bin)) => bin.len(), - _ => 0, - } - } -} - -/// Execution context for http actors -pub struct HttpContext -where - A: Actor>, -{ - inner: ContextParts, - stream: Option>, - request: HttpRequest, - disconnected: bool, -} - -impl ActorContext for HttpContext -where - A: Actor, -{ - fn stop(&mut self) { - self.inner.stop(); - } - fn terminate(&mut self) { - self.inner.terminate() - } - fn state(&self) -> ActorState { - self.inner.state() - } -} - -impl AsyncContext for HttpContext -where - A: Actor, -{ - #[inline] - fn spawn(&mut self, fut: F) -> SpawnHandle - where - F: ActorFuture + 'static, - { - self.inner.spawn(fut) - } - #[inline] - fn wait(&mut self, fut: F) - where - F: ActorFuture + 'static, - { - self.inner.wait(fut) - } - #[doc(hidden)] - #[inline] - fn waiting(&self) -> bool { - self.inner.waiting() - || self.inner.state() == ActorState::Stopping - || self.inner.state() == ActorState::Stopped - } - #[inline] - fn cancel_future(&mut self, handle: SpawnHandle) -> bool { - self.inner.cancel_future(handle) - } - #[inline] - fn address(&self) -> Addr { - self.inner.address() - } -} - -impl HttpContext -where - A: Actor, -{ - #[inline] - /// Create a new HTTP Context from a request and an actor - pub fn create(req: HttpRequest, actor: A) -> Body { - let mb = Mailbox::default(); - let ctx = HttpContext { - inner: ContextParts::new(mb.sender_producer()), - stream: None, - request: req, - disconnected: false, - }; - Body::Actor(Box::new(HttpContextFut::new(ctx, actor, mb))) - } - - /// Create a new HTTP Context - pub fn with_factory(req: HttpRequest, f: F) -> Body - where - F: FnOnce(&mut Self) -> A + 'static, - { - let mb = Mailbox::default(); - let mut ctx = HttpContext { - inner: ContextParts::new(mb.sender_producer()), - stream: None, - request: req, - disconnected: false, - }; - - let act = f(&mut ctx); - Body::Actor(Box::new(HttpContextFut::new(ctx, act, mb))) - } -} - -impl HttpContext -where - A: Actor, -{ - /// Shared application state - #[inline] - pub fn state(&self) -> &S { - self.request.state() - } - - /// Incoming request - #[inline] - pub fn request(&mut self) -> &mut HttpRequest { - &mut self.request - } - - /// Write payload - #[inline] - pub fn write>(&mut self, data: B) { - if !self.disconnected { - self.add_frame(Frame::Chunk(Some(data.into()))); - } else { - warn!("Trying to write to disconnected response"); - } - } - - /// Indicate end of streaming payload. Also this method calls `Self::close`. - #[inline] - pub fn write_eof(&mut self) { - self.add_frame(Frame::Chunk(None)); - } - - /// Returns drain future - pub fn drain(&mut self) -> Drain { - let (tx, rx) = oneshot::channel(); - self.add_frame(Frame::Drain(tx)); - Drain::new(rx) - } - - /// Check if connection still open - #[inline] - pub fn connected(&self) -> bool { - !self.disconnected - } - - #[inline] - fn add_frame(&mut self, frame: Frame) { - if self.stream.is_none() { - self.stream = Some(SmallVec::new()); - } - if let Some(s) = self.stream.as_mut() { - s.push(frame) - } - } - - /// Handle of the running future - /// - /// SpawnHandle is the handle returned by `AsyncContext::spawn()` method. - pub fn handle(&self) -> SpawnHandle { - self.inner.curr_handle() - } -} - -impl AsyncContextParts for HttpContext -where - A: Actor, -{ - fn parts(&mut self) -> &mut ContextParts { - &mut self.inner - } -} - -struct HttpContextFut -where - A: Actor>, -{ - fut: ContextFut>, -} - -impl HttpContextFut -where - A: Actor>, -{ - fn new(ctx: HttpContext, act: A, mailbox: Mailbox) -> Self { - let fut = ContextFut::new(ctx, act, mailbox); - HttpContextFut { fut } - } -} - -impl ActorHttpContext for HttpContextFut -where - A: Actor>, - S: 'static, -{ - #[inline] - fn disconnected(&mut self) { - self.fut.ctx().disconnected = true; - self.fut.ctx().stop(); - } - - fn poll(&mut self) -> Poll>, Error> { - if self.fut.alive() { - match self.fut.poll() { - Ok(Async::NotReady) | Ok(Async::Ready(())) => (), - Err(_) => return Err(ErrorInternalServerError("error")), - } - } - - // frames - if let Some(data) = self.fut.ctx().stream.take() { - Ok(Async::Ready(Some(data))) - } else if self.fut.alive() { - Ok(Async::NotReady) - } else { - Ok(Async::Ready(None)) - } - } -} - -impl ToEnvelope for HttpContext -where - A: Actor> + Handler, - M: Message + Send + 'static, - M::Result: Send, -{ - fn pack(msg: M, tx: Option>) -> Envelope { - Envelope::new(msg, tx) - } -} - -/// Consume a future -pub struct Drain { - fut: oneshot::Receiver<()>, - _a: PhantomData, -} - -impl Drain { - /// Create a drain from a future - pub fn new(fut: oneshot::Receiver<()>) -> Self { - Drain { - fut, - _a: PhantomData, - } - } -} - -impl ActorFuture for Drain { - type Item = (); - type Error = (); - type Actor = A; - - #[inline] - fn poll( - &mut self, _: &mut A, _: &mut ::Context, - ) -> Poll { - self.fut.poll().map_err(|_| ()) - } -} diff --git a/src/de.rs b/src/de.rs deleted file mode 100644 index 05f8914f8..000000000 --- a/src/de.rs +++ /dev/null @@ -1,455 +0,0 @@ -use std::rc::Rc; - -use serde::de::{self, Deserializer, Error as DeError, Visitor}; - -use httprequest::HttpRequest; -use param::ParamsIter; -use uri::RESERVED_QUOTER; - -macro_rules! unsupported_type { - ($trait_fn:ident, $name:expr) => { - fn $trait_fn(self, _: V) -> Result - where V: Visitor<'de> - { - Err(de::value::Error::custom(concat!("unsupported type: ", $name))) - } - }; -} - -macro_rules! percent_decode_if_needed { - ($value:expr, $decode:expr) => { - if $decode { - if let Some(ref mut value) = RESERVED_QUOTER.requote($value.as_bytes()) { - Rc::make_mut(value).parse() - } else { - $value.parse() - } - } else { - $value.parse() - } - } -} - -macro_rules! parse_single_value { - ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { - fn $trait_fn(self, visitor: V) -> Result - where V: Visitor<'de> - { - if self.req.match_info().len() != 1 { - Err(de::value::Error::custom( - format!("wrong number of parameters: {} expected 1", - self.req.match_info().len()).as_str())) - } else { - let v_parsed = percent_decode_if_needed!(&self.req.match_info()[0], self.decode) - .map_err(|_| de::value::Error::custom( - format!("can not parse {:?} to a {}", &self.req.match_info()[0], $tp) - ))?; - visitor.$visit_fn(v_parsed) - } - } - } -} - -pub struct PathDeserializer<'de, S: 'de> { - req: &'de HttpRequest, - decode: bool, -} - -impl<'de, S: 'de> PathDeserializer<'de, S> { - pub fn new(req: &'de HttpRequest, decode: bool) -> Self { - PathDeserializer { req, decode } - } -} - -impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> { - type Error = de::value::Error; - - fn deserialize_map(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_map(ParamsDeserializer { - params: self.req.match_info().iter(), - current: None, - decode: self.decode, - }) - } - - fn deserialize_struct( - self, _: &'static str, _: &'static [&'static str], visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - self.deserialize_map(visitor) - } - - fn deserialize_unit(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_unit_struct( - self, _: &'static str, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - self.deserialize_unit(visitor) - } - - fn deserialize_newtype_struct( - self, _: &'static str, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_newtype_struct(self) - } - - fn deserialize_tuple( - self, len: usize, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - if self.req.match_info().len() < len { - Err(de::value::Error::custom( - format!( - "wrong number of parameters: {} expected {}", - self.req.match_info().len(), - len - ).as_str(), - )) - } else { - visitor.visit_seq(ParamsSeq { - params: self.req.match_info().iter(), - decode: self.decode, - }) - } - } - - fn deserialize_tuple_struct( - self, _: &'static str, len: usize, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - if self.req.match_info().len() < len { - Err(de::value::Error::custom( - format!( - "wrong number of parameters: {} expected {}", - self.req.match_info().len(), - len - ).as_str(), - )) - } else { - visitor.visit_seq(ParamsSeq { - params: self.req.match_info().iter(), - decode: self.decode, - }) - } - } - - fn deserialize_enum( - self, _: &'static str, _: &'static [&'static str], _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: enum")) - } - - fn deserialize_seq(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_seq(ParamsSeq { - params: self.req.match_info().iter(), - decode: self.decode, - }) - } - - unsupported_type!(deserialize_any, "'any'"); - unsupported_type!(deserialize_bytes, "bytes"); - unsupported_type!(deserialize_option, "Option"); - unsupported_type!(deserialize_identifier, "identifier"); - unsupported_type!(deserialize_ignored_any, "ignored_any"); - - parse_single_value!(deserialize_bool, visit_bool, "bool"); - parse_single_value!(deserialize_i8, visit_i8, "i8"); - parse_single_value!(deserialize_i16, visit_i16, "i16"); - parse_single_value!(deserialize_i32, visit_i32, "i32"); - parse_single_value!(deserialize_i64, visit_i64, "i64"); - parse_single_value!(deserialize_u8, visit_u8, "u8"); - parse_single_value!(deserialize_u16, visit_u16, "u16"); - parse_single_value!(deserialize_u32, visit_u32, "u32"); - parse_single_value!(deserialize_u64, visit_u64, "u64"); - parse_single_value!(deserialize_f32, visit_f32, "f32"); - parse_single_value!(deserialize_f64, visit_f64, "f64"); - parse_single_value!(deserialize_string, visit_string, "String"); - parse_single_value!(deserialize_str, visit_string, "String"); - parse_single_value!(deserialize_byte_buf, visit_string, "String"); - parse_single_value!(deserialize_char, visit_char, "char"); - -} - -struct ParamsDeserializer<'de> { - params: ParamsIter<'de>, - current: Option<(&'de str, &'de str)>, - decode: bool, -} - -impl<'de> de::MapAccess<'de> for ParamsDeserializer<'de> { - type Error = de::value::Error; - - fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> - where - K: de::DeserializeSeed<'de>, - { - self.current = self.params.next().map(|ref item| (item.0, item.1)); - match self.current { - Some((key, _)) => Ok(Some(seed.deserialize(Key { key })?)), - None => Ok(None), - } - } - - fn next_value_seed(&mut self, seed: V) -> Result - where - V: de::DeserializeSeed<'de>, - { - if let Some((_, value)) = self.current.take() { - seed.deserialize(Value { value, decode: self.decode }) - } else { - Err(de::value::Error::custom("unexpected item")) - } - } -} - -struct Key<'de> { - key: &'de str, -} - -impl<'de> Deserializer<'de> for Key<'de> { - type Error = de::value::Error; - - fn deserialize_identifier(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_str(self.key) - } - - fn deserialize_any(self, _visitor: V) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("Unexpected")) - } - - forward_to_deserialize_any! { - bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes - byte_buf option unit unit_struct newtype_struct seq tuple - tuple_struct map struct enum ignored_any - } -} - -macro_rules! parse_value { - ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { - fn $trait_fn(self, visitor: V) -> Result - where V: Visitor<'de> - { - let v_parsed = percent_decode_if_needed!(&self.value, self.decode) - .map_err(|_| de::value::Error::custom( - format!("can not parse {:?} to a {}", &self.value, $tp) - ))?; - visitor.$visit_fn(v_parsed) - } - } -} - -struct Value<'de> { - value: &'de str, - decode: bool, -} - -impl<'de> Deserializer<'de> for Value<'de> { - type Error = de::value::Error; - - parse_value!(deserialize_bool, visit_bool, "bool"); - parse_value!(deserialize_i8, visit_i8, "i8"); - parse_value!(deserialize_i16, visit_i16, "i16"); - parse_value!(deserialize_i32, visit_i32, "i16"); - parse_value!(deserialize_i64, visit_i64, "i64"); - parse_value!(deserialize_u8, visit_u8, "u8"); - parse_value!(deserialize_u16, visit_u16, "u16"); - parse_value!(deserialize_u32, visit_u32, "u32"); - parse_value!(deserialize_u64, visit_u64, "u64"); - parse_value!(deserialize_f32, visit_f32, "f32"); - parse_value!(deserialize_f64, visit_f64, "f64"); - parse_value!(deserialize_string, visit_string, "String"); - parse_value!(deserialize_byte_buf, visit_string, "String"); - parse_value!(deserialize_char, visit_char, "char"); - - fn deserialize_ignored_any(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_unit(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_unit_struct( - self, _: &'static str, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_bytes(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_borrowed_bytes(self.value.as_bytes()) - } - - fn deserialize_str(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_borrowed_str(self.value) - } - - fn deserialize_option(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_some(self) - } - - fn deserialize_enum( - self, _: &'static str, _: &'static [&'static str], visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_enum(ValueEnum { value: self.value }) - } - - fn deserialize_newtype_struct( - self, _: &'static str, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_newtype_struct(self) - } - - fn deserialize_tuple(self, _: usize, _: V) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: tuple")) - } - - fn deserialize_struct( - self, _: &'static str, _: &'static [&'static str], _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: struct")) - } - - fn deserialize_tuple_struct( - self, _: &'static str, _: usize, _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: tuple struct")) - } - - unsupported_type!(deserialize_any, "any"); - unsupported_type!(deserialize_seq, "seq"); - unsupported_type!(deserialize_map, "map"); - unsupported_type!(deserialize_identifier, "identifier"); -} - -struct ParamsSeq<'de> { - params: ParamsIter<'de>, - decode: bool, -} - -impl<'de> de::SeqAccess<'de> for ParamsSeq<'de> { - type Error = de::value::Error; - - fn next_element_seed(&mut self, seed: T) -> Result, Self::Error> - where - T: de::DeserializeSeed<'de>, - { - match self.params.next() { - Some(item) => Ok(Some(seed.deserialize(Value { value: item.1, decode: self.decode })?)), - None => Ok(None), - } - } -} - -struct ValueEnum<'de> { - value: &'de str, -} - -impl<'de> de::EnumAccess<'de> for ValueEnum<'de> { - type Error = de::value::Error; - type Variant = UnitVariant; - - fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error> - where - V: de::DeserializeSeed<'de>, - { - Ok((seed.deserialize(Key { key: self.value })?, UnitVariant)) - } -} - -struct UnitVariant; - -impl<'de> de::VariantAccess<'de> for UnitVariant { - type Error = de::value::Error; - - fn unit_variant(self) -> Result<(), Self::Error> { - Ok(()) - } - - fn newtype_variant_seed(self, _seed: T) -> Result - where - T: de::DeserializeSeed<'de>, - { - Err(de::value::Error::custom("not supported")) - } - - fn tuple_variant(self, _len: usize, _visitor: V) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("not supported")) - } - - fn struct_variant( - self, _: &'static [&'static str], _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("not supported")) - } -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index f4ea981c0..000000000 --- a/src/error.rs +++ /dev/null @@ -1,1426 +0,0 @@ -//! Error and Result module -use std::io::Error as IoError; -use std::str::Utf8Error; -use std::string::FromUtf8Error; -use std::sync::Mutex; -use std::{fmt, io, result}; - -use actix::{MailboxError, SendError}; -use cookie; -use failure::{self, Backtrace, Fail}; -use futures::Canceled; -use http::uri::InvalidUri; -use http::{header, Error as HttpError, StatusCode}; -use http2::Error as Http2Error; -use httparse; -use serde::de::value::Error as DeError; -use serde_json::error::Error as JsonError; -use serde_urlencoded::ser::Error as FormError; -use tokio_timer::Error as TimerError; -pub use url::ParseError as UrlParseError; - -// re-exports -pub use cookie::ParseError as CookieParseError; - -use handler::Responder; -use httprequest::HttpRequest; -use httpresponse::{HttpResponse, HttpResponseParts}; - -/// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html) -/// for actix web operations -/// -/// This typedef is generally used to avoid writing out -/// `actix_web::error::Error` directly and is otherwise a direct mapping to -/// `Result`. -pub type Result = result::Result; - -/// General purpose actix web error. -/// -/// An actix web error is used to carry errors from `failure` or `std::error` -/// through actix in a convenient way. It can be created through -/// converting errors with `into()`. -/// -/// Whenever it is created from an external object a response error is created -/// for it that can be used to create an http response from it this means that -/// if you have access to an actix `Error` you can always get a -/// `ResponseError` reference from it. -pub struct Error { - cause: Box, - backtrace: Option, -} - -impl Error { - /// Deprecated way to reference the underlying response error. - #[deprecated( - since = "0.6.0", - note = "please use `Error::as_response_error()` instead" - )] - pub fn cause(&self) -> &ResponseError { - self.cause.as_ref() - } - - /// Returns a reference to the underlying cause of this `Error` as `Fail` - pub fn as_fail(&self) -> &Fail { - self.cause.as_fail() - } - - /// Returns the reference to the underlying `ResponseError`. - pub fn as_response_error(&self) -> &ResponseError { - self.cause.as_ref() - } - - /// Returns a reference to the Backtrace carried by this error, if it - /// carries one. - /// - /// This uses the same `Backtrace` type that `failure` uses. - pub fn backtrace(&self) -> &Backtrace { - if let Some(bt) = self.cause.backtrace() { - bt - } else { - self.backtrace.as_ref().unwrap() - } - } - - /// Attempts to downcast this `Error` to a particular `Fail` type by - /// reference. - /// - /// If the underlying error is not of type `T`, this will return `None`. - pub fn downcast_ref(&self) -> Option<&T> { - // in the most trivial way the cause is directly of the requested type. - if let Some(rv) = Fail::downcast_ref(self.cause.as_fail()) { - return Some(rv); - } - - // in the more complex case the error has been constructed from a failure - // error. This happens because we implement From by - // calling compat() and then storing it here. In failure this is - // represented by a failure::Error being wrapped in a failure::Compat. - // - // So we first downcast into that compat, to then further downcast through - // the failure's Error downcasting system into the original failure. - let compat: Option<&failure::Compat> = - Fail::downcast_ref(self.cause.as_fail()); - compat.and_then(|e| e.get_ref().downcast_ref()) - } -} - -/// Helper trait to downcast a response error into a fail. -/// -/// This is currently not exposed because it's unclear if this is the best way -/// to achieve the downcasting on `Error` for which this is needed. -#[doc(hidden)] -pub trait InternalResponseErrorAsFail { - #[doc(hidden)] - fn as_fail(&self) -> &Fail; - #[doc(hidden)] - fn as_mut_fail(&mut self) -> &mut Fail; -} - -#[doc(hidden)] -impl InternalResponseErrorAsFail for T { - fn as_fail(&self) -> &Fail { - self - } - fn as_mut_fail(&mut self) -> &mut Fail { - self - } -} - -/// Error that can be converted to `HttpResponse` -pub trait ResponseError: Fail + InternalResponseErrorAsFail { - /// Create response for error - /// - /// Internal server error is generated by default. - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR) - } -} - -impl ResponseError for SendError -where T: Send + Sync + 'static { -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.cause, f) - } -} - -impl fmt::Debug for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(bt) = self.cause.backtrace() { - write!(f, "{:?}\n\n{:?}", &self.cause, bt) - } else { - write!( - f, - "{:?}\n\n{:?}", - &self.cause, - self.backtrace.as_ref().unwrap() - ) - } - } -} - -/// Convert `Error` to a `HttpResponse` instance -impl From for HttpResponse { - fn from(err: Error) -> Self { - HttpResponse::from_error(err) - } -} - -/// `Error` for any error that implements `ResponseError` -impl From for Error { - fn from(err: T) -> Error { - let backtrace = if err.backtrace().is_none() { - Some(Backtrace::new()) - } else { - None - }; - Error { - cause: Box::new(err), - backtrace, - } - } -} - -/// Compatibility for `failure::Error` -impl ResponseError for failure::Compat where - T: fmt::Display + fmt::Debug + Sync + Send + 'static -{} - -impl From for Error { - fn from(err: failure::Error) -> Error { - err.compat().into() - } -} - -/// `InternalServerError` for `JsonError` -impl ResponseError for JsonError {} - -/// `InternalServerError` for `FormError` -impl ResponseError for FormError {} - -/// `InternalServerError` for `TimerError` -impl ResponseError for TimerError {} - -/// `InternalServerError` for `UrlParseError` -impl ResponseError for UrlParseError {} - -/// Return `BAD_REQUEST` for `de::value::Error` -impl ResponseError for DeError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// Return `BAD_REQUEST` for `Utf8Error` -impl ResponseError for Utf8Error { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// Return `InternalServerError` for `HttpError`, -/// Response generation can return `HttpError`, so it is internal error -impl ResponseError for HttpError {} - -/// Return `InternalServerError` for `io::Error` -impl ResponseError for io::Error { - fn error_response(&self) -> HttpResponse { - match self.kind() { - io::ErrorKind::NotFound => HttpResponse::new(StatusCode::NOT_FOUND), - io::ErrorKind::PermissionDenied => HttpResponse::new(StatusCode::FORBIDDEN), - _ => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR), - } - } -} - -/// `BadRequest` for `InvalidHeaderValue` -impl ResponseError for header::InvalidHeaderValue { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// `BadRequest` for `InvalidHeaderValue` -impl ResponseError for header::InvalidHeaderValueBytes { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// `InternalServerError` for `futures::Canceled` -impl ResponseError for Canceled {} - -/// `InternalServerError` for `actix::MailboxError` -impl ResponseError for MailboxError {} - -/// A set of errors that can occur during parsing HTTP streams -#[derive(Fail, Debug)] -pub enum ParseError { - /// An invalid `Method`, such as `GE.T`. - #[fail(display = "Invalid Method specified")] - Method, - /// An invalid `Uri`, such as `exam ple.domain`. - #[fail(display = "Uri error: {}", _0)] - Uri(InvalidUri), - /// An invalid `HttpVersion`, such as `HTP/1.1` - #[fail(display = "Invalid HTTP version specified")] - Version, - /// An invalid `Header`. - #[fail(display = "Invalid Header provided")] - Header, - /// A message head is too large to be reasonable. - #[fail(display = "Message head is too large")] - TooLarge, - /// A message reached EOF, but is not complete. - #[fail(display = "Message is incomplete")] - Incomplete, - /// An invalid `Status`, such as `1337 ELITE`. - #[fail(display = "Invalid Status provided")] - Status, - /// A timeout occurred waiting for an IO event. - #[allow(dead_code)] - #[fail(display = "Timeout")] - Timeout, - /// An `io::Error` that occurred while trying to read or write to a network - /// stream. - #[fail(display = "IO error: {}", _0)] - Io(#[cause] IoError), - /// Parsing a field as string failed - #[fail(display = "UTF8 error: {}", _0)] - Utf8(#[cause] Utf8Error), -} - -/// Return `BadRequest` for `ParseError` -impl ResponseError for ParseError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -impl From for ParseError { - fn from(err: IoError) -> ParseError { - ParseError::Io(err) - } -} - -impl From for ParseError { - fn from(err: InvalidUri) -> ParseError { - ParseError::Uri(err) - } -} - -impl From for ParseError { - fn from(err: Utf8Error) -> ParseError { - ParseError::Utf8(err) - } -} - -impl From for ParseError { - fn from(err: FromUtf8Error) -> ParseError { - ParseError::Utf8(err.utf8_error()) - } -} - -impl From for ParseError { - fn from(err: httparse::Error) -> ParseError { - match err { - httparse::Error::HeaderName - | httparse::Error::HeaderValue - | httparse::Error::NewLine - | httparse::Error::Token => ParseError::Header, - httparse::Error::Status => ParseError::Status, - httparse::Error::TooManyHeaders => ParseError::TooLarge, - httparse::Error::Version => ParseError::Version, - } - } -} - -#[derive(Fail, Debug)] -/// A set of errors that can occur during payload parsing -pub enum PayloadError { - /// A payload reached EOF, but is not complete. - #[fail(display = "A payload reached EOF, but is not complete.")] - Incomplete, - /// Content encoding stream corruption - #[fail(display = "Can not decode content-encoding.")] - EncodingCorrupted, - /// A payload reached size limit. - #[fail(display = "A payload reached size limit.")] - Overflow, - /// A payload length is unknown. - #[fail(display = "A payload length is unknown.")] - UnknownLength, - /// Io error - #[fail(display = "{}", _0)] - Io(#[cause] IoError), - /// Http2 error - #[fail(display = "{}", _0)] - Http2(#[cause] Http2Error), -} - -impl From for PayloadError { - fn from(err: IoError) -> PayloadError { - PayloadError::Io(err) - } -} - -/// `PayloadError` returns two possible results: -/// -/// - `Overflow` returns `PayloadTooLarge` -/// - Other errors returns `BadRequest` -impl ResponseError for PayloadError { - fn error_response(&self) -> HttpResponse { - match *self { - PayloadError::Overflow => HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE), - _ => HttpResponse::new(StatusCode::BAD_REQUEST), - } - } -} - -/// Return `BadRequest` for `cookie::ParseError` -impl ResponseError for cookie::ParseError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// A set of errors that can occur during parsing multipart streams -#[derive(Fail, Debug)] -pub enum MultipartError { - /// Content-Type header is not found - #[fail(display = "No Content-type header found")] - NoContentType, - /// Can not parse Content-Type header - #[fail(display = "Can not parse Content-Type header")] - ParseContentType, - /// Multipart boundary is not found - #[fail(display = "Multipart boundary is not found")] - Boundary, - /// Multipart stream is incomplete - #[fail(display = "Multipart stream is incomplete")] - Incomplete, - /// Error during field parsing - #[fail(display = "{}", _0)] - Parse(#[cause] ParseError), - /// Payload error - #[fail(display = "{}", _0)] - Payload(#[cause] PayloadError), -} - -impl From for MultipartError { - fn from(err: ParseError) -> MultipartError { - MultipartError::Parse(err) - } -} - -impl From for MultipartError { - fn from(err: PayloadError) -> MultipartError { - MultipartError::Payload(err) - } -} - -/// Return `BadRequest` for `MultipartError` -impl ResponseError for MultipartError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// Error during handling `Expect` header -#[derive(Fail, PartialEq, Debug)] -pub enum ExpectError { - /// Expect header value can not be converted to utf8 - #[fail(display = "Expect header value can not be converted to utf8")] - Encoding, - /// Unknown expect value - #[fail(display = "Unknown expect value")] - UnknownExpect, -} - -impl ResponseError for ExpectError { - fn error_response(&self) -> HttpResponse { - HttpResponse::with_body(StatusCode::EXPECTATION_FAILED, "Unknown Expect") - } -} - -/// A set of error that can occure during parsing content type -#[derive(Fail, PartialEq, Debug)] -pub enum ContentTypeError { - /// Can not parse content type - #[fail(display = "Can not parse content type")] - ParseError, - /// Unknown content encoding - #[fail(display = "Unknown content encoding")] - UnknownEncoding, -} - -/// Return `BadRequest` for `ContentTypeError` -impl ResponseError for ContentTypeError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// A set of errors that can occur during parsing urlencoded payloads -#[derive(Fail, Debug)] -pub enum UrlencodedError { - /// Can not decode chunked transfer encoding - #[fail(display = "Can not decode chunked transfer encoding")] - Chunked, - /// Payload size is bigger than allowed. (default: 256kB) - #[fail( - display = "Urlencoded payload size is bigger than allowed. (default: 256kB)" - )] - Overflow, - /// Payload size is now known - #[fail(display = "Payload size is now known")] - UnknownLength, - /// Content type error - #[fail(display = "Content type error")] - ContentType, - /// Parse error - #[fail(display = "Parse error")] - Parse, - /// Payload error - #[fail(display = "Error that occur during reading payload: {}", _0)] - Payload(#[cause] PayloadError), -} - -/// Return `BadRequest` for `UrlencodedError` -impl ResponseError for UrlencodedError { - fn error_response(&self) -> HttpResponse { - match *self { - UrlencodedError::Overflow => { - HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) - } - UrlencodedError::UnknownLength => { - HttpResponse::new(StatusCode::LENGTH_REQUIRED) - } - _ => HttpResponse::new(StatusCode::BAD_REQUEST), - } - } -} - -impl From for UrlencodedError { - fn from(err: PayloadError) -> UrlencodedError { - UrlencodedError::Payload(err) - } -} - -/// A set of errors that can occur during parsing json payloads -#[derive(Fail, Debug)] -pub enum JsonPayloadError { - /// Payload size is bigger than allowed. (default: 256kB) - #[fail(display = "Json payload size is bigger than allowed. (default: 256kB)")] - Overflow, - /// Content type error - #[fail(display = "Content type error")] - ContentType, - /// Deserialize error - #[fail(display = "Json deserialize error: {}", _0)] - Deserialize(#[cause] JsonError), - /// Payload error - #[fail(display = "Error that occur during reading payload: {}", _0)] - Payload(#[cause] PayloadError), -} - -/// Return `BadRequest` for `UrlencodedError` -impl ResponseError for JsonPayloadError { - fn error_response(&self) -> HttpResponse { - match *self { - JsonPayloadError::Overflow => { - HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) - } - _ => HttpResponse::new(StatusCode::BAD_REQUEST), - } - } -} - -impl From for JsonPayloadError { - fn from(err: PayloadError) -> JsonPayloadError { - JsonPayloadError::Payload(err) - } -} - -impl From for JsonPayloadError { - fn from(err: JsonError) -> JsonPayloadError { - JsonPayloadError::Deserialize(err) - } -} - -/// Error type returned when reading body as lines. -pub enum ReadlinesError { - /// Error when decoding a line. - EncodingError, - /// Payload error. - PayloadError(PayloadError), - /// Line limit exceeded. - LimitOverflow, - /// ContentType error. - ContentTypeError(ContentTypeError), -} - -impl From for ReadlinesError { - fn from(err: PayloadError) -> Self { - ReadlinesError::PayloadError(err) - } -} - -impl From for ReadlinesError { - fn from(err: ContentTypeError) -> Self { - ReadlinesError::ContentTypeError(err) - } -} - -/// Errors which can occur when attempting to interpret a segment string as a -/// valid path segment. -#[derive(Fail, Debug, PartialEq)] -pub enum UriSegmentError { - /// The segment started with the wrapped invalid character. - #[fail(display = "The segment started with the wrapped invalid character")] - BadStart(char), - /// The segment contained the wrapped invalid character. - #[fail(display = "The segment contained the wrapped invalid character")] - BadChar(char), - /// The segment ended with the wrapped invalid character. - #[fail(display = "The segment ended with the wrapped invalid character")] - BadEnd(char), -} - -/// Return `BadRequest` for `UriSegmentError` -impl ResponseError for UriSegmentError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// Errors which can occur when attempting to generate resource uri. -#[derive(Fail, Debug, PartialEq)] -pub enum UrlGenerationError { - /// Resource not found - #[fail(display = "Resource not found")] - ResourceNotFound, - /// Not all path pattern covered - #[fail(display = "Not all path pattern covered")] - NotEnoughElements, - /// URL parse error - #[fail(display = "{}", _0)] - ParseError(#[cause] UrlParseError), -} - -/// `InternalServerError` for `UrlGeneratorError` -impl ResponseError for UrlGenerationError {} - -impl From for UrlGenerationError { - fn from(err: UrlParseError) -> Self { - UrlGenerationError::ParseError(err) - } -} - -/// Errors which can occur when serving static files. -#[derive(Fail, Debug, PartialEq)] -pub enum StaticFileError { - /// Path is not a directory - #[fail(display = "Path is not a directory. Unable to serve static files")] - IsNotDirectory, - /// Cannot render directory - #[fail(display = "Unable to render directory without index file")] - IsDirectory, -} - -/// Return `NotFound` for `StaticFileError` -impl ResponseError for StaticFileError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::NOT_FOUND) - } -} - -/// Helper type that can wrap any error and generate custom response. -/// -/// In following example any `io::Error` will be converted into "BAD REQUEST" -/// response as opposite to *INTERNAL SERVER ERROR* which is defined by -/// default. -/// -/// ```rust -/// # extern crate actix_web; -/// # use actix_web::*; -/// use actix_web::fs::NamedFile; -/// -/// fn index(req: HttpRequest) -> Result { -/// let f = NamedFile::open("test.txt").map_err(error::ErrorBadRequest)?; -/// Ok(f) -/// } -/// # fn main() {} -/// ``` -pub struct InternalError { - cause: T, - status: InternalErrorType, - backtrace: Backtrace, -} - -enum InternalErrorType { - Status(StatusCode), - Response(Box>>), -} - -impl InternalError { - /// Create `InternalError` instance - pub fn new(cause: T, status: StatusCode) -> Self { - InternalError { - cause, - status: InternalErrorType::Status(status), - backtrace: Backtrace::new(), - } - } - - /// Create `InternalError` with predefined `HttpResponse`. - pub fn from_response(cause: T, response: HttpResponse) -> Self { - let resp = response.into_parts(); - InternalError { - cause, - status: InternalErrorType::Response(Box::new(Mutex::new(Some(resp)))), - backtrace: Backtrace::new(), - } - } -} - -impl Fail for InternalError -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - fn backtrace(&self) -> Option<&Backtrace> { - Some(&self.backtrace) - } -} - -impl fmt::Debug for InternalError -where - T: Send + Sync + fmt::Debug + 'static, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(&self.cause, f) - } -} - -impl fmt::Display for InternalError -where - T: Send + Sync + fmt::Display + 'static, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.cause, f) - } -} - -impl ResponseError for InternalError -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - fn error_response(&self) -> HttpResponse { - match self.status { - InternalErrorType::Status(st) => HttpResponse::new(st), - InternalErrorType::Response(ref resp) => { - if let Some(resp) = resp.lock().unwrap().take() { - HttpResponse::from_parts(resp) - } else { - HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR) - } - } - } - } -} - -impl Responder for InternalError -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, _: &HttpRequest) -> Result { - Err(self.into()) - } -} - -/// Helper function that creates wrapper of any error and generate *BAD -/// REQUEST* response. -#[allow(non_snake_case)] -pub fn ErrorBadRequest(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::BAD_REQUEST).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UNAUTHORIZED* response. -#[allow(non_snake_case)] -pub fn ErrorUnauthorized(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UNAUTHORIZED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *PAYMENT_REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorPaymentRequired(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PAYMENT_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate *FORBIDDEN* -/// response. -#[allow(non_snake_case)] -pub fn ErrorForbidden(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::FORBIDDEN).into() -} - -/// Helper function that creates wrapper of any error and generate *NOT FOUND* -/// response. -#[allow(non_snake_case)] -pub fn ErrorNotFound(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NOT_FOUND).into() -} - -/// Helper function that creates wrapper of any error and generate *METHOD NOT -/// ALLOWED* response. -#[allow(non_snake_case)] -pub fn ErrorMethodNotAllowed(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::METHOD_NOT_ALLOWED).into() -} - -/// Helper function that creates wrapper of any error and generate *NOT -/// ACCEPTABLE* response. -#[allow(non_snake_case)] -pub fn ErrorNotAcceptable(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NOT_ACCEPTABLE).into() -} - -/// Helper function that creates wrapper of any error and generate *PROXY -/// AUTHENTICATION REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorProxyAuthenticationRequired(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PROXY_AUTHENTICATION_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate *REQUEST -/// TIMEOUT* response. -#[allow(non_snake_case)] -pub fn ErrorRequestTimeout(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::REQUEST_TIMEOUT).into() -} - -/// Helper function that creates wrapper of any error and generate *CONFLICT* -/// response. -#[allow(non_snake_case)] -pub fn ErrorConflict(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::CONFLICT).into() -} - -/// Helper function that creates wrapper of any error and generate *GONE* -/// response. -#[allow(non_snake_case)] -pub fn ErrorGone(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::GONE).into() -} - -/// Helper function that creates wrapper of any error and generate *LENGTH -/// REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorLengthRequired(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::LENGTH_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *PRECONDITION FAILED* response. -#[allow(non_snake_case)] -pub fn ErrorPreconditionFailed(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PRECONDITION_FAILED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *PAYLOAD TOO LARGE* response. -#[allow(non_snake_case)] -pub fn ErrorPayloadTooLarge(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PAYLOAD_TOO_LARGE).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *URI TOO LONG* response. -#[allow(non_snake_case)] -pub fn ErrorUriTooLong(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::URI_TOO_LONG).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UNSUPPORTED MEDIA TYPE* response. -#[allow(non_snake_case)] -pub fn ErrorUnsupportedMediaType(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UNSUPPORTED_MEDIA_TYPE).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *RANGE NOT SATISFIABLE* response. -#[allow(non_snake_case)] -pub fn ErrorRangeNotSatisfiable(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::RANGE_NOT_SATISFIABLE).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *EXPECTATION FAILED* response. -#[allow(non_snake_case)] -pub fn ErrorExpectationFailed(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::EXPECTATION_FAILED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *IM A TEAPOT* response. -#[allow(non_snake_case)] -pub fn ErrorImATeapot(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::IM_A_TEAPOT).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *MISDIRECTED REQUEST* response. -#[allow(non_snake_case)] -pub fn ErrorMisdirectedRequest(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::MISDIRECTED_REQUEST).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UNPROCESSABLE ENTITY* response. -#[allow(non_snake_case)] -pub fn ErrorUnprocessableEntity(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UNPROCESSABLE_ENTITY).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *LOCKED* response. -#[allow(non_snake_case)] -pub fn ErrorLocked(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::LOCKED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *FAILED DEPENDENCY* response. -#[allow(non_snake_case)] -pub fn ErrorFailedDependency(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::FAILED_DEPENDENCY).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UPGRADE REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorUpgradeRequired(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UPGRADE_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *PRECONDITION REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorPreconditionRequired(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PRECONDITION_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *TOO MANY REQUESTS* response. -#[allow(non_snake_case)] -pub fn ErrorTooManyRequests(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::TOO_MANY_REQUESTS).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *REQUEST HEADER FIELDS TOO LARGE* response. -#[allow(non_snake_case)] -pub fn ErrorRequestHeaderFieldsTooLarge(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UNAVAILABLE FOR LEGAL REASONS* response. -#[allow(non_snake_case)] -pub fn ErrorUnavailableForLegalReasons(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *INTERNAL SERVER ERROR* response. -#[allow(non_snake_case)] -pub fn ErrorInternalServerError(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::INTERNAL_SERVER_ERROR).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *NOT IMPLEMENTED* response. -#[allow(non_snake_case)] -pub fn ErrorNotImplemented(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NOT_IMPLEMENTED).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *BAD GATEWAY* response. -#[allow(non_snake_case)] -pub fn ErrorBadGateway(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::BAD_GATEWAY).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *SERVICE UNAVAILABLE* response. -#[allow(non_snake_case)] -pub fn ErrorServiceUnavailable(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::SERVICE_UNAVAILABLE).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *GATEWAY TIMEOUT* response. -#[allow(non_snake_case)] -pub fn ErrorGatewayTimeout(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::GATEWAY_TIMEOUT).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *HTTP VERSION NOT SUPPORTED* response. -#[allow(non_snake_case)] -pub fn ErrorHttpVersionNotSupported(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::HTTP_VERSION_NOT_SUPPORTED).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *VARIANT ALSO NEGOTIATES* response. -#[allow(non_snake_case)] -pub fn ErrorVariantAlsoNegotiates(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::VARIANT_ALSO_NEGOTIATES).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *INSUFFICIENT STORAGE* response. -#[allow(non_snake_case)] -pub fn ErrorInsufficientStorage(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::INSUFFICIENT_STORAGE).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *LOOP DETECTED* response. -#[allow(non_snake_case)] -pub fn ErrorLoopDetected(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::LOOP_DETECTED).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *NOT EXTENDED* response. -#[allow(non_snake_case)] -pub fn ErrorNotExtended(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NOT_EXTENDED).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *NETWORK AUTHENTICATION REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorNetworkAuthenticationRequired(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into() -} - -#[cfg(test)] -mod tests { - use super::*; - use cookie::ParseError as CookieParseError; - use failure; - use http::{Error as HttpError, StatusCode}; - use httparse; - use std::env; - use std::error::Error as StdError; - use std::io; - - #[test] - #[cfg(actix_nightly)] - fn test_nightly() { - let resp: HttpResponse = - IoError::new(io::ErrorKind::Other, "test").error_response(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[test] - fn test_into_response() { - let resp: HttpResponse = ParseError::Incomplete.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - - let resp: HttpResponse = CookieParseError::EmptyName.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - - let resp: HttpResponse = MultipartError::Boundary.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - - let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into(); - let resp: HttpResponse = err.error_response(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[test] - fn test_as_fail() { - let orig = io::Error::new(io::ErrorKind::Other, "other"); - let desc = orig.description().to_owned(); - let e = ParseError::Io(orig); - assert_eq!(format!("{}", e.cause().unwrap()), desc); - } - - #[test] - fn test_backtrace() { - let e = ErrorBadRequest("err"); - let _ = e.backtrace(); - } - - #[test] - fn test_error_cause() { - let orig = io::Error::new(io::ErrorKind::Other, "other"); - let desc = orig.description().to_owned(); - let e = Error::from(orig); - assert_eq!(format!("{}", e.as_fail()), desc); - } - - #[test] - fn test_error_display() { - let orig = io::Error::new(io::ErrorKind::Other, "other"); - let desc = orig.description().to_owned(); - let e = Error::from(orig); - assert_eq!(format!("{}", e), desc); - } - - #[test] - fn test_error_http_response() { - let orig = io::Error::new(io::ErrorKind::Other, "other"); - let e = Error::from(orig); - let resp: HttpResponse = e.into(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[test] - fn test_expect_error() { - let resp: HttpResponse = ExpectError::Encoding.error_response(); - assert_eq!(resp.status(), StatusCode::EXPECTATION_FAILED); - let resp: HttpResponse = ExpectError::UnknownExpect.error_response(); - assert_eq!(resp.status(), StatusCode::EXPECTATION_FAILED); - } - - macro_rules! from { - ($from:expr => $error:pat) => { - match ParseError::from($from) { - e @ $error => { - assert!(format!("{}", e).len() >= 5); - } - e => unreachable!("{:?}", e), - } - }; - } - - macro_rules! from_and_cause { - ($from:expr => $error:pat) => { - match ParseError::from($from) { - e @ $error => { - let desc = format!("{}", e.cause().unwrap()); - assert_eq!(desc, $from.description().to_owned()); - } - _ => unreachable!("{:?}", $from), - } - }; - } - - #[test] - fn test_from() { - from_and_cause!(io::Error::new(io::ErrorKind::Other, "other") => ParseError::Io(..)); - - from!(httparse::Error::HeaderName => ParseError::Header); - from!(httparse::Error::HeaderName => ParseError::Header); - from!(httparse::Error::HeaderValue => ParseError::Header); - from!(httparse::Error::NewLine => ParseError::Header); - from!(httparse::Error::Status => ParseError::Status); - from!(httparse::Error::Token => ParseError::Header); - from!(httparse::Error::TooManyHeaders => ParseError::TooLarge); - from!(httparse::Error::Version => ParseError::Version); - } - - #[test] - fn failure_error() { - const NAME: &str = "RUST_BACKTRACE"; - let old_tb = env::var(NAME); - env::set_var(NAME, "0"); - let error = failure::err_msg("Hello!"); - let resp: Error = error.into(); - assert_eq!( - format!("{:?}", resp), - "Compat { error: ErrorMessage { msg: \"Hello!\" } }\n\n" - ); - match old_tb { - Ok(x) => env::set_var(NAME, x), - _ => env::remove_var(NAME), - } - } - - #[test] - fn test_internal_error() { - let err = InternalError::from_response( - ExpectError::Encoding, - HttpResponse::Ok().into(), - ); - let resp: HttpResponse = err.error_response(); - assert_eq!(resp.status(), StatusCode::OK); - } - - #[test] - fn test_error_downcasting_direct() { - #[derive(Debug, Fail)] - #[fail(display = "demo error")] - struct DemoError; - - impl ResponseError for DemoError {} - - let err: Error = DemoError.into(); - let err_ref: &DemoError = err.downcast_ref().unwrap(); - assert_eq!(err_ref.to_string(), "demo error"); - } - - #[test] - fn test_error_downcasting_compat() { - #[derive(Debug, Fail)] - #[fail(display = "demo error")] - struct DemoError; - - impl ResponseError for DemoError {} - - let err: Error = failure::Error::from(DemoError).into(); - let err_ref: &DemoError = err.downcast_ref().unwrap(); - assert_eq!(err_ref.to_string(), "demo error"); - } - - #[test] - fn test_error_helpers() { - let r: HttpResponse = ErrorBadRequest("err").into(); - assert_eq!(r.status(), StatusCode::BAD_REQUEST); - - let r: HttpResponse = ErrorUnauthorized("err").into(); - assert_eq!(r.status(), StatusCode::UNAUTHORIZED); - - let r: HttpResponse = ErrorPaymentRequired("err").into(); - assert_eq!(r.status(), StatusCode::PAYMENT_REQUIRED); - - let r: HttpResponse = ErrorForbidden("err").into(); - assert_eq!(r.status(), StatusCode::FORBIDDEN); - - let r: HttpResponse = ErrorNotFound("err").into(); - assert_eq!(r.status(), StatusCode::NOT_FOUND); - - let r: HttpResponse = ErrorMethodNotAllowed("err").into(); - assert_eq!(r.status(), StatusCode::METHOD_NOT_ALLOWED); - - let r: HttpResponse = ErrorNotAcceptable("err").into(); - assert_eq!(r.status(), StatusCode::NOT_ACCEPTABLE); - - let r: HttpResponse = ErrorProxyAuthenticationRequired("err").into(); - assert_eq!(r.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED); - - let r: HttpResponse = ErrorRequestTimeout("err").into(); - assert_eq!(r.status(), StatusCode::REQUEST_TIMEOUT); - - let r: HttpResponse = ErrorConflict("err").into(); - assert_eq!(r.status(), StatusCode::CONFLICT); - - let r: HttpResponse = ErrorGone("err").into(); - assert_eq!(r.status(), StatusCode::GONE); - - let r: HttpResponse = ErrorLengthRequired("err").into(); - assert_eq!(r.status(), StatusCode::LENGTH_REQUIRED); - - let r: HttpResponse = ErrorPreconditionFailed("err").into(); - assert_eq!(r.status(), StatusCode::PRECONDITION_FAILED); - - let r: HttpResponse = ErrorPayloadTooLarge("err").into(); - assert_eq!(r.status(), StatusCode::PAYLOAD_TOO_LARGE); - - let r: HttpResponse = ErrorUriTooLong("err").into(); - assert_eq!(r.status(), StatusCode::URI_TOO_LONG); - - let r: HttpResponse = ErrorUnsupportedMediaType("err").into(); - assert_eq!(r.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); - - let r: HttpResponse = ErrorRangeNotSatisfiable("err").into(); - assert_eq!(r.status(), StatusCode::RANGE_NOT_SATISFIABLE); - - let r: HttpResponse = ErrorExpectationFailed("err").into(); - assert_eq!(r.status(), StatusCode::EXPECTATION_FAILED); - - let r: HttpResponse = ErrorImATeapot("err").into(); - assert_eq!(r.status(), StatusCode::IM_A_TEAPOT); - - let r: HttpResponse = ErrorMisdirectedRequest("err").into(); - assert_eq!(r.status(), StatusCode::MISDIRECTED_REQUEST); - - let r: HttpResponse = ErrorUnprocessableEntity("err").into(); - assert_eq!(r.status(), StatusCode::UNPROCESSABLE_ENTITY); - - let r: HttpResponse = ErrorLocked("err").into(); - assert_eq!(r.status(), StatusCode::LOCKED); - - let r: HttpResponse = ErrorFailedDependency("err").into(); - assert_eq!(r.status(), StatusCode::FAILED_DEPENDENCY); - - let r: HttpResponse = ErrorUpgradeRequired("err").into(); - assert_eq!(r.status(), StatusCode::UPGRADE_REQUIRED); - - let r: HttpResponse = ErrorPreconditionRequired("err").into(); - assert_eq!(r.status(), StatusCode::PRECONDITION_REQUIRED); - - let r: HttpResponse = ErrorTooManyRequests("err").into(); - assert_eq!(r.status(), StatusCode::TOO_MANY_REQUESTS); - - let r: HttpResponse = ErrorRequestHeaderFieldsTooLarge("err").into(); - assert_eq!(r.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE); - - let r: HttpResponse = ErrorUnavailableForLegalReasons("err").into(); - assert_eq!(r.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS); - - let r: HttpResponse = ErrorInternalServerError("err").into(); - assert_eq!(r.status(), StatusCode::INTERNAL_SERVER_ERROR); - - let r: HttpResponse = ErrorNotImplemented("err").into(); - assert_eq!(r.status(), StatusCode::NOT_IMPLEMENTED); - - let r: HttpResponse = ErrorBadGateway("err").into(); - assert_eq!(r.status(), StatusCode::BAD_GATEWAY); - - let r: HttpResponse = ErrorServiceUnavailable("err").into(); - assert_eq!(r.status(), StatusCode::SERVICE_UNAVAILABLE); - - let r: HttpResponse = ErrorGatewayTimeout("err").into(); - assert_eq!(r.status(), StatusCode::GATEWAY_TIMEOUT); - - let r: HttpResponse = ErrorHttpVersionNotSupported("err").into(); - assert_eq!(r.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED); - - let r: HttpResponse = ErrorVariantAlsoNegotiates("err").into(); - assert_eq!(r.status(), StatusCode::VARIANT_ALSO_NEGOTIATES); - - let r: HttpResponse = ErrorInsufficientStorage("err").into(); - assert_eq!(r.status(), StatusCode::INSUFFICIENT_STORAGE); - - let r: HttpResponse = ErrorLoopDetected("err").into(); - assert_eq!(r.status(), StatusCode::LOOP_DETECTED); - - let r: HttpResponse = ErrorNotExtended("err").into(); - assert_eq!(r.status(), StatusCode::NOT_EXTENDED); - - let r: HttpResponse = ErrorNetworkAuthenticationRequired("err").into(); - assert_eq!(r.status(), StatusCode::NETWORK_AUTHENTICATION_REQUIRED); - } -} diff --git a/src/extensions.rs b/src/extensions.rs deleted file mode 100644 index 430b87bda..000000000 --- a/src/extensions.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::any::{Any, TypeId}; -use std::collections::HashMap; -use std::fmt; -use std::hash::{BuildHasherDefault, Hasher}; - -struct IdHasher { - id: u64, -} - -impl Default for IdHasher { - fn default() -> IdHasher { - IdHasher { id: 0 } - } -} - -impl Hasher for IdHasher { - fn write(&mut self, bytes: &[u8]) { - for &x in bytes { - self.id.wrapping_add(u64::from(x)); - } - } - - fn write_u64(&mut self, u: u64) { - self.id = u; - } - - fn finish(&self) -> u64 { - self.id - } -} - -type AnyMap = HashMap, BuildHasherDefault>; - -#[derive(Default)] -/// A type map of request extensions. -pub struct Extensions { - map: AnyMap, -} - -impl Extensions { - /// Create an empty `Extensions`. - #[inline] - pub fn new() -> Extensions { - Extensions { - map: HashMap::default(), - } - } - - /// Insert a type into this `Extensions`. - /// - /// If a extension of this type already existed, it will - /// be returned. - pub fn insert(&mut self, val: T) { - self.map.insert(TypeId::of::(), Box::new(val)); - } - - /// Get a reference to a type previously inserted on this `Extensions`. - pub fn get(&self) -> Option<&T> { - self.map - .get(&TypeId::of::()) - .and_then(|boxed| (&**boxed as &(Any + 'static)).downcast_ref()) - } - - /// Get a mutable reference to a type previously inserted on this `Extensions`. - pub fn get_mut(&mut self) -> Option<&mut T> { - self.map - .get_mut(&TypeId::of::()) - .and_then(|boxed| (&mut **boxed as &mut (Any + 'static)).downcast_mut()) - } - - /// Remove a type from this `Extensions`. - /// - /// If a extension of this type existed, it will be returned. - pub fn remove(&mut self) -> Option { - self.map.remove(&TypeId::of::()).and_then(|boxed| { - (boxed as Box) - .downcast() - .ok() - .map(|boxed| *boxed) - }) - } - - /// Clear the `Extensions` of all inserted extensions. - #[inline] - pub fn clear(&mut self) { - self.map.clear(); - } -} - -impl fmt::Debug for Extensions { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Extensions").finish() - } -} - -#[test] -fn test_extensions() { - #[derive(Debug, PartialEq)] - struct MyType(i32); - - let mut extensions = Extensions::new(); - - extensions.insert(5i32); - extensions.insert(MyType(10)); - - assert_eq!(extensions.get(), Some(&5i32)); - assert_eq!(extensions.get_mut(), Some(&mut 5i32)); - - assert_eq!(extensions.remove::(), Some(5i32)); - assert!(extensions.get::().is_none()); - - assert_eq!(extensions.get::(), None); - assert_eq!(extensions.get(), Some(&MyType(10))); -} diff --git a/src/extractor.rs b/src/extractor.rs index 337057235..53209ad00 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -1,4 +1,3 @@ -use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::rc::Rc; use std::{fmt, str}; @@ -6,25 +5,34 @@ use std::{fmt, str}; use bytes::Bytes; use encoding::all::UTF_8; use encoding::types::{DecoderTrap, Encoding}; -use futures::{future, Async, Future, Poll}; +use futures::future::{err, ok, Either, FutureResult}; +use futures::{future, Async, Future, IntoFuture, Poll, Stream}; use mime::Mime; use serde::de::{self, DeserializeOwned}; +use serde::Serialize; +use serde_json; use serde_urlencoded; -use de::PathDeserializer; -use error::{Error, ErrorBadRequest, ErrorNotFound, UrlencodedError, ErrorConflict}; -use handler::{AsyncResult, FromRequest}; -use httpmessage::{HttpMessage, MessageBody, UrlEncoded}; -use httprequest::HttpRequest; -use Either; +use actix_http::dev::{JsonBody, MessageBody, UrlEncoded}; +use actix_http::error::{ + Error, ErrorBadRequest, ErrorNotFound, JsonPayloadError, PayloadError, + UrlencodedError, +}; +use actix_http::http::StatusCode; +use actix_http::{HttpMessage, Response}; +use actix_router::PathDeserializer; + +use crate::handler::FromRequest; +use crate::request::HttpRequest; +use crate::responder::Responder; +use crate::service::ServiceRequest; #[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from the request's path. Information from the path is -/// URL decoded. Decoding of special characters can be disabled through `PathConfig`. +/// Extract typed information from the request's path. /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// # extern crate bytes; /// # extern crate actix_web; /// # extern crate futures; @@ -48,7 +56,7 @@ use Either; /// It is possible to extract path information to a specific type that /// implements `Deserialize` trait from *serde*. /// -/// ```rust +/// ```rust,ignore /// # extern crate bytes; /// # extern crate actix_web; /// # extern crate futures; @@ -101,6 +109,15 @@ impl Path { pub fn into_inner(self) -> T { self.inner } + + /// Extract path information from a request + pub fn extract

    (req: &ServiceRequest

    ) -> Result, de::value::Error> + where + T: DeserializeOwned, + { + de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) + .map(|inner| Path { inner }) + } } impl From for Path { @@ -109,74 +126,16 @@ impl From for Path { } } -impl FromRequest for Path +impl FromRequest

    for Path where T: DeserializeOwned, { - type Config = PathConfig; - type Result = Result; + type Error = Error; + type Future = FutureResult; #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - let req = req.clone(); - let req2 = req.clone(); - let err = Rc::clone(&cfg.ehandler); - de::Deserialize::deserialize(PathDeserializer::new(&req, cfg.decode)) - .map_err(move |e| (*err)(e, &req2)) - .map(|inner| Path { inner }) - } -} - -/// Path extractor configuration -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{error, http, App, HttpResponse, Path, Result}; -/// -/// /// deserialize `Info` from request's body, max payload size is 4kb -/// fn index(info: Path<(u32, String)>) -> Result { -/// Ok(format!("Welcome {}!", info.1)) -/// } -/// -/// fn main() { -/// let app = App::new().resource("/index.html/{id}/{name}", |r| { -/// r.method(http::Method::GET).with_config(index, |cfg| { -/// cfg.0.error_handler(|err, req| { -/// // <- create custom error response -/// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() -/// }); -/// }) -/// }); -/// } -/// ``` -pub struct PathConfig { - ehandler: Rc) -> Error>, - decode: bool, -} -impl PathConfig { - /// Set custom error handler - pub fn error_handler(&mut self, f: F) -> &mut Self - where - F: Fn(serde_urlencoded::de::Error, &HttpRequest) -> Error + 'static, - { - self.ehandler = Rc::new(f); - self - } - - /// Disable decoding of URL encoded special charaters from the path - pub fn disable_decoding(&mut self) -> &mut Self - { - self.decode = false; - self - } -} - -impl Default for PathConfig { - fn default() -> Self { - PathConfig { - ehandler: Rc::new(|e, _| ErrorNotFound(e)), - decode: true, - } + fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + Self::extract(req).map_err(ErrorNotFound).into_future() } } @@ -193,11 +152,11 @@ impl fmt::Display for Path { } #[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from the request's query. +/// Extract typed information from from the request's query. /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// # extern crate bytes; /// # extern crate actix_web; /// # extern crate futures; @@ -253,70 +212,18 @@ impl Query { } } -impl FromRequest for Query +impl FromRequest

    for Query where T: de::DeserializeOwned, { - type Config = QueryConfig; - type Result = Result; + type Error = Error; + type Future = FutureResult; #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - let req2 = req.clone(); - let err = Rc::clone(&cfg.ehandler); + fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { serde_urlencoded::from_str::(req.query_string()) - .map_err(move |e| (*err)(e, &req2)) - .map(Query) - } -} - -/// Query extractor configuration -/// -/// ```rust -/// # extern crate actix_web; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{error, http, App, HttpResponse, Query, Result}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// deserialize `Info` from request's body, max payload size is 4kb -/// fn index(info: Query) -> Result { -/// Ok(format!("Welcome {}!", info.username)) -/// } -/// -/// fn main() { -/// let app = App::new().resource("/index.html", |r| { -/// r.method(http::Method::GET).with_config(index, |cfg| { -/// cfg.0.error_handler(|err, req| { -/// // <- create custom error response -/// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() -/// }); -/// }) -/// }); -/// } -/// ``` -pub struct QueryConfig { - ehandler: Rc) -> Error>, -} -impl QueryConfig { - /// Set custom error handler - pub fn error_handler(&mut self, f: F) -> &mut Self - where - F: Fn(serde_urlencoded::de::Error, &HttpRequest) -> Error + 'static, - { - self.ehandler = Rc::new(f); - self - } -} - -impl Default for QueryConfig { - fn default() -> Self { - QueryConfig { - ehandler: Rc::new(|e, _| e.into()), - } + .map(|val| ok(Query(val))) + .unwrap_or_else(|e| err(e.into())) } } @@ -343,7 +250,7 @@ impl fmt::Display for Query { /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// #[macro_use] extern crate serde_derive; /// use actix_web::{App, Form, Result}; @@ -384,16 +291,18 @@ impl DerefMut for Form { } } -impl FromRequest for Form +impl FromRequest

    for Form where T: DeserializeOwned + 'static, - S: 'static, + P: Stream + 'static, { - type Config = FormConfig; - type Result = Box>; + type Error = Error; + type Future = Box>; #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { + fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + let cfg = FormConfig::default(); + let req2 = req.clone(); let err = Rc::clone(&cfg.ehandler); Box::new( @@ -419,7 +328,7 @@ impl fmt::Display for Form { /// Form extractor configuration /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// #[macro_use] extern crate serde_derive; /// use actix_web::{http, App, Form, Result}; @@ -446,12 +355,12 @@ impl fmt::Display for Form { /// ); /// } /// ``` -pub struct FormConfig { +pub struct FormConfig { limit: usize, - ehandler: Rc) -> Error>, + ehandler: Rc Error>, } -impl FormConfig { +impl FormConfig { /// Change max size of payload. By default max size is 256Kb pub fn limit(&mut self, limit: usize) -> &mut Self { self.limit = limit; @@ -461,14 +370,14 @@ impl FormConfig { /// Set custom error handler pub fn error_handler(&mut self, f: F) -> &mut Self where - F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static, + F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static, { self.ehandler = Rc::new(f); self } } -impl Default for FormConfig { +impl Default for FormConfig { fn default() -> Self { FormConfig { limit: 262_144, @@ -477,6 +386,205 @@ impl Default for FormConfig { } } +/// Json helper +/// +/// Json can be used for two different purpose. First is for json response +/// generation and second is for extracting typed information from request's +/// payload. +/// +/// To extract typed information from request's body, the type `T` must +/// implement the `Deserialize` trait from *serde*. +/// +/// [**JsonConfig**](dev/struct.JsonConfig.html) allows to configure extraction +/// process. +/// +/// ## Example +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{App, Json, Result, http}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// deserialize `Info` from request's body +/// fn index(info: Json) -> Result { +/// Ok(format!("Welcome {}!", info.username)) +/// } +/// +/// fn main() { +/// let app = App::new().resource( +/// "/index.html", +/// |r| r.method(http::Method::POST).with(index)); // <- use `with` extractor +/// } +/// ``` +/// +/// The `Json` type allows you to respond with well-formed JSON data: simply +/// return a value of type Json where T is the type of a structure +/// to serialize into *JSON*. The type `T` must implement the `Serialize` +/// trait from *serde*. +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// # #[macro_use] extern crate serde_derive; +/// # use actix_web::*; +/// # +/// #[derive(Serialize)] +/// struct MyObj { +/// name: String, +/// } +/// +/// fn index(req: HttpRequest) -> Result> { +/// Ok(Json(MyObj { +/// name: req.match_info().query("name")?, +/// })) +/// } +/// # fn main() {} +/// ``` +pub struct Json(pub T); + +impl Json { + /// Deconstruct to an inner value + pub fn into_inner(self) -> T { + self.0 + } +} + +impl Deref for Json { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl DerefMut for Json { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl fmt::Debug for Json +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Json: {:?}", self.0) + } +} + +impl fmt::Display for Json +where + T: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl Responder for Json { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + let body = match serde_json::to_string(&self.0) { + Ok(body) => body, + Err(e) => return err(e.into()), + }; + + ok(Response::build(StatusCode::OK) + .content_type("application/json") + .body(body)) + } +} + +impl FromRequest

    for Json +where + T: DeserializeOwned + 'static, + P: Stream + 'static, +{ + type Error = Error; + type Future = Box>; + + #[inline] + fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + let cfg = JsonConfig::default(); + + let req2 = req.clone(); + let err = Rc::clone(&cfg.ehandler); + Box::new( + JsonBody::new(req) + .limit(cfg.limit) + .map_err(move |e| (*err)(e, &req2)) + .map(Json), + ) + } +} + +/// Json extractor configuration +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{error, http, App, HttpResponse, Json, Result}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// deserialize `Info` from request's body, max payload size is 4kb +/// fn index(info: Json) -> Result { +/// Ok(format!("Welcome {}!", info.username)) +/// } +/// +/// fn main() { +/// let app = App::new().resource("/index.html", |r| { +/// r.method(http::Method::POST) +/// .with_config(index, |cfg| { +/// cfg.0.limit(4096) // <- change json extractor configuration +/// .error_handler(|err, req| { // <- create custom error response +/// error::InternalError::from_response( +/// err, HttpResponse::Conflict().finish()).into() +/// }); +/// }) +/// }); +/// } +/// ``` +pub struct JsonConfig { + limit: usize, + ehandler: Rc Error>, +} + +impl JsonConfig { + /// Change max size of payload. By default max size is 256Kb + pub fn limit(&mut self, limit: usize) -> &mut Self { + self.limit = limit; + self + } + + /// Set custom error handler + pub fn error_handler(&mut self, f: F) -> &mut Self + where + F: Fn(JsonPayloadError, &HttpRequest) -> Error + 'static, + { + self.ehandler = Rc::new(f); + self + } +} + +impl Default for JsonConfig { + fn default() -> Self { + JsonConfig { + limit: 262_144, + ehandler: Rc::new(|e, _| e.into()), + } + } +} + /// Request payload extractor. /// /// Loads request's payload and construct Bytes instance. @@ -486,7 +594,7 @@ impl Default for FormConfig { /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// extern crate bytes; /// # extern crate actix_web; /// use actix_web::{http, App, Result}; @@ -501,16 +609,23 @@ impl Default for FormConfig { /// .resource("/index.html", |r| r.method(http::Method::GET).with(index)); /// } /// ``` -impl FromRequest for Bytes { - type Config = PayloadConfig; - type Result = Result>, Error>; +impl

    FromRequest

    for Bytes +where + P: Stream + 'static, +{ + type Error = Error; + type Future = + Either>, FutureResult>; #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - // check content-type - cfg.check_mimetype(req)?; + fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + let cfg = PayloadConfig::default(); - Ok(Box::new(MessageBody::new(req).limit(cfg.limit).from_err())) + if let Err(e) = cfg.check_mimetype(req) { + return Either::B(err(e)); + } + + Either::A(Box::new(MessageBody::new(req).limit(cfg.limit).from_err())) } } @@ -523,7 +638,7 @@ impl FromRequest for Bytes { /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// use actix_web::{http, App, Result}; /// @@ -541,19 +656,30 @@ impl FromRequest for Bytes { /// }); /// } /// ``` -impl FromRequest for String { - type Config = PayloadConfig; - type Result = Result>, Error>; +impl

    FromRequest

    for String +where + P: Stream + 'static, +{ + type Error = Error; + type Future = + Either>, FutureResult>; #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { + fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + let cfg = PayloadConfig::default(); + // check content-type - cfg.check_mimetype(req)?; + if let Err(e) = cfg.check_mimetype(req) { + return Either::B(err(e)); + } // check charset - let encoding = req.encoding()?; + let encoding = match req.encoding() { + Ok(enc) => enc, + Err(e) => return Either::B(err(e.into())), + }; - Ok(Box::new( + Either::A(Box::new( MessageBody::new(req) .limit(cfg.limit) .from_err() @@ -579,7 +705,7 @@ impl FromRequest for String { /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// extern crate rand; /// #[macro_use] extern crate serde_derive; @@ -619,176 +745,30 @@ impl FromRequest for String { /// }); /// } /// ``` -impl FromRequest for Option +impl FromRequest

    for Option where - T: FromRequest, + T: FromRequest

    , + T::Future: 'static, { - type Config = T::Config; - type Result = Box, Error = Error>>; + type Error = Error; + type Future = Box, Error = Error>>; #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - Box::new(T::from_request(req, cfg).into().then(|r| match r { + fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + Box::new(T::from_request(req).then(|r| match r { Ok(v) => future::ok(Some(v)), Err(_) => future::ok(None), })) } } -/// Extract either one of two fields from the request. -/// -/// If both or none of the fields can be extracted, the default behaviour is to prefer the first -/// successful, last that failed. The behaviour can be changed by setting the appropriate -/// ```EitherCollisionStrategy```. -/// -/// CAVEAT: Most of the time both extractors will be run. Make sure that the extractors you specify -/// can be run one after another (or in parallel). This will always fail for extractors that modify -/// the request state (such as the `Form` extractors that read in the body stream). -/// So Either, Form> will not work correctly - it will only succeed if it matches the first -/// option, but will always fail to match the second (since the body stream will be at the end, and -/// appear to be empty). -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// extern crate rand; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Result, HttpRequest, Error, FromRequest}; -/// use actix_web::error::ErrorBadRequest; -/// use actix_web::Either; -/// -/// #[derive(Debug, Deserialize)] -/// struct Thing { name: String } -/// -/// #[derive(Debug, Deserialize)] -/// struct OtherThing { id: String } -/// -/// impl FromRequest for Thing { -/// type Config = (); -/// type Result = Result; -/// -/// #[inline] -/// fn from_request(req: &HttpRequest, _cfg: &Self::Config) -> Self::Result { -/// if rand::random() { -/// Ok(Thing { name: "thingy".into() }) -/// } else { -/// Err(ErrorBadRequest("no luck")) -/// } -/// } -/// } -/// -/// impl FromRequest for OtherThing { -/// type Config = (); -/// type Result = Result; -/// -/// #[inline] -/// fn from_request(req: &HttpRequest, _cfg: &Self::Config) -> Self::Result { -/// if rand::random() { -/// Ok(OtherThing { id: "otherthingy".into() }) -/// } else { -/// Err(ErrorBadRequest("no luck")) -/// } -/// } -/// } -/// -/// /// extract text data from request -/// fn index(supplied_thing: Either) -> Result { -/// match supplied_thing { -/// Either::A(thing) => Ok(format!("Got something: {:?}", thing)), -/// Either::B(other_thing) => Ok(format!("Got anotherthing: {:?}", other_thing)) -/// } -/// } -/// -/// fn main() { -/// let app = App::new().resource("/users/:first", |r| { -/// r.method(http::Method::POST).with(index) -/// }); -/// } -/// ``` -impl FromRequest for Either where A: FromRequest, B: FromRequest { - type Config = EitherConfig; - type Result = AsyncResult>; - - #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - let a = A::from_request(&req.clone(), &cfg.a).into().map(|a| Either::A(a)); - let b = B::from_request(req, &cfg.b).into().map(|b| Either::B(b)); - - match &cfg.collision_strategy { - EitherCollisionStrategy::PreferA => AsyncResult::future(Box::new(a.or_else(|_| b))), - EitherCollisionStrategy::PreferB => AsyncResult::future(Box::new(b.or_else(|_| a))), - EitherCollisionStrategy::FastestSuccessful => AsyncResult::future(Box::new(a.select2(b).then( |r| match r { - Ok(future::Either::A((ares, _b))) => AsyncResult::ok(ares), - Ok(future::Either::B((bres, _a))) => AsyncResult::ok(bres), - Err(future::Either::A((_aerr, b))) => AsyncResult::future(Box::new(b)), - Err(future::Either::B((_berr, a))) => AsyncResult::future(Box::new(a)) - }))), - EitherCollisionStrategy::ErrorA => AsyncResult::future(Box::new(b.then(|r| match r { - Err(_berr) => AsyncResult::future(Box::new(a)), - Ok(b) => AsyncResult::future(Box::new(a.then( |r| match r { - Ok(_a) => Err(ErrorConflict("Both wings of either extractor completed")), - Err(_arr) => Ok(b) - }))) - }))), - EitherCollisionStrategy::ErrorB => AsyncResult::future(Box::new(a.then(|r| match r { - Err(_aerr) => AsyncResult::future(Box::new(b)), - Ok(a) => AsyncResult::future(Box::new(b.then( |r| match r { - Ok(_b) => Err(ErrorConflict("Both wings of either extractor completed")), - Err(_berr) => Ok(a) - }))) - }))), - } - } -} - -/// Defines the result if neither or both of the extractors supplied to an Either extractor succeed. -#[derive(Debug)] -pub enum EitherCollisionStrategy { - /// If both are successful, return A, if both fail, return error of B - PreferA, - /// If both are successful, return B, if both fail, return error of A - PreferB, - /// Return result of the faster, error of the slower if both fail - FastestSuccessful, - /// Return error if both succeed, return error of A if both fail - ErrorA, - /// Return error if both succeed, return error of B if both fail - ErrorB -} - -impl Default for EitherCollisionStrategy { - fn default() -> Self { - EitherCollisionStrategy::FastestSuccessful - } -} - -///Determines Either extractor configuration -/// -///By default `EitherCollisionStrategy::FastestSuccessful` is used. -pub struct EitherConfig where A: FromRequest, B: FromRequest { - a: A::Config, - b: B::Config, - collision_strategy: EitherCollisionStrategy -} - -impl Default for EitherConfig where A: FromRequest, B: FromRequest { - fn default() -> Self { - EitherConfig { - a: A::Config::default(), - b: B::Config::default(), - collision_strategy: EitherCollisionStrategy::default() - } - } -} - /// Optionally extract a field from the request or extract the Error if unsuccessful /// /// If the FromRequest for T fails, inject Err into handler rather than returning an error response /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// extern crate rand; /// #[macro_use] extern crate serde_derive; @@ -798,12 +778,12 @@ impl Default for EitherConfig where A: FromRequest, B: FromRequ /// #[derive(Debug, Deserialize)] /// struct Thing { name: String } /// -/// impl FromRequest for Thing { +/// impl FromRequest for Thing { /// type Config = (); /// type Result = Result; /// /// #[inline] -/// fn from_request(req: &HttpRequest, _cfg: &Self::Config) -> Self::Result { +/// fn from_request(req: &Request, _cfg: &Self::Config) -> Self::Result { /// if rand::random() { /// Ok(Thing { name: "thingy".into() }) /// } else { @@ -827,16 +807,21 @@ impl Default for EitherConfig where A: FromRequest, B: FromRequ /// }); /// } /// ``` -impl FromRequest for Result +impl FromRequest

    for Result where - T: FromRequest, + T: FromRequest

    , + T::Future: 'static, + T::Error: 'static, { - type Config = T::Config; - type Result = Box, Error = Error>>; + type Error = Error; + type Future = Box, Error = Error>>; #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - Box::new(T::from_request(req, cfg).into().then(future::ok)) + fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + Box::new(T::from_request(req).then(|res| match res { + Ok(v) => ok(Ok(v)), + Err(e) => ok(Err(e)), + })) } } @@ -860,7 +845,7 @@ impl PayloadConfig { self } - fn check_mimetype(&self, req: &HttpRequest) -> Result<(), Error> { + fn check_mimetype

    (&self, req: &ServiceRequest

    ) -> Result<(), Error> { // check content-type if let Some(ref mt) = self.mimetype { match req.mime_type() { @@ -893,34 +878,26 @@ impl Default for PayloadConfig { macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { /// FromRequest implementation for tuple - impl + 'static),+> FromRequest for ($($T,)+) - where - S: 'static, + impl + 'static),+> FromRequest

    for ($($T,)+) { - type Config = ($($T::Config,)+); - type Result = Box>; + type Error = Error; + type Future = $fut_type; - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - Box::new($fut_type { - s: PhantomData, + fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + $fut_type { items: <($(Option<$T>,)+)>::default(), - futs: ($(Some($T::from_request(req, &cfg.$n).into()),)+), - }) + futs: ($($T::from_request(req),)+), + } } } - struct $fut_type),+> - where - S: 'static, - { - s: PhantomData, + #[doc(hidden)] + pub struct $fut_type),+> { items: ($(Option<$T>,)+), - futs: ($(Option>,)+), + futs: ($($T::Future,)+), } - impl),+> Future for $fut_type - where - S: 'static, + impl),+> Future for $fut_type { type Item = ($($T,)+); type Error = Error; @@ -929,14 +906,13 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { let mut ready = true; $( - if self.futs.$n.is_some() { - match self.futs.$n.as_mut().unwrap().poll() { + if self.items.$n.is_none() { + match self.futs.$n.poll() { Ok(Async::Ready(item)) => { self.items.$n = Some(item); - self.futs.$n.take(); } Ok(Async::NotReady) => ready = false, - Err(e) => return Err(e), + Err(e) => return Err(e.into()), } } )+ @@ -952,10 +928,13 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { } }); -impl FromRequest for () { - type Config = (); - type Result = Self; - fn from_request(_req: &HttpRequest, _cfg: &Self::Config) -> Self::Result {} +impl

    FromRequest

    for () { + type Error = Error; + type Future = FutureResult<(), Error>; + + fn from_request(_req: &mut ServiceRequest

    ) -> Self::Future { + ok(()) + } } tuple_from_req!(TupleFromRequest1, (0, A)); @@ -1005,385 +984,298 @@ tuple_from_req!( (7, H), (8, I) ); +tuple_from_req!( + TupleFromRequest10, + (0, A), + (1, B), + (2, C), + (3, D), + (4, E), + (5, F), + (6, G), + (7, H), + (8, I), + (9, J) +); -#[cfg(test)] -mod tests { - use super::*; - use bytes::Bytes; - use futures::{Async, Future}; - use http::header; - use mime; - use resource::Resource; - use router::{ResourceDef, Router}; - use test::TestRequest; +// #[cfg(test)] +// mod tests { +// use super::*; +// use actix_http::http::header; +// use actix_http::test::TestRequest; +// use bytes::Bytes; +// use futures::{Async, Future}; +// use mime; +// use serde::{Deserialize, Serialize}; - #[derive(Deserialize, Debug, PartialEq)] - struct Info { - hello: String, - } +// use crate::resource::Resource; +// // use crate::router::{ResourceDef, Router}; - #[derive(Deserialize, Debug, PartialEq)] - struct OtherInfo { - bye: String, - } +// #[derive(Deserialize, Debug, PartialEq)] +// struct Info { +// hello: String, +// } - #[test] - fn test_bytes() { - let cfg = PayloadConfig::default(); - let req = TestRequest::with_header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); +// #[test] +// fn test_bytes() { +// let cfg = PayloadConfig::default(); +// let req = TestRequest::with_header(header::CONTENT_LENGTH, "11") +// .set_payload(Bytes::from_static(b"hello=world")) +// .finish(); - match Bytes::from_request(&req, &cfg).unwrap().poll().unwrap() { - Async::Ready(s) => { - assert_eq!(s, Bytes::from_static(b"hello=world")); - } - _ => unreachable!(), - } - } +// match Bytes::from_request(&req, &cfg).unwrap().poll().unwrap() { +// Async::Ready(s) => { +// assert_eq!(s, Bytes::from_static(b"hello=world")); +// } +// _ => unreachable!(), +// } +// } - #[test] - fn test_string() { - let cfg = PayloadConfig::default(); - let req = TestRequest::with_header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); +// #[test] +// fn test_string() { +// let cfg = PayloadConfig::default(); +// let req = TestRequest::with_header(header::CONTENT_LENGTH, "11") +// .set_payload(Bytes::from_static(b"hello=world")) +// .finish(); - match String::from_request(&req, &cfg).unwrap().poll().unwrap() { - Async::Ready(s) => { - assert_eq!(s, "hello=world"); - } - _ => unreachable!(), - } - } +// match String::from_request(&req, &cfg).unwrap().poll().unwrap() { +// Async::Ready(s) => { +// assert_eq!(s, "hello=world"); +// } +// _ => unreachable!(), +// } +// } - #[test] - fn test_form() { - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); +// #[test] +// fn test_form() { +// let req = TestRequest::with_header( +// header::CONTENT_TYPE, +// "application/x-www-form-urlencoded", +// ) +// .header(header::CONTENT_LENGTH, "11") +// .set_payload(Bytes::from_static(b"hello=world")) +// .finish(); - let mut cfg = FormConfig::default(); - cfg.limit(4096); - match Form::::from_request(&req, &cfg).poll().unwrap() { - Async::Ready(s) => { - assert_eq!(s.hello, "world"); - } - _ => unreachable!(), - } - } +// let mut cfg = FormConfig::default(); +// cfg.limit(4096); +// match Form::::from_request(&req, &cfg).poll().unwrap() { +// Async::Ready(s) => { +// assert_eq!(s.hello, "world"); +// } +// _ => unreachable!(), +// } +// } - #[test] - fn test_option() { - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).finish(); +// #[test] +// fn test_option() { +// let req = TestRequest::with_header( +// header::CONTENT_TYPE, +// "application/x-www-form-urlencoded", +// ) +// .finish(); - let mut cfg = FormConfig::default(); - cfg.limit(4096); +// let mut cfg = FormConfig::default(); +// cfg.limit(4096); - match Option::>::from_request(&req, &cfg) - .poll() - .unwrap() - { - Async::Ready(r) => assert_eq!(r, None), - _ => unreachable!(), - } +// match Option::>::from_request(&req, &cfg) +// .poll() +// .unwrap() +// { +// Async::Ready(r) => assert_eq!(r, None), +// _ => unreachable!(), +// } - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "9") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); +// let req = TestRequest::with_header( +// header::CONTENT_TYPE, +// "application/x-www-form-urlencoded", +// ) +// .header(header::CONTENT_LENGTH, "9") +// .set_payload(Bytes::from_static(b"hello=world")) +// .finish(); - match Option::>::from_request(&req, &cfg) - .poll() - .unwrap() - { - Async::Ready(r) => assert_eq!( - r, - Some(Form(Info { - hello: "world".into() - })) - ), - _ => unreachable!(), - } +// match Option::>::from_request(&req, &cfg) +// .poll() +// .unwrap() +// { +// Async::Ready(r) => assert_eq!( +// r, +// Some(Form(Info { +// hello: "world".into() +// })) +// ), +// _ => unreachable!(), +// } - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "9") - .set_payload(Bytes::from_static(b"bye=world")) - .finish(); +// let req = TestRequest::with_header( +// header::CONTENT_TYPE, +// "application/x-www-form-urlencoded", +// ) +// .header(header::CONTENT_LENGTH, "9") +// .set_payload(Bytes::from_static(b"bye=world")) +// .finish(); - match Option::>::from_request(&req, &cfg) - .poll() - .unwrap() - { - Async::Ready(r) => assert_eq!(r, None), - _ => unreachable!(), - } - } +// match Option::>::from_request(&req, &cfg) +// .poll() +// .unwrap() +// { +// Async::Ready(r) => assert_eq!(r, None), +// _ => unreachable!(), +// } +// } - #[test] - fn test_either() { - let req = TestRequest::default().finish(); - let mut cfg: EitherConfig, Query, _> = EitherConfig::default(); +// #[test] +// fn test_result() { +// let req = TestRequest::with_header( +// header::CONTENT_TYPE, +// "application/x-www-form-urlencoded", +// ) +// .header(header::CONTENT_LENGTH, "11") +// .set_payload(Bytes::from_static(b"hello=world")) +// .finish(); - assert!(Either::, Query>::from_request(&req, &cfg).poll().is_err()); +// match Result::, Error>::from_request(&req, &FormConfig::default()) +// .poll() +// .unwrap() +// { +// Async::Ready(Ok(r)) => assert_eq!( +// r, +// Form(Info { +// hello: "world".into() +// }) +// ), +// _ => unreachable!(), +// } - let req = TestRequest::default().uri("/index?hello=world").finish(); +// let req = TestRequest::with_header( +// header::CONTENT_TYPE, +// "application/x-www-form-urlencoded", +// ) +// .header(header::CONTENT_LENGTH, "9") +// .set_payload(Bytes::from_static(b"bye=world")) +// .finish(); - match Either::, Query>::from_request(&req, &cfg).poll().unwrap() { - Async::Ready(r) => assert_eq!(r, Either::A(Query(Info { hello: "world".into() }))), - _ => unreachable!(), - } +// match Result::, Error>::from_request(&req, &FormConfig::default()) +// .poll() +// .unwrap() +// { +// Async::Ready(r) => assert!(r.is_err()), +// _ => unreachable!(), +// } +// } - let req = TestRequest::default().uri("/index?bye=world").finish(); - match Either::, Query>::from_request(&req, &cfg).poll().unwrap() { - Async::Ready(r) => assert_eq!(r, Either::B(Query(OtherInfo { bye: "world".into() }))), - _ => unreachable!(), - } +// #[test] +// fn test_payload_config() { +// let req = TestRequest::default().finish(); +// let mut cfg = PayloadConfig::default(); +// cfg.mimetype(mime::APPLICATION_JSON); +// assert!(cfg.check_mimetype(&req).is_err()); - let req = TestRequest::default().uri("/index?hello=world&bye=world").finish(); - cfg.collision_strategy = EitherCollisionStrategy::PreferA; +// let req = TestRequest::with_header( +// header::CONTENT_TYPE, +// "application/x-www-form-urlencoded", +// ) +// .finish(); +// assert!(cfg.check_mimetype(&req).is_err()); - match Either::, Query>::from_request(&req, &cfg).poll().unwrap() { - Async::Ready(r) => assert_eq!(r, Either::A(Query(Info { hello: "world".into() }))), - _ => unreachable!(), - } +// let req = +// TestRequest::with_header(header::CONTENT_TYPE, "application/json").finish(); +// assert!(cfg.check_mimetype(&req).is_ok()); +// } - cfg.collision_strategy = EitherCollisionStrategy::PreferB; +// #[derive(Deserialize)] +// struct MyStruct { +// key: String, +// value: String, +// } - match Either::, Query>::from_request(&req, &cfg).poll().unwrap() { - Async::Ready(r) => assert_eq!(r, Either::B(Query(OtherInfo { bye: "world".into() }))), - _ => unreachable!(), - } +// #[derive(Deserialize)] +// struct Id { +// id: String, +// } - cfg.collision_strategy = EitherCollisionStrategy::ErrorA; - assert!(Either::, Query>::from_request(&req, &cfg).poll().is_err()); +// #[derive(Deserialize)] +// struct Test2 { +// key: String, +// value: u32, +// } - cfg.collision_strategy = EitherCollisionStrategy::FastestSuccessful; - assert!(Either::, Query>::from_request(&req, &cfg).poll().is_ok()); - } +// #[test] +// fn test_request_extract() { +// let req = TestRequest::with_uri("/name/user1/?id=test").finish(); - #[test] - fn test_result() { - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); +// let mut router = Router::<()>::default(); +// router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); +// let info = router.recognize(&req, &(), 0); +// let req = req.with_route_info(info); - match Result::, Error>::from_request(&req, &FormConfig::default()) - .poll() - .unwrap() - { - Async::Ready(Ok(r)) => assert_eq!( - r, - Form(Info { - hello: "world".into() - }) - ), - _ => unreachable!(), - } +// let s = Path::::from_request(&req, &()).unwrap(); +// assert_eq!(s.key, "name"); +// assert_eq!(s.value, "user1"); - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "9") - .set_payload(Bytes::from_static(b"bye=world")) - .finish(); +// let s = Path::<(String, String)>::from_request(&req, &()).unwrap(); +// assert_eq!(s.0, "name"); +// assert_eq!(s.1, "user1"); - match Result::, Error>::from_request(&req, &FormConfig::default()) - .poll() - .unwrap() - { - Async::Ready(r) => assert!(r.is_err()), - _ => unreachable!(), - } - } +// let s = Query::::from_request(&req, &()).unwrap(); +// assert_eq!(s.id, "test"); - #[test] - fn test_payload_config() { - let req = TestRequest::default().finish(); - let mut cfg = PayloadConfig::default(); - cfg.mimetype(mime::APPLICATION_JSON); - assert!(cfg.check_mimetype(&req).is_err()); +// let mut router = Router::<()>::default(); +// router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); +// let req = TestRequest::with_uri("/name/32/").finish(); +// let info = router.recognize(&req, &(), 0); +// let req = req.with_route_info(info); - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).finish(); - assert!(cfg.check_mimetype(&req).is_err()); +// let s = Path::::from_request(&req, &()).unwrap(); +// assert_eq!(s.as_ref().key, "name"); +// assert_eq!(s.value, 32); - let req = - TestRequest::with_header(header::CONTENT_TYPE, "application/json").finish(); - assert!(cfg.check_mimetype(&req).is_ok()); - } +// let s = Path::<(String, u8)>::from_request(&req, &()).unwrap(); +// assert_eq!(s.0, "name"); +// assert_eq!(s.1, 32); - #[derive(Deserialize)] - struct MyStruct { - key: String, - value: String, - } +// let res = Path::>::extract(&req).unwrap(); +// assert_eq!(res[0], "name".to_owned()); +// assert_eq!(res[1], "32".to_owned()); +// } - #[derive(Deserialize)] - struct Id { - id: String, - } +// #[test] +// fn test_extract_path_single() { +// let mut router = Router::<()>::default(); +// router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); - #[derive(Deserialize)] - struct Test2 { - key: String, - value: u32, - } +// let req = TestRequest::with_uri("/32/").finish(); +// let info = router.recognize(&req, &(), 0); +// let req = req.with_route_info(info); +// assert_eq!(*Path::::from_request(&req, &()).unwrap(), 32); +// } - #[test] - fn test_request_extract() { - let req = TestRequest::with_uri("/name/user1/?id=test").finish(); +// #[test] +// fn test_tuple_extract() { +// let mut router = Router::<()>::default(); +// router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); +// let req = TestRequest::with_uri("/name/user1/?id=test").finish(); +// let info = router.recognize(&req, &(), 0); +// let req = req.with_route_info(info); - let s = Path::::from_request(&req, &PathConfig::default()).unwrap(); - assert_eq!(s.key, "name"); - assert_eq!(s.value, "user1"); +// let res = match <(Path<(String, String)>,)>::extract(&req).poll() { +// Ok(Async::Ready(res)) => res, +// _ => panic!("error"), +// }; +// assert_eq!((res.0).0, "name"); +// assert_eq!((res.0).1, "user1"); - let s = Path::<(String, String)>::from_request(&req, &PathConfig::default()).unwrap(); - assert_eq!(s.0, "name"); - assert_eq!(s.1, "user1"); +// let res = match <(Path<(String, String)>, Path<(String, String)>)>::extract(&req) +// .poll() +// { +// Ok(Async::Ready(res)) => res, +// _ => panic!("error"), +// }; +// assert_eq!((res.0).0, "name"); +// assert_eq!((res.0).1, "user1"); +// assert_eq!((res.1).0, "name"); +// assert_eq!((res.1).1, "user1"); - let s = Query::::from_request(&req, &QueryConfig::default()).unwrap(); - assert_eq!(s.id, "test"); - - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - let req = TestRequest::with_uri("/name/32/").finish(); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - - let s = Path::::from_request(&req, &PathConfig::default()).unwrap(); - assert_eq!(s.as_ref().key, "name"); - assert_eq!(s.value, 32); - - let s = Path::<(String, u8)>::from_request(&req, &PathConfig::default()).unwrap(); - assert_eq!(s.0, "name"); - assert_eq!(s.1, 32); - - let res = Path::>::extract(&req).unwrap(); - assert_eq!(res[0], "name".to_owned()); - assert_eq!(res[1], "32".to_owned()); - } - - #[test] - fn test_extract_path_single() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); - - let req = TestRequest::with_uri("/32/").finish(); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - assert_eq!(*Path::::from_request(&req, &&PathConfig::default()).unwrap(), 32); - } - - #[test] - fn test_extract_path_decode() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); - - macro_rules! test_single_value { - ($value:expr, $expected:expr) => { - { - let req = TestRequest::with_uri($value).finish(); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - assert_eq!(*Path::::from_request(&req, &PathConfig::default()).unwrap(), $expected); - } - } - } - - test_single_value!("/%25/", "%"); - test_single_value!("/%40%C2%A3%24%25%5E%26%2B%3D/", "@£$%^&+="); - test_single_value!("/%2B/", "+"); - test_single_value!("/%252B/", "%2B"); - test_single_value!("/%2F/", "/"); - test_single_value!("/%252F/", "%2F"); - test_single_value!("/http%3A%2F%2Flocalhost%3A80%2Ffoo/", "http://localhost:80/foo"); - test_single_value!("/%2Fvar%2Flog%2Fsyslog/", "/var/log/syslog"); - test_single_value!( - "/http%3A%2F%2Flocalhost%3A80%2Ffile%2F%252Fvar%252Flog%252Fsyslog/", - "http://localhost:80/file/%2Fvar%2Flog%2Fsyslog" - ); - - let req = TestRequest::with_uri("/%25/7/?id=test").finish(); - - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - - let s = Path::::from_request(&req, &PathConfig::default()).unwrap(); - assert_eq!(s.key, "%"); - assert_eq!(s.value, 7); - - let s = Path::<(String, String)>::from_request(&req, &PathConfig::default()).unwrap(); - assert_eq!(s.0, "%"); - assert_eq!(s.1, "7"); - } - - #[test] - fn test_extract_path_no_decode() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); - - let req = TestRequest::with_uri("/%25/").finish(); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - assert_eq!( - *Path::::from_request( - &req, - &&PathConfig::default().disable_decoding() - ).unwrap(), - "%25" - ); - } - - #[test] - fn test_tuple_extract() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - - let req = TestRequest::with_uri("/name/user1/?id=test").finish(); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - - let res = match <(Path<(String, String)>,)>::extract(&req).poll() { - Ok(Async::Ready(res)) => res, - _ => panic!("error"), - }; - assert_eq!((res.0).0, "name"); - assert_eq!((res.0).1, "user1"); - - let res = match <(Path<(String, String)>, Path<(String, String)>)>::extract(&req) - .poll() - { - Ok(Async::Ready(res)) => res, - _ => panic!("error"), - }; - assert_eq!((res.0).0, "name"); - assert_eq!((res.0).1, "user1"); - assert_eq!((res.1).0, "name"); - assert_eq!((res.1).1, "user1"); - - let () = <()>::extract(&req); - } -} +// let () = <()>::extract(&req); +// } +// } diff --git a/src/filter.rs b/src/filter.rs new file mode 100644 index 000000000..a0566092e --- /dev/null +++ b/src/filter.rs @@ -0,0 +1,327 @@ +//! Route match predicates +#![allow(non_snake_case)] +use actix_http::http::{self, header, HttpTryFrom}; + +use crate::request::HttpRequest; + +/// Trait defines resource predicate. +/// Predicate can modify request object. It is also possible to +/// to store extra attributes on request by using `Extensions` container, +/// Extensions container available via `HttpRequest::extensions()` method. +pub trait Filter { + /// Check if request matches predicate + fn check(&self, request: &HttpRequest) -> bool; +} + +/// Return filter that matches if any of supplied filters. +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// use actix_web2::{filter, App, HttpResponse}; +/// +/// fn main() { +/// App::new().resource("/index.html", |r| { +/// r.route() +/// .filter(pred::Any(pred::Get()).or(pred::Post())) +/// .f(|r| HttpResponse::MethodNotAllowed()) +/// }); +/// } +/// ``` +pub fn Any(filter: F) -> AnyFilter { + AnyFilter(vec![Box::new(filter)]) +} + +/// Matches if any of supplied filters matche. +pub struct AnyFilter(Vec>); + +impl AnyFilter { + /// Add filter to a list of filters to check + pub fn or(mut self, filter: F) -> Self { + self.0.push(Box::new(filter)); + self + } +} + +impl Filter for AnyFilter { + fn check(&self, req: &HttpRequest) -> bool { + for p in &self.0 { + if p.check(req) { + return true; + } + } + false + } +} + +/// Return filter that matches if all of supplied filters match. +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// use actix_web::{pred, App, HttpResponse}; +/// +/// fn main() { +/// App::new().resource("/index.html", |r| { +/// r.route() +/// .filter( +/// pred::All(pred::Get()) +/// .and(pred::Header("content-type", "text/plain")), +/// ) +/// .f(|_| HttpResponse::MethodNotAllowed()) +/// }); +/// } +/// ``` +pub fn All(filter: F) -> AllFilter { + AllFilter(vec![Box::new(filter)]) +} + +/// Matches if all of supplied filters matche. +pub struct AllFilter(Vec>); + +impl AllFilter { + /// Add new predicate to list of predicates to check + pub fn and(mut self, filter: F) -> Self { + self.0.push(Box::new(filter)); + self + } +} + +impl Filter for AllFilter { + fn check(&self, request: &HttpRequest) -> bool { + for p in &self.0 { + if !p.check(request) { + return false; + } + } + true + } +} + +/// Return predicate that matches if supplied predicate does not match. +pub fn Not(filter: F) -> NotFilter { + NotFilter(Box::new(filter)) +} + +#[doc(hidden)] +pub struct NotFilter(Box); + +impl Filter for NotFilter { + fn check(&self, request: &HttpRequest) -> bool { + !self.0.check(request) + } +} + +/// Http method predicate +#[doc(hidden)] +pub struct MethodFilter(http::Method); + +impl Filter for MethodFilter { + fn check(&self, request: &HttpRequest) -> bool { + request.method() == self.0 + } +} + +/// Predicate to match *GET* http method +pub fn Get() -> MethodFilter { + MethodFilter(http::Method::GET) +} + +/// Predicate to match *POST* http method +pub fn Post() -> MethodFilter { + MethodFilter(http::Method::POST) +} + +/// Predicate to match *PUT* http method +pub fn Put() -> MethodFilter { + MethodFilter(http::Method::PUT) +} + +/// Predicate to match *DELETE* http method +pub fn Delete() -> MethodFilter { + MethodFilter(http::Method::DELETE) +} + +/// Predicate to match *HEAD* http method +pub fn Head() -> MethodFilter { + MethodFilter(http::Method::HEAD) +} + +/// Predicate to match *OPTIONS* http method +pub fn Options() -> MethodFilter { + MethodFilter(http::Method::OPTIONS) +} + +/// Predicate to match *CONNECT* http method +pub fn Connect() -> MethodFilter { + MethodFilter(http::Method::CONNECT) +} + +/// Predicate to match *PATCH* http method +pub fn Patch() -> MethodFilter { + MethodFilter(http::Method::PATCH) +} + +/// Predicate to match *TRACE* http method +pub fn Trace() -> MethodFilter { + MethodFilter(http::Method::TRACE) +} + +/// Predicate to match specified http method +pub fn Method(method: http::Method) -> MethodFilter { + MethodFilter(method) +} + +/// Return predicate that matches if request contains specified header and +/// value. +pub fn Header(name: &'static str, value: &'static str) -> HeaderFilter { + HeaderFilter( + header::HeaderName::try_from(name).unwrap(), + header::HeaderValue::from_static(value), + ) +} + +#[doc(hidden)] +pub struct HeaderFilter(header::HeaderName, header::HeaderValue); + +impl Filter for HeaderFilter { + fn check(&self, req: &HttpRequest) -> bool { + if let Some(val) = req.headers().get(&self.0) { + return val == self.1; + } + false + } +} + +/// Return predicate that matches if request contains specified Host name. +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// use actix_web::{pred, App, HttpResponse}; +/// +/// fn main() { +/// App::new().resource("/index.html", |r| { +/// r.route() +/// .filter(pred::Host("www.rust-lang.org")) +/// .f(|_| HttpResponse::MethodNotAllowed()) +/// }); +/// } +/// ``` +pub fn Host>(host: H) -> HostFilter { + HostFilter(host.as_ref().to_string(), None) +} + +#[doc(hidden)] +pub struct HostFilter(String, Option); + +impl HostFilter { + /// Set reuest scheme to match + pub fn scheme>(&mut self, scheme: H) { + self.1 = Some(scheme.as_ref().to_string()) + } +} + +impl Filter for HostFilter { + fn check(&self, _req: &HttpRequest) -> bool { + // let info = req.connection_info(); + // if let Some(ref scheme) = self.1 { + // self.0 == info.host() && scheme == info.scheme() + // } else { + // self.0 == info.host() + // } + false + } +} + +// #[cfg(test)] +// mod tests { +// use actix_http::http::{header, Method}; +// use actix_http::test::TestRequest; + +// use super::*; + +// #[test] +// fn test_header() { +// let req = TestRequest::with_header( +// header::TRANSFER_ENCODING, +// header::HeaderValue::from_static("chunked"), +// ) +// .finish(); + +// let pred = Header("transfer-encoding", "chunked"); +// assert!(pred.check(&req, req.state())); + +// let pred = Header("transfer-encoding", "other"); +// assert!(!pred.check(&req, req.state())); + +// let pred = Header("content-type", "other"); +// assert!(!pred.check(&req, req.state())); +// } + +// #[test] +// fn test_host() { +// let req = TestRequest::default() +// .header( +// header::HOST, +// header::HeaderValue::from_static("www.rust-lang.org"), +// ) +// .finish(); + +// let pred = Host("www.rust-lang.org"); +// assert!(pred.check(&req, req.state())); + +// let pred = Host("localhost"); +// assert!(!pred.check(&req, req.state())); +// } + +// #[test] +// fn test_methods() { +// let req = TestRequest::default().finish(); +// let req2 = TestRequest::default().method(Method::POST).finish(); + +// assert!(Get().check(&req, req.state())); +// assert!(!Get().check(&req2, req2.state())); +// assert!(Post().check(&req2, req2.state())); +// assert!(!Post().check(&req, req.state())); + +// let r = TestRequest::default().method(Method::PUT).finish(); +// assert!(Put().check(&r, r.state())); +// assert!(!Put().check(&req, req.state())); + +// let r = TestRequest::default().method(Method::DELETE).finish(); +// assert!(Delete().check(&r, r.state())); +// assert!(!Delete().check(&req, req.state())); + +// let r = TestRequest::default().method(Method::HEAD).finish(); +// assert!(Head().check(&r, r.state())); +// assert!(!Head().check(&req, req.state())); + +// let r = TestRequest::default().method(Method::OPTIONS).finish(); +// assert!(Options().check(&r, r.state())); +// assert!(!Options().check(&req, req.state())); + +// let r = TestRequest::default().method(Method::CONNECT).finish(); +// assert!(Connect().check(&r, r.state())); +// assert!(!Connect().check(&req, req.state())); + +// let r = TestRequest::default().method(Method::PATCH).finish(); +// assert!(Patch().check(&r, r.state())); +// assert!(!Patch().check(&req, req.state())); + +// let r = TestRequest::default().method(Method::TRACE).finish(); +// assert!(Trace().check(&r, r.state())); +// assert!(!Trace().check(&req, req.state())); +// } + +// #[test] +// fn test_preds() { +// let r = TestRequest::default().method(Method::TRACE).finish(); + +// assert!(Not(Get()).check(&r, r.state())); +// assert!(!Not(Trace()).check(&r, r.state())); + +// assert!(All(Trace()).and(Trace()).check(&r, r.state())); +// assert!(!All(Get()).and(Trace()).check(&r, r.state())); + +// assert!(Any(Get()).or(Trace()).check(&r, r.state())); +// assert!(!Any(Get()).or(Get()).check(&r, r.state())); +// } +// } diff --git a/src/framed_app.rs b/src/framed_app.rs new file mode 100644 index 000000000..ba9254146 --- /dev/null +++ b/src/framed_app.rs @@ -0,0 +1,240 @@ +use std::marker::PhantomData; +use std::rc::Rc; + +use actix_codec::Framed; +use actix_http::h1::Codec; +use actix_http::{Request, Response, SendResponse}; +use actix_router::{Path, Router, Url}; +use actix_service::{IntoNewService, NewService, Service}; +use actix_utils::cloneable::CloneableService; +use futures::{Async, Future, Poll}; +use tokio_io::{AsyncRead, AsyncWrite}; + +use crate::app::{HttpServiceFactory, State}; +use crate::framed_handler::FramedRequest; +use crate::helpers::{BoxedHttpNewService, BoxedHttpService, HttpNewService}; +use crate::request::Request as WebRequest; + +pub type FRequest = (Request, Framed); +type BoxedResponse = Box>; + +/// Application builder +pub struct FramedApp { + services: Vec<(String, BoxedHttpNewService, ()>)>, + state: State, +} + +impl FramedApp { + pub fn new() -> Self { + FramedApp { + services: Vec::new(), + state: State::new(()), + } + } +} + +impl FramedApp { + pub fn with(state: S) -> FramedApp { + FramedApp { + services: Vec::new(), + state: State::new(state), + } + } + + pub fn service(mut self, factory: U) -> Self + where + U: HttpServiceFactory, + U::Factory: NewService, Response = ()> + 'static, + ::Future: 'static, + ::Service: Service>, + <::Service as Service>::Future: 'static, + { + let path = factory.path().to_string(); + self.services.push(( + path, + Box::new(HttpNewService::new(factory.create(self.state.clone()))), + )); + self + } + + pub fn register_service(&mut self, factory: U) + where + U: HttpServiceFactory, + U::Factory: NewService, Response = ()> + 'static, + ::Future: 'static, + ::Service: Service>, + <::Service as Service>::Future: 'static, + { + let path = factory.path().to_string(); + self.services.push(( + path, + Box::new(HttpNewService::new(factory.create(self.state.clone()))), + )); + } +} + +impl IntoNewService> for FramedApp +where + T: AsyncRead + AsyncWrite, +{ + fn into_new_service(self) -> FramedAppFactory { + FramedAppFactory { + state: self.state, + services: Rc::new(self.services), + _t: PhantomData, + } + } +} + +#[derive(Clone)] +pub struct FramedAppFactory { + state: State, + services: Rc, ()>)>>, + _t: PhantomData, +} + +impl NewService for FramedAppFactory +where + T: AsyncRead + AsyncWrite, +{ + type Request = FRequest; + type Response = (); + type Error = (); + type InitError = (); + type Service = CloneableService>; + type Future = CreateService; + + fn new_service(&self) -> Self::Future { + CreateService { + fut: self + .services + .iter() + .map(|(path, service)| { + CreateServiceItem::Future(Some(path.clone()), service.new_service()) + }) + .collect(), + state: self.state.clone(), + } + } +} + +#[doc(hidden)] +pub struct CreateService { + fut: Vec>, + state: State, +} + +enum CreateServiceItem { + Future( + Option, + Box, ()>, Error = ()>>, + ), + Service(String, BoxedHttpService, ()>), +} + +impl Future for CreateService +where + T: AsyncRead + AsyncWrite, +{ + type Item = CloneableService>; + type Error = (); + + fn poll(&mut self) -> Poll { + let mut done = true; + + // poll http services + for item in &mut self.fut { + let res = match item { + CreateServiceItem::Future(ref mut path, ref mut fut) => { + match fut.poll()? { + Async::Ready(service) => Some((path.take().unwrap(), service)), + Async::NotReady => { + done = false; + None + } + } + } + CreateServiceItem::Service(_, _) => continue, + }; + + if let Some((path, service)) = res { + *item = CreateServiceItem::Service(path, service); + } + } + + if done { + let router = self + .fut + .drain(..) + .fold(Router::build(), |mut router, item| { + match item { + CreateServiceItem::Service(path, service) => { + router.path(&path, service) + } + CreateServiceItem::Future(_, _) => unreachable!(), + } + router + }); + Ok(Async::Ready(CloneableService::new(FramedAppService { + router: router.finish(), + state: self.state.clone(), + // default: self.default.take().expect("something is wrong"), + }))) + } else { + Ok(Async::NotReady) + } + } +} + +pub struct FramedAppService { + state: State, + router: Router, ()>>, +} + +impl Service for FramedAppService +where + T: AsyncRead + AsyncWrite, +{ + type Request = FRequest; + type Response = (); + type Error = (); + type Future = BoxedResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + // let mut ready = true; + // for service in &mut self.services { + // if let Async::NotReady = service.poll_ready()? { + // ready = false; + // } + // } + // if ready { + // Ok(Async::Ready(())) + // } else { + // Ok(Async::NotReady) + // } + Ok(Async::Ready(())) + } + + fn call(&mut self, (req, framed): (Request, Framed)) -> Self::Future { + let mut path = Path::new(Url::new(req.uri().clone())); + + if let Some((srv, _info)) = self.router.recognize_mut(&mut path) { + return srv.call(FramedRequest::new( + WebRequest::new(self.state.clone(), req, path), + framed, + )); + } + // for item in &mut self.services { + // req = match item.handle(req) { + // Ok(fut) => return fut, + // Err(req) => req, + // }; + // } + // self.default.call(req) + Box::new( + SendResponse::send(framed, Response::NotFound().finish().into()) + .map(|_| ()) + .map_err(|_| ()), + ) + } +} diff --git a/src/framed_handler.rs b/src/framed_handler.rs new file mode 100644 index 000000000..109b5f0a2 --- /dev/null +++ b/src/framed_handler.rs @@ -0,0 +1,379 @@ +use std::marker::PhantomData; +use std::rc::Rc; + +use actix_codec::Framed; +use actix_http::{h1::Codec, Error}; +use actix_service::{NewService, Service}; +use futures::future::{ok, FutureResult}; +use futures::{Async, Future, IntoFuture, Poll}; +use log::error; + +use crate::handler::FromRequest; +use crate::request::Request; + +pub struct FramedError { + pub err: Error, + pub framed: Framed, +} + +pub struct FramedRequest { + req: Request, + framed: Framed, + param: Ex, +} + +impl FramedRequest { + pub fn new(req: Request, framed: Framed) -> Self { + Self { + req, + framed, + param: (), + } + } +} + +impl FramedRequest { + pub fn request(&self) -> &Request { + &self.req + } + + pub fn request_mut(&mut self) -> &mut Request { + &mut self.req + } + + pub fn into_parts(self) -> (Request, Framed, Ex) { + (self.req, self.framed, self.param) + } + + pub fn map(self, op: F) -> FramedRequest + where + F: FnOnce(Ex) -> Ex2, + { + FramedRequest { + req: self.req, + framed: self.framed, + param: op(self.param), + } + } +} + +/// T handler converter factory +pub trait FramedFactory: Clone + 'static +where + R: IntoFuture, + E: Into, +{ + fn call(&self, framed: Framed, param: T, extra: Ex) -> R; +} + +#[doc(hidden)] +pub struct FramedHandle +where + F: FramedFactory, + R: IntoFuture, + E: Into, +{ + hnd: F, + _t: PhantomData<(S, Io, Ex, T, R, E)>, +} + +impl FramedHandle +where + F: FramedFactory, + R: IntoFuture, + E: Into, +{ + pub fn new(hnd: F) -> Self { + FramedHandle { + hnd, + _t: PhantomData, + } + } +} +impl NewService for FramedHandle +where + F: FramedFactory, + R: IntoFuture, + E: Into, +{ + type Request = (T, FramedRequest); + type Response = (); + type Error = FramedError; + type InitError = (); + type Service = FramedHandleService; + type Future = FutureResult; + + fn new_service(&self) -> Self::Future { + ok(FramedHandleService { + hnd: self.hnd.clone(), + _t: PhantomData, + }) + } +} + +#[doc(hidden)] +pub struct FramedHandleService +where + F: FramedFactory, + R: IntoFuture, + E: Into, +{ + hnd: F, + _t: PhantomData<(S, Io, Ex, T, R, E)>, +} + +impl Service for FramedHandleService +where + F: FramedFactory, + R: IntoFuture, + E: Into, +{ + type Request = (T, FramedRequest); + type Response = (); + type Error = FramedError; + type Future = FramedHandleServiceResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, (param, framed): (T, FramedRequest)) -> Self::Future { + let (_, framed, ex) = framed.into_parts(); + FramedHandleServiceResponse { + fut: self.hnd.call(framed, param, ex).into_future(), + _t: PhantomData, + } + } +} + +#[doc(hidden)] +pub struct FramedHandleServiceResponse { + fut: F, + _t: PhantomData, +} + +impl Future for FramedHandleServiceResponse +where + F: Future, + F::Error: Into, +{ + type Item = (); + type Error = FramedError; + + fn poll(&mut self) -> Poll { + match self.fut.poll() { + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(res)) => Ok(Async::Ready(res.into())), + Err(e) => { + let e: Error = e.into(); + error!("Error in handler: {:?}", e); + Ok(Async::Ready(())) + } + } + } +} + +pub struct FramedExtract +where + T: FromRequest, +{ + cfg: Rc, + _t: PhantomData<(Io, Ex)>, +} + +impl FramedExtract +where + T: FromRequest + 'static, +{ + pub fn new(cfg: T::Config) -> FramedExtract { + FramedExtract { + cfg: Rc::new(cfg), + _t: PhantomData, + } + } +} +impl NewService for FramedExtract +where + T: FromRequest + 'static, +{ + type Request = FramedRequest; + type Response = (T, FramedRequest); + type Error = FramedError; + type InitError = (); + type Service = FramedExtractService; + type Future = FutureResult; + + fn new_service(&self) -> Self::Future { + ok(FramedExtractService { + cfg: self.cfg.clone(), + _t: PhantomData, + }) + } +} + +pub struct FramedExtractService +where + T: FromRequest, +{ + cfg: Rc, + _t: PhantomData<(Io, Ex)>, +} + +impl Service for FramedExtractService +where + T: FromRequest + 'static, +{ + type Request = FramedRequest; + type Response = (T, FramedRequest); + type Error = FramedError; + type Future = FramedExtractResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: FramedRequest) -> Self::Future { + FramedExtractResponse { + fut: T::from_request(&req.request(), self.cfg.as_ref()), + req: Some(req), + } + } +} + +pub struct FramedExtractResponse +where + T: FromRequest + 'static, +{ + req: Option>, + fut: T::Future, +} + +impl Future for FramedExtractResponse +where + T: FromRequest + 'static, +{ + type Item = (T, FramedRequest); + type Error = FramedError; + + fn poll(&mut self) -> Poll { + match self.fut.poll() { + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(item)) => Ok(Async::Ready((item, self.req.take().unwrap()))), + Err(err) => Err(FramedError { + err: err.into(), + framed: self.req.take().unwrap().into_parts().1, + }), + } + } +} + +macro_rules! factory_tuple ({ ($(($nex:tt, $Ex:ident)),+), $(($n:tt, $T:ident)),+} => { + impl FramedFactory for Func + where Func: Fn(Framed, $($Ex,)+ $($T,)+) -> Res + Clone + 'static, + $($T: FromRequest + 'static,)+ + Res: IntoFuture + 'static, + Err: Into, + { + fn call(&self, framed: Framed, param: ($($T,)+), extra: ($($Ex,)+)) -> Res { + (self)(framed, $(extra.$nex,)+ $(param.$n,)+) + } + } +}); + +macro_rules! factory_tuple_unit ({$(($n:tt, $T:ident)),+} => { + impl FramedFactory for Func + where Func: Fn(Framed, $($T,)+) -> Res + Clone + 'static, + $($T: FromRequest + 'static,)+ + Res: IntoFuture + 'static, + Err: Into, + { + fn call(&self, framed: Framed, param: ($($T,)+), _extra: () ) -> Res { + (self)(framed, $(param.$n,)+) + } + } +}); + +#[cfg_attr(rustfmt, rustfmt_skip)] +mod m { + use super::*; + +factory_tuple_unit!((0, A)); +factory_tuple!(((0, Aex)), (0, A)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A)); + +factory_tuple_unit!((0, A), (1, B)); +factory_tuple!(((0, Aex)), (0, A), (1, B)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B)); + +factory_tuple_unit!((0, A), (1, B), (2, C)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C)); + +factory_tuple_unit!((0, A), (1, B), (2, C), (3, D)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D)); + +factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E)); + +factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); + +factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); + +factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); + +factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); + +factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +} diff --git a/src/framed_route.rs b/src/framed_route.rs new file mode 100644 index 000000000..90555a9c8 --- /dev/null +++ b/src/framed_route.rs @@ -0,0 +1,448 @@ +use std::marker::PhantomData; + +use actix_http::http::{HeaderName, HeaderValue, Method}; +use actix_http::Error; +use actix_service::{IntoNewService, NewService, NewServiceExt, Service}; +use futures::{try_ready, Async, Future, IntoFuture, Poll}; +use log::{debug, error}; +use tokio_io::{AsyncRead, AsyncWrite}; + +use crate::app::{HttpServiceFactory, State}; +use crate::framed_handler::{ + FramedError, FramedExtract, FramedFactory, FramedHandle, FramedRequest, +}; +use crate::handler::FromRequest; + +/// Resource route definition +/// +/// Route uses builder-like pattern for configuration. +/// If handler is not explicitly set, default *404 Not Found* handler is used. +pub struct FramedRoute { + service: T, + pattern: String, + methods: Vec, + headers: Vec<(HeaderName, HeaderValue)>, + state: PhantomData<(S, Io)>, +} + +impl FramedRoute { + pub fn build(path: &str) -> FramedRoutePatternBuilder { + FramedRoutePatternBuilder::new(path) + } + + pub fn get(path: &str) -> FramedRoutePatternBuilder { + FramedRoutePatternBuilder::new(path).method(Method::GET) + } + + pub fn post(path: &str) -> FramedRoutePatternBuilder { + FramedRoutePatternBuilder::new(path).method(Method::POST) + } + + pub fn put(path: &str) -> FramedRoutePatternBuilder { + FramedRoutePatternBuilder::new(path).method(Method::PUT) + } + + pub fn delete(path: &str) -> FramedRoutePatternBuilder { + FramedRoutePatternBuilder::new(path).method(Method::DELETE) + } +} + +impl FramedRoute +where + T: NewService< + Request = FramedRequest, + Response = (), + Error = FramedError, + > + 'static, +{ + pub fn new>(pattern: &str, factory: F) -> Self { + FramedRoute { + pattern: pattern.to_string(), + service: factory.into_new_service(), + headers: Vec::new(), + methods: Vec::new(), + state: PhantomData, + } + } + + pub fn method(mut self, method: Method) -> Self { + self.methods.push(method); + self + } + + pub fn header(mut self, name: HeaderName, value: HeaderValue) -> Self { + self.headers.push((name, value)); + self + } +} + +impl HttpServiceFactory for FramedRoute +where + Io: AsyncRead + AsyncWrite + 'static, + T: NewService< + Request = FramedRequest, + Response = (), + Error = FramedError, + > + 'static, + T::Service: 'static, +{ + type Factory = FramedRouteFactory; + + fn path(&self) -> &str { + &self.pattern + } + + fn create(self, state: State) -> Self::Factory { + FramedRouteFactory { + state, + service: self.service, + pattern: self.pattern, + methods: self.methods, + headers: self.headers, + _t: PhantomData, + } + } +} + +pub struct FramedRouteFactory { + service: T, + pattern: String, + methods: Vec, + headers: Vec<(HeaderName, HeaderValue)>, + state: State, + _t: PhantomData, +} + +impl NewService for FramedRouteFactory +where + Io: AsyncRead + AsyncWrite + 'static, + T: NewService< + Request = FramedRequest, + Response = (), + Error = FramedError, + > + 'static, + T::Service: 'static, +{ + type Request = FramedRequest; + type Response = T::Response; + type Error = (); + type InitError = T::InitError; + type Service = FramedRouteService; + type Future = CreateRouteService; + + fn new_service(&self) -> Self::Future { + CreateRouteService { + fut: self.service.new_service(), + pattern: self.pattern.clone(), + methods: self.methods.clone(), + headers: self.headers.clone(), + state: self.state.clone(), + _t: PhantomData, + } + } +} + +pub struct CreateRouteService { + fut: T::Future, + pattern: String, + methods: Vec, + headers: Vec<(HeaderName, HeaderValue)>, + state: State, + _t: PhantomData, +} + +impl Future for CreateRouteService +where + T: NewService< + Request = FramedRequest, + Response = (), + Error = FramedError, + >, +{ + type Item = FramedRouteService; + type Error = T::InitError; + + fn poll(&mut self) -> Poll { + let service = try_ready!(self.fut.poll()); + + Ok(Async::Ready(FramedRouteService { + service, + state: self.state.clone(), + pattern: self.pattern.clone(), + methods: self.methods.clone(), + headers: self.headers.clone(), + _t: PhantomData, + })) + } +} + +pub struct FramedRouteService { + service: T, + pattern: String, + methods: Vec, + headers: Vec<(HeaderName, HeaderValue)>, + state: State, + _t: PhantomData, +} + +impl Service for FramedRouteService +where + Io: AsyncRead + AsyncWrite + 'static, + T: Service, Response = (), Error = FramedError> + + 'static, +{ + type Request = FramedRequest; + type Response = (); + type Error = (); + type Future = FramedRouteServiceResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready().map_err(|e| { + debug!("Service not available: {}", e.err); + () + }) + } + + fn call(&mut self, req: FramedRequest) -> Self::Future { + FramedRouteServiceResponse { + fut: self.service.call(req), + send: None, + _t: PhantomData, + } + } +} + +// impl HttpService<(Request, Framed)> for FramedRouteService +// where +// Io: AsyncRead + AsyncWrite + 'static, +// S: 'static, +// T: Service, Response = (), Error = FramedError> + 'static, +// { +// fn handle( +// &mut self, +// (req, framed): (Request, Framed), +// ) -> Result)> { +// if self.methods.is_empty() +// || !self.methods.is_empty() && self.methods.contains(req.method()) +// { +// if let Some(params) = self.pattern.match_with_params(&req, 0) { +// return Ok(FramedRouteServiceResponse { +// fut: self.service.call(FramedRequest::new( +// WebRequest::new(self.state.clone(), req, params), +// framed, +// )), +// send: None, +// _t: PhantomData, +// }); +// } +// } +// Err((req, framed)) +// } +// } + +#[doc(hidden)] +pub struct FramedRouteServiceResponse { + fut: F, + send: Option>>, + _t: PhantomData, +} + +impl Future for FramedRouteServiceResponse +where + F: Future>, + Io: AsyncRead + AsyncWrite + 'static, +{ + type Item = (); + type Error = (); + + fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.send { + return match fut.poll() { + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(_)) => Ok(Async::Ready(())), + Err(e) => { + debug!("Error during error response send: {}", e); + Err(()) + } + }; + }; + + match self.fut.poll() { + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(_)) => Ok(Async::Ready(())), + Err(e) => { + error!("Error occurred during request handling: {}", e.err); + Err(()) + } + } + } +} + +pub struct FramedRoutePatternBuilder { + pattern: String, + methods: Vec, + headers: Vec<(HeaderName, HeaderValue)>, + state: PhantomData<(Io, S)>, +} + +impl FramedRoutePatternBuilder { + fn new(path: &str) -> FramedRoutePatternBuilder { + FramedRoutePatternBuilder { + pattern: path.to_string(), + methods: Vec::new(), + headers: Vec::new(), + state: PhantomData, + } + } + + pub fn method(mut self, method: Method) -> Self { + self.methods.push(method); + self + } + + pub fn map>( + self, + md: F, + ) -> FramedRouteBuilder + where + T: NewService< + Request = FramedRequest, + Response = FramedRequest, + Error = FramedError, + InitError = (), + >, + { + FramedRouteBuilder { + service: md.into_new_service(), + pattern: self.pattern, + methods: self.methods, + headers: self.headers, + state: PhantomData, + } + } + + pub fn with( + self, + handler: F, + ) -> FramedRoute< + Io, + impl NewService< + Request = FramedRequest, + Response = (), + Error = FramedError, + InitError = (), + >, + S, + > + where + F: FramedFactory, + P: FromRequest + 'static, + R: IntoFuture, + E: Into, + { + FramedRoute { + service: FramedExtract::new(P::Config::default()) + .and_then(FramedHandle::new(handler)), + pattern: self.pattern, + methods: self.methods, + headers: self.headers, + state: PhantomData, + } + } +} + +pub struct FramedRouteBuilder { + service: T, + pattern: String, + methods: Vec, + headers: Vec<(HeaderName, HeaderValue)>, + state: PhantomData<(Io, S, U1, U2)>, +} + +impl FramedRouteBuilder +where + T: NewService< + Request = FramedRequest, + Response = FramedRequest, + Error = FramedError, + InitError = (), + >, +{ + pub fn new>(path: &str, factory: F) -> Self { + FramedRouteBuilder { + service: factory.into_new_service(), + pattern: path.to_string(), + methods: Vec::new(), + headers: Vec::new(), + state: PhantomData, + } + } + + pub fn method(mut self, method: Method) -> Self { + self.methods.push(method); + self + } + + pub fn map>( + self, + md: F, + ) -> FramedRouteBuilder< + Io, + S, + impl NewService< + Request = FramedRequest, + Response = FramedRequest, + Error = FramedError, + InitError = (), + >, + U1, + U3, + > + where + K: NewService< + Request = FramedRequest, + Response = FramedRequest, + Error = FramedError, + InitError = (), + >, + { + FramedRouteBuilder { + service: self.service.from_err().and_then(md.into_new_service()), + pattern: self.pattern, + methods: self.methods, + headers: self.headers, + state: PhantomData, + } + } + + pub fn with( + self, + handler: F, + ) -> FramedRoute< + Io, + impl NewService< + Request = FramedRequest, + Response = (), + Error = FramedError, + InitError = (), + >, + S, + > + where + F: FramedFactory, + P: FromRequest + 'static, + R: IntoFuture, + E: Into, + { + FramedRoute { + service: self + .service + .and_then(FramedExtract::new(P::Config::default())) + .and_then(FramedHandle::new(handler)), + pattern: self.pattern, + methods: self.methods, + headers: self.headers, + state: PhantomData, + } + } +} diff --git a/src/fs.rs b/src/fs.rs index 604ac5504..3c83af6eb 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,34 +1,41 @@ //! Static files support +use std::cell::RefCell; use std::fmt::Write; use std::fs::{DirEntry, File, Metadata}; use std::io::{Read, Seek}; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; +use std::rc::Rc; use std::time::{SystemTime, UNIX_EPOCH}; use std::{cmp, io}; #[cfg(unix)] use std::os::unix::fs::MetadataExt; -use v_htmlescape::escape as escape_html_entity; use bytes::Bytes; +use derive_more::Display; use futures::{Async, Future, Poll, Stream}; -use futures_cpupool::{CpuFuture, CpuPool}; use mime; use mime_guess::{get_mime_type, guess_mime_type}; use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; +use v_htmlescape::escape as escape_html_entity; -use error::{Error, StaticFileError}; -use handler::{AsyncResult, Handler, Responder, RouteHandler, WrapHandler}; -use header; -use header::{ContentDisposition, DispositionParam, DispositionType}; -use http::{ContentEncoding, Method, StatusCode}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use param::FromParam; -use server::settings::DEFAULT_CPUPOOL; +use actix_http::error::{Error, ErrorInternalServerError, ResponseError}; +use actix_http::http::header::{ + self, ContentDisposition, DispositionParam, DispositionType, +}; +use actix_http::http::{ContentEncoding, Method, StatusCode}; +use actix_http::{HttpMessage, Response}; +use actix_service::{NewService, Service}; +use futures::future::{err, ok, FutureResult}; + +use crate::blocking; +use crate::handler::FromRequest; +use crate::helpers::HttpDefaultNewService; +use crate::request::HttpRequest; +use crate::responder::Responder; +use crate::service::{ServiceRequest, ServiceResponse}; ///Describes `StaticFiles` configiration /// @@ -39,7 +46,7 @@ use server::settings::DEFAULT_CPUPOOL; /// ///## Example /// -///```rust +///```rust,ignore /// extern crate mime; /// extern crate actix_web; /// use actix_web::http::header::DispositionType; @@ -113,7 +120,6 @@ pub struct NamedFile { content_disposition: header::ContentDisposition, md: Metadata, modified: Option, - cpu_pool: Option, encoding: Option, status_code: StatusCode, _cd_map: PhantomData, @@ -127,7 +133,7 @@ impl NamedFile { /// /// # Examples /// - /// ```no_run + /// ```rust,ignore /// extern crate actix_web; /// /// use actix_web::fs::NamedFile; @@ -150,7 +156,7 @@ impl NamedFile { /// /// # Examples /// - /// ```rust + /// ```rust,ignore /// use actix_web::fs::NamedFile; /// /// let file = NamedFile::open("foo.txt"); @@ -168,7 +174,7 @@ impl NamedFile { /// /// # Examples /// - /// ```no_run + /// ```rust,ignore /// extern crate actix_web; /// /// use actix_web::fs::{DefaultConfig, NamedFile}; @@ -183,7 +189,11 @@ impl NamedFile { /// Ok(()) /// } /// ``` - pub fn from_file_with_config>(file: File, path: P, _: C) -> io::Result> { + pub fn from_file_with_config>( + file: File, + path: P, + _: C, + ) -> io::Result> { let path = path.as_ref().to_path_buf(); // Get the name of the file and use it to construct default Content-Type @@ -195,7 +205,7 @@ impl NamedFile { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Provided path has no filename", - )) + )); } }; @@ -210,7 +220,6 @@ impl NamedFile { let md = file.metadata()?; let modified = md.modified().ok(); - let cpu_pool = None; let encoding = None; Ok(NamedFile { path, @@ -219,7 +228,6 @@ impl NamedFile { content_disposition, md, modified, - cpu_pool, encoding, status_code: StatusCode::OK, _cd_map: PhantomData, @@ -230,12 +238,15 @@ impl NamedFile { /// /// # Examples /// - /// ```rust + /// ```rust,ignore /// use actix_web::fs::{DefaultConfig, NamedFile}; /// /// let file = NamedFile::open_with_config("foo.txt", DefaultConfig); /// ``` - pub fn open_with_config>(path: P, config: C) -> io::Result> { + pub fn open_with_config>( + path: P, + config: C, + ) -> io::Result> { Self::from_file_with_config(File::open(&path)?, path, config) } @@ -249,7 +260,7 @@ impl NamedFile { /// /// # Examples /// - /// ```rust + /// ```rust,ignore /// # use std::io; /// use actix_web::fs::NamedFile; /// @@ -264,13 +275,6 @@ impl NamedFile { self.path.as_path() } - /// Set `CpuPool` to use - #[inline] - pub fn set_cpu_pool(mut self, cpu_pool: CpuPool) -> Self { - self.cpu_pool = Some(cpu_pool); - self - } - /// Set response **Status Code** pub fn set_status_code(mut self, status: StatusCode) -> Self { self.status_code = status; @@ -352,7 +356,7 @@ impl DerefMut for NamedFile { } /// Returns true if `req` has no `If-Match` header or one which matches `etag`. -fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { +fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { match req.get_header::() { None | Some(header::IfMatch::Any) => true, Some(header::IfMatch::Items(ref items)) => { @@ -369,7 +373,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool } /// Returns true if `req` doesn't have an `If-None-Match` header matching `req`. -fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { +fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { match req.get_header::() { Some(header::IfNoneMatch::Any) => false, Some(header::IfNoneMatch::Items(ref items)) => { @@ -387,34 +391,33 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool } impl Responder for NamedFile { - type Item = HttpResponse; - type Error = io::Error; + type Error = Error; + type Future = FutureResult; - fn respond_to(self, req: &HttpRequest) -> Result { + fn respond_to(self, req: &HttpRequest) -> Self::Future { if self.status_code != StatusCode::OK { - let mut resp = HttpResponse::build(self.status_code); + let mut resp = Response::build(self.status_code); resp.set(header::ContentType(self.content_type.clone())) .header( header::CONTENT_DISPOSITION, self.content_disposition.to_string(), ); - - if let Some(current_encoding) = self.encoding { - resp.content_encoding(current_encoding); - } + // TODO blocking by compressing + // if let Some(current_encoding) = self.encoding { + // resp.content_encoding(current_encoding); + // } let reader = ChunkedReadFile { size: self.md.len(), offset: 0, - cpu_pool: self.cpu_pool.unwrap_or_else(|| req.cpu_pool().clone()), file: Some(self.file), fut: None, counter: 0, }; - return Ok(resp.streaming(reader)); + return ok(resp.streaming(reader)); } if !C::is_method_allowed(req.method()) { - return Ok(HttpResponse::MethodNotAllowed() + return ok(Response::MethodNotAllowed() .header(header::CONTENT_TYPE, "text/plain") .header(header::ALLOW, "GET, HEAD") .body("This resource only supports GET and HEAD.")); @@ -451,20 +454,21 @@ impl Responder for NamedFile { false }; - let mut resp = HttpResponse::build(self.status_code); + let mut resp = Response::build(self.status_code); resp.set(header::ContentType(self.content_type.clone())) .header( header::CONTENT_DISPOSITION, self.content_disposition.to_string(), ); - - if let Some(current_encoding) = self.encoding { - resp.content_encoding(current_encoding); - } + // TODO blocking by compressing + // if let Some(current_encoding) = self.encoding { + // resp.content_encoding(current_encoding); + // } resp.if_some(last_modified, |lm, resp| { resp.set(header::LastModified(lm)); - }).if_some(etag, |etag, resp| { + }) + .if_some(etag, |etag, resp| { resp.set(header::ETag(etag)); }); @@ -479,7 +483,8 @@ impl Responder for NamedFile { if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) { length = rangesvec[0].length; offset = rangesvec[0].start; - resp.content_encoding(ContentEncoding::Identity); + // TODO blocking by compressing + // resp.content_encoding(ContentEncoding::Identity); resp.header( header::CONTENT_RANGE, format!( @@ -491,59 +496,66 @@ impl Responder for NamedFile { ); } else { resp.header(header::CONTENT_RANGE, format!("bytes */{}", length)); - return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish()); + return ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish()); }; } else { - return Ok(resp.status(StatusCode::BAD_REQUEST).finish()); + return ok(resp.status(StatusCode::BAD_REQUEST).finish()); }; }; resp.header(header::CONTENT_LENGTH, format!("{}", length)); if precondition_failed { - return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish()); + return ok(resp.status(StatusCode::PRECONDITION_FAILED).finish()); } else if not_modified { - return Ok(resp.status(StatusCode::NOT_MODIFIED).finish()); + return ok(resp.status(StatusCode::NOT_MODIFIED).finish()); } if *req.method() == Method::HEAD { - Ok(resp.finish()) + ok(resp.finish()) } else { let reader = ChunkedReadFile { offset, size: length, - cpu_pool: self.cpu_pool.unwrap_or_else(|| req.cpu_pool().clone()), file: Some(self.file), fut: None, counter: 0, }; if offset != 0 || length != self.md.len() { - return Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader)); + return ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader)); }; - Ok(resp.streaming(reader)) + ok(resp.streaming(reader)) } } } #[doc(hidden)] /// A helper created from a `std::fs::File` which reads the file -/// chunk-by-chunk on a `CpuPool`. +/// chunk-by-chunk on a `ThreadPool`. pub struct ChunkedReadFile { size: u64, offset: u64, - cpu_pool: CpuPool, file: Option, - fut: Option>, + fut: Option>, counter: u64, } +fn handle_error(err: blocking::BlockingError) -> Error { + match err { + blocking::BlockingError::Error(err) => err.into(), + blocking::BlockingError::Canceled => { + ErrorInternalServerError("Unexpected error").into() + } + } +} + impl Stream for ChunkedReadFile { type Item = Bytes; type Error = Error; fn poll(&mut self) -> Poll, Error> { if self.fut.is_some() { - return match self.fut.as_mut().unwrap().poll()? { + return match self.fut.as_mut().unwrap().poll().map_err(handle_error)? { Async::Ready((file, bytes)) => { self.fut.take(); self.file = Some(file); @@ -563,7 +575,7 @@ impl Stream for ChunkedReadFile { Ok(Async::Ready(None)) } else { let mut file = self.file.take().expect("Use after completion"); - self.fut = Some(self.cpu_pool.spawn_fn(move || { + self.fut = Some(blocking::run(move || { let max_bytes: usize; max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; let mut buf = Vec::with_capacity(max_bytes); @@ -580,8 +592,8 @@ impl Stream for ChunkedReadFile { } } -type DirectoryRenderer = - Fn(&Directory, &HttpRequest) -> Result; +type DirectoryRenderer = + Fn(&Directory, &HttpRequest) -> Result; /// A directory; responds with the generated directory listing. #[derive(Debug)] @@ -629,10 +641,10 @@ macro_rules! encode_file_name { }; } -fn directory_listing( +fn directory_listing( dir: &Directory, - req: &HttpRequest, -) -> Result { + req: &HttpRequest, +) -> Result { let index_of = format!("Index of {}", req.path()); let mut body = String::new(); let base = Path::new(req.path()); @@ -677,9 +689,12 @@ fn directory_listing( \n", index_of, index_of, body ); - Ok(HttpResponse::Ok() - .content_type("text/html; charset=utf-8") - .body(html)) + Ok(ServiceResponse::new( + req.clone(), + Response::Ok() + .content_type("text/html; charset=utf-8") + .body(html), + )) } /// Static files handling @@ -687,7 +702,7 @@ fn directory_listing( /// `StaticFile` handler must be registered with `App::handler()` method, /// because `StaticFile` handler requires access sub-path information. /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// use actix_web::{fs, App}; /// @@ -701,9 +716,10 @@ pub struct StaticFiles { directory: PathBuf, index: Option, show_index: bool, - cpu_pool: CpuPool, - default: Box>, - renderer: Box>, + default: Rc< + RefCell, ServiceResponse>>>>, + >, + renderer: Rc, _chunk_size: usize, _follow_symlinks: bool, _cd_map: PhantomData, @@ -712,21 +728,12 @@ pub struct StaticFiles { impl StaticFiles { /// Create new `StaticFiles` instance for specified base directory. /// - /// `StaticFile` uses `CpuPool` for blocking filesystem operations. - /// By default pool with 20 threads is used. + /// `StaticFile` uses `ThreadPool` for blocking filesystem operations. + /// By default pool with 5x threads of available cpus is used. /// Pool size can be changed by setting ACTIX_CPU_POOL environment variable. pub fn new>(dir: T) -> Result, Error> { Self::with_config(dir, DefaultConfig) } - - /// Create new `StaticFiles` instance for specified base directory and - /// `CpuPool`. - pub fn with_pool>( - dir: T, - pool: CpuPool, - ) -> Result, Error> { - Self::with_config_pool(dir, pool, DefaultConfig) - } } impl StaticFiles { @@ -735,36 +742,20 @@ impl StaticFiles { /// Identical with `new` but allows to specify configiration to use. pub fn with_config>( dir: T, - config: C, - ) -> Result, Error> { - // use default CpuPool - let pool = { DEFAULT_CPUPOOL.lock().clone() }; - - StaticFiles::with_config_pool(dir, pool, config) - } - - /// Create new `StaticFiles` instance for specified base directory with config and - /// `CpuPool`. - pub fn with_config_pool>( - dir: T, - pool: CpuPool, _: C, ) -> Result, Error> { let dir = dir.into().canonicalize()?; if !dir.is_dir() { - return Err(StaticFileError::IsNotDirectory.into()); + return Err(StaticFilesError::IsNotDirectory.into()); } Ok(StaticFiles { directory: dir, index: None, show_index: false, - cpu_pool: pool, - default: Box::new(WrapHandler::new(|_: &_| { - HttpResponse::new(StatusCode::NOT_FOUND) - })), - renderer: Box::new(directory_listing), + default: Rc::new(RefCell::new(None)), + renderer: Rc::new(directory_listing), _chunk_size: 0, _follow_symlinks: false, _cd_map: PhantomData, @@ -782,11 +773,11 @@ impl StaticFiles { /// Set custom directory renderer pub fn files_listing_renderer(mut self, f: F) -> Self where - for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) - -> Result - + 'static, + for<'r, 's> F: + Fn(&'r Directory, &'s HttpRequest) -> Result + + 'static, { - self.renderer = Box::new(f); + self.renderer = Rc::new(f); self } @@ -798,54 +789,174 @@ impl StaticFiles { self.index = Some(index.into()); self } +} - /// Sets default handler which is used when no matched file could be found. - pub fn default_handler>(mut self, handler: H) -> StaticFiles { - self.default = Box::new(WrapHandler::new(handler)); - self +impl NewService for StaticFiles { + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Service = StaticFilesService; + type InitError = Error; + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(StaticFilesService { + directory: self.directory.clone(), + index: self.index.clone(), + show_index: self.show_index, + default: self.default.clone(), + renderer: self.renderer.clone(), + _chunk_size: self._chunk_size, + _follow_symlinks: self._follow_symlinks, + _cd_map: self._cd_map, + }) + } +} + +pub struct StaticFilesService { + directory: PathBuf, + index: Option, + show_index: bool, + default: Rc< + RefCell, ServiceResponse>>>>, + >, + renderer: Rc, + _chunk_size: usize, + _follow_symlinks: bool, + _cd_map: PhantomData, +} + +impl Service for StaticFilesService { + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Future = FutureResult; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) } - fn try_handle( - &self, - req: &HttpRequest, - ) -> Result, Error> { - let tail: String = req.match_info().get_decoded("tail").unwrap_or_else(|| "".to_string()); - let relpath = PathBuf::from_param(tail.trim_left_matches('/'))?; - + fn call(&mut self, req: Self::Request) -> Self::Future { + let mut req = req; + let real_path = match PathBuf::from_request(&mut req).poll() { + Ok(Async::Ready(item)) => item, + Ok(Async::NotReady) => unreachable!(), + Err(e) => return err(Error::from(e)), + }; // full filepath - let path = self.directory.join(&relpath).canonicalize()?; + let path = match self.directory.join(&real_path).canonicalize() { + Ok(path) => path, + Err(e) => return err(Error::from(e)), + }; if path.is_dir() { if let Some(ref redir_index) = self.index { let path = path.join(redir_index); - NamedFile::open_with_config(path, C::default())? - .set_cpu_pool(self.cpu_pool.clone()) - .respond_to(&req)? - .respond_to(&req) + match NamedFile::open_with_config(path, C::default()) { + Ok(named_file) => match named_file.respond_to(&req).poll() { + Ok(Async::Ready(item)) => { + ok(ServiceResponse::new(req.clone(), item)) + } + Ok(Async::NotReady) => unreachable!(), + Err(e) => err(Error::from(e)), + }, + Err(e) => err(Error::from(e)), + } } else if self.show_index { let dir = Directory::new(self.directory.clone(), path); - Ok((*self.renderer)(&dir, &req)?.into()) + let x = (self.renderer)(&dir, &req); + match x { + Ok(resp) => ok(resp), + Err(e) => err(Error::from(e)), + } } else { - Err(StaticFileError::IsDirectory.into()) + err(StaticFilesError::IsDirectory.into()) } } else { - NamedFile::open_with_config(path, C::default())? - .set_cpu_pool(self.cpu_pool.clone()) - .respond_to(&req)? - .respond_to(&req) + match NamedFile::open_with_config(path, C::default()) { + Ok(named_file) => match named_file.respond_to(&req).poll() { + Ok(Async::Ready(item)) => { + ok(ServiceResponse::new(req.clone(), item)) + } + Ok(Async::NotReady) => unreachable!(), + Err(e) => err(Error::from(e)), + }, + Err(e) => err(Error::from(e)), + } } } } -impl Handler for StaticFiles { - type Result = Result, Error>; +impl

    FromRequest

    for PathBuf { + type Error = UriSegmentError; + type Future = FutureResult; - fn handle(&self, req: &HttpRequest) -> Self::Result { - self.try_handle(req).or_else(|e| { - debug!("StaticFiles: Failed to handle {}: {}", req.path(), e); - Ok(self.default.handle(req)) - }) + fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + let path_str = req.match_info().path(); + let mut buf = PathBuf::new(); + for segment in path_str.split('/') { + if segment == ".." { + buf.pop(); + } else if segment.starts_with('.') { + return err(UriSegmentError::BadStart('.')); + } else if segment.starts_with('*') { + return err(UriSegmentError::BadStart('*')); + } else if segment.ends_with(':') { + return err(UriSegmentError::BadEnd(':')); + } else if segment.ends_with('>') { + return err(UriSegmentError::BadEnd('>')); + } else if segment.ends_with('<') { + return err(UriSegmentError::BadEnd('<')); + } else if segment.is_empty() { + continue; + } else if cfg!(windows) && segment.contains('\\') { + return err(UriSegmentError::BadChar('\\')); + } else { + buf.push(segment) + } + } + + ok(buf) + } +} + +/// Errors which can occur when serving static files. +#[derive(Display, Debug, PartialEq)] +enum StaticFilesError { + /// Path is not a directory + #[display(fmt = "Path is not a directory. Unable to serve static files")] + IsNotDirectory, + + /// Cannot render directory + #[display(fmt = "Unable to render directory without index file")] + IsDirectory, +} + +/// Return `NotFound` for `StaticFilesError` +impl ResponseError for StaticFilesError { + fn error_response(&self) -> Response { + Response::new(StatusCode::NOT_FOUND) + } +} + +#[derive(Display, Debug, PartialEq)] +pub enum UriSegmentError { + /// The segment started with the wrapped invalid character. + #[display(fmt = "The segment started with the wrapped invalid character")] + BadStart(char), + /// The segment contained the wrapped invalid character. + #[display(fmt = "The segment contained the wrapped invalid character")] + BadChar(char), + /// The segment ended with the wrapped invalid character. + #[display(fmt = "The segment ended with the wrapped invalid character")] + BadEnd(char), +} + +/// Return `BadRequest` for `UriSegmentError` +impl ResponseError for UriSegmentError { + fn error_response(&self) -> Response { + Response::new(StatusCode::BAD_REQUEST) } } @@ -888,7 +999,7 @@ impl HttpRange { if start_str.is_empty() { // If no start is specified, end specifies the // range start relative to the end of the file. - let mut length: i64 = try!(end_str.parse().map_err(|_| ())); + let mut length: i64 = end_str.parse().map_err(|_| ())?; if length > size_sig { length = size_sig; @@ -931,7 +1042,8 @@ impl HttpRange { length: length as u64, })) } - }).collect::>()?; + }) + .collect::>()?; let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); @@ -943,985 +1055,981 @@ impl HttpRange { } } -#[cfg(test)] -mod tests { - use std::fs; - use std::time::Duration; - use std::ops::Add; - - use super::*; - use application::App; - use body::{Binary, Body}; - use http::{header, Method, StatusCode}; - use test::{self, TestRequest}; - - #[test] - fn test_file_extension_to_mime() { - let m = file_extension_to_mime("jpg"); - assert_eq!(m, mime::IMAGE_JPEG); - - let m = file_extension_to_mime("invalid extension!!"); - assert_eq!(m, mime::APPLICATION_OCTET_STREAM); - - let m = file_extension_to_mime(""); - assert_eq!(m, mime::APPLICATION_OCTET_STREAM); - } - - #[test] - fn test_if_modified_since_without_if_none_match() { - let mut file = NamedFile::open("Cargo.toml") - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - let since = header::HttpDate::from( - SystemTime::now().add(Duration::from_secs(60))); - - let req = TestRequest::default() - .header(header::IF_MODIFIED_SINCE, since) - .finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.status(), - StatusCode::NOT_MODIFIED - ); - } - - #[test] - fn test_if_modified_since_with_if_none_match() { - let mut file = NamedFile::open("Cargo.toml") - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - let since = header::HttpDate::from( - SystemTime::now().add(Duration::from_secs(60))); - - let req = TestRequest::default() - .header(header::IF_NONE_MATCH, "miss_etag") - .header(header::IF_MODIFIED_SINCE, since) - .finish(); - let resp = file.respond_to(&req).unwrap(); - assert_ne!( - resp.status(), - StatusCode::NOT_MODIFIED - ); - } - - #[test] - fn test_named_file_text() { - assert!(NamedFile::open("test--").is_err()); - let mut file = NamedFile::open("Cargo.toml") - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/x-toml" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"Cargo.toml\"" - ); - } - - #[test] - fn test_named_file_set_content_type() { - let mut file = NamedFile::open("Cargo.toml") - .unwrap() - .set_content_type(mime::TEXT_XML) - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/xml" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"Cargo.toml\"" - ); - } - - #[test] - fn test_named_file_image() { - let mut file = NamedFile::open("tests/test.png") - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "image/png" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"test.png\"" - ); - } - - #[test] - fn test_named_file_image_attachment() { - use header::{ContentDisposition, DispositionParam, DispositionType}; - let cd = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::Filename(String::from("test.png"))], - }; - let mut file = NamedFile::open("tests/test.png") - .unwrap() - .set_content_disposition(cd) - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "image/png" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "attachment; filename=\"test.png\"" - ); - } - - #[derive(Default)] - pub struct AllAttachmentConfig; - impl StaticFileConfig for AllAttachmentConfig { - fn content_disposition_map(_typ: mime::Name) -> DispositionType { - DispositionType::Attachment - } - } - - #[derive(Default)] - pub struct AllInlineConfig; - impl StaticFileConfig for AllInlineConfig { - fn content_disposition_map(_typ: mime::Name) -> DispositionType { - DispositionType::Inline - } - } - - #[test] - fn test_named_file_image_attachment_and_custom_config() { - let file = NamedFile::open_with_config("tests/test.png", AllAttachmentConfig) - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "image/png" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "attachment; filename=\"test.png\"" - ); - - let file = NamedFile::open_with_config("tests/test.png", AllInlineConfig) - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "image/png" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"test.png\"" - ); - } - - #[test] - fn test_named_file_binary() { - let mut file = NamedFile::open("tests/test.binary") - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "application/octet-stream" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "attachment; filename=\"test.binary\"" - ); - } - - #[test] - fn test_named_file_status_code_text() { - let mut file = NamedFile::open("Cargo.toml") - .unwrap() - .set_status_code(StatusCode::NOT_FOUND) - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/x-toml" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"Cargo.toml\"" - ); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_named_file_ranges_status_code() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".").unwrap().index_file("Cargo.toml"), - ) - }); - - // Valid range header - let request = srv - .get() - .uri(srv.url("/t%65st/Cargo.toml")) - .header(header::RANGE, "bytes=10-20") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); - - // Invalid range header - let request = srv - .get() - .uri(srv.url("/t%65st/Cargo.toml")) - .header(header::RANGE, "bytes=1-0") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - - assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); - } - - #[test] - fn test_named_file_content_range_headers() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".") - .unwrap() - .index_file("tests/test.binary"), - ) - }); - - // Valid range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .header(header::RANGE, "bytes=10-20") - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentrange = response - .headers() - .get(header::CONTENT_RANGE) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentrange, "bytes 10-20/100"); - - // Invalid range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .header(header::RANGE, "bytes=10-5") - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentrange = response - .headers() - .get(header::CONTENT_RANGE) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentrange, "bytes */100"); - } - - #[test] - fn test_named_file_content_length_headers() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".") - .unwrap() - .index_file("tests/test.binary"), - ) - }); - - // Valid range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .header(header::RANGE, "bytes=10-20") - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentlength = response - .headers() - .get(header::CONTENT_LENGTH) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentlength, "11"); - - // Invalid range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .header(header::RANGE, "bytes=10-8") - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentlength = response - .headers() - .get(header::CONTENT_LENGTH) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentlength, "0"); - - // Without range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .no_default_headers() - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentlength = response - .headers() - .get(header::CONTENT_LENGTH) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentlength, "100"); - - // chunked - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - { - let te = response - .headers() - .get(header::TRANSFER_ENCODING) - .unwrap() - .to_str() - .unwrap(); - assert_eq!(te, "chunked"); - } - let bytes = srv.execute(response.body()).unwrap(); - let data = Bytes::from(fs::read("tests/test.binary").unwrap()); - assert_eq!(bytes, data); - } - - #[test] - fn test_static_files_with_spaces() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "/", - StaticFiles::new(".").unwrap().index_file("Cargo.toml"), - ) - }); - let request = srv - .get() - .uri(srv.url("/tests/test%20space.binary")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - let bytes = srv.execute(response.body()).unwrap(); - let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); - assert_eq!(bytes, data); - } - - #[derive(Default)] - pub struct OnlyMethodHeadConfig; - impl StaticFileConfig for OnlyMethodHeadConfig { - fn is_method_allowed(method: &Method) -> bool { - match *method { - Method::HEAD => true, - _ => false, - } - } - } - - #[test] - fn test_named_file_not_allowed() { - let file = - NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); - let req = TestRequest::default().method(Method::POST).finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - - let file = - NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); - let req = TestRequest::default().method(Method::PUT).finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - - let file = - NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); - let req = TestRequest::default().method(Method::GET).finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - } - - #[test] - fn test_named_file_content_encoding() { - let req = TestRequest::default().method(Method::GET).finish(); - let file = NamedFile::open("Cargo.toml").unwrap(); - - assert!(file.encoding.is_none()); - let resp = file - .set_content_encoding(ContentEncoding::Identity) - .respond_to(&req) - .unwrap(); - - assert!(resp.content_encoding().is_some()); - assert_eq!(resp.content_encoding().unwrap().as_str(), "identity"); - } - - #[test] - fn test_named_file_any_method() { - let req = TestRequest::default().method(Method::POST).finish(); - let file = NamedFile::open("Cargo.toml").unwrap(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - } - - #[test] - fn test_static_files() { - let mut st = StaticFiles::new(".").unwrap().show_files_listing(); - let req = TestRequest::with_uri("/missing") - .param("tail", "missing") - .finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - - st.show_index = false; - let req = TestRequest::default().finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - - let req = TestRequest::default().param("tail", "").finish(); - - st.show_index = true; - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/html; charset=utf-8" - ); - assert!(resp.body().is_binary()); - assert!(format!("{:?}", resp.body()).contains("README.md")); - } - - #[test] - fn test_static_files_bad_directory() { - let st: Result, Error> = StaticFiles::new("missing"); - assert!(st.is_err()); - - let st: Result, Error> = StaticFiles::new("Cargo.toml"); - assert!(st.is_err()); - } - - #[test] - fn test_default_handler_file_missing() { - let st = StaticFiles::new(".") - .unwrap() - .default_handler(|_: &_| "default content"); - let req = TestRequest::with_uri("/missing") - .param("tail", "missing") - .finish(); - - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.body(), - &Body::Binary(Binary::Slice(b"default content")) - ); - } - - #[test] - fn test_serve_index() { - let st = StaticFiles::new(".").unwrap().index_file("test.binary"); - let req = TestRequest::default().uri("/tests").finish(); - - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).expect("content type"), - "application/octet-stream" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).expect("content disposition"), - "attachment; filename=\"test.binary\"" - ); - - let req = TestRequest::default().uri("/tests/").finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "application/octet-stream" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "attachment; filename=\"test.binary\"" - ); - - // nonexistent index file - let req = TestRequest::default().uri("/tests/unknown").finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - - let req = TestRequest::default().uri("/tests/unknown/").finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_serve_index_nested() { - let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); - let req = TestRequest::default().uri("/src/client").finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/x-rust" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"mod.rs\"" - ); - } - - #[test] - fn integration_serve_index_with_prefix() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .prefix("public") - .handler("/", StaticFiles::new(".").unwrap().index_file("Cargo.toml")) - }); - - let request = srv.get().uri(srv.url("/public")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let bytes = srv.execute(response.body()).unwrap(); - let data = Bytes::from(fs::read("Cargo.toml").unwrap()); - assert_eq!(bytes, data); - - let request = srv.get().uri(srv.url("/public/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let bytes = srv.execute(response.body()).unwrap(); - let data = Bytes::from(fs::read("Cargo.toml").unwrap()); - assert_eq!(bytes, data); - } - - #[test] - fn integration_serve_index() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".").unwrap().index_file("Cargo.toml"), - ) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let bytes = srv.execute(response.body()).unwrap(); - let data = Bytes::from(fs::read("Cargo.toml").unwrap()); - assert_eq!(bytes, data); - - let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let bytes = srv.execute(response.body()).unwrap(); - let data = Bytes::from(fs::read("Cargo.toml").unwrap()); - assert_eq!(bytes, data); - - // nonexistent index file - let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); - - let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[test] - fn integration_percent_encoded() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".").unwrap().index_file("Cargo.toml"), - ) - }); - - let request = srv - .get() - .uri(srv.url("/test/%43argo.toml")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - } - - struct T(&'static str, u64, Vec); - - #[test] - fn test_parse() { - let tests = vec![ - T("", 0, vec![]), - T("", 1000, vec![]), - T("foo", 0, vec![]), - T("bytes=", 0, vec![]), - T("bytes=7", 10, vec![]), - T("bytes= 7 ", 10, vec![]), - T("bytes=1-", 0, vec![]), - T("bytes=5-4", 10, vec![]), - T("bytes=0-2,5-4", 10, vec![]), - T("bytes=2-5,4-3", 10, vec![]), - T("bytes=--5,4--3", 10, vec![]), - T("bytes=A-", 10, vec![]), - T("bytes=A- ", 10, vec![]), - T("bytes=A-Z", 10, vec![]), - T("bytes= -Z", 10, vec![]), - T("bytes=5-Z", 10, vec![]), - T("bytes=Ran-dom, garbage", 10, vec![]), - T("bytes=0x01-0x02", 10, vec![]), - T("bytes= ", 10, vec![]), - T("bytes= , , , ", 10, vec![]), - T( - "bytes=0-9", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=0-", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=5-", - 10, - vec![HttpRange { - start: 5, - length: 5, - }], - ), - T( - "bytes=0-20", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=15-,0-5", - 10, - vec![HttpRange { - start: 0, - length: 6, - }], - ), - T( - "bytes=1-2,5-", - 10, - vec![ - HttpRange { - start: 1, - length: 2, - }, - HttpRange { - start: 5, - length: 5, - }, - ], - ), - T( - "bytes=-2 , 7-", - 11, - vec![ - HttpRange { - start: 9, - length: 2, - }, - HttpRange { - start: 7, - length: 4, - }, - ], - ), - T( - "bytes=0-0 ,2-2, 7-", - 11, - vec![ - HttpRange { - start: 0, - length: 1, - }, - HttpRange { - start: 2, - length: 1, - }, - HttpRange { - start: 7, - length: 4, - }, - ], - ), - T( - "bytes=-5", - 10, - vec![HttpRange { - start: 5, - length: 5, - }], - ), - T( - "bytes=-15", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=0-499", - 10000, - vec![HttpRange { - start: 0, - length: 500, - }], - ), - T( - "bytes=500-999", - 10000, - vec![HttpRange { - start: 500, - length: 500, - }], - ), - T( - "bytes=-500", - 10000, - vec![HttpRange { - start: 9500, - length: 500, - }], - ), - T( - "bytes=9500-", - 10000, - vec![HttpRange { - start: 9500, - length: 500, - }], - ), - T( - "bytes=0-0,-1", - 10000, - vec![ - HttpRange { - start: 0, - length: 1, - }, - HttpRange { - start: 9999, - length: 1, - }, - ], - ), - T( - "bytes=500-600,601-999", - 10000, - vec![ - HttpRange { - start: 500, - length: 101, - }, - HttpRange { - start: 601, - length: 399, - }, - ], - ), - T( - "bytes=500-700,601-999", - 10000, - vec![ - HttpRange { - start: 500, - length: 201, - }, - HttpRange { - start: 601, - length: 399, - }, - ], - ), - // Match Apache laxity: - T( - "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", - 11, - vec![ - HttpRange { - start: 1, - length: 2, - }, - HttpRange { - start: 4, - length: 2, - }, - HttpRange { - start: 7, - length: 2, - }, - ], - ), - ]; - - for t in tests { - let header = t.0; - let size = t.1; - let expected = t.2; - - let res = HttpRange::parse(header, size); - - if res.is_err() { - if expected.is_empty() { - continue; - } else { - assert!( - false, - "parse({}, {}) returned error {:?}", - header, - size, - res.unwrap_err() - ); - } - } - - let got = res.unwrap(); - - if got.len() != expected.len() { - assert!( - false, - "len(parseRange({}, {})) = {}, want {}", - header, - size, - got.len(), - expected.len() - ); - continue; - } - - for i in 0..expected.len() { - if got[i].start != expected[i].start { - assert!( - false, - "parseRange({}, {})[{}].start = {}, want {}", - header, size, i, got[i].start, expected[i].start - ) - } - if got[i].length != expected[i].length { - assert!( - false, - "parseRange({}, {})[{}].length = {}, want {}", - header, size, i, got[i].length, expected[i].length - ) - } - } - } - } -} +// #[cfg(test)] +// mod tests { +// use std::fs; +// use std::ops::Add; +// use std::time::Duration; + +// use super::*; +// use application::App; +// use body::{Binary, Body}; +// use http::{header, Method, StatusCode}; +// use test::{self, TestRequest}; + +// #[test] +// fn test_file_extension_to_mime() { +// let m = file_extension_to_mime("jpg"); +// assert_eq!(m, mime::IMAGE_JPEG); + +// let m = file_extension_to_mime("invalid extension!!"); +// assert_eq!(m, mime::APPLICATION_OCTET_STREAM); + +// let m = file_extension_to_mime(""); +// assert_eq!(m, mime::APPLICATION_OCTET_STREAM); +// } + +// #[test] +// fn test_if_modified_since_without_if_none_match() { +// let mut file = NamedFile::open("Cargo.toml") +// .unwrap() +// .set_cpu_pool(CpuPool::new(1)); +// let since = +// header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + +// let req = TestRequest::default() +// .header(header::IF_MODIFIED_SINCE, since) +// .finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); +// } + +// #[test] +// fn test_if_modified_since_with_if_none_match() { +// let mut file = NamedFile::open("Cargo.toml") +// .unwrap() +// .set_cpu_pool(CpuPool::new(1)); +// let since = +// header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + +// let req = TestRequest::default() +// .header(header::IF_NONE_MATCH, "miss_etag") +// .header(header::IF_MODIFIED_SINCE, since) +// .finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); +// } + +// #[test] +// fn test_named_file_text() { +// assert!(NamedFile::open("test--").is_err()); +// let mut file = NamedFile::open("Cargo.toml") +// .unwrap() +// .set_cpu_pool(CpuPool::new(1)); +// { +// file.file(); +// let _f: &File = &file; +// } +// { +// let _f: &mut File = &mut file; +// } + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "text/x-toml" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "inline; filename=\"Cargo.toml\"" +// ); +// } + +// #[test] +// fn test_named_file_set_content_type() { +// let mut file = NamedFile::open("Cargo.toml") +// .unwrap() +// .set_content_type(mime::TEXT_XML) +// .set_cpu_pool(CpuPool::new(1)); +// { +// file.file(); +// let _f: &File = &file; +// } +// { +// let _f: &mut File = &mut file; +// } + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "text/xml" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "inline; filename=\"Cargo.toml\"" +// ); +// } + +// #[test] +// fn test_named_file_image() { +// let mut file = NamedFile::open("tests/test.png") +// .unwrap() +// .set_cpu_pool(CpuPool::new(1)); +// { +// file.file(); +// let _f: &File = &file; +// } +// { +// let _f: &mut File = &mut file; +// } + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "image/png" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "inline; filename=\"test.png\"" +// ); +// } + +// #[test] +// fn test_named_file_image_attachment() { +// use header::{ContentDisposition, DispositionParam, DispositionType}; +// let cd = ContentDisposition { +// disposition: DispositionType::Attachment, +// parameters: vec![DispositionParam::Filename(String::from("test.png"))], +// }; +// let mut file = NamedFile::open("tests/test.png") +// .unwrap() +// .set_content_disposition(cd) +// .set_cpu_pool(CpuPool::new(1)); +// { +// file.file(); +// let _f: &File = &file; +// } +// { +// let _f: &mut File = &mut file; +// } + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "image/png" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "attachment; filename=\"test.png\"" +// ); +// } + +// #[derive(Default)] +// pub struct AllAttachmentConfig; +// impl StaticFileConfig for AllAttachmentConfig { +// fn content_disposition_map(_typ: mime::Name) -> DispositionType { +// DispositionType::Attachment +// } +// } + +// #[derive(Default)] +// pub struct AllInlineConfig; +// impl StaticFileConfig for AllInlineConfig { +// fn content_disposition_map(_typ: mime::Name) -> DispositionType { +// DispositionType::Inline +// } +// } + +// #[test] +// fn test_named_file_image_attachment_and_custom_config() { +// let file = NamedFile::open_with_config("tests/test.png", AllAttachmentConfig) +// .unwrap() +// .set_cpu_pool(CpuPool::new(1)); + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "image/png" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "attachment; filename=\"test.png\"" +// ); + +// let file = NamedFile::open_with_config("tests/test.png", AllInlineConfig) +// .unwrap() +// .set_cpu_pool(CpuPool::new(1)); + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "image/png" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "inline; filename=\"test.png\"" +// ); +// } + +// #[test] +// fn test_named_file_binary() { +// let mut file = NamedFile::open("tests/test.binary") +// .unwrap() +// .set_cpu_pool(CpuPool::new(1)); +// { +// file.file(); +// let _f: &File = &file; +// } +// { +// let _f: &mut File = &mut file; +// } + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "application/octet-stream" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "attachment; filename=\"test.binary\"" +// ); +// } + +// #[test] +// fn test_named_file_status_code_text() { +// let mut file = NamedFile::open("Cargo.toml") +// .unwrap() +// .set_status_code(StatusCode::NOT_FOUND) +// .set_cpu_pool(CpuPool::new(1)); +// { +// file.file(); +// let _f: &File = &file; +// } +// { +// let _f: &mut File = &mut file; +// } + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "text/x-toml" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "inline; filename=\"Cargo.toml\"" +// ); +// assert_eq!(resp.status(), StatusCode::NOT_FOUND); +// } + +// #[test] +// fn test_named_file_ranges_status_code() { +// let mut srv = test::TestServer::with_factory(|| { +// App::new().handler( +// "test", +// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), +// ) +// }); + +// // Valid range header +// let request = srv +// .get() +// .uri(srv.url("/t%65st/Cargo.toml")) +// .header(header::RANGE, "bytes=10-20") +// .finish() +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + +// // Invalid range header +// let request = srv +// .get() +// .uri(srv.url("/t%65st/Cargo.toml")) +// .header(header::RANGE, "bytes=1-0") +// .finish() +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); + +// assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); +// } + +// #[test] +// fn test_named_file_content_range_headers() { +// let mut srv = test::TestServer::with_factory(|| { +// App::new().handler( +// "test", +// StaticFiles::new(".") +// .unwrap() +// .index_file("tests/test.binary"), +// ) +// }); + +// // Valid range header +// let request = srv +// .get() +// .uri(srv.url("/t%65st/tests/test.binary")) +// .header(header::RANGE, "bytes=10-20") +// .finish() +// .unwrap(); + +// let response = srv.execute(request.send()).unwrap(); + +// let contentrange = response +// .headers() +// .get(header::CONTENT_RANGE) +// .unwrap() +// .to_str() +// .unwrap(); + +// assert_eq!(contentrange, "bytes 10-20/100"); + +// // Invalid range header +// let request = srv +// .get() +// .uri(srv.url("/t%65st/tests/test.binary")) +// .header(header::RANGE, "bytes=10-5") +// .finish() +// .unwrap(); + +// let response = srv.execute(request.send()).unwrap(); + +// let contentrange = response +// .headers() +// .get(header::CONTENT_RANGE) +// .unwrap() +// .to_str() +// .unwrap(); + +// assert_eq!(contentrange, "bytes */100"); +// } + +// #[test] +// fn test_named_file_content_length_headers() { +// let mut srv = test::TestServer::with_factory(|| { +// App::new().handler( +// "test", +// StaticFiles::new(".") +// .unwrap() +// .index_file("tests/test.binary"), +// ) +// }); + +// // Valid range header +// let request = srv +// .get() +// .uri(srv.url("/t%65st/tests/test.binary")) +// .header(header::RANGE, "bytes=10-20") +// .finish() +// .unwrap(); + +// let response = srv.execute(request.send()).unwrap(); + +// let contentlength = response +// .headers() +// .get(header::CONTENT_LENGTH) +// .unwrap() +// .to_str() +// .unwrap(); + +// assert_eq!(contentlength, "11"); + +// // Invalid range header +// let request = srv +// .get() +// .uri(srv.url("/t%65st/tests/test.binary")) +// .header(header::RANGE, "bytes=10-8") +// .finish() +// .unwrap(); + +// let response = srv.execute(request.send()).unwrap(); + +// let contentlength = response +// .headers() +// .get(header::CONTENT_LENGTH) +// .unwrap() +// .to_str() +// .unwrap(); + +// assert_eq!(contentlength, "0"); + +// // Without range header +// let request = srv +// .get() +// .uri(srv.url("/t%65st/tests/test.binary")) +// .no_default_headers() +// .finish() +// .unwrap(); + +// let response = srv.execute(request.send()).unwrap(); + +// let contentlength = response +// .headers() +// .get(header::CONTENT_LENGTH) +// .unwrap() +// .to_str() +// .unwrap(); + +// assert_eq!(contentlength, "100"); + +// // chunked +// let request = srv +// .get() +// .uri(srv.url("/t%65st/tests/test.binary")) +// .finish() +// .unwrap(); + +// let response = srv.execute(request.send()).unwrap(); +// { +// let te = response +// .headers() +// .get(header::TRANSFER_ENCODING) +// .unwrap() +// .to_str() +// .unwrap(); +// assert_eq!(te, "chunked"); +// } +// let bytes = srv.execute(response.body()).unwrap(); +// let data = Bytes::from(fs::read("tests/test.binary").unwrap()); +// assert_eq!(bytes, data); +// } + +// #[test] +// fn test_static_files_with_spaces() { +// let mut srv = test::TestServer::with_factory(|| { +// App::new() +// .handler("/", StaticFiles::new(".").unwrap().index_file("Cargo.toml")) +// }); +// let request = srv +// .get() +// .uri(srv.url("/tests/test%20space.binary")) +// .finish() +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::OK); + +// let bytes = srv.execute(response.body()).unwrap(); +// let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); +// assert_eq!(bytes, data); +// } + +// #[derive(Default)] +// pub struct OnlyMethodHeadConfig; +// impl StaticFileConfig for OnlyMethodHeadConfig { +// fn is_method_allowed(method: &Method) -> bool { +// match *method { +// Method::HEAD => true, +// _ => false, +// } +// } +// } + +// #[test] +// fn test_named_file_not_allowed() { +// let file = +// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); +// let req = TestRequest::default().method(Method::POST).finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + +// let file = +// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); +// let req = TestRequest::default().method(Method::PUT).finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + +// let file = +// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); +// let req = TestRequest::default().method(Method::GET).finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); +// } + +// #[test] +// fn test_named_file_content_encoding() { +// let req = TestRequest::default().method(Method::GET).finish(); +// let file = NamedFile::open("Cargo.toml").unwrap(); + +// assert!(file.encoding.is_none()); +// let resp = file +// .set_content_encoding(ContentEncoding::Identity) +// .respond_to(&req) +// .unwrap(); + +// assert!(resp.content_encoding().is_some()); +// assert_eq!(resp.content_encoding().unwrap().as_str(), "identity"); +// } + +// #[test] +// fn test_named_file_any_method() { +// let req = TestRequest::default().method(Method::POST).finish(); +// let file = NamedFile::open("Cargo.toml").unwrap(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!(resp.status(), StatusCode::OK); +// } + +// #[test] +// fn test_static_files() { +// let mut st = StaticFiles::new(".").unwrap().show_files_listing(); +// let req = TestRequest::with_uri("/missing") +// .param("tail", "missing") +// .finish(); +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::NOT_FOUND); + +// st.show_index = false; +// let req = TestRequest::default().finish(); +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::NOT_FOUND); + +// let req = TestRequest::default().param("tail", "").finish(); + +// st.show_index = true; +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "text/html; charset=utf-8" +// ); +// assert!(resp.body().is_binary()); +// assert!(format!("{:?}", resp.body()).contains("README.md")); +// } + +// #[test] +// fn test_static_files_bad_directory() { +// let st: Result, Error> = StaticFiles::new("missing"); +// assert!(st.is_err()); + +// let st: Result, Error> = StaticFiles::new("Cargo.toml"); +// assert!(st.is_err()); +// } + +// #[test] +// fn test_default_handler_file_missing() { +// let st = StaticFiles::new(".") +// .unwrap() +// .default_handler(|_: &_| "default content"); +// let req = TestRequest::with_uri("/missing") +// .param("tail", "missing") +// .finish(); + +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::OK); +// assert_eq!( +// resp.body(), +// &Body::Binary(Binary::Slice(b"default content")) +// ); +// } + +// #[test] +// fn test_serve_index() { +// let st = StaticFiles::new(".").unwrap().index_file("test.binary"); +// let req = TestRequest::default().uri("/tests").finish(); + +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::OK); +// assert_eq!( +// resp.headers() +// .get(header::CONTENT_TYPE) +// .expect("content type"), +// "application/octet-stream" +// ); +// assert_eq!( +// resp.headers() +// .get(header::CONTENT_DISPOSITION) +// .expect("content disposition"), +// "attachment; filename=\"test.binary\"" +// ); + +// let req = TestRequest::default().uri("/tests/").finish(); +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::OK); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "application/octet-stream" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "attachment; filename=\"test.binary\"" +// ); + +// // nonexistent index file +// let req = TestRequest::default().uri("/tests/unknown").finish(); +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::NOT_FOUND); + +// let req = TestRequest::default().uri("/tests/unknown/").finish(); +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::NOT_FOUND); +// } + +// #[test] +// fn test_serve_index_nested() { +// let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); +// let req = TestRequest::default().uri("/src/client").finish(); +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::OK); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "text/x-rust" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "inline; filename=\"mod.rs\"" +// ); +// } + +// #[test] +// fn integration_serve_index_with_prefix() { +// let mut srv = test::TestServer::with_factory(|| { +// App::new() +// .prefix("public") +// .handler("/", StaticFiles::new(".").unwrap().index_file("Cargo.toml")) +// }); + +// let request = srv.get().uri(srv.url("/public")).finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::OK); +// let bytes = srv.execute(response.body()).unwrap(); +// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); +// assert_eq!(bytes, data); + +// let request = srv.get().uri(srv.url("/public/")).finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::OK); +// let bytes = srv.execute(response.body()).unwrap(); +// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); +// assert_eq!(bytes, data); +// } + +// #[test] +// fn integration_serve_index() { +// let mut srv = test::TestServer::with_factory(|| { +// App::new().handler( +// "test", +// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), +// ) +// }); + +// let request = srv.get().uri(srv.url("/test")).finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::OK); +// let bytes = srv.execute(response.body()).unwrap(); +// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); +// assert_eq!(bytes, data); + +// let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::OK); +// let bytes = srv.execute(response.body()).unwrap(); +// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); +// assert_eq!(bytes, data); + +// // nonexistent index file +// let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::NOT_FOUND); + +// let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::NOT_FOUND); +// } + +// #[test] +// fn integration_percent_encoded() { +// let mut srv = test::TestServer::with_factory(|| { +// App::new().handler( +// "test", +// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), +// ) +// }); + +// let request = srv +// .get() +// .uri(srv.url("/test/%43argo.toml")) +// .finish() +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::OK); +// } + +// struct T(&'static str, u64, Vec); + +// #[test] +// fn test_parse() { +// let tests = vec![ +// T("", 0, vec![]), +// T("", 1000, vec![]), +// T("foo", 0, vec![]), +// T("bytes=", 0, vec![]), +// T("bytes=7", 10, vec![]), +// T("bytes= 7 ", 10, vec![]), +// T("bytes=1-", 0, vec![]), +// T("bytes=5-4", 10, vec![]), +// T("bytes=0-2,5-4", 10, vec![]), +// T("bytes=2-5,4-3", 10, vec![]), +// T("bytes=--5,4--3", 10, vec![]), +// T("bytes=A-", 10, vec![]), +// T("bytes=A- ", 10, vec![]), +// T("bytes=A-Z", 10, vec![]), +// T("bytes= -Z", 10, vec![]), +// T("bytes=5-Z", 10, vec![]), +// T("bytes=Ran-dom, garbage", 10, vec![]), +// T("bytes=0x01-0x02", 10, vec![]), +// T("bytes= ", 10, vec![]), +// T("bytes= , , , ", 10, vec![]), +// T( +// "bytes=0-9", +// 10, +// vec![HttpRange { +// start: 0, +// length: 10, +// }], +// ), +// T( +// "bytes=0-", +// 10, +// vec![HttpRange { +// start: 0, +// length: 10, +// }], +// ), +// T( +// "bytes=5-", +// 10, +// vec![HttpRange { +// start: 5, +// length: 5, +// }], +// ), +// T( +// "bytes=0-20", +// 10, +// vec![HttpRange { +// start: 0, +// length: 10, +// }], +// ), +// T( +// "bytes=15-,0-5", +// 10, +// vec![HttpRange { +// start: 0, +// length: 6, +// }], +// ), +// T( +// "bytes=1-2,5-", +// 10, +// vec![ +// HttpRange { +// start: 1, +// length: 2, +// }, +// HttpRange { +// start: 5, +// length: 5, +// }, +// ], +// ), +// T( +// "bytes=-2 , 7-", +// 11, +// vec![ +// HttpRange { +// start: 9, +// length: 2, +// }, +// HttpRange { +// start: 7, +// length: 4, +// }, +// ], +// ), +// T( +// "bytes=0-0 ,2-2, 7-", +// 11, +// vec![ +// HttpRange { +// start: 0, +// length: 1, +// }, +// HttpRange { +// start: 2, +// length: 1, +// }, +// HttpRange { +// start: 7, +// length: 4, +// }, +// ], +// ), +// T( +// "bytes=-5", +// 10, +// vec![HttpRange { +// start: 5, +// length: 5, +// }], +// ), +// T( +// "bytes=-15", +// 10, +// vec![HttpRange { +// start: 0, +// length: 10, +// }], +// ), +// T( +// "bytes=0-499", +// 10000, +// vec![HttpRange { +// start: 0, +// length: 500, +// }], +// ), +// T( +// "bytes=500-999", +// 10000, +// vec![HttpRange { +// start: 500, +// length: 500, +// }], +// ), +// T( +// "bytes=-500", +// 10000, +// vec![HttpRange { +// start: 9500, +// length: 500, +// }], +// ), +// T( +// "bytes=9500-", +// 10000, +// vec![HttpRange { +// start: 9500, +// length: 500, +// }], +// ), +// T( +// "bytes=0-0,-1", +// 10000, +// vec![ +// HttpRange { +// start: 0, +// length: 1, +// }, +// HttpRange { +// start: 9999, +// length: 1, +// }, +// ], +// ), +// T( +// "bytes=500-600,601-999", +// 10000, +// vec![ +// HttpRange { +// start: 500, +// length: 101, +// }, +// HttpRange { +// start: 601, +// length: 399, +// }, +// ], +// ), +// T( +// "bytes=500-700,601-999", +// 10000, +// vec![ +// HttpRange { +// start: 500, +// length: 201, +// }, +// HttpRange { +// start: 601, +// length: 399, +// }, +// ], +// ), +// // Match Apache laxity: +// T( +// "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", +// 11, +// vec![ +// HttpRange { +// start: 1, +// length: 2, +// }, +// HttpRange { +// start: 4, +// length: 2, +// }, +// HttpRange { +// start: 7, +// length: 2, +// }, +// ], +// ), +// ]; + +// for t in tests { +// let header = t.0; +// let size = t.1; +// let expected = t.2; + +// let res = HttpRange::parse(header, size); + +// if res.is_err() { +// if expected.is_empty() { +// continue; +// } else { +// assert!( +// false, +// "parse({}, {}) returned error {:?}", +// header, +// size, +// res.unwrap_err() +// ); +// } +// } + +// let got = res.unwrap(); + +// if got.len() != expected.len() { +// assert!( +// false, +// "len(parseRange({}, {})) = {}, want {}", +// header, +// size, +// got.len(), +// expected.len() +// ); +// continue; +// } + +// for i in 0..expected.len() { +// if got[i].start != expected[i].start { +// assert!( +// false, +// "parseRange({}, {})[{}].start = {}, want {}", +// header, size, i, got[i].start, expected[i].start +// ) +// } +// if got[i].length != expected[i].length { +// assert!( +// false, +// "parseRange({}, {})[{}].length = {}, want {}", +// header, size, i, got[i].length, expected[i].length +// ) +// } +// } +// } +// } +// } diff --git a/src/handler.rs b/src/handler.rs index c68808181..e957d15e7 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,562 +1,402 @@ use std::marker::PhantomData; -use std::ops::Deref; -use futures::future::{err, ok, Future}; -use futures::{Async, Poll}; +use actix_http::{Error, Response}; +use actix_service::{NewService, Service}; +use actix_utils::Never; +use futures::future::{ok, FutureResult}; +use futures::{try_ready, Async, Future, IntoFuture, Poll}; -use error::Error; -use http::StatusCode; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use resource::DefaultResource; - -/// Trait defines object that could be registered as route handler -#[allow(unused_variables)] -pub trait Handler: 'static { - /// The type of value that handler will return. - type Result: Responder; - - /// Handle request - fn handle(&self, req: &HttpRequest) -> Self::Result; -} - -/// Trait implemented by types that generate responses for clients. -/// -/// Types that implement this trait can be used as the return type of a handler. -pub trait Responder { - /// The associated item which can be returned. - type Item: Into>; - - /// The associated error which can be returned. - type Error: Into; - - /// Convert itself to `AsyncResult` or `Error`. - fn respond_to( - self, req: &HttpRequest, - ) -> Result; -} +use crate::request::HttpRequest; +use crate::responder::Responder; +use crate::service::{ServiceRequest, ServiceResponse}; /// Trait implemented by types that can be extracted from request. /// -/// Types that implement this trait can be used with `Route::with()` method. -pub trait FromRequest: Sized { - /// Configuration for conversion process - type Config: Default; +/// Types that implement this trait can be used with `Route` handlers. +pub trait FromRequest

    : Sized { + /// The associated error which can be returned. + type Error: Into; /// Future that resolves to a Self - type Result: Into>; + type Future: Future; /// Convert request to a Self - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result; - - /// Convert request to a Self - /// - /// This method uses default extractor configuration - fn extract(req: &HttpRequest) -> Self::Result { - Self::from_request(req, &Self::Config::default()) - } + fn from_request(req: &mut ServiceRequest

    ) -> Self::Future; } -/// Combines two different responder types into a single type -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate futures; -/// # use futures::future::Future; -/// use actix_web::{AsyncResponder, Either, Error, HttpRequest, HttpResponse}; -/// use futures::future::result; -/// -/// type RegisterResult = -/// Either>>; -/// -/// fn index(req: HttpRequest) -> RegisterResult { -/// if is_a_variant() { -/// // <- choose variant A -/// Either::A(HttpResponse::BadRequest().body("Bad data")) -/// } else { -/// Either::B( -/// // <- variant B -/// result(Ok(HttpResponse::Ok() -/// .content_type("text/html") -/// .body("Hello!"))) -/// .responder(), -/// ) -/// } -/// } -/// # fn is_a_variant() -> bool { true } -/// # fn main() {} -/// ``` -#[derive(Debug, PartialEq)] -pub enum Either { - /// First branch of the type - A(A), - /// Second branch of the type - B(B), -} - -impl Responder for Either +/// Handler converter factory +pub trait Factory: Clone where - A: Responder, - B: Responder, -{ - type Item = AsyncResult; - type Error = Error; - - fn respond_to( - self, req: &HttpRequest, - ) -> Result, Error> { - match self { - Either::A(a) => match a.respond_to(req) { - Ok(val) => Ok(val.into()), - Err(err) => Err(err.into()), - }, - Either::B(b) => match b.respond_to(req) { - Ok(val) => Ok(val.into()), - Err(err) => Err(err.into()), - }, - } - } -} - -impl Future for Either -where - A: Future, - B: Future, -{ - type Item = I; - type Error = E; - - fn poll(&mut self) -> Poll { - match *self { - Either::A(ref mut fut) => fut.poll(), - Either::B(ref mut fut) => fut.poll(), - } - } -} - -impl Responder for Option -where - T: Responder, -{ - type Item = AsyncResult; - type Error = Error; - - fn respond_to( - self, req: &HttpRequest, - ) -> Result, Error> { - match self { - Some(t) => match t.respond_to(req) { - Ok(val) => Ok(val.into()), - Err(err) => Err(err.into()), - }, - None => Ok(req.build_response(StatusCode::NOT_FOUND).finish().into()), - } - } -} - -/// Convenience trait that converts `Future` object to a `Boxed` future -/// -/// For example loading json from request's body is async operation. -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate futures; -/// # #[macro_use] extern crate serde_derive; -/// use actix_web::{ -/// App, AsyncResponder, Error, HttpMessage, HttpRequest, HttpResponse, -/// }; -/// use futures::future::Future; -/// -/// #[derive(Deserialize, Debug)] -/// struct MyObj { -/// name: String, -/// } -/// -/// fn index(mut req: HttpRequest) -> Box> { -/// req.json() // <- get JsonBody future -/// .from_err() -/// .and_then(|val: MyObj| { // <- deserialized value -/// Ok(HttpResponse::Ok().into()) -/// }) -/// // Construct boxed future by using `AsyncResponder::responder()` method -/// .responder() -/// } -/// # fn main() {} -/// ``` -pub trait AsyncResponder: Sized { - /// Convert to a boxed future - fn responder(self) -> Box>; -} - -impl AsyncResponder for F -where - F: Future + 'static, - I: Responder + 'static, - E: Into + 'static, -{ - fn responder(self) -> Box> { - Box::new(self) - } -} - -/// Handler for Fn() -impl Handler for F -where - F: Fn(&HttpRequest) -> R + 'static, - R: Responder + 'static, -{ - type Result = R; - - fn handle(&self, req: &HttpRequest) -> R { - (self)(req) - } -} - -/// Represents async result -/// -/// Result could be in tree different forms. -/// * Ok(T) - ready item -/// * Err(E) - error happen during reply process -/// * Future - reply process completes in the future -pub struct AsyncResult(Option>); - -impl Future for AsyncResult { - type Item = I; - type Error = E; - - fn poll(&mut self) -> Poll { - let res = self.0.take().expect("use after resolve"); - match res { - AsyncResultItem::Ok(msg) => Ok(Async::Ready(msg)), - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Future(mut fut) => match fut.poll() { - Ok(Async::NotReady) => { - self.0 = Some(AsyncResultItem::Future(fut)); - Ok(Async::NotReady) - } - Ok(Async::Ready(msg)) => Ok(Async::Ready(msg)), - Err(err) => Err(err), - }, - } - } -} - -pub(crate) enum AsyncResultItem { - Ok(I), - Err(E), - Future(Box>), -} - -impl AsyncResult { - /// Create async response - #[inline] - pub fn future(fut: Box>) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Future(fut))) - } - - /// Send response - #[inline] - pub fn ok>(ok: R) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Ok(ok.into()))) - } - - /// Send error - #[inline] - pub fn err>(err: R) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Err(err.into()))) - } - - #[inline] - pub(crate) fn into(self) -> AsyncResultItem { - self.0.expect("use after resolve") - } - - #[cfg(test)] - pub(crate) fn as_msg(&self) -> &I { - match self.0.as_ref().unwrap() { - &AsyncResultItem::Ok(ref resp) => resp, - _ => panic!(), - } - } - - #[cfg(test)] - pub(crate) fn as_err(&self) -> Option<&E> { - match self.0.as_ref().unwrap() { - &AsyncResultItem::Err(ref err) => Some(err), - _ => None, - } - } -} - -impl Responder for AsyncResult { - type Item = AsyncResult; - type Error = Error; - - fn respond_to( - self, _: &HttpRequest, - ) -> Result, Error> { - Ok(self) - } -} - -impl Responder for HttpResponse { - type Item = AsyncResult; - type Error = Error; - - #[inline] - fn respond_to( - self, _: &HttpRequest, - ) -> Result, Error> { - Ok(AsyncResult(Some(AsyncResultItem::Ok(self)))) - } -} - -impl From for AsyncResult { - #[inline] - fn from(resp: T) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Ok(resp))) - } -} - -impl> Responder for Result { - type Item = ::Item; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - match self { - Ok(val) => match val.respond_to(req) { - Ok(val) => Ok(val), - Err(err) => Err(err.into()), - }, - Err(err) => Err(err.into()), - } - } -} - -impl> From, E>> for AsyncResult { - #[inline] - fn from(res: Result, E>) -> Self { - match res { - Ok(val) => val, - Err(err) => AsyncResult(Some(AsyncResultItem::Err(err.into()))), - } - } -} - -impl> From> for AsyncResult { - #[inline] - fn from(res: Result) -> Self { - match res { - Ok(val) => AsyncResult(Some(AsyncResultItem::Ok(val))), - Err(err) => AsyncResult(Some(AsyncResultItem::Err(err.into()))), - } - } -} - -impl From>, E>> for AsyncResult -where - T: 'static, - E: Into + 'static, -{ - #[inline] - fn from(res: Result>, E>) -> Self { - match res { - Ok(fut) => AsyncResult(Some(AsyncResultItem::Future(Box::new( - fut.map_err(|e| e.into()), - )))), - Err(err) => AsyncResult(Some(AsyncResultItem::Err(err.into()))), - } - } -} - -impl From>> for AsyncResult { - #[inline] - fn from(fut: Box>) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Future(fut))) - } -} - -/// Convenience type alias -pub type FutureResponse = Box>; - -impl Responder for Box> -where - I: Responder + 'static, - E: Into + 'static, -{ - type Item = AsyncResult; - type Error = Error; - - #[inline] - fn respond_to( - self, req: &HttpRequest, - ) -> Result, Error> { - let req = req.clone(); - let fut = self - .map_err(|e| e.into()) - .then(move |r| match r.respond_to(&req) { - Ok(reply) => match reply.into().into() { - AsyncResultItem::Ok(resp) => ok(resp), - _ => panic!("Nested async replies are not supported"), - }, - Err(e) => err(e), - }); - Ok(AsyncResult::future(Box::new(fut))) - } -} - -pub(crate) trait RouteHandler: 'static { - fn handle(&self, &HttpRequest) -> AsyncResult; - - fn has_default_resource(&self) -> bool { - false - } - - fn default_resource(&mut self, _: DefaultResource) {} - - fn finish(&mut self) {} -} - -/// Route handler wrapper for Handler -pub(crate) struct WrapHandler -where - H: Handler, R: Responder, - S: 'static, { - h: H, - s: PhantomData, + fn call(&self, param: T) -> R; } -impl WrapHandler +impl Factory<(), R> for F where - H: Handler, + F: Fn() -> R + Clone + 'static, + R: Responder + 'static, +{ + fn call(&self, _: ()) -> R { + (self)() + } +} + +#[doc(hidden)] +pub struct Handle +where + F: Factory, R: Responder, - S: 'static, { - pub fn new(h: H) -> Self { - WrapHandler { h, s: PhantomData } + hnd: F, + _t: PhantomData<(T, R)>, +} + +impl Handle +where + F: Factory, + R: Responder, +{ + pub fn new(hnd: F) -> Self { + Handle { + hnd, + _t: PhantomData, + } + } +} +impl NewService for Handle +where + F: Factory, + R: Responder + 'static, +{ + type Request = (T, HttpRequest); + type Response = ServiceResponse; + type Error = Never; + type InitError = (); + type Service = HandleService; + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(HandleService { + hnd: self.hnd.clone(), + _t: PhantomData, + }) } } -impl RouteHandler for WrapHandler +#[doc(hidden)] +pub struct HandleService where - H: Handler, + F: Factory, R: Responder + 'static, - S: 'static, { - fn handle(&self, req: &HttpRequest) -> AsyncResult { - match self.h.handle(req).respond_to(req) { - Ok(reply) => reply.into(), - Err(err) => AsyncResult::err(err.into()), + hnd: F, + _t: PhantomData<(T, R)>, +} + +impl Service for HandleService +where + F: Factory, + R: Responder + 'static, +{ + type Request = (T, HttpRequest); + type Response = ServiceResponse; + type Error = Never; + type Future = HandleServiceResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, (param, req): (T, HttpRequest)) -> Self::Future { + let fut = self.hnd.call(param).respond_to(&req); + HandleServiceResponse { + fut, + req: Some(req), } } } -/// Async route handler -pub(crate) struct AsyncHandler -where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - S: 'static, -{ - h: Box, - s: PhantomData, +pub struct HandleServiceResponse { + fut: T, + req: Option, } -impl AsyncHandler +impl Future for HandleServiceResponse where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - S: 'static, + T: Future, + T::Error: Into, { - pub fn new(h: H) -> Self { - AsyncHandler { - h: Box::new(h), - s: PhantomData, - } - } -} + type Item = ServiceResponse; + type Error = Never; -impl RouteHandler for AsyncHandler -where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - S: 'static, -{ - fn handle(&self, req: &HttpRequest) -> AsyncResult { - let req = req.clone(); - let fut = (self.h)(&req).map_err(|e| e.into()).then(move |r| { - match r.respond_to(&req) { - Ok(reply) => match reply.into().into() { - AsyncResultItem::Ok(resp) => Either::A(ok(resp)), - AsyncResultItem::Err(e) => Either::A(err(e)), - AsyncResultItem::Future(fut) => Either::B(fut), - }, - Err(e) => Either::A(err(e)), + fn poll(&mut self) -> Poll { + match self.fut.poll() { + Ok(Async::Ready(res)) => Ok(Async::Ready(ServiceResponse::new( + self.req.take().unwrap(), + res, + ))), + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(e) => { + let res: Response = e.into().into(); + Ok(Async::Ready(ServiceResponse::new( + self.req.take().unwrap(), + res, + ))) } - }); - AsyncResult::future(Box::new(fut)) + } } } -/// Access an application state -/// -/// `S` - application state type -/// -/// ## Example -/// -/// ```rust -/// # extern crate bytes; -/// # extern crate actix_web; -/// # extern crate futures; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Path, State}; -/// -/// /// Application state -/// struct MyApp { -/// msg: &'static str, -/// } -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// extract path info using serde -/// fn index(state: State, path: Path) -> String { -/// format!("{} {}!", state.msg, path.username) -/// } -/// -/// fn main() { -/// let app = App::with_state(MyApp { msg: "Welcome" }).resource( -/// "/{username}/index.html", // <- define path parameters -/// |r| r.method(http::Method::GET).with(index), -/// ); // <- use `with` extractor -/// } -/// ``` -pub struct State(HttpRequest); +/// Async handler converter factory +pub trait AsyncFactory: Clone + 'static +where + R: IntoFuture, + R::Item: Into, + R::Error: Into, +{ + fn call(&self, param: T) -> R; +} -impl Deref for State { - type Target = S; - - fn deref(&self) -> &S { - self.0.state() +impl AsyncFactory<(), R> for F +where + F: Fn() -> R + Clone + 'static, + R: IntoFuture, + R::Item: Into, + R::Error: Into, +{ + fn call(&self, _: ()) -> R { + (self)() } } -impl FromRequest for State { - type Config = (); - type Result = State; +#[doc(hidden)] +pub struct AsyncHandle +where + F: AsyncFactory, + R: IntoFuture, + R::Item: Into, + R::Error: Into, +{ + hnd: F, + _t: PhantomData<(T, R)>, +} - #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { - State(req.clone()) +impl AsyncHandle +where + F: AsyncFactory, + R: IntoFuture, + R::Item: Into, + R::Error: Into, +{ + pub fn new(hnd: F) -> Self { + AsyncHandle { + hnd, + _t: PhantomData, + } } } +impl NewService for AsyncHandle +where + F: AsyncFactory, + R: IntoFuture, + R::Item: Into, + R::Error: Into, +{ + type Request = (T, HttpRequest); + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = AsyncHandleService; + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(AsyncHandleService { + hnd: self.hnd.clone(), + _t: PhantomData, + }) + } +} + +#[doc(hidden)] +pub struct AsyncHandleService +where + F: AsyncFactory, + R: IntoFuture, + R::Item: Into, + R::Error: Into, +{ + hnd: F, + _t: PhantomData<(T, R)>, +} + +impl Service for AsyncHandleService +where + F: AsyncFactory, + R: IntoFuture, + R::Item: Into, + R::Error: Into, +{ + type Request = (T, HttpRequest); + type Response = ServiceResponse; + type Error = (); + type Future = AsyncHandleServiceResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, (param, req): (T, HttpRequest)) -> Self::Future { + AsyncHandleServiceResponse { + fut: self.hnd.call(param).into_future(), + req: Some(req), + } + } +} + +#[doc(hidden)] +pub struct AsyncHandleServiceResponse { + fut: T, + req: Option, +} + +impl Future for AsyncHandleServiceResponse +where + T: Future, + T::Item: Into, + T::Error: Into, +{ + type Item = ServiceResponse; + type Error = (); + + fn poll(&mut self) -> Poll { + match self.fut.poll() { + Ok(Async::Ready(res)) => Ok(Async::Ready(ServiceResponse::new( + self.req.take().unwrap(), + res.into(), + ))), + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(e) => { + let res: Response = e.into().into(); + Ok(Async::Ready(ServiceResponse::new( + self.req.take().unwrap(), + res, + ))) + } + } + } +} + +/// Extract arguments from request +pub struct Extract> { + _t: PhantomData<(P, T)>, +} + +impl> Extract { + pub fn new() -> Self { + Extract { _t: PhantomData } + } +} + +impl> Default for Extract { + fn default() -> Self { + Self::new() + } +} + +impl> NewService for Extract { + type Request = ServiceRequest

    ; + type Response = (T, HttpRequest); + type Error = (Error, ServiceRequest

    ); + type InitError = (); + type Service = ExtractService; + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(ExtractService { _t: PhantomData }) + } +} + +pub struct ExtractService> { + _t: PhantomData<(P, T)>, +} + +impl> Service for ExtractService { + type Request = ServiceRequest

    ; + type Response = (T, HttpRequest); + type Error = (Error, ServiceRequest

    ); + type Future = ExtractResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, mut req: ServiceRequest

    ) -> Self::Future { + ExtractResponse { + fut: T::from_request(&mut req), + req: Some(req), + } + } +} + +pub struct ExtractResponse> { + req: Option>, + fut: T::Future, +} + +impl> Future for ExtractResponse { + type Item = (T, HttpRequest); + type Error = (Error, ServiceRequest

    ); + + fn poll(&mut self) -> Poll { + let item = try_ready!(self + .fut + .poll() + .map_err(|e| (e.into(), self.req.take().unwrap()))); + + let req = self.req.take().unwrap(); + let req = req.into_request(); + + Ok(Async::Ready((item, req))) + } +} + +/// FromRequest trait impl for tuples +macro_rules! factory_tuple ({ $(($n:tt, $T:ident)),+} => { + impl Factory<($($T,)+), Res> for Func + where Func: Fn($($T,)+) -> Res + Clone + 'static, + //$($T,)+ + Res: Responder + 'static, + { + fn call(&self, param: ($($T,)+)) -> Res { + (self)($(param.$n,)+) + } + } + + impl AsyncFactory<($($T,)+), Res> for Func + where Func: Fn($($T,)+) -> Res + Clone + 'static, + Res: IntoFuture + 'static, + Res::Item: Into, + Res::Error: Into, + { + fn call(&self, param: ($($T,)+)) -> Res { + (self)($(param.$n,)+) + } + } +}); + +#[rustfmt::skip] +mod m { + use super::*; + +factory_tuple!((0, A)); +factory_tuple!((0, A), (1, B)); +factory_tuple!((0, A), (1, B), (2, C)); +factory_tuple!((0, A), (1, B), (2, C), (3, D)); +factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E)); +factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +} diff --git a/src/header/common/accept.rs b/src/header/common/accept.rs deleted file mode 100644 index d736e53af..000000000 --- a/src/header/common/accept.rs +++ /dev/null @@ -1,159 +0,0 @@ -use header::{qitem, QualityItem}; -use http::header as http; -use mime::{self, Mime}; - -header! { - /// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2) - /// - /// The `Accept` header field can be used by user agents to specify - /// response media types that are acceptable. Accept header fields can - /// be used to indicate that the request is specifically limited to a - /// small set of desired types, as in the case of a request for an - /// in-line image - /// - /// # ABNF - /// - /// ```text - /// Accept = #( media-range [ accept-params ] ) - /// - /// media-range = ( "*/*" - /// / ( type "/" "*" ) - /// / ( type "/" subtype ) - /// ) *( OWS ";" OWS parameter ) - /// accept-params = weight *( accept-ext ) - /// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ] - /// ``` - /// - /// # Example values - /// * `audio/*; q=0.2, audio/basic` - /// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c` - /// - /// # Examples - /// ```rust - /// # extern crate actix_web; - /// extern crate mime; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// - /// builder.set( - /// Accept(vec![ - /// qitem(mime::TEXT_HTML), - /// ]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate actix_web; - /// extern crate mime; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// - /// builder.set( - /// Accept(vec![ - /// qitem(mime::APPLICATION_JSON), - /// ]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate actix_web; - /// extern crate mime; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, QualityItem, q, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// - /// builder.set( - /// Accept(vec![ - /// qitem(mime::TEXT_HTML), - /// qitem("application/xhtml+xml".parse().unwrap()), - /// QualityItem::new( - /// mime::TEXT_XML, - /// q(900) - /// ), - /// qitem("image/webp".parse().unwrap()), - /// QualityItem::new( - /// mime::STAR_STAR, - /// q(800) - /// ), - /// ]) - /// ); - /// # } - /// ``` - (Accept, http::ACCEPT) => (QualityItem)+ - - test_accept { - // Tests from the RFC - test_header!( - test1, - vec![b"audio/*; q=0.2, audio/basic"], - Some(HeaderField(vec![ - QualityItem::new("audio/*".parse().unwrap(), q(200)), - qitem("audio/basic".parse().unwrap()), - ]))); - test_header!( - test2, - vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], - Some(HeaderField(vec![ - QualityItem::new(TEXT_PLAIN, q(500)), - qitem(TEXT_HTML), - QualityItem::new( - "text/x-dvi".parse().unwrap(), - q(800)), - qitem("text/x-c".parse().unwrap()), - ]))); - // Custom tests - test_header!( - test3, - vec![b"text/plain; charset=utf-8"], - Some(Accept(vec![ - qitem(TEXT_PLAIN_UTF_8), - ]))); - test_header!( - test4, - vec![b"text/plain; charset=utf-8; q=0.5"], - Some(Accept(vec![ - QualityItem::new(TEXT_PLAIN_UTF_8, - q(500)), - ]))); - - #[test] - fn test_fuzzing1() { - use test::TestRequest; - let req = TestRequest::with_header(super::http::ACCEPT, "chunk#;e").finish(); - let header = Accept::parse(&req); - assert!(header.is_ok()); - } - } -} - -impl Accept { - /// A constructor to easily create `Accept: */*`. - pub fn star() -> Accept { - Accept(vec![qitem(mime::STAR_STAR)]) - } - - /// A constructor to easily create `Accept: application/json`. - pub fn json() -> Accept { - Accept(vec![qitem(mime::APPLICATION_JSON)]) - } - - /// A constructor to easily create `Accept: text/*`. - pub fn text() -> Accept { - Accept(vec![qitem(mime::TEXT_STAR)]) - } - - /// A constructor to easily create `Accept: image/*`. - pub fn image() -> Accept { - Accept(vec![qitem(mime::IMAGE_STAR)]) - } -} diff --git a/src/header/common/accept_charset.rs b/src/header/common/accept_charset.rs deleted file mode 100644 index 674415fba..000000000 --- a/src/header/common/accept_charset.rs +++ /dev/null @@ -1,69 +0,0 @@ -use header::{Charset, QualityItem, ACCEPT_CHARSET}; - -header! { - /// `Accept-Charset` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.3) - /// - /// The `Accept-Charset` header field can be sent by a user agent to - /// indicate what charsets are acceptable in textual response content. - /// This field allows user agents capable of understanding more - /// comprehensive or special-purpose charsets to signal that capability - /// to an origin server that is capable of representing information in - /// those charsets. - /// - /// # ABNF - /// - /// ```text - /// Accept-Charset = 1#( ( charset / "*" ) [ weight ] ) - /// ``` - /// - /// # Example values - /// * `iso-8859-5, unicode-1-1;q=0.8` - /// - /// # Examples - /// ```rust - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptCharset, Charset, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// AcceptCharset(vec![qitem(Charset::Us_Ascii)]) - /// ); - /// # } - /// ``` - /// ```rust - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptCharset, Charset, q, QualityItem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// AcceptCharset(vec![ - /// QualityItem::new(Charset::Us_Ascii, q(900)), - /// QualityItem::new(Charset::Iso_8859_10, q(200)), - /// ]) - /// ); - /// # } - /// ``` - /// ```rust - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptCharset, Charset, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))]) - /// ); - /// # } - /// ``` - (AcceptCharset, ACCEPT_CHARSET) => (QualityItem)+ - - test_accept_charset { - /// Test case from RFC - test_header!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); - } -} diff --git a/src/header/common/accept_encoding.rs b/src/header/common/accept_encoding.rs deleted file mode 100644 index c90f529bc..000000000 --- a/src/header/common/accept_encoding.rs +++ /dev/null @@ -1,72 +0,0 @@ -use header::{Encoding, QualityItem}; - -header! { - /// `Accept-Encoding` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.4) - /// - /// The `Accept-Encoding` header field can be used by user agents to - /// indicate what response content-codings are - /// acceptable in the response. An `identity` token is used as a synonym - /// for "no encoding" in order to communicate when no encoding is - /// preferred. - /// - /// # ABNF - /// - /// ```text - /// Accept-Encoding = #( codings [ weight ] ) - /// codings = content-coding / "identity" / "*" - /// ``` - /// - /// # Example values - /// * `compress, gzip` - /// * `` - /// * `*` - /// * `compress;q=0.5, gzip;q=1` - /// * `gzip;q=1.0, identity; q=0.5, *;q=0` - /// - /// # Examples - /// ``` - /// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem}; - /// - /// let mut headers = Headers::new(); - /// headers.set( - /// AcceptEncoding(vec![qitem(Encoding::Chunked)]) - /// ); - /// ``` - /// ``` - /// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem}; - /// - /// let mut headers = Headers::new(); - /// headers.set( - /// AcceptEncoding(vec![ - /// qitem(Encoding::Chunked), - /// qitem(Encoding::Gzip), - /// qitem(Encoding::Deflate), - /// ]) - /// ); - /// ``` - /// ``` - /// use hyper::header::{Headers, AcceptEncoding, Encoding, QualityItem, q, qitem}; - /// - /// let mut headers = Headers::new(); - /// headers.set( - /// AcceptEncoding(vec![ - /// qitem(Encoding::Chunked), - /// QualityItem::new(Encoding::Gzip, q(600)), - /// QualityItem::new(Encoding::EncodingExt("*".to_owned()), q(0)), - /// ]) - /// ); - /// ``` - (AcceptEncoding, "Accept-Encoding") => (QualityItem)* - - test_accept_encoding { - // From the RFC - test_header!(test1, vec![b"compress, gzip"]); - test_header!(test2, vec![b""], Some(AcceptEncoding(vec![]))); - test_header!(test3, vec![b"*"]); - // Note: Removed quality 1 from gzip - test_header!(test4, vec![b"compress;q=0.5, gzip"]); - // Note: Removed quality 1 from gzip - test_header!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); - } -} diff --git a/src/header/common/accept_language.rs b/src/header/common/accept_language.rs deleted file mode 100644 index 12593e1ac..000000000 --- a/src/header/common/accept_language.rs +++ /dev/null @@ -1,75 +0,0 @@ -use header::{QualityItem, ACCEPT_LANGUAGE}; -use language_tags::LanguageTag; - -header! { - /// `Accept-Language` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.5) - /// - /// The `Accept-Language` header field can be used by user agents to - /// indicate the set of natural languages that are preferred in the - /// response. - /// - /// # ABNF - /// - /// ```text - /// Accept-Language = 1#( language-range [ weight ] ) - /// language-range = - /// ``` - /// - /// # Example values - /// * `da, en-gb;q=0.8, en;q=0.7` - /// * `en-us;q=1.0, en;q=0.5, fr` - /// - /// # Examples - /// - /// ```rust - /// # extern crate actix_web; - /// # extern crate language_tags; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptLanguage, LanguageTag, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// let mut langtag: LanguageTag = Default::default(); - /// langtag.language = Some("en".to_owned()); - /// langtag.region = Some("US".to_owned()); - /// builder.set( - /// AcceptLanguage(vec![ - /// qitem(langtag), - /// ]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate actix_web; - /// # #[macro_use] extern crate language_tags; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptLanguage, QualityItem, q, qitem}; - /// # - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// AcceptLanguage(vec![ - /// qitem(langtag!(da)), - /// QualityItem::new(langtag!(en;;;GB), q(800)), - /// QualityItem::new(langtag!(en), q(700)), - /// ]) - /// ); - /// # } - /// ``` - (AcceptLanguage, ACCEPT_LANGUAGE) => (QualityItem)+ - - test_accept_language { - // From the RFC - test_header!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]); - // Own test - test_header!( - test2, vec![b"en-US, en; q=0.5, fr"], - Some(AcceptLanguage(vec![ - qitem("en-US".parse().unwrap()), - QualityItem::new("en".parse().unwrap(), q(500)), - qitem("fr".parse().unwrap()), - ]))); - } -} diff --git a/src/header/common/allow.rs b/src/header/common/allow.rs deleted file mode 100644 index 5046290de..000000000 --- a/src/header/common/allow.rs +++ /dev/null @@ -1,85 +0,0 @@ -use http::Method; -use http::header; - -header! { - /// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1) - /// - /// The `Allow` header field lists the set of methods advertised as - /// supported by the target resource. The purpose of this field is - /// strictly to inform the recipient of valid request methods associated - /// with the resource. - /// - /// # ABNF - /// - /// ```text - /// Allow = #method - /// ``` - /// - /// # Example values - /// * `GET, HEAD, PUT` - /// * `OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH, fOObAr` - /// * `` - /// - /// # Examples - /// - /// ```rust - /// # extern crate http; - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::Allow; - /// use http::Method; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// Allow(vec![Method::GET]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate http; - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::Allow; - /// use http::Method; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// Allow(vec![ - /// Method::GET, - /// Method::POST, - /// Method::PATCH, - /// ]) - /// ); - /// # } - /// ``` - (Allow, header::ALLOW) => (Method)* - - test_allow { - // From the RFC - test_header!( - test1, - vec![b"GET, HEAD, PUT"], - Some(HeaderField(vec![Method::GET, Method::HEAD, Method::PUT]))); - // Own tests - test_header!( - test2, - vec![b"OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH"], - Some(HeaderField(vec![ - Method::OPTIONS, - Method::GET, - Method::PUT, - Method::POST, - Method::DELETE, - Method::HEAD, - Method::TRACE, - Method::CONNECT, - Method::PATCH]))); - test_header!( - test3, - vec![b""], - Some(HeaderField(Vec::::new()))); - } -} diff --git a/src/header/common/cache_control.rs b/src/header/common/cache_control.rs deleted file mode 100644 index adc60e4a5..000000000 --- a/src/header/common/cache_control.rs +++ /dev/null @@ -1,254 +0,0 @@ -use header::{Header, IntoHeaderValue, Writer}; -use header::{fmt_comma_delimited, from_comma_delimited}; -use http::header; -use std::fmt::{self, Write}; -use std::str::FromStr; - -/// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2) -/// -/// The `Cache-Control` header field is used to specify directives for -/// caches along the request/response chain. Such cache directives are -/// unidirectional in that the presence of a directive in a request does -/// not imply that the same directive is to be given in the response. -/// -/// # ABNF -/// -/// ```text -/// Cache-Control = 1#cache-directive -/// cache-directive = token [ "=" ( token / quoted-string ) ] -/// ``` -/// -/// # Example values -/// -/// * `no-cache` -/// * `private, community="UCI"` -/// * `max-age=30` -/// -/// # Examples -/// ```rust -/// use actix_web::HttpResponse; -/// use actix_web::http::header::{CacheControl, CacheDirective}; -/// -/// let mut builder = HttpResponse::Ok(); -/// builder.set(CacheControl(vec![CacheDirective::MaxAge(86400u32)])); -/// ``` -/// -/// ```rust -/// use actix_web::HttpResponse; -/// use actix_web::http::header::{CacheControl, CacheDirective}; -/// -/// let mut builder = HttpResponse::Ok(); -/// builder.set(CacheControl(vec![ -/// CacheDirective::NoCache, -/// CacheDirective::Private, -/// CacheDirective::MaxAge(360u32), -/// CacheDirective::Extension("foo".to_owned(), Some("bar".to_owned())), -/// ])); -/// ``` -#[derive(PartialEq, Clone, Debug)] -pub struct CacheControl(pub Vec); - -__hyper__deref!(CacheControl => Vec); - -//TODO: this could just be the header! macro -impl Header for CacheControl { - fn name() -> header::HeaderName { - header::CACHE_CONTROL - } - - #[inline] - fn parse(msg: &T) -> Result - where - T: ::HttpMessage, - { - let directives = from_comma_delimited(msg.headers().get_all(Self::name()))?; - if !directives.is_empty() { - Ok(CacheControl(directives)) - } else { - Err(::error::ParseError::Header) - } - } -} - -impl fmt::Display for CacheControl { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt_comma_delimited(f, &self[..]) - } -} - -impl IntoHeaderValue for CacheControl { - type Error = header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut writer = Writer::new(); - let _ = write!(&mut writer, "{}", self); - header::HeaderValue::from_shared(writer.take()) - } -} - -/// `CacheControl` contains a list of these directives. -#[derive(PartialEq, Clone, Debug)] -pub enum CacheDirective { - /// "no-cache" - NoCache, - /// "no-store" - NoStore, - /// "no-transform" - NoTransform, - /// "only-if-cached" - OnlyIfCached, - - // request directives - /// "max-age=delta" - MaxAge(u32), - /// "max-stale=delta" - MaxStale(u32), - /// "min-fresh=delta" - MinFresh(u32), - - // response directives - /// "must-revalidate" - MustRevalidate, - /// "public" - Public, - /// "private" - Private, - /// "proxy-revalidate" - ProxyRevalidate, - /// "s-maxage=delta" - SMaxAge(u32), - - /// Extension directives. Optionally include an argument. - Extension(String, Option), -} - -impl fmt::Display for CacheDirective { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::CacheDirective::*; - fmt::Display::fmt( - match *self { - NoCache => "no-cache", - NoStore => "no-store", - NoTransform => "no-transform", - OnlyIfCached => "only-if-cached", - - MaxAge(secs) => return write!(f, "max-age={}", secs), - MaxStale(secs) => return write!(f, "max-stale={}", secs), - MinFresh(secs) => return write!(f, "min-fresh={}", secs), - - MustRevalidate => "must-revalidate", - Public => "public", - Private => "private", - ProxyRevalidate => "proxy-revalidate", - SMaxAge(secs) => return write!(f, "s-maxage={}", secs), - - Extension(ref name, None) => &name[..], - Extension(ref name, Some(ref arg)) => { - return write!(f, "{}={}", name, arg) - } - }, - f, - ) - } -} - -impl FromStr for CacheDirective { - type Err = Option<::Err>; - fn from_str(s: &str) -> Result::Err>> { - use self::CacheDirective::*; - match s { - "no-cache" => Ok(NoCache), - "no-store" => Ok(NoStore), - "no-transform" => Ok(NoTransform), - "only-if-cached" => Ok(OnlyIfCached), - "must-revalidate" => Ok(MustRevalidate), - "public" => Ok(Public), - "private" => Ok(Private), - "proxy-revalidate" => Ok(ProxyRevalidate), - "" => Err(None), - _ => match s.find('=') { - Some(idx) if idx + 1 < s.len() => { - match (&s[..idx], (&s[idx + 1..]).trim_matches('"')) { - ("max-age", secs) => secs.parse().map(MaxAge).map_err(Some), - ("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some), - ("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some), - ("s-maxage", secs) => secs.parse().map(SMaxAge).map_err(Some), - (left, right) => { - Ok(Extension(left.to_owned(), Some(right.to_owned()))) - } - } - } - Some(_) => Err(None), - None => Ok(Extension(s.to_owned(), None)), - }, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use header::Header; - use test::TestRequest; - - #[test] - fn test_parse_multiple_headers() { - let req = TestRequest::with_header(header::CACHE_CONTROL, "no-cache, private") - .finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::NoCache, - CacheDirective::Private, - ])) - ) - } - - #[test] - fn test_parse_argument() { - let req = - TestRequest::with_header(header::CACHE_CONTROL, "max-age=100, private") - .finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::MaxAge(100), - CacheDirective::Private, - ])) - ) - } - - #[test] - fn test_parse_quote_form() { - let req = - TestRequest::with_header(header::CACHE_CONTROL, "max-age=\"200\"").finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![CacheDirective::MaxAge(200)])) - ) - } - - #[test] - fn test_parse_extension() { - let req = - TestRequest::with_header(header::CACHE_CONTROL, "foo, bar=baz").finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::Extension("foo".to_owned(), None), - CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned())), - ])) - ) - } - - #[test] - fn test_parse_bad_syntax() { - let req = TestRequest::with_header(header::CACHE_CONTROL, "foo=").finish(); - let cache: Result = Header::parse(&req); - assert_eq!(cache.ok(), None) - } -} diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs deleted file mode 100644 index 5e8cbd67a..000000000 --- a/src/header/common/content_disposition.rs +++ /dev/null @@ -1,914 +0,0 @@ -// # References -// -// "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt -// "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt -// "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt -// Browser conformance tests at: http://greenbytes.de/tech/tc2231/ -// IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml - -use header; -use header::ExtendedValue; -use header::{Header, IntoHeaderValue, Writer}; -use regex::Regex; - -use std::fmt::{self, Write}; - -/// Split at the index of the first `needle` if it exists or at the end. -fn split_once(haystack: &str, needle: char) -> (&str, &str) { - haystack.find(needle).map_or_else( - || (haystack, ""), - |sc| { - let (first, last) = haystack.split_at(sc); - (first, last.split_at(1).1) - }, - ) -} - -/// Split at the index of the first `needle` if it exists or at the end, trim the right of the -/// first part and the left of the last part. -fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) { - let (first, last) = split_once(haystack, needle); - (first.trim_right(), last.trim_left()) -} - -/// The implied disposition of the content of the HTTP body. -#[derive(Clone, Debug, PartialEq)] -pub enum DispositionType { - /// Inline implies default processing - Inline, - /// Attachment implies that the recipient should prompt the user to save the response locally, - /// rather than process it normally (as per its media type). - Attachment, - /// Used in *multipart/form-data* as defined in - /// [RFC7578](https://tools.ietf.org/html/rfc7578) to carry the field name and the file name. - FormData, - /// Extension type. Should be handled by recipients the same way as Attachment - Ext(String), -} - -impl<'a> From<&'a str> for DispositionType { - fn from(origin: &'a str) -> DispositionType { - if origin.eq_ignore_ascii_case("inline") { - DispositionType::Inline - } else if origin.eq_ignore_ascii_case("attachment") { - DispositionType::Attachment - } else if origin.eq_ignore_ascii_case("form-data") { - DispositionType::FormData - } else { - DispositionType::Ext(origin.to_owned()) - } - } -} - -/// Parameter in [`ContentDisposition`]. -/// -/// # Examples -/// ``` -/// use actix_web::http::header::DispositionParam; -/// -/// let param = DispositionParam::Filename(String::from("sample.txt")); -/// assert!(param.is_filename()); -/// assert_eq!(param.as_filename().unwrap(), "sample.txt"); -/// ``` -#[derive(Clone, Debug, PartialEq)] -pub enum DispositionParam { - /// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from - /// the form. - Name(String), - /// A plain file name. - Filename(String), - /// An extended file name. It must not exist for `ContentType::Formdata` according to - /// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2). - FilenameExt(ExtendedValue), - /// An unrecognized regular parameter as defined in - /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *reg-parameter*, in - /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *token "=" value*. Recipients should - /// ignore unrecognizable parameters. - Unknown(String, String), - /// An unrecognized extended paramater as defined in - /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *ext-parameter*, in - /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *ext-token "=" ext-value*. The single - /// trailling asterisk is not included. Recipients should ignore unrecognizable parameters. - UnknownExt(String, ExtendedValue), -} - -impl DispositionParam { - /// Returns `true` if the paramater is [`Name`](DispositionParam::Name). - #[inline] - pub fn is_name(&self) -> bool { - self.as_name().is_some() - } - - /// Returns `true` if the paramater is [`Filename`](DispositionParam::Filename). - #[inline] - pub fn is_filename(&self) -> bool { - self.as_filename().is_some() - } - - /// Returns `true` if the paramater is [`FilenameExt`](DispositionParam::FilenameExt). - #[inline] - pub fn is_filename_ext(&self) -> bool { - self.as_filename_ext().is_some() - } - - /// Returns `true` if the paramater is [`Unknown`](DispositionParam::Unknown) and the `name` - #[inline] - /// matches. - pub fn is_unknown>(&self, name: T) -> bool { - self.as_unknown(name).is_some() - } - - /// Returns `true` if the paramater is [`UnknownExt`](DispositionParam::UnknownExt) and the - /// `name` matches. - #[inline] - pub fn is_unknown_ext>(&self, name: T) -> bool { - self.as_unknown_ext(name).is_some() - } - - /// Returns the name if applicable. - #[inline] - pub fn as_name(&self) -> Option<&str> { - match self { - DispositionParam::Name(ref name) => Some(name.as_str()), - _ => None, - } - } - - /// Returns the filename if applicable. - #[inline] - pub fn as_filename(&self) -> Option<&str> { - match self { - DispositionParam::Filename(ref filename) => Some(filename.as_str()), - _ => None, - } - } - - /// Returns the filename* if applicable. - #[inline] - pub fn as_filename_ext(&self) -> Option<&ExtendedValue> { - match self { - DispositionParam::FilenameExt(ref value) => Some(value), - _ => None, - } - } - - /// Returns the value of the unrecognized regular parameter if it is - /// [`Unknown`](DispositionParam::Unknown) and the `name` matches. - #[inline] - pub fn as_unknown>(&self, name: T) -> Option<&str> { - match self { - DispositionParam::Unknown(ref ext_name, ref value) - if ext_name.eq_ignore_ascii_case(name.as_ref()) => - { - Some(value.as_str()) - } - _ => None, - } - } - - /// Returns the value of the unrecognized extended parameter if it is - /// [`Unknown`](DispositionParam::Unknown) and the `name` matches. - #[inline] - pub fn as_unknown_ext>(&self, name: T) -> Option<&ExtendedValue> { - match self { - DispositionParam::UnknownExt(ref ext_name, ref value) - if ext_name.eq_ignore_ascii_case(name.as_ref()) => - { - Some(value) - } - _ => None, - } - } -} - -/// A *Content-Disposition* header. It is compatible to be used either as -/// [a response header for the main body](https://mdn.io/Content-Disposition#As_a_response_header_for_the_main_body) -/// as (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266), or as -/// [a header for a multipart body](https://mdn.io/Content-Disposition#As_a_header_for_a_multipart_body) -/// as (re)defined in [RFC7587](https://tools.ietf.org/html/rfc7578). -/// -/// In a regular HTTP response, the *Content-Disposition* response header is a header indicating if -/// the content is expected to be displayed *inline* in the browser, that is, as a Web page or as -/// part of a Web page, or as an attachment, that is downloaded and saved locally, and also can be -/// used to attach additional metadata, such as the filename to use when saving the response payload -/// locally. -/// -/// In a *multipart/form-data* body, the HTTP *Content-Disposition* general header is a header that -/// can be used on the subpart of a multipart body to give information about the field it applies to. -/// The subpart is delimited by the boundary defined in the *Content-Type* header. Used on the body -/// itself, *Content-Disposition* has no effect. -/// -/// # ABNF - -/// ```text -/// content-disposition = "Content-Disposition" ":" -/// disposition-type *( ";" disposition-parm ) -/// -/// disposition-type = "inline" | "attachment" | disp-ext-type -/// ; case-insensitive -/// -/// disp-ext-type = token -/// -/// disposition-parm = filename-parm | disp-ext-parm -/// -/// filename-parm = "filename" "=" value -/// | "filename*" "=" ext-value -/// -/// disp-ext-parm = token "=" value -/// | ext-token "=" ext-value -/// -/// ext-token = -/// ``` -/// -/// **Note**: filename* [must not](https://tools.ietf.org/html/rfc7578#section-4.2) be used within -/// *multipart/form-data*. -/// -/// # Example -/// -/// ``` -/// use actix_web::http::header::{ -/// Charset, ContentDisposition, DispositionParam, DispositionType, -/// ExtendedValue, -/// }; -/// -/// let cd1 = ContentDisposition { -/// disposition: DispositionType::Attachment, -/// parameters: vec![DispositionParam::FilenameExt(ExtendedValue { -/// charset: Charset::Iso_8859_1, // The character set for the bytes of the filename -/// language_tag: None, // The optional language tag (see `language-tag` crate) -/// value: b"\xa9 Copyright 1989.txt".to_vec(), // the actual bytes of the filename -/// })], -/// }; -/// assert!(cd1.is_attachment()); -/// assert!(cd1.get_filename_ext().is_some()); -/// -/// let cd2 = ContentDisposition { -/// disposition: DispositionType::FormData, -/// parameters: vec![ -/// DispositionParam::Name(String::from("file")), -/// DispositionParam::Filename(String::from("bill.odt")), -/// ], -/// }; -/// assert_eq!(cd2.get_name(), Some("file")); // field name -/// assert_eq!(cd2.get_filename(), Some("bill.odt")); -/// ``` -/// -/// # WARN -/// If "filename" parameter is supplied, do not use the file name blindly, check and possibly -/// change to match local file system conventions if applicable, and do not use directory path -/// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3) -/// . -#[derive(Clone, Debug, PartialEq)] -pub struct ContentDisposition { - /// The disposition type - pub disposition: DispositionType, - /// Disposition parameters - pub parameters: Vec, -} - -impl ContentDisposition { - /// Parse a raw Content-Disposition header value. - pub fn from_raw(hv: &header::HeaderValue) -> Result { - // `header::from_one_raw_str` invokes `hv.to_str` which assumes `hv` contains only visible - // ASCII characters. So `hv.as_bytes` is necessary here. - let hv = String::from_utf8(hv.as_bytes().to_vec()) - .map_err(|_| ::error::ParseError::Header)?; - let (disp_type, mut left) = split_once_and_trim(hv.as_str().trim(), ';'); - if disp_type.is_empty() { - return Err(::error::ParseError::Header); - } - let mut cd = ContentDisposition { - disposition: disp_type.into(), - parameters: Vec::new(), - }; - - while !left.is_empty() { - let (param_name, new_left) = split_once_and_trim(left, '='); - if param_name.is_empty() || param_name == "*" || new_left.is_empty() { - return Err(::error::ParseError::Header); - } - left = new_left; - if param_name.ends_with('*') { - // extended parameters - let param_name = ¶m_name[..param_name.len() - 1]; // trim asterisk - let (ext_value, new_left) = split_once_and_trim(left, ';'); - left = new_left; - let ext_value = header::parse_extended_value(ext_value)?; - - let param = if param_name.eq_ignore_ascii_case("filename") { - DispositionParam::FilenameExt(ext_value) - } else { - DispositionParam::UnknownExt(param_name.to_owned(), ext_value) - }; - cd.parameters.push(param); - } else { - // regular parameters - let value = if left.starts_with('\"') { - // quoted-string: defined in RFC6266 -> RFC2616 Section 3.6 - let mut escaping = false; - let mut quoted_string = vec![]; - let mut end = None; - // search for closing quote - for (i, &c) in left.as_bytes().iter().skip(1).enumerate() { - if escaping { - escaping = false; - quoted_string.push(c); - } else if c == 0x5c { - // backslash - escaping = true; - } else if c == 0x22 { - // double quote - end = Some(i + 1); // cuz skipped 1 for the leading quote - break; - } else { - quoted_string.push(c); - } - } - left = &left[end.ok_or(::error::ParseError::Header)? + 1..]; - left = split_once(left, ';').1.trim_left(); - // In fact, it should not be Err if the above code is correct. - String::from_utf8(quoted_string).map_err(|_| ::error::ParseError::Header)? - } else { - // token: won't contains semicolon according to RFC 2616 Section 2.2 - let (token, new_left) = split_once_and_trim(left, ';'); - left = new_left; - token.to_owned() - }; - if value.is_empty() { - return Err(::error::ParseError::Header); - } - - let param = if param_name.eq_ignore_ascii_case("name") { - DispositionParam::Name(value) - } else if param_name.eq_ignore_ascii_case("filename") { - DispositionParam::Filename(value) - } else { - DispositionParam::Unknown(param_name.to_owned(), value) - }; - cd.parameters.push(param); - } - } - - Ok(cd) - } - - /// Returns `true` if it is [`Inline`](DispositionType::Inline). - pub fn is_inline(&self) -> bool { - match self.disposition { - DispositionType::Inline => true, - _ => false, - } - } - - /// Returns `true` if it is [`Attachment`](DispositionType::Attachment). - pub fn is_attachment(&self) -> bool { - match self.disposition { - DispositionType::Attachment => true, - _ => false, - } - } - - /// Returns `true` if it is [`FormData`](DispositionType::FormData). - pub fn is_form_data(&self) -> bool { - match self.disposition { - DispositionType::FormData => true, - _ => false, - } - } - - /// Returns `true` if it is [`Ext`](DispositionType::Ext) and the `disp_type` matches. - pub fn is_ext>(&self, disp_type: T) -> bool { - match self.disposition { - DispositionType::Ext(ref t) - if t.eq_ignore_ascii_case(disp_type.as_ref()) => - { - true - } - _ => false, - } - } - - /// Return the value of *name* if exists. - pub fn get_name(&self) -> Option<&str> { - self.parameters.iter().filter_map(|p| p.as_name()).nth(0) - } - - /// Return the value of *filename* if exists. - pub fn get_filename(&self) -> Option<&str> { - self.parameters - .iter() - .filter_map(|p| p.as_filename()) - .nth(0) - } - - /// Return the value of *filename\** if exists. - pub fn get_filename_ext(&self) -> Option<&ExtendedValue> { - self.parameters - .iter() - .filter_map(|p| p.as_filename_ext()) - .nth(0) - } - - /// Return the value of the parameter which the `name` matches. - pub fn get_unknown>(&self, name: T) -> Option<&str> { - let name = name.as_ref(); - self.parameters - .iter() - .filter_map(|p| p.as_unknown(name)) - .nth(0) - } - - /// Return the value of the extended parameter which the `name` matches. - pub fn get_unknown_ext>(&self, name: T) -> Option<&ExtendedValue> { - let name = name.as_ref(); - self.parameters - .iter() - .filter_map(|p| p.as_unknown_ext(name)) - .nth(0) - } -} - -impl IntoHeaderValue for ContentDisposition { - type Error = header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut writer = Writer::new(); - let _ = write!(&mut writer, "{}", self); - header::HeaderValue::from_shared(writer.take()) - } -} - -impl Header for ContentDisposition { - fn name() -> header::HeaderName { - header::CONTENT_DISPOSITION - } - - fn parse(msg: &T) -> Result { - if let Some(h) = msg.headers().get(Self::name()) { - Self::from_raw(&h) - } else { - Err(::error::ParseError::Header) - } - } -} - -impl fmt::Display for DispositionType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - DispositionType::Inline => write!(f, "inline"), - DispositionType::Attachment => write!(f, "attachment"), - DispositionType::FormData => write!(f, "form-data"), - DispositionType::Ext(ref s) => write!(f, "{}", s), - } - } -} - -impl fmt::Display for DispositionParam { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // All ASCII control charaters (0-30, 127) excepting horizontal tab, double quote, and - // backslash should be escaped in quoted-string (i.e. "foobar"). - // Ref: RFC6266 S4.1 -> RFC2616 S2.2; RFC 7578 S4.2 -> RFC2183 S2 -> ... . - lazy_static! { - static ref RE: Regex = Regex::new("[\x01-\x08\x10\x1F\x7F\"\\\\]").unwrap(); - } - match self { - DispositionParam::Name(ref value) => write!(f, "name={}", value), - DispositionParam::Filename(ref value) => { - write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref()) - } - DispositionParam::Unknown(ref name, ref value) => write!( - f, - "{}=\"{}\"", - name, - &RE.replace_all(value, "\\$0").as_ref() - ), - DispositionParam::FilenameExt(ref ext_value) => { - write!(f, "filename*={}", ext_value) - } - DispositionParam::UnknownExt(ref name, ref ext_value) => { - write!(f, "{}*={}", name, ext_value) - } - } - } -} - -impl fmt::Display for ContentDisposition { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.disposition)?; - self.parameters - .iter() - .map(|param| write!(f, "; {}", param)) - .collect() - } -} - -#[cfg(test)] -mod tests { - use super::{ContentDisposition, DispositionParam, DispositionType}; - use header::shared::Charset; - use header::{ExtendedValue, HeaderValue}; - #[test] - fn test_from_raw_basic() { - assert!(ContentDisposition::from_raw(&HeaderValue::from_static("")).is_err()); - - let a = HeaderValue::from_static( - "form-data; dummy=3; name=upload; filename=\"sample.png\"", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), - DispositionParam::Name("upload".to_owned()), - DispositionParam::Filename("sample.png".to_owned()), - ], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static("attachment; filename=\"image.jpg\""); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::Filename("image.jpg".to_owned())], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static("inline; filename=image.jpg"); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![DispositionParam::Filename("image.jpg".to_owned())], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static( - "attachment; creation-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::Unknown( - String::from("creation-date"), - "Wed, 12 Feb 1997 16:29:51 -0500".to_owned(), - )], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_extended() { - let a = HeaderValue::from_static( - "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::FilenameExt(ExtendedValue { - charset: Charset::Ext(String::from("UTF-8")), - language_tag: None, - value: vec![ - 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, - b'r', b'a', b't', b'e', b's', - ], - })], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static( - "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::FilenameExt(ExtendedValue { - charset: Charset::Ext(String::from("UTF-8")), - language_tag: None, - value: vec![ - 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, - b'r', b'a', b't', b'e', b's', - ], - })], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_extra_whitespace() { - let a = HeaderValue::from_static( - "form-data ; du-mmy= 3 ; name =upload ; filename = \"sample.png\" ; ", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("du-mmy".to_owned(), "3".to_owned()), - DispositionParam::Name("upload".to_owned()), - DispositionParam::Filename("sample.png".to_owned()), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_unordered() { - let a = HeaderValue::from_static( - "form-data; dummy=3; filename=\"sample.png\" ; name=upload;", - // Actually, a trailling semolocon is not compliant. But it is fine to accept. - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), - DispositionParam::Filename("sample.png".to_owned()), - DispositionParam::Name("upload".to_owned()), - ], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_str( - "attachment; filename*=iso-8859-1''foo-%E4.html; filename=\"foo-ä.html\"", - ).unwrap(); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![ - DispositionParam::FilenameExt(ExtendedValue { - charset: Charset::Iso_8859_1, - language_tag: None, - value: b"foo-\xe4.html".to_vec(), - }), - DispositionParam::Filename("foo-ä.html".to_owned()), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_only_disp() { - let a = ContentDisposition::from_raw(&HeaderValue::from_static("attachment")) - .unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![], - }; - assert_eq!(a, b); - - let a = - ContentDisposition::from_raw(&HeaderValue::from_static("inline ;")).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![], - }; - assert_eq!(a, b); - - let a = ContentDisposition::from_raw(&HeaderValue::from_static( - "unknown-disp-param", - )).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Ext(String::from("unknown-disp-param")), - parameters: vec![], - }; - assert_eq!(a, b); - } - - #[test] - fn from_raw_with_mixed_case() { - let a = HeaderValue::from_str( - "InLInE; fIlenAME*=iso-8859-1''foo-%E4.html; filEName=\"foo-ä.html\"", - ).unwrap(); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![ - DispositionParam::FilenameExt(ExtendedValue { - charset: Charset::Iso_8859_1, - language_tag: None, - value: b"foo-\xe4.html".to_vec(), - }), - DispositionParam::Filename("foo-ä.html".to_owned()), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn from_raw_with_unicode() { - /* RFC7578 Section 4.2: - Some commonly deployed systems use multipart/form-data with file names directly encoded - including octets outside the US-ASCII range. The encoding used for the file names is - typically UTF-8, although HTML forms will use the charset associated with the form. - - Mainstream browsers like Firefox (gecko) and Chrome use UTF-8 directly as above. - (And now, only UTF-8 is handled by this implementation.) - */ - let a = - HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"") - .unwrap(); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Name(String::from("upload")), - DispositionParam::Filename(String::from("文件.webp")), - ], - }; - assert_eq!(a, b); - - let a = - HeaderValue::from_str("form-data; name=upload; filename=\"余固知謇謇之為患兮,å¿è€Œä¸èƒ½èˆä¹Ÿ.pptx\"").unwrap(); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Name(String::from("upload")), - DispositionParam::Filename(String::from( - "余固知謇謇之為患兮,å¿è€Œä¸èƒ½èˆä¹Ÿ.pptx", - )), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_escape() { - let a = HeaderValue::from_static( - "form-data; dummy=3; name=upload; filename=\"s\\amp\\\"le.png\"", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), - DispositionParam::Name("upload".to_owned()), - DispositionParam::Filename( - ['s', 'a', 'm', 'p', '\"', 'l', 'e', '.', 'p', 'n', 'g'] - .iter() - .collect(), - ), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_semicolon() { - let a = - HeaderValue::from_static("form-data; filename=\"A semicolon here;.pdf\""); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![DispositionParam::Filename(String::from( - "A semicolon here;.pdf", - ))], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_uncessary_percent_decode() { - let a = HeaderValue::from_static( - "form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"", // Should not be decoded! - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Name("photo".to_owned()), - DispositionParam::Filename(String::from("%74%65%73%74%2e%70%6e%67")), - ], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static( - "form-data; name=photo; filename=\"%74%65%73%74.png\"", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Name("photo".to_owned()), - DispositionParam::Filename(String::from("%74%65%73%74.png")), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_param_value_missing() { - let a = HeaderValue::from_static("form-data; name=upload ; filename="); - assert!(ContentDisposition::from_raw(&a).is_err()); - - let a = HeaderValue::from_static("attachment; dummy=; filename=invoice.pdf"); - assert!(ContentDisposition::from_raw(&a).is_err()); - - let a = HeaderValue::from_static("inline; filename= "); - assert!(ContentDisposition::from_raw(&a).is_err()); - } - - #[test] - fn test_from_raw_param_name_missing() { - let a = HeaderValue::from_static("inline; =\"test.txt\""); - assert!(ContentDisposition::from_raw(&a).is_err()); - - let a = HeaderValue::from_static("inline; =diary.odt"); - assert!(ContentDisposition::from_raw(&a).is_err()); - - let a = HeaderValue::from_static("inline; ="); - assert!(ContentDisposition::from_raw(&a).is_err()); - } - - #[test] - fn test_display_extended() { - let as_string = - "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates"; - let a = HeaderValue::from_static(as_string); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!(as_string, display_rendered); - - let a = HeaderValue::from_static("attachment; filename=colourful.csv"); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!( - "attachment; filename=\"colourful.csv\"".to_owned(), - display_rendered - ); - } - - #[test] - fn test_display_quote() { - let as_string = "form-data; name=upload; filename=\"Quote\\\"here.png\""; - as_string - .find(['\\', '\"'].iter().collect::().as_str()) - .unwrap(); // ensure `\"` is there - let a = HeaderValue::from_static(as_string); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!(as_string, display_rendered); - } - - #[test] - fn test_display_space_tab() { - let as_string = "form-data; name=upload; filename=\"Space here.png\""; - let a = HeaderValue::from_static(as_string); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!(as_string, display_rendered); - - let a: ContentDisposition = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![DispositionParam::Filename(String::from("Tab\there.png"))], - }; - let display_rendered = format!("{}", a); - assert_eq!("inline; filename=\"Tab\x09here.png\"", display_rendered); - } - - #[test] - fn test_display_control_characters() { - /* let a = "attachment; filename=\"carriage\rreturn.png\""; - let a = HeaderValue::from_static(a); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!( - "attachment; filename=\"carriage\\\rreturn.png\"", - display_rendered - );*/ - // No way to create a HeaderValue containing a carriage return. - - let a: ContentDisposition = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![DispositionParam::Filename(String::from("bell\x07.png"))], - }; - let display_rendered = format!("{}", a); - assert_eq!("inline; filename=\"bell\\\x07.png\"", display_rendered); - } - - #[test] - fn test_param_methods() { - let param = DispositionParam::Filename(String::from("sample.txt")); - assert!(param.is_filename()); - assert_eq!(param.as_filename().unwrap(), "sample.txt"); - - let param = DispositionParam::Unknown(String::from("foo"), String::from("bar")); - assert!(param.is_unknown("foo")); - assert_eq!(param.as_unknown("fOo"), Some("bar")); - } - - #[test] - fn test_disposition_methods() { - let cd = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), - DispositionParam::Name("upload".to_owned()), - DispositionParam::Filename("sample.png".to_owned()), - ], - }; - assert_eq!(cd.get_name(), Some("upload")); - assert_eq!(cd.get_unknown("dummy"), Some("3")); - assert_eq!(cd.get_filename(), Some("sample.png")); - assert_eq!(cd.get_unknown_ext("dummy"), None); - assert_eq!(cd.get_unknown("duMMy"), Some("3")); - } -} diff --git a/src/header/common/content_language.rs b/src/header/common/content_language.rs deleted file mode 100644 index e12d34d0d..000000000 --- a/src/header/common/content_language.rs +++ /dev/null @@ -1,65 +0,0 @@ -use header::{QualityItem, CONTENT_LANGUAGE}; -use language_tags::LanguageTag; - -header! { - /// `Content-Language` header, defined in - /// [RFC7231](https://tools.ietf.org/html/rfc7231#section-3.1.3.2) - /// - /// The `Content-Language` header field describes the natural language(s) - /// of the intended audience for the representation. Note that this - /// might not be equivalent to all the languages used within the - /// representation. - /// - /// # ABNF - /// - /// ```text - /// Content-Language = 1#language-tag - /// ``` - /// - /// # Example values - /// - /// * `da` - /// * `mi, en` - /// - /// # Examples - /// - /// ```rust - /// # extern crate actix_web; - /// # #[macro_use] extern crate language_tags; - /// use actix_web::HttpResponse; - /// # use actix_web::http::header::{ContentLanguage, qitem}; - /// # - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// ContentLanguage(vec![ - /// qitem(langtag!(en)), - /// ]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate actix_web; - /// # #[macro_use] extern crate language_tags; - /// use actix_web::HttpResponse; - /// # use actix_web::http::header::{ContentLanguage, qitem}; - /// # - /// # fn main() { - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// ContentLanguage(vec![ - /// qitem(langtag!(da)), - /// qitem(langtag!(en;;;GB)), - /// ]) - /// ); - /// # } - /// ``` - (ContentLanguage, CONTENT_LANGUAGE) => (QualityItem)+ - - test_content_language { - test_header!(test1, vec![b"da"]); - test_header!(test2, vec![b"mi, en"]); - } -} diff --git a/src/header/common/content_range.rs b/src/header/common/content_range.rs deleted file mode 100644 index 999307e2a..000000000 --- a/src/header/common/content_range.rs +++ /dev/null @@ -1,210 +0,0 @@ -use error::ParseError; -use header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, Writer, - CONTENT_RANGE}; -use std::fmt::{self, Display, Write}; -use std::str::FromStr; - -header! { - /// `Content-Range` header, defined in - /// [RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2) - (ContentRange, CONTENT_RANGE) => [ContentRangeSpec] - - test_content_range { - test_header!(test_bytes, - vec![b"bytes 0-499/500"], - Some(ContentRange(ContentRangeSpec::Bytes { - range: Some((0, 499)), - instance_length: Some(500) - }))); - - test_header!(test_bytes_unknown_len, - vec![b"bytes 0-499/*"], - Some(ContentRange(ContentRangeSpec::Bytes { - range: Some((0, 499)), - instance_length: None - }))); - - test_header!(test_bytes_unknown_range, - vec![b"bytes */500"], - Some(ContentRange(ContentRangeSpec::Bytes { - range: None, - instance_length: Some(500) - }))); - - test_header!(test_unregistered, - vec![b"seconds 1-2"], - Some(ContentRange(ContentRangeSpec::Unregistered { - unit: "seconds".to_owned(), - resp: "1-2".to_owned() - }))); - - test_header!(test_no_len, - vec![b"bytes 0-499"], - None::); - - test_header!(test_only_unit, - vec![b"bytes"], - None::); - - test_header!(test_end_less_than_start, - vec![b"bytes 499-0/500"], - None::); - - test_header!(test_blank, - vec![b""], - None::); - - test_header!(test_bytes_many_spaces, - vec![b"bytes 1-2/500 3"], - None::); - - test_header!(test_bytes_many_slashes, - vec![b"bytes 1-2/500/600"], - None::); - - test_header!(test_bytes_many_dashes, - vec![b"bytes 1-2-3/500"], - None::); - - } -} - -/// Content-Range, described in [RFC7233](https://tools.ietf.org/html/rfc7233#section-4.2) -/// -/// # ABNF -/// -/// ```text -/// Content-Range = byte-content-range -/// / other-content-range -/// -/// byte-content-range = bytes-unit SP -/// ( byte-range-resp / unsatisfied-range ) -/// -/// byte-range-resp = byte-range "/" ( complete-length / "*" ) -/// byte-range = first-byte-pos "-" last-byte-pos -/// unsatisfied-range = "*/" complete-length -/// -/// complete-length = 1*DIGIT -/// -/// other-content-range = other-range-unit SP other-range-resp -/// other-range-resp = *CHAR -/// ``` -#[derive(PartialEq, Clone, Debug)] -pub enum ContentRangeSpec { - /// Byte range - Bytes { - /// First and last bytes of the range, omitted if request could not be - /// satisfied - range: Option<(u64, u64)>, - - /// Total length of the instance, can be omitted if unknown - instance_length: Option, - }, - - /// Custom range, with unit not registered at IANA - Unregistered { - /// other-range-unit - unit: String, - - /// other-range-resp - resp: String, - }, -} - -fn split_in_two(s: &str, separator: char) -> Option<(&str, &str)> { - let mut iter = s.splitn(2, separator); - match (iter.next(), iter.next()) { - (Some(a), Some(b)) => Some((a, b)), - _ => None, - } -} - -impl FromStr for ContentRangeSpec { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - let res = match split_in_two(s, ' ') { - Some(("bytes", resp)) => { - let (range, instance_length) = - split_in_two(resp, '/').ok_or(ParseError::Header)?; - - let instance_length = if instance_length == "*" { - None - } else { - Some(instance_length - .parse() - .map_err(|_| ParseError::Header)?) - }; - - let range = if range == "*" { - None - } else { - let (first_byte, last_byte) = - split_in_two(range, '-').ok_or(ParseError::Header)?; - let first_byte = first_byte.parse().map_err(|_| ParseError::Header)?; - let last_byte = last_byte.parse().map_err(|_| ParseError::Header)?; - if last_byte < first_byte { - return Err(ParseError::Header); - } - Some((first_byte, last_byte)) - }; - - ContentRangeSpec::Bytes { - range, - instance_length, - } - } - Some((unit, resp)) => ContentRangeSpec::Unregistered { - unit: unit.to_owned(), - resp: resp.to_owned(), - }, - _ => return Err(ParseError::Header), - }; - Ok(res) - } -} - -impl Display for ContentRangeSpec { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ContentRangeSpec::Bytes { - range, - instance_length, - } => { - f.write_str("bytes ")?; - match range { - Some((first_byte, last_byte)) => { - write!(f, "{}-{}", first_byte, last_byte)?; - } - None => { - f.write_str("*")?; - } - }; - f.write_str("/")?; - if let Some(v) = instance_length { - write!(f, "{}", v) - } else { - f.write_str("*") - } - } - ContentRangeSpec::Unregistered { - ref unit, - ref resp, - } => { - f.write_str(unit)?; - f.write_str(" ")?; - f.write_str(resp) - } - } - } -} - -impl IntoHeaderValue for ContentRangeSpec { - type Error = InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut writer = Writer::new(); - let _ = write!(&mut writer, "{}", self); - HeaderValue::from_shared(writer.take()) - } -} diff --git a/src/header/common/content_type.rs b/src/header/common/content_type.rs deleted file mode 100644 index 08900e1cc..000000000 --- a/src/header/common/content_type.rs +++ /dev/null @@ -1,122 +0,0 @@ -use header::CONTENT_TYPE; -use mime::{self, Mime}; - -header! { - /// `Content-Type` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-3.1.1.5) - /// - /// The `Content-Type` header field indicates the media type of the - /// associated representation: either the representation enclosed in the - /// message payload or the selected representation, as determined by the - /// message semantics. The indicated media type defines both the data - /// format and how that data is intended to be processed by a recipient, - /// within the scope of the received message semantics, after any content - /// codings indicated by Content-Encoding are decoded. - /// - /// Although the `mime` crate allows the mime options to be any slice, this crate - /// forces the use of Vec. This is to make sure the same header can't have more than 1 type. If - /// this is an issue, it's possible to implement `Header` on a custom struct. - /// - /// # ABNF - /// - /// ```text - /// Content-Type = media-type - /// ``` - /// - /// # Example values - /// - /// * `text/html; charset=utf-8` - /// * `application/json` - /// - /// # Examples - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::ContentType; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// ContentType::json() - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate mime; - /// # extern crate actix_web; - /// use mime::TEXT_HTML; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::ContentType; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// ContentType(TEXT_HTML) - /// ); - /// # } - /// ``` - (ContentType, CONTENT_TYPE) => [Mime] - - test_content_type { - test_header!( - test1, - vec![b"text/html"], - Some(HeaderField(TEXT_HTML))); - } -} - -impl ContentType { - /// A constructor to easily create a `Content-Type: application/json` - /// header. - #[inline] - pub fn json() -> ContentType { - ContentType(mime::APPLICATION_JSON) - } - - /// A constructor to easily create a `Content-Type: text/plain; - /// charset=utf-8` header. - #[inline] - pub fn plaintext() -> ContentType { - ContentType(mime::TEXT_PLAIN_UTF_8) - } - - /// A constructor to easily create a `Content-Type: text/html` header. - #[inline] - pub fn html() -> ContentType { - ContentType(mime::TEXT_HTML) - } - - /// A constructor to easily create a `Content-Type: text/xml` header. - #[inline] - pub fn xml() -> ContentType { - ContentType(mime::TEXT_XML) - } - - /// A constructor to easily create a `Content-Type: - /// application/www-form-url-encoded` header. - #[inline] - pub fn form_url_encoded() -> ContentType { - ContentType(mime::APPLICATION_WWW_FORM_URLENCODED) - } - /// A constructor to easily create a `Content-Type: image/jpeg` header. - #[inline] - pub fn jpeg() -> ContentType { - ContentType(mime::IMAGE_JPEG) - } - - /// A constructor to easily create a `Content-Type: image/png` header. - #[inline] - pub fn png() -> ContentType { - ContentType(mime::IMAGE_PNG) - } - - /// A constructor to easily create a `Content-Type: - /// application/octet-stream` header. - #[inline] - pub fn octet_stream() -> ContentType { - ContentType(mime::APPLICATION_OCTET_STREAM) - } -} - -impl Eq for ContentType {} diff --git a/src/header/common/date.rs b/src/header/common/date.rs deleted file mode 100644 index 88a47bc3f..000000000 --- a/src/header/common/date.rs +++ /dev/null @@ -1,42 +0,0 @@ -use header::{HttpDate, DATE}; -use std::time::SystemTime; - -header! { - /// `Date` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.1.1.2) - /// - /// The `Date` header field represents the date and time at which the - /// message was originated. - /// - /// # ABNF - /// - /// ```text - /// Date = HTTP-date - /// ``` - /// - /// # Example values - /// - /// * `Tue, 15 Nov 1994 08:12:31 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::Date; - /// use std::time::SystemTime; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(Date(SystemTime::now().into())); - /// ``` - (Date, DATE) => [HttpDate] - - test_date { - test_header!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]); - } -} - -impl Date { - /// Create a date instance set to the current system time - pub fn now() -> Date { - Date(SystemTime::now().into()) - } -} diff --git a/src/header/common/etag.rs b/src/header/common/etag.rs deleted file mode 100644 index 39dd908c1..000000000 --- a/src/header/common/etag.rs +++ /dev/null @@ -1,96 +0,0 @@ -use header::{EntityTag, ETAG}; - -header! { - /// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3) - /// - /// The `ETag` header field in a response provides the current entity-tag - /// for the selected representation, as determined at the conclusion of - /// handling the request. An entity-tag is an opaque validator for - /// differentiating between multiple representations of the same - /// resource, regardless of whether those multiple representations are - /// due to resource state changes over time, content negotiation - /// resulting in multiple representations being valid at the same time, - /// or both. An entity-tag consists of an opaque quoted string, possibly - /// prefixed by a weakness indicator. - /// - /// # ABNF - /// - /// ```text - /// ETag = entity-tag - /// ``` - /// - /// # Example values - /// - /// * `"xyzzy"` - /// * `W/"xyzzy"` - /// * `""` - /// - /// # Examples - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{ETag, EntityTag}; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(ETag(EntityTag::new(false, "xyzzy".to_owned()))); - /// ``` - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{ETag, EntityTag}; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(ETag(EntityTag::new(true, "xyzzy".to_owned()))); - /// ``` - (ETag, ETAG) => [EntityTag] - - test_etag { - // From the RFC - test_header!(test1, - vec![b"\"xyzzy\""], - Some(ETag(EntityTag::new(false, "xyzzy".to_owned())))); - test_header!(test2, - vec![b"W/\"xyzzy\""], - Some(ETag(EntityTag::new(true, "xyzzy".to_owned())))); - test_header!(test3, - vec![b"\"\""], - Some(ETag(EntityTag::new(false, "".to_owned())))); - // Own tests - test_header!(test4, - vec![b"\"foobar\""], - Some(ETag(EntityTag::new(false, "foobar".to_owned())))); - test_header!(test5, - vec![b"\"\""], - Some(ETag(EntityTag::new(false, "".to_owned())))); - test_header!(test6, - vec![b"W/\"weak-etag\""], - Some(ETag(EntityTag::new(true, "weak-etag".to_owned())))); - test_header!(test7, - vec![b"W/\"\x65\x62\""], - Some(ETag(EntityTag::new(true, "\u{0065}\u{0062}".to_owned())))); - test_header!(test8, - vec![b"W/\"\""], - Some(ETag(EntityTag::new(true, "".to_owned())))); - test_header!(test9, - vec![b"no-dquotes"], - None::); - test_header!(test10, - vec![b"w/\"the-first-w-is-case-sensitive\""], - None::); - test_header!(test11, - vec![b""], - None::); - test_header!(test12, - vec![b"\"unmatched-dquotes1"], - None::); - test_header!(test13, - vec![b"unmatched-dquotes2\""], - None::); - test_header!(test14, - vec![b"matched-\"dquotes\""], - None::); - test_header!(test15, - vec![b"\""], - None::); - } -} diff --git a/src/header/common/expires.rs b/src/header/common/expires.rs deleted file mode 100644 index 4ec66b880..000000000 --- a/src/header/common/expires.rs +++ /dev/null @@ -1,39 +0,0 @@ -use header::{HttpDate, EXPIRES}; - -header! { - /// `Expires` header, defined in [RFC7234](http://tools.ietf.org/html/rfc7234#section-5.3) - /// - /// The `Expires` header field gives the date/time after which the - /// response is considered stale. - /// - /// The presence of an Expires field does not imply that the original - /// resource will change or cease to exist at, before, or after that - /// time. - /// - /// # ABNF - /// - /// ```text - /// Expires = HTTP-date - /// ``` - /// - /// # Example values - /// * `Thu, 01 Dec 1994 16:00:00 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::Expires; - /// use std::time::{SystemTime, Duration}; - /// - /// let mut builder = HttpResponse::Ok(); - /// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24); - /// builder.set(Expires(expiration.into())); - /// ``` - (Expires, EXPIRES) => [HttpDate] - - test_expires { - // Test case from RFC - test_header!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]); - } -} diff --git a/src/header/common/if_match.rs b/src/header/common/if_match.rs deleted file mode 100644 index 20a2b1e6b..000000000 --- a/src/header/common/if_match.rs +++ /dev/null @@ -1,70 +0,0 @@ -use header::{EntityTag, IF_MATCH}; - -header! { - /// `If-Match` header, defined in - /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.1) - /// - /// The `If-Match` header field makes the request method conditional on - /// the recipient origin server either having at least one current - /// representation of the target resource, when the field-value is "*", - /// or having a current representation of the target resource that has an - /// entity-tag matching a member of the list of entity-tags provided in - /// the field-value. - /// - /// An origin server MUST use the strong comparison function when - /// comparing entity-tags for `If-Match`, since the client - /// intends this precondition to prevent the method from being applied if - /// there have been any changes to the representation data. - /// - /// # ABNF - /// - /// ```text - /// If-Match = "*" / 1#entity-tag - /// ``` - /// - /// # Example values - /// - /// * `"xyzzy"` - /// * "xyzzy", "r2d2xxxx", "c3piozzzz" - /// - /// # Examples - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfMatch; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(IfMatch::Any); - /// ``` - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{IfMatch, EntityTag}; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// IfMatch::Items(vec![ - /// EntityTag::new(false, "xyzzy".to_owned()), - /// EntityTag::new(false, "foobar".to_owned()), - /// EntityTag::new(false, "bazquux".to_owned()), - /// ]) - /// ); - /// ``` - (IfMatch, IF_MATCH) => {Any / (EntityTag)+} - - test_if_match { - test_header!( - test1, - vec![b"\"xyzzy\""], - Some(HeaderField::Items( - vec![EntityTag::new(false, "xyzzy".to_owned())]))); - test_header!( - test2, - vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""], - Some(HeaderField::Items( - vec![EntityTag::new(false, "xyzzy".to_owned()), - EntityTag::new(false, "r2d2xxxx".to_owned()), - EntityTag::new(false, "c3piozzzz".to_owned())]))); - test_header!(test3, vec![b"*"], Some(IfMatch::Any)); - } -} diff --git a/src/header/common/if_modified_since.rs b/src/header/common/if_modified_since.rs deleted file mode 100644 index 1914d34d3..000000000 --- a/src/header/common/if_modified_since.rs +++ /dev/null @@ -1,39 +0,0 @@ -use header::{HttpDate, IF_MODIFIED_SINCE}; - -header! { - /// `If-Modified-Since` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.3) - /// - /// The `If-Modified-Since` header field makes a GET or HEAD request - /// method conditional on the selected representation's modification date - /// being more recent than the date provided in the field-value. - /// Transfer of the selected representation's data is avoided if that - /// data has not changed. - /// - /// # ABNF - /// - /// ```text - /// If-Unmodified-Since = HTTP-date - /// ``` - /// - /// # Example values - /// * `Sat, 29 Oct 1994 19:43:31 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfModifiedSince; - /// use std::time::{SystemTime, Duration}; - /// - /// let mut builder = HttpResponse::Ok(); - /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); - /// builder.set(IfModifiedSince(modified.into())); - /// ``` - (IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate] - - test_if_modified_since { - // Test case from RFC - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); - } -} diff --git a/src/header/common/if_none_match.rs b/src/header/common/if_none_match.rs deleted file mode 100644 index 124f4b8e0..000000000 --- a/src/header/common/if_none_match.rs +++ /dev/null @@ -1,92 +0,0 @@ -use header::{EntityTag, IF_NONE_MATCH}; - -header! { - /// `If-None-Match` header, defined in - /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.2) - /// - /// The `If-None-Match` header field makes the request method conditional - /// on a recipient cache or origin server either not having any current - /// representation of the target resource, when the field-value is "*", - /// or having a selected representation with an entity-tag that does not - /// match any of those listed in the field-value. - /// - /// A recipient MUST use the weak comparison function when comparing - /// entity-tags for If-None-Match (Section 2.3.2), since weak entity-tags - /// can be used for cache validation even if there have been changes to - /// the representation data. - /// - /// # ABNF - /// - /// ```text - /// If-None-Match = "*" / 1#entity-tag - /// ``` - /// - /// # Example values - /// - /// * `"xyzzy"` - /// * `W/"xyzzy"` - /// * `"xyzzy", "r2d2xxxx", "c3piozzzz"` - /// * `W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"` - /// * `*` - /// - /// # Examples - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfNoneMatch; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(IfNoneMatch::Any); - /// ``` - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{IfNoneMatch, EntityTag}; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// IfNoneMatch::Items(vec![ - /// EntityTag::new(false, "xyzzy".to_owned()), - /// EntityTag::new(false, "foobar".to_owned()), - /// EntityTag::new(false, "bazquux".to_owned()), - /// ]) - /// ); - /// ``` - (IfNoneMatch, IF_NONE_MATCH) => {Any / (EntityTag)+} - - test_if_none_match { - test_header!(test1, vec![b"\"xyzzy\""]); - test_header!(test2, vec![b"W/\"xyzzy\""]); - test_header!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); - test_header!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]); - test_header!(test5, vec![b"*"]); - } -} - -#[cfg(test)] -mod tests { - use super::IfNoneMatch; - use header::{EntityTag, Header, IF_NONE_MATCH}; - use test::TestRequest; - - #[test] - fn test_if_none_match() { - let mut if_none_match: Result; - - let req = TestRequest::with_header(IF_NONE_MATCH, "*").finish(); - if_none_match = Header::parse(&req); - assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any)); - - let req = - TestRequest::with_header(IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..]) - .finish(); - - if_none_match = Header::parse(&req); - let mut entities: Vec = Vec::new(); - let foobar_etag = EntityTag::new(false, "foobar".to_owned()); - let weak_etag = EntityTag::new(true, "weak-etag".to_owned()); - entities.push(foobar_etag); - entities.push(weak_etag); - assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities))); - } -} diff --git a/src/header/common/if_range.rs b/src/header/common/if_range.rs deleted file mode 100644 index dd95b7ba8..000000000 --- a/src/header/common/if_range.rs +++ /dev/null @@ -1,115 +0,0 @@ -use error::ParseError; -use header::from_one_raw_str; -use header::{EntityTag, Header, HeaderName, HeaderValue, HttpDate, IntoHeaderValue, - InvalidHeaderValueBytes, Writer}; -use http::header; -use httpmessage::HttpMessage; -use std::fmt::{self, Display, Write}; - -/// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2) -/// -/// If a client has a partial copy of a representation and wishes to have -/// an up-to-date copy of the entire representation, it could use the -/// Range header field with a conditional GET (using either or both of -/// If-Unmodified-Since and If-Match.) However, if the precondition -/// fails because the representation has been modified, the client would -/// then have to make a second request to obtain the entire current -/// representation. -/// -/// The `If-Range` header field allows a client to \"short-circuit\" the -/// second request. Informally, its meaning is as follows: if the -/// representation is unchanged, send me the part(s) that I am requesting -/// in Range; otherwise, send me the entire representation. -/// -/// # ABNF -/// -/// ```text -/// If-Range = entity-tag / HTTP-date -/// ``` -/// -/// # Example values -/// -/// * `Sat, 29 Oct 1994 19:43:31 GMT` -/// * `\"xyzzy\"` -/// -/// # Examples -/// -/// ```rust -/// use actix_web::HttpResponse; -/// use actix_web::http::header::{EntityTag, IfRange}; -/// -/// let mut builder = HttpResponse::Ok(); -/// builder.set(IfRange::EntityTag(EntityTag::new( -/// false, -/// "xyzzy".to_owned(), -/// ))); -/// ``` -/// -/// ```rust -/// use actix_web::HttpResponse; -/// use actix_web::http::header::IfRange; -/// use std::time::{Duration, SystemTime}; -/// -/// let mut builder = HttpResponse::Ok(); -/// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24); -/// builder.set(IfRange::Date(fetched.into())); -/// ``` -#[derive(Clone, Debug, PartialEq)] -pub enum IfRange { - /// The entity-tag the client has of the resource - EntityTag(EntityTag), - /// The date when the client retrieved the resource - Date(HttpDate), -} - -impl Header for IfRange { - fn name() -> HeaderName { - header::IF_RANGE - } - #[inline] - fn parse(msg: &T) -> Result - where - T: HttpMessage, - { - let etag: Result = - from_one_raw_str(msg.headers().get(header::IF_RANGE)); - if let Ok(etag) = etag { - return Ok(IfRange::EntityTag(etag)); - } - let date: Result = - from_one_raw_str(msg.headers().get(header::IF_RANGE)); - if let Ok(date) = date { - return Ok(IfRange::Date(date)); - } - Err(ParseError::Header) - } -} - -impl Display for IfRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - IfRange::EntityTag(ref x) => Display::fmt(x, f), - IfRange::Date(ref x) => Display::fmt(x, f), - } - } -} - -impl IntoHeaderValue for IfRange { - type Error = InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut writer = Writer::new(); - let _ = write!(&mut writer, "{}", self); - HeaderValue::from_shared(writer.take()) - } -} - -#[cfg(test)] -mod test_if_range { - use super::IfRange as HeaderField; - use header::*; - use std::str; - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); - test_header!(test2, vec![b"\"xyzzy\""]); - test_header!(test3, vec![b"this-is-invalid"], None::); -} diff --git a/src/header/common/if_unmodified_since.rs b/src/header/common/if_unmodified_since.rs deleted file mode 100644 index f87e760c0..000000000 --- a/src/header/common/if_unmodified_since.rs +++ /dev/null @@ -1,40 +0,0 @@ -use header::{HttpDate, IF_UNMODIFIED_SINCE}; - -header! { - /// `If-Unmodified-Since` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.4) - /// - /// The `If-Unmodified-Since` header field makes the request method - /// conditional on the selected representation's last modification date - /// being earlier than or equal to the date provided in the field-value. - /// This field accomplishes the same purpose as If-Match for cases where - /// the user agent does not have an entity-tag for the representation. - /// - /// # ABNF - /// - /// ```text - /// If-Unmodified-Since = HTTP-date - /// ``` - /// - /// # Example values - /// - /// * `Sat, 29 Oct 1994 19:43:31 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfUnmodifiedSince; - /// use std::time::{SystemTime, Duration}; - /// - /// let mut builder = HttpResponse::Ok(); - /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); - /// builder.set(IfUnmodifiedSince(modified.into())); - /// ``` - (IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate] - - test_if_unmodified_since { - // Test case from RFC - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); - } -} diff --git a/src/header/common/last_modified.rs b/src/header/common/last_modified.rs deleted file mode 100644 index aba828883..000000000 --- a/src/header/common/last_modified.rs +++ /dev/null @@ -1,38 +0,0 @@ -use header::{HttpDate, LAST_MODIFIED}; - -header! { - /// `Last-Modified` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2) - /// - /// The `Last-Modified` header field in a response provides a timestamp - /// indicating the date and time at which the origin server believes the - /// selected representation was last modified, as determined at the - /// conclusion of handling the request. - /// - /// # ABNF - /// - /// ```text - /// Expires = HTTP-date - /// ``` - /// - /// # Example values - /// - /// * `Sat, 29 Oct 1994 19:43:31 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::LastModified; - /// use std::time::{SystemTime, Duration}; - /// - /// let mut builder = HttpResponse::Ok(); - /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); - /// builder.set(LastModified(modified.into())); - /// ``` - (LastModified, LAST_MODIFIED) => [HttpDate] - - test_last_modified { - // Test case from RFC - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);} -} diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs deleted file mode 100644 index e6185b5a7..000000000 --- a/src/header/common/mod.rs +++ /dev/null @@ -1,350 +0,0 @@ -//! A Collection of Header implementations for common HTTP Headers. -//! -//! ## Mime -//! -//! Several header fields use MIME values for their contents. Keeping with the -//! strongly-typed theme, the [mime](https://docs.rs/mime) crate -//! is used, such as `ContentType(pub Mime)`. -#![cfg_attr(rustfmt, rustfmt_skip)] - -pub use self::accept_charset::AcceptCharset; -//pub use self::accept_encoding::AcceptEncoding; -pub use self::accept_language::AcceptLanguage; -pub use self::accept::Accept; -pub use self::allow::Allow; -pub use self::cache_control::{CacheControl, CacheDirective}; -pub use self::content_disposition::{ContentDisposition, DispositionType, DispositionParam}; -pub use self::content_language::ContentLanguage; -pub use self::content_range::{ContentRange, ContentRangeSpec}; -pub use self::content_type::ContentType; -pub use self::date::Date; -pub use self::etag::ETag; -pub use self::expires::Expires; -pub use self::if_match::IfMatch; -pub use self::if_modified_since::IfModifiedSince; -pub use self::if_none_match::IfNoneMatch; -pub use self::if_range::IfRange; -pub use self::if_unmodified_since::IfUnmodifiedSince; -pub use self::last_modified::LastModified; -//pub use self::range::{Range, ByteRangeSpec}; - -#[doc(hidden)] -#[macro_export] -macro_rules! __hyper__deref { - ($from:ty => $to:ty) => { - impl ::std::ops::Deref for $from { - type Target = $to; - - #[inline] - fn deref(&self) -> &$to { - &self.0 - } - } - - impl ::std::ops::DerefMut for $from { - #[inline] - fn deref_mut(&mut self) -> &mut $to { - &mut self.0 - } - } - } -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __hyper__tm { - ($id:ident, $tm:ident{$($tf:item)*}) => { - #[allow(unused_imports)] - #[cfg(test)] - mod $tm{ - use std::str; - use http::Method; - use $crate::header::*; - use $crate::mime::*; - use super::$id as HeaderField; - $($tf)* - } - - } -} - -#[doc(hidden)] -#[macro_export] -macro_rules! test_header { - ($id:ident, $raw:expr) => { - #[test] - fn $id() { - use test; - let raw = $raw; - let a: Vec> = raw.iter().map(|x| x.to_vec()).collect(); - let mut req = test::TestRequest::default(); - for item in a { - req = req.header(HeaderField::name(), item); - } - let req = req.finish(); - let value = HeaderField::parse(&req); - let result = format!("{}", value.unwrap()); - let expected = String::from_utf8(raw[0].to_vec()).unwrap(); - let result_cmp: Vec = result - .to_ascii_lowercase() - .split(' ') - .map(|x| x.to_owned()) - .collect(); - let expected_cmp: Vec = expected - .to_ascii_lowercase() - .split(' ') - .map(|x| x.to_owned()) - .collect(); - assert_eq!(result_cmp.concat(), expected_cmp.concat()); - } - }; - ($id:ident, $raw:expr, $typed:expr) => { - #[test] - fn $id() { - use $crate::test; - let a: Vec> = $raw.iter().map(|x| x.to_vec()).collect(); - let mut req = test::TestRequest::default(); - for item in a { - req = req.header(HeaderField::name(), item); - } - let req = req.finish(); - let val = HeaderField::parse(&req); - let typed: Option = $typed; - // Test parsing - assert_eq!(val.ok(), typed); - // Test formatting - if typed.is_some() { - let raw = &($raw)[..]; - let mut iter = raw.iter().map(|b|str::from_utf8(&b[..]).unwrap()); - let mut joined = String::new(); - joined.push_str(iter.next().unwrap()); - for s in iter { - joined.push_str(", "); - joined.push_str(s); - } - assert_eq!(format!("{}", typed.unwrap()), joined); - } - } - } -} - -#[macro_export] -macro_rules! header { - // $a:meta: Attributes associated with the header item (usually docs) - // $id:ident: Identifier of the header - // $n:expr: Lowercase name of the header - // $nn:expr: Nice name of the header - - // List header, zero or more items - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)*) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub struct $id(pub Vec<$item>); - __hyper__deref!($id => Vec<$item>); - impl $crate::header::Header for $id { - #[inline] - fn name() -> $crate::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::header::from_comma_delimited( - msg.headers().get_all(Self::name())).map($id) - } - } - impl ::std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - $crate::header::fmt_comma_delimited(f, &self.0[..]) - } - } - impl $crate::header::IntoHeaderValue for $id { - type Error = $crate::header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> { - use std::fmt::Write; - let mut writer = $crate::header::Writer::new(); - let _ = write!(&mut writer, "{}", self); - $crate::header::HeaderValue::from_shared(writer.take()) - } - } - }; - // List header, one or more items - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)+) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub struct $id(pub Vec<$item>); - __hyper__deref!($id => Vec<$item>); - impl $crate::header::Header for $id { - #[inline] - fn name() -> $crate::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::header::from_comma_delimited( - msg.headers().get_all(Self::name())).map($id) - } - } - impl ::std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - $crate::header::fmt_comma_delimited(f, &self.0[..]) - } - } - impl $crate::header::IntoHeaderValue for $id { - type Error = $crate::header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> { - use std::fmt::Write; - let mut writer = $crate::header::Writer::new(); - let _ = write!(&mut writer, "{}", self); - $crate::header::HeaderValue::from_shared(writer.take()) - } - } - }; - // Single value header - ($(#[$a:meta])*($id:ident, $name:expr) => [$value:ty]) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub struct $id(pub $value); - __hyper__deref!($id => $value); - impl $crate::header::Header for $id { - #[inline] - fn name() -> $crate::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::header::from_one_raw_str( - msg.headers().get(Self::name())).map($id) - } - } - impl ::std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - ::std::fmt::Display::fmt(&self.0, f) - } - } - impl $crate::header::IntoHeaderValue for $id { - type Error = $crate::header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> { - self.0.try_into() - } - } - }; - // List header, one or more items with "*" option - ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub enum $id { - /// Any value is a match - Any, - /// Only the listed items are a match - Items(Vec<$item>), - } - impl $crate::header::Header for $id { - #[inline] - fn name() -> $crate::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - let any = msg.headers().get(Self::name()).and_then(|hdr| { - hdr.to_str().ok().and_then(|hdr| Some(hdr.trim() == "*"))}); - - if let Some(true) = any { - Ok($id::Any) - } else { - Ok($id::Items( - $crate::header::from_comma_delimited( - msg.headers().get_all(Self::name()))?)) - } - } - } - impl ::std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - match *self { - $id::Any => f.write_str("*"), - $id::Items(ref fields) => $crate::header::fmt_comma_delimited( - f, &fields[..]) - } - } - } - impl $crate::header::IntoHeaderValue for $id { - type Error = $crate::header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> { - use std::fmt::Write; - let mut writer = $crate::header::Writer::new(); - let _ = write!(&mut writer, "{}", self); - $crate::header::HeaderValue::from_shared(writer.take()) - } - } - }; - - // optional test module - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* - ($id, $name) => ($item)* - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; - ($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* - ($id, $n) => ($item)+ - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; - ($(#[$a:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* ($id, $name) => [$item] - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; - ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* - ($id, $name) => {Any / ($item)+} - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; -} - - -mod accept_charset; -//mod accept_encoding; -mod accept_language; -mod accept; -mod allow; -mod cache_control; -mod content_disposition; -mod content_language; -mod content_range; -mod content_type; -mod date; -mod etag; -mod expires; -mod if_match; -mod if_modified_since; -mod if_none_match; -mod if_range; -mod if_unmodified_since; -mod last_modified; -//mod range; diff --git a/src/header/common/range.rs b/src/header/common/range.rs deleted file mode 100644 index 71718fc7a..000000000 --- a/src/header/common/range.rs +++ /dev/null @@ -1,434 +0,0 @@ -use std::fmt::{self, Display}; -use std::str::FromStr; - -use header::parsing::from_one_raw_str; -use header::{Header, Raw}; - -/// `Range` header, defined in [RFC7233](https://tools.ietf.org/html/rfc7233#section-3.1) -/// -/// The "Range" header field on a GET request modifies the method -/// semantics to request transfer of only one or more subranges of the -/// selected representation data, rather than the entire selected -/// representation data. -/// -/// # ABNF -/// -/// ```text -/// Range = byte-ranges-specifier / other-ranges-specifier -/// other-ranges-specifier = other-range-unit "=" other-range-set -/// other-range-set = 1*VCHAR -/// -/// bytes-unit = "bytes" -/// -/// byte-ranges-specifier = bytes-unit "=" byte-range-set -/// byte-range-set = 1#(byte-range-spec / suffix-byte-range-spec) -/// byte-range-spec = first-byte-pos "-" [last-byte-pos] -/// first-byte-pos = 1*DIGIT -/// last-byte-pos = 1*DIGIT -/// ``` -/// -/// # Example values -/// -/// * `bytes=1000-` -/// * `bytes=-2000` -/// * `bytes=0-1,30-40` -/// * `bytes=0-10,20-90,-100` -/// * `custom_unit=0-123` -/// * `custom_unit=xxx-yyy` -/// -/// # Examples -/// -/// ``` -/// use hyper::header::{Headers, Range, ByteRangeSpec}; -/// -/// let mut headers = Headers::new(); -/// headers.set(Range::Bytes( -/// vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::AllFrom(200)] -/// )); -/// -/// headers.clear(); -/// headers.set(Range::Unregistered("letters".to_owned(), "a-f".to_owned())); -/// ``` -/// -/// ``` -/// use hyper::header::{Headers, Range}; -/// -/// let mut headers = Headers::new(); -/// headers.set(Range::bytes(1, 100)); -/// -/// headers.clear(); -/// headers.set(Range::bytes_multi(vec![(1, 100), (200, 300)])); -/// ``` -#[derive(PartialEq, Clone, Debug)] -pub enum Range { - /// Byte range - Bytes(Vec), - /// Custom range, with unit not registered at IANA - /// (`other-range-unit`: String , `other-range-set`: String) - Unregistered(String, String), -} - -/// Each `Range::Bytes` header can contain one or more `ByteRangeSpecs`. -/// Each `ByteRangeSpec` defines a range of bytes to fetch -#[derive(PartialEq, Clone, Debug)] -pub enum ByteRangeSpec { - /// Get all bytes between x and y ("x-y") - FromTo(u64, u64), - /// Get all bytes starting from x ("x-") - AllFrom(u64), - /// Get last x bytes ("-x") - Last(u64), -} - -impl ByteRangeSpec { - /// Given the full length of the entity, attempt to normalize the byte range - /// into an satisfiable end-inclusive (from, to) range. - /// - /// The resulting range is guaranteed to be a satisfiable range within the - /// bounds of `0 <= from <= to < full_length`. - /// - /// If the byte range is deemed unsatisfiable, `None` is returned. - /// An unsatisfiable range is generally cause for a server to either reject - /// the client request with a `416 Range Not Satisfiable` status code, or to - /// simply ignore the range header and serve the full entity using a `200 - /// OK` status code. - /// - /// This function closely follows [RFC 7233][1] section 2.1. - /// As such, it considers ranges to be satisfiable if they meet the - /// following conditions: - /// - /// > If a valid byte-range-set includes at least one byte-range-spec with - /// a first-byte-pos that is less than the current length of the - /// representation, or at least one suffix-byte-range-spec with a - /// non-zero suffix-length, then the byte-range-set is satisfiable. - /// Otherwise, the byte-range-set is unsatisfiable. - /// - /// The function also computes remainder ranges based on the RFC: - /// - /// > If the last-byte-pos value is - /// absent, or if the value is greater than or equal to the current - /// length of the representation data, the byte range is interpreted as - /// the remainder of the representation (i.e., the server replaces the - /// value of last-byte-pos with a value that is one less than the current - /// length of the selected representation). - /// - /// [1]: https://tools.ietf.org/html/rfc7233 - pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> { - // If the full length is zero, there is no satisfiable end-inclusive range. - if full_length == 0 { - return None; - } - match self { - &ByteRangeSpec::FromTo(from, to) => { - if from < full_length && from <= to { - Some((from, ::std::cmp::min(to, full_length - 1))) - } else { - None - } - } - &ByteRangeSpec::AllFrom(from) => { - if from < full_length { - Some((from, full_length - 1)) - } else { - None - } - } - &ByteRangeSpec::Last(last) => { - if last > 0 { - // From the RFC: If the selected representation is shorter - // than the specified suffix-length, - // the entire representation is used. - if last > full_length { - Some((0, full_length - 1)) - } else { - Some((full_length - last, full_length - 1)) - } - } else { - None - } - } - } - } -} - -impl Range { - /// Get the most common byte range header ("bytes=from-to") - pub fn bytes(from: u64, to: u64) -> Range { - Range::Bytes(vec![ByteRangeSpec::FromTo(from, to)]) - } - - /// Get byte range header with multiple subranges - /// ("bytes=from1-to1,from2-to2,fromX-toX") - pub fn bytes_multi(ranges: Vec<(u64, u64)>) -> Range { - Range::Bytes( - ranges - .iter() - .map(|r| ByteRangeSpec::FromTo(r.0, r.1)) - .collect(), - ) - } -} - -impl fmt::Display for ByteRangeSpec { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ByteRangeSpec::FromTo(from, to) => write!(f, "{}-{}", from, to), - ByteRangeSpec::Last(pos) => write!(f, "-{}", pos), - ByteRangeSpec::AllFrom(pos) => write!(f, "{}-", pos), - } - } -} - -impl fmt::Display for Range { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Range::Bytes(ref ranges) => { - try!(write!(f, "bytes=")); - - for (i, range) in ranges.iter().enumerate() { - if i != 0 { - try!(f.write_str(",")); - } - try!(Display::fmt(range, f)); - } - Ok(()) - } - Range::Unregistered(ref unit, ref range_str) => { - write!(f, "{}={}", unit, range_str) - } - } - } -} - -impl FromStr for Range { - type Err = ::Error; - - fn from_str(s: &str) -> ::Result { - let mut iter = s.splitn(2, '='); - - match (iter.next(), iter.next()) { - (Some("bytes"), Some(ranges)) => { - let ranges = from_comma_delimited(ranges); - if ranges.is_empty() { - return Err(::Error::Header); - } - Ok(Range::Bytes(ranges)) - } - (Some(unit), Some(range_str)) if unit != "" && range_str != "" => Ok( - Range::Unregistered(unit.to_owned(), range_str.to_owned()), - ), - _ => Err(::Error::Header), - } - } -} - -impl FromStr for ByteRangeSpec { - type Err = ::Error; - - fn from_str(s: &str) -> ::Result { - let mut parts = s.splitn(2, '-'); - - match (parts.next(), parts.next()) { - (Some(""), Some(end)) => end.parse() - .or(Err(::Error::Header)) - .map(ByteRangeSpec::Last), - (Some(start), Some("")) => start - .parse() - .or(Err(::Error::Header)) - .map(ByteRangeSpec::AllFrom), - (Some(start), Some(end)) => match (start.parse(), end.parse()) { - (Ok(start), Ok(end)) if start <= end => { - Ok(ByteRangeSpec::FromTo(start, end)) - } - _ => Err(::Error::Header), - }, - _ => Err(::Error::Header), - } - } -} - -fn from_comma_delimited(s: &str) -> Vec { - s.split(',') - .filter_map(|x| match x.trim() { - "" => None, - y => Some(y), - }) - .filter_map(|x| x.parse().ok()) - .collect() -} - -impl Header for Range { - fn header_name() -> &'static str { - static NAME: &'static str = "Range"; - NAME - } - - fn parse_header(raw: &Raw) -> ::Result { - from_one_raw_str(raw) - } - - fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result { - f.fmt_line(self) - } -} - -#[test] -fn test_parse_bytes_range_valid() { - let r: Range = Header::parse_header(&"bytes=1-100".into()).unwrap(); - let r2: Range = Header::parse_header(&"bytes=1-100,-".into()).unwrap(); - let r3 = Range::bytes(1, 100); - assert_eq!(r, r2); - assert_eq!(r2, r3); - - let r: Range = Header::parse_header(&"bytes=1-100,200-".into()).unwrap(); - let r2: Range = - Header::parse_header(&"bytes= 1-100 , 101-xxx, 200- ".into()).unwrap(); - let r3 = Range::Bytes(vec![ - ByteRangeSpec::FromTo(1, 100), - ByteRangeSpec::AllFrom(200), - ]); - assert_eq!(r, r2); - assert_eq!(r2, r3); - - let r: Range = Header::parse_header(&"bytes=1-100,-100".into()).unwrap(); - let r2: Range = Header::parse_header(&"bytes=1-100, ,,-100".into()).unwrap(); - let r3 = Range::Bytes(vec![ - ByteRangeSpec::FromTo(1, 100), - ByteRangeSpec::Last(100), - ]); - assert_eq!(r, r2); - assert_eq!(r2, r3); - - let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); - assert_eq!(r, r2); -} - -#[test] -fn test_parse_unregistered_range_valid() { - let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); - assert_eq!(r, r2); - - let r: Range = Header::parse_header(&"custom=abcd".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "abcd".to_owned()); - assert_eq!(r, r2); - - let r: Range = Header::parse_header(&"custom=xxx-yyy".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "xxx-yyy".to_owned()); - assert_eq!(r, r2); -} - -#[test] -fn test_parse_invalid() { - let r: ::Result = Header::parse_header(&"bytes=1-a,-".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"bytes=1-2-3".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"abc".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"bytes=1-100=".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"bytes=".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"custom=".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"=1-100".into()); - assert_eq!(r.ok(), None); -} - -#[test] -fn test_fmt() { - use header::Headers; - - let mut headers = Headers::new(); - - headers.set(Range::Bytes(vec![ - ByteRangeSpec::FromTo(0, 1000), - ByteRangeSpec::AllFrom(2000), - ])); - assert_eq!(&headers.to_string(), "Range: bytes=0-1000,2000-\r\n"); - - headers.clear(); - headers.set(Range::Bytes(vec![])); - - assert_eq!(&headers.to_string(), "Range: bytes=\r\n"); - - headers.clear(); - headers.set(Range::Unregistered( - "custom".to_owned(), - "1-xxx".to_owned(), - )); - - assert_eq!(&headers.to_string(), "Range: custom=1-xxx\r\n"); -} - -#[test] -fn test_byte_range_spec_to_satisfiable_range() { - assert_eq!( - Some((0, 0)), - ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(3) - ); - assert_eq!( - Some((1, 2)), - ByteRangeSpec::FromTo(1, 2).to_satisfiable_range(3) - ); - assert_eq!( - Some((1, 2)), - ByteRangeSpec::FromTo(1, 5).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::FromTo(3, 3).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::FromTo(2, 1).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(0) - ); - - assert_eq!( - Some((0, 2)), - ByteRangeSpec::AllFrom(0).to_satisfiable_range(3) - ); - assert_eq!( - Some((2, 2)), - ByteRangeSpec::AllFrom(2).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::AllFrom(3).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::AllFrom(5).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::AllFrom(0).to_satisfiable_range(0) - ); - - assert_eq!( - Some((1, 2)), - ByteRangeSpec::Last(2).to_satisfiable_range(3) - ); - assert_eq!( - Some((2, 2)), - ByteRangeSpec::Last(1).to_satisfiable_range(3) - ); - assert_eq!( - Some((0, 2)), - ByteRangeSpec::Last(5).to_satisfiable_range(3) - ); - assert_eq!(None, ByteRangeSpec::Last(0).to_satisfiable_range(3)); - assert_eq!(None, ByteRangeSpec::Last(2).to_satisfiable_range(0)); -} diff --git a/src/header/mod.rs b/src/header/mod.rs deleted file mode 100644 index 74e4b03e5..000000000 --- a/src/header/mod.rs +++ /dev/null @@ -1,471 +0,0 @@ -//! Various http headers -// This is mostly copy of [hyper](https://github.com/hyperium/hyper/tree/master/src/header) - -use std::fmt; -use std::str::FromStr; - -use bytes::{Bytes, BytesMut}; -use mime::Mime; -use modhttp::header::GetAll; -use modhttp::Error as HttpError; -use percent_encoding; - -pub use modhttp::header::*; - -use error::ParseError; -use httpmessage::HttpMessage; - -mod common; -mod shared; -#[doc(hidden)] -pub use self::common::*; -#[doc(hidden)] -pub use self::shared::*; - -#[doc(hidden)] -/// A trait for any object that will represent a header field and value. -pub trait Header -where - Self: IntoHeaderValue, -{ - /// Returns the name of the header field - fn name() -> HeaderName; - - /// Parse a header - fn parse(msg: &T) -> Result; -} - -#[doc(hidden)] -/// A trait for any object that can be Converted to a `HeaderValue` -pub trait IntoHeaderValue: Sized { - /// The type returned in the event of a conversion error. - type Error: Into; - - /// Try to convert value to a Header value. - fn try_into(self) -> Result; -} - -impl IntoHeaderValue for HeaderValue { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - Ok(self) - } -} - -impl<'a> IntoHeaderValue for &'a str { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - self.parse() - } -} - -impl<'a> IntoHeaderValue for &'a [u8] { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_bytes(self) - } -} - -impl IntoHeaderValue for Bytes { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(self) - } -} - -impl IntoHeaderValue for Vec { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(Bytes::from(self)) - } -} - -impl IntoHeaderValue for String { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(Bytes::from(self)) - } -} - -impl IntoHeaderValue for Mime { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(Bytes::from(format!("{}", self))) - } -} - -/// Represents supported types of content encodings -#[derive(Copy, Clone, PartialEq, Debug)] -pub enum ContentEncoding { - /// Automatically select encoding based on encoding negotiation - Auto, - /// A format using the Brotli algorithm - #[cfg(feature = "brotli")] - Br, - /// A format using the zlib structure with deflate algorithm - #[cfg(feature = "flate2")] - Deflate, - /// Gzip algorithm - #[cfg(feature = "flate2")] - Gzip, - /// Indicates the identity function (i.e. no compression, nor modification) - Identity, -} - -impl ContentEncoding { - #[inline] - /// Is the content compressed? - pub fn is_compression(self) -> bool { - match self { - ContentEncoding::Identity | ContentEncoding::Auto => false, - _ => true, - } - } - - #[inline] - /// Convert content encoding to string - pub fn as_str(self) -> &'static str { - match self { - #[cfg(feature = "brotli")] - ContentEncoding::Br => "br", - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => "gzip", - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => "deflate", - ContentEncoding::Identity | ContentEncoding::Auto => "identity", - } - } - - #[inline] - /// default quality value - pub fn quality(self) -> f64 { - match self { - #[cfg(feature = "brotli")] - ContentEncoding::Br => 1.1, - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => 1.0, - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => 0.9, - ContentEncoding::Identity | ContentEncoding::Auto => 0.1, - } - } -} - -// TODO: remove memory allocation -impl<'a> From<&'a str> for ContentEncoding { - fn from(s: &'a str) -> ContentEncoding { - match AsRef::::as_ref(&s.trim().to_lowercase()) { - #[cfg(feature = "brotli")] - "br" => ContentEncoding::Br, - #[cfg(feature = "flate2")] - "gzip" => ContentEncoding::Gzip, - #[cfg(feature = "flate2")] - "deflate" => ContentEncoding::Deflate, - _ => ContentEncoding::Identity, - } - } -} - -#[doc(hidden)] -pub(crate) struct Writer { - buf: BytesMut, -} - -impl Writer { - fn new() -> Writer { - Writer { - buf: BytesMut::new(), - } - } - fn take(&mut self) -> Bytes { - self.buf.take().freeze() - } -} - -impl fmt::Write for Writer { - #[inline] - fn write_str(&mut self, s: &str) -> fmt::Result { - self.buf.extend_from_slice(s.as_bytes()); - Ok(()) - } - - #[inline] - fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result { - fmt::write(self, args) - } -} - -#[inline] -#[doc(hidden)] -/// Reads a comma-delimited raw header into a Vec. -pub fn from_comma_delimited( - all: GetAll, -) -> Result, ParseError> { - let mut result = Vec::new(); - for h in all { - let s = h.to_str().map_err(|_| ParseError::Header)?; - result.extend( - s.split(',') - .filter_map(|x| match x.trim() { - "" => None, - y => Some(y), - }).filter_map(|x| x.trim().parse().ok()), - ) - } - Ok(result) -} - -#[inline] -#[doc(hidden)] -/// Reads a single string when parsing a header. -pub fn from_one_raw_str(val: Option<&HeaderValue>) -> Result { - if let Some(line) = val { - let line = line.to_str().map_err(|_| ParseError::Header)?; - if !line.is_empty() { - return T::from_str(line).or(Err(ParseError::Header)); - } - } - Err(ParseError::Header) -} - -#[inline] -#[doc(hidden)] -/// Format an array into a comma-delimited string. -pub fn fmt_comma_delimited(f: &mut fmt::Formatter, parts: &[T]) -> fmt::Result -where - T: fmt::Display, -{ - let mut iter = parts.iter(); - if let Some(part) = iter.next() { - fmt::Display::fmt(part, f)?; - } - for part in iter { - f.write_str(", ")?; - fmt::Display::fmt(part, f)?; - } - Ok(()) -} - -// From hyper v0.11.27 src/header/parsing.rs - -/// The value part of an extended parameter consisting of three parts: -/// the REQUIRED character set name (`charset`), the OPTIONAL language information (`language_tag`), -/// and a character sequence representing the actual value (`value`), separated by single quote -/// characters. It is defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). -#[derive(Clone, Debug, PartialEq)] -pub struct ExtendedValue { - /// The character set that is used to encode the `value` to a string. - pub charset: Charset, - /// The human language details of the `value`, if available. - pub language_tag: Option, - /// The parameter value, as expressed in octets. - pub value: Vec, -} - -/// Parses extended header parameter values (`ext-value`), as defined in -/// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). -/// -/// Extended values are denoted by parameter names that end with `*`. -/// -/// ## ABNF -/// -/// ```text -/// ext-value = charset "'" [ language ] "'" value-chars -/// ; like RFC 2231's -/// ; (see [RFC2231], Section 7) -/// -/// charset = "UTF-8" / "ISO-8859-1" / mime-charset -/// -/// mime-charset = 1*mime-charsetc -/// mime-charsetc = ALPHA / DIGIT -/// / "!" / "#" / "$" / "%" / "&" -/// / "+" / "-" / "^" / "_" / "`" -/// / "{" / "}" / "~" -/// ; as in Section 2.3 of [RFC2978] -/// ; except that the single quote is not included -/// ; SHOULD be registered in the IANA charset registry -/// -/// language = -/// -/// value-chars = *( pct-encoded / attr-char ) -/// -/// pct-encoded = "%" HEXDIG HEXDIG -/// ; see [RFC3986], Section 2.1 -/// -/// attr-char = ALPHA / DIGIT -/// / "!" / "#" / "$" / "&" / "+" / "-" / "." -/// / "^" / "_" / "`" / "|" / "~" -/// ; token except ( "*" / "'" / "%" ) -/// ``` -pub fn parse_extended_value(val: &str) -> Result { - // Break into three pieces separated by the single-quote character - let mut parts = val.splitn(3, '\''); - - // Interpret the first piece as a Charset - let charset: Charset = match parts.next() { - None => return Err(::error::ParseError::Header), - Some(n) => FromStr::from_str(n).map_err(|_| ::error::ParseError::Header)?, - }; - - // Interpret the second piece as a language tag - let language_tag: Option = match parts.next() { - None => return Err(::error::ParseError::Header), - Some("") => None, - Some(s) => match s.parse() { - Ok(lt) => Some(lt), - Err(_) => return Err(::error::ParseError::Header), - }, - }; - - // Interpret the third piece as a sequence of value characters - let value: Vec = match parts.next() { - None => return Err(::error::ParseError::Header), - Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(), - }; - - Ok(ExtendedValue { - value, - charset, - language_tag, - }) -} - -impl fmt::Display for ExtendedValue { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let encoded_value = percent_encoding::percent_encode( - &self.value[..], - self::percent_encoding_http::HTTP_VALUE, - ); - if let Some(ref lang) = self.language_tag { - write!(f, "{}'{}'{}", self.charset, lang, encoded_value) - } else { - write!(f, "{}''{}", self.charset, encoded_value) - } - } -} - -/// Percent encode a sequence of bytes with a character set defined in -/// [https://tools.ietf.org/html/rfc5987#section-3.2][url] -/// -/// [url]: https://tools.ietf.org/html/rfc5987#section-3.2 -pub fn http_percent_encode(f: &mut fmt::Formatter, bytes: &[u8]) -> fmt::Result { - let encoded = - percent_encoding::percent_encode(bytes, self::percent_encoding_http::HTTP_VALUE); - fmt::Display::fmt(&encoded, f) -} -mod percent_encoding_http { - use percent_encoding; - - // internal module because macro is hard-coded to make a public item - // but we don't want to public export this item - define_encode_set! { - // This encode set is used for HTTP header values and is defined at - // https://tools.ietf.org/html/rfc5987#section-3.2 - pub HTTP_VALUE = [percent_encoding::SIMPLE_ENCODE_SET] | { - ' ', '"', '%', '\'', '(', ')', '*', ',', '/', ':', ';', '<', '-', '>', '?', - '[', '\\', ']', '{', '}' - } - } -} - -#[cfg(test)] -mod tests { - use super::{parse_extended_value, ExtendedValue}; - use header::shared::Charset; - use language_tags::LanguageTag; - - #[test] - fn test_parse_extended_value_with_encoding_and_language_tag() { - let expected_language_tag = "en".parse::().unwrap(); - // RFC 5987, Section 3.2.2 - // Extended notation, using the Unicode character U+00A3 (POUND SIGN) - let result = parse_extended_value("iso-8859-1'en'%A3%20rates"); - assert!(result.is_ok()); - let extended_value = result.unwrap(); - assert_eq!(Charset::Iso_8859_1, extended_value.charset); - assert!(extended_value.language_tag.is_some()); - assert_eq!(expected_language_tag, extended_value.language_tag.unwrap()); - assert_eq!( - vec![163, b' ', b'r', b'a', b't', b'e', b's'], - extended_value.value - ); - } - - #[test] - fn test_parse_extended_value_with_encoding() { - // RFC 5987, Section 3.2.2 - // Extended notation, using the Unicode characters U+00A3 (POUND SIGN) - // and U+20AC (EURO SIGN) - let result = parse_extended_value("UTF-8''%c2%a3%20and%20%e2%82%ac%20rates"); - assert!(result.is_ok()); - let extended_value = result.unwrap(); - assert_eq!(Charset::Ext("UTF-8".to_string()), extended_value.charset); - assert!(extended_value.language_tag.is_none()); - assert_eq!( - vec![ - 194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', - b't', b'e', b's', - ], - extended_value.value - ); - } - - #[test] - fn test_parse_extended_value_missing_language_tag_and_encoding() { - // From: https://greenbytes.de/tech/tc2231/#attwithfn2231quot2 - let result = parse_extended_value("foo%20bar.html"); - assert!(result.is_err()); - } - - #[test] - fn test_parse_extended_value_partially_formatted() { - let result = parse_extended_value("UTF-8'missing third part"); - assert!(result.is_err()); - } - - #[test] - fn test_parse_extended_value_partially_formatted_blank() { - let result = parse_extended_value("blank second part'"); - assert!(result.is_err()); - } - - #[test] - fn test_fmt_extended_value_with_encoding_and_language_tag() { - let extended_value = ExtendedValue { - charset: Charset::Iso_8859_1, - language_tag: Some("en".parse().expect("Could not parse language tag")), - value: vec![163, b' ', b'r', b'a', b't', b'e', b's'], - }; - assert_eq!("ISO-8859-1'en'%A3%20rates", format!("{}", extended_value)); - } - - #[test] - fn test_fmt_extended_value_with_encoding() { - let extended_value = ExtendedValue { - charset: Charset::Ext("UTF-8".to_string()), - language_tag: None, - value: vec![ - 194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', - b't', b'e', b's', - ], - }; - assert_eq!( - "UTF-8''%C2%A3%20and%20%E2%82%AC%20rates", - format!("{}", extended_value) - ); - } -} diff --git a/src/header/shared/charset.rs b/src/header/shared/charset.rs deleted file mode 100644 index b679971b0..000000000 --- a/src/header/shared/charset.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::fmt::{self, Display}; -use std::str::FromStr; - -use self::Charset::*; - -/// A Mime charset. -/// -/// The string representation is normalized to upper case. -/// -/// See [http://www.iana.org/assignments/character-sets/character-sets.xhtml][url]. -/// -/// [url]: http://www.iana.org/assignments/character-sets/character-sets.xhtml -#[derive(Clone, Debug, PartialEq)] -#[allow(non_camel_case_types)] -pub enum Charset { - /// US ASCII - Us_Ascii, - /// ISO-8859-1 - Iso_8859_1, - /// ISO-8859-2 - Iso_8859_2, - /// ISO-8859-3 - Iso_8859_3, - /// ISO-8859-4 - Iso_8859_4, - /// ISO-8859-5 - Iso_8859_5, - /// ISO-8859-6 - Iso_8859_6, - /// ISO-8859-7 - Iso_8859_7, - /// ISO-8859-8 - Iso_8859_8, - /// ISO-8859-9 - Iso_8859_9, - /// ISO-8859-10 - Iso_8859_10, - /// Shift_JIS - Shift_Jis, - /// EUC-JP - Euc_Jp, - /// ISO-2022-KR - Iso_2022_Kr, - /// EUC-KR - Euc_Kr, - /// ISO-2022-JP - Iso_2022_Jp, - /// ISO-2022-JP-2 - Iso_2022_Jp_2, - /// ISO-8859-6-E - Iso_8859_6_E, - /// ISO-8859-6-I - Iso_8859_6_I, - /// ISO-8859-8-E - Iso_8859_8_E, - /// ISO-8859-8-I - Iso_8859_8_I, - /// GB2312 - Gb2312, - /// Big5 - Big5, - /// KOI8-R - Koi8_R, - /// An arbitrary charset specified as a string - Ext(String), -} - -impl Charset { - fn label(&self) -> &str { - match *self { - Us_Ascii => "US-ASCII", - Iso_8859_1 => "ISO-8859-1", - Iso_8859_2 => "ISO-8859-2", - Iso_8859_3 => "ISO-8859-3", - Iso_8859_4 => "ISO-8859-4", - Iso_8859_5 => "ISO-8859-5", - Iso_8859_6 => "ISO-8859-6", - Iso_8859_7 => "ISO-8859-7", - Iso_8859_8 => "ISO-8859-8", - Iso_8859_9 => "ISO-8859-9", - Iso_8859_10 => "ISO-8859-10", - Shift_Jis => "Shift-JIS", - Euc_Jp => "EUC-JP", - Iso_2022_Kr => "ISO-2022-KR", - Euc_Kr => "EUC-KR", - Iso_2022_Jp => "ISO-2022-JP", - Iso_2022_Jp_2 => "ISO-2022-JP-2", - Iso_8859_6_E => "ISO-8859-6-E", - Iso_8859_6_I => "ISO-8859-6-I", - Iso_8859_8_E => "ISO-8859-8-E", - Iso_8859_8_I => "ISO-8859-8-I", - Gb2312 => "GB2312", - Big5 => "big5", - Koi8_R => "KOI8-R", - Ext(ref s) => s, - } - } -} - -impl Display for Charset { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(self.label()) - } -} - -impl FromStr for Charset { - type Err = ::Error; - fn from_str(s: &str) -> ::Result { - Ok(match s.to_ascii_uppercase().as_ref() { - "US-ASCII" => Us_Ascii, - "ISO-8859-1" => Iso_8859_1, - "ISO-8859-2" => Iso_8859_2, - "ISO-8859-3" => Iso_8859_3, - "ISO-8859-4" => Iso_8859_4, - "ISO-8859-5" => Iso_8859_5, - "ISO-8859-6" => Iso_8859_6, - "ISO-8859-7" => Iso_8859_7, - "ISO-8859-8" => Iso_8859_8, - "ISO-8859-9" => Iso_8859_9, - "ISO-8859-10" => Iso_8859_10, - "SHIFT-JIS" => Shift_Jis, - "EUC-JP" => Euc_Jp, - "ISO-2022-KR" => Iso_2022_Kr, - "EUC-KR" => Euc_Kr, - "ISO-2022-JP" => Iso_2022_Jp, - "ISO-2022-JP-2" => Iso_2022_Jp_2, - "ISO-8859-6-E" => Iso_8859_6_E, - "ISO-8859-6-I" => Iso_8859_6_I, - "ISO-8859-8-E" => Iso_8859_8_E, - "ISO-8859-8-I" => Iso_8859_8_I, - "GB2312" => Gb2312, - "big5" => Big5, - "KOI8-R" => Koi8_R, - s => Ext(s.to_owned()), - }) - } -} - -#[test] -fn test_parse() { - assert_eq!(Us_Ascii, "us-ascii".parse().unwrap()); - assert_eq!(Us_Ascii, "US-Ascii".parse().unwrap()); - assert_eq!(Us_Ascii, "US-ASCII".parse().unwrap()); - assert_eq!(Shift_Jis, "Shift-JIS".parse().unwrap()); - assert_eq!(Ext("ABCD".to_owned()), "abcd".parse().unwrap()); -} - -#[test] -fn test_display() { - assert_eq!("US-ASCII", format!("{}", Us_Ascii)); - assert_eq!("ABCD", format!("{}", Ext("ABCD".to_owned()))); -} diff --git a/src/header/shared/encoding.rs b/src/header/shared/encoding.rs deleted file mode 100644 index 64027d8a5..000000000 --- a/src/header/shared/encoding.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::fmt; -use std::str; - -pub use self::Encoding::{ - Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, -}; - -/// A value to represent an encoding used in `Transfer-Encoding` -/// or `Accept-Encoding` header. -#[derive(Clone, PartialEq, Debug)] -pub enum Encoding { - /// The `chunked` encoding. - Chunked, - /// The `br` encoding. - Brotli, - /// The `gzip` encoding. - Gzip, - /// The `deflate` encoding. - Deflate, - /// The `compress` encoding. - Compress, - /// The `identity` encoding. - Identity, - /// The `trailers` encoding. - Trailers, - /// Some other encoding that is less common, can be any String. - EncodingExt(String), -} - -impl fmt::Display for Encoding { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(match *self { - Chunked => "chunked", - Brotli => "br", - Gzip => "gzip", - Deflate => "deflate", - Compress => "compress", - Identity => "identity", - Trailers => "trailers", - EncodingExt(ref s) => s.as_ref(), - }) - } -} - -impl str::FromStr for Encoding { - type Err = ::error::ParseError; - fn from_str(s: &str) -> Result { - match s { - "chunked" => Ok(Chunked), - "br" => Ok(Brotli), - "deflate" => Ok(Deflate), - "gzip" => Ok(Gzip), - "compress" => Ok(Compress), - "identity" => Ok(Identity), - "trailers" => Ok(Trailers), - _ => Ok(EncodingExt(s.to_owned())), - } - } -} diff --git a/src/header/shared/entity.rs b/src/header/shared/entity.rs deleted file mode 100644 index 0d3b0a4ef..000000000 --- a/src/header/shared/entity.rs +++ /dev/null @@ -1,266 +0,0 @@ -use header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, Writer}; -use std::fmt::{self, Display, Write}; -use std::str::FromStr; - -/// check that each char in the slice is either: -/// 1. `%x21`, or -/// 2. in the range `%x23` to `%x7E`, or -/// 3. above `%x80` -fn check_slice_validity(slice: &str) -> bool { - slice - .bytes() - .all(|c| c == b'\x21' || (c >= b'\x23' && c <= b'\x7e') | (c >= b'\x80')) -} - -/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3) -/// -/// An entity tag consists of a string enclosed by two literal double quotes. -/// Preceding the first double quote is an optional weakness indicator, -/// which always looks like `W/`. Examples for valid tags are `"xyzzy"` and -/// `W/"xyzzy"`. -/// -/// # ABNF -/// -/// ```text -/// entity-tag = [ weak ] opaque-tag -/// weak = %x57.2F ; "W/", case-sensitive -/// opaque-tag = DQUOTE *etagc DQUOTE -/// etagc = %x21 / %x23-7E / obs-text -/// ; VCHAR except double quotes, plus obs-text -/// ``` -/// -/// # Comparison -/// To check if two entity tags are equivalent in an application always use the -/// `strong_eq` or `weak_eq` methods based on the context of the Tag. Only use -/// `==` to check if two tags are identical. -/// -/// The example below shows the results for a set of entity-tag pairs and -/// both the weak and strong comparison function results: -/// -/// | `ETag 1`| `ETag 2`| Strong Comparison | Weak Comparison | -/// |---------|---------|-------------------|-----------------| -/// | `W/"1"` | `W/"1"` | no match | match | -/// | `W/"1"` | `W/"2"` | no match | no match | -/// | `W/"1"` | `"1"` | no match | match | -/// | `"1"` | `"1"` | match | match | -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct EntityTag { - /// Weakness indicator for the tag - pub weak: bool, - /// The opaque string in between the DQUOTEs - tag: String, -} - -impl EntityTag { - /// Constructs a new EntityTag. - /// # Panics - /// If the tag contains invalid characters. - pub fn new(weak: bool, tag: String) -> EntityTag { - assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); - EntityTag { weak, tag } - } - - /// Constructs a new weak EntityTag. - /// # Panics - /// If the tag contains invalid characters. - pub fn weak(tag: String) -> EntityTag { - EntityTag::new(true, tag) - } - - /// Constructs a new strong EntityTag. - /// # Panics - /// If the tag contains invalid characters. - pub fn strong(tag: String) -> EntityTag { - EntityTag::new(false, tag) - } - - /// Get the tag. - pub fn tag(&self) -> &str { - self.tag.as_ref() - } - - /// Set the tag. - /// # Panics - /// If the tag contains invalid characters. - pub fn set_tag(&mut self, tag: String) { - assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); - self.tag = tag - } - - /// For strong comparison two entity-tags are equivalent if both are not - /// weak and their opaque-tags match character-by-character. - pub fn strong_eq(&self, other: &EntityTag) -> bool { - !self.weak && !other.weak && self.tag == other.tag - } - - /// For weak comparison two entity-tags are equivalent if their - /// opaque-tags match character-by-character, regardless of either or - /// both being tagged as "weak". - pub fn weak_eq(&self, other: &EntityTag) -> bool { - self.tag == other.tag - } - - /// The inverse of `EntityTag.strong_eq()`. - pub fn strong_ne(&self, other: &EntityTag) -> bool { - !self.strong_eq(other) - } - - /// The inverse of `EntityTag.weak_eq()`. - pub fn weak_ne(&self, other: &EntityTag) -> bool { - !self.weak_eq(other) - } -} - -impl Display for EntityTag { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.weak { - write!(f, "W/\"{}\"", self.tag) - } else { - write!(f, "\"{}\"", self.tag) - } - } -} - -impl FromStr for EntityTag { - type Err = ::error::ParseError; - - fn from_str(s: &str) -> Result { - let length: usize = s.len(); - let slice = &s[..]; - // Early exits if it doesn't terminate in a DQUOTE. - if !slice.ends_with('"') || slice.len() < 2 { - return Err(::error::ParseError::Header); - } - // The etag is weak if its first char is not a DQUOTE. - if slice.len() >= 2 - && slice.starts_with('"') - && check_slice_validity(&slice[1..length - 1]) - { - // No need to check if the last char is a DQUOTE, - // we already did that above. - return Ok(EntityTag { - weak: false, - tag: slice[1..length - 1].to_owned(), - }); - } else if slice.len() >= 4 - && slice.starts_with("W/\"") - && check_slice_validity(&slice[3..length - 1]) - { - return Ok(EntityTag { - weak: true, - tag: slice[3..length - 1].to_owned(), - }); - } - Err(::error::ParseError::Header) - } -} - -impl IntoHeaderValue for EntityTag { - type Error = InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut wrt = Writer::new(); - write!(wrt, "{}", self).unwrap(); - HeaderValue::from_shared(wrt.take()) - } -} - -#[cfg(test)] -mod tests { - use super::EntityTag; - - #[test] - fn test_etag_parse_success() { - // Expected success - assert_eq!( - "\"foobar\"".parse::().unwrap(), - EntityTag::strong("foobar".to_owned()) - ); - assert_eq!( - "\"\"".parse::().unwrap(), - EntityTag::strong("".to_owned()) - ); - assert_eq!( - "W/\"weaktag\"".parse::().unwrap(), - EntityTag::weak("weaktag".to_owned()) - ); - assert_eq!( - "W/\"\x65\x62\"".parse::().unwrap(), - EntityTag::weak("\x65\x62".to_owned()) - ); - assert_eq!( - "W/\"\"".parse::().unwrap(), - EntityTag::weak("".to_owned()) - ); - } - - #[test] - fn test_etag_parse_failures() { - // Expected failures - assert!("no-dquotes".parse::().is_err()); - assert!( - "w/\"the-first-w-is-case-sensitive\"" - .parse::() - .is_err() - ); - assert!("".parse::().is_err()); - assert!("\"unmatched-dquotes1".parse::().is_err()); - assert!("unmatched-dquotes2\"".parse::().is_err()); - assert!("matched-\"dquotes\"".parse::().is_err()); - } - - #[test] - fn test_etag_fmt() { - assert_eq!( - format!("{}", EntityTag::strong("foobar".to_owned())), - "\"foobar\"" - ); - assert_eq!(format!("{}", EntityTag::strong("".to_owned())), "\"\""); - assert_eq!( - format!("{}", EntityTag::weak("weak-etag".to_owned())), - "W/\"weak-etag\"" - ); - assert_eq!( - format!("{}", EntityTag::weak("\u{0065}".to_owned())), - "W/\"\x65\"" - ); - assert_eq!(format!("{}", EntityTag::weak("".to_owned())), "W/\"\""); - } - - #[test] - fn test_cmp() { - // | ETag 1 | ETag 2 | Strong Comparison | Weak Comparison | - // |---------|---------|-------------------|-----------------| - // | `W/"1"` | `W/"1"` | no match | match | - // | `W/"1"` | `W/"2"` | no match | no match | - // | `W/"1"` | `"1"` | no match | match | - // | `"1"` | `"1"` | match | match | - let mut etag1 = EntityTag::weak("1".to_owned()); - let mut etag2 = EntityTag::weak("1".to_owned()); - assert!(!etag1.strong_eq(&etag2)); - assert!(etag1.weak_eq(&etag2)); - assert!(etag1.strong_ne(&etag2)); - assert!(!etag1.weak_ne(&etag2)); - - etag1 = EntityTag::weak("1".to_owned()); - etag2 = EntityTag::weak("2".to_owned()); - assert!(!etag1.strong_eq(&etag2)); - assert!(!etag1.weak_eq(&etag2)); - assert!(etag1.strong_ne(&etag2)); - assert!(etag1.weak_ne(&etag2)); - - etag1 = EntityTag::weak("1".to_owned()); - etag2 = EntityTag::strong("1".to_owned()); - assert!(!etag1.strong_eq(&etag2)); - assert!(etag1.weak_eq(&etag2)); - assert!(etag1.strong_ne(&etag2)); - assert!(!etag1.weak_ne(&etag2)); - - etag1 = EntityTag::strong("1".to_owned()); - etag2 = EntityTag::strong("1".to_owned()); - assert!(etag1.strong_eq(&etag2)); - assert!(etag1.weak_eq(&etag2)); - assert!(!etag1.strong_ne(&etag2)); - assert!(!etag1.weak_ne(&etag2)); - } -} diff --git a/src/header/shared/httpdate.rs b/src/header/shared/httpdate.rs deleted file mode 100644 index 7fd26b121..000000000 --- a/src/header/shared/httpdate.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::fmt::{self, Display}; -use std::io::Write; -use std::str::FromStr; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use bytes::{BufMut, BytesMut}; -use http::header::{HeaderValue, InvalidHeaderValueBytes}; -use time; - -use error::ParseError; -use header::IntoHeaderValue; - -/// A timestamp with HTTP formatting and parsing -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct HttpDate(time::Tm); - -impl FromStr for HttpDate { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - match time::strptime(s, "%a, %d %b %Y %T %Z") - .or_else(|_| time::strptime(s, "%A, %d-%b-%y %T %Z")) - .or_else(|_| time::strptime(s, "%c")) - { - Ok(t) => Ok(HttpDate(t)), - Err(_) => Err(ParseError::Header), - } - } -} - -impl Display for HttpDate { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0.to_utc().rfc822(), f) - } -} - -impl From for HttpDate { - fn from(tm: time::Tm) -> HttpDate { - HttpDate(tm) - } -} - -impl From for HttpDate { - fn from(sys: SystemTime) -> HttpDate { - let tmspec = match sys.duration_since(UNIX_EPOCH) { - Ok(dur) => { - time::Timespec::new(dur.as_secs() as i64, dur.subsec_nanos() as i32) - } - Err(err) => { - let neg = err.duration(); - time::Timespec::new( - -(neg.as_secs() as i64), - -(neg.subsec_nanos() as i32), - ) - } - }; - HttpDate(time::at_utc(tmspec)) - } -} - -impl IntoHeaderValue for HttpDate { - type Error = InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut wrt = BytesMut::with_capacity(29).writer(); - write!(wrt, "{}", self.0.rfc822()).unwrap(); - HeaderValue::from_shared(wrt.get_mut().take().freeze()) - } -} - -impl From for SystemTime { - fn from(date: HttpDate) -> SystemTime { - let spec = date.0.to_timespec(); - if spec.sec >= 0 { - UNIX_EPOCH + Duration::new(spec.sec as u64, spec.nsec as u32) - } else { - UNIX_EPOCH - Duration::new(spec.sec as u64, spec.nsec as u32) - } - } -} - -#[cfg(test)] -mod tests { - use super::HttpDate; - use time::Tm; - - const NOV_07: HttpDate = HttpDate(Tm { - tm_nsec: 0, - tm_sec: 37, - tm_min: 48, - tm_hour: 8, - tm_mday: 7, - tm_mon: 10, - tm_year: 94, - tm_wday: 0, - tm_isdst: 0, - tm_yday: 0, - tm_utcoff: 0, - }); - - #[test] - fn test_date() { - assert_eq!( - "Sun, 07 Nov 1994 08:48:37 GMT".parse::().unwrap(), - NOV_07 - ); - assert_eq!( - "Sunday, 07-Nov-94 08:48:37 GMT" - .parse::() - .unwrap(), - NOV_07 - ); - assert_eq!( - "Sun Nov 7 08:48:37 1994".parse::().unwrap(), - NOV_07 - ); - assert!("this-is-no-date".parse::().is_err()); - } -} diff --git a/src/header/shared/mod.rs b/src/header/shared/mod.rs deleted file mode 100644 index f2bc91634..000000000 --- a/src/header/shared/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Copied for `hyper::header::shared`; - -pub use self::charset::Charset; -pub use self::encoding::Encoding; -pub use self::entity::EntityTag; -pub use self::httpdate::HttpDate; -pub use self::quality_item::{q, qitem, Quality, QualityItem}; -pub use language_tags::LanguageTag; - -mod charset; -mod encoding; -mod entity; -mod httpdate; -mod quality_item; diff --git a/src/header/shared/quality_item.rs b/src/header/shared/quality_item.rs deleted file mode 100644 index 80bd7e1c2..000000000 --- a/src/header/shared/quality_item.rs +++ /dev/null @@ -1,294 +0,0 @@ -use std::cmp; -use std::default::Default; -use std::fmt; -use std::str; - -use self::internal::IntoQuality; - -/// Represents a quality used in quality values. -/// -/// Can be created with the `q` function. -/// -/// # Implementation notes -/// -/// The quality value is defined as a number between 0 and 1 with three decimal -/// places. This means there are 1001 possible values. Since floating point -/// numbers are not exact and the smallest floating point data type (`f32`) -/// consumes four bytes, hyper uses an `u16` value to store the -/// quality internally. For performance reasons you may set quality directly to -/// a value between 0 and 1000 e.g. `Quality(532)` matches the quality -/// `q=0.532`. -/// -/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1) -/// gives more information on quality values in HTTP header fields. -#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct Quality(u16); - -impl Default for Quality { - fn default() -> Quality { - Quality(1000) - } -} - -/// Represents an item with a quality value as defined in -/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.1). -#[derive(Clone, PartialEq, Debug)] -pub struct QualityItem { - /// The actual contents of the field. - pub item: T, - /// The quality (client or server preference) for the value. - pub quality: Quality, -} - -impl QualityItem { - /// Creates a new `QualityItem` from an item and a quality. - /// The item can be of any type. - /// The quality should be a value in the range [0, 1]. - pub fn new(item: T, quality: Quality) -> QualityItem { - QualityItem { item, quality } - } -} - -impl cmp::PartialOrd for QualityItem { - fn partial_cmp(&self, other: &QualityItem) -> Option { - self.quality.partial_cmp(&other.quality) - } -} - -impl fmt::Display for QualityItem { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.item, f)?; - match self.quality.0 { - 1000 => Ok(()), - 0 => f.write_str("; q=0"), - x => write!(f, "; q=0.{}", format!("{:03}", x).trim_right_matches('0')), - } - } -} - -impl str::FromStr for QualityItem { - type Err = ::error::ParseError; - - fn from_str(s: &str) -> Result, ::error::ParseError> { - if !s.is_ascii() { - return Err(::error::ParseError::Header); - } - // Set defaults used if parsing fails. - let mut raw_item = s; - let mut quality = 1f32; - - let parts: Vec<&str> = s.rsplitn(2, ';').map(|x| x.trim()).collect(); - if parts.len() == 2 { - if parts[0].len() < 2 { - return Err(::error::ParseError::Header); - } - let start = &parts[0][0..2]; - if start == "q=" || start == "Q=" { - let q_part = &parts[0][2..parts[0].len()]; - if q_part.len() > 5 { - return Err(::error::ParseError::Header); - } - match q_part.parse::() { - Ok(q_value) => { - if 0f32 <= q_value && q_value <= 1f32 { - quality = q_value; - raw_item = parts[1]; - } else { - return Err(::error::ParseError::Header); - } - } - Err(_) => return Err(::error::ParseError::Header), - } - } - } - match raw_item.parse::() { - // we already checked above that the quality is within range - Ok(item) => Ok(QualityItem::new(item, from_f32(quality))), - Err(_) => Err(::error::ParseError::Header), - } - } -} - -#[inline] -fn from_f32(f: f32) -> Quality { - // this function is only used internally. A check that `f` is within range - // should be done before calling this method. Just in case, this - // debug_assert should catch if we were forgetful - debug_assert!( - f >= 0f32 && f <= 1f32, - "q value must be between 0.0 and 1.0" - ); - Quality((f * 1000f32) as u16) -} - -/// Convenience function to wrap a value in a `QualityItem` -/// Sets `q` to the default 1.0 -pub fn qitem(item: T) -> QualityItem { - QualityItem::new(item, Default::default()) -} - -/// Convenience function to create a `Quality` from a float or integer. -/// -/// Implemented for `u16` and `f32`. Panics if value is out of range. -pub fn q(val: T) -> Quality { - val.into_quality() -} - -mod internal { - use super::Quality; - - // TryFrom is probably better, but it's not stable. For now, we want to - // keep the functionality of the `q` function, while allowing it to be - // generic over `f32` and `u16`. - // - // `q` would panic before, so keep that behavior. `TryFrom` can be - // introduced later for a non-panicking conversion. - - pub trait IntoQuality: Sealed + Sized { - fn into_quality(self) -> Quality; - } - - impl IntoQuality for f32 { - fn into_quality(self) -> Quality { - assert!( - self >= 0f32 && self <= 1f32, - "float must be between 0.0 and 1.0" - ); - super::from_f32(self) - } - } - - impl IntoQuality for u16 { - fn into_quality(self) -> Quality { - assert!(self <= 1000, "u16 must be between 0 and 1000"); - Quality(self) - } - } - - pub trait Sealed {} - impl Sealed for u16 {} - impl Sealed for f32 {} -} - -#[cfg(test)] -mod tests { - use super::super::encoding::*; - use super::*; - - #[test] - fn test_quality_item_fmt_q_1() { - let x = qitem(Chunked); - assert_eq!(format!("{}", x), "chunked"); - } - #[test] - fn test_quality_item_fmt_q_0001() { - let x = QualityItem::new(Chunked, Quality(1)); - assert_eq!(format!("{}", x), "chunked; q=0.001"); - } - #[test] - fn test_quality_item_fmt_q_05() { - // Custom value - let x = QualityItem { - item: EncodingExt("identity".to_owned()), - quality: Quality(500), - }; - assert_eq!(format!("{}", x), "identity; q=0.5"); - } - - #[test] - fn test_quality_item_fmt_q_0() { - // Custom value - let x = QualityItem { - item: EncodingExt("identity".to_owned()), - quality: Quality(0), - }; - assert_eq!(x.to_string(), "identity; q=0"); - } - - #[test] - fn test_quality_item_from_str1() { - let x: Result, _> = "chunked".parse(); - assert_eq!( - x.unwrap(), - QualityItem { - item: Chunked, - quality: Quality(1000), - } - ); - } - #[test] - fn test_quality_item_from_str2() { - let x: Result, _> = "chunked; q=1".parse(); - assert_eq!( - x.unwrap(), - QualityItem { - item: Chunked, - quality: Quality(1000), - } - ); - } - #[test] - fn test_quality_item_from_str3() { - let x: Result, _> = "gzip; q=0.5".parse(); - assert_eq!( - x.unwrap(), - QualityItem { - item: Gzip, - quality: Quality(500), - } - ); - } - #[test] - fn test_quality_item_from_str4() { - let x: Result, _> = "gzip; q=0.273".parse(); - assert_eq!( - x.unwrap(), - QualityItem { - item: Gzip, - quality: Quality(273), - } - ); - } - #[test] - fn test_quality_item_from_str5() { - let x: Result, _> = "gzip; q=0.2739999".parse(); - assert!(x.is_err()); - } - #[test] - fn test_quality_item_from_str6() { - let x: Result, _> = "gzip; q=2".parse(); - assert!(x.is_err()); - } - #[test] - fn test_quality_item_ordering() { - let x: QualityItem = "gzip; q=0.5".parse().ok().unwrap(); - let y: QualityItem = "gzip; q=0.273".parse().ok().unwrap(); - let comparision_result: bool = x.gt(&y); - assert!(comparision_result) - } - - #[test] - fn test_quality() { - assert_eq!(q(0.5), Quality(500)); - } - - #[test] - #[should_panic] // FIXME - 32-bit msvc unwinding broken - #[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)] - fn test_quality_invalid() { - q(-1.0); - } - - #[test] - #[should_panic] // FIXME - 32-bit msvc unwinding broken - #[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)] - fn test_quality_invalid2() { - q(2.0); - } - - #[test] - fn test_fuzzing_bugs() { - assert!("99999;".parse::>().is_err()); - assert!("\x0d;;;=\u{d6aa}==".parse::>().is_err()) - } -} diff --git a/src/helpers.rs b/src/helpers.rs index e82d61616..860a02a4d 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,571 +1,180 @@ -//! Various helpers +use actix_http::Response; +use actix_service::{NewService, Service}; +use futures::future::{ok, FutureResult}; +use futures::{Future, Poll}; -use http::{header, StatusCode}; -use regex::Regex; +pub(crate) type BoxedHttpService = Box< + Service< + Request = Req, + Response = Res, + Error = (), + Future = Box>, + >, +>; -use handler::Handler; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; +pub(crate) type BoxedHttpNewService = Box< + NewService< + Request = Req, + Response = Res, + Error = (), + InitError = (), + Service = BoxedHttpService, + Future = Box, Error = ()>>, + >, +>; -/// Path normalization helper -/// -/// By normalizing it means: -/// -/// - Add a trailing slash to the path. -/// - Remove a trailing slash from the path. -/// - Double slashes are replaced by one. -/// -/// The handler returns as soon as it finds a path that resolves -/// correctly. The order if all enable is 1) merge, 3) both merge and append -/// and 3) append. If the path resolves with -/// at least one of those conditions, it will redirect to the new path. -/// -/// If *append* is *true* append slash when needed. If a resource is -/// defined with trailing slash and the request comes without it, it will -/// append it automatically. -/// -/// If *merge* is *true*, merge multiple consecutive slashes in the path into -/// one. -/// -/// This handler designed to be use as a handler for application's *default -/// resource*. -/// -/// ```rust -/// # extern crate actix_web; -/// # #[macro_use] extern crate serde_derive; -/// # use actix_web::*; -/// use actix_web::http::NormalizePath; -/// -/// # fn index(req: &HttpRequest) -> HttpResponse { -/// # HttpResponse::Ok().into() -/// # } -/// fn main() { -/// let app = App::new() -/// .resource("/test/", |r| r.f(index)) -/// .default_resource(|r| r.h(NormalizePath::default())) -/// .finish(); -/// } -/// ``` -/// In this example `/test`, `/test///` will be redirected to `/test/` url. -pub struct NormalizePath { - append: bool, - merge: bool, - re_merge: Regex, - redirect: StatusCode, - not_found: StatusCode, -} +pub(crate) struct HttpNewService(T); -impl Default for NormalizePath { - /// Create default `NormalizePath` instance, *append* is set to *true*, - /// *merge* is set to *true* and *redirect* is set to - /// `StatusCode::MOVED_PERMANENTLY` - fn default() -> NormalizePath { - NormalizePath { - append: true, - merge: true, - re_merge: Regex::new("//+").unwrap(), - redirect: StatusCode::MOVED_PERMANENTLY, - not_found: StatusCode::NOT_FOUND, - } +impl HttpNewService +where + T: NewService, + T::Response: 'static, + T::Future: 'static, + T::Service: Service, + ::Future: 'static, +{ + pub fn new(service: T) -> Self { + HttpNewService(service) } } -impl NormalizePath { - /// Create new `NormalizePath` instance - pub fn new(append: bool, merge: bool, redirect: StatusCode) -> NormalizePath { - NormalizePath { - append, - merge, - redirect, - re_merge: Regex::new("//+").unwrap(), - not_found: StatusCode::NOT_FOUND, - } +impl NewService for HttpNewService +where + T: NewService, + T::Request: 'static, + T::Response: 'static, + T::Future: 'static, + T::Service: Service + 'static, + ::Future: 'static, +{ + type Request = T::Request; + type Response = T::Response; + type Error = (); + type InitError = (); + type Service = BoxedHttpService; + type Future = Box>; + + fn new_service(&self, _: &()) -> Self::Future { + Box::new(self.0.new_service(&()).map_err(|_| ()).and_then(|service| { + let service: BoxedHttpService<_, _> = + Box::new(HttpServiceWrapper { service }); + Ok(service) + })) } } -impl Handler for NormalizePath { - type Result = HttpResponse; +struct HttpServiceWrapper { + service: T, +} - fn handle(&self, req: &HttpRequest) -> Self::Result { - let query = req.query_string(); - if self.merge { - // merge slashes - let p = self.re_merge.replace_all(req.path(), "/"); - if p.len() != req.path().len() { - if req.resource().has_prefixed_resource(p.as_ref()) { - let p = if !query.is_empty() { - p + "?" + query - } else { - p - }; - return HttpResponse::build(self.redirect) - .header(header::LOCATION, p.as_ref()) - .finish(); - } - // merge slashes and append trailing slash - if self.append && !p.ends_with('/') { - let p = p.as_ref().to_owned() + "/"; - if req.resource().has_prefixed_resource(&p) { - let p = if !query.is_empty() { - p + "?" + query - } else { - p - }; - return HttpResponse::build(self.redirect) - .header(header::LOCATION, p.as_str()) - .finish(); - } - } +impl Service for HttpServiceWrapper +where + T: Service, + T::Request: 'static, + T::Response: 'static, + T::Future: 'static, +{ + type Request = T::Request; + type Response = T::Response; + type Error = (); + type Future = Box>; - // try to remove trailing slash - if p.ends_with('/') { - let p = p.as_ref().trim_right_matches('/'); - if req.resource().has_prefixed_resource(p) { - let mut req = HttpResponse::build(self.redirect); - return if !query.is_empty() { - req.header( - header::LOCATION, - (p.to_owned() + "?" + query).as_str(), - ) - } else { - req.header(header::LOCATION, p) - }.finish(); - } - } - } else if p.ends_with('/') { - // try to remove trailing slash - let p = p.as_ref().trim_right_matches('/'); - if req.resource().has_prefixed_resource(p) { - let mut req = HttpResponse::build(self.redirect); - return if !query.is_empty() { - req.header( - header::LOCATION, - (p.to_owned() + "?" + query).as_str(), - ) - } else { - req.header(header::LOCATION, p) - }.finish(); - } - } - } - // append trailing slash - if self.append && !req.path().ends_with('/') { - let p = req.path().to_owned() + "/"; - if req.resource().has_prefixed_resource(&p) { - let p = if !query.is_empty() { - p + "?" + query - } else { - p - }; - return HttpResponse::build(self.redirect) - .header(header::LOCATION, p.as_str()) - .finish(); - } - } - HttpResponse::new(self.not_found) + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready().map_err(|_| ()) + } + + fn call(&mut self, req: Self::Request) -> Self::Future { + Box::new(self.service.call(req).map_err(|_| ())) } } -#[cfg(test)] -mod tests { - use super::*; - use application::App; - use http::{header, Method}; - use test::TestRequest; +pub(crate) fn not_found(_: Req) -> FutureResult { + ok(Response::NotFound().finish()) +} - fn index(_req: &HttpRequest) -> HttpResponse { - HttpResponse::new(StatusCode::OK) - } +pub(crate) type HttpDefaultService = Box< + Service< + Request = Req, + Response = Res, + Error = (), + Future = Box>, + >, +>; - #[test] - fn test_normalize_path_trailing_slashes() { - let app = App::new() - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource2/", |r| r.method(Method::GET).f(index)) - .default_resource(|r| r.h(NormalizePath::default())) - .finish(); +pub(crate) type HttpDefaultNewService = Box< + NewService< + Request = Req, + Response = Res, + Error = (), + InitError = (), + Service = HttpDefaultService, + Future = Box, Error = ()>>, + >, +>; - // trailing slashes - let params = vec![ - ("/resource1", "", StatusCode::OK), - ("/resource1/", "/resource1", StatusCode::MOVED_PERMANENTLY), - ("/resource2", "/resource2/", StatusCode::MOVED_PERMANENTLY), - ("/resource2/", "", StatusCode::OK), - ("/resource1?p1=1&p2=2", "", StatusCode::OK), - ( - "/resource1/?p1=1&p2=2", - "/resource1?p1=1&p2=2", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/resource2?p1=1&p2=2", - "/resource2/?p1=1&p2=2", - StatusCode::MOVED_PERMANENTLY, - ), - ("/resource2/?p1=1&p2=2", "", StatusCode::OK), - ]; - for (path, target, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - if !target.is_empty() { - assert_eq!( - target, - r.headers().get(header::LOCATION).unwrap().to_str().unwrap() - ); - } - } - } +pub(crate) struct DefaultNewService { + service: T, +} - #[test] - fn test_prefixed_normalize_path_trailing_slashes() { - let app = App::new() - .prefix("/test") - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource2/", |r| r.method(Method::GET).f(index)) - .default_resource(|r| r.h(NormalizePath::default())) - .finish(); - - // trailing slashes - let params = vec![ - ("/test/resource1", "", StatusCode::OK), - ( - "/test/resource1/", - "/test/resource1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/test/resource2", - "/test/resource2/", - StatusCode::MOVED_PERMANENTLY, - ), - ("/test/resource2/", "", StatusCode::OK), - ("/test/resource1?p1=1&p2=2", "", StatusCode::OK), - ( - "/test/resource1/?p1=1&p2=2", - "/test/resource1?p1=1&p2=2", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/test/resource2?p1=1&p2=2", - "/test/resource2/?p1=1&p2=2", - StatusCode::MOVED_PERMANENTLY, - ), - ("/test/resource2/?p1=1&p2=2", "", StatusCode::OK), - ]; - for (path, target, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - if !target.is_empty() { - assert_eq!( - target, - r.headers().get(header::LOCATION).unwrap().to_str().unwrap() - ); - } - } - } - - #[test] - fn test_normalize_path_trailing_slashes_disabled() { - let app = App::new() - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource2/", |r| r.method(Method::GET).f(index)) - .default_resource(|r| { - r.h(NormalizePath::new( - false, - true, - StatusCode::MOVED_PERMANENTLY, - )) - }).finish(); - - // trailing slashes - let params = vec![ - ("/resource1", StatusCode::OK), - ("/resource1/", StatusCode::MOVED_PERMANENTLY), - ("/resource2", StatusCode::NOT_FOUND), - ("/resource2/", StatusCode::OK), - ("/resource1?p1=1&p2=2", StatusCode::OK), - ("/resource1/?p1=1&p2=2", StatusCode::MOVED_PERMANENTLY), - ("/resource2?p1=1&p2=2", StatusCode::NOT_FOUND), - ("/resource2/?p1=1&p2=2", StatusCode::OK), - ]; - for (path, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - } - } - - #[test] - fn test_normalize_path_merge_slashes() { - let app = App::new() - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource1/a/b", |r| r.method(Method::GET).f(index)) - .default_resource(|r| r.h(NormalizePath::default())) - .finish(); - - // trailing slashes - let params = vec![ - ("/resource1/a/b", "", StatusCode::OK), - ("/resource1/", "/resource1", StatusCode::MOVED_PERMANENTLY), - ("/resource1//", "/resource1", StatusCode::MOVED_PERMANENTLY), - ( - "//resource1//a//b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource1//a//b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource1//a//b//", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a//b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ("/resource1/a/b?p=1", "", StatusCode::OK), - ( - "//resource1//a//b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource1//a//b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a//b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a//b//?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ]; - for (path, target, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - if !target.is_empty() { - assert_eq!( - target, - r.headers().get(header::LOCATION).unwrap().to_str().unwrap() - ); - } - } - } - - #[test] - fn test_normalize_path_merge_and_append_slashes() { - let app = App::new() - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource2/", |r| r.method(Method::GET).f(index)) - .resource("/resource1/a/b", |r| r.method(Method::GET).f(index)) - .resource("/resource2/a/b/", |r| r.method(Method::GET).f(index)) - .default_resource(|r| r.h(NormalizePath::default())) - .finish(); - - // trailing slashes - let params = vec![ - ("/resource1/a/b", "", StatusCode::OK), - ( - "/resource1/a/b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b/", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b//", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/resource2/a/b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ("/resource2/a/b/", "", StatusCode::OK), - ( - "//resource2//a//b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b/", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource2//a//b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource2//a//b/", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource2/a///b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource2/a///b/", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ("/resource1/a/b?p=1", "", StatusCode::OK), - ( - "/resource1/a/b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b/?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b//?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/resource2/a/b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b/?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource2//a//b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource2//a//b/?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource2/a///b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource2/a///b/?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ]; - for (path, target, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - if !target.is_empty() { - assert_eq!( - target, - r.headers().get(header::LOCATION).unwrap().to_str().unwrap() - ); - } - } +impl DefaultNewService +where + T: NewService + 'static, + T::Future: 'static, + ::Future: 'static, +{ + pub fn new(service: T) -> Self { + DefaultNewService { service } + } +} + +impl NewService for DefaultNewService +where + T: NewService + 'static, + T::Request: 'static, + T::Future: 'static, + T::Service: 'static, + ::Future: 'static, +{ + type Request = T::Request; + type Response = T::Response; + type Error = (); + type InitError = (); + type Service = HttpDefaultService; + type Future = Box>; + + fn new_service(&self, _: &()) -> Self::Future { + Box::new( + self.service + .new_service(&()) + .map_err(|_| ()) + .and_then(|service| { + let service: HttpDefaultService<_, _> = + Box::new(DefaultServiceWrapper { service }); + Ok(service) + }), + ) + } +} + +struct DefaultServiceWrapper { + service: T, +} + +impl Service for DefaultServiceWrapper +where + T: Service + 'static, + T::Future: 'static, +{ + type Request = T::Request; + type Response = T::Response; + type Error = (); + type Future = Box>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready().map_err(|_| ()) + } + + fn call(&mut self, req: T::Request) -> Self::Future { + Box::new(self.service.call(req).map_err(|_| ())) } } diff --git a/src/httpcodes.rs b/src/httpcodes.rs deleted file mode 100644 index 41e57d1ee..000000000 --- a/src/httpcodes.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! Basic http responses -#![allow(non_upper_case_globals)] -use http::StatusCode; -use httpresponse::{HttpResponse, HttpResponseBuilder}; - -macro_rules! STATIC_RESP { - ($name:ident, $status:expr) => { - #[allow(non_snake_case, missing_docs)] - pub fn $name() -> HttpResponseBuilder { - HttpResponse::build($status) - } - }; -} - -impl HttpResponse { - STATIC_RESP!(Ok, StatusCode::OK); - STATIC_RESP!(Created, StatusCode::CREATED); - STATIC_RESP!(Accepted, StatusCode::ACCEPTED); - STATIC_RESP!( - NonAuthoritativeInformation, - StatusCode::NON_AUTHORITATIVE_INFORMATION - ); - - STATIC_RESP!(NoContent, StatusCode::NO_CONTENT); - STATIC_RESP!(ResetContent, StatusCode::RESET_CONTENT); - STATIC_RESP!(PartialContent, StatusCode::PARTIAL_CONTENT); - STATIC_RESP!(MultiStatus, StatusCode::MULTI_STATUS); - STATIC_RESP!(AlreadyReported, StatusCode::ALREADY_REPORTED); - - STATIC_RESP!(MultipleChoices, StatusCode::MULTIPLE_CHOICES); - STATIC_RESP!(MovedPermanenty, StatusCode::MOVED_PERMANENTLY); - STATIC_RESP!(MovedPermanently, StatusCode::MOVED_PERMANENTLY); - STATIC_RESP!(Found, StatusCode::FOUND); - STATIC_RESP!(SeeOther, StatusCode::SEE_OTHER); - STATIC_RESP!(NotModified, StatusCode::NOT_MODIFIED); - STATIC_RESP!(UseProxy, StatusCode::USE_PROXY); - STATIC_RESP!(TemporaryRedirect, StatusCode::TEMPORARY_REDIRECT); - STATIC_RESP!(PermanentRedirect, StatusCode::PERMANENT_REDIRECT); - - STATIC_RESP!(BadRequest, StatusCode::BAD_REQUEST); - STATIC_RESP!(NotFound, StatusCode::NOT_FOUND); - STATIC_RESP!(Unauthorized, StatusCode::UNAUTHORIZED); - STATIC_RESP!(PaymentRequired, StatusCode::PAYMENT_REQUIRED); - STATIC_RESP!(Forbidden, StatusCode::FORBIDDEN); - STATIC_RESP!(MethodNotAllowed, StatusCode::METHOD_NOT_ALLOWED); - STATIC_RESP!(NotAcceptable, StatusCode::NOT_ACCEPTABLE); - STATIC_RESP!( - ProxyAuthenticationRequired, - StatusCode::PROXY_AUTHENTICATION_REQUIRED - ); - STATIC_RESP!(RequestTimeout, StatusCode::REQUEST_TIMEOUT); - STATIC_RESP!(Conflict, StatusCode::CONFLICT); - STATIC_RESP!(Gone, StatusCode::GONE); - STATIC_RESP!(LengthRequired, StatusCode::LENGTH_REQUIRED); - STATIC_RESP!(PreconditionFailed, StatusCode::PRECONDITION_FAILED); - STATIC_RESP!(PayloadTooLarge, StatusCode::PAYLOAD_TOO_LARGE); - STATIC_RESP!(UriTooLong, StatusCode::URI_TOO_LONG); - STATIC_RESP!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE); - STATIC_RESP!(RangeNotSatisfiable, StatusCode::RANGE_NOT_SATISFIABLE); - STATIC_RESP!(ExpectationFailed, StatusCode::EXPECTATION_FAILED); - - STATIC_RESP!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR); - STATIC_RESP!(NotImplemented, StatusCode::NOT_IMPLEMENTED); - STATIC_RESP!(BadGateway, StatusCode::BAD_GATEWAY); - STATIC_RESP!(ServiceUnavailable, StatusCode::SERVICE_UNAVAILABLE); - STATIC_RESP!(GatewayTimeout, StatusCode::GATEWAY_TIMEOUT); - STATIC_RESP!(VersionNotSupported, StatusCode::HTTP_VERSION_NOT_SUPPORTED); - STATIC_RESP!(VariantAlsoNegotiates, StatusCode::VARIANT_ALSO_NEGOTIATES); - STATIC_RESP!(InsufficientStorage, StatusCode::INSUFFICIENT_STORAGE); - STATIC_RESP!(LoopDetected, StatusCode::LOOP_DETECTED); -} - -#[cfg(test)] -mod tests { - use body::Body; - use http::StatusCode; - use httpresponse::HttpResponse; - - #[test] - fn test_build() { - let resp = HttpResponse::Ok().body(Body::Empty); - assert_eq!(resp.status(), StatusCode::OK); - } -} diff --git a/src/httpmessage.rs b/src/httpmessage.rs deleted file mode 100644 index ea5e4d862..000000000 --- a/src/httpmessage.rs +++ /dev/null @@ -1,855 +0,0 @@ -use bytes::{Bytes, BytesMut}; -use encoding::all::UTF_8; -use encoding::label::encoding_from_whatwg_label; -use encoding::types::{DecoderTrap, Encoding}; -use encoding::EncodingRef; -use futures::{Async, Future, Poll, Stream}; -use http::{header, HeaderMap}; -use mime::Mime; -use serde::de::DeserializeOwned; -use serde_urlencoded; -use std::str; - -use error::{ - ContentTypeError, ParseError, PayloadError, ReadlinesError, UrlencodedError, -}; -use header::Header; -use json::JsonBody; -use multipart::Multipart; - -/// Trait that implements general purpose operations on http messages -pub trait HttpMessage: Sized { - /// Type of message payload stream - type Stream: Stream + Sized; - - /// Read the message headers. - fn headers(&self) -> &HeaderMap; - - /// Message payload stream - fn payload(&self) -> Self::Stream; - - #[doc(hidden)] - /// Get a header - fn get_header(&self) -> Option - where - Self: Sized, - { - if self.headers().contains_key(H::name()) { - H::parse(self).ok() - } else { - None - } - } - - /// Read the request content type. If request does not contain - /// *Content-Type* header, empty str get returned. - fn content_type(&self) -> &str { - if let Some(content_type) = self.headers().get(header::CONTENT_TYPE) { - if let Ok(content_type) = content_type.to_str() { - return content_type.split(';').next().unwrap().trim(); - } - } - "" - } - - /// Get content type encoding - /// - /// UTF-8 is used by default, If request charset is not set. - fn encoding(&self) -> Result { - if let Some(mime_type) = self.mime_type()? { - if let Some(charset) = mime_type.get_param("charset") { - if let Some(enc) = encoding_from_whatwg_label(charset.as_str()) { - Ok(enc) - } else { - Err(ContentTypeError::UnknownEncoding) - } - } else { - Ok(UTF_8) - } - } else { - Ok(UTF_8) - } - } - - /// Convert the request content type to a known mime type. - fn mime_type(&self) -> Result, ContentTypeError> { - if let Some(content_type) = self.headers().get(header::CONTENT_TYPE) { - if let Ok(content_type) = content_type.to_str() { - return match content_type.parse() { - Ok(mt) => Ok(Some(mt)), - Err(_) => Err(ContentTypeError::ParseError), - }; - } else { - return Err(ContentTypeError::ParseError); - } - } - Ok(None) - } - - /// Check if request has chunked transfer encoding - fn chunked(&self) -> Result { - if let Some(encodings) = self.headers().get(header::TRANSFER_ENCODING) { - if let Ok(s) = encodings.to_str() { - Ok(s.to_lowercase().contains("chunked")) - } else { - Err(ParseError::Header) - } - } else { - Ok(false) - } - } - - /// Load http message body. - /// - /// By default only 256Kb payload reads to a memory, then - /// `PayloadError::Overflow` get returned. Use `MessageBody::limit()` - /// method to change upper limit. - /// - /// ## Server example - /// - /// ```rust - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; - /// # #[macro_use] extern crate serde_derive; - /// use actix_web::{ - /// AsyncResponder, FutureResponse, HttpMessage, HttpRequest, HttpResponse, - /// }; - /// use bytes::Bytes; - /// use futures::future::Future; - /// - /// fn index(mut req: HttpRequest) -> FutureResponse { - /// req.body() // <- get Body future - /// .limit(1024) // <- change max size of the body to a 1kb - /// .from_err() - /// .and_then(|bytes: Bytes| { // <- complete body - /// println!("==== BODY ==== {:?}", bytes); - /// Ok(HttpResponse::Ok().into()) - /// }).responder() - /// } - /// # fn main() {} - /// ``` - fn body(&self) -> MessageBody { - MessageBody::new(self) - } - - /// Parse `application/x-www-form-urlencoded` encoded request's body. - /// Return `UrlEncoded` future. Form can be deserialized to any type that - /// implements `Deserialize` trait from *serde*. - /// - /// Returns error: - /// - /// * content type is not `application/x-www-form-urlencoded` - /// * content-length is greater than 256k - /// - /// ## Server example - /// - /// ```rust - /// # extern crate actix_web; - /// # extern crate futures; - /// # use futures::Future; - /// # use std::collections::HashMap; - /// use actix_web::{FutureResponse, HttpMessage, HttpRequest, HttpResponse}; - /// - /// fn index(mut req: HttpRequest) -> FutureResponse { - /// Box::new( - /// req.urlencoded::>() // <- get UrlEncoded future - /// .from_err() - /// .and_then(|params| { // <- url encoded parameters - /// println!("==== BODY ==== {:?}", params); - /// Ok(HttpResponse::Ok().into()) - /// }), - /// ) - /// } - /// # fn main() {} - /// ``` - fn urlencoded(&self) -> UrlEncoded { - UrlEncoded::new(self) - } - - /// Parse `application/json` encoded body. - /// Return `JsonBody` future. It resolves to a `T` value. - /// - /// Returns error: - /// - /// * content type is not `application/json` - /// * content length is greater than 256k - /// - /// ## Server example - /// - /// ```rust - /// # extern crate actix_web; - /// # extern crate futures; - /// # #[macro_use] extern crate serde_derive; - /// use actix_web::*; - /// use futures::future::{ok, Future}; - /// - /// #[derive(Deserialize, Debug)] - /// struct MyObj { - /// name: String, - /// } - /// - /// fn index(mut req: HttpRequest) -> Box> { - /// req.json() // <- get JsonBody future - /// .from_err() - /// .and_then(|val: MyObj| { // <- deserialized value - /// println!("==== BODY ==== {:?}", val); - /// Ok(HttpResponse::Ok().into()) - /// }).responder() - /// } - /// # fn main() {} - /// ``` - fn json(&self) -> JsonBody { - JsonBody::new::<()>(self, None) - } - - /// Return stream to http payload processes as multipart. - /// - /// Content-type: multipart/form-data; - /// - /// ## Server example - /// - /// ```rust - /// # extern crate actix_web; - /// # extern crate env_logger; - /// # extern crate futures; - /// # extern crate actix; - /// # use std::str; - /// # use actix_web::*; - /// # use actix::FinishStream; - /// # use futures::{Future, Stream}; - /// # use futures::future::{ok, result, Either}; - /// fn index(mut req: HttpRequest) -> Box> { - /// req.multipart().from_err() // <- get multipart stream for current request - /// .and_then(|item| match item { // <- iterate over multipart items - /// multipart::MultipartItem::Field(field) => { - /// // Field in turn is stream of *Bytes* object - /// Either::A(field.from_err() - /// .map(|c| println!("-- CHUNK: \n{:?}", str::from_utf8(&c))) - /// .finish()) - /// }, - /// multipart::MultipartItem::Nested(mp) => { - /// // Or item could be nested Multipart stream - /// Either::B(ok(())) - /// } - /// }) - /// .finish() // <- Stream::finish() combinator from actix - /// .map(|_| HttpResponse::Ok().into()) - /// .responder() - /// } - /// # fn main() {} - /// ``` - fn multipart(&self) -> Multipart { - let boundary = Multipart::boundary(self.headers()); - Multipart::new(boundary, self.payload()) - } - - /// Return stream of lines. - fn readlines(&self) -> Readlines { - Readlines::new(self) - } -} - -/// Stream to read request line by line. -pub struct Readlines { - stream: T::Stream, - buff: BytesMut, - limit: usize, - checked_buff: bool, - encoding: EncodingRef, - err: Option, -} - -impl Readlines { - /// Create a new stream to read request line by line. - fn new(req: &T) -> Self { - let encoding = match req.encoding() { - Ok(enc) => enc, - Err(err) => return Self::err(req, err.into()), - }; - - Readlines { - stream: req.payload(), - buff: BytesMut::with_capacity(262_144), - limit: 262_144, - checked_buff: true, - err: None, - encoding, - } - } - - /// Change max line size. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - fn err(req: &T, err: ReadlinesError) -> Self { - Readlines { - stream: req.payload(), - buff: BytesMut::new(), - limit: 262_144, - checked_buff: true, - encoding: UTF_8, - err: Some(err), - } - } -} - -impl Stream for Readlines { - type Item = String; - type Error = ReadlinesError; - - fn poll(&mut self) -> Poll, Self::Error> { - if let Some(err) = self.err.take() { - return Err(err); - } - - // check if there is a newline in the buffer - if !self.checked_buff { - let mut found: Option = None; - for (ind, b) in self.buff.iter().enumerate() { - if *b == b'\n' { - found = Some(ind); - break; - } - } - if let Some(ind) = found { - // check if line is longer than limit - if ind + 1 > self.limit { - return Err(ReadlinesError::LimitOverflow); - } - let enc: *const Encoding = self.encoding as *const Encoding; - let line = if enc == UTF_8 { - str::from_utf8(&self.buff.split_to(ind + 1)) - .map_err(|_| ReadlinesError::EncodingError)? - .to_owned() - } else { - self.encoding - .decode(&self.buff.split_to(ind + 1), DecoderTrap::Strict) - .map_err(|_| ReadlinesError::EncodingError)? - }; - return Ok(Async::Ready(Some(line))); - } - self.checked_buff = true; - } - // poll req for more bytes - match self.stream.poll() { - Ok(Async::Ready(Some(mut bytes))) => { - // check if there is a newline in bytes - let mut found: Option = None; - for (ind, b) in bytes.iter().enumerate() { - if *b == b'\n' { - found = Some(ind); - break; - } - } - if let Some(ind) = found { - // check if line is longer than limit - if ind + 1 > self.limit { - return Err(ReadlinesError::LimitOverflow); - } - let enc: *const Encoding = self.encoding as *const Encoding; - let line = if enc == UTF_8 { - str::from_utf8(&bytes.split_to(ind + 1)) - .map_err(|_| ReadlinesError::EncodingError)? - .to_owned() - } else { - self.encoding - .decode(&bytes.split_to(ind + 1), DecoderTrap::Strict) - .map_err(|_| ReadlinesError::EncodingError)? - }; - // extend buffer with rest of the bytes; - self.buff.extend_from_slice(&bytes); - self.checked_buff = false; - return Ok(Async::Ready(Some(line))); - } - self.buff.extend_from_slice(&bytes); - Ok(Async::NotReady) - } - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(None)) => { - if self.buff.is_empty() { - return Ok(Async::Ready(None)); - } - if self.buff.len() > self.limit { - return Err(ReadlinesError::LimitOverflow); - } - let enc: *const Encoding = self.encoding as *const Encoding; - let line = if enc == UTF_8 { - str::from_utf8(&self.buff) - .map_err(|_| ReadlinesError::EncodingError)? - .to_owned() - } else { - self.encoding - .decode(&self.buff, DecoderTrap::Strict) - .map_err(|_| ReadlinesError::EncodingError)? - }; - self.buff.clear(); - Ok(Async::Ready(Some(line))) - } - Err(e) => Err(ReadlinesError::from(e)), - } - } -} - -/// Future that resolves to a complete http message body. -pub struct MessageBody { - limit: usize, - length: Option, - stream: Option, - err: Option, - fut: Option>>, -} - -impl MessageBody { - /// Create `MessageBody` for request. - pub fn new(req: &T) -> MessageBody { - let mut len = None; - if let Some(l) = req.headers().get(header::CONTENT_LENGTH) { - if let Ok(s) = l.to_str() { - if let Ok(l) = s.parse::() { - len = Some(l) - } else { - return Self::err(PayloadError::UnknownLength); - } - } else { - return Self::err(PayloadError::UnknownLength); - } - } - - MessageBody { - limit: 262_144, - length: len, - stream: Some(req.payload()), - fut: None, - err: None, - } - } - - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - fn err(e: PayloadError) -> Self { - MessageBody { - stream: None, - limit: 262_144, - fut: None, - err: Some(e), - length: None, - } - } -} - -impl Future for MessageBody -where - T: HttpMessage + 'static, -{ - type Item = Bytes; - type Error = PayloadError; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut { - return fut.poll(); - } - - if let Some(err) = self.err.take() { - return Err(err); - } - - if let Some(len) = self.length.take() { - if len > self.limit { - return Err(PayloadError::Overflow); - } - } - - // future - let limit = self.limit; - self.fut = Some(Box::new( - self.stream - .take() - .expect("Can not be used second time") - .from_err() - .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { - if (body.len() + chunk.len()) > limit { - Err(PayloadError::Overflow) - } else { - body.extend_from_slice(&chunk); - Ok(body) - } - }).map(|body| body.freeze()), - )); - self.poll() - } -} - -/// Future that resolves to a parsed urlencoded values. -pub struct UrlEncoded { - stream: Option, - limit: usize, - length: Option, - encoding: EncodingRef, - err: Option, - fut: Option>>, -} - -impl UrlEncoded { - /// Create a new future to URL encode a request - pub fn new(req: &T) -> UrlEncoded { - // check content type - if req.content_type().to_lowercase() != "application/x-www-form-urlencoded" { - return Self::err(UrlencodedError::ContentType); - } - let encoding = match req.encoding() { - Ok(enc) => enc, - Err(_) => return Self::err(UrlencodedError::ContentType), - }; - - let mut len = None; - if let Some(l) = req.headers().get(header::CONTENT_LENGTH) { - if let Ok(s) = l.to_str() { - if let Ok(l) = s.parse::() { - len = Some(l) - } else { - return Self::err(UrlencodedError::UnknownLength); - } - } else { - return Self::err(UrlencodedError::UnknownLength); - } - }; - - UrlEncoded { - encoding, - stream: Some(req.payload()), - limit: 262_144, - length: len, - fut: None, - err: None, - } - } - - fn err(e: UrlencodedError) -> Self { - UrlEncoded { - stream: None, - limit: 262_144, - fut: None, - err: Some(e), - length: None, - encoding: UTF_8, - } - } - - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } -} - -impl Future for UrlEncoded -where - T: HttpMessage + 'static, - U: DeserializeOwned + 'static, -{ - type Item = U; - type Error = UrlencodedError; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut { - return fut.poll(); - } - - if let Some(err) = self.err.take() { - return Err(err); - } - - // payload size - let limit = self.limit; - if let Some(len) = self.length.take() { - if len > limit { - return Err(UrlencodedError::Overflow); - } - } - - // future - let encoding = self.encoding; - let fut = self - .stream - .take() - .expect("UrlEncoded could not be used second time") - .from_err() - .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { - if (body.len() + chunk.len()) > limit { - Err(UrlencodedError::Overflow) - } else { - body.extend_from_slice(&chunk); - Ok(body) - } - }).and_then(move |body| { - if (encoding as *const Encoding) == UTF_8 { - serde_urlencoded::from_bytes::(&body) - .map_err(|_| UrlencodedError::Parse) - } else { - let body = encoding - .decode(&body, DecoderTrap::Strict) - .map_err(|_| UrlencodedError::Parse)?; - serde_urlencoded::from_str::(&body) - .map_err(|_| UrlencodedError::Parse) - } - }); - self.fut = Some(Box::new(fut)); - self.poll() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use encoding::all::ISO_8859_2; - use encoding::Encoding; - use futures::Async; - use mime; - use test::TestRequest; - - #[test] - fn test_content_type() { - let req = TestRequest::with_header("content-type", "text/plain").finish(); - assert_eq!(req.content_type(), "text/plain"); - let req = - TestRequest::with_header("content-type", "application/json; charset=utf=8") - .finish(); - assert_eq!(req.content_type(), "application/json"); - let req = TestRequest::default().finish(); - assert_eq!(req.content_type(), ""); - } - - #[test] - fn test_mime_type() { - let req = TestRequest::with_header("content-type", "application/json").finish(); - assert_eq!(req.mime_type().unwrap(), Some(mime::APPLICATION_JSON)); - let req = TestRequest::default().finish(); - assert_eq!(req.mime_type().unwrap(), None); - let req = - TestRequest::with_header("content-type", "application/json; charset=utf-8") - .finish(); - let mt = req.mime_type().unwrap().unwrap(); - assert_eq!(mt.get_param(mime::CHARSET), Some(mime::UTF_8)); - assert_eq!(mt.type_(), mime::APPLICATION); - assert_eq!(mt.subtype(), mime::JSON); - } - - #[test] - fn test_mime_type_error() { - let req = TestRequest::with_header( - "content-type", - "applicationadfadsfasdflknadsfklnadsfjson", - ).finish(); - assert_eq!(Err(ContentTypeError::ParseError), req.mime_type()); - } - - #[test] - fn test_encoding() { - let req = TestRequest::default().finish(); - assert_eq!(UTF_8.name(), req.encoding().unwrap().name()); - - let req = TestRequest::with_header("content-type", "application/json").finish(); - assert_eq!(UTF_8.name(), req.encoding().unwrap().name()); - - let req = TestRequest::with_header( - "content-type", - "application/json; charset=ISO-8859-2", - ).finish(); - assert_eq!(ISO_8859_2.name(), req.encoding().unwrap().name()); - } - - #[test] - fn test_encoding_error() { - let req = TestRequest::with_header("content-type", "applicatjson").finish(); - assert_eq!(Some(ContentTypeError::ParseError), req.encoding().err()); - - let req = TestRequest::with_header( - "content-type", - "application/json; charset=kkkttktk", - ).finish(); - assert_eq!( - Some(ContentTypeError::UnknownEncoding), - req.encoding().err() - ); - } - - #[test] - fn test_chunked() { - let req = TestRequest::default().finish(); - assert!(!req.chunked().unwrap()); - - let req = - TestRequest::with_header(header::TRANSFER_ENCODING, "chunked").finish(); - assert!(req.chunked().unwrap()); - - let req = TestRequest::default() - .header( - header::TRANSFER_ENCODING, - Bytes::from_static(b"some va\xadscc\xacas0xsdasdlue"), - ).finish(); - assert!(req.chunked().is_err()); - } - - impl PartialEq for UrlencodedError { - fn eq(&self, other: &UrlencodedError) -> bool { - match *self { - UrlencodedError::Chunked => match *other { - UrlencodedError::Chunked => true, - _ => false, - }, - UrlencodedError::Overflow => match *other { - UrlencodedError::Overflow => true, - _ => false, - }, - UrlencodedError::UnknownLength => match *other { - UrlencodedError::UnknownLength => true, - _ => false, - }, - UrlencodedError::ContentType => match *other { - UrlencodedError::ContentType => true, - _ => false, - }, - _ => false, - } - } - } - - #[derive(Deserialize, Debug, PartialEq)] - struct Info { - hello: String, - } - - #[test] - fn test_urlencoded_error() { - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "xxxx") - .finish(); - assert_eq!( - req.urlencoded::().poll().err().unwrap(), - UrlencodedError::UnknownLength - ); - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "1000000") - .finish(); - assert_eq!( - req.urlencoded::().poll().err().unwrap(), - UrlencodedError::Overflow - ); - - let req = TestRequest::with_header(header::CONTENT_TYPE, "text/plain") - .header(header::CONTENT_LENGTH, "10") - .finish(); - assert_eq!( - req.urlencoded::().poll().err().unwrap(), - UrlencodedError::ContentType - ); - } - - #[test] - fn test_urlencoded() { - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); - - let result = req.urlencoded::().poll().ok().unwrap(); - assert_eq!( - result, - Async::Ready(Info { - hello: "world".to_owned() - }) - ); - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded; charset=utf-8", - ).header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); - - let result = req.urlencoded().poll().ok().unwrap(); - assert_eq!( - result, - Async::Ready(Info { - hello: "world".to_owned() - }) - ); - } - - #[test] - fn test_message_body() { - let req = TestRequest::with_header(header::CONTENT_LENGTH, "xxxx").finish(); - match req.body().poll().err().unwrap() { - PayloadError::UnknownLength => (), - _ => unreachable!("error"), - } - - let req = TestRequest::with_header(header::CONTENT_LENGTH, "1000000").finish(); - match req.body().poll().err().unwrap() { - PayloadError::Overflow => (), - _ => unreachable!("error"), - } - - let req = TestRequest::default() - .set_payload(Bytes::from_static(b"test")) - .finish(); - match req.body().poll().ok().unwrap() { - Async::Ready(bytes) => assert_eq!(bytes, Bytes::from_static(b"test")), - _ => unreachable!("error"), - } - - let req = TestRequest::default() - .set_payload(Bytes::from_static(b"11111111111111")) - .finish(); - match req.body().limit(5).poll().err().unwrap() { - PayloadError::Overflow => (), - _ => unreachable!("error"), - } - } - - #[test] - fn test_readlines() { - let req = TestRequest::default() - .set_payload(Bytes::from_static( - b"Lorem Ipsum is simply dummy text of the printing and typesetting\n\ - industry. Lorem Ipsum has been the industry's standard dummy\n\ - Contrary to popular belief, Lorem Ipsum is not simply random text.", - )).finish(); - let mut r = Readlines::new(&req); - match r.poll().ok().unwrap() { - Async::Ready(Some(s)) => assert_eq!( - s, - "Lorem Ipsum is simply dummy text of the printing and typesetting\n" - ), - _ => unreachable!("error"), - } - match r.poll().ok().unwrap() { - Async::Ready(Some(s)) => assert_eq!( - s, - "industry. Lorem Ipsum has been the industry's standard dummy\n" - ), - _ => unreachable!("error"), - } - match r.poll().ok().unwrap() { - Async::Ready(Some(s)) => assert_eq!( - s, - "Contrary to popular belief, Lorem Ipsum is not simply random text." - ), - _ => unreachable!("error"), - } - } -} diff --git a/src/httprequest.rs b/src/httprequest.rs deleted file mode 100644 index 0e4f74e5e..000000000 --- a/src/httprequest.rs +++ /dev/null @@ -1,545 +0,0 @@ -//! HTTP Request message related code. -use std::cell::{Ref, RefMut}; -use std::collections::HashMap; -use std::net::SocketAddr; -use std::ops::Deref; -use std::rc::Rc; -use std::{fmt, str}; - -use cookie::Cookie; -use futures_cpupool::CpuPool; -use http::{header, HeaderMap, Method, StatusCode, Uri, Version}; -use url::{form_urlencoded, Url}; - -use body::Body; -use error::{CookieParseError, UrlGenerationError}; -use extensions::Extensions; -use handler::FromRequest; -use httpmessage::HttpMessage; -use httpresponse::{HttpResponse, HttpResponseBuilder}; -use info::ConnectionInfo; -use param::Params; -use payload::Payload; -use router::ResourceInfo; -use server::Request; - -struct Query(HashMap); -struct Cookies(Vec>); - -/// An HTTP Request -pub struct HttpRequest { - req: Option, - state: Rc, - resource: ResourceInfo, -} - -impl HttpMessage for HttpRequest { - type Stream = Payload; - - #[inline] - fn headers(&self) -> &HeaderMap { - self.request().headers() - } - - #[inline] - fn payload(&self) -> Payload { - if let Some(payload) = self.request().inner.payload.borrow_mut().take() { - payload - } else { - Payload::empty() - } - } -} - -impl Deref for HttpRequest { - type Target = Request; - - fn deref(&self) -> &Request { - self.request() - } -} - -impl HttpRequest { - #[inline] - pub(crate) fn new( - req: Request, state: Rc, resource: ResourceInfo, - ) -> HttpRequest { - HttpRequest { - state, - resource, - req: Some(req), - } - } - - #[inline] - /// Construct new http request with state. - pub(crate) fn with_state(&self, state: Rc) -> HttpRequest { - HttpRequest { - state, - req: self.req.as_ref().map(|r| r.clone()), - resource: self.resource.clone(), - } - } - - /// Construct new http request with empty state. - pub fn drop_state(&self) -> HttpRequest { - HttpRequest { - state: Rc::new(()), - req: self.req.as_ref().map(|r| r.clone()), - resource: self.resource.clone(), - } - } - - #[inline] - /// Construct new http request with new RouteInfo. - pub(crate) fn with_route_info(&self, mut resource: ResourceInfo) -> HttpRequest { - resource.merge(&self.resource); - - HttpRequest { - resource, - req: self.req.as_ref().map(|r| r.clone()), - state: self.state.clone(), - } - } - - /// Shared application state - #[inline] - pub fn state(&self) -> &S { - &self.state - } - - #[inline] - /// Server request - pub fn request(&self) -> &Request { - self.req.as_ref().unwrap() - } - - /// Request extensions - #[inline] - pub fn extensions(&self) -> Ref { - self.request().extensions() - } - - /// Mutable reference to a the request's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut { - self.request().extensions_mut() - } - - /// Default `CpuPool` - #[inline] - #[doc(hidden)] - pub fn cpu_pool(&self) -> &CpuPool { - self.request().server_settings().cpu_pool() - } - - #[inline] - /// Create http response - pub fn response(&self, status: StatusCode, body: Body) -> HttpResponse { - self.request().server_settings().get_response(status, body) - } - - #[inline] - /// Create http response builder - pub fn build_response(&self, status: StatusCode) -> HttpResponseBuilder { - self.request() - .server_settings() - .get_response_builder(status) - } - - /// Read the Request Uri. - #[inline] - pub fn uri(&self) -> &Uri { - self.request().inner.url.uri() - } - - /// Read the Request method. - #[inline] - pub fn method(&self) -> &Method { - &self.request().inner.method - } - - /// Read the Request Version. - #[inline] - pub fn version(&self) -> Version { - self.request().inner.version - } - - /// The target path of this Request. - #[inline] - pub fn path(&self) -> &str { - self.request().inner.url.path() - } - - /// Get *ConnectionInfo* for the correct request. - #[inline] - pub fn connection_info(&self) -> Ref { - self.request().connection_info() - } - - /// Generate url for named resource - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::{App, HttpRequest, HttpResponse, http}; - /// # - /// fn index(req: HttpRequest) -> HttpResponse { - /// let url = req.url_for("foo", &["1", "2", "3"]); // <- generate url for "foo" resource - /// HttpResponse::Ok().into() - /// } - /// - /// fn main() { - /// let app = App::new() - /// .resource("/test/{one}/{two}/{three}", |r| { - /// r.name("foo"); // <- set resource name, then it could be used in `url_for` - /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); - /// }) - /// .finish(); - /// } - /// ``` - pub fn url_for( - &self, name: &str, elements: U, - ) -> Result - where - U: IntoIterator, - I: AsRef, - { - self.resource.url_for(&self, name, elements) - } - - /// Generate url for named resource - /// - /// This method is similar to `HttpRequest::url_for()` but it can be used - /// for urls that do not contain variable parts. - pub fn url_for_static(&self, name: &str) -> Result { - const NO_PARAMS: [&str; 0] = []; - self.url_for(name, &NO_PARAMS) - } - - /// This method returns reference to current `ResourceInfo` object. - #[inline] - pub fn resource(&self) -> &ResourceInfo { - &self.resource - } - - /// Peer socket address - /// - /// Peer address is actual socket address, if proxy is used in front of - /// actix http server, then peer address would be address of this proxy. - /// - /// To get client connection information `connection_info()` method should - /// be used. - #[inline] - pub fn peer_addr(&self) -> Option { - self.request().inner.addr - } - - /// url query parameters. - pub fn query(&self) -> Ref> { - if self.extensions().get::().is_none() { - let mut query = HashMap::new(); - for (key, val) in form_urlencoded::parse(self.query_string().as_ref()) { - query.insert(key.as_ref().to_string(), val.to_string()); - } - self.extensions_mut().insert(Query(query)); - } - Ref::map(self.extensions(), |ext| &ext.get::().unwrap().0) - } - - /// The query string in the URL. - /// - /// E.g., id=10 - #[inline] - pub fn query_string(&self) -> &str { - if let Some(query) = self.uri().query().as_ref() { - query - } else { - "" - } - } - - /// Load request cookies. - #[inline] - pub fn cookies(&self) -> Result>>, CookieParseError> { - if self.extensions().get::().is_none() { - let mut cookies = Vec::new(); - for hdr in self.request().inner.headers.get_all(header::COOKIE) { - let s = - str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; - for cookie_str in s.split(';').map(|s| s.trim()) { - if !cookie_str.is_empty() { - cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned()); - } - } - } - self.extensions_mut().insert(Cookies(cookies)); - } - Ok(Ref::map(self.extensions(), |ext| { - &ext.get::().unwrap().0 - })) - } - - /// Return request cookie. - #[inline] - pub fn cookie(&self, name: &str) -> Option> { - if let Ok(cookies) = self.cookies() { - for cookie in cookies.iter() { - if cookie.name() == name { - return Some(cookie.to_owned()); - } - } - } - None - } - - pub(crate) fn set_cookies(&mut self, cookies: Option>>) { - if let Some(cookies) = cookies { - self.extensions_mut().insert(Cookies(cookies)); - } - } - - /// Get a reference to the Params object. - /// - /// Params is a container for url parameters. - /// A variable segment is specified in the form `{identifier}`, - /// where the identifier can be used later in a request handler to - /// access the matched value for that segment. - #[inline] - pub fn match_info(&self) -> &Params { - &self.resource.match_info() - } - - /// Check if request requires connection upgrade - pub(crate) fn upgrade(&self) -> bool { - self.request().upgrade() - } - - /// Set read buffer capacity - /// - /// Default buffer capacity is 32Kb. - pub fn set_read_buffer_capacity(&mut self, cap: usize) { - if let Some(payload) = self.request().inner.payload.borrow_mut().as_mut() { - payload.set_read_buffer_capacity(cap) - } - } -} - -impl Drop for HttpRequest { - fn drop(&mut self) { - if let Some(req) = self.req.take() { - req.release(); - } - } -} - -impl Clone for HttpRequest { - fn clone(&self) -> HttpRequest { - HttpRequest { - req: self.req.as_ref().map(|r| r.clone()), - state: self.state.clone(), - resource: self.resource.clone(), - } - } -} - -impl FromRequest for HttpRequest { - type Config = (); - type Result = Self; - - #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { - req.clone() - } -} - -impl fmt::Debug for HttpRequest { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!( - f, - "\nHttpRequest {:?} {}:{}", - self.version(), - self.method(), - self.path() - )?; - if !self.query_string().is_empty() { - writeln!(f, " query: ?{:?}", self.query_string())?; - } - if !self.match_info().is_empty() { - writeln!(f, " params: {:?}", self.match_info())?; - } - writeln!(f, " headers:")?; - for (key, val) in self.headers().iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use resource::Resource; - use router::{ResourceDef, Router}; - use test::TestRequest; - - #[test] - fn test_debug() { - let req = TestRequest::with_header("content-type", "text/plain").finish(); - let dbg = format!("{:?}", req); - assert!(dbg.contains("HttpRequest")); - } - - #[test] - fn test_no_request_cookies() { - let req = TestRequest::default().finish(); - assert!(req.cookies().unwrap().is_empty()); - } - - #[test] - fn test_request_cookies() { - let req = TestRequest::default() - .header(header::COOKIE, "cookie1=value1") - .header(header::COOKIE, "cookie2=value2") - .finish(); - { - let cookies = req.cookies().unwrap(); - assert_eq!(cookies.len(), 2); - assert_eq!(cookies[0].name(), "cookie1"); - assert_eq!(cookies[0].value(), "value1"); - assert_eq!(cookies[1].name(), "cookie2"); - assert_eq!(cookies[1].value(), "value2"); - } - - let cookie = req.cookie("cookie1"); - assert!(cookie.is_some()); - let cookie = cookie.unwrap(); - assert_eq!(cookie.name(), "cookie1"); - assert_eq!(cookie.value(), "value1"); - - let cookie = req.cookie("cookie-unknown"); - assert!(cookie.is_none()); - } - - #[test] - fn test_request_query() { - let req = TestRequest::with_uri("/?id=test").finish(); - assert_eq!(req.query_string(), "id=test"); - let query = req.query(); - assert_eq!(&query["id"], "test"); - } - - #[test] - fn test_request_match_info() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{key}/"))); - - let req = TestRequest::with_uri("/value/?id=test").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.match_info().get("key"), Some("value")); - } - - #[test] - fn test_url_for() { - let mut router = Router::<()>::default(); - let mut resource = Resource::new(ResourceDef::new("/user/{name}.{ext}")); - resource.name("index"); - router.register_resource(resource); - - let info = router.default_route_info(); - assert!(!info.has_prefixed_resource("/use/")); - assert!(info.has_resource("/user/test.html")); - assert!(info.has_prefixed_resource("/user/test.html")); - assert!(!info.has_resource("/test/unknown")); - assert!(!info.has_prefixed_resource("/test/unknown")); - - let req = TestRequest::with_header(header::HOST, "www.rust-lang.org") - .finish_with_router(router); - - assert_eq!( - req.url_for("unknown", &["test"]), - Err(UrlGenerationError::ResourceNotFound) - ); - assert_eq!( - req.url_for("index", &["test"]), - Err(UrlGenerationError::NotEnoughElements) - ); - let url = req.url_for("index", &["test", "html"]); - assert_eq!( - url.ok().unwrap().as_str(), - "http://www.rust-lang.org/user/test.html" - ); - } - - #[test] - fn test_url_for_with_prefix() { - let mut resource = Resource::new(ResourceDef::new("/user/{name}.html")); - resource.name("index"); - let mut router = Router::<()>::default(); - router.set_prefix("/prefix"); - router.register_resource(resource); - - let mut info = router.default_route_info(); - info.set_prefix(7); - assert!(!info.has_prefixed_resource("/use/")); - assert!(info.has_resource("/user/test.html")); - assert!(!info.has_prefixed_resource("/user/test.html")); - assert!(!info.has_resource("/prefix/user/test.html")); - assert!(info.has_prefixed_resource("/prefix/user/test.html")); - - let req = TestRequest::with_uri("/prefix/test") - .prefix(7) - .header(header::HOST, "www.rust-lang.org") - .finish_with_router(router); - let url = req.url_for("index", &["test"]); - assert_eq!( - url.ok().unwrap().as_str(), - "http://www.rust-lang.org/prefix/user/test.html" - ); - } - - #[test] - fn test_url_for_static() { - let mut resource = Resource::new(ResourceDef::new("/index.html")); - resource.name("index"); - let mut router = Router::<()>::default(); - router.set_prefix("/prefix"); - router.register_resource(resource); - - let mut info = router.default_route_info(); - info.set_prefix(7); - assert!(info.has_resource("/index.html")); - assert!(!info.has_prefixed_resource("/index.html")); - assert!(!info.has_resource("/prefix/index.html")); - assert!(info.has_prefixed_resource("/prefix/index.html")); - - let req = TestRequest::with_uri("/prefix/test") - .prefix(7) - .header(header::HOST, "www.rust-lang.org") - .finish_with_router(router); - let url = req.url_for_static("index"); - assert_eq!( - url.ok().unwrap().as_str(), - "http://www.rust-lang.org/prefix/index.html" - ); - } - - #[test] - fn test_url_for_external() { - let mut router = Router::<()>::default(); - router.register_external( - "youtube", - ResourceDef::external("https://youtube.com/watch/{video_id}"), - ); - - let info = router.default_route_info(); - assert!(!info.has_resource("https://youtube.com/watch/unknown")); - assert!(!info.has_prefixed_resource("https://youtube.com/watch/unknown")); - - let req = TestRequest::default().finish_with_router(router); - let url = req.url_for("youtube", &["oHg5SJYRHA0"]); - assert_eq!( - url.ok().unwrap().as_str(), - "https://youtube.com/watch/oHg5SJYRHA0" - ); - } -} diff --git a/src/httpresponse.rs b/src/httpresponse.rs deleted file mode 100644 index 226c847f3..000000000 --- a/src/httpresponse.rs +++ /dev/null @@ -1,1458 +0,0 @@ -//! Http response -use std::cell::RefCell; -use std::collections::VecDeque; -use std::io::Write; -use std::{fmt, mem, str}; - -use bytes::{BufMut, Bytes, BytesMut}; -use cookie::{Cookie, CookieJar}; -use futures::Stream; -use http::header::{self, HeaderName, HeaderValue}; -use http::{Error as HttpError, HeaderMap, HttpTryFrom, StatusCode, Version}; -use serde::Serialize; -use serde_json; - -use body::Body; -use client::ClientResponse; -use error::Error; -use handler::Responder; -use header::{ContentEncoding, Header, IntoHeaderValue}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; - -/// max write buffer size 64k -pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; - -/// Represents various types of connection -#[derive(Copy, Clone, PartialEq, Debug)] -pub enum ConnectionType { - /// Close connection after response - Close, - /// Keep connection alive after response - KeepAlive, - /// Connection is upgraded to different type - Upgrade, -} - -/// An HTTP Response -pub struct HttpResponse(Box, &'static HttpResponsePool); - -impl HttpResponse { - #[inline] - fn get_ref(&self) -> &InnerHttpResponse { - self.0.as_ref() - } - - #[inline] - fn get_mut(&mut self) -> &mut InnerHttpResponse { - self.0.as_mut() - } - - /// Create http response builder with specific status. - #[inline] - pub fn build(status: StatusCode) -> HttpResponseBuilder { - HttpResponsePool::get(status) - } - - /// Create http response builder - #[inline] - pub fn build_from>(source: T) -> HttpResponseBuilder { - source.into() - } - - /// Constructs a response - #[inline] - pub fn new(status: StatusCode) -> HttpResponse { - HttpResponsePool::with_body(status, Body::Empty) - } - - /// Constructs a response with body - #[inline] - pub fn with_body>(status: StatusCode, body: B) -> HttpResponse { - HttpResponsePool::with_body(status, body.into()) - } - - /// Constructs an error response - #[inline] - pub fn from_error(error: Error) -> HttpResponse { - let mut resp = error.as_response_error().error_response(); - resp.get_mut().error = Some(error); - resp - } - - /// Convert `HttpResponse` to a `HttpResponseBuilder` - #[inline] - pub fn into_builder(self) -> HttpResponseBuilder { - // If this response has cookies, load them into a jar - let mut jar: Option = None; - for c in self.cookies() { - if let Some(ref mut j) = jar { - j.add_original(c.into_owned()); - } else { - let mut j = CookieJar::new(); - j.add_original(c.into_owned()); - jar = Some(j); - } - } - - HttpResponseBuilder { - pool: self.1, - response: Some(self.0), - err: None, - cookies: jar, - } - } - - /// The source `error` for this response - #[inline] - pub fn error(&self) -> Option<&Error> { - self.get_ref().error.as_ref() - } - - /// Get the HTTP version of this response - #[inline] - pub fn version(&self) -> Option { - self.get_ref().version - } - - /// Get the headers from the response - #[inline] - pub fn headers(&self) -> &HeaderMap { - &self.get_ref().headers - } - - /// Get a mutable reference to the headers - #[inline] - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.get_mut().headers - } - - /// Get an iterator for the cookies set by this response - #[inline] - pub fn cookies(&self) -> CookieIter { - CookieIter { - iter: self.get_ref().headers.get_all(header::SET_COOKIE).iter(), - } - } - - /// Add a cookie to this response - #[inline] - pub fn add_cookie(&mut self, cookie: &Cookie) -> Result<(), HttpError> { - let h = &mut self.get_mut().headers; - HeaderValue::from_str(&cookie.to_string()) - .map(|c| { - h.append(header::SET_COOKIE, c); - }).map_err(|e| e.into()) - } - - /// Remove all cookies with the given name from this response. Returns - /// the number of cookies removed. - #[inline] - pub fn del_cookie(&mut self, name: &str) -> usize { - let h = &mut self.get_mut().headers; - let vals: Vec = h - .get_all(header::SET_COOKIE) - .iter() - .map(|v| v.to_owned()) - .collect(); - h.remove(header::SET_COOKIE); - - let mut count: usize = 0; - for v in vals { - if let Ok(s) = v.to_str() { - if let Ok(c) = Cookie::parse_encoded(s) { - if c.name() == name { - count += 1; - continue; - } - } - } - h.append(header::SET_COOKIE, v); - } - count - } - - /// Get the response status code - #[inline] - pub fn status(&self) -> StatusCode { - self.get_ref().status - } - - /// Set the `StatusCode` for this response - #[inline] - pub fn status_mut(&mut self) -> &mut StatusCode { - &mut self.get_mut().status - } - - /// Get custom reason for the response - #[inline] - pub fn reason(&self) -> &str { - if let Some(reason) = self.get_ref().reason { - reason - } else { - self.get_ref() - .status - .canonical_reason() - .unwrap_or("") - } - } - - /// Set the custom reason for the response - #[inline] - pub fn set_reason(&mut self, reason: &'static str) -> &mut Self { - self.get_mut().reason = Some(reason); - self - } - - /// Set connection type - pub fn set_connection_type(&mut self, conn: ConnectionType) -> &mut Self { - self.get_mut().connection_type = Some(conn); - self - } - - /// Connection upgrade status - #[inline] - pub fn upgrade(&self) -> bool { - self.get_ref().connection_type == Some(ConnectionType::Upgrade) - } - - /// Keep-alive status for this connection - pub fn keep_alive(&self) -> Option { - if let Some(ct) = self.get_ref().connection_type { - match ct { - ConnectionType::KeepAlive => Some(true), - ConnectionType::Close | ConnectionType::Upgrade => Some(false), - } - } else { - None - } - } - - /// is chunked encoding enabled - #[inline] - pub fn chunked(&self) -> Option { - self.get_ref().chunked - } - - /// Content encoding - #[inline] - pub fn content_encoding(&self) -> Option { - self.get_ref().encoding - } - - /// Set content encoding - pub fn set_content_encoding(&mut self, enc: ContentEncoding) -> &mut Self { - self.get_mut().encoding = Some(enc); - self - } - - /// Get body of this response - #[inline] - pub fn body(&self) -> &Body { - &self.get_ref().body - } - - /// Set a body - pub fn set_body>(&mut self, body: B) { - self.get_mut().body = body.into(); - } - - /// Set a body and return previous body value - pub fn replace_body>(&mut self, body: B) -> Body { - mem::replace(&mut self.get_mut().body, body.into()) - } - - /// Size of response in bytes, excluding HTTP headers - pub fn response_size(&self) -> u64 { - self.get_ref().response_size - } - - /// Set content encoding - pub(crate) fn set_response_size(&mut self, size: u64) { - self.get_mut().response_size = size; - } - - /// Get write buffer capacity - pub fn write_buffer_capacity(&self) -> usize { - self.get_ref().write_capacity - } - - /// Set write buffer capacity - pub fn set_write_buffer_capacity(&mut self, cap: usize) { - self.get_mut().write_capacity = cap; - } - - pub(crate) fn release(self) { - self.1.release(self.0); - } - - pub(crate) fn into_parts(self) -> HttpResponseParts { - self.0.into_parts() - } - - pub(crate) fn from_parts(parts: HttpResponseParts) -> HttpResponse { - HttpResponse( - Box::new(InnerHttpResponse::from_parts(parts)), - HttpResponsePool::get_pool(), - ) - } -} - -impl fmt::Debug for HttpResponse { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let res = writeln!( - f, - "\nHttpResponse {:?} {}{}", - self.get_ref().version, - self.get_ref().status, - self.get_ref().reason.unwrap_or("") - ); - let _ = writeln!(f, " encoding: {:?}", self.get_ref().encoding); - let _ = writeln!(f, " headers:"); - for (key, val) in self.get_ref().headers.iter() { - let _ = writeln!(f, " {:?}: {:?}", key, val); - } - res - } -} - -pub struct CookieIter<'a> { - iter: header::ValueIter<'a, HeaderValue>, -} - -impl<'a> Iterator for CookieIter<'a> { - type Item = Cookie<'a>; - - #[inline] - fn next(&mut self) -> Option> { - for v in self.iter.by_ref() { - if let Ok(c) = Cookie::parse_encoded(v.to_str().ok()?) { - return Some(c); - } - } - None - } -} - -/// An HTTP response builder -/// -/// This type can be used to construct an instance of `HttpResponse` through a -/// builder-like pattern. -pub struct HttpResponseBuilder { - pool: &'static HttpResponsePool, - response: Option>, - err: Option, - cookies: Option, -} - -impl HttpResponseBuilder { - /// Set HTTP status code of this response. - #[inline] - pub fn status(&mut self, status: StatusCode) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.status = status; - } - self - } - - /// Set HTTP version of this response. - /// - /// By default response's http version depends on request's version. - #[inline] - pub fn version(&mut self, version: Version) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.version = Some(version); - } - self - } - - /// Append a header. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse, Result}; - /// - /// fn index(req: HttpRequest) -> Result { - /// Ok(HttpResponse::Ok() - /// .set(http::header::IfModifiedSince( - /// "Sun, 07 Nov 1994 08:48:37 GMT".parse()?, - /// )) - /// .finish()) - /// } - /// fn main() {} - /// ``` - #[doc(hidden)] - pub fn set(&mut self, hdr: H) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - match hdr.try_into() { - Ok(value) => { - parts.headers.append(H::name(), value); - } - Err(e) => self.err = Some(e.into()), - } - } - self - } - - /// Append a header. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse}; - /// - /// fn index(req: HttpRequest) -> HttpResponse { - /// HttpResponse::Ok() - /// .header("X-TEST", "value") - /// .header(http::header::CONTENT_TYPE, "application/json") - /// .finish() - /// } - /// fn main() {} - /// ``` - pub fn header(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.response, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - parts.headers.append(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - /// Set or replace a header with a single value. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse}; - /// - /// fn index(req: HttpRequest) -> HttpResponse { - /// HttpResponse::Ok() - /// .insert("X-TEST", "value") - /// .insert(http::header::CONTENT_TYPE, "application/json") - /// .finish() - /// } - /// fn main() {} - /// ``` - pub fn insert(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.response, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - parts.headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Remove all instances of a header already set on this `HttpResponseBuilder`. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse}; - /// - /// fn index(req: HttpRequest) -> HttpResponse { - /// HttpResponse::Ok() - /// .header(http::header::CONTENT_TYPE, "nevermind") // won't be used - /// .remove(http::header::CONTENT_TYPE) - /// .finish() - /// } - /// ``` - pub fn remove(&mut self, key: K) -> &mut Self - where HeaderName: HttpTryFrom - { - if let Some(parts) = parts(&mut self.response, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => { - parts.headers.remove(key); - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set the custom reason for the response. - #[inline] - pub fn reason(&mut self, reason: &'static str) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.reason = Some(reason); - } - self - } - - /// Set content encoding. - /// - /// By default `ContentEncoding::Auto` is used, which automatically - /// negotiates content encoding based on request's `Accept-Encoding` - /// headers. To enforce specific encoding, use specific - /// ContentEncoding` value. - #[inline] - pub fn content_encoding(&mut self, enc: ContentEncoding) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.encoding = Some(enc); - } - self - } - - /// Set connection type - #[inline] - #[doc(hidden)] - pub fn connection_type(&mut self, conn: ConnectionType) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.connection_type = Some(conn); - } - self - } - - /// Set connection type to Upgrade - #[inline] - #[doc(hidden)] - pub fn upgrade(&mut self) -> &mut Self { - self.connection_type(ConnectionType::Upgrade) - } - - /// Force close connection, even if it is marked as keep-alive - #[inline] - pub fn force_close(&mut self) -> &mut Self { - self.connection_type(ConnectionType::Close) - } - - /// Enables automatic chunked transfer encoding - #[inline] - pub fn chunked(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.chunked = Some(true); - } - self - } - - /// Force disable chunked encoding - #[inline] - pub fn no_chunking(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.chunked = Some(false); - } - self - } - - /// Set response content type - #[inline] - pub fn content_type(&mut self, value: V) -> &mut Self - where - HeaderValue: HttpTryFrom, - { - if let Some(parts) = parts(&mut self.response, &self.err) { - match HeaderValue::try_from(value) { - Ok(value) => { - parts.headers.insert(header::CONTENT_TYPE, value); - } - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set content length - #[inline] - pub fn content_length(&mut self, len: u64) -> &mut Self { - let mut wrt = BytesMut::new().writer(); - let _ = write!(wrt, "{}", len); - self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze()) - } - - /// Set a cookie - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse, Result}; - /// - /// fn index(req: HttpRequest) -> HttpResponse { - /// HttpResponse::Ok() - /// .cookie( - /// http::Cookie::build("name", "value") - /// .domain("www.rust-lang.org") - /// .path("/") - /// .secure(true) - /// .http_only(true) - /// .finish(), - /// ) - /// .finish() - /// } - /// ``` - pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { - if self.cookies.is_none() { - let mut jar = CookieJar::new(); - jar.add(cookie.into_owned()); - self.cookies = Some(jar) - } else { - self.cookies.as_mut().unwrap().add(cookie.into_owned()); - } - self - } - - /// Remove cookie - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse, Result}; - /// - /// fn index(req: &HttpRequest) -> HttpResponse { - /// let mut builder = HttpResponse::Ok(); - /// - /// if let Some(ref cookie) = req.cookie("name") { - /// builder.del_cookie(cookie); - /// } - /// - /// builder.finish() - /// } - /// ``` - pub fn del_cookie<'a>(&mut self, cookie: &Cookie<'a>) -> &mut Self { - { - if self.cookies.is_none() { - self.cookies = Some(CookieJar::new()) - } - let jar = self.cookies.as_mut().unwrap(); - let cookie = cookie.clone().into_owned(); - jar.add_original(cookie.clone()); - jar.remove(cookie); - } - self - } - - /// This method calls provided closure with builder reference if value is - /// true. - pub fn if_true(&mut self, value: bool, f: F) -> &mut Self - where - F: FnOnce(&mut HttpResponseBuilder), - { - if value { - f(self); - } - self - } - - /// This method calls provided closure with builder reference if value is - /// Some. - pub fn if_some(&mut self, value: Option, f: F) -> &mut Self - where - F: FnOnce(T, &mut HttpResponseBuilder), - { - if let Some(val) = value { - f(val, self); - } - self - } - - /// Set write buffer capacity - /// - /// This parameter makes sense only for streaming response - /// or actor. If write buffer reaches specified capacity, stream or actor - /// get paused. - /// - /// Default write buffer capacity is 64kb - pub fn write_buffer_capacity(&mut self, cap: usize) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.write_capacity = cap; - } - self - } - - /// Set a body and generate `HttpResponse`. - /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn body>(&mut self, body: B) -> HttpResponse { - if let Some(e) = self.err.take() { - return Error::from(e).into(); - } - let mut response = self.response.take().expect("cannot reuse response builder"); - if let Some(ref jar) = self.cookies { - for cookie in jar.delta() { - match HeaderValue::from_str(&cookie.to_string()) { - Ok(val) => response.headers.append(header::SET_COOKIE, val), - Err(e) => return Error::from(e).into(), - }; - } - } - response.body = body.into(); - HttpResponse(response, self.pool) - } - - #[inline] - /// Set a streaming body and generate `HttpResponse`. - /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn streaming(&mut self, stream: S) -> HttpResponse - where - S: Stream + 'static, - E: Into, - { - self.body(Body::Streaming(Box::new(stream.map_err(|e| e.into())))) - } - - /// Set a json body and generate `HttpResponse` - /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn json(&mut self, value: T) -> HttpResponse { - self.json2(&value) - } - - /// Set a json body and generate `HttpResponse` - /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn json2(&mut self, value: &T) -> HttpResponse { - match serde_json::to_string(value) { - Ok(body) => { - let contains = if let Some(parts) = parts(&mut self.response, &self.err) - { - parts.headers.contains_key(header::CONTENT_TYPE) - } else { - true - }; - if !contains { - self.header(header::CONTENT_TYPE, "application/json"); - } - - self.body(body) - } - Err(e) => Error::from(e).into(), - } - } - - #[inline] - /// Set an empty body and generate `HttpResponse` - /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn finish(&mut self) -> HttpResponse { - self.body(Body::Empty) - } - - /// This method construct new `HttpResponseBuilder` - pub fn take(&mut self) -> HttpResponseBuilder { - HttpResponseBuilder { - pool: self.pool, - response: self.response.take(), - err: self.err.take(), - cookies: self.cookies.take(), - } - } -} - -#[inline] -#[cfg_attr(feature = "cargo-clippy", allow(borrowed_box))] -fn parts<'a>( - parts: &'a mut Option>, err: &Option, -) -> Option<&'a mut Box> { - if err.is_some() { - return None; - } - parts.as_mut() -} - -/// Helper converters -impl, E: Into> From> for HttpResponse { - fn from(res: Result) -> Self { - match res { - Ok(val) => val.into(), - Err(err) => err.into().into(), - } - } -} - -impl From for HttpResponse { - fn from(mut builder: HttpResponseBuilder) -> Self { - builder.finish() - } -} - -impl Responder for HttpResponseBuilder { - type Item = HttpResponse; - type Error = Error; - - #[inline] - fn respond_to(mut self, _: &HttpRequest) -> Result { - Ok(self.finish()) - } -} - -impl From<&'static str> for HttpResponse { - fn from(val: &'static str) -> Self { - HttpResponse::Ok() - .content_type("text/plain; charset=utf-8") - .body(val) - } -} - -impl Responder for &'static str { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("text/plain; charset=utf-8") - .body(self)) - } -} - -impl From<&'static [u8]> for HttpResponse { - fn from(val: &'static [u8]) -> Self { - HttpResponse::Ok() - .content_type("application/octet-stream") - .body(val) - } -} - -impl Responder for &'static [u8] { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("application/octet-stream") - .body(self)) - } -} - -impl From for HttpResponse { - fn from(val: String) -> Self { - HttpResponse::Ok() - .content_type("text/plain; charset=utf-8") - .body(val) - } -} - -impl Responder for String { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("text/plain; charset=utf-8") - .body(self)) - } -} - -impl<'a> From<&'a String> for HttpResponse { - fn from(val: &'a String) -> Self { - HttpResponse::build(StatusCode::OK) - .content_type("text/plain; charset=utf-8") - .body(val) - } -} - -impl<'a> Responder for &'a String { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("text/plain; charset=utf-8") - .body(self)) - } -} - -impl From for HttpResponse { - fn from(val: Bytes) -> Self { - HttpResponse::Ok() - .content_type("application/octet-stream") - .body(val) - } -} - -impl Responder for Bytes { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("application/octet-stream") - .body(self)) - } -} - -impl From for HttpResponse { - fn from(val: BytesMut) -> Self { - HttpResponse::Ok() - .content_type("application/octet-stream") - .body(val) - } -} - -impl Responder for BytesMut { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("application/octet-stream") - .body(self)) - } -} - -/// Create `HttpResponseBuilder` from `ClientResponse` -/// -/// It is useful for proxy response. This implementation -/// copies all responses's headers and status. -impl<'a> From<&'a ClientResponse> for HttpResponseBuilder { - fn from(resp: &'a ClientResponse) -> HttpResponseBuilder { - let mut builder = HttpResponse::build(resp.status()); - for (key, value) in resp.headers() { - builder.header(key.clone(), value.clone()); - } - builder - } -} - -impl<'a, S> From<&'a HttpRequest> for HttpResponseBuilder { - fn from(req: &'a HttpRequest) -> HttpResponseBuilder { - req.request() - .server_settings() - .get_response_builder(StatusCode::OK) - } -} - -#[derive(Debug)] -struct InnerHttpResponse { - version: Option, - headers: HeaderMap, - status: StatusCode, - reason: Option<&'static str>, - body: Body, - chunked: Option, - encoding: Option, - connection_type: Option, - write_capacity: usize, - response_size: u64, - error: Option, -} - -pub(crate) struct HttpResponseParts { - version: Option, - headers: HeaderMap, - status: StatusCode, - reason: Option<&'static str>, - body: Option, - encoding: Option, - connection_type: Option, - error: Option, -} - -impl InnerHttpResponse { - #[inline] - fn new(status: StatusCode, body: Body) -> InnerHttpResponse { - InnerHttpResponse { - status, - body, - version: None, - headers: HeaderMap::with_capacity(16), - reason: None, - chunked: None, - encoding: None, - connection_type: None, - response_size: 0, - write_capacity: MAX_WRITE_BUFFER_SIZE, - error: None, - } - } - - /// This is for failure, we can not have Send + Sync on Streaming and Actor response - fn into_parts(mut self) -> HttpResponseParts { - let body = match mem::replace(&mut self.body, Body::Empty) { - Body::Empty => None, - Body::Binary(mut bin) => Some(bin.take()), - Body::Streaming(_) | Body::Actor(_) => { - error!("Streaming or Actor body is not support by error response"); - None - } - }; - - HttpResponseParts { - body, - version: self.version, - headers: self.headers, - status: self.status, - reason: self.reason, - encoding: self.encoding, - connection_type: self.connection_type, - error: self.error, - } - } - - fn from_parts(parts: HttpResponseParts) -> InnerHttpResponse { - let body = if let Some(ref body) = parts.body { - Body::Binary(body.clone().into()) - } else { - Body::Empty - }; - - InnerHttpResponse { - body, - status: parts.status, - version: parts.version, - headers: parts.headers, - reason: parts.reason, - chunked: None, - encoding: parts.encoding, - connection_type: parts.connection_type, - response_size: 0, - write_capacity: MAX_WRITE_BUFFER_SIZE, - error: parts.error, - } - } -} - -/// Internal use only! -pub(crate) struct HttpResponsePool(RefCell>>); - -thread_local!(static POOL: &'static HttpResponsePool = HttpResponsePool::pool()); - -impl HttpResponsePool { - fn pool() -> &'static HttpResponsePool { - let pool = HttpResponsePool(RefCell::new(VecDeque::with_capacity(128))); - Box::leak(Box::new(pool)) - } - - pub fn get_pool() -> &'static HttpResponsePool { - POOL.with(|p| *p) - } - - #[inline] - pub fn get_builder( - pool: &'static HttpResponsePool, status: StatusCode, - ) -> HttpResponseBuilder { - if let Some(mut msg) = pool.0.borrow_mut().pop_front() { - msg.status = status; - HttpResponseBuilder { - pool, - response: Some(msg), - err: None, - cookies: None, - } - } else { - let msg = Box::new(InnerHttpResponse::new(status, Body::Empty)); - HttpResponseBuilder { - pool, - response: Some(msg), - err: None, - cookies: None, - } - } - } - - #[inline] - pub fn get_response( - pool: &'static HttpResponsePool, status: StatusCode, body: Body, - ) -> HttpResponse { - if let Some(mut msg) = pool.0.borrow_mut().pop_front() { - msg.status = status; - msg.body = body; - HttpResponse(msg, pool) - } else { - let msg = Box::new(InnerHttpResponse::new(status, body)); - HttpResponse(msg, pool) - } - } - - #[inline] - fn get(status: StatusCode) -> HttpResponseBuilder { - POOL.with(|pool| HttpResponsePool::get_builder(pool, status)) - } - - #[inline] - fn with_body(status: StatusCode, body: Body) -> HttpResponse { - POOL.with(|pool| HttpResponsePool::get_response(pool, status, body)) - } - - #[inline] - fn release(&self, mut inner: Box) { - let mut p = self.0.borrow_mut(); - if p.len() < 128 { - inner.headers.clear(); - inner.version = None; - inner.chunked = None; - inner.reason = None; - inner.encoding = None; - inner.connection_type = None; - inner.response_size = 0; - inner.error = None; - inner.write_capacity = MAX_WRITE_BUFFER_SIZE; - p.push_front(inner); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use body::Binary; - use http; - use http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; - use time::Duration; - - use test::TestRequest; - - #[test] - fn test_debug() { - let resp = HttpResponse::Ok() - .header(COOKIE, HeaderValue::from_static("cookie1=value1; ")) - .header(COOKIE, HeaderValue::from_static("cookie2=value2; ")) - .finish(); - let dbg = format!("{:?}", resp); - assert!(dbg.contains("HttpResponse")); - } - - #[test] - fn test_response_cookies() { - let req = TestRequest::default() - .header(COOKIE, "cookie1=value1") - .header(COOKIE, "cookie2=value2") - .finish(); - let cookies = req.cookies().unwrap(); - - let resp = HttpResponse::Ok() - .cookie( - http::Cookie::build("name", "value") - .domain("www.rust-lang.org") - .path("/test") - .http_only(true) - .max_age(Duration::days(1)) - .finish(), - ).del_cookie(&cookies[0]) - .finish(); - - let mut val: Vec<_> = resp - .headers() - .get_all("Set-Cookie") - .iter() - .map(|v| v.to_str().unwrap().to_owned()) - .collect(); - val.sort(); - assert!(val[0].starts_with("cookie1=; Max-Age=0;")); - assert_eq!( - val[1], - "name=value; HttpOnly; Path=/test; Domain=www.rust-lang.org; Max-Age=86400" - ); - } - - #[test] - fn test_update_response_cookies() { - let mut r = HttpResponse::Ok() - .cookie(http::Cookie::new("original", "val100")) - .finish(); - - r.add_cookie(&http::Cookie::new("cookie2", "val200")) - .unwrap(); - r.add_cookie(&http::Cookie::new("cookie2", "val250")) - .unwrap(); - r.add_cookie(&http::Cookie::new("cookie3", "val300")) - .unwrap(); - - assert_eq!(r.cookies().count(), 4); - r.del_cookie("cookie2"); - - let mut iter = r.cookies(); - let v = iter.next().unwrap(); - assert_eq!((v.name(), v.value()), ("original", "val100")); - let v = iter.next().unwrap(); - assert_eq!((v.name(), v.value()), ("cookie3", "val300")); - } - - #[test] - fn test_basic_builder() { - let resp = HttpResponse::Ok() - .header("X-TEST", "value") - .version(Version::HTTP_10) - .finish(); - assert_eq!(resp.version(), Some(Version::HTTP_10)); - assert_eq!(resp.status(), StatusCode::OK); - } - - #[test] - fn test_insert() { - let resp = HttpResponse::Ok() - .insert("deleteme", "old value") - .insert("deleteme", "new value") - .finish(); - assert_eq!("new value", resp.headers().get("deleteme").expect("new value")); - } - - #[test] - fn test_remove() { - let resp = HttpResponse::Ok() - .header("deleteme", "value") - .remove("deleteme") - .finish(); - assert!(resp.headers().get("deleteme").is_none()) - } - - #[test] - fn test_remove_replace() { - let resp = HttpResponse::Ok() - .header("some-header", "old_value1") - .header("some-header", "old_value2") - .remove("some-header") - .header("some-header", "new_value1") - .header("some-header", "new_value2") - .remove("unrelated-header") - .finish(); - let mut v = resp.headers().get_all("some-header").into_iter(); - assert_eq!("new_value1", v.next().unwrap()); - assert_eq!("new_value2", v.next().unwrap()); - assert_eq!(None, v.next()); - } - - #[test] - fn test_upgrade() { - let resp = HttpResponse::build(StatusCode::OK).upgrade().finish(); - assert!(resp.upgrade()) - } - - #[test] - fn test_force_close() { - let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); - assert!(!resp.keep_alive().unwrap()) - } - - #[test] - fn test_content_type() { - let resp = HttpResponse::build(StatusCode::OK) - .content_type("text/plain") - .body(Body::Empty); - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") - } - - #[test] - fn test_content_encoding() { - let resp = HttpResponse::build(StatusCode::OK).finish(); - assert_eq!(resp.content_encoding(), None); - - #[cfg(feature = "brotli")] - { - let resp = HttpResponse::build(StatusCode::OK) - .content_encoding(ContentEncoding::Br) - .finish(); - assert_eq!(resp.content_encoding(), Some(ContentEncoding::Br)); - } - - let resp = HttpResponse::build(StatusCode::OK) - .content_encoding(ContentEncoding::Gzip) - .finish(); - assert_eq!(resp.content_encoding(), Some(ContentEncoding::Gzip)); - } - - #[test] - fn test_json() { - let resp = HttpResponse::build(StatusCode::OK).json(vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); - assert_eq!(ct, HeaderValue::from_static("application/json")); - assert_eq!( - *resp.body(), - Body::from(Bytes::from_static(b"[\"v1\",\"v2\",\"v3\"]")) - ); - } - - #[test] - fn test_json_ct() { - let resp = HttpResponse::build(StatusCode::OK) - .header(CONTENT_TYPE, "text/json") - .json(vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); - assert_eq!(ct, HeaderValue::from_static("text/json")); - assert_eq!( - *resp.body(), - Body::from(Bytes::from_static(b"[\"v1\",\"v2\",\"v3\"]")) - ); - } - - #[test] - fn test_json2() { - let resp = HttpResponse::build(StatusCode::OK).json2(&vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); - assert_eq!(ct, HeaderValue::from_static("application/json")); - assert_eq!( - *resp.body(), - Body::from(Bytes::from_static(b"[\"v1\",\"v2\",\"v3\"]")) - ); - } - - #[test] - fn test_json2_ct() { - let resp = HttpResponse::build(StatusCode::OK) - .header(CONTENT_TYPE, "text/json") - .json2(&vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); - assert_eq!(ct, HeaderValue::from_static("text/json")); - assert_eq!( - *resp.body(), - Body::from(Bytes::from_static(b"[\"v1\",\"v2\",\"v3\"]")) - ); - } - - impl Body { - pub(crate) fn bin_ref(&self) -> &Binary { - match *self { - Body::Binary(ref bin) => bin, - _ => panic!(), - } - } - } - - #[test] - fn test_into_response() { - let req = TestRequest::default().finish(); - - let resp: HttpResponse = "test".into(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from("test")); - - let resp: HttpResponse = "test".respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from("test")); - - let resp: HttpResponse = b"test".as_ref().into(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(b"test".as_ref())); - - let resp: HttpResponse = b"test".as_ref().respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(b"test".as_ref())); - - let resp: HttpResponse = "test".to_owned().into(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from("test".to_owned())); - - let resp: HttpResponse = "test".to_owned().respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from("test".to_owned())); - - let resp: HttpResponse = (&"test".to_owned()).into(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(&"test".to_owned())); - - let resp: HttpResponse = (&"test".to_owned()).respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(&"test".to_owned())); - - let b = Bytes::from_static(b"test"); - let resp: HttpResponse = b.into(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.body().bin_ref(), - &Binary::from(Bytes::from_static(b"test")) - ); - - let b = Bytes::from_static(b"test"); - let resp: HttpResponse = b.respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.body().bin_ref(), - &Binary::from(Bytes::from_static(b"test")) - ); - - let b = BytesMut::from("test"); - let resp: HttpResponse = b.into(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(BytesMut::from("test"))); - - let b = BytesMut::from("test"); - let resp: HttpResponse = b.respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(BytesMut::from("test"))); - } - - #[test] - fn test_into_builder() { - let mut resp: HttpResponse = "test".into(); - assert_eq!(resp.status(), StatusCode::OK); - - resp.add_cookie(&http::Cookie::new("cookie1", "val100")) - .unwrap(); - - let mut builder = resp.into_builder(); - let resp = builder.status(StatusCode::BAD_REQUEST).finish(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - - let cookie = resp.cookies().next().unwrap(); - assert_eq!((cookie.name(), cookie.value()), ("cookie1", "val100")); - } -} diff --git a/src/info.rs b/src/info.rs index 43c22123e..3b51215fe 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,10 +1,17 @@ -use http::header::{self, HeaderName}; -use server::Request; +use std::cell::Ref; + +use actix_http::http::header::{self, HeaderName}; +use actix_http::RequestHead; const X_FORWARDED_FOR: &[u8] = b"x-forwarded-for"; const X_FORWARDED_HOST: &[u8] = b"x-forwarded-host"; const X_FORWARDED_PROTO: &[u8] = b"x-forwarded-proto"; +pub enum ConnectionInfoError { + UnknownHost, + UnknownScheme, +} + /// `HttpRequest` connection information #[derive(Clone, Default)] pub struct ConnectionInfo { @@ -16,18 +23,22 @@ pub struct ConnectionInfo { impl ConnectionInfo { /// Create *ConnectionInfo* instance for a request. - #[cfg_attr( - feature = "cargo-clippy", - allow(cyclomatic_complexity) - )] - pub fn update(&mut self, req: &Request) { + pub fn get(req: &RequestHead) -> Ref { + if !req.extensions().contains::() { + req.extensions_mut().insert(ConnectionInfo::new(req)); + } + Ref::map(req.extensions(), |e| e.get().unwrap()) + } + + #[cfg_attr(feature = "cargo-clippy", allow(cyclomatic_complexity))] + fn new(req: &RequestHead) -> ConnectionInfo { let mut host = None; let mut scheme = None; let mut remote = None; let mut peer = None; // load forwarded header - for hdr in req.headers().get_all(header::FORWARDED) { + for hdr in req.headers.get_all(header::FORWARDED) { if let Ok(val) = hdr.to_str() { for pair in val.split(';') { for el in pair.split(',') { @@ -35,15 +46,21 @@ impl ConnectionInfo { if let Some(name) = items.next() { if let Some(val) = items.next() { match &name.to_lowercase() as &str { - "for" => if remote.is_none() { - remote = Some(val.trim()); - }, - "proto" => if scheme.is_none() { - scheme = Some(val.trim()); - }, - "host" => if host.is_none() { - host = Some(val.trim()); - }, + "for" => { + if remote.is_none() { + remote = Some(val.trim()); + } + } + "proto" => { + if scheme.is_none() { + scheme = Some(val.trim()); + } + } + "host" => { + if host.is_none() { + host = Some(val.trim()); + } + } _ => (), } } @@ -56,7 +73,7 @@ impl ConnectionInfo { // scheme if scheme.is_none() { if let Some(h) = req - .headers() + .headers .get(HeaderName::from_lowercase(X_FORWARDED_PROTO).unwrap()) { if let Ok(h) = h.to_str() { @@ -64,7 +81,7 @@ impl ConnectionInfo { } } if scheme.is_none() { - scheme = req.uri().scheme_part().map(|a| a.as_str()); + scheme = req.uri.scheme_part().map(|a| a.as_str()); if scheme.is_none() && req.server_settings().secure() { scheme = Some("https") } @@ -74,7 +91,7 @@ impl ConnectionInfo { // host if host.is_none() { if let Some(h) = req - .headers() + .headers .get(HeaderName::from_lowercase(X_FORWARDED_HOST).unwrap()) { if let Ok(h) = h.to_str() { @@ -82,11 +99,11 @@ impl ConnectionInfo { } } if host.is_none() { - if let Some(h) = req.headers().get(header::HOST) { + if let Some(h) = req.headers.get(header::HOST) { host = h.to_str().ok(); } if host.is_none() { - host = req.uri().authority_part().map(|a| a.as_str()); + host = req.uri.authority_part().map(|a| a.as_str()); if host.is_none() { host = Some(req.server_settings().host()); } @@ -97,7 +114,7 @@ impl ConnectionInfo { // remote addr if remote.is_none() { if let Some(h) = req - .headers() + .headers .get(HeaderName::from_lowercase(X_FORWARDED_FOR).unwrap()) { if let Ok(h) = h.to_str() { @@ -110,10 +127,12 @@ impl ConnectionInfo { } } - self.scheme = scheme.unwrap_or("http").to_owned(); - self.host = host.unwrap_or("localhost").to_owned(); - self.remote = remote.map(|s| s.to_owned()); - self.peer = peer; + ConnectionInfo { + scheme: scheme.unwrap_or("http").to_owned(), + host: host.unwrap_or("localhost").to_owned(), + remote: remote.map(|s| s.to_owned()), + peer: peer, + } } /// Scheme of the request. @@ -163,7 +182,7 @@ impl ConnectionInfo { #[cfg(test)] mod tests { use super::*; - use test::TestRequest; + use crate::test::TestRequest; #[test] fn test_forwarded() { @@ -177,7 +196,8 @@ mod tests { .header( header::FORWARDED, "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org", - ).request(); + ) + .request(); let mut info = ConnectionInfo::default(); info.update(&req); diff --git a/src/json.rs b/src/json.rs deleted file mode 100644 index b04cad2fb..000000000 --- a/src/json.rs +++ /dev/null @@ -1,519 +0,0 @@ -use bytes::BytesMut; -use futures::{Future, Poll, Stream}; -use http::header::CONTENT_LENGTH; -use std::fmt; -use std::ops::{Deref, DerefMut}; -use std::rc::Rc; - -use mime; -use serde::de::DeserializeOwned; -use serde::Serialize; -use serde_json; - -use error::{Error, JsonPayloadError}; -use handler::{FromRequest, Responder}; -use http::StatusCode; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; - -/// Json helper -/// -/// Json can be used for two different purpose. First is for json response -/// generation and second is for extracting typed information from request's -/// payload. -/// -/// To extract typed information from request's body, the type `T` must -/// implement the `Deserialize` trait from *serde*. -/// -/// [**JsonConfig**](dev/struct.JsonConfig.html) allows to configure extraction -/// process. -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{App, Json, Result, http}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// deserialize `Info` from request's body -/// fn index(info: Json) -> Result { -/// Ok(format!("Welcome {}!", info.username)) -/// } -/// -/// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| r.method(http::Method::POST).with(index)); // <- use `with` extractor -/// } -/// ``` -/// -/// The `Json` type allows you to respond with well-formed JSON data: simply -/// return a value of type Json where T is the type of a structure -/// to serialize into *JSON*. The type `T` must implement the `Serialize` -/// trait from *serde*. -/// -/// ```rust -/// # extern crate actix_web; -/// # #[macro_use] extern crate serde_derive; -/// # use actix_web::*; -/// # -/// #[derive(Serialize)] -/// struct MyObj { -/// name: String, -/// } -/// -/// fn index(req: HttpRequest) -> Result> { -/// Ok(Json(MyObj { -/// name: req.match_info().query("name")?, -/// })) -/// } -/// # fn main() {} -/// ``` -pub struct Json(pub T); - -impl Json { - /// Deconstruct to an inner value - pub fn into_inner(self) -> T { - self.0 - } -} - -impl Deref for Json { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Json { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl fmt::Debug for Json -where - T: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Json: {:?}", self.0) - } -} - -impl fmt::Display for Json -where - T: fmt::Display, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} - -impl Responder for Json { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - let body = serde_json::to_string(&self.0)?; - - Ok(req - .build_response(StatusCode::OK) - .content_type("application/json") - .body(body)) - } -} - -impl FromRequest for Json -where - T: DeserializeOwned + 'static, - S: 'static, -{ - type Config = JsonConfig; - type Result = Box>; - - #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - let req2 = req.clone(); - let err = Rc::clone(&cfg.ehandler); - Box::new( - JsonBody::new(req, Some(cfg)) - .limit(cfg.limit) - .map_err(move |e| (*err)(e, &req2)) - .map(Json), - ) - } -} - -/// Json extractor configuration -/// -/// ```rust -/// # extern crate actix_web; -/// extern crate mime; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{error, http, App, HttpResponse, Json, Result}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// deserialize `Info` from request's body, max payload size is 4kb -/// fn index(info: Json) -> Result { -/// Ok(format!("Welcome {}!", info.username)) -/// } -/// -/// fn main() { -/// let app = App::new().resource("/index.html", |r| { -/// 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() -/// }); -/// }) -/// }); -/// } -/// ``` -pub struct JsonConfig { - limit: usize, - ehandler: Rc) -> Error>, - content_type: Option bool>>, -} - -impl JsonConfig { - /// Change max size of payload. By default max size is 256Kb - pub fn limit(&mut self, limit: usize) -> &mut Self { - self.limit = limit; - self - } - - /// Set custom error handler - pub fn error_handler(&mut self, f: F) -> &mut Self - where - F: Fn(JsonPayloadError, &HttpRequest) -> Error + 'static, - { - 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 { - fn default() -> Self { - JsonConfig { - limit: 262_144, - ehandler: Rc::new(|e, _| e.into()), - content_type: None, - } - } -} - -/// Request payload json parser that resolves to a deserialized `T` value. -/// -/// Returns error: -/// -/// * content type is not `application/json` -/// (unless specified in [`JsonConfig`](struct.JsonConfig.html)) -/// * content length is greater than 256k -/// -/// # Server example -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate futures; -/// # #[macro_use] extern crate serde_derive; -/// use actix_web::{AsyncResponder, Error, HttpMessage, HttpRequest, HttpResponse}; -/// use futures::future::Future; -/// -/// #[derive(Deserialize, Debug)] -/// struct MyObj { -/// name: String, -/// } -/// -/// fn index(mut req: HttpRequest) -> Box> { -/// req.json() // <- get JsonBody future -/// .from_err() -/// .and_then(|val: MyObj| { // <- deserialized value -/// println!("==== BODY ==== {:?}", val); -/// Ok(HttpResponse::Ok().into()) -/// }).responder() -/// } -/// # fn main() {} -/// ``` -pub struct JsonBody { - limit: usize, - length: Option, - stream: Option, - err: Option, - fut: Option>>, -} - -impl JsonBody { - /// Create `JsonBody` for request. - 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) || - cfg.map_or(false, |cfg| { - cfg.content_type.as_ref().map_or(false, |predicate| predicate(mime)) - }) - } else { - false - }; - if !json { - return JsonBody { - limit: 262_144, - length: None, - stream: None, - fut: None, - err: Some(JsonPayloadError::ContentType), - }; - } - - let mut len = None; - if let Some(l) = req.headers().get(CONTENT_LENGTH) { - if let Ok(s) = l.to_str() { - if let Ok(l) = s.parse::() { - len = Some(l) - } - } - } - - JsonBody { - limit: 262_144, - length: len, - stream: Some(req.payload()), - fut: None, - err: None, - } - } - - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } -} - -impl Future for JsonBody { - type Item = U; - type Error = JsonPayloadError; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut { - return fut.poll(); - } - - if let Some(err) = self.err.take() { - return Err(err); - } - - let limit = self.limit; - if let Some(len) = self.length.take() { - if len > limit { - return Err(JsonPayloadError::Overflow); - } - } - - let fut = self - .stream - .take() - .expect("JsonBody could not be used second time") - .from_err() - .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { - if (body.len() + chunk.len()) > limit { - Err(JsonPayloadError::Overflow) - } else { - body.extend_from_slice(&chunk); - Ok(body) - } - }).and_then(|body| Ok(serde_json::from_slice::(&body)?)); - self.fut = Some(Box::new(fut)); - self.poll() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use bytes::Bytes; - use futures::Async; - use http::header; - - use handler::Handler; - use test::TestRequest; - use with::With; - - impl PartialEq for JsonPayloadError { - fn eq(&self, other: &JsonPayloadError) -> bool { - match *self { - JsonPayloadError::Overflow => match *other { - JsonPayloadError::Overflow => true, - _ => false, - }, - JsonPayloadError::ContentType => match *other { - JsonPayloadError::ContentType => true, - _ => false, - }, - _ => false, - } - } - } - - #[derive(Serialize, Deserialize, PartialEq, Debug)] - struct MyObject { - name: String, - } - - #[test] - fn test_json() { - let json = Json(MyObject { - name: "test".to_owned(), - }); - let resp = json.respond_to(&TestRequest::default().finish()).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "application/json" - ); - } - - #[test] - fn test_json_body() { - let req = TestRequest::default().finish(); - let mut json = req.json::(); - assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); - - let req = TestRequest::default() - .header( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/text"), - ).finish(); - let mut json = req.json::(); - assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); - - let req = TestRequest::default() - .header( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - ).header( - header::CONTENT_LENGTH, - header::HeaderValue::from_static("10000"), - ).finish(); - let mut json = req.json::().limit(100); - assert_eq!(json.poll().err().unwrap(), JsonPayloadError::Overflow); - - let req = TestRequest::default() - .header( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - ).header( - header::CONTENT_LENGTH, - header::HeaderValue::from_static("16"), - ).set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) - .finish(); - - let mut json = req.json::(); - assert_eq!( - json.poll().ok().unwrap(), - Async::Ready(MyObject { - name: "test".to_owned() - }) - ); - } - - #[test] - fn test_with_json() { - let mut cfg = JsonConfig::default(); - cfg.limit(4096); - let handler = With::new(|data: Json| data, cfg); - - let req = TestRequest::default().finish(); - assert!(handler.handle(&req).as_err().is_some()); - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - ).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_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()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 3b00cda16..f09c11ced 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,292 +1,37 @@ -//! Actix web is a small, pragmatic, and extremely fast web framework -//! for Rust. -//! -//! ```rust -//! use actix_web::{server, App, Path, Responder}; -//! # use std::thread; -//! -//! fn index(info: Path<(String, u32)>) -> impl Responder { -//! format!("Hello {}! id:{}", info.0, info.1) -//! } -//! -//! fn main() { -//! # thread::spawn(|| { -//! server::new(|| { -//! App::new().resource("/{name}/{id}/index.html", |r| r.with(index)) -//! }).bind("127.0.0.1:8080") -//! .unwrap() -//! .run(); -//! # }); -//! } -//! ``` -//! -//! ## Documentation & community resources -//! -//! Besides the API documentation (which you are currently looking -//! at!), several other resources are available: -//! -//! * [User Guide](https://actix.rs/docs/) -//! * [Chat on gitter](https://gitter.im/actix/actix) -//! * [GitHub repository](https://github.com/actix/actix-web) -//! * [Cargo package](https://crates.io/crates/actix-web) -//! -//! To get started navigating the API documentation you may want to -//! consider looking at the following pages: -//! -//! * [App](struct.App.html): This struct represents an actix-web -//! application and is used to configure routes and other common -//! settings. -//! -//! * [HttpServer](server/struct.HttpServer.html): This struct -//! represents an HTTP server instance and is used to instantiate and -//! configure servers. -//! -//! * [HttpRequest](struct.HttpRequest.html) and -//! [HttpResponse](struct.HttpResponse.html): These structs -//! represent HTTP requests and responses and expose various methods -//! for inspecting, creating and otherwise utilizing them. -//! -//! ## Features -//! -//! * Supported *HTTP/1.x* and *HTTP/2.0* protocols -//! * Streaming and pipelining -//! * Keep-alive and slow requests handling -//! * `WebSockets` server/client -//! * Transparent content compression/decompression (br, gzip, deflate) -//! * Configurable request routing -//! * Graceful server shutdown -//! * Multipart streams -//! * SSL support with OpenSSL or `native-tls` -//! * Middlewares (`Logger`, `Session`, `CORS`, `CSRF`, `DefaultHeaders`) -//! * Built on top of [Actix actor framework](https://github.com/actix/actix) -//! * Supported Rust version: 1.26 or later -//! -//! ## Package feature -//! -//! * `tls` - enables ssl support via `native-tls` crate -//! * `ssl` - enables ssl support via `openssl` crate, supports `http/2` -//! * `rust-tls` - enables ssl support via `rustls` crate, supports `http/2` -//! * `uds` - enables support for making client requests via Unix Domain Sockets. -//! Unix only. Not necessary for *serving* requests. -//! * `session` - enables session support, includes `ring` crate as -//! dependency -//! * `brotli` - enables `brotli` compression support, requires `c` -//! compiler -//! * `flate2-c` - enables `gzip`, `deflate` compression support, requires -//! `c` compiler -//! * `flate2-rust` - experimental rust based implementation for -//! `gzip`, `deflate` compression. -//! -#![cfg_attr(actix_nightly, feature( - specialization, // for impl ErrorResponse for std::error::Error - extern_prelude, - tool_lints, -))] -#![warn(missing_docs)] +#![allow(clippy::type_complexity, dead_code, unused_variables)] -#[macro_use] -extern crate log; -extern crate base64; -extern crate byteorder; -extern crate bytes; -extern crate regex; -extern crate sha1; -extern crate time; -#[macro_use] -extern crate bitflags; -#[macro_use] -extern crate failure; -#[macro_use] -extern crate lazy_static; -#[macro_use] -extern crate futures; -extern crate cookie; -extern crate futures_cpupool; -extern crate http as modhttp; -extern crate httparse; -extern crate language_tags; -extern crate lazycell; -extern crate mime; -extern crate mime_guess; -extern crate mio; -extern crate net2; -extern crate parking_lot; -extern crate rand; -extern crate slab; -extern crate tokio; -extern crate tokio_current_thread; -extern crate tokio_io; -extern crate tokio_reactor; -extern crate tokio_tcp; -extern crate tokio_timer; -#[cfg(all(unix, feature = "uds"))] -extern crate tokio_uds; -extern crate url; -#[macro_use] -extern crate serde; -#[cfg(feature = "brotli")] -extern crate brotli2; -extern crate encoding; -#[cfg(feature = "flate2")] -extern crate flate2; -extern crate h2 as http2; -extern crate num_cpus; -extern crate serde_urlencoded; -#[macro_use] -extern crate percent_encoding; -extern crate serde_json; -extern crate smallvec; -extern crate v_htmlescape; - -extern crate actix_net; -#[macro_use] -extern crate actix as actix_inner; - -#[cfg(test)] -#[macro_use] -extern crate serde_derive; - -#[cfg(feature = "tls")] -extern crate native_tls; -#[cfg(feature = "tls")] -extern crate tokio_tls; - -#[cfg(feature = "openssl")] -extern crate openssl; -#[cfg(feature = "openssl")] -extern crate tokio_openssl; - -#[cfg(feature = "rust-tls")] -extern crate rustls; -#[cfg(feature = "rust-tls")] -extern crate tokio_rustls; -#[cfg(feature = "rust-tls")] -extern crate webpki; -#[cfg(feature = "rust-tls")] -extern crate webpki_roots; - -mod application; -mod body; -mod context; -mod de; -mod extensions; +mod app; mod extractor; -mod handler; -mod header; +pub mod handler; mod helpers; -mod httpcodes; -mod httpmessage; -mod httprequest; -mod httpresponse; -mod info; -mod json; -mod param; -mod payload; -mod pipeline; -mod resource; -mod route; -mod router; -mod scope; -mod uri; -mod with; - -pub mod client; -pub mod error; +// mod info; +pub mod blocking; +pub mod filter; pub mod fs; pub mod middleware; -pub mod multipart; -pub mod pred; -pub mod server; -pub mod test; -pub mod ws; -pub use application::App; -pub use body::{Binary, Body}; -pub use context::HttpContext; -pub use error::{Error, ResponseError, Result}; -pub use extensions::Extensions; -pub use extractor::{Form, Path, Query}; -pub use handler::{ - AsyncResponder, Either, FromRequest, FutureResponse, Responder, State, -}; -pub use httpmessage::HttpMessage; -pub use httprequest::HttpRequest; -pub use httpresponse::HttpResponse; -pub use json::Json; -pub use scope::Scope; -pub use server::Request; +mod request; +mod resource; +mod responder; +mod route; +mod service; +mod state; -pub mod actix { - //! Re-exports [actix's](https://docs.rs/actix/) prelude - pub use super::actix_inner::actors::resolver; - pub use super::actix_inner::actors::signal; - pub use super::actix_inner::fut; - pub use super::actix_inner::msgs; - pub use super::actix_inner::prelude::*; - pub use super::actix_inner::{run, spawn}; -} +// re-export for convenience +pub use actix_http::Response as HttpResponse; +pub use actix_http::{http, Error, HttpMessage, ResponseError}; -#[cfg(feature = "openssl")] -pub(crate) const HAS_OPENSSL: bool = true; -#[cfg(not(feature = "openssl"))] -pub(crate) const HAS_OPENSSL: bool = false; - -#[cfg(feature = "tls")] -pub(crate) const HAS_TLS: bool = true; -#[cfg(not(feature = "tls"))] -pub(crate) const HAS_TLS: bool = false; - -#[cfg(feature = "rust-tls")] -pub(crate) const HAS_RUSTLS: bool = true; -#[cfg(not(feature = "rust-tls"))] -pub(crate) const HAS_RUSTLS: bool = false; +pub use crate::app::App; +pub use crate::extractor::{Form, Json, Path, Query}; +pub use crate::handler::FromRequest; +pub use crate::request::HttpRequest; +pub use crate::resource::Resource; +pub use crate::responder::{Either, Responder}; +pub use crate::service::{ServiceRequest, ServiceResponse}; +pub use crate::state::State; pub mod dev { - //! The `actix-web` prelude for library developers - //! - //! The purpose of this module is to alleviate imports of many common actix - //! traits by adding a glob import to the top of actix heavy modules: - //! - //! ``` - //! # #![allow(unused_imports)] - //! use actix_web::dev::*; - //! ``` - - pub use body::BodyStream; - pub use context::Drain; - pub use extractor::{FormConfig, PayloadConfig, QueryConfig, PathConfig, EitherConfig, EitherCollisionStrategy}; - pub use handler::{AsyncResult, Handler}; - pub use httpmessage::{MessageBody, Readlines, UrlEncoded}; - pub use httpresponse::HttpResponseBuilder; - pub use info::ConnectionInfo; - pub use json::{JsonBody, JsonConfig}; - pub use param::{FromParam, Params}; - pub use payload::{Payload, PayloadBuffer}; - pub use pipeline::Pipeline; - pub use resource::Resource; - pub use route::Route; - pub use router::{ResourceDef, ResourceInfo, ResourceType, Router}; -} - -pub mod http { - //! Various HTTP related types - - // re-exports - pub use modhttp::{Method, StatusCode, Version}; - - #[doc(hidden)] - pub use modhttp::{uri, Error, Extensions, HeaderMap, HttpTryFrom, Uri}; - - pub use cookie::{Cookie, CookieBuilder}; - - pub use helpers::NormalizePath; - - /// Various http headers - pub mod header { - pub use header::*; - pub use header::{ - Charset, ContentDisposition, DispositionParam, DispositionType, LanguageTag, - }; - } - pub use header::ContentEncoding; - pub use httpresponse::ConnectionType; + pub use crate::app::AppService; + pub use crate::handler::{AsyncFactory, Extract, Factory, Handle}; + pub use crate::route::{Route, RouteBuilder}; + // pub use crate::info::ConnectionInfo; } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs new file mode 100644 index 000000000..fee17ce13 --- /dev/null +++ b/src/middleware/compress.rs @@ -0,0 +1,443 @@ +use std::io::Write; +use std::str::FromStr; +use std::{cmp, fmt, io}; + +use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; +use actix_http::http::header::{ + ContentEncoding, HeaderValue, ACCEPT_ENCODING, CONTENT_ENCODING, +}; +use actix_http::http::{HttpTryFrom, StatusCode}; +use actix_http::{Error, Head, ResponseHead}; +use actix_service::{IntoNewTransform, Service, Transform}; +use bytes::{Bytes, BytesMut}; +use futures::{Async, Future, Poll}; +use log::trace; + +#[cfg(feature = "brotli")] +use brotli2::write::BrotliEncoder; +#[cfg(feature = "flate2")] +use flate2::write::{GzEncoder, ZlibEncoder}; + +use crate::middleware::MiddlewareFactory; +use crate::service::{ServiceRequest, ServiceResponse}; + +#[derive(Debug, Clone)] +pub struct Compress(ContentEncoding); + +impl Compress { + pub fn new(encoding: ContentEncoding) -> Self { + Compress(encoding) + } +} + +impl Default for Compress { + fn default() -> Self { + Compress::new(ContentEncoding::Auto) + } +} + +impl Transform for Compress +where + P: 'static, + B: MessageBody, + S: Service, Response = ServiceResponse>, + S::Future: 'static, +{ + type Request = ServiceRequest

    ; + type Response = ServiceResponse>; + type Error = S::Error; + type Future = CompressResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: ServiceRequest

    , srv: &mut S) -> Self::Future { + // negotiate content-encoding + let encoding = if let Some(val) = req.headers.get(ACCEPT_ENCODING) { + if let Ok(enc) = val.to_str() { + AcceptEncoding::parse(enc, self.0) + } else { + ContentEncoding::Identity + } + } else { + ContentEncoding::Identity + }; + + CompressResponse { + encoding, + fut: srv.call(req), + } + } +} + +#[doc(hidden)] +pub struct CompressResponse +where + P: 'static, + B: MessageBody, + S: Service, Response = ServiceResponse>, + S::Future: 'static, +{ + fut: S::Future, + encoding: ContentEncoding, +} + +impl Future for CompressResponse +where + P: 'static, + B: MessageBody, + S: Service, Response = ServiceResponse>, + S::Future: 'static, +{ + type Item = ServiceResponse>; + type Error = S::Error; + + fn poll(&mut self) -> Poll { + let resp = futures::try_ready!(self.fut.poll()); + + Ok(Async::Ready(resp.map_body(move |head, body| { + Encoder::body(self.encoding, head, body) + }))) + } +} + +impl IntoNewTransform, S> for Compress +where + P: 'static, + B: MessageBody, + S: Service, Response = ServiceResponse>, + S::Future: 'static, +{ + fn into_new_transform(self) -> MiddlewareFactory { + MiddlewareFactory::new(self) + } +} + +enum EncoderBody { + Body(B), + Other(Box), + None, +} + +pub struct Encoder { + body: EncoderBody, + encoder: Option, +} + +impl MessageBody for Encoder { + fn length(&self) -> BodyLength { + if self.encoder.is_none() { + match self.body { + EncoderBody::Body(ref b) => b.length(), + EncoderBody::Other(ref b) => b.length(), + EncoderBody::None => BodyLength::Empty, + } + } else { + BodyLength::Stream + } + } + + fn poll_next(&mut self) -> Poll, Error> { + loop { + let result = match self.body { + EncoderBody::Body(ref mut b) => b.poll_next()?, + EncoderBody::Other(ref mut b) => b.poll_next()?, + EncoderBody::None => return Ok(Async::Ready(None)), + }; + match result { + Async::NotReady => return Ok(Async::NotReady), + Async::Ready(Some(chunk)) => { + if let Some(ref mut encoder) = self.encoder { + if encoder.write(&chunk)? { + return Ok(Async::Ready(Some(encoder.take()))); + } + } else { + return Ok(Async::Ready(Some(chunk))); + } + } + Async::Ready(None) => { + if let Some(encoder) = self.encoder.take() { + let chunk = encoder.finish()?; + if chunk.is_empty() { + return Ok(Async::Ready(None)); + } else { + return Ok(Async::Ready(Some(chunk))); + } + } else { + return Ok(Async::Ready(None)); + } + } + } + } + } +} + +fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { + head.headers_mut().insert( + CONTENT_ENCODING, + HeaderValue::try_from(Bytes::from_static(encoding.as_str().as_bytes())).unwrap(), + ); +} + +impl Encoder { + fn body( + encoding: ContentEncoding, + head: &mut ResponseHead, + body: ResponseBody, + ) -> ResponseBody> { + let has_ce = head.headers().contains_key(CONTENT_ENCODING); + match body { + ResponseBody::Other(b) => match b { + Body::None => ResponseBody::Other(Body::None), + Body::Empty => ResponseBody::Other(Body::Empty), + Body::Bytes(buf) => { + if !(has_ce + || encoding == ContentEncoding::Identity + || encoding == ContentEncoding::Auto) + { + let mut enc = ContentEncoder::encoder(encoding).unwrap(); + + // TODO return error! + let _ = enc.write(buf.as_ref()); + let body = enc.finish().unwrap(); + update_head(encoding, head); + ResponseBody::Other(Body::Bytes(body)) + } else { + ResponseBody::Other(Body::Bytes(buf)) + } + } + Body::Message(stream) => { + if has_ce || head.status == StatusCode::SWITCHING_PROTOCOLS { + ResponseBody::Body(Encoder { + body: EncoderBody::Other(stream), + encoder: None, + }) + } else { + update_head(encoding, head); + head.no_chunking = false; + ResponseBody::Body(Encoder { + body: EncoderBody::Other(stream), + encoder: ContentEncoder::encoder(encoding), + }) + } + } + }, + ResponseBody::Body(stream) => { + if has_ce || head.status == StatusCode::SWITCHING_PROTOCOLS { + ResponseBody::Body(Encoder { + body: EncoderBody::Body(stream), + encoder: None, + }) + } else { + update_head(encoding, head); + head.no_chunking = false; + ResponseBody::Body(Encoder { + body: EncoderBody::Body(stream), + encoder: ContentEncoder::encoder(encoding), + }) + } + } + } + } +} + +pub(crate) struct Writer { + buf: BytesMut, +} + +impl Writer { + fn new() -> Writer { + Writer { + buf: BytesMut::with_capacity(8192), + } + } + fn take(&mut self) -> Bytes { + self.buf.take().freeze() + } +} + +impl io::Write for Writer { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.buf.extend_from_slice(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +pub(crate) enum ContentEncoder { + #[cfg(feature = "flate2")] + Deflate(ZlibEncoder), + #[cfg(feature = "flate2")] + Gzip(GzEncoder), + #[cfg(feature = "brotli")] + Br(BrotliEncoder), +} + +impl fmt::Debug for ContentEncoder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + #[cfg(feature = "brotli")] + ContentEncoder::Br(_) => writeln!(f, "ContentEncoder(Brotli)"), + #[cfg(feature = "flate2")] + ContentEncoder::Deflate(_) => writeln!(f, "ContentEncoder(Deflate)"), + #[cfg(feature = "flate2")] + ContentEncoder::Gzip(_) => writeln!(f, "ContentEncoder(Gzip)"), + } + } +} + +impl ContentEncoder { + fn encoder(encoding: ContentEncoding) -> Option { + match encoding { + #[cfg(feature = "flate2")] + ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new( + Writer::new(), + flate2::Compression::fast(), + ))), + #[cfg(feature = "flate2")] + ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( + Writer::new(), + flate2::Compression::fast(), + ))), + #[cfg(feature = "brotli")] + ContentEncoding::Br => { + Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) + } + _ => None, + } + } + + #[inline] + pub(crate) fn take(&mut self) -> Bytes { + match *self { + #[cfg(feature = "brotli")] + ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "flate2")] + ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "flate2")] + ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), + } + } + + fn finish(self) -> Result { + match self { + #[cfg(feature = "brotli")] + ContentEncoder::Br(encoder) => match encoder.finish() { + Ok(writer) => Ok(writer.buf.freeze()), + Err(err) => Err(err), + }, + #[cfg(feature = "flate2")] + ContentEncoder::Gzip(encoder) => match encoder.finish() { + Ok(writer) => Ok(writer.buf.freeze()), + Err(err) => Err(err), + }, + #[cfg(feature = "flate2")] + ContentEncoder::Deflate(encoder) => match encoder.finish() { + Ok(writer) => Ok(writer.buf.freeze()), + Err(err) => Err(err), + }, + } + } + + fn write(&mut self, data: &[u8]) -> Result { + match *self { + #[cfg(feature = "brotli")] + ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { + Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), + Err(err) => { + trace!("Error decoding br encoding: {}", err); + Err(err) + } + }, + #[cfg(feature = "flate2")] + ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { + Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), + Err(err) => { + trace!("Error decoding gzip encoding: {}", err); + Err(err) + } + }, + #[cfg(feature = "flate2")] + ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { + Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), + Err(err) => { + trace!("Error decoding deflate encoding: {}", err); + Err(err) + } + }, + } + } +} + +struct AcceptEncoding { + encoding: ContentEncoding, + quality: f64, +} + +impl Eq for AcceptEncoding {} + +impl Ord for AcceptEncoding { + fn cmp(&self, other: &AcceptEncoding) -> cmp::Ordering { + if self.quality > other.quality { + cmp::Ordering::Less + } else if self.quality < other.quality { + cmp::Ordering::Greater + } else { + cmp::Ordering::Equal + } + } +} + +impl PartialOrd for AcceptEncoding { + fn partial_cmp(&self, other: &AcceptEncoding) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for AcceptEncoding { + fn eq(&self, other: &AcceptEncoding) -> bool { + self.quality == other.quality + } +} + +impl AcceptEncoding { + fn new(tag: &str) -> Option { + let parts: Vec<&str> = tag.split(';').collect(); + let encoding = match parts.len() { + 0 => return None, + _ => ContentEncoding::from(parts[0]), + }; + let quality = match parts.len() { + 1 => encoding.quality(), + _ => match f64::from_str(parts[1]) { + Ok(q) => q, + Err(_) => 0.0, + }, + }; + Some(AcceptEncoding { encoding, quality }) + } + + /// Parse a raw Accept-Encoding header value into an ordered list. + pub fn parse(raw: &str, encoding: ContentEncoding) -> ContentEncoding { + let mut encodings: Vec<_> = raw + .replace(' ', "") + .split(',') + .map(|l| AcceptEncoding::new(l)) + .collect(); + encodings.sort(); + + for enc in encodings { + if let Some(enc) = enc { + if encoding == ContentEncoding::Auto { + return enc.encoding; + } else if encoding == enc.encoding { + return encoding; + } + } + } + ContentEncoding::Identity + } +} diff --git a/src/middleware/cors.rs b/src/middleware/cors.rs deleted file mode 100644 index 386d00078..000000000 --- a/src/middleware/cors.rs +++ /dev/null @@ -1,1227 +0,0 @@ -//! Cross-origin resource sharing (CORS) for Actix applications -//! -//! CORS middleware could be used with application and with resource. -//! First you need to construct CORS middleware instance. -//! -//! To construct a cors: -//! -//! 1. Call [`Cors::build`](struct.Cors.html#method.build) to start building. -//! 2. Use any of the builder methods to set fields in the backend. -//! 3. Call [finish](struct.Cors.html#method.finish) to retrieve the -//! constructed backend. -//! -//! Cors middleware could be used as parameter for `App::middleware()` or -//! `Resource::middleware()` methods. But you have to use -//! `Cors::for_app()` method to support *preflight* OPTIONS request. -//! -//! -//! # Example -//! -//! ```rust -//! # extern crate actix_web; -//! use actix_web::middleware::cors::Cors; -//! use actix_web::{http, App, HttpRequest, HttpResponse}; -//! -//! fn index(mut req: HttpRequest) -> &'static str { -//! "Hello world" -//! } -//! -//! fn main() { -//! let app = App::new().configure(|app| { -//! Cors::for_app(app) // <- Construct CORS middleware builder -//! .allowed_origin("https://www.rust-lang.org/") -//! .allowed_methods(vec!["GET", "POST"]) -//! .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) -//! .allowed_header(http::header::CONTENT_TYPE) -//! .max_age(3600) -//! .resource("/index.html", |r| { -//! r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -//! r.method(http::Method::HEAD).f(|_| HttpResponse::MethodNotAllowed()); -//! }) -//! .register() -//! }); -//! } -//! ``` -//! In this example custom *CORS* middleware get registered for "/index.html" -//! endpoint. -//! -//! Cors middleware automatically handle *OPTIONS* preflight request. -use std::collections::HashSet; -use std::iter::FromIterator; -use std::rc::Rc; - -use http::header::{self, HeaderName, HeaderValue}; -use http::{self, HttpTryFrom, Method, StatusCode, Uri}; - -use application::App; -use error::{ResponseError, Result}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; -use resource::Resource; -use router::ResourceDef; -use server::Request; - -/// A set of errors that can occur during processing CORS -#[derive(Debug, Fail)] -pub enum CorsError { - /// The HTTP request header `Origin` is required but was not provided - #[fail( - display = "The HTTP request header `Origin` is required but was not provided" - )] - MissingOrigin, - /// The HTTP request header `Origin` could not be parsed correctly. - #[fail(display = "The HTTP request header `Origin` could not be parsed correctly.")] - BadOrigin, - /// The request header `Access-Control-Request-Method` is required but is - /// missing - #[fail( - display = "The request header `Access-Control-Request-Method` is required but is missing" - )] - MissingRequestMethod, - /// The request header `Access-Control-Request-Method` has an invalid value - #[fail( - display = "The request header `Access-Control-Request-Method` has an invalid value" - )] - BadRequestMethod, - /// The request header `Access-Control-Request-Headers` has an invalid - /// value - #[fail( - display = "The request header `Access-Control-Request-Headers` has an invalid value" - )] - BadRequestHeaders, - /// The request header `Access-Control-Request-Headers` is required but is - /// missing. - #[fail( - display = "The request header `Access-Control-Request-Headers` is required but is - missing" - )] - MissingRequestHeaders, - /// Origin is not allowed to make this request - #[fail(display = "Origin is not allowed to make this request")] - OriginNotAllowed, - /// Requested method is not allowed - #[fail(display = "Requested method is not allowed")] - MethodNotAllowed, - /// One or more headers requested are not allowed - #[fail(display = "One or more headers requested are not allowed")] - HeadersNotAllowed, -} - -impl ResponseError for CorsError { - fn error_response(&self) -> HttpResponse { - HttpResponse::with_body(StatusCode::BAD_REQUEST, format!("{}", self)) - } -} - -/// An enum signifying that some of type T is allowed, or `All` (everything is -/// allowed). -/// -/// `Default` is implemented for this enum and is `All`. -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum AllOrSome { - /// Everything is allowed. Usually equivalent to the "*" value. - All, - /// Only some of `T` is allowed - Some(T), -} - -impl Default for AllOrSome { - fn default() -> Self { - AllOrSome::All - } -} - -impl AllOrSome { - /// Returns whether this is an `All` variant - pub fn is_all(&self) -> bool { - match *self { - AllOrSome::All => true, - AllOrSome::Some(_) => false, - } - } - - /// Returns whether this is a `Some` variant - pub fn is_some(&self) -> bool { - !self.is_all() - } - - /// Returns &T - pub fn as_ref(&self) -> Option<&T> { - match *self { - AllOrSome::All => None, - AllOrSome::Some(ref t) => Some(t), - } - } -} - -/// `Middleware` for Cross-origin resource sharing support -/// -/// The Cors struct contains the settings for CORS requests to be validated and -/// for responses to be generated. -#[derive(Clone)] -pub struct Cors { - inner: Rc, -} - -struct Inner { - methods: HashSet, - origins: AllOrSome>, - origins_str: Option, - headers: AllOrSome>, - expose_hdrs: Option, - max_age: Option, - preflight: bool, - send_wildcard: bool, - supports_credentials: bool, - vary_header: bool, -} - -impl Default for Cors { - fn default() -> Cors { - let inner = Inner { - origins: AllOrSome::default(), - origins_str: None, - methods: HashSet::from_iter( - vec![ - Method::GET, - Method::HEAD, - Method::POST, - Method::OPTIONS, - Method::PUT, - Method::PATCH, - Method::DELETE, - ].into_iter(), - ), - headers: AllOrSome::All, - expose_hdrs: None, - max_age: None, - preflight: true, - send_wildcard: false, - supports_credentials: false, - vary_header: true, - }; - Cors { - inner: Rc::new(inner), - } - } -} - -impl Cors { - /// Build a new CORS middleware instance - pub fn build() -> CorsBuilder<()> { - CorsBuilder { - cors: Some(Inner { - origins: AllOrSome::All, - origins_str: None, - methods: HashSet::new(), - headers: AllOrSome::All, - expose_hdrs: None, - max_age: None, - preflight: true, - send_wildcard: false, - supports_credentials: false, - vary_header: true, - }), - methods: false, - error: None, - expose_hdrs: HashSet::new(), - resources: Vec::new(), - app: None, - } - } - - /// Create CorsBuilder for a specified application. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::middleware::cors::Cors; - /// use actix_web::{http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().configure( - /// |app| { - /// Cors::for_app(app) // <- Construct CORS builder - /// .allowed_origin("https://www.rust-lang.org/") - /// .resource("/resource", |r| { // register resource - /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); - /// }) - /// .register() - /// }, // construct CORS and return application instance - /// ); - /// } - /// ``` - pub fn for_app(app: App) -> CorsBuilder { - CorsBuilder { - cors: Some(Inner { - origins: AllOrSome::All, - origins_str: None, - methods: HashSet::new(), - headers: AllOrSome::All, - expose_hdrs: None, - max_age: None, - preflight: true, - send_wildcard: false, - supports_credentials: false, - vary_header: true, - }), - methods: false, - error: None, - expose_hdrs: HashSet::new(), - resources: Vec::new(), - app: Some(app), - } - } - - /// This method register cors middleware with resource and - /// adds route for *OPTIONS* preflight requests. - /// - /// It is possible to register *Cors* middleware with - /// `Resource::middleware()` method, but in that case *Cors* - /// middleware wont be able to handle *OPTIONS* requests. - pub fn register(self, resource: &mut Resource) { - resource - .method(Method::OPTIONS) - .h(|_: &_| HttpResponse::Ok()); - resource.middleware(self); - } - - fn validate_origin(&self, req: &Request) -> Result<(), CorsError> { - if let Some(hdr) = req.headers().get(header::ORIGIN) { - if let Ok(origin) = hdr.to_str() { - return match self.inner.origins { - AllOrSome::All => Ok(()), - AllOrSome::Some(ref allowed_origins) => allowed_origins - .get(origin) - .and_then(|_| Some(())) - .ok_or_else(|| CorsError::OriginNotAllowed), - }; - } - Err(CorsError::BadOrigin) - } else { - return match self.inner.origins { - AllOrSome::All => Ok(()), - _ => Err(CorsError::MissingOrigin), - }; - } - } - - fn validate_allowed_method(&self, req: &Request) -> Result<(), CorsError> { - if let Some(hdr) = req.headers().get(header::ACCESS_CONTROL_REQUEST_METHOD) { - if let Ok(meth) = hdr.to_str() { - if let Ok(method) = Method::try_from(meth) { - return self - .inner - .methods - .get(&method) - .and_then(|_| Some(())) - .ok_or_else(|| CorsError::MethodNotAllowed); - } - } - Err(CorsError::BadRequestMethod) - } else { - Err(CorsError::MissingRequestMethod) - } - } - - fn validate_allowed_headers(&self, req: &Request) -> Result<(), CorsError> { - match self.inner.headers { - AllOrSome::All => Ok(()), - AllOrSome::Some(ref allowed_headers) => { - if let Some(hdr) = - req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) - { - if let Ok(headers) = hdr.to_str() { - let mut hdrs = HashSet::new(); - for hdr in headers.split(',') { - match HeaderName::try_from(hdr.trim()) { - Ok(hdr) => hdrs.insert(hdr), - Err(_) => return Err(CorsError::BadRequestHeaders), - }; - } - - if !hdrs.is_empty() && !hdrs.is_subset(allowed_headers) { - return Err(CorsError::HeadersNotAllowed); - } - return Ok(()); - } - Err(CorsError::BadRequestHeaders) - } else { - Err(CorsError::MissingRequestHeaders) - } - } - } - } -} - -impl Middleware for Cors { - fn start(&self, req: &HttpRequest) -> Result { - if self.inner.preflight && Method::OPTIONS == *req.method() { - self.validate_origin(req)?; - self.validate_allowed_method(&req)?; - self.validate_allowed_headers(&req)?; - - // allowed headers - let headers = if let Some(headers) = self.inner.headers.as_ref() { - Some( - HeaderValue::try_from( - &headers - .iter() - .fold(String::new(), |s, v| s + "," + v.as_str()) - .as_str()[1..], - ).unwrap(), - ) - } else if let Some(hdr) = - req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) - { - Some(hdr.clone()) - } else { - None - }; - - Ok(Started::Response( - HttpResponse::Ok() - .if_some(self.inner.max_age.as_ref(), |max_age, resp| { - let _ = resp.header( - header::ACCESS_CONTROL_MAX_AGE, - format!("{}", max_age).as_str(), - ); - }).if_some(headers, |headers, resp| { - let _ = - resp.header(header::ACCESS_CONTROL_ALLOW_HEADERS, headers); - }).if_true(self.inner.origins.is_all(), |resp| { - if self.inner.send_wildcard { - resp.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - } else { - let origin = req.headers().get(header::ORIGIN).unwrap(); - resp.header( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - origin.clone(), - ); - } - }).if_true(self.inner.origins.is_some(), |resp| { - resp.header( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - self.inner.origins_str.as_ref().unwrap().clone(), - ); - }).if_true(self.inner.supports_credentials, |resp| { - resp.header(header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - }).header( - header::ACCESS_CONTROL_ALLOW_METHODS, - &self - .inner - .methods - .iter() - .fold(String::new(), |s, v| s + "," + v.as_str()) - .as_str()[1..], - ).finish(), - )) - } else { - // Only check requests with a origin header. - if req.headers().contains_key(header::ORIGIN) { - self.validate_origin(req)?; - } - - Ok(Started::Done) - } - } - - fn response( - &self, req: &HttpRequest, mut resp: HttpResponse, - ) -> Result { - match self.inner.origins { - AllOrSome::All => { - if self.inner.send_wildcard { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - HeaderValue::from_static("*"), - ); - } else if let Some(origin) = req.headers().get(header::ORIGIN) { - resp.headers_mut() - .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone()); - } - } - AllOrSome::Some(ref origins) => { - if let Some(origin) = req.headers().get(header::ORIGIN).filter(|o| { - match o.to_str() { - Ok(os) => origins.contains(os), - _ => false - } - }) { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - origin.clone(), - ); - } else { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - self.inner.origins_str.as_ref().unwrap().clone() - ); - }; - } - } - - if let Some(ref expose) = self.inner.expose_hdrs { - resp.headers_mut().insert( - header::ACCESS_CONTROL_EXPOSE_HEADERS, - HeaderValue::try_from(expose.as_str()).unwrap(), - ); - } - if self.inner.supports_credentials { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_CREDENTIALS, - HeaderValue::from_static("true"), - ); - } - if self.inner.vary_header { - let value = if let Some(hdr) = resp.headers_mut().get(header::VARY) { - let mut val: Vec = Vec::with_capacity(hdr.as_bytes().len() + 8); - val.extend(hdr.as_bytes()); - val.extend(b", Origin"); - HeaderValue::try_from(&val[..]).unwrap() - } else { - HeaderValue::from_static("Origin") - }; - resp.headers_mut().insert(header::VARY, value); - } - Ok(Response::Done(resp)) - } -} - -/// Structure that follows the builder pattern for building `Cors` middleware -/// structs. -/// -/// To construct a cors: -/// -/// 1. Call [`Cors::build`](struct.Cors.html#method.build) to start building. -/// 2. Use any of the builder methods to set fields in the backend. -/// 3. Call [finish](struct.Cors.html#method.finish) to retrieve the -/// constructed backend. -/// -/// # Example -/// -/// ```rust -/// # extern crate http; -/// # extern crate actix_web; -/// use actix_web::middleware::cors; -/// use http::header; -/// -/// # fn main() { -/// let cors = cors::Cors::build() -/// .allowed_origin("https://www.rust-lang.org/") -/// .allowed_methods(vec!["GET", "POST"]) -/// .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) -/// .allowed_header(header::CONTENT_TYPE) -/// .max_age(3600) -/// .finish(); -/// # } -/// ``` -pub struct CorsBuilder { - cors: Option, - methods: bool, - error: Option, - expose_hdrs: HashSet, - resources: Vec>, - app: Option>, -} - -fn cors<'a>( - parts: &'a mut Option, err: &Option, -) -> Option<&'a mut Inner> { - if err.is_some() { - return None; - } - parts.as_mut() -} - -impl CorsBuilder { - /// Add an origin that are allowed to make requests. - /// Will be verified against the `Origin` request header. - /// - /// When `All` is set, and `send_wildcard` is set, "*" will be sent in - /// the `Access-Control-Allow-Origin` response header. Otherwise, the - /// client's `Origin` request header will be echoed back in the - /// `Access-Control-Allow-Origin` response header. - /// - /// When `Some` is set, the client's `Origin` request header will be - /// checked in a case-sensitive manner. - /// - /// This is the `list of origins` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// Defaults to `All`. - /// - /// Builder panics if supplied origin is not valid uri. - pub fn allowed_origin(&mut self, origin: &str) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - match Uri::try_from(origin) { - Ok(_) => { - if cors.origins.is_all() { - cors.origins = AllOrSome::Some(HashSet::new()); - } - if let AllOrSome::Some(ref mut origins) = cors.origins { - origins.insert(origin.to_owned()); - } - } - Err(e) => { - self.error = Some(e.into()); - } - } - } - self - } - - /// Set a list of methods which the allowed origins are allowed to access - /// for requests. - /// - /// This is the `list of methods` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// Defaults to `[GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE]` - pub fn allowed_methods(&mut self, methods: U) -> &mut CorsBuilder - where - U: IntoIterator, - Method: HttpTryFrom, - { - self.methods = true; - if let Some(cors) = cors(&mut self.cors, &self.error) { - for m in methods { - match Method::try_from(m) { - Ok(method) => { - cors.methods.insert(method); - } - Err(e) => { - self.error = Some(e.into()); - break; - } - } - } - } - self - } - - /// Set an allowed header - pub fn allowed_header(&mut self, header: H) -> &mut CorsBuilder - where - HeaderName: HttpTryFrom, - { - if let Some(cors) = cors(&mut self.cors, &self.error) { - match HeaderName::try_from(header) { - Ok(method) => { - if cors.headers.is_all() { - cors.headers = AllOrSome::Some(HashSet::new()); - } - if let AllOrSome::Some(ref mut headers) = cors.headers { - headers.insert(method); - } - } - Err(e) => self.error = Some(e.into()), - } - } - self - } - - /// Set a list of header field names which can be used when - /// this resource is accessed by allowed origins. - /// - /// If `All` is set, whatever is requested by the client in - /// `Access-Control-Request-Headers` will be echoed back in the - /// `Access-Control-Allow-Headers` header. - /// - /// This is the `list of headers` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// Defaults to `All`. - pub fn allowed_headers(&mut self, headers: U) -> &mut CorsBuilder - where - U: IntoIterator, - HeaderName: HttpTryFrom, - { - if let Some(cors) = cors(&mut self.cors, &self.error) { - for h in headers { - match HeaderName::try_from(h) { - Ok(method) => { - if cors.headers.is_all() { - cors.headers = AllOrSome::Some(HashSet::new()); - } - if let AllOrSome::Some(ref mut headers) = cors.headers { - headers.insert(method); - } - } - Err(e) => { - self.error = Some(e.into()); - break; - } - } - } - } - self - } - - /// Set a list of headers which are safe to expose to the API of a CORS API - /// specification. This corresponds to the - /// `Access-Control-Expose-Headers` response header. - /// - /// This is the `list of exposed headers` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// This defaults to an empty set. - pub fn expose_headers(&mut self, headers: U) -> &mut CorsBuilder - where - U: IntoIterator, - HeaderName: HttpTryFrom, - { - for h in headers { - match HeaderName::try_from(h) { - Ok(method) => { - self.expose_hdrs.insert(method); - } - Err(e) => { - self.error = Some(e.into()); - break; - } - } - } - self - } - - /// Set a maximum time for which this CORS request maybe cached. - /// This value is set as the `Access-Control-Max-Age` header. - /// - /// This defaults to `None` (unset). - pub fn max_age(&mut self, max_age: usize) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.max_age = Some(max_age) - } - self - } - - /// Set a wildcard origins - /// - /// If send wildcard is set and the `allowed_origins` parameter is `All`, a - /// wildcard `Access-Control-Allow-Origin` response header is sent, - /// rather than the request’s `Origin` header. - /// - /// This is the `supports credentials flag` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// This **CANNOT** be used in conjunction with `allowed_origins` set to - /// `All` and `allow_credentials` set to `true`. Depending on the mode - /// of usage, this will either result in an `Error:: - /// CredentialsWithWildcardOrigin` error during actix launch or runtime. - /// - /// Defaults to `false`. - pub fn send_wildcard(&mut self) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.send_wildcard = true - } - self - } - - /// Allows users to make authenticated requests - /// - /// If true, injects the `Access-Control-Allow-Credentials` header in - /// responses. This allows cookies and credentials to be submitted - /// across domains. - /// - /// This option cannot be used in conjunction with an `allowed_origin` set - /// to `All` and `send_wildcards` set to `true`. - /// - /// Defaults to `false`. - /// - /// Builder panics if credentials are allowed, but the Origin is set to "*". - /// This is not allowed by W3C - pub fn supports_credentials(&mut self) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.supports_credentials = true - } - self - } - - /// Disable `Vary` header support. - /// - /// When enabled the header `Vary: Origin` will be returned as per the W3 - /// implementation guidelines. - /// - /// Setting this header when the `Access-Control-Allow-Origin` is - /// dynamically generated (e.g. when there is more than one allowed - /// origin, and an Origin than '*' is returned) informs CDNs and other - /// caches that the CORS headers are dynamic, and cannot be cached. - /// - /// By default `vary` header support is enabled. - pub fn disable_vary_header(&mut self) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.vary_header = false - } - self - } - - /// Disable *preflight* request support. - /// - /// When enabled cors middleware automatically handles *OPTIONS* request. - /// This is useful application level middleware. - /// - /// By default *preflight* support is enabled. - pub fn disable_preflight(&mut self) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.preflight = false - } - self - } - - /// Configure resource for a specific path. - /// - /// This is similar to a `App::resource()` method. Except, cors middleware - /// get registered for the resource. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::middleware::cors::Cors; - /// use actix_web::{http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().configure( - /// |app| { - /// Cors::for_app(app) // <- Construct CORS builder - /// .allowed_origin("https://www.rust-lang.org/") - /// .allowed_methods(vec!["GET", "POST"]) - /// .allowed_header(http::header::CONTENT_TYPE) - /// .max_age(3600) - /// .resource("/resource1", |r| { // register resource - /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); - /// }) - /// .resource("/resource2", |r| { // register another resource - /// r.method(http::Method::HEAD) - /// .f(|_| HttpResponse::MethodNotAllowed()); - /// }) - /// .register() - /// }, // construct CORS and return application instance - /// ); - /// } - /// ``` - pub fn resource(&mut self, path: &str, f: F) -> &mut CorsBuilder - where - F: FnOnce(&mut Resource) -> R + 'static, - { - // add resource handler - let mut resource = Resource::new(ResourceDef::new(path)); - f(&mut resource); - - self.resources.push(resource); - self - } - - fn construct(&mut self) -> Cors { - if !self.methods { - self.allowed_methods(vec![ - Method::GET, - Method::HEAD, - Method::POST, - Method::OPTIONS, - Method::PUT, - Method::PATCH, - Method::DELETE, - ]); - } - - if let Some(e) = self.error.take() { - panic!("{}", e); - } - - let mut cors = self.cors.take().expect("cannot reuse CorsBuilder"); - - if cors.supports_credentials && cors.send_wildcard && cors.origins.is_all() { - panic!("Credentials are allowed, but the Origin is set to \"*\""); - } - - if let AllOrSome::Some(ref origins) = cors.origins { - let s = origins - .iter() - .fold(String::new(), |s, v| format!("{}, {}", s, v)); - cors.origins_str = Some(HeaderValue::try_from(&s[2..]).unwrap()); - } - - if !self.expose_hdrs.is_empty() { - cors.expose_hdrs = Some( - self.expose_hdrs - .iter() - .fold(String::new(), |s, v| format!("{}, {}", s, v.as_str()))[2..] - .to_owned(), - ); - } - Cors { - inner: Rc::new(cors), - } - } - - /// Finishes building and returns the built `Cors` instance. - /// - /// This method panics in case of any configuration error. - pub fn finish(&mut self) -> Cors { - if !self.resources.is_empty() { - panic!( - "CorsBuilder::resource() was used, - to construct CORS `.register(app)` method should be used" - ); - } - self.construct() - } - - /// Finishes building Cors middleware and register middleware for - /// application - /// - /// This method panics in case of any configuration error or if non of - /// resources are registered. - pub fn register(&mut self) -> App { - if self.resources.is_empty() { - panic!("No resources are registered."); - } - - let cors = self.construct(); - let mut app = self - .app - .take() - .expect("CorsBuilder has to be constructed with Cors::for_app(app)"); - - // register resources - for mut resource in self.resources.drain(..) { - cors.clone().register(&mut resource); - app.register_resource(resource); - } - - app - } -} - -#[cfg(test)] -mod tests { - use super::*; - use test::{self, TestRequest}; - - impl Started { - fn is_done(&self) -> bool { - match *self { - Started::Done => true, - _ => false, - } - } - fn response(self) -> HttpResponse { - match self { - Started::Response(resp) => resp, - _ => panic!(), - } - } - } - impl Response { - fn response(self) -> HttpResponse { - match self { - Response::Done(resp) => resp, - _ => panic!(), - } - } - } - - #[test] - #[should_panic(expected = "Credentials are allowed, but the Origin is set to")] - fn cors_validates_illegal_allow_credentials() { - Cors::build() - .supports_credentials() - .send_wildcard() - .finish(); - } - - #[test] - #[should_panic(expected = "No resources are registered")] - fn no_resource() { - Cors::build() - .supports_credentials() - .send_wildcard() - .register(); - } - - #[test] - #[should_panic(expected = "Cors::for_app(app)")] - fn no_resource2() { - Cors::build() - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .register(); - } - - #[test] - fn validate_origin_allows_all_origins() { - let cors = Cors::default(); - let req = TestRequest::with_header("Origin", "https://www.example.com").finish(); - - assert!(cors.start(&req).ok().unwrap().is_done()) - } - - #[test] - fn test_preflight() { - let mut cors = Cors::build() - .send_wildcard() - .max_age(3600) - .allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST]) - .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) - .allowed_header(header::CONTENT_TYPE) - .finish(); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .method(Method::OPTIONS) - .finish(); - - assert!(cors.start(&req).is_err()); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .header(header::ACCESS_CONTROL_REQUEST_METHOD, "put") - .method(Method::OPTIONS) - .finish(); - - assert!(cors.start(&req).is_err()); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST") - .header( - header::ACCESS_CONTROL_REQUEST_HEADERS, - "AUTHORIZATION,ACCEPT", - ).method(Method::OPTIONS) - .finish(); - - let resp = cors.start(&req).unwrap().response(); - assert_eq!( - &b"*"[..], - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .as_bytes() - ); - assert_eq!( - &b"3600"[..], - resp.headers() - .get(header::ACCESS_CONTROL_MAX_AGE) - .unwrap() - .as_bytes() - ); - //assert_eq!( - // &b"authorization,accept,content-type"[..], - // resp.headers().get(header::ACCESS_CONTROL_ALLOW_HEADERS).unwrap(). - // as_bytes()); assert_eq!( - // &b"POST,GET,OPTIONS"[..], - // resp.headers().get(header::ACCESS_CONTROL_ALLOW_METHODS).unwrap(). - // as_bytes()); - - Rc::get_mut(&mut cors.inner).unwrap().preflight = false; - assert!(cors.start(&req).unwrap().is_done()); - } - - // #[test] - // #[should_panic(expected = "MissingOrigin")] - // fn test_validate_missing_origin() { - // let cors = Cors::build() - // .allowed_origin("https://www.example.com") - // .finish(); - // let mut req = HttpRequest::default(); - // cors.start(&req).unwrap(); - // } - - #[test] - #[should_panic(expected = "OriginNotAllowed")] - fn test_validate_not_allowed_origin() { - let cors = Cors::build() - .allowed_origin("https://www.example.com") - .finish(); - - let req = TestRequest::with_header("Origin", "https://www.unknown.com") - .method(Method::GET) - .finish(); - cors.start(&req).unwrap(); - } - - #[test] - fn test_validate_origin() { - let cors = Cors::build() - .allowed_origin("https://www.example.com") - .finish(); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .method(Method::GET) - .finish(); - - assert!(cors.start(&req).unwrap().is_done()); - } - - #[test] - fn test_no_origin_response() { - let cors = Cors::build().finish(); - - let req = TestRequest::default().method(Method::GET).finish(); - let resp: HttpResponse = HttpResponse::Ok().into(); - let resp = cors.response(&req, resp).unwrap().response(); - assert!( - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .is_none() - ); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .method(Method::OPTIONS) - .finish(); - let resp = cors.response(&req, resp).unwrap().response(); - assert_eq!( - &b"https://www.example.com"[..], - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .as_bytes() - ); - } - - #[test] - fn test_response() { - let exposed_headers = vec![header::AUTHORIZATION, header::ACCEPT]; - let cors = Cors::build() - .send_wildcard() - .disable_preflight() - .max_age(3600) - .allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST]) - .allowed_headers(exposed_headers.clone()) - .expose_headers(exposed_headers.clone()) - .allowed_header(header::CONTENT_TYPE) - .finish(); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .method(Method::OPTIONS) - .finish(); - - let resp: HttpResponse = HttpResponse::Ok().into(); - let resp = cors.response(&req, resp).unwrap().response(); - assert_eq!( - &b"*"[..], - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .as_bytes() - ); - assert_eq!( - &b"Origin"[..], - resp.headers().get(header::VARY).unwrap().as_bytes() - ); - - { - let headers = resp - .headers() - .get(header::ACCESS_CONTROL_EXPOSE_HEADERS) - .unwrap() - .to_str() - .unwrap() - .split(',') - .map(|s| s.trim()) - .collect::>(); - - for h in exposed_headers { - assert!(headers.contains(&h.as_str())); - } - } - - let resp: HttpResponse = - HttpResponse::Ok().header(header::VARY, "Accept").finish(); - let resp = cors.response(&req, resp).unwrap().response(); - assert_eq!( - &b"Accept, Origin"[..], - resp.headers().get(header::VARY).unwrap().as_bytes() - ); - - let cors = Cors::build() - .disable_vary_header() - .allowed_origin("https://www.example.com") - .allowed_origin("https://www.google.com") - .finish(); - let resp: HttpResponse = HttpResponse::Ok().into(); - let resp = cors.response(&req, resp).unwrap().response(); - - let origins_str = resp - .headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!( - "https://www.example.com", - origins_str - ); - } - - #[test] - fn cors_resource() { - let mut srv = test::TestServer::with_factory(|| { - App::new().configure(|app| { - Cors::for_app(app) - .allowed_origin("https://www.example.com") - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .register() - }) - }); - - let request = srv - .get() - .uri(srv.url("/test")) - .header("ORIGIN", "https://www.example2.com") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - let request = srv - .get() - .uri(srv.url("/test")) - .header("ORIGIN", "https://www.example.com") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - } - - #[test] - fn test_multiple_origins() { - let cors = Cors::build() - .allowed_origin("https://example.com") - .allowed_origin("https://example.org") - .allowed_methods(vec![Method::GET]) - .finish(); - - - let req = TestRequest::with_header("Origin", "https://example.com") - .method(Method::GET) - .finish(); - let resp: HttpResponse = HttpResponse::Ok().into(); - - let resp = cors.response(&req, resp).unwrap().response(); - print!("{:?}", resp); - assert_eq!( - &b"https://example.com"[..], - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .as_bytes() - ); - - let req = TestRequest::with_header("Origin", "https://example.org") - .method(Method::GET) - .finish(); - let resp: HttpResponse = HttpResponse::Ok().into(); - - let resp = cors.response(&req, resp).unwrap().response(); - assert_eq!( - &b"https://example.org"[..], - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .as_bytes() - ); - } -} diff --git a/src/middleware/csrf.rs b/src/middleware/csrf.rs deleted file mode 100644 index cacfc8d53..000000000 --- a/src/middleware/csrf.rs +++ /dev/null @@ -1,275 +0,0 @@ -//! A filter for cross-site request forgery (CSRF). -//! -//! This middleware is stateless and [based on request -//! headers](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Verifying_Same_Origin_with_Standard_Headers). -//! -//! By default requests are allowed only if one of these is true: -//! -//! * The request method is safe (`GET`, `HEAD`, `OPTIONS`). It is the -//! applications responsibility to ensure these methods cannot be used to -//! execute unwanted actions. Note that upgrade requests for websockets are -//! also considered safe. -//! * The `Origin` header (added automatically by the browser) matches one -//! of the allowed origins. -//! * There is no `Origin` header but the `Referer` header matches one of -//! the allowed origins. -//! -//! Use [`CsrfFilter::allow_xhr()`](struct.CsrfFilter.html#method.allow_xhr) -//! if you want to allow requests with unprotected methods via -//! [CORS](../cors/struct.Cors.html). -//! -//! # Example -//! -//! ``` -//! # extern crate actix_web; -//! use actix_web::middleware::csrf; -//! use actix_web::{http, App, HttpRequest, HttpResponse}; -//! -//! fn handle_post(_: &HttpRequest) -> &'static str { -//! "This action should only be triggered with requests from the same site" -//! } -//! -//! fn main() { -//! let app = App::new() -//! .middleware( -//! csrf::CsrfFilter::new().allowed_origin("https://www.example.com"), -//! ) -//! .resource("/", |r| { -//! r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -//! r.method(http::Method::POST).f(handle_post); -//! }) -//! .finish(); -//! } -//! ``` -//! -//! In this example the entire application is protected from CSRF. - -use std::borrow::Cow; -use std::collections::HashSet; - -use bytes::Bytes; -use error::{ResponseError, Result}; -use http::{header, HeaderMap, HttpTryFrom, Uri}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Started}; -use server::Request; - -/// Potential cross-site request forgery detected. -#[derive(Debug, Fail)] -pub enum CsrfError { - /// The HTTP request header `Origin` was required but not provided. - #[fail(display = "Origin header required")] - MissingOrigin, - /// The HTTP request header `Origin` could not be parsed correctly. - #[fail(display = "Could not parse Origin header")] - BadOrigin, - /// The cross-site request was denied. - #[fail(display = "Cross-site request denied")] - CsrDenied, -} - -impl ResponseError for CsrfError { - fn error_response(&self) -> HttpResponse { - HttpResponse::Forbidden().body(self.to_string()) - } -} - -fn uri_origin(uri: &Uri) -> Option { - match (uri.scheme_part(), uri.host(), uri.port_part().map(|port| port.as_u16())) { - (Some(scheme), Some(host), Some(port)) => { - Some(format!("{}://{}:{}", scheme, host, port)) - } - (Some(scheme), Some(host), None) => Some(format!("{}://{}", scheme, host)), - _ => None, - } -} - -fn origin(headers: &HeaderMap) -> Option, CsrfError>> { - headers - .get(header::ORIGIN) - .map(|origin| { - origin - .to_str() - .map_err(|_| CsrfError::BadOrigin) - .map(|o| o.into()) - }).or_else(|| { - headers.get(header::REFERER).map(|referer| { - Uri::try_from(Bytes::from(referer.as_bytes())) - .ok() - .as_ref() - .and_then(uri_origin) - .ok_or(CsrfError::BadOrigin) - .map(|o| o.into()) - }) - }) -} - -/// A middleware that filters cross-site requests. -/// -/// To construct a CSRF filter: -/// -/// 1. Call [`CsrfFilter::build`](struct.CsrfFilter.html#method.build) to -/// start building. -/// 2. [Add](struct.CsrfFilterBuilder.html#method.allowed_origin) allowed -/// origins. -/// 3. Call [finish](struct.CsrfFilterBuilder.html#method.finish) to retrieve -/// the constructed filter. -/// -/// # Example -/// -/// ``` -/// use actix_web::middleware::csrf; -/// use actix_web::App; -/// -/// # fn main() { -/// let app = App::new() -/// .middleware(csrf::CsrfFilter::new().allowed_origin("https://www.example.com")); -/// # } -/// ``` -#[derive(Default)] -pub struct CsrfFilter { - origins: HashSet, - allow_xhr: bool, - allow_missing_origin: bool, - allow_upgrade: bool, -} - -impl CsrfFilter { - /// Start building a `CsrfFilter`. - pub fn new() -> CsrfFilter { - CsrfFilter { - origins: HashSet::new(), - allow_xhr: false, - allow_missing_origin: false, - allow_upgrade: false, - } - } - - /// Add an origin that is allowed to make requests. Will be verified - /// against the `Origin` request header. - pub fn allowed_origin>(mut self, origin: T) -> CsrfFilter { - self.origins.insert(origin.into()); - self - } - - /// Allow all requests with an `X-Requested-With` header. - /// - /// A cross-site attacker should not be able to send requests with custom - /// headers unless a CORS policy whitelists them. Therefore it should be - /// safe to allow requests with an `X-Requested-With` header (added - /// automatically by many JavaScript libraries). - /// - /// This is disabled by default, because in Safari it is possible to - /// circumvent this using redirects and Flash. - /// - /// Use this method to enable more lax filtering. - pub fn allow_xhr(mut self) -> CsrfFilter { - self.allow_xhr = true; - self - } - - /// Allow requests if the expected `Origin` header is missing (and - /// there is no `Referer` to fall back on). - /// - /// The filter is conservative by default, but it should be safe to allow - /// missing `Origin` headers because a cross-site attacker cannot prevent - /// the browser from sending `Origin` on unprotected requests. - pub fn allow_missing_origin(mut self) -> CsrfFilter { - self.allow_missing_origin = true; - self - } - - /// Allow cross-site upgrade requests (for example to open a WebSocket). - pub fn allow_upgrade(mut self) -> CsrfFilter { - self.allow_upgrade = true; - self - } - - fn validate(&self, req: &Request) -> Result<(), CsrfError> { - let is_upgrade = req.headers().contains_key(header::UPGRADE); - let is_safe = req.method().is_safe() && (self.allow_upgrade || !is_upgrade); - - if is_safe || (self.allow_xhr && req.headers().contains_key("x-requested-with")) - { - Ok(()) - } else if let Some(header) = origin(req.headers()) { - match header { - Ok(ref origin) if self.origins.contains(origin.as_ref()) => Ok(()), - Ok(_) => Err(CsrfError::CsrDenied), - Err(err) => Err(err), - } - } else if self.allow_missing_origin { - Ok(()) - } else { - Err(CsrfError::MissingOrigin) - } - } -} - -impl Middleware for CsrfFilter { - fn start(&self, req: &HttpRequest) -> Result { - self.validate(req)?; - Ok(Started::Done) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use http::Method; - use test::TestRequest; - - #[test] - fn test_safe() { - let csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); - - let req = TestRequest::with_header("Origin", "https://www.w3.org") - .method(Method::HEAD) - .finish(); - - assert!(csrf.start(&req).is_ok()); - } - - #[test] - fn test_csrf() { - let csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); - - let req = TestRequest::with_header("Origin", "https://www.w3.org") - .method(Method::POST) - .finish(); - - assert!(csrf.start(&req).is_err()); - } - - #[test] - fn test_referer() { - let csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); - - let req = TestRequest::with_header( - "Referer", - "https://www.example.com/some/path?query=param", - ).method(Method::POST) - .finish(); - - assert!(csrf.start(&req).is_ok()); - } - - #[test] - fn test_upgrade() { - let strict_csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); - - let lax_csrf = CsrfFilter::new() - .allowed_origin("https://www.example.com") - .allow_upgrade(); - - let req = TestRequest::with_header("Origin", "https://cswsh.com") - .header("Connection", "Upgrade") - .header("Upgrade", "websocket") - .method(Method::GET) - .finish(); - - assert!(strict_csrf.start(&req).is_err()); - assert!(lax_csrf.start(&req).is_ok()); - } -} diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index a33fa6a33..1ace34fe8 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -1,17 +1,19 @@ -//! Default response headers -use http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; -use http::{HeaderMap, HttpTryFrom}; +//! Middleware for setting default response headers +use std::rc::Rc; -use error::Result; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response}; +use actix_http::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; +use actix_http::http::{HeaderMap, HttpTryFrom}; +use actix_service::{IntoNewTransform, Service, Transform}; +use futures::{Async, Future, Poll}; + +use crate::middleware::MiddlewareFactory; +use crate::service::{ServiceRequest, ServiceResponse}; /// `Middleware` for setting default response headers. /// /// This middleware does not set header if response headers already contains it. /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// use actix_web::{http, middleware, App, HttpResponse}; /// @@ -22,11 +24,15 @@ use middleware::{Middleware, Response}; /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); /// r.method(http::Method::HEAD) /// .f(|_| HttpResponse::MethodNotAllowed()); -/// }) -/// .finish(); +/// }); /// } /// ``` +#[derive(Clone)] pub struct DefaultHeaders { + inner: Rc, +} + +struct Inner { ct: bool, headers: HeaderMap, } @@ -34,8 +40,10 @@ pub struct DefaultHeaders { impl Default for DefaultHeaders { fn default() -> Self { DefaultHeaders { - ct: false, - headers: HeaderMap::new(), + inner: Rc::new(Inner { + ct: false, + headers: HeaderMap::new(), + }), } } } @@ -48,16 +56,19 @@ impl DefaultHeaders { /// Set a header. #[inline] - #[cfg_attr(feature = "cargo-clippy", allow(match_wild_err_arm))] pub fn header(mut self, key: K, value: V) -> Self where HeaderName: HttpTryFrom, HeaderValue: HttpTryFrom, { + #[allow(clippy::match_wild_err_arm)] match HeaderName::try_from(key) { Ok(key) => match HeaderValue::try_from(value) { Ok(value) => { - self.headers.append(key, value); + Rc::get_mut(&mut self.inner) + .expect("Multiple copies exist") + .headers + .append(key, value); } Err(_) => panic!("Can not create header value"), }, @@ -68,53 +79,85 @@ impl DefaultHeaders { /// Set *CONTENT-TYPE* header if response does not contain this header. pub fn content_type(mut self) -> Self { - self.ct = true; + Rc::get_mut(&mut self.inner) + .expect("Multiple copies exist") + .ct = true; self } } -impl Middleware for DefaultHeaders { - fn response(&self, _: &HttpRequest, mut resp: HttpResponse) -> Result { - for (key, value) in self.headers.iter() { - if !resp.headers().contains_key(key) { - resp.headers_mut().insert(key, value.clone()); +impl IntoNewTransform, S> + for DefaultHeaders +where + S: Service, Response = ServiceResponse>, + S::Future: 'static, +{ + fn into_new_transform(self) -> MiddlewareFactory { + MiddlewareFactory::new(self) + } +} + +impl Transform for DefaultHeaders +where + S: Service, Response = ServiceResponse>, + S::Future: 'static, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = S::Error; + type Future = Box>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: ServiceRequest, srv: &mut S) -> Self::Future { + let inner = self.inner.clone(); + + Box::new(srv.call(req).map(move |mut res| { + // set response headers + for (key, value) in inner.headers.iter() { + if !res.headers().contains_key(key) { + res.headers_mut().insert(key, value.clone()); + } } - } - // default content-type - if self.ct && !resp.headers().contains_key(CONTENT_TYPE) { - resp.headers_mut().insert( - CONTENT_TYPE, - HeaderValue::from_static("application/octet-stream"), - ); - } - Ok(Response::Done(resp)) + // default content-type + if inner.ct && !res.headers().contains_key(CONTENT_TYPE) { + res.headers_mut().insert( + CONTENT_TYPE, + HeaderValue::from_static("application/octet-stream"), + ); + } + + res + })) } } -#[cfg(test)] -mod tests { - use super::*; - use http::header::CONTENT_TYPE; - use test::TestRequest; +// #[cfg(test)] +// mod tests { +// use super::*; +// use actix_http::http::header::CONTENT_TYPE; +// use actix_http::test::TestRequest; - #[test] - fn test_default_headers() { - let mw = DefaultHeaders::new().header(CONTENT_TYPE, "0001"); +// #[test] +// fn test_default_headers() { +// let mw = DefaultHeaders::new().header(CONTENT_TYPE, "0001"); - let req = TestRequest::default().finish(); +// let req = TestRequest::default().finish(); - let resp = HttpResponse::Ok().finish(); - let resp = match mw.response(&req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); +// let resp = Response::Ok().finish(); +// let resp = match mw.response(&req, resp) { +// Ok(Response::Done(resp)) => resp, +// _ => panic!(), +// }; +// assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - let resp = HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish(); - let resp = match mw.response(&req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); - } -} +// let resp = Response::Ok().header(CONTENT_TYPE, "0002").finish(); +// let resp = match mw.response(&req, resp) { +// Ok(Response::Done(resp)) => resp, +// _ => panic!(), +// }; +// assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); +// } +// } diff --git a/src/middleware/errhandlers.rs b/src/middleware/errhandlers.rs deleted file mode 100644 index c7d19d334..000000000 --- a/src/middleware/errhandlers.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::collections::HashMap; - -use error::Result; -use http::StatusCode; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response}; - -type ErrorHandler = Fn(&HttpRequest, HttpResponse) -> Result; - -/// `Middleware` for allowing custom handlers for responses. -/// -/// You can use `ErrorHandlers::handler()` method to register a custom error -/// handler for specific status code. You can modify existing response or -/// create completely new one. -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::{ErrorHandlers, Response}; -/// use actix_web::{http, App, HttpRequest, HttpResponse, Result}; -/// -/// fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { -/// let mut builder = resp.into_builder(); -/// builder.header(http::header::CONTENT_TYPE, "application/json"); -/// Ok(Response::Done(builder.into())) -/// } -/// -/// fn main() { -/// let app = App::new() -/// .middleware( -/// ErrorHandlers::new() -/// .handler(http::StatusCode::INTERNAL_SERVER_ERROR, render_500), -/// ) -/// .resource("/test", |r| { -/// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -/// r.method(http::Method::HEAD) -/// .f(|_| HttpResponse::MethodNotAllowed()); -/// }) -/// .finish(); -/// } -/// ``` -pub struct ErrorHandlers { - handlers: HashMap>>, -} - -impl Default for ErrorHandlers { - fn default() -> Self { - ErrorHandlers { - handlers: HashMap::new(), - } - } -} - -impl ErrorHandlers { - /// Construct new `ErrorHandlers` instance - pub fn new() -> Self { - ErrorHandlers::default() - } - - /// Register error handler for specified status code - pub fn handler(mut self, status: StatusCode, handler: F) -> Self - where - F: Fn(&HttpRequest, HttpResponse) -> Result + 'static, - { - self.handlers.insert(status, Box::new(handler)); - self - } -} - -impl Middleware for ErrorHandlers { - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(handler) = self.handlers.get(&resp.status()) { - handler(req, resp) - } else { - Ok(Response::Done(resp)) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use error::{Error, ErrorInternalServerError}; - use http::header::CONTENT_TYPE; - use http::StatusCode; - use httpmessage::HttpMessage; - use middleware::Started; - use test::{self, TestRequest}; - - fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { - let mut builder = resp.into_builder(); - builder.header(CONTENT_TYPE, "0001"); - Ok(Response::Done(builder.into())) - } - - #[test] - fn test_handler() { - let mw = - ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); - - let mut req = TestRequest::default().finish(); - let resp = HttpResponse::InternalServerError().finish(); - let resp = match mw.response(&mut req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - - let resp = HttpResponse::Ok().finish(); - let resp = match mw.response(&mut req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert!(!resp.headers().contains_key(CONTENT_TYPE)); - } - - struct MiddlewareOne; - - impl Middleware for MiddlewareOne { - fn start(&self, _: &HttpRequest) -> Result { - Err(ErrorInternalServerError("middleware error")) - } - } - - #[test] - fn test_middleware_start_error() { - let mut srv = test::TestServer::new(move |app| { - app.middleware( - ErrorHandlers::new() - .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), - ).middleware(MiddlewareOne) - .handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "0001"); - } -} diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs deleted file mode 100644 index a664ba1f0..000000000 --- a/src/middleware/identity.rs +++ /dev/null @@ -1,399 +0,0 @@ -//! Request identity service for Actix applications. -//! -//! [**IdentityService**](struct.IdentityService.html) middleware can be -//! used with different policies types to store identity information. -//! -//! By default, only cookie identity policy is implemented. Other backend -//! implementations can be added separately. -//! -//! [**CookieIdentityPolicy**](struct.CookieIdentityPolicy.html) -//! uses cookies as identity storage. -//! -//! To access current request identity -//! [**RequestIdentity**](trait.RequestIdentity.html) should be used. -//! *HttpRequest* implements *RequestIdentity* trait. -//! -//! ```rust -//! use actix_web::middleware::identity::RequestIdentity; -//! use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; -//! use actix_web::*; -//! -//! fn index(req: HttpRequest) -> Result { -//! // access request identity -//! if let Some(id) = req.identity() { -//! Ok(format!("Welcome! {}", id)) -//! } else { -//! Ok("Welcome Anonymous!".to_owned()) -//! } -//! } -//! -//! fn login(mut req: HttpRequest) -> HttpResponse { -//! req.remember("User1".to_owned()); // <- remember identity -//! HttpResponse::Ok().finish() -//! } -//! -//! fn logout(mut req: HttpRequest) -> HttpResponse { -//! req.forget(); // <- remove identity -//! HttpResponse::Ok().finish() -//! } -//! -//! fn main() { -//! let app = App::new().middleware(IdentityService::new( -//! // <- create identity middleware -//! CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend -//! .name("auth-cookie") -//! .secure(false), -//! )); -//! } -//! ``` -use std::rc::Rc; - -use cookie::{Cookie, CookieJar, Key, SameSite}; -use futures::future::{err as FutErr, ok as FutOk, FutureResult}; -use futures::Future; -use time::Duration; - -use error::{Error, Result}; -use http::header::{self, HeaderValue}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; - -/// The helper trait to obtain your identity from a request. -/// -/// ```rust -/// use actix_web::middleware::identity::RequestIdentity; -/// use actix_web::*; -/// -/// fn index(req: HttpRequest) -> Result { -/// // access request identity -/// if let Some(id) = req.identity() { -/// Ok(format!("Welcome! {}", id)) -/// } else { -/// Ok("Welcome Anonymous!".to_owned()) -/// } -/// } -/// -/// fn login(mut req: HttpRequest) -> HttpResponse { -/// req.remember("User1".to_owned()); // <- remember identity -/// HttpResponse::Ok().finish() -/// } -/// -/// fn logout(mut req: HttpRequest) -> HttpResponse { -/// req.forget(); // <- remove identity -/// HttpResponse::Ok().finish() -/// } -/// # fn main() {} -/// ``` -pub trait RequestIdentity { - /// Return the claimed identity of the user associated request or - /// ``None`` if no identity can be found associated with the request. - fn identity(&self) -> Option; - - /// Remember identity. - fn remember(&self, identity: String); - - /// This method is used to 'forget' the current identity on subsequent - /// requests. - fn forget(&self); -} - -impl RequestIdentity for HttpRequest { - fn identity(&self) -> Option { - if let Some(id) = self.extensions().get::() { - return id.0.identity().map(|s| s.to_owned()); - } - None - } - - fn remember(&self, identity: String) { - if let Some(id) = self.extensions_mut().get_mut::() { - return id.0.as_mut().remember(identity); - } - } - - fn forget(&self) { - if let Some(id) = self.extensions_mut().get_mut::() { - return id.0.forget(); - } - } -} - -/// An identity -pub trait Identity: 'static { - /// Return the claimed identity of the user associated request or - /// ``None`` if no identity can be found associated with the request. - fn identity(&self) -> Option<&str>; - - /// Remember identity. - fn remember(&mut self, key: String); - - /// This method is used to 'forget' the current identity on subsequent - /// requests. - fn forget(&mut self); - - /// Write session to storage backend. - fn write(&mut self, resp: HttpResponse) -> Result; -} - -/// Identity policy definition. -pub trait IdentityPolicy: Sized + 'static { - /// The associated identity - type Identity: Identity; - - /// The return type of the middleware - type Future: Future; - - /// Parse the session from request and load data from a service identity. - fn from_request(&self, request: &HttpRequest) -> Self::Future; -} - -/// Request identity middleware -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; -/// use actix_web::App; -/// -/// fn main() { -/// let app = App::new().middleware(IdentityService::new( -/// // <- create identity middleware -/// CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend -/// .name("auth-cookie") -/// .secure(false), -/// )); -/// } -/// ``` -pub struct IdentityService { - backend: T, -} - -impl IdentityService { - /// Create new identity service with specified backend. - pub fn new(backend: T) -> Self { - IdentityService { backend } - } -} - -struct IdentityBox(Box); - -impl> Middleware for IdentityService { - fn start(&self, req: &HttpRequest) -> Result { - let req = req.clone(); - let fut = self.backend.from_request(&req).then(move |res| match res { - Ok(id) => { - req.extensions_mut().insert(IdentityBox(Box::new(id))); - FutOk(None) - } - Err(err) => FutErr(err), - }); - Ok(Started::Future(Box::new(fut))) - } - - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(ref mut id) = req.extensions_mut().get_mut::() { - id.0.as_mut().write(resp) - } else { - Ok(Response::Done(resp)) - } - } -} - -#[doc(hidden)] -/// Identity that uses private cookies as identity storage. -pub struct CookieIdentity { - changed: bool, - identity: Option, - inner: Rc, -} - -impl Identity for CookieIdentity { - fn identity(&self) -> Option<&str> { - self.identity.as_ref().map(|s| s.as_ref()) - } - - fn remember(&mut self, value: String) { - self.changed = true; - self.identity = Some(value); - } - - fn forget(&mut self) { - self.changed = true; - self.identity = None; - } - - fn write(&mut self, mut resp: HttpResponse) -> Result { - if self.changed { - let _ = self.inner.set_cookie(&mut resp, self.identity.take()); - } - Ok(Response::Done(resp)) - } -} - -struct CookieIdentityInner { - key: Key, - name: String, - path: String, - domain: Option, - secure: bool, - max_age: Option, - same_site: Option, -} - -impl CookieIdentityInner { - fn new(key: &[u8]) -> CookieIdentityInner { - CookieIdentityInner { - key: Key::from_master(key), - name: "actix-identity".to_owned(), - path: "/".to_owned(), - domain: None, - secure: true, - max_age: None, - same_site: None, - } - } - - fn set_cookie(&self, resp: &mut HttpResponse, id: Option) -> Result<()> { - let some = id.is_some(); - { - let id = id.unwrap_or_else(String::new); - let mut cookie = Cookie::new(self.name.clone(), id); - cookie.set_path(self.path.clone()); - cookie.set_secure(self.secure); - cookie.set_http_only(true); - - if let Some(ref domain) = self.domain { - cookie.set_domain(domain.clone()); - } - - if let Some(max_age) = self.max_age { - cookie.set_max_age(max_age); - } - - if let Some(same_site) = self.same_site { - cookie.set_same_site(same_site); - } - - let mut jar = CookieJar::new(); - if some { - jar.private(&self.key).add(cookie); - } else { - jar.add_original(cookie.clone()); - jar.private(&self.key).remove(cookie); - } - - for cookie in jar.delta() { - let val = HeaderValue::from_str(&cookie.to_string())?; - resp.headers_mut().append(header::SET_COOKIE, val); - } - } - - Ok(()) - } - - fn load(&self, req: &HttpRequest) -> Option { - if let Ok(cookies) = req.cookies() { - for cookie in cookies.iter() { - if cookie.name() == self.name { - let mut jar = CookieJar::new(); - jar.add_original(cookie.clone()); - - let cookie_opt = jar.private(&self.key).get(&self.name); - if let Some(cookie) = cookie_opt { - return Some(cookie.value().into()); - } - } - } - } - None - } -} - -/// Use cookies for request identity storage. -/// -/// The constructors take a key as an argument. -/// This is the private key for cookie - when this value is changed, -/// all identities are lost. The constructors will panic if the key is less -/// than 32 bytes in length. -/// -/// # Example -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; -/// use actix_web::App; -/// -/// fn main() { -/// let app = App::new().middleware(IdentityService::new( -/// // <- create identity middleware -/// CookieIdentityPolicy::new(&[0; 32]) // <- construct cookie policy -/// .domain("www.rust-lang.org") -/// .name("actix_auth") -/// .path("/") -/// .secure(true), -/// )); -/// } -/// ``` -pub struct CookieIdentityPolicy(Rc); - -impl CookieIdentityPolicy { - /// Construct new `CookieIdentityPolicy` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn new(key: &[u8]) -> CookieIdentityPolicy { - CookieIdentityPolicy(Rc::new(CookieIdentityInner::new(key))) - } - - /// Sets the `path` field in the session cookie being built. - pub fn path>(mut self, value: S) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().path = value.into(); - self - } - - /// Sets the `name` field in the session cookie being built. - pub fn name>(mut self, value: S) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().name = value.into(); - self - } - - /// Sets the `domain` field in the session cookie being built. - pub fn domain>(mut self, value: S) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); - self - } - - /// Sets the `secure` field in the session cookie being built. - /// - /// If the `secure` field is set, a cookie will only be transmitted when the - /// connection is secure - i.e. `https` - pub fn secure(mut self, value: bool) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().secure = value; - self - } - - /// Sets the `max-age` field in the session cookie being built. - pub fn max_age(mut self, value: Duration) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); - self - } - - /// Sets the `same_site` field in the session cookie being built. - pub fn same_site(mut self, same_site: SameSite) -> Self { - Rc::get_mut(&mut self.0).unwrap().same_site = Some(same_site); - self - } -} - -impl IdentityPolicy for CookieIdentityPolicy { - type Identity = CookieIdentity; - type Future = FutureResult; - - fn from_request(&self, req: &HttpRequest) -> Self::Future { - let identity = self.0.load(req); - FutOk(CookieIdentity { - identity, - changed: false, - inner: Rc::clone(&self.0), - }) - } -} diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs deleted file mode 100644 index b7bb1bb80..000000000 --- a/src/middleware/logger.rs +++ /dev/null @@ -1,384 +0,0 @@ -//! Request logging middleware -use std::collections::HashSet; -use std::env; -use std::fmt::{self, Display, Formatter}; - -use regex::Regex; -use time; - -use error::Result; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Finished, Middleware, Started}; - -/// `Middleware` for logging request and response info to the terminal. -/// -/// `Logger` middleware uses standard log crate to log information. You should -/// enable logger for `actix_web` package to see access log. -/// ([`env_logger`](https://docs.rs/env_logger/*/env_logger/) or similar) -/// -/// ## Usage -/// -/// Create `Logger` middleware with the specified `format`. -/// Default `Logger` could be created with `default` method, it uses the -/// default format: -/// -/// ```ignore -/// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T -/// ``` -/// ```rust -/// # extern crate actix_web; -/// extern crate env_logger; -/// use actix_web::middleware::Logger; -/// use actix_web::App; -/// -/// fn main() { -/// std::env::set_var("RUST_LOG", "actix_web=info"); -/// env_logger::init(); -/// -/// let app = App::new() -/// .middleware(Logger::default()) -/// .middleware(Logger::new("%a %{User-Agent}i")) -/// .finish(); -/// } -/// ``` -/// -/// ## Format -/// -/// `%%` The percent sign -/// -/// `%a` Remote IP-address (IP-address of proxy if using reverse proxy) -/// -/// `%t` Time when the request was started to process -/// -/// `%r` First line of request -/// -/// `%s` Response status code -/// -/// `%b` Size of response in bytes, including HTTP headers -/// -/// `%T` Time taken to serve the request, in seconds with floating fraction in -/// .06f format -/// -/// `%D` Time taken to serve the request, in milliseconds -/// -/// `%{FOO}i` request.headers['FOO'] -/// -/// `%{FOO}o` response.headers['FOO'] -/// -/// `%{FOO}e` os.environ['FOO'] -/// -pub struct Logger { - format: Format, - exclude: HashSet, -} - -impl Logger { - /// Create `Logger` middleware with the specified `format`. - pub fn new(format: &str) -> Logger { - Logger { - format: Format::new(format), - exclude: HashSet::new(), - } - } - - /// Ignore and do not log access info for specified path. - pub fn exclude>(mut self, path: T) -> Self { - self.exclude.insert(path.into()); - self - } -} - -impl Default for Logger { - /// Create `Logger` middleware with format: - /// - /// ```ignore - /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T - /// ``` - fn default() -> Logger { - Logger { - format: Format::default(), - exclude: HashSet::new(), - } - } -} - -struct StartTime(time::Tm); - -impl Logger { - fn log(&self, req: &HttpRequest, resp: &HttpResponse) { - if let Some(entry_time) = req.extensions().get::() { - let render = |fmt: &mut Formatter| { - for unit in &self.format.0 { - unit.render(fmt, req, resp, entry_time.0)?; - } - Ok(()) - }; - info!("{}", FormatDisplay(&render)); - } - } -} - -impl Middleware for Logger { - fn start(&self, req: &HttpRequest) -> Result { - if !self.exclude.contains(req.path()) { - req.extensions_mut().insert(StartTime(time::now())); - } - Ok(Started::Done) - } - - fn finish(&self, req: &HttpRequest, resp: &HttpResponse) -> Finished { - self.log(req, resp); - Finished::Done - } -} - -/// A formatting style for the `Logger`, consisting of multiple -/// `FormatText`s concatenated into one line. -#[derive(Clone)] -#[doc(hidden)] -struct Format(Vec); - -impl Default for Format { - /// Return the default formatting style for the `Logger`: - fn default() -> Format { - Format::new(r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#) - } -} - -impl Format { - /// Create a `Format` from a format string. - /// - /// Returns `None` if the format string syntax is incorrect. - pub fn new(s: &str) -> Format { - trace!("Access log format: {}", s); - let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbTD]?)").unwrap(); - - let mut idx = 0; - let mut results = Vec::new(); - for cap in fmt.captures_iter(s) { - let m = cap.get(0).unwrap(); - let pos = m.start(); - if idx != pos { - results.push(FormatText::Str(s[idx..pos].to_owned())); - } - idx = m.end(); - - if let Some(key) = cap.get(2) { - results.push(match cap.get(3).unwrap().as_str() { - "i" => FormatText::RequestHeader(key.as_str().to_owned()), - "o" => FormatText::ResponseHeader(key.as_str().to_owned()), - "e" => FormatText::EnvironHeader(key.as_str().to_owned()), - _ => unreachable!(), - }) - } else { - let m = cap.get(1).unwrap(); - results.push(match m.as_str() { - "%" => FormatText::Percent, - "a" => FormatText::RemoteAddr, - "t" => FormatText::RequestTime, - "r" => FormatText::RequestLine, - "s" => FormatText::ResponseStatus, - "b" => FormatText::ResponseSize, - "T" => FormatText::Time, - "D" => FormatText::TimeMillis, - _ => FormatText::Str(m.as_str().to_owned()), - }); - } - } - if idx != s.len() { - results.push(FormatText::Str(s[idx..].to_owned())); - } - - Format(results) - } -} - -/// A string of text to be logged. This is either one of the data -/// fields supported by the `Logger`, or a custom `String`. -#[doc(hidden)] -#[derive(Debug, Clone)] -pub enum FormatText { - Str(String), - Percent, - RequestLine, - RequestTime, - ResponseStatus, - ResponseSize, - Time, - TimeMillis, - RemoteAddr, - RequestHeader(String), - ResponseHeader(String), - EnvironHeader(String), -} - -impl FormatText { - fn render( - &self, fmt: &mut Formatter, req: &HttpRequest, resp: &HttpResponse, - entry_time: time::Tm, - ) -> Result<(), fmt::Error> { - match *self { - FormatText::Str(ref string) => fmt.write_str(string), - FormatText::Percent => "%".fmt(fmt), - FormatText::RequestLine => { - if req.query_string().is_empty() { - fmt.write_fmt(format_args!( - "{} {} {:?}", - req.method(), - req.path(), - req.version() - )) - } else { - fmt.write_fmt(format_args!( - "{} {}?{} {:?}", - req.method(), - req.path(), - req.query_string(), - req.version() - )) - } - } - FormatText::ResponseStatus => resp.status().as_u16().fmt(fmt), - FormatText::ResponseSize => resp.response_size().fmt(fmt), - FormatText::Time => { - let rt = time::now() - entry_time; - let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000_000.0; - fmt.write_fmt(format_args!("{:.6}", rt)) - } - FormatText::TimeMillis => { - let rt = time::now() - entry_time; - let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000.0; - fmt.write_fmt(format_args!("{:.6}", rt)) - } - FormatText::RemoteAddr => { - if let Some(remote) = req.connection_info().remote() { - return remote.fmt(fmt); - } else { - "-".fmt(fmt) - } - } - FormatText::RequestTime => entry_time - .strftime("[%d/%b/%Y:%H:%M:%S %z]") - .unwrap() - .fmt(fmt), - FormatText::RequestHeader(ref name) => { - let s = if let Some(val) = req.headers().get(name) { - if let Ok(s) = val.to_str() { - s - } else { - "-" - } - } else { - "-" - }; - fmt.write_fmt(format_args!("{}", s)) - } - FormatText::ResponseHeader(ref name) => { - let s = if let Some(val) = resp.headers().get(name) { - if let Ok(s) = val.to_str() { - s - } else { - "-" - } - } else { - "-" - }; - fmt.write_fmt(format_args!("{}", s)) - } - FormatText::EnvironHeader(ref name) => { - if let Ok(val) = env::var(name) { - fmt.write_fmt(format_args!("{}", val)) - } else { - "-".fmt(fmt) - } - } - } - } -} - -pub(crate) struct FormatDisplay<'a>(&'a Fn(&mut Formatter) -> Result<(), fmt::Error>); - -impl<'a> fmt::Display for FormatDisplay<'a> { - fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { - (self.0)(fmt) - } -} - -#[cfg(test)] -mod tests { - use time; - - use super::*; - use http::{header, StatusCode}; - use test::TestRequest; - - #[test] - fn test_logger() { - let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test"); - - let req = TestRequest::with_header( - header::USER_AGENT, - header::HeaderValue::from_static("ACTIX-WEB"), - ).finish(); - let resp = HttpResponse::build(StatusCode::OK) - .header("X-Test", "ttt") - .force_close() - .finish(); - - match logger.start(&req) { - Ok(Started::Done) => (), - _ => panic!(), - }; - match logger.finish(&req, &resp) { - Finished::Done => (), - _ => panic!(), - } - let entry_time = time::now(); - let render = |fmt: &mut Formatter| { - for unit in &logger.format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("ACTIX-WEB ttt")); - } - - #[test] - fn test_default_format() { - let format = Format::default(); - - let req = TestRequest::with_header( - header::USER_AGENT, - header::HeaderValue::from_static("ACTIX-WEB"), - ).finish(); - let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); - let entry_time = time::now(); - - let render = |fmt: &mut Formatter| { - for unit in &format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("GET / HTTP/1.1")); - assert!(s.contains("200 0")); - assert!(s.contains("ACTIX-WEB")); - - let req = TestRequest::with_uri("/?test").finish(); - let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); - let entry_time = time::now(); - - let render = |fmt: &mut Formatter| { - for unit in &format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("GET /?test HTTP/1.1")); - } -} diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index c69dbb3e0..a8b4b3c67 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1,68 +1,65 @@ -//! Middlewares -use futures::Future; +use std::marker::PhantomData; -use error::{Error, Result}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; +use actix_service::{NewTransform, Service, Transform}; +use futures::future::{ok, FutureResult}; -mod logger; +#[cfg(any(feature = "brotli", feature = "flate2"))] +mod compress; +#[cfg(any(feature = "brotli", feature = "flate2"))] +pub use self::compress::Compress; -pub mod cors; -pub mod csrf; mod defaultheaders; -mod errhandlers; -#[cfg(feature = "session")] -pub mod identity; -#[cfg(feature = "session")] -pub mod session; pub use self::defaultheaders::DefaultHeaders; -pub use self::errhandlers::ErrorHandlers; -pub use self::logger::Logger; -/// Middleware start result -pub enum Started { - /// Middleware is completed, continue to next middleware - Done, - /// New http response got generated. If middleware generates response - /// handler execution halts. - Response(HttpResponse), - /// Execution completed, runs future to completion. - Future(Box, Error = Error>>), +/// Helper for middleware service factory +pub struct MiddlewareFactory +where + T: Transform + Clone, + S: Service, +{ + tr: T, + _t: PhantomData, } -/// Middleware execution result -pub enum Response { - /// New http response got generated - Done(HttpResponse), - /// Result is a future that resolves to a new http response - Future(Box>), -} - -/// Middleware finish result -pub enum Finished { - /// Execution completed - Done, - /// Execution completed, but run future to completion - Future(Box>), -} - -/// Middleware definition -#[allow(unused_variables)] -pub trait Middleware: 'static { - /// Method is called when request is ready. It may return - /// future, which should resolve before next middleware get called. - fn start(&self, req: &HttpRequest) -> Result { - Ok(Started::Done) - } - - /// Method is called when handler returns response, - /// but before sending http message to peer. - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - Ok(Response::Done(resp)) - } - - /// Method is called after body stream get sent to peer. - fn finish(&self, req: &HttpRequest, resp: &HttpResponse) -> Finished { - Finished::Done +impl MiddlewareFactory +where + T: Transform + Clone, + S: Service, +{ + pub fn new(tr: T) -> Self { + MiddlewareFactory { + tr, + _t: PhantomData, + } + } +} + +impl Clone for MiddlewareFactory +where + T: Transform + Clone, + S: Service, +{ + fn clone(&self) -> Self { + Self { + tr: self.tr.clone(), + _t: PhantomData, + } + } +} + +impl NewTransform for MiddlewareFactory +where + T: Transform + Clone, + S: Service, +{ + type Request = T::Request; + type Response = T::Response; + type Error = T::Error; + type Transform = T; + type InitError = (); + type Future = FutureResult; + + fn new_transform(&self) -> Self::Future { + ok(self.tr.clone()) } } diff --git a/src/middleware/session.rs b/src/middleware/session.rs deleted file mode 100644 index 0271a13f8..000000000 --- a/src/middleware/session.rs +++ /dev/null @@ -1,618 +0,0 @@ -//! User sessions. -//! -//! Actix provides a general solution for session management. The -//! [**SessionStorage**](struct.SessionStorage.html) -//! middleware can be used with different backend types to store session -//! data in different backends. -//! -//! By default, only cookie session backend is implemented. Other -//! backend implementations can be added. -//! -//! [**CookieSessionBackend**](struct.CookieSessionBackend.html) -//! uses cookies as session storage. `CookieSessionBackend` creates sessions -//! which are limited to storing fewer than 4000 bytes of data, as the payload -//! must fit into a single cookie. An internal server error is generated if a -//! session contains more than 4000 bytes. -//! -//! A cookie may have a security policy of *signed* or *private*. Each has -//! a respective `CookieSessionBackend` constructor. -//! -//! A *signed* cookie may be viewed but not modified by the client. A *private* -//! cookie may neither be viewed nor modified by the client. -//! -//! The constructors take a key as an argument. This is the private key -//! for cookie session - when this value is changed, all session data is lost. -//! -//! In general, you create a `SessionStorage` middleware and initialize it -//! with specific backend implementation, such as a `CookieSessionBackend`. -//! To access session data, -//! [*HttpRequest::session()*](trait.RequestSession.html#tymethod.session) -//! must be used. This method returns a -//! [*Session*](struct.Session.html) object, which allows us to get or set -//! session data. -//! -//! ```rust -//! # extern crate actix_web; -//! # extern crate actix; -//! use actix_web::{server, App, HttpRequest, Result}; -//! use actix_web::middleware::session::{RequestSession, SessionStorage, CookieSessionBackend}; -//! -//! fn index(req: HttpRequest) -> Result<&'static str> { -//! // access session data -//! if let Some(count) = req.session().get::("counter")? { -//! println!("SESSION value: {}", count); -//! req.session().set("counter", count+1)?; -//! } else { -//! req.session().set("counter", 1)?; -//! } -//! -//! Ok("Welcome!") -//! } -//! -//! fn main() { -//! actix::System::run(|| { -//! server::new( -//! || App::new().middleware( -//! SessionStorage::new( // <- create session middleware -//! CookieSessionBackend::signed(&[0; 32]) // <- create signed cookie session backend -//! .secure(false) -//! ))) -//! .bind("127.0.0.1:59880").unwrap() -//! .start(); -//! # actix::System::current().stop(); -//! }); -//! } -//! ``` -use std::cell::RefCell; -use std::collections::HashMap; -use std::marker::PhantomData; -use std::rc::Rc; -use std::sync::Arc; - -use cookie::{Cookie, CookieJar, Key, SameSite}; -use futures::future::{err as FutErr, ok as FutOk, FutureResult}; -use futures::Future; -use http::header::{self, HeaderValue}; -use serde::de::DeserializeOwned; -use serde::Serialize; -use serde_json; -use serde_json::error::Error as JsonError; -use time::Duration; - -use error::{Error, ResponseError, Result}; -use handler::FromRequest; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; - -/// The helper trait to obtain your session data from a request. -/// -/// ```rust -/// use actix_web::middleware::session::RequestSession; -/// use actix_web::*; -/// -/// fn index(mut req: HttpRequest) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = req.session().get::("counter")? { -/// req.session().set("counter", count + 1)?; -/// } else { -/// req.session().set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -pub trait RequestSession { - /// Get the session from the request - fn session(&self) -> Session; -} - -impl RequestSession for HttpRequest { - fn session(&self) -> Session { - if let Some(s_impl) = self.extensions().get::>() { - return Session(SessionInner::Session(Arc::clone(&s_impl))); - } - Session(SessionInner::None) - } -} - -/// The high-level interface you use to modify session data. -/// -/// Session object could be obtained with -/// [`RequestSession::session`](trait.RequestSession.html#tymethod.session) -/// method. `RequestSession` trait is implemented for `HttpRequest`. -/// -/// ```rust -/// use actix_web::middleware::session::RequestSession; -/// use actix_web::*; -/// -/// fn index(mut req: HttpRequest) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = req.session().get::("counter")? { -/// req.session().set("counter", count + 1)?; -/// } else { -/// req.session().set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -pub struct Session(SessionInner); - -enum SessionInner { - Session(Arc), - None, -} - -impl Session { - /// Get a `value` from the session. - pub fn get(&self, key: &str) -> Result> { - match self.0 { - SessionInner::Session(ref sess) => { - if let Some(s) = sess.as_ref().0.borrow().get(key) { - Ok(Some(serde_json::from_str(s)?)) - } else { - Ok(None) - } - } - SessionInner::None => Ok(None), - } - } - - /// Set a `value` from the session. - pub fn set(&self, key: &str, value: T) -> Result<()> { - match self.0 { - SessionInner::Session(ref sess) => { - sess.as_ref() - .0 - .borrow_mut() - .set(key, serde_json::to_string(&value)?); - Ok(()) - } - SessionInner::None => Ok(()), - } - } - - /// Remove value from the session. - pub fn remove(&self, key: &str) { - match self.0 { - SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().remove(key), - SessionInner::None => (), - } - } - - /// Clear the session. - pub fn clear(&self) { - match self.0 { - SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().clear(), - SessionInner::None => (), - } - } -} - -/// Extractor implementation for Session type. -/// -/// ```rust -/// # use actix_web::*; -/// use actix_web::middleware::session::Session; -/// -/// fn index(session: Session) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = session.get::("counter")? { -/// session.set("counter", count + 1)?; -/// } else { -/// session.set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -impl FromRequest for Session { - type Config = (); - type Result = Session; - - #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { - req.session() - } -} - -struct SessionImplCell(RefCell>); - -/// Session storage middleware -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::session::{CookieSessionBackend, SessionStorage}; -/// use actix_web::App; -/// -/// fn main() { -/// let app = App::new().middleware(SessionStorage::new( -/// // <- create session middleware -/// CookieSessionBackend::signed(&[0; 32]) // <- create cookie session backend -/// .secure(false), -/// )); -/// } -/// ``` -pub struct SessionStorage(T, PhantomData); - -impl> SessionStorage { - /// Create session storage - pub fn new(backend: T) -> SessionStorage { - SessionStorage(backend, PhantomData) - } -} - -impl> Middleware for SessionStorage { - fn start(&self, req: &HttpRequest) -> Result { - let mut req = req.clone(); - - let fut = self.0.from_request(&mut req).then(move |res| match res { - Ok(sess) => { - req.extensions_mut() - .insert(Arc::new(SessionImplCell(RefCell::new(Box::new(sess))))); - FutOk(None) - } - Err(err) => FutErr(err), - }); - Ok(Started::Future(Box::new(fut))) - } - - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(s_box) = req.extensions().get::>() { - s_box.0.borrow_mut().write(resp) - } else { - Ok(Response::Done(resp)) - } - } -} - -/// A simple key-value storage interface that is internally used by `Session`. -pub trait SessionImpl: 'static { - /// Get session value by key - fn get(&self, key: &str) -> Option<&str>; - - /// Set session value - fn set(&mut self, key: &str, value: String); - - /// Remove specific key from session - fn remove(&mut self, key: &str); - - /// Remove all values from session - fn clear(&mut self); - - /// Write session to storage backend. - fn write(&self, resp: HttpResponse) -> Result; -} - -/// Session's storage backend trait definition. -pub trait SessionBackend: Sized + 'static { - /// Session item - type Session: SessionImpl; - /// Future that reads session - type ReadFuture: Future; - - /// Parse the session from request and load data from a storage backend. - fn from_request(&self, request: &mut HttpRequest) -> Self::ReadFuture; -} - -/// Session that uses signed cookies as session storage -pub struct CookieSession { - changed: bool, - state: HashMap, - inner: Rc, -} - -/// Errors that can occur during handling cookie session -#[derive(Fail, Debug)] -pub enum CookieSessionError { - /// Size of the serialized session is greater than 4000 bytes. - #[fail(display = "Size of the serialized session is greater than 4000 bytes.")] - Overflow, - /// Fail to serialize session. - #[fail(display = "Fail to serialize session")] - Serialize(JsonError), -} - -impl ResponseError for CookieSessionError {} - -impl SessionImpl for CookieSession { - fn get(&self, key: &str) -> Option<&str> { - if let Some(s) = self.state.get(key) { - Some(s) - } else { - None - } - } - - fn set(&mut self, key: &str, value: String) { - self.changed = true; - self.state.insert(key.to_owned(), value); - } - - fn remove(&mut self, key: &str) { - self.changed = true; - self.state.remove(key); - } - - fn clear(&mut self) { - self.changed = true; - self.state.clear() - } - - fn write(&self, mut resp: HttpResponse) -> Result { - if self.changed { - let _ = self.inner.set_cookie(&mut resp, &self.state); - } - Ok(Response::Done(resp)) - } -} - -enum CookieSecurity { - Signed, - Private, -} - -struct CookieSessionInner { - key: Key, - security: CookieSecurity, - name: String, - path: String, - domain: Option, - secure: bool, - http_only: bool, - max_age: Option, - same_site: Option, -} - -impl CookieSessionInner { - fn new(key: &[u8], security: CookieSecurity) -> CookieSessionInner { - CookieSessionInner { - security, - key: Key::from_master(key), - name: "actix-session".to_owned(), - path: "/".to_owned(), - domain: None, - secure: true, - http_only: true, - max_age: None, - same_site: None, - } - } - - fn set_cookie( - &self, resp: &mut HttpResponse, state: &HashMap, - ) -> Result<()> { - let value = - serde_json::to_string(&state).map_err(CookieSessionError::Serialize)?; - if value.len() > 4064 { - return Err(CookieSessionError::Overflow.into()); - } - - let mut cookie = Cookie::new(self.name.clone(), value); - cookie.set_path(self.path.clone()); - cookie.set_secure(self.secure); - cookie.set_http_only(self.http_only); - - if let Some(ref domain) = self.domain { - cookie.set_domain(domain.clone()); - } - - if let Some(max_age) = self.max_age { - cookie.set_max_age(max_age); - } - - if let Some(same_site) = self.same_site { - cookie.set_same_site(same_site); - } - - let mut jar = CookieJar::new(); - - match self.security { - CookieSecurity::Signed => jar.signed(&self.key).add(cookie), - CookieSecurity::Private => jar.private(&self.key).add(cookie), - } - - for cookie in jar.delta() { - let val = HeaderValue::from_str(&cookie.encoded().to_string())?; - resp.headers_mut().append(header::SET_COOKIE, val); - } - - Ok(()) - } - - fn load(&self, req: &mut HttpRequest) -> HashMap { - if let Ok(cookies) = req.cookies() { - for cookie in cookies.iter() { - if cookie.name() == self.name { - let mut jar = CookieJar::new(); - jar.add_original(cookie.clone()); - - let cookie_opt = match self.security { - CookieSecurity::Signed => jar.signed(&self.key).get(&self.name), - CookieSecurity::Private => { - jar.private(&self.key).get(&self.name) - } - }; - if let Some(cookie) = cookie_opt { - if let Ok(val) = serde_json::from_str(cookie.value()) { - return val; - } - } - } - } - } - HashMap::new() - } -} - -/// Use cookies for session storage. -/// -/// `CookieSessionBackend` creates sessions which are limited to storing -/// fewer than 4000 bytes of data (as the payload must fit into a single -/// cookie). An Internal Server Error is generated if the session contains more -/// than 4000 bytes. -/// -/// A cookie may have a security policy of *signed* or *private*. Each has a -/// respective `CookieSessionBackend` constructor. -/// -/// A *signed* cookie is stored on the client as plaintext alongside -/// a signature such that the cookie may be viewed but not modified by the -/// client. -/// -/// A *private* cookie is stored on the client as encrypted text -/// such that it may neither be viewed nor modified by the client. -/// -/// The constructors take a key as an argument. -/// This is the private key for cookie session - when this value is changed, -/// all session data is lost. The constructors will panic if the key is less -/// than 32 bytes in length. -/// -/// The backend relies on `cookie` crate to create and read cookies. -/// By default all cookies are percent encoded, but certain symbols may -/// cause troubles when reading cookie, if they are not properly percent encoded. -/// -/// # Example -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::session::CookieSessionBackend; -/// -/// # fn main() { -/// let backend: CookieSessionBackend = CookieSessionBackend::signed(&[0; 32]) -/// .domain("www.rust-lang.org") -/// .name("actix_session") -/// .path("/") -/// .secure(true); -/// # } -/// ``` -pub struct CookieSessionBackend(Rc); - -impl CookieSessionBackend { - /// Construct new *signed* `CookieSessionBackend` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn signed(key: &[u8]) -> CookieSessionBackend { - CookieSessionBackend(Rc::new(CookieSessionInner::new( - key, - CookieSecurity::Signed, - ))) - } - - /// Construct new *private* `CookieSessionBackend` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn private(key: &[u8]) -> CookieSessionBackend { - CookieSessionBackend(Rc::new(CookieSessionInner::new( - key, - CookieSecurity::Private, - ))) - } - - /// Sets the `path` field in the session cookie being built. - pub fn path>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().path = value.into(); - self - } - - /// Sets the `name` field in the session cookie being built. - pub fn name>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().name = value.into(); - self - } - - /// Sets the `domain` field in the session cookie being built. - pub fn domain>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); - self - } - - /// Sets the `secure` field in the session cookie being built. - /// - /// If the `secure` field is set, a cookie will only be transmitted when the - /// connection is secure - i.e. `https` - pub fn secure(mut self, value: bool) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().secure = value; - self - } - - /// Sets the `http_only` field in the session cookie being built. - pub fn http_only(mut self, value: bool) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().http_only = value; - self - } - - /// Sets the `same_site` field in the session cookie being built. - pub fn same_site(mut self, value: SameSite) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().same_site = Some(value); - self - } - - /// Sets the `max-age` field in the session cookie being built. - pub fn max_age(mut self, value: Duration) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); - self - } -} - -impl SessionBackend for CookieSessionBackend { - type Session = CookieSession; - type ReadFuture = FutureResult; - - fn from_request(&self, req: &mut HttpRequest) -> Self::ReadFuture { - let state = self.0.load(req); - FutOk(CookieSession { - changed: false, - inner: Rc::clone(&self.0), - state, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use application::App; - use test; - - #[test] - fn cookie_session() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/", |r| { - r.f(|req| { - let _ = req.session().set("counter", 100); - "test" - }) - }) - }); - - let request = srv.get().uri(srv.url("/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.cookie("actix-session").is_some()); - } - - #[test] - fn cookie_session_extractor() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/", |r| { - r.with(|ses: Session| { - let _ = ses.set("counter", 100); - "test" - }) - }) - }); - - let request = srv.get().uri(srv.url("/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.cookie("actix-session").is_some()); - } -} diff --git a/src/multipart.rs b/src/multipart.rs deleted file mode 100644 index 862f60ecb..000000000 --- a/src/multipart.rs +++ /dev/null @@ -1,815 +0,0 @@ -//! Multipart requests support -use std::cell::{RefCell, UnsafeCell}; -use std::marker::PhantomData; -use std::rc::Rc; -use std::{cmp, fmt}; - -use bytes::Bytes; -use futures::task::{current as current_task, Task}; -use futures::{Async, Poll, Stream}; -use http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}; -use http::HttpTryFrom; -use httparse; -use mime; - -use error::{MultipartError, ParseError, PayloadError}; -use payload::PayloadBuffer; - -const MAX_HEADERS: usize = 32; - -/// The server-side implementation of `multipart/form-data` requests. -/// -/// This will parse the incoming stream into `MultipartItem` instances via its -/// Stream implementation. -/// `MultipartItem::Field` contains multipart field. `MultipartItem::Multipart` -/// is used for nested multipart streams. -pub struct Multipart { - safety: Safety, - error: Option, - inner: Option>>>, -} - -/// -pub enum MultipartItem { - /// Multipart field - Field(Field), - /// Nested multipart stream - Nested(Multipart), -} - -enum InnerMultipartItem { - None, - Field(Rc>>), - Multipart(Rc>>), -} - -#[derive(PartialEq, Debug)] -enum InnerState { - /// Stream eof - Eof, - /// Skip data until first boundary - FirstBoundary, - /// Reading boundary - Boundary, - /// Reading Headers, - Headers, -} - -struct InnerMultipart { - payload: PayloadRef, - boundary: String, - state: InnerState, - item: InnerMultipartItem, -} - -impl Multipart<()> { - /// Extract boundary info from headers. - pub fn boundary(headers: &HeaderMap) -> Result { - if let Some(content_type) = headers.get(header::CONTENT_TYPE) { - if let Ok(content_type) = content_type.to_str() { - if let Ok(ct) = content_type.parse::() { - if let Some(boundary) = ct.get_param(mime::BOUNDARY) { - Ok(boundary.as_str().to_owned()) - } else { - Err(MultipartError::Boundary) - } - } else { - Err(MultipartError::ParseContentType) - } - } else { - Err(MultipartError::ParseContentType) - } - } else { - Err(MultipartError::NoContentType) - } - } -} - -impl Multipart -where - S: Stream, -{ - /// Create multipart instance for boundary. - pub fn new(boundary: Result, stream: S) -> Multipart { - match boundary { - Ok(boundary) => Multipart { - error: None, - safety: Safety::new(), - inner: Some(Rc::new(RefCell::new(InnerMultipart { - boundary, - payload: PayloadRef::new(PayloadBuffer::new(stream)), - state: InnerState::FirstBoundary, - item: InnerMultipartItem::None, - }))), - }, - Err(err) => Multipart { - error: Some(err), - safety: Safety::new(), - inner: None, - }, - } - } -} - -impl Stream for Multipart -where - S: Stream, -{ - type Item = MultipartItem; - type Error = MultipartError; - - fn poll(&mut self) -> Poll, Self::Error> { - if let Some(err) = self.error.take() { - Err(err) - } else if self.safety.current() { - self.inner.as_mut().unwrap().borrow_mut().poll(&self.safety) - } else { - Ok(Async::NotReady) - } - } -} - -impl InnerMultipart -where - S: Stream, -{ - fn read_headers(payload: &mut PayloadBuffer) -> Poll { - match payload.read_until(b"\r\n\r\n")? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(None) => Err(MultipartError::Incomplete), - Async::Ready(Some(bytes)) => { - let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS]; - match httparse::parse_headers(&bytes, &mut hdrs) { - Ok(httparse::Status::Complete((_, hdrs))) => { - // convert headers - let mut headers = HeaderMap::with_capacity(hdrs.len()); - for h in hdrs { - if let Ok(name) = HeaderName::try_from(h.name) { - if let Ok(value) = HeaderValue::try_from(h.value) { - headers.append(name, value); - } else { - return Err(ParseError::Header.into()); - } - } else { - return Err(ParseError::Header.into()); - } - } - Ok(Async::Ready(headers)) - } - Ok(httparse::Status::Partial) => Err(ParseError::Header.into()), - Err(err) => Err(ParseError::from(err).into()), - } - } - } - } - - fn read_boundary( - payload: &mut PayloadBuffer, boundary: &str, - ) -> Poll { - // TODO: need to read epilogue - match payload.readline()? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(None) => Err(MultipartError::Incomplete), - Async::Ready(Some(chunk)) => { - if chunk.len() == boundary.len() + 4 - && &chunk[..2] == b"--" - && &chunk[2..boundary.len() + 2] == boundary.as_bytes() - { - Ok(Async::Ready(false)) - } else if chunk.len() == boundary.len() + 6 - && &chunk[..2] == b"--" - && &chunk[2..boundary.len() + 2] == boundary.as_bytes() - && &chunk[boundary.len() + 2..boundary.len() + 4] == b"--" - { - Ok(Async::Ready(true)) - } else { - Err(MultipartError::Boundary) - } - } - } - } - - fn skip_until_boundary( - payload: &mut PayloadBuffer, boundary: &str, - ) -> Poll { - let mut eof = false; - loop { - match payload.readline()? { - Async::Ready(Some(chunk)) => { - if chunk.is_empty() { - //ValueError("Could not find starting boundary %r" - //% (self._boundary)) - } - if chunk.len() < boundary.len() { - continue; - } - if &chunk[..2] == b"--" - && &chunk[2..chunk.len() - 2] == boundary.as_bytes() - { - break; - } else { - if chunk.len() < boundary.len() + 2 { - continue; - } - let b: &[u8] = boundary.as_ref(); - if &chunk[..boundary.len()] == b - && &chunk[boundary.len()..boundary.len() + 2] == b"--" - { - eof = true; - break; - } - } - } - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(None) => return Err(MultipartError::Incomplete), - } - } - Ok(Async::Ready(eof)) - } - - fn poll( - &mut self, safety: &Safety, - ) -> Poll>, MultipartError> { - if self.state == InnerState::Eof { - Ok(Async::Ready(None)) - } else { - // release field - loop { - // Nested multipart streams of fields has to be consumed - // before switching to next - if safety.current() { - let stop = match self.item { - InnerMultipartItem::Field(ref mut field) => { - match field.borrow_mut().poll(safety)? { - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(Some(_)) => continue, - Async::Ready(None) => true, - } - } - InnerMultipartItem::Multipart(ref mut multipart) => { - match multipart.borrow_mut().poll(safety)? { - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(Some(_)) => continue, - Async::Ready(None) => true, - } - } - _ => false, - }; - if stop { - self.item = InnerMultipartItem::None; - } - if let InnerMultipartItem::None = self.item { - break; - } - } - } - - let headers = if let Some(payload) = self.payload.get_mut(safety) { - match self.state { - // read until first boundary - InnerState::FirstBoundary => { - match InnerMultipart::skip_until_boundary( - payload, - &self.boundary, - )? { - Async::Ready(eof) => { - if eof { - self.state = InnerState::Eof; - return Ok(Async::Ready(None)); - } else { - self.state = InnerState::Headers; - } - } - Async::NotReady => return Ok(Async::NotReady), - } - } - // read boundary - InnerState::Boundary => { - match InnerMultipart::read_boundary(payload, &self.boundary)? { - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(eof) => { - if eof { - self.state = InnerState::Eof; - return Ok(Async::Ready(None)); - } else { - self.state = InnerState::Headers; - } - } - } - } - _ => (), - } - - // read field headers for next field - if self.state == InnerState::Headers { - if let Async::Ready(headers) = InnerMultipart::read_headers(payload)? - { - self.state = InnerState::Boundary; - headers - } else { - return Ok(Async::NotReady); - } - } else { - unreachable!() - } - } else { - debug!("NotReady: field is in flight"); - return Ok(Async::NotReady); - }; - - // content type - let mut mt = mime::APPLICATION_OCTET_STREAM; - if let Some(content_type) = headers.get(header::CONTENT_TYPE) { - if let Ok(content_type) = content_type.to_str() { - if let Ok(ct) = content_type.parse::() { - mt = ct; - } - } - } - - self.state = InnerState::Boundary; - - // nested multipart stream - if mt.type_() == mime::MULTIPART { - let inner = if let Some(boundary) = mt.get_param(mime::BOUNDARY) { - Rc::new(RefCell::new(InnerMultipart { - payload: self.payload.clone(), - boundary: boundary.as_str().to_owned(), - state: InnerState::FirstBoundary, - item: InnerMultipartItem::None, - })) - } else { - return Err(MultipartError::Boundary); - }; - - self.item = InnerMultipartItem::Multipart(Rc::clone(&inner)); - - Ok(Async::Ready(Some(MultipartItem::Nested(Multipart { - safety: safety.clone(), - error: None, - inner: Some(inner), - })))) - } else { - let field = Rc::new(RefCell::new(InnerField::new( - self.payload.clone(), - self.boundary.clone(), - &headers, - )?)); - self.item = InnerMultipartItem::Field(Rc::clone(&field)); - - Ok(Async::Ready(Some(MultipartItem::Field(Field::new( - safety.clone(), - headers, - mt, - field, - ))))) - } - } - } -} - -impl Drop for InnerMultipart { - fn drop(&mut self) { - // InnerMultipartItem::Field has to be dropped first because of Safety. - self.item = InnerMultipartItem::None; - } -} - -/// A single field in a multipart stream -pub struct Field { - ct: mime::Mime, - headers: HeaderMap, - inner: Rc>>, - safety: Safety, -} - -impl Field -where - S: Stream, -{ - fn new( - safety: Safety, headers: HeaderMap, ct: mime::Mime, - inner: Rc>>, - ) -> Self { - Field { - ct, - headers, - inner, - safety, - } - } - - /// Get a map of headers - pub fn headers(&self) -> &HeaderMap { - &self.headers - } - - /// Get the content type of the field - pub fn content_type(&self) -> &mime::Mime { - &self.ct - } - - /// Get the content disposition of the field, if it exists - pub fn content_disposition(&self) -> Option { - // RFC 7578: 'Each part MUST contain a Content-Disposition header field - // where the disposition type is "form-data".' - if let Some(content_disposition) = - self.headers.get(::http::header::CONTENT_DISPOSITION) - { - ContentDisposition::from_raw(content_disposition).ok() - } else { - None - } - } -} - -impl Stream for Field -where - S: Stream, -{ - type Item = Bytes; - type Error = MultipartError; - - fn poll(&mut self) -> Poll, Self::Error> { - if self.safety.current() { - self.inner.borrow_mut().poll(&self.safety) - } else { - Ok(Async::NotReady) - } - } -} - -impl fmt::Debug for Field { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "\nMultipartField: {}", self.ct)?; - writeln!(f, " boundary: {}", self.inner.borrow().boundary)?; - writeln!(f, " headers:")?; - for (key, val) in self.headers.iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -struct InnerField { - payload: Option>, - boundary: String, - eof: bool, - length: Option, -} - -impl InnerField -where - S: Stream, -{ - fn new( - payload: PayloadRef, boundary: String, headers: &HeaderMap, - ) -> Result, PayloadError> { - let len = if let Some(len) = headers.get(header::CONTENT_LENGTH) { - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - Some(len) - } else { - return Err(PayloadError::Incomplete); - } - } else { - return Err(PayloadError::Incomplete); - } - } else { - None - }; - - Ok(InnerField { - boundary, - payload: Some(payload), - eof: false, - length: len, - }) - } - - /// Reads body part content chunk of the specified size. - /// The body part must has `Content-Length` header with proper value. - fn read_len( - payload: &mut PayloadBuffer, size: &mut u64, - ) -> Poll, MultipartError> { - if *size == 0 { - Ok(Async::Ready(None)) - } else { - match payload.readany() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(None)) => Err(MultipartError::Incomplete), - Ok(Async::Ready(Some(mut chunk))) => { - let len = cmp::min(chunk.len() as u64, *size); - *size -= len; - let ch = chunk.split_to(len as usize); - if !chunk.is_empty() { - payload.unprocessed(chunk); - } - Ok(Async::Ready(Some(ch))) - } - Err(err) => Err(err.into()), - } - } - } - - /// Reads content chunk of body part with unknown length. - /// The `Content-Length` header for body part is not necessary. - fn read_stream( - payload: &mut PayloadBuffer, boundary: &str, - ) -> Poll, MultipartError> { - match payload.read_until(b"\r")? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(None) => Err(MultipartError::Incomplete), - Async::Ready(Some(mut chunk)) => { - if chunk.len() == 1 { - payload.unprocessed(chunk); - match payload.read_exact(boundary.len() + 4)? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(None) => Err(MultipartError::Incomplete), - Async::Ready(Some(mut chunk)) => { - if &chunk[..2] == b"\r\n" - && &chunk[2..4] == b"--" - && &chunk[4..] == boundary.as_bytes() - { - payload.unprocessed(chunk); - Ok(Async::Ready(None)) - } else { - // \r might be part of data stream - let ch = chunk.split_to(1); - payload.unprocessed(chunk); - Ok(Async::Ready(Some(ch))) - } - } - } - } else { - let to = chunk.len() - 1; - let ch = chunk.split_to(to); - payload.unprocessed(chunk); - Ok(Async::Ready(Some(ch))) - } - } - } - } - - fn poll(&mut self, s: &Safety) -> Poll, MultipartError> { - if self.payload.is_none() { - return Ok(Async::Ready(None)); - } - - let result = if let Some(payload) = self.payload.as_ref().unwrap().get_mut(s) { - let res = if let Some(ref mut len) = self.length { - InnerField::read_len(payload, len)? - } else { - InnerField::read_stream(payload, &self.boundary)? - }; - - match res { - Async::NotReady => Async::NotReady, - Async::Ready(Some(bytes)) => Async::Ready(Some(bytes)), - Async::Ready(None) => { - self.eof = true; - match payload.readline()? { - Async::NotReady => Async::NotReady, - Async::Ready(None) => Async::Ready(None), - Async::Ready(Some(line)) => { - if line.as_ref() != b"\r\n" { - warn!("multipart field did not read all the data or it is malformed"); - } - Async::Ready(None) - } - } - } - } - } else { - Async::NotReady - }; - - if Async::Ready(None) == result { - self.payload.take(); - } - Ok(result) - } -} - -struct PayloadRef { - payload: Rc>>, -} - -impl PayloadRef -where - S: Stream, -{ - fn new(payload: PayloadBuffer) -> PayloadRef { - PayloadRef { - payload: Rc::new(payload.into()), - } - } - - fn get_mut<'a, 'b>(&'a self, s: &'b Safety) -> Option<&'a mut PayloadBuffer> - where - 'a: 'b, - { - // Unsafe: Invariant is inforced by Safety Safety is used as ref counter, - // only top most ref can have mutable access to payload. - if s.current() { - let payload: &mut PayloadBuffer = unsafe { &mut *self.payload.get() }; - Some(payload) - } else { - None - } - } -} - -impl Clone for PayloadRef { - fn clone(&self) -> PayloadRef { - PayloadRef { - payload: Rc::clone(&self.payload), - } - } -} - -/// Counter. It tracks of number of clones of payloads and give access to -/// payload only to top most task panics if Safety get destroyed and it not top -/// most task. -#[derive(Debug)] -struct Safety { - task: Option, - level: usize, - payload: Rc>, -} - -impl Safety { - fn new() -> Safety { - let payload = Rc::new(PhantomData); - Safety { - task: None, - level: Rc::strong_count(&payload), - payload, - } - } - - fn current(&self) -> bool { - Rc::strong_count(&self.payload) == self.level - } -} - -impl Clone for Safety { - fn clone(&self) -> Safety { - let payload = Rc::clone(&self.payload); - Safety { - task: Some(current_task()), - level: Rc::strong_count(&payload), - payload, - } - } -} - -impl Drop for Safety { - fn drop(&mut self) { - // parent task is dead - if Rc::strong_count(&self.payload) != self.level { - panic!("Safety get dropped but it is not from top-most task"); - } - if let Some(task) = self.task.take() { - task.notify() - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use bytes::Bytes; - use futures::future::{lazy, result}; - use payload::{Payload, PayloadWriter}; - use tokio::runtime::current_thread::Runtime; - - #[test] - fn test_boundary() { - let headers = HeaderMap::new(); - match Multipart::boundary(&headers) { - Err(MultipartError::NoContentType) => (), - _ => unreachable!("should not happen"), - } - - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("test"), - ); - - match Multipart::boundary(&headers) { - Err(MultipartError::ParseContentType) => (), - _ => unreachable!("should not happen"), - } - - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("multipart/mixed"), - ); - match Multipart::boundary(&headers) { - Err(MultipartError::Boundary) => (), - _ => unreachable!("should not happen"), - } - - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static( - "multipart/mixed; boundary=\"5c02368e880e436dab70ed54e1c58209\"", - ), - ); - - assert_eq!( - Multipart::boundary(&headers).unwrap(), - "5c02368e880e436dab70ed54e1c58209" - ); - } - - #[test] - fn test_multipart() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (mut sender, payload) = Payload::new(false); - - let bytes = Bytes::from( - "testasdadsad\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ - Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ - Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ - test\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ - Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ - data\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n"); - sender.feed_data(bytes); - - let mut multipart = Multipart::new( - Ok("abbc761f78ff4d7cb7573b5a23f96ef0".to_owned()), - payload, - ); - match multipart.poll() { - Ok(Async::Ready(Some(item))) => match item { - MultipartItem::Field(mut field) => { - { - use http::header::{DispositionParam, DispositionType}; - let cd = field.content_disposition().unwrap(); - assert_eq!(cd.disposition, DispositionType::FormData); - assert_eq!( - cd.parameters[0], - DispositionParam::Name("file".into()) - ); - } - assert_eq!(field.content_type().type_(), mime::TEXT); - assert_eq!(field.content_type().subtype(), mime::PLAIN); - - match field.poll() { - Ok(Async::Ready(Some(chunk))) => { - assert_eq!(chunk, "test") - } - _ => unreachable!(), - } - match field.poll() { - Ok(Async::Ready(None)) => (), - _ => unreachable!(), - } - } - _ => unreachable!(), - }, - _ => unreachable!(), - } - - match multipart.poll() { - Ok(Async::Ready(Some(item))) => match item { - MultipartItem::Field(mut field) => { - assert_eq!(field.content_type().type_(), mime::TEXT); - assert_eq!(field.content_type().subtype(), mime::PLAIN); - - match field.poll() { - Ok(Async::Ready(Some(chunk))) => { - assert_eq!(chunk, "data") - } - _ => unreachable!(), - } - match field.poll() { - Ok(Async::Ready(None)) => (), - _ => unreachable!(), - } - } - _ => unreachable!(), - }, - _ => unreachable!(), - } - - match multipart.poll() { - Ok(Async::Ready(None)) => (), - _ => unreachable!(), - } - - let res: Result<(), ()> = Ok(()); - result(res) - })).unwrap(); - } -} diff --git a/src/param.rs b/src/param.rs deleted file mode 100644 index a3f602599..000000000 --- a/src/param.rs +++ /dev/null @@ -1,334 +0,0 @@ -use std; -use std::ops::Index; -use std::path::PathBuf; -use std::rc::Rc; -use std::str::FromStr; - -use http::StatusCode; -use smallvec::SmallVec; - -use error::{InternalError, ResponseError, UriSegmentError}; -use uri::{Url, RESERVED_QUOTER}; - -/// A trait to abstract the idea of creating a new instance of a type from a -/// path parameter. -pub trait FromParam: Sized { - /// The associated error which can be returned from parsing. - type Err: ResponseError; - - /// Parses a string `s` to return a value of this type. - fn from_param(s: &str) -> Result; -} - -#[derive(Debug, Clone)] -pub(crate) enum ParamItem { - Static(&'static str), - UrlSegment(u16, u16), -} - -/// Route match information -/// -/// If resource path contains variable patterns, `Params` stores this variables. -#[derive(Debug, Clone)] -pub struct Params { - url: Url, - pub(crate) tail: u16, - pub(crate) segments: SmallVec<[(Rc, ParamItem); 3]>, -} - -impl Params { - pub(crate) fn new() -> Params { - Params { - url: Url::default(), - tail: 0, - segments: SmallVec::new(), - } - } - - pub(crate) fn with_url(url: &Url) -> Params { - Params { - url: url.clone(), - tail: 0, - segments: SmallVec::new(), - } - } - - pub(crate) fn clear(&mut self) { - self.segments.clear(); - } - - pub(crate) fn set_tail(&mut self, tail: u16) { - self.tail = tail; - } - - pub(crate) fn set_url(&mut self, url: Url) { - self.url = url; - } - - pub(crate) fn add(&mut self, name: Rc, value: ParamItem) { - self.segments.push((name, value)); - } - - pub(crate) fn add_static(&mut self, name: &str, value: &'static str) { - self.segments - .push((Rc::new(name.to_string()), ParamItem::Static(value))); - } - - /// Check if there are any matched patterns - pub fn is_empty(&self) -> bool { - self.segments.is_empty() - } - - /// Check number of extracted parameters - pub fn len(&self) -> usize { - self.segments.len() - } - - /// Get matched parameter by name without type conversion - pub fn get(&self, key: &str) -> Option<&str> { - for item in self.segments.iter() { - if key == item.0.as_str() { - return match item.1 { - ParamItem::Static(ref s) => Some(&s), - ParamItem::UrlSegment(s, e) => { - Some(&self.url.path()[(s as usize)..(e as usize)]) - } - }; - } - } - if key == "tail" { - Some(&self.url.path()[(self.tail as usize)..]) - } else { - None - } - } - - /// Get URL-decoded matched parameter by name without type conversion - pub fn get_decoded(&self, key: &str) -> Option { - self.get(key).map(|value| { - if let Some(ref mut value) = RESERVED_QUOTER.requote(value.as_bytes()) { - Rc::make_mut(value).to_string() - } else { - value.to_string() - } - }) - } - - /// Get unprocessed part of path - pub fn unprocessed(&self) -> &str { - &self.url.path()[(self.tail as usize)..] - } - - /// Get matched `FromParam` compatible parameter by name. - /// - /// If keyed parameter is not available empty string is used as default - /// value. - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// fn index(req: HttpRequest) -> Result { - /// let ivalue: isize = req.match_info().query("val")?; - /// Ok(format!("isuze value: {:?}", ivalue)) - /// } - /// # fn main() {} - /// ``` - pub fn query(&self, key: &str) -> Result::Err> { - if let Some(s) = self.get(key) { - T::from_param(s) - } else { - T::from_param("") - } - } - - /// Return iterator to items in parameter container - pub fn iter(&self) -> ParamsIter { - ParamsIter { - idx: 0, - params: self, - } - } -} - -#[derive(Debug)] -pub struct ParamsIter<'a> { - idx: usize, - params: &'a Params, -} - -impl<'a> Iterator for ParamsIter<'a> { - type Item = (&'a str, &'a str); - - #[inline] - fn next(&mut self) -> Option<(&'a str, &'a str)> { - if self.idx < self.params.len() { - let idx = self.idx; - let res = match self.params.segments[idx].1 { - ParamItem::Static(ref s) => &s, - ParamItem::UrlSegment(s, e) => { - &self.params.url.path()[(s as usize)..(e as usize)] - } - }; - self.idx += 1; - return Some((&self.params.segments[idx].0, res)); - } - None - } -} - -impl<'a> Index<&'a str> for Params { - type Output = str; - - fn index(&self, name: &'a str) -> &str { - self.get(name) - .expect("Value for parameter is not available") - } -} - -impl Index for Params { - type Output = str; - - fn index(&self, idx: usize) -> &str { - match self.segments[idx].1 { - ParamItem::Static(ref s) => &s, - ParamItem::UrlSegment(s, e) => &self.url.path()[(s as usize)..(e as usize)], - } - } -} - -/// Creates a `PathBuf` from a path parameter. The returned `PathBuf` is -/// percent-decoded. If a segment is equal to "..", the previous segment (if -/// any) is skipped. -/// -/// For security purposes, if a segment meets any of the following conditions, -/// an `Err` is returned indicating the condition met: -/// -/// * Decoded segment starts with any of: `.` (except `..`), `*` -/// * Decoded segment ends with any of: `:`, `>`, `<` -/// * Decoded segment contains any of: `/` -/// * On Windows, decoded segment contains any of: '\' -/// * Percent-encoding results in invalid UTF8. -/// -/// As a result of these conditions, a `PathBuf` parsed from request path -/// parameter is safe to interpolate within, or use as a suffix of, a path -/// without additional checks. -impl FromParam for PathBuf { - type Err = UriSegmentError; - - fn from_param(val: &str) -> Result { - let mut buf = PathBuf::new(); - for segment in val.split('/') { - if segment == ".." { - buf.pop(); - } else if segment.starts_with('.') { - return Err(UriSegmentError::BadStart('.')); - } else if segment.starts_with('*') { - return Err(UriSegmentError::BadStart('*')); - } else if segment.ends_with(':') { - return Err(UriSegmentError::BadEnd(':')); - } else if segment.ends_with('>') { - return Err(UriSegmentError::BadEnd('>')); - } else if segment.ends_with('<') { - return Err(UriSegmentError::BadEnd('<')); - } else if segment.is_empty() { - continue; - } else if cfg!(windows) && segment.contains('\\') { - return Err(UriSegmentError::BadChar('\\')); - } else { - buf.push(segment) - } - } - - Ok(buf) - } -} - -macro_rules! FROM_STR { - ($type:ty) => { - impl FromParam for $type { - type Err = InternalError<<$type as FromStr>::Err>; - fn from_param(val: &str) -> Result { - <$type as FromStr>::from_str(val) - .map_err(|e| InternalError::new(e, StatusCode::BAD_REQUEST)) - } - } - }; -} - -FROM_STR!(u8); -FROM_STR!(u16); -FROM_STR!(u32); -FROM_STR!(u64); -FROM_STR!(usize); -FROM_STR!(i8); -FROM_STR!(i16); -FROM_STR!(i32); -FROM_STR!(i64); -FROM_STR!(isize); -FROM_STR!(f32); -FROM_STR!(f64); -FROM_STR!(String); -FROM_STR!(std::net::IpAddr); -FROM_STR!(std::net::Ipv4Addr); -FROM_STR!(std::net::Ipv6Addr); -FROM_STR!(std::net::SocketAddr); -FROM_STR!(std::net::SocketAddrV4); -FROM_STR!(std::net::SocketAddrV6); - -#[cfg(test)] -mod tests { - use super::*; - use std::iter::FromIterator; - - #[test] - fn test_path_buf() { - assert_eq!( - PathBuf::from_param("/test/.tt"), - Err(UriSegmentError::BadStart('.')) - ); - assert_eq!( - PathBuf::from_param("/test/*tt"), - Err(UriSegmentError::BadStart('*')) - ); - assert_eq!( - PathBuf::from_param("/test/tt:"), - Err(UriSegmentError::BadEnd(':')) - ); - assert_eq!( - PathBuf::from_param("/test/tt<"), - Err(UriSegmentError::BadEnd('<')) - ); - assert_eq!( - PathBuf::from_param("/test/tt>"), - Err(UriSegmentError::BadEnd('>')) - ); - assert_eq!( - PathBuf::from_param("/seg1/seg2/"), - Ok(PathBuf::from_iter(vec!["seg1", "seg2"])) - ); - assert_eq!( - PathBuf::from_param("/seg1/../seg2/"), - Ok(PathBuf::from_iter(vec!["seg2"])) - ); - } - - #[test] - fn test_get_param_by_name() { - let mut params = Params::new(); - params.add_static("item1", "path"); - params.add_static("item2", "http%3A%2F%2Flocalhost%3A80%2Ffoo"); - - assert_eq!(params.get("item0"), None); - assert_eq!(params.get_decoded("item0"), None); - assert_eq!(params.get("item1"), Some("path")); - assert_eq!(params.get_decoded("item1"), Some("path".to_string())); - assert_eq!( - params.get("item2"), - Some("http%3A%2F%2Flocalhost%3A80%2Ffoo") - ); - assert_eq!( - params.get_decoded("item2"), - Some("http://localhost:80/foo".to_string()) - ); - } -} diff --git a/src/payload.rs b/src/payload.rs deleted file mode 100644 index 2131e3c3c..000000000 --- a/src/payload.rs +++ /dev/null @@ -1,715 +0,0 @@ -//! Payload stream -use bytes::{Bytes, BytesMut}; -#[cfg(not(test))] -use futures::task::current as current_task; -use futures::task::Task; -use futures::{Async, Poll, Stream}; -use std::cell::RefCell; -use std::cmp; -use std::collections::VecDeque; -use std::rc::{Rc, Weak}; - -use error::PayloadError; - -/// max buffer size 32k -pub(crate) const MAX_BUFFER_SIZE: usize = 32_768; - -#[derive(Debug, PartialEq)] -pub(crate) enum PayloadStatus { - Read, - Pause, - Dropped, -} - -/// Buffered stream of bytes chunks -/// -/// Payload stores chunks in a vector. First chunk can be received with -/// `.readany()` method. Payload stream is not thread safe. Payload does not -/// notify current task when new data is available. -/// -/// Payload stream can be used as `HttpResponse` body stream. -#[derive(Debug)] -pub struct Payload { - inner: Rc>, -} - -impl Payload { - /// Create payload stream. - /// - /// This method construct two objects responsible for bytes stream - /// generation. - /// - /// * `PayloadSender` - *Sender* side of the stream - /// - /// * `Payload` - *Receiver* side of the stream - pub fn new(eof: bool) -> (PayloadSender, Payload) { - let shared = Rc::new(RefCell::new(Inner::new(eof))); - - ( - PayloadSender { - inner: Rc::downgrade(&shared), - }, - Payload { inner: shared }, - ) - } - - /// Create empty payload - #[doc(hidden)] - pub fn empty() -> Payload { - Payload { - inner: Rc::new(RefCell::new(Inner::new(true))), - } - } - - /// Length of the data in this payload - #[cfg(test)] - pub fn len(&self) -> usize { - self.inner.borrow().len() - } - - /// Is payload empty - #[cfg(test)] - pub fn is_empty(&self) -> bool { - self.inner.borrow().len() == 0 - } - - /// Put unused data back to payload - #[inline] - pub fn unread_data(&mut self, data: Bytes) { - self.inner.borrow_mut().unread_data(data); - } - - #[cfg(test)] - pub(crate) fn readall(&self) -> Option { - self.inner.borrow_mut().readall() - } - - #[inline] - /// Set read buffer capacity - /// - /// Default buffer capacity is 32Kb. - pub fn set_read_buffer_capacity(&mut self, cap: usize) { - self.inner.borrow_mut().capacity = cap; - } -} - -impl Stream for Payload { - type Item = Bytes; - type Error = PayloadError; - - #[inline] - fn poll(&mut self) -> Poll, PayloadError> { - self.inner.borrow_mut().readany() - } -} - -impl Clone for Payload { - fn clone(&self) -> Payload { - Payload { - inner: Rc::clone(&self.inner), - } - } -} - -/// Payload writer interface. -pub(crate) trait PayloadWriter { - /// Set stream error. - fn set_error(&mut self, err: PayloadError); - - /// Write eof into a stream which closes reading side of a stream. - fn feed_eof(&mut self); - - /// Feed bytes into a payload stream - fn feed_data(&mut self, data: Bytes); - - /// Need read data - fn need_read(&self) -> PayloadStatus; -} - -/// Sender part of the payload stream -pub struct PayloadSender { - inner: Weak>, -} - -impl PayloadWriter for PayloadSender { - #[inline] - fn set_error(&mut self, err: PayloadError) { - if let Some(shared) = self.inner.upgrade() { - shared.borrow_mut().set_error(err) - } - } - - #[inline] - fn feed_eof(&mut self) { - if let Some(shared) = self.inner.upgrade() { - shared.borrow_mut().feed_eof() - } - } - - #[inline] - fn feed_data(&mut self, data: Bytes) { - if let Some(shared) = self.inner.upgrade() { - shared.borrow_mut().feed_data(data) - } - } - - #[inline] - fn need_read(&self) -> PayloadStatus { - // we check need_read only if Payload (other side) is alive, - // otherwise always return true (consume payload) - if let Some(shared) = self.inner.upgrade() { - if shared.borrow().need_read { - PayloadStatus::Read - } else { - #[cfg(not(test))] - { - if shared.borrow_mut().io_task.is_none() { - shared.borrow_mut().io_task = Some(current_task()); - } - } - PayloadStatus::Pause - } - } else { - PayloadStatus::Dropped - } - } -} - -#[derive(Debug)] -struct Inner { - len: usize, - eof: bool, - err: Option, - need_read: bool, - items: VecDeque, - capacity: usize, - task: Option, - io_task: Option, -} - -impl Inner { - fn new(eof: bool) -> Self { - Inner { - eof, - len: 0, - err: None, - items: VecDeque::new(), - need_read: true, - capacity: MAX_BUFFER_SIZE, - task: None, - io_task: None, - } - } - - #[inline] - fn set_error(&mut self, err: PayloadError) { - self.err = Some(err); - } - - #[inline] - fn feed_eof(&mut self) { - self.eof = true; - } - - #[inline] - fn feed_data(&mut self, data: Bytes) { - self.len += data.len(); - self.items.push_back(data); - self.need_read = self.len < self.capacity; - if let Some(task) = self.task.take() { - task.notify() - } - } - - #[cfg(test)] - fn len(&self) -> usize { - self.len - } - - #[cfg(test)] - pub(crate) fn readall(&mut self) -> Option { - let len = self.items.iter().map(|b| b.len()).sum(); - if len > 0 { - let mut buf = BytesMut::with_capacity(len); - for item in &self.items { - buf.extend_from_slice(item); - } - self.items = VecDeque::new(); - self.len = 0; - Some(buf.take().freeze()) - } else { - self.need_read = true; - None - } - } - - fn readany(&mut self) -> Poll, PayloadError> { - if let Some(data) = self.items.pop_front() { - self.len -= data.len(); - self.need_read = self.len < self.capacity; - #[cfg(not(test))] - { - if self.need_read && self.task.is_none() { - self.task = Some(current_task()); - } - if let Some(task) = self.io_task.take() { - task.notify() - } - } - Ok(Async::Ready(Some(data))) - } else if let Some(err) = self.err.take() { - Err(err) - } else if self.eof { - Ok(Async::Ready(None)) - } else { - self.need_read = true; - #[cfg(not(test))] - { - if self.task.is_none() { - self.task = Some(current_task()); - } - if let Some(task) = self.io_task.take() { - task.notify() - } - } - Ok(Async::NotReady) - } - } - - fn unread_data(&mut self, data: Bytes) { - self.len += data.len(); - self.items.push_front(data); - } -} - -/// Payload buffer -pub struct PayloadBuffer { - len: usize, - items: VecDeque, - stream: S, -} - -impl PayloadBuffer -where - S: Stream, -{ - /// Create new `PayloadBuffer` instance - pub fn new(stream: S) -> Self { - PayloadBuffer { - len: 0, - items: VecDeque::new(), - stream, - } - } - - /// Get mutable reference to an inner stream. - pub fn get_mut(&mut self) -> &mut S { - &mut self.stream - } - - #[inline] - fn poll_stream(&mut self) -> Poll { - self.stream.poll().map(|res| match res { - Async::Ready(Some(data)) => { - self.len += data.len(); - self.items.push_back(data); - Async::Ready(true) - } - Async::Ready(None) => Async::Ready(false), - Async::NotReady => Async::NotReady, - }) - } - - /// Read first available chunk of bytes - #[inline] - pub fn readany(&mut self) -> Poll, PayloadError> { - if let Some(data) = self.items.pop_front() { - self.len -= data.len(); - Ok(Async::Ready(Some(data))) - } else { - match self.poll_stream()? { - Async::Ready(true) => self.readany(), - Async::Ready(false) => Ok(Async::Ready(None)), - Async::NotReady => Ok(Async::NotReady), - } - } - } - - /// Check if buffer contains enough bytes - #[inline] - pub fn can_read(&mut self, size: usize) -> Poll, PayloadError> { - if size <= self.len { - Ok(Async::Ready(Some(true))) - } else { - match self.poll_stream()? { - Async::Ready(true) => self.can_read(size), - Async::Ready(false) => Ok(Async::Ready(None)), - Async::NotReady => Ok(Async::NotReady), - } - } - } - - /// Return reference to the first chunk of data - #[inline] - pub fn get_chunk(&mut self) -> Poll, PayloadError> { - if self.items.is_empty() { - match self.poll_stream()? { - Async::Ready(true) => (), - Async::Ready(false) => return Ok(Async::Ready(None)), - Async::NotReady => return Ok(Async::NotReady), - } - } - match self.items.front().map(|c| c.as_ref()) { - Some(chunk) => Ok(Async::Ready(Some(chunk))), - None => Ok(Async::NotReady), - } - } - - /// Read exact number of bytes - #[inline] - pub fn read_exact(&mut self, size: usize) -> Poll, PayloadError> { - if size <= self.len { - self.len -= size; - let mut chunk = self.items.pop_front().unwrap(); - if size < chunk.len() { - let buf = chunk.split_to(size); - self.items.push_front(chunk); - Ok(Async::Ready(Some(buf))) - } else if size == chunk.len() { - Ok(Async::Ready(Some(chunk))) - } else { - let mut buf = BytesMut::with_capacity(size); - buf.extend_from_slice(&chunk); - - while buf.len() < size { - let mut chunk = self.items.pop_front().unwrap(); - let rem = cmp::min(size - buf.len(), chunk.len()); - buf.extend_from_slice(&chunk.split_to(rem)); - if !chunk.is_empty() { - self.items.push_front(chunk); - } - } - Ok(Async::Ready(Some(buf.freeze()))) - } - } else { - match self.poll_stream()? { - Async::Ready(true) => self.read_exact(size), - Async::Ready(false) => Ok(Async::Ready(None)), - Async::NotReady => Ok(Async::NotReady), - } - } - } - - /// Remove specified amount if bytes from buffer - #[inline] - pub fn drop_bytes(&mut self, size: usize) { - if size <= self.len { - self.len -= size; - - let mut len = 0; - while len < size { - let mut chunk = self.items.pop_front().unwrap(); - let rem = cmp::min(size - len, chunk.len()); - len += rem; - if rem < chunk.len() { - chunk.split_to(rem); - self.items.push_front(chunk); - } - } - } - } - - /// Copy buffered data - pub fn copy(&mut self, size: usize) -> Poll, PayloadError> { - if size <= self.len { - let mut buf = BytesMut::with_capacity(size); - for chunk in &self.items { - if buf.len() < size { - let rem = cmp::min(size - buf.len(), chunk.len()); - buf.extend_from_slice(&chunk[..rem]); - } - if buf.len() == size { - return Ok(Async::Ready(Some(buf))); - } - } - } - - match self.poll_stream()? { - Async::Ready(true) => self.copy(size), - Async::Ready(false) => Ok(Async::Ready(None)), - Async::NotReady => Ok(Async::NotReady), - } - } - - /// Read until specified ending - pub fn read_until(&mut self, line: &[u8]) -> Poll, PayloadError> { - let mut idx = 0; - let mut num = 0; - let mut offset = 0; - let mut found = false; - let mut length = 0; - - for no in 0..self.items.len() { - { - let chunk = &self.items[no]; - for (pos, ch) in chunk.iter().enumerate() { - if *ch == line[idx] { - idx += 1; - if idx == line.len() { - num = no; - offset = pos + 1; - length += pos + 1; - found = true; - break; - } - } else { - idx = 0 - } - } - if !found { - length += chunk.len() - } - } - - if found { - let mut buf = BytesMut::with_capacity(length); - if num > 0 { - for _ in 0..num { - buf.extend_from_slice(&self.items.pop_front().unwrap()); - } - } - if offset > 0 { - let mut chunk = self.items.pop_front().unwrap(); - buf.extend_from_slice(&chunk.split_to(offset)); - if !chunk.is_empty() { - self.items.push_front(chunk) - } - } - self.len -= length; - return Ok(Async::Ready(Some(buf.freeze()))); - } - } - - match self.poll_stream()? { - Async::Ready(true) => self.read_until(line), - Async::Ready(false) => Ok(Async::Ready(None)), - Async::NotReady => Ok(Async::NotReady), - } - } - - /// Read bytes until new line delimiter - pub fn readline(&mut self) -> Poll, PayloadError> { - self.read_until(b"\n") - } - - /// Put unprocessed data back to the buffer - pub fn unprocessed(&mut self, data: Bytes) { - self.len += data.len(); - self.items.push_front(data); - } - - /// Get remaining data from the buffer - pub fn remaining(&mut self) -> Bytes { - self.items - .iter_mut() - .fold(BytesMut::new(), |mut b, c| { - b.extend_from_slice(c); - b - }).freeze() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use failure::Fail; - use futures::future::{lazy, result}; - use std::io; - use tokio::runtime::current_thread::Runtime; - - #[test] - fn test_error() { - let err: PayloadError = - io::Error::new(io::ErrorKind::Other, "ParseError").into(); - assert_eq!(format!("{}", err), "ParseError"); - assert_eq!(format!("{}", err.cause().unwrap()), "ParseError"); - - let err = PayloadError::Incomplete; - assert_eq!( - format!("{}", err), - "A payload reached EOF, but is not complete." - ); - } - - #[test] - fn test_basic() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (_, payload) = Payload::new(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(payload.len, 0); - assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); - - let res: Result<(), ()> = Ok(()); - result(res) - })).unwrap(); - } - - #[test] - fn test_eof() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (mut sender, payload) = Payload::new(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); - sender.feed_data(Bytes::from("data")); - sender.feed_eof(); - - assert_eq!( - Async::Ready(Some(Bytes::from("data"))), - payload.readany().ok().unwrap() - ); - assert_eq!(payload.len, 0); - assert_eq!(Async::Ready(None), payload.readany().ok().unwrap()); - - let res: Result<(), ()> = Ok(()); - result(res) - })).unwrap(); - } - - #[test] - fn test_err() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (mut sender, payload) = Payload::new(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); - - sender.set_error(PayloadError::Incomplete); - payload.readany().err().unwrap(); - let res: Result<(), ()> = Ok(()); - result(res) - })).unwrap(); - } - - #[test] - fn test_readany() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (mut sender, payload) = Payload::new(false); - let mut payload = PayloadBuffer::new(payload); - - sender.feed_data(Bytes::from("line1")); - sender.feed_data(Bytes::from("line2")); - - assert_eq!( - Async::Ready(Some(Bytes::from("line1"))), - payload.readany().ok().unwrap() - ); - assert_eq!(payload.len, 0); - - assert_eq!( - Async::Ready(Some(Bytes::from("line2"))), - payload.readany().ok().unwrap() - ); - assert_eq!(payload.len, 0); - - let res: Result<(), ()> = Ok(()); - result(res) - })).unwrap(); - } - - #[test] - fn test_readexactly() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (mut sender, payload) = Payload::new(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(Async::NotReady, payload.read_exact(2).ok().unwrap()); - - sender.feed_data(Bytes::from("line1")); - sender.feed_data(Bytes::from("line2")); - - assert_eq!( - Async::Ready(Some(Bytes::from_static(b"li"))), - payload.read_exact(2).ok().unwrap() - ); - assert_eq!(payload.len, 3); - - assert_eq!( - Async::Ready(Some(Bytes::from_static(b"ne1l"))), - payload.read_exact(4).ok().unwrap() - ); - assert_eq!(payload.len, 4); - - sender.set_error(PayloadError::Incomplete); - payload.read_exact(10).err().unwrap(); - - let res: Result<(), ()> = Ok(()); - result(res) - })).unwrap(); - } - - #[test] - fn test_readuntil() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (mut sender, payload) = Payload::new(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(Async::NotReady, payload.read_until(b"ne").ok().unwrap()); - - sender.feed_data(Bytes::from("line1")); - sender.feed_data(Bytes::from("line2")); - - assert_eq!( - Async::Ready(Some(Bytes::from("line"))), - payload.read_until(b"ne").ok().unwrap() - ); - assert_eq!(payload.len, 1); - - assert_eq!( - Async::Ready(Some(Bytes::from("1line2"))), - payload.read_until(b"2").ok().unwrap() - ); - assert_eq!(payload.len, 0); - - sender.set_error(PayloadError::Incomplete); - payload.read_until(b"b").err().unwrap(); - - let res: Result<(), ()> = Ok(()); - result(res) - })).unwrap(); - } - - #[test] - fn test_unread_data() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (_, mut payload) = Payload::new(false); - - payload.unread_data(Bytes::from("data")); - assert!(!payload.is_empty()); - assert_eq!(payload.len(), 4); - - assert_eq!( - Async::Ready(Some(Bytes::from("data"))), - payload.poll().ok().unwrap() - ); - - let res: Result<(), ()> = Ok(()); - result(res) - })).unwrap(); - } -} diff --git a/src/pipeline.rs b/src/pipeline.rs deleted file mode 100644 index a938f2eb2..000000000 --- a/src/pipeline.rs +++ /dev/null @@ -1,869 +0,0 @@ -use std::marker::PhantomData; -use std::rc::Rc; -use std::{io, mem}; - -use futures::sync::oneshot; -use futures::{Async, Future, Poll, Stream}; -use log::Level::Debug; - -use body::{Body, BodyStream}; -use context::{ActorHttpContext, Frame}; -use error::Error; -use handler::{AsyncResult, AsyncResultItem}; -use header::ContentEncoding; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Finished, Middleware, Response, Started}; -use server::{HttpHandlerTask, Writer, WriterState}; - -#[doc(hidden)] -pub trait PipelineHandler { - fn encoding(&self) -> ContentEncoding; - - fn handle(&self, &HttpRequest) -> AsyncResult; -} - -#[doc(hidden)] -pub struct Pipeline( - PipelineInfo, - PipelineState, - Rc>>>, -); - -enum PipelineState { - None, - Error, - Starting(StartMiddlewares), - Handler(WaitingResponse), - RunMiddlewares(RunMiddlewares), - Response(ProcessResponse), - Finishing(FinishingMiddlewares), - Completed(Completed), -} - -impl> PipelineState { - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - match *self { - PipelineState::Starting(ref mut state) => state.poll(info, mws), - PipelineState::Handler(ref mut state) => state.poll(info, mws), - PipelineState::RunMiddlewares(ref mut state) => state.poll(info, mws), - PipelineState::Finishing(ref mut state) => state.poll(info, mws), - PipelineState::Completed(ref mut state) => state.poll(info), - PipelineState::Response(ref mut state) => state.poll(info, mws), - PipelineState::None | PipelineState::Error => None, - } - } -} - -struct PipelineInfo { - req: HttpRequest, - count: u16, - context: Option>, - error: Option, - disconnected: Option, - encoding: ContentEncoding, -} - -impl PipelineInfo { - fn poll_context(&mut self) -> Poll<(), Error> { - if let Some(ref mut context) = self.context { - match context.poll() { - Err(err) => Err(err), - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(_)) => Ok(Async::Ready(())), - } - } else { - Ok(Async::Ready(())) - } - } -} - -impl> Pipeline { - pub(crate) fn new( - req: HttpRequest, mws: Rc>>>, handler: Rc, - ) -> Pipeline { - let mut info = PipelineInfo { - req, - count: 0, - error: None, - context: None, - disconnected: None, - encoding: handler.encoding(), - }; - let state = StartMiddlewares::init(&mut info, &mws, handler); - - Pipeline(info, state, mws) - } -} - -impl Pipeline { - #[inline] - fn is_done(&self) -> bool { - match self.1 { - PipelineState::None - | PipelineState::Error - | PipelineState::Starting(_) - | PipelineState::Handler(_) - | PipelineState::RunMiddlewares(_) - | PipelineState::Response(_) => true, - PipelineState::Finishing(_) | PipelineState::Completed(_) => false, - } - } -} - -impl> HttpHandlerTask for Pipeline { - fn disconnected(&mut self) { - self.0.disconnected = Some(true); - } - - fn poll_io(&mut self, io: &mut Writer) -> Poll { - let mut state = mem::replace(&mut self.1, PipelineState::None); - - loop { - if let PipelineState::Response(st) = state { - match st.poll_io(io, &mut self.0, &self.2) { - Ok(state) => { - self.1 = state; - if let Some(error) = self.0.error.take() { - return Err(error); - } else { - return Ok(Async::Ready(self.is_done())); - } - } - Err(state) => { - self.1 = state; - return Ok(Async::NotReady); - } - } - } - match state { - PipelineState::None => return Ok(Async::Ready(true)), - PipelineState::Error => { - return Err( - io::Error::new(io::ErrorKind::Other, "Internal error").into() - ) - } - _ => (), - } - - match state.poll(&mut self.0, &self.2) { - Some(st) => state = st, - None => { - return { - self.1 = state; - Ok(Async::NotReady) - } - } - } - } - } - - fn poll_completed(&mut self) -> Poll<(), Error> { - let mut state = mem::replace(&mut self.1, PipelineState::None); - loop { - match state { - PipelineState::None | PipelineState::Error => { - return Ok(Async::Ready(())) - } - _ => (), - } - - if let Some(st) = state.poll(&mut self.0, &self.2) { - state = st; - } else { - self.1 = state; - return Ok(Async::NotReady); - } - } - } -} - -type Fut = Box, Error = Error>>; - -/// Middlewares start executor -struct StartMiddlewares { - hnd: Rc, - fut: Option, - _s: PhantomData, -} - -impl> StartMiddlewares { - fn init( - info: &mut PipelineInfo, mws: &[Box>], hnd: Rc, - ) -> PipelineState { - // execute middlewares, we need this stage because middlewares could be - // non-async and we can move to next state immediately - let len = mws.len() as u16; - - loop { - if info.count == len { - let reply = hnd.handle(&info.req); - return WaitingResponse::init(info, mws, reply); - } else { - match mws[info.count as usize].start(&info.req) { - Ok(Started::Done) => info.count += 1, - Ok(Started::Response(resp)) => { - return RunMiddlewares::init(info, mws, resp); - } - Ok(Started::Future(fut)) => { - return PipelineState::Starting(StartMiddlewares { - hnd, - fut: Some(fut), - _s: PhantomData, - }) - } - Err(err) => { - return RunMiddlewares::init(info, mws, err.into()); - } - } - } - } - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - let len = mws.len() as u16; - - 'outer: loop { - match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => { - return None; - } - Ok(Async::Ready(resp)) => { - info.count += 1; - if let Some(resp) = resp { - return Some(RunMiddlewares::init(info, mws, resp)); - } - loop { - if info.count == len { - let reply = self.hnd.handle(&info.req); - return Some(WaitingResponse::init(info, mws, reply)); - } else { - let res = mws[info.count as usize].start(&info.req); - match res { - Ok(Started::Done) => info.count += 1, - Ok(Started::Response(resp)) => { - return Some(RunMiddlewares::init(info, mws, resp)); - } - Ok(Started::Future(fut)) => { - self.fut = Some(fut); - continue 'outer; - } - Err(err) => { - return Some(RunMiddlewares::init( - info, - mws, - err.into(), - )); - } - } - } - } - } - Err(err) => { - return Some(RunMiddlewares::init(info, mws, err.into())); - } - } - } - } -} - -// waiting for response -struct WaitingResponse { - fut: Box>, - _s: PhantomData, - _h: PhantomData, -} - -impl WaitingResponse { - #[inline] - fn init( - info: &mut PipelineInfo, mws: &[Box>], - reply: AsyncResult, - ) -> PipelineState { - match reply.into() { - AsyncResultItem::Ok(resp) => RunMiddlewares::init(info, mws, resp), - AsyncResultItem::Err(err) => RunMiddlewares::init(info, mws, err.into()), - AsyncResultItem::Future(fut) => PipelineState::Handler(WaitingResponse { - fut, - _s: PhantomData, - _h: PhantomData, - }), - } - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - match self.fut.poll() { - Ok(Async::NotReady) => None, - Ok(Async::Ready(resp)) => Some(RunMiddlewares::init(info, mws, resp)), - Err(err) => Some(RunMiddlewares::init(info, mws, err.into())), - } - } -} - -/// Middlewares response executor -struct RunMiddlewares { - curr: usize, - fut: Option>>, - _s: PhantomData, - _h: PhantomData, -} - -impl RunMiddlewares { - #[inline] - fn init( - info: &mut PipelineInfo, mws: &[Box>], mut resp: HttpResponse, - ) -> PipelineState { - if info.count == 0 { - return ProcessResponse::init(resp); - } - let mut curr = 0; - let len = mws.len(); - - loop { - let state = mws[curr].response(&info.req, resp); - resp = match state { - Err(err) => { - info.count = (curr + 1) as u16; - return ProcessResponse::init(err.into()); - } - Ok(Response::Done(r)) => { - curr += 1; - if curr == len { - return ProcessResponse::init(r); - } else { - r - } - } - Ok(Response::Future(fut)) => { - return PipelineState::RunMiddlewares(RunMiddlewares { - curr, - fut: Some(fut), - _s: PhantomData, - _h: PhantomData, - }); - } - }; - } - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - let len = mws.len(); - - loop { - // poll latest fut - let mut resp = match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => return None, - Ok(Async::Ready(resp)) => { - self.curr += 1; - resp - } - Err(err) => return Some(ProcessResponse::init(err.into())), - }; - - loop { - if self.curr == len { - return Some(ProcessResponse::init(resp)); - } else { - let state = mws[self.curr].response(&info.req, resp); - match state { - Err(err) => return Some(ProcessResponse::init(err.into())), - Ok(Response::Done(r)) => { - self.curr += 1; - resp = r - } - Ok(Response::Future(fut)) => { - self.fut = Some(fut); - break; - } - } - } - } - } - } -} - -struct ProcessResponse { - resp: Option, - iostate: IOState, - running: RunningState, - drain: Option>, - _s: PhantomData, - _h: PhantomData, -} - -#[derive(PartialEq, Debug)] -enum RunningState { - Running, - Paused, - Done, -} - -impl RunningState { - #[inline] - fn pause(&mut self) { - if *self != RunningState::Done { - *self = RunningState::Paused - } - } - #[inline] - fn resume(&mut self) { - if *self != RunningState::Done { - *self = RunningState::Running - } - } -} - -enum IOState { - Response, - Payload(BodyStream), - Actor(Box), - Done, -} - -impl ProcessResponse { - #[inline] - fn init(resp: HttpResponse) -> PipelineState { - PipelineState::Response(ProcessResponse { - resp: Some(resp), - iostate: IOState::Response, - running: RunningState::Running, - drain: None, - _s: PhantomData, - _h: PhantomData, - }) - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - // connection is dead at this point - match mem::replace(&mut self.iostate, IOState::Done) { - IOState::Response => Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )), - IOState::Payload(_) => Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )), - IOState::Actor(mut ctx) => { - if info.disconnected.take().is_some() { - ctx.disconnected(); - } - loop { - match ctx.poll() { - Ok(Async::Ready(Some(vec))) => { - if vec.is_empty() { - continue; - } - for frame in vec { - match frame { - Frame::Chunk(None) => { - info.context = Some(ctx); - return Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - Frame::Chunk(Some(_)) => (), - Frame::Drain(fut) => { - let _ = fut.send(()); - } - } - } - } - Ok(Async::Ready(None)) => { - return Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )) - } - Ok(Async::NotReady) => { - self.iostate = IOState::Actor(ctx); - return None; - } - Err(err) => { - info.context = Some(ctx); - info.error = Some(err); - return Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - } - } - } - IOState::Done => Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )), - } - } - - fn poll_io( - mut self, io: &mut Writer, info: &mut PipelineInfo, - mws: &[Box>], - ) -> Result, PipelineState> { - loop { - if self.drain.is_none() && self.running != RunningState::Paused { - // if task is paused, write buffer is probably full - 'inner: loop { - let result = match mem::replace(&mut self.iostate, IOState::Done) { - IOState::Response => { - let encoding = self - .resp - .as_ref() - .unwrap() - .content_encoding() - .unwrap_or(info.encoding); - - let result = match io.start( - &info.req, - self.resp.as_mut().unwrap(), - encoding, - ) { - Ok(res) => res, - Err(err) => { - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - }; - - if let Some(err) = self.resp.as_ref().unwrap().error() { - if self.resp.as_ref().unwrap().status().is_server_error() - { - error!( - "Error occurred during request handling, status: {} {}", - self.resp.as_ref().unwrap().status(), err - ); - } else { - warn!( - "Error occurred during request handling: {}", - err - ); - } - if log_enabled!(Debug) { - debug!("{:?}", err); - } - } - - // always poll stream or actor for the first time - match self.resp.as_mut().unwrap().replace_body(Body::Empty) { - Body::Streaming(stream) => { - self.iostate = IOState::Payload(stream); - continue 'inner; - } - Body::Actor(ctx) => { - self.iostate = IOState::Actor(ctx); - continue 'inner; - } - _ => (), - } - - result - } - IOState::Payload(mut body) => match body.poll() { - Ok(Async::Ready(None)) => { - if let Err(err) = io.write_eof() { - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - break; - } - Ok(Async::Ready(Some(chunk))) => { - self.iostate = IOState::Payload(body); - match io.write(&chunk.into()) { - Err(err) => { - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - Ok(result) => result, - } - } - Ok(Async::NotReady) => { - self.iostate = IOState::Payload(body); - break; - } - Err(err) => { - info.error = Some(err); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - }, - IOState::Actor(mut ctx) => { - if info.disconnected.take().is_some() { - ctx.disconnected(); - } - match ctx.poll() { - Ok(Async::Ready(Some(vec))) => { - if vec.is_empty() { - self.iostate = IOState::Actor(ctx); - break; - } - let mut res = None; - for frame in vec { - match frame { - Frame::Chunk(None) => { - info.context = Some(ctx); - if let Err(err) = io.write_eof() { - info.error = Some(err.into()); - return Ok( - FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - ), - ); - } - break 'inner; - } - Frame::Chunk(Some(chunk)) => match io - .write(&chunk) - { - Err(err) => { - info.context = Some(ctx); - info.error = Some(err.into()); - return Ok( - FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - ), - ); - } - Ok(result) => res = Some(result), - }, - Frame::Drain(fut) => self.drain = Some(fut), - } - } - self.iostate = IOState::Actor(ctx); - if self.drain.is_some() { - self.running.resume(); - break 'inner; - } - res.unwrap() - } - Ok(Async::Ready(None)) => break, - Ok(Async::NotReady) => { - self.iostate = IOState::Actor(ctx); - break; - } - Err(err) => { - info.context = Some(ctx); - info.error = Some(err); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - } - } - IOState::Done => break, - }; - - match result { - WriterState::Pause => { - self.running.pause(); - break; - } - WriterState::Done => self.running.resume(), - } - } - } - - // flush io but only if we need to - if self.running == RunningState::Paused || self.drain.is_some() { - match io.poll_completed(false) { - Ok(Async::Ready(_)) => { - self.running.resume(); - - // resolve drain futures - if let Some(tx) = self.drain.take() { - let _ = tx.send(()); - } - // restart io processing - continue; - } - Ok(Async::NotReady) => return Err(PipelineState::Response(self)), - Err(err) => { - if let IOState::Actor(mut ctx) = - mem::replace(&mut self.iostate, IOState::Done) - { - ctx.disconnected(); - info.context = Some(ctx); - } - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - } - } - break; - } - - // response is completed - match self.iostate { - IOState::Done => { - match io.write_eof() { - Ok(_) => (), - Err(err) => { - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - } - self.resp.as_mut().unwrap().set_response_size(io.written()); - Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )) - } - _ => Err(PipelineState::Response(self)), - } - } -} - -/// Middlewares start executor -struct FinishingMiddlewares { - resp: Option, - fut: Option>>, - _s: PhantomData, - _h: PhantomData, -} - -impl FinishingMiddlewares { - #[inline] - fn init( - info: &mut PipelineInfo, mws: &[Box>], resp: HttpResponse, - ) -> PipelineState { - if info.count == 0 { - resp.release(); - Completed::init(info) - } else { - let mut state = FinishingMiddlewares { - resp: Some(resp), - fut: None, - _s: PhantomData, - _h: PhantomData, - }; - if let Some(st) = state.poll(info, mws) { - st - } else { - PipelineState::Finishing(state) - } - } - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - loop { - // poll latest fut - let not_ready = if let Some(ref mut fut) = self.fut { - match fut.poll() { - Ok(Async::NotReady) => true, - Ok(Async::Ready(())) => false, - Err(err) => { - error!("Middleware finish error: {}", err); - false - } - } - } else { - false - }; - if not_ready { - return None; - } - self.fut = None; - if info.count == 0 { - self.resp.take().unwrap().release(); - return Some(Completed::init(info)); - } - - info.count -= 1; - let state = - mws[info.count as usize].finish(&info.req, self.resp.as_ref().unwrap()); - match state { - Finished::Done => { - if info.count == 0 { - self.resp.take().unwrap().release(); - return Some(Completed::init(info)); - } - } - Finished::Future(fut) => { - self.fut = Some(fut); - } - } - } - } -} - -#[derive(Debug)] -struct Completed(PhantomData, PhantomData); - -impl Completed { - #[inline] - fn init(info: &mut PipelineInfo) -> PipelineState { - if let Some(ref err) = info.error { - error!("Error occurred during request handling: {}", err); - } - - if info.context.is_none() { - PipelineState::None - } else { - match info.poll_context() { - Ok(Async::NotReady) => { - PipelineState::Completed(Completed(PhantomData, PhantomData)) - } - Ok(Async::Ready(())) => PipelineState::None, - Err(_) => PipelineState::Error, - } - } - } - - #[inline] - fn poll(&mut self, info: &mut PipelineInfo) -> Option> { - match info.poll_context() { - Ok(Async::NotReady) => None, - Ok(Async::Ready(())) => Some(PipelineState::None), - Err(_) => Some(PipelineState::Error), - } - } -} diff --git a/src/pred.rs b/src/pred.rs deleted file mode 100644 index 99d6e608b..000000000 --- a/src/pred.rs +++ /dev/null @@ -1,328 +0,0 @@ -//! Route match predicates -#![allow(non_snake_case)] -use std::marker::PhantomData; - -use http; -use http::{header, HttpTryFrom}; -use server::message::Request; - -/// Trait defines resource route predicate. -/// Predicate can modify request object. It is also possible to -/// to store extra attributes on request by using `Extensions` container, -/// Extensions container available via `HttpRequest::extensions()` method. -pub trait Predicate { - /// Check if request matches predicate - fn check(&self, &Request, &S) -> bool; -} - -/// Return predicate that matches if any of supplied predicate matches. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{pred, App, HttpResponse}; -/// -/// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter(pred::Any(pred::Get()).or(pred::Post())) -/// .f(|r| HttpResponse::MethodNotAllowed()) -/// }); -/// } -/// ``` -pub fn Any + 'static>(pred: P) -> AnyPredicate { - AnyPredicate(vec![Box::new(pred)]) -} - -/// Matches if any of supplied predicate matches. -pub struct AnyPredicate(Vec>>); - -impl AnyPredicate { - /// Add new predicate to list of predicates to check - pub fn or + 'static>(mut self, pred: P) -> Self { - self.0.push(Box::new(pred)); - self - } -} - -impl Predicate for AnyPredicate { - fn check(&self, req: &Request, state: &S) -> bool { - for p in &self.0 { - if p.check(req, state) { - return true; - } - } - false - } -} - -/// Return predicate that matches if all of supplied predicate matches. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{pred, App, HttpResponse}; -/// -/// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter( -/// pred::All(pred::Get()) -/// .and(pred::Header("content-type", "text/plain")), -/// ) -/// .f(|_| HttpResponse::MethodNotAllowed()) -/// }); -/// } -/// ``` -pub fn All + 'static>(pred: P) -> AllPredicate { - AllPredicate(vec![Box::new(pred)]) -} - -/// Matches if all of supplied predicate matches. -pub struct AllPredicate(Vec>>); - -impl AllPredicate { - /// Add new predicate to list of predicates to check - pub fn and + 'static>(mut self, pred: P) -> Self { - self.0.push(Box::new(pred)); - self - } -} - -impl Predicate for AllPredicate { - fn check(&self, req: &Request, state: &S) -> bool { - for p in &self.0 { - if !p.check(req, state) { - return false; - } - } - true - } -} - -/// Return predicate that matches if supplied predicate does not match. -pub fn Not + 'static>(pred: P) -> NotPredicate { - NotPredicate(Box::new(pred)) -} - -#[doc(hidden)] -pub struct NotPredicate(Box>); - -impl Predicate for NotPredicate { - fn check(&self, req: &Request, state: &S) -> bool { - !self.0.check(req, state) - } -} - -/// Http method predicate -#[doc(hidden)] -pub struct MethodPredicate(http::Method, PhantomData); - -impl Predicate for MethodPredicate { - fn check(&self, req: &Request, _: &S) -> bool { - *req.method() == self.0 - } -} - -/// Predicate to match *GET* http method -pub fn Get() -> MethodPredicate { - MethodPredicate(http::Method::GET, PhantomData) -} - -/// Predicate to match *POST* http method -pub fn Post() -> MethodPredicate { - MethodPredicate(http::Method::POST, PhantomData) -} - -/// Predicate to match *PUT* http method -pub fn Put() -> MethodPredicate { - MethodPredicate(http::Method::PUT, PhantomData) -} - -/// Predicate to match *DELETE* http method -pub fn Delete() -> MethodPredicate { - MethodPredicate(http::Method::DELETE, PhantomData) -} - -/// Predicate to match *HEAD* http method -pub fn Head() -> MethodPredicate { - MethodPredicate(http::Method::HEAD, PhantomData) -} - -/// Predicate to match *OPTIONS* http method -pub fn Options() -> MethodPredicate { - MethodPredicate(http::Method::OPTIONS, PhantomData) -} - -/// Predicate to match *CONNECT* http method -pub fn Connect() -> MethodPredicate { - MethodPredicate(http::Method::CONNECT, PhantomData) -} - -/// Predicate to match *PATCH* http method -pub fn Patch() -> MethodPredicate { - MethodPredicate(http::Method::PATCH, PhantomData) -} - -/// Predicate to match *TRACE* http method -pub fn Trace() -> MethodPredicate { - MethodPredicate(http::Method::TRACE, PhantomData) -} - -/// Predicate to match specified http method -pub fn Method(method: http::Method) -> MethodPredicate { - MethodPredicate(method, PhantomData) -} - -/// Return predicate that matches if request contains specified header and -/// value. -pub fn Header( - name: &'static str, value: &'static str, -) -> HeaderPredicate { - HeaderPredicate( - header::HeaderName::try_from(name).unwrap(), - header::HeaderValue::from_static(value), - PhantomData, - ) -} - -#[doc(hidden)] -pub struct HeaderPredicate(header::HeaderName, header::HeaderValue, PhantomData); - -impl Predicate for HeaderPredicate { - fn check(&self, req: &Request, _: &S) -> bool { - if let Some(val) = req.headers().get(&self.0) { - return val == self.1; - } - false - } -} - -/// Return predicate that matches if request contains specified Host name. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{pred, App, HttpResponse}; -/// -/// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter(pred::Host("www.rust-lang.org")) -/// .f(|_| HttpResponse::MethodNotAllowed()) -/// }); -/// } -/// ``` -pub fn Host>(host: H) -> HostPredicate { - HostPredicate(host.as_ref().to_string(), None, PhantomData) -} - -#[doc(hidden)] -pub struct HostPredicate(String, Option, PhantomData); - -impl HostPredicate { - /// Set reuest scheme to match - pub fn scheme>(&mut self, scheme: H) { - self.1 = Some(scheme.as_ref().to_string()) - } -} - -impl Predicate for HostPredicate { - fn check(&self, req: &Request, _: &S) -> bool { - let info = req.connection_info(); - if let Some(ref scheme) = self.1 { - self.0 == info.host() && scheme == info.scheme() - } else { - self.0 == info.host() - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use http::{header, Method}; - use test::TestRequest; - - #[test] - fn test_header() { - let req = TestRequest::with_header( - header::TRANSFER_ENCODING, - header::HeaderValue::from_static("chunked"), - ).finish(); - - let pred = Header("transfer-encoding", "chunked"); - assert!(pred.check(&req, req.state())); - - let pred = Header("transfer-encoding", "other"); - assert!(!pred.check(&req, req.state())); - - let pred = Header("content-type", "other"); - assert!(!pred.check(&req, req.state())); - } - - #[test] - fn test_host() { - let req = TestRequest::default() - .header( - header::HOST, - header::HeaderValue::from_static("www.rust-lang.org"), - ).finish(); - - let pred = Host("www.rust-lang.org"); - assert!(pred.check(&req, req.state())); - - let pred = Host("localhost"); - assert!(!pred.check(&req, req.state())); - } - - #[test] - fn test_methods() { - let req = TestRequest::default().finish(); - let req2 = TestRequest::default().method(Method::POST).finish(); - - assert!(Get().check(&req, req.state())); - assert!(!Get().check(&req2, req2.state())); - assert!(Post().check(&req2, req2.state())); - assert!(!Post().check(&req, req.state())); - - let r = TestRequest::default().method(Method::PUT).finish(); - assert!(Put().check(&r, r.state())); - assert!(!Put().check(&req, req.state())); - - let r = TestRequest::default().method(Method::DELETE).finish(); - assert!(Delete().check(&r, r.state())); - assert!(!Delete().check(&req, req.state())); - - let r = TestRequest::default().method(Method::HEAD).finish(); - assert!(Head().check(&r, r.state())); - assert!(!Head().check(&req, req.state())); - - let r = TestRequest::default().method(Method::OPTIONS).finish(); - assert!(Options().check(&r, r.state())); - assert!(!Options().check(&req, req.state())); - - let r = TestRequest::default().method(Method::CONNECT).finish(); - assert!(Connect().check(&r, r.state())); - assert!(!Connect().check(&req, req.state())); - - let r = TestRequest::default().method(Method::PATCH).finish(); - assert!(Patch().check(&r, r.state())); - assert!(!Patch().check(&req, req.state())); - - let r = TestRequest::default().method(Method::TRACE).finish(); - assert!(Trace().check(&r, r.state())); - assert!(!Trace().check(&req, req.state())); - } - - #[test] - fn test_preds() { - let r = TestRequest::default().method(Method::TRACE).finish(); - - assert!(Not(Get()).check(&r, r.state())); - assert!(!Not(Trace()).check(&r, r.state())); - - assert!(All(Trace()).and(Trace()).check(&r, r.state())); - assert!(!All(Get()).and(Trace()).check(&r, r.state())); - - assert!(Any(Get()).or(Trace()).check(&r, r.state())); - assert!(!Any(Get()).or(Get()).check(&r, r.state())); - } -} diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 000000000..571431cc2 --- /dev/null +++ b/src/request.rs @@ -0,0 +1,174 @@ +use std::cell::{Ref, RefMut}; +use std::fmt; +use std::ops::Deref; +use std::rc::Rc; + +use actix_http::http::{HeaderMap, Method, Uri, Version}; +use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; +use actix_router::{Path, Url}; +use futures::future::{ok, FutureResult}; + +use crate::handler::FromRequest; +use crate::service::ServiceRequest; + +#[derive(Clone)] +pub struct HttpRequest { + head: Message, + pub(crate) path: Path, + extensions: Rc, +} + +impl HttpRequest { + #[inline] + pub fn new( + head: Message, + path: Path, + extensions: Rc, + ) -> HttpRequest { + HttpRequest { + head, + path, + extensions, + } + } +} + +impl HttpRequest { + /// This method returns reference to the request head + #[inline] + pub fn head(&self) -> &RequestHead { + &self.head + } + + /// Request's uri. + #[inline] + pub fn uri(&self) -> &Uri { + &self.head().uri + } + + /// Read the Request method. + #[inline] + pub fn method(&self) -> &Method { + &self.head().method + } + + /// Read the Request Version. + #[inline] + pub fn version(&self) -> Version { + self.head().version + } + + /// The target path of this Request. + #[inline] + pub fn path(&self) -> &str { + self.head().uri.path() + } + + #[inline] + /// Returns Request's headers. + pub fn headers(&self) -> &HeaderMap { + &self.head().headers + } + + /// The query string in the URL. + /// + /// E.g., id=10 + #[inline] + pub fn query_string(&self) -> &str { + if let Some(query) = self.uri().query().as_ref() { + query + } else { + "" + } + } + + /// Get a reference to the Path parameters. + /// + /// Params is a container for url parameters. + /// A variable segment is specified in the form `{identifier}`, + /// where the identifier can be used later in a request handler to + /// access the matched value for that segment. + #[inline] + pub fn match_info(&self) -> &Path { + &self.path + } + + /// Request extensions + #[inline] + pub fn extensions(&self) -> Ref { + self.head.extensions() + } + + /// Mutable reference to a the request's extensions + #[inline] + pub fn extensions_mut(&self) -> RefMut { + self.head.extensions_mut() + } + + /// Application extensions + #[inline] + pub fn app_extensions(&self) -> &Extensions { + &self.extensions + } + + // /// Get *ConnectionInfo* for the correct request. + // #[inline] + // pub fn connection_info(&self) -> Ref { + // ConnectionInfo::get(&*self) + // } +} + +impl Deref for HttpRequest { + type Target = RequestHead; + + fn deref(&self) -> &RequestHead { + self.head() + } +} + +impl HttpMessage for HttpRequest { + type Stream = (); + + #[inline] + fn headers(&self) -> &HeaderMap { + self.headers() + } + + #[inline] + fn take_payload(&mut self) -> Payload { + Payload::None + } +} + +impl

    FromRequest

    for HttpRequest { + type Error = Error; + type Future = FutureResult; + + #[inline] + fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + ok(req.clone()) + } +} + +impl fmt::Debug for HttpRequest { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!( + f, + "\nHttpRequest {:?} {}:{}", + self.head.version, + self.head.method, + self.path() + )?; + if !self.query_string().is_empty() { + writeln!(f, " query: ?{:?}", self.query_string())?; + } + if !self.match_info().is_empty() { + writeln!(f, " params: {:?}", self.match_info())?; + } + writeln!(f, " headers:")?; + for (key, val) in self.headers().iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } +} diff --git a/src/resource.rs b/src/resource.rs index d884dd447..88f7ae5a9 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,82 +1,65 @@ -use std::ops::Deref; +use std::cell::RefCell; use std::rc::Rc; -use futures::Future; -use http::Method; -use smallvec::SmallVec; +use actix_http::{http::Method, Error, Response}; +use actix_service::{ + ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, +}; +use futures::future::{ok, Either, FutureResult}; +use futures::{Async, Future, IntoFuture, Poll}; -use error::Error; -use handler::{AsyncResult, FromRequest, Handler, Responder}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::Middleware; -use pred; -use route::Route; -use router::ResourceDef; -use with::WithFactory; +use crate::handler::{AsyncFactory, Factory, FromRequest}; +use crate::helpers::{DefaultNewService, HttpDefaultNewService, HttpDefaultService}; +use crate::responder::Responder; +use crate::route::{CreateRouteService, Route, RouteBuilder, RouteService}; +use crate::service::{ServiceRequest, ServiceResponse}; -#[derive(Copy, Clone)] -pub(crate) struct RouteId(usize); - -/// *Resource* is an entry in route table which corresponds to requested URL. +/// Resource route definition /// -/// Resource in turn has at least one route. -/// Route consists of an object that implements `Handler` trait (handler) -/// and list of predicates (objects that implement `Predicate` trait). /// Route uses builder-like pattern for configuration. -/// During request handling, resource object iterate through all routes -/// and check all predicates for specific route, if request matches all -/// predicates route route considered matched and route handler get called. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{App, HttpResponse, http}; -/// -/// fn main() { -/// let app = App::new() -/// .resource( -/// "/", |r| r.method(http::Method::GET).f(|r| HttpResponse::Ok())) -/// .finish(); -/// } -pub struct Resource { - rdef: ResourceDef, - routes: SmallVec<[Route; 3]>, - middlewares: Rc>>>, +/// If handler is not explicitly set, default *404 Not Found* handler is used. +pub struct Resource> { + routes: Vec>, + endpoint: T, + default: Rc< + RefCell, ServiceResponse>>>>, + >, + factory_ref: Rc>>>, } -impl Resource { - /// Create new resource with specified resource definition - pub fn new(rdef: ResourceDef) -> Self { +impl

    Resource

    { + pub fn new() -> Resource

    { + let fref = Rc::new(RefCell::new(None)); + Resource { - rdef, - routes: SmallVec::new(), - middlewares: Rc::new(Vec::new()), + routes: Vec::new(), + endpoint: ResourceEndpoint::new(fref.clone()), + factory_ref: fref, + default: Rc::new(RefCell::new(None)), } } +} - /// Name of the resource - pub(crate) fn get_name(&self) -> &str { - self.rdef.name() - } - - /// Set resource name - pub fn name(&mut self, name: &str) { - self.rdef.set_name(name); - } - - /// Resource definition - pub fn rdef(&self) -> &ResourceDef { - &self.rdef +impl

    Default for Resource

    { + fn default() -> Self { + Self::new() } } -impl Resource { +impl Resource +where + T: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ /// Register a new route and return mutable reference to *Route* object. /// *Route* is used for route configuration, i.e. adding predicates, /// setting up handler. /// - /// ```rust - /// # extern crate actix_web; + /// ```rust,ignore /// use actix_web::*; /// /// fn main() { @@ -90,44 +73,72 @@ impl Resource { /// .finish(); /// } /// ``` - pub fn route(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap() + pub fn route(mut self, f: F) -> Self + where + F: FnOnce(RouteBuilder

    ) -> Route

    , + { + self.routes.push(f(Route::build())); + self } /// Register a new `GET` route. - pub fn get(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Get()) + pub fn get(mut self, f: F) -> Self + where + F: Factory + 'static, + I: FromRequest

    + 'static, + R: Responder + 'static, + { + self.routes.push(Route::get().to(f)); + self } /// Register a new `POST` route. - pub fn post(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Post()) + pub fn post(mut self, f: F) -> Self + where + F: Factory + 'static, + I: FromRequest

    + 'static, + R: Responder + 'static, + { + self.routes.push(Route::post().to(f)); + self } /// Register a new `PUT` route. - pub fn put(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Put()) + pub fn put(mut self, f: F) -> Self + where + F: Factory + 'static, + I: FromRequest

    + 'static, + R: Responder + 'static, + { + self.routes.push(Route::put().to(f)); + self } /// Register a new `DELETE` route. - pub fn delete(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Delete()) + pub fn delete(mut self, f: F) -> Self + where + F: Factory + 'static, + I: FromRequest

    + 'static, + R: Responder + 'static, + { + self.routes.push(Route::delete().to(f)); + self } /// Register a new `HEAD` route. - pub fn head(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Head()) + pub fn head(mut self, f: F) -> Self + where + F: Factory + 'static, + I: FromRequest

    + 'static, + R: Responder + 'static, + { + self.routes.push(Route::build().method(Method::HEAD).to(f)); + self } /// Register a new route and add method check to route. /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// use actix_web::*; /// fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } @@ -137,70 +148,23 @@ impl Resource { /// /// This is shortcut for: /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// # use actix_web::*; /// # fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } /// App::new().resource("/", |r| r.route().filter(pred::Get()).f(index)); /// ``` - pub fn method(&mut self, method: Method) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Method(method)) - } - - /// Register a new route and add handler object. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::*; - /// fn handler(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// - /// App::new().resource("/", |r| r.h(handler)); - /// ``` - /// - /// This is shortcut for: - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// # fn handler(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// App::new().resource("/", |r| r.route().h(handler)); - /// ``` - pub fn h>(&mut self, handler: H) { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().h(handler) - } - - /// Register a new route and add handler function. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::*; - /// fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// - /// App::new().resource("/", |r| r.f(index)); - /// ``` - /// - /// This is shortcut for: - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// # fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// App::new().resource("/", |r| r.route().f(index)); - /// ``` - pub fn f(&mut self, handler: F) + pub fn method(mut self, method: Method, f: F) -> Self where - F: Fn(&HttpRequest) -> R + 'static, - R: Responder + 'static, + F: FnOnce(RouteBuilder

    ) -> Route

    , { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().f(handler) + self.routes.push(f(Route::build().method(method))); + self } /// Register a new route and add handler. /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// use actix_web::*; /// fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } @@ -210,25 +174,25 @@ impl Resource { /// /// This is shortcut for: /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// # use actix_web::*; /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } /// App::new().resource("/", |r| r.route().with(index)); /// ``` - pub fn with(&mut self, handler: F) + pub fn to(mut self, handler: F) -> Self where - F: WithFactory, + F: Factory + 'static, + I: FromRequest

    + 'static, R: Responder + 'static, - T: FromRequest + 'static, { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().with(handler); + self.routes.push(Route::build().to(handler)); + self } /// Register a new route and add async handler. /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// # extern crate futures; /// use actix_web::*; @@ -243,7 +207,7 @@ impl Resource { /// /// This is shortcut for: /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// # extern crate futures; /// # use actix_web::*; @@ -253,72 +217,259 @@ impl Resource { /// # } /// App::new().resource("/", |r| r.route().with_async(index)); /// ``` - pub fn with_async(&mut self, handler: F) + #[allow(clippy::wrong_self_convention)] + pub fn to_async(mut self, handler: F) -> Self where - F: Fn(T) -> R + 'static, - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, + F: AsyncFactory, + I: FromRequest

    + 'static, + R: IntoFuture + 'static, + R::Item: Into, + R::Error: Into, { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().with_async(handler); + self.routes.push(Route::build().to_async(handler)); + self } /// Register a resource middleware /// /// This is similar to `App's` middlewares, but /// middlewares get invoked on resource level. - /// - /// *Note* `Middleware::finish()` fires right after response get - /// prepared. It does not wait until body get sent to peer. - pub fn middleware>(&mut self, mw: M) { - Rc::get_mut(&mut self.middlewares) - .unwrap() - .push(Box::new(mw)); + pub fn middleware( + self, + mw: F, + ) -> Resource< + P, + impl NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, + > + where + M: NewTransform< + T::Service, + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, + F: IntoNewTransform, + { + let endpoint = ApplyNewService::new(mw, self.endpoint); + Resource { + endpoint, + routes: self.routes, + default: self.default, + factory_ref: self.factory_ref, + } } - #[inline] - pub(crate) fn get_route_id(&self, req: &HttpRequest) -> Option { - for idx in 0..self.routes.len() { - if (&self.routes[idx]).check(req) { - return Some(RouteId(idx)); + /// Default resource to be used if no matching route could be found. + pub fn default_resource(mut self, f: F) -> Self + where + F: FnOnce(Resource

    ) -> R, + R: IntoNewService, + U: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + > + 'static, + { + // create and configure default resource + self.default = Rc::new(RefCell::new(Some(Rc::new(Box::new( + DefaultNewService::new(f(Resource::new()).into_new_service()), + ))))); + + self + } + + pub(crate) fn get_default( + &self, + ) -> Rc, ServiceResponse>>>>> + { + self.default.clone() + } +} + +impl IntoNewService for Resource +where + T: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + fn into_new_service(self) -> T { + *self.factory_ref.borrow_mut() = Some(ResourceFactory { + routes: self.routes, + default: self.default, + }); + + self.endpoint + } +} + +pub struct ResourceFactory

    { + routes: Vec>, + default: Rc< + RefCell, ServiceResponse>>>>, + >, +} + +impl

    NewService for ResourceFactory

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = ResourceService

    ; + type Future = CreateResourceService

    ; + + fn new_service(&self, _: &()) -> Self::Future { + let default_fut = if let Some(ref default) = *self.default.borrow() { + Some(default.new_service(&())) + } else { + None + }; + + CreateResourceService { + fut: self + .routes + .iter() + .map(|route| CreateRouteServiceItem::Future(route.new_service(&()))) + .collect(), + default: None, + default_fut, + } + } +} + +enum CreateRouteServiceItem

    { + Future(CreateRouteService

    ), + Service(RouteService

    ), +} + +pub struct CreateResourceService

    { + fut: Vec>, + default: Option, ServiceResponse>>, + default_fut: Option< + Box< + Future< + Item = HttpDefaultService, ServiceResponse>, + Error = (), + >, + >, + >, +} + +impl

    Future for CreateResourceService

    { + type Item = ResourceService

    ; + type Error = (); + + fn poll(&mut self) -> Poll { + let mut done = true; + + if let Some(ref mut fut) = self.default_fut { + match fut.poll()? { + Async::Ready(default) => self.default = Some(default), + Async::NotReady => done = false, } } - None - } - #[inline] - pub(crate) fn handle( - &self, id: RouteId, req: &HttpRequest, - ) -> AsyncResult { - if self.middlewares.is_empty() { - (&self.routes[id.0]).handle(req) + // poll http services + for item in &mut self.fut { + match item { + CreateRouteServiceItem::Future(ref mut fut) => match fut.poll()? { + Async::Ready(route) => { + *item = CreateRouteServiceItem::Service(route) + } + Async::NotReady => { + done = false; + } + }, + CreateRouteServiceItem::Service(_) => continue, + }; + } + + if done { + let routes = self + .fut + .drain(..) + .map(|item| match item { + CreateRouteServiceItem::Service(service) => service, + CreateRouteServiceItem::Future(_) => unreachable!(), + }) + .collect(); + Ok(Async::Ready(ResourceService { + routes, + default: self.default.take(), + })) } else { - (&self.routes[id.0]).compose(req.clone(), Rc::clone(&self.middlewares)) + Ok(Async::NotReady) } } } -/// Default resource -pub struct DefaultResource(Rc>); +pub struct ResourceService

    { + routes: Vec>, + default: Option, ServiceResponse>>, +} -impl Deref for DefaultResource { - type Target = Resource; +impl

    Service for ResourceService

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type Future = Either< + Box>, + Either< + Box>, + FutureResult, + >, + >; - fn deref(&self) -> &Resource { - self.0.as_ref() + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, mut req: ServiceRequest

    ) -> Self::Future { + for route in self.routes.iter_mut() { + if route.check(&mut req) { + return Either::A(route.call(req)); + } + } + if let Some(ref mut default) = self.default { + Either::B(Either::A(default.call(req))) + } else { + let req = req.into_request(); + Either::B(Either::B(ok(ServiceResponse::new( + req, + Response::NotFound().finish(), + )))) + } } } -impl Clone for DefaultResource { - fn clone(&self) -> Self { - DefaultResource(self.0.clone()) +#[doc(hidden)] +pub struct ResourceEndpoint

    { + factory: Rc>>>, +} + +impl

    ResourceEndpoint

    { + fn new(factory: Rc>>>) -> Self { + ResourceEndpoint { factory } } } -impl From> for DefaultResource { - fn from(res: Resource) -> Self { - DefaultResource(Rc::new(res)) +impl

    NewService for ResourceEndpoint

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = ResourceService

    ; + type Future = CreateResourceService

    ; + + fn new_service(&self, _: &()) -> Self::Future { + self.factory.borrow_mut().as_mut().unwrap().new_service(&()) } } diff --git a/src/responder.rs b/src/responder.rs new file mode 100644 index 000000000..5520c610c --- /dev/null +++ b/src/responder.rs @@ -0,0 +1,259 @@ +use actix_http::dev::ResponseBuilder; +use actix_http::http::StatusCode; +use actix_http::{Error, Response}; +use bytes::{Bytes, BytesMut}; +use futures::future::{err, ok, Either as EitherFuture, FutureResult}; +use futures::{Future, Poll}; + +use crate::request::HttpRequest; + +/// Trait implemented by types that generate http responses. +/// +/// Types that implement this trait can be used as the return type of a handler. +pub trait Responder { + /// The associated error which can be returned. + type Error: Into; + + /// The future response value. + type Future: Future; + + /// Convert itself to `AsyncResult` or `Error`. + fn respond_to(self, req: &HttpRequest) -> Self::Future; +} + +impl Responder for Response { + type Error = Error; + type Future = FutureResult; + + #[inline] + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(self) + } +} + +impl Responder for Option +where + T: Responder, +{ + type Error = T::Error; + type Future = EitherFuture>; + + fn respond_to(self, req: &HttpRequest) -> Self::Future { + match self { + Some(t) => EitherFuture::A(t.respond_to(req)), + None => EitherFuture::B(ok(Response::build(StatusCode::NOT_FOUND).finish())), + } + } +} + +impl Responder for Result +where + T: Responder, + E: Into, +{ + type Error = Error; + type Future = EitherFuture, FutureResult>; + + fn respond_to(self, req: &HttpRequest) -> Self::Future { + match self { + Ok(val) => EitherFuture::A(ResponseFuture::new(val.respond_to(req))), + Err(e) => EitherFuture::B(err(e.into())), + } + } +} + +impl Responder for ResponseBuilder { + type Error = Error; + type Future = FutureResult; + + #[inline] + fn respond_to(mut self, _: &HttpRequest) -> Self::Future { + ok(self.finish()) + } +} + +impl Responder for &'static str { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(Response::build(StatusCode::OK) + .content_type("text/plain; charset=utf-8") + .body(self)) + } +} + +impl Responder for &'static [u8] { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(Response::build(StatusCode::OK) + .content_type("application/octet-stream") + .body(self)) + } +} + +impl Responder for String { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(Response::build(StatusCode::OK) + .content_type("text/plain; charset=utf-8") + .body(self)) + } +} + +impl<'a> Responder for &'a String { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(Response::build(StatusCode::OK) + .content_type("text/plain; charset=utf-8") + .body(self)) + } +} + +impl Responder for Bytes { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(Response::build(StatusCode::OK) + .content_type("application/octet-stream") + .body(self)) + } +} + +impl Responder for BytesMut { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(Response::build(StatusCode::OK) + .content_type("application/octet-stream") + .body(self)) + } +} + +/// Combines two different responder types into a single type +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// # extern crate futures; +/// # use futures::future::Future; +/// use actix_web::{AsyncResponder, Either, Error, Request, Response}; +/// use futures::future::result; +/// +/// type RegisterResult = +/// Either>>; +/// +/// fn index(req: Request) -> RegisterResult { +/// if is_a_variant() { +/// // <- choose variant A +/// Either::A(Response::BadRequest().body("Bad data")) +/// } else { +/// Either::B( +/// // <- variant B +/// result(Ok(Response::Ok() +/// .content_type("text/html") +/// .body("Hello!"))) +/// .responder(), +/// ) +/// } +/// } +/// # fn is_a_variant() -> bool { true } +/// # fn main() {} +/// ``` +pub enum Either { + /// First branch of the type + A(A), + /// Second branch of the type + B(B), +} + +impl Responder for Either +where + A: Responder, + B: Responder, +{ + type Error = Error; + type Future = EitherResponder; + + fn respond_to(self, req: &HttpRequest) -> Self::Future { + match self { + Either::A(a) => EitherResponder::A(a.respond_to(req)), + Either::B(b) => EitherResponder::B(b.respond_to(req)), + } + } +} + +pub enum EitherResponder +where + A: Future, + A::Error: Into, + B: Future, + B::Error: Into, +{ + A(A), + B(B), +} + +impl Future for EitherResponder +where + A: Future, + A::Error: Into, + B: Future, + B::Error: Into, +{ + type Item = Response; + type Error = Error; + + fn poll(&mut self) -> Poll { + match self { + EitherResponder::A(ref mut fut) => Ok(fut.poll().map_err(|e| e.into())?), + EitherResponder::B(ref mut fut) => Ok(fut.poll().map_err(|e| e.into())?), + } + } +} + +impl Responder for Box> +where + I: Responder + 'static, + E: Into + 'static, +{ + type Error = Error; + type Future = Box>; + + #[inline] + fn respond_to(self, req: &HttpRequest) -> Self::Future { + let req = req.clone(); + Box::new( + self.map_err(|e| e.into()) + .and_then(move |r| ResponseFuture(r.respond_to(&req))), + ) + } +} + +pub struct ResponseFuture(T); + +impl ResponseFuture { + pub fn new(fut: T) -> Self { + ResponseFuture(fut) + } +} + +impl Future for ResponseFuture +where + T: Future, + T::Error: Into, +{ + type Item = Response; + type Error = Error; + + fn poll(&mut self) -> Poll { + Ok(self.0.poll().map_err(|e| e.into())?) + } +} diff --git a/src/route.rs b/src/route.rs index 884a367ed..574e8e34c 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,68 +1,153 @@ use std::marker::PhantomData; use std::rc::Rc; -use futures::{Async, Future, Poll}; +use actix_http::{http::Method, Error, Response}; +use actix_service::{NewService, Service}; +use futures::{Async, Future, IntoFuture, Poll}; -use error::Error; -use handler::{ - AsyncHandler, AsyncResult, AsyncResultItem, FromRequest, Handler, Responder, - RouteHandler, WrapHandler, -}; -use http::StatusCode; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{ - Finished as MiddlewareFinished, Middleware, Response as MiddlewareResponse, - Started as MiddlewareStarted, -}; -use pred::Predicate; -use with::{WithAsyncFactory, WithFactory}; +use crate::filter::{self, Filter}; +use crate::handler::{AsyncFactory, AsyncHandle, Extract, Factory, FromRequest, Handle}; +use crate::responder::Responder; +use crate::service::{ServiceRequest, ServiceResponse}; + +type BoxedRouteService = Box< + Service< + Request = Req, + Response = Res, + Error = (), + Future = Box>, + >, +>; + +type BoxedRouteNewService = Box< + NewService< + Request = Req, + Response = Res, + Error = (), + InitError = (), + Service = BoxedRouteService, + Future = Box, Error = ()>>, + >, +>; /// Resource route definition /// /// Route uses builder-like pattern for configuration. /// If handler is not explicitly set, default *404 Not Found* handler is used. -pub struct Route { - preds: Vec>>, - handler: InnerHandler, +pub struct Route

    { + service: BoxedRouteNewService, ServiceResponse>, + filters: Rc>>, } -impl Default for Route { - fn default() -> Route { - Route { - preds: Vec::new(), - handler: InnerHandler::new(|_: &_| HttpResponse::new(StatusCode::NOT_FOUND)), +impl Route

    { + pub fn build() -> RouteBuilder

    { + RouteBuilder::new() + } + + pub fn get() -> RouteBuilder

    { + RouteBuilder::new().method(Method::GET) + } + + pub fn post() -> RouteBuilder

    { + RouteBuilder::new().method(Method::POST) + } + + pub fn put() -> RouteBuilder

    { + RouteBuilder::new().method(Method::PUT) + } + + pub fn delete() -> RouteBuilder

    { + RouteBuilder::new().method(Method::DELETE) + } +} + +impl

    NewService for Route

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = RouteService

    ; + type Future = CreateRouteService

    ; + + fn new_service(&self, _: &()) -> Self::Future { + CreateRouteService { + fut: self.service.new_service(&()), + filters: self.filters.clone(), } } } -impl Route { - #[inline] - pub(crate) fn check(&self, req: &HttpRequest) -> bool { - let state = req.state(); - for pred in &self.preds { - if !pred.check(req, state) { +type RouteFuture

    = Box< + Future, ServiceResponse>, Error = ()>, +>; + +pub struct CreateRouteService

    { + fut: RouteFuture

    , + filters: Rc>>, +} + +impl

    Future for CreateRouteService

    { + type Item = RouteService

    ; + type Error = (); + + fn poll(&mut self) -> Poll { + match self.fut.poll()? { + Async::Ready(service) => Ok(Async::Ready(RouteService { + service, + filters: self.filters.clone(), + })), + Async::NotReady => Ok(Async::NotReady), + } + } +} + +pub struct RouteService

    { + service: BoxedRouteService, ServiceResponse>, + filters: Rc>>, +} + +impl

    RouteService

    { + pub fn check(&self, req: &mut ServiceRequest

    ) -> bool { + for f in self.filters.iter() { + if !f.check(req.request()) { return false; } } true } +} - #[inline] - pub(crate) fn handle(&self, req: &HttpRequest) -> AsyncResult { - self.handler.handle(req) +impl

    Service for RouteService

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type Future = Box>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready() } - #[inline] - pub(crate) fn compose( - &self, req: HttpRequest, mws: Rc>>>, - ) -> AsyncResult { - AsyncResult::future(Box::new(Compose::new(req, mws, self.handler.clone()))) + fn call(&mut self, req: Self::Request) -> Self::Future { + self.service.call(req) + } +} + +pub struct RouteBuilder

    { + filters: Vec>, + _t: PhantomData

    , +} + +impl RouteBuilder

    { + fn new() -> RouteBuilder

    { + RouteBuilder { + filters: Vec::new(), + _t: PhantomData, + } } - /// Add match predicate to route. + /// Add method match filter to the route. /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// # use actix_web::*; /// # fn main() { @@ -75,41 +160,52 @@ impl Route { /// # .finish(); /// # } /// ``` - pub fn filter + 'static>(&mut self, p: T) -> &mut Self { - self.preds.push(Box::new(p)); + pub fn method(mut self, method: Method) -> Self { + self.filters.push(Box::new(filter::Method(method))); self } - /// Set handler object. Usually call to this method is last call - /// during route configuration, so it does not return reference to self. - pub fn h>(&mut self, handler: H) { - self.handler = InnerHandler::new(handler); + /// Add filter to the route. + /// + /// ```rust,ignore + /// # extern crate actix_web; + /// # use actix_web::*; + /// # fn main() { + /// App::new().resource("/path", |r| { + /// r.route() + /// .filter(pred::Get()) + /// .filter(pred::Header("content-type", "text/plain")) + /// .f(|req| HttpResponse::Ok()) + /// }) + /// # .finish(); + /// # } + /// ``` + pub fn filter(&mut self, f: F) -> &mut Self { + self.filters.push(Box::new(f)); + self } - /// Set handler function. Usually call to this method is last call - /// during route configuration, so it does not return reference to self. - pub fn f(&mut self, handler: F) - where - F: Fn(&HttpRequest) -> R + 'static, - R: Responder + 'static, - { - self.handler = InnerHandler::new(handler); - } - - /// Set async handler function. - pub fn a(&mut self, handler: H) - where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - { - self.handler = InnerHandler::async(handler); - } + // pub fn map>( + // self, + // md: F, + // ) -> RouteServiceBuilder + // where + // T: NewService< + // Request = HandlerRequest, + // Response = HandlerRequest, + // InitError = (), + // >, + // { + // RouteServiceBuilder { + // service: md.into_new_service(), + // filters: self.filters, + // _t: PhantomData, + // } + // } /// Set handler function, use request extractor for parameters. /// - /// ```rust + /// ```rust,ignore /// # extern crate bytes; /// # extern crate actix_web; /// # extern crate futures; @@ -136,7 +232,7 @@ impl Route { /// /// It is possible to use multiple extractors for one handler function. /// - /// ```rust + /// ```rust,ignore /// # extern crate bytes; /// # extern crate actix_web; /// # extern crate futures; @@ -163,56 +259,25 @@ impl Route { /// ); // <- use `with` extractor /// } /// ``` - pub fn with(&mut self, handler: F) + pub fn to(self, handler: F) -> Route

    where - F: WithFactory + 'static, + F: Factory + 'static, + T: FromRequest

    + 'static, R: Responder + 'static, - T: FromRequest + 'static, { - self.h(handler.create()); - } - - /// Set handler function. Same as `.with()` but it allows to configure - /// extractor. Configuration closure accepts config objects as tuple. - /// - /// ```rust - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; - /// #[macro_use] extern crate serde_derive; - /// use actix_web::{http, App, Path, Result}; - /// - /// /// extract text data from request - /// fn index(body: String) -> Result { - /// Ok(format!("Body {}!", body)) - /// } - /// - /// fn main() { - /// let app = App::new().resource("/index.html", |r| { - /// r.method(http::Method::GET) - /// .with_config(index, |cfg| { // <- register handler - /// cfg.0.limit(4096); // <- limit size of the payload - /// }) - /// }); - /// } - /// ``` - pub fn with_config(&mut self, handler: F, cfg_f: C) - where - F: WithFactory, - R: Responder + 'static, - T: FromRequest + 'static, - C: FnOnce(&mut T::Config), - { - let mut cfg = ::default(); - cfg_f(&mut cfg); - self.h(handler.create_with_config(cfg)); + Route { + service: Box::new(RouteNewService::new( + Extract::new().and_then(Handle::new(handler).map_err(|_| panic!())), + )), + filters: Rc::new(self.filters), + } } /// Set async handler function, use request extractor for parameters. /// Also this method needs to be used if your handler function returns /// `impl Future<>` /// - /// ```rust + /// ```rust,ignore /// # extern crate bytes; /// # extern crate actix_web; /// # extern crate futures; @@ -237,430 +302,233 @@ impl Route { /// ); // <- use `with` extractor /// } /// ``` - pub fn with_async(&mut self, handler: F) + #[allow(clippy::wrong_self_convention)] + pub fn to_async(self, handler: F) -> Route

    where - F: WithAsyncFactory, - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, + F: AsyncFactory, + T: FromRequest

    + 'static, + R: IntoFuture + 'static, + R::Item: Into, + R::Error: Into, { - self.h(handler.create()); - } - - /// Set async handler function, use request extractor for parameters. - /// This method allows to configure extractor. Configuration closure - /// accepts config objects as tuple. - /// - /// ```rust - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; - /// #[macro_use] extern crate serde_derive; - /// use actix_web::{http, App, Error, Form}; - /// use futures::Future; - /// - /// #[derive(Deserialize)] - /// struct Info { - /// username: String, - /// } - /// - /// /// extract path info using serde - /// fn index(info: Form) -> Box> { - /// unimplemented!() - /// } - /// - /// fn main() { - /// let app = App::new().resource( - /// "/{username}/index.html", // <- define path parameters - /// |r| r.method(http::Method::GET) - /// .with_async_config(index, |cfg| { - /// cfg.0.limit(4096); - /// }), - /// ); // <- use `with` extractor - /// } - /// ``` - pub fn with_async_config(&mut self, handler: F, cfg: C) - where - F: WithAsyncFactory, - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, - C: FnOnce(&mut T::Config), - { - let mut extractor_cfg = ::default(); - cfg(&mut extractor_cfg); - self.h(handler.create_with_config(extractor_cfg)); - } -} - -/// `RouteHandler` wrapper. This struct is required because it needs to be -/// shared for resource level middlewares. -struct InnerHandler(Rc>>); - -impl InnerHandler { - #[inline] - fn new>(h: H) -> Self { - InnerHandler(Rc::new(Box::new(WrapHandler::new(h)))) - } - - #[inline] - fn async(h: H) -> Self - where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - { - InnerHandler(Rc::new(Box::new(AsyncHandler::new(h)))) - } - - #[inline] - pub fn handle(&self, req: &HttpRequest) -> AsyncResult { - self.0.handle(req) - } -} - -impl Clone for InnerHandler { - #[inline] - fn clone(&self) -> Self { - InnerHandler(Rc::clone(&self.0)) - } -} - -/// Compose resource level middlewares with route handler. -struct Compose { - info: ComposeInfo, - state: ComposeState, -} - -struct ComposeInfo { - count: usize, - req: HttpRequest, - mws: Rc>>>, - handler: InnerHandler, -} - -enum ComposeState { - Starting(StartMiddlewares), - Handler(WaitingResponse), - RunMiddlewares(RunMiddlewares), - Finishing(FinishingMiddlewares), - Completed(Response), -} - -impl ComposeState { - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - match *self { - ComposeState::Starting(ref mut state) => state.poll(info), - ComposeState::Handler(ref mut state) => state.poll(info), - ComposeState::RunMiddlewares(ref mut state) => state.poll(info), - ComposeState::Finishing(ref mut state) => state.poll(info), - ComposeState::Completed(_) => None, + Route { + service: Box::new(RouteNewService::new( + Extract::new().and_then(AsyncHandle::new(handler).map_err(|_| panic!())), + )), + filters: Rc::new(self.filters), } } } -impl Compose { - fn new( - req: HttpRequest, mws: Rc>>>, handler: InnerHandler, - ) -> Self { - let mut info = ComposeInfo { - count: 0, - req, - mws, - handler, - }; - let state = StartMiddlewares::init(&mut info); +pub struct RouteServiceBuilder { + service: T, + filters: Vec>, + _t: PhantomData<(P, U1, U2)>, +} - Compose { state, info } +// impl RouteServiceBuilder +// where +// T: NewService< +// Request = HandlerRequest, +// Response = HandlerRequest, +// Error = Error, +// InitError = (), +// >, +// { +// pub fn new>(factory: F) -> Self { +// RouteServiceBuilder { +// service: factory.into_new_service(), +// filters: Vec::new(), +// _t: PhantomData, +// } +// } + +// /// Add method match filter to the route. +// /// +// /// ```rust +// /// # extern crate actix_web; +// /// # use actix_web::*; +// /// # fn main() { +// /// App::new().resource("/path", |r| { +// /// r.route() +// /// .filter(pred::Get()) +// /// .filter(pred::Header("content-type", "text/plain")) +// /// .f(|req| HttpResponse::Ok()) +// /// }) +// /// # .finish(); +// /// # } +// /// ``` +// pub fn method(mut self, method: Method) -> Self { +// self.filters.push(Box::new(filter::Method(method))); +// self +// } + +// /// Add filter to the route. +// /// +// /// ```rust +// /// # extern crate actix_web; +// /// # use actix_web::*; +// /// # fn main() { +// /// App::new().resource("/path", |r| { +// /// r.route() +// /// .filter(pred::Get()) +// /// .filter(pred::Header("content-type", "text/plain")) +// /// .f(|req| HttpResponse::Ok()) +// /// }) +// /// # .finish(); +// /// # } +// /// ``` +// pub fn filter + 'static>(&mut self, f: F) -> &mut Self { +// self.filters.push(Box::new(f)); +// self +// } + +// pub fn map>( +// self, +// md: F, +// ) -> RouteServiceBuilder< +// impl NewService< +// Request = HandlerRequest, +// Response = HandlerRequest, +// Error = Error, +// InitError = (), +// >, +// S, +// U1, +// U2, +// > +// where +// T1: NewService< +// Request = HandlerRequest, +// Response = HandlerRequest, +// InitError = (), +// >, +// T1::Error: Into, +// { +// RouteServiceBuilder { +// service: self +// .service +// .and_then(md.into_new_service().map_err(|e| e.into())), +// filters: self.filters, +// _t: PhantomData, +// } +// } + +// pub fn to_async(self, handler: F) -> Route +// where +// F: AsyncFactory, +// P: FromRequest + 'static, +// R: IntoFuture, +// R::Item: Into, +// R::Error: Into, +// { +// Route { +// service: self +// .service +// .and_then(Extract::new(P::Config::default())) +// .then(AsyncHandle::new(handler)), +// filters: Rc::new(self.filters), +// } +// } + +// pub fn to(self, handler: F) -> Route +// where +// F: Factory + 'static, +// P: FromRequest + 'static, +// R: Responder + 'static, +// { +// Route { +// service: Box::new(RouteNewService::new( +// self.service +// .and_then(Extract::new(P::Config::default())) +// .and_then(Handle::new(handler)), +// )), +// filters: Rc::new(self.filters), +// } +// } +// } + +struct RouteNewService +where + T: NewService, Error = (Error, ServiceRequest

    )>, +{ + service: T, +} + +impl RouteNewService +where + T: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (Error, ServiceRequest

    ), + >, + T::Future: 'static, + T::Service: 'static, + ::Future: 'static, +{ + pub fn new(service: T) -> Self { + RouteNewService { service } } } -impl Future for Compose { - type Item = HttpResponse; - type Error = Error; +impl NewService for RouteNewService +where + T: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (Error, ServiceRequest

    ), + >, + T::Future: 'static, + T::Service: 'static, + ::Future: 'static, +{ + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = BoxedRouteService; + type Future = Box>; - fn poll(&mut self) -> Poll { - loop { - if let ComposeState::Completed(ref mut resp) = self.state { - let resp = resp.resp.take().unwrap(); - return Ok(Async::Ready(resp)); - } - if let Some(state) = self.state.poll(&mut self.info) { - self.state = state; - } else { - return Ok(Async::NotReady); - } - } + fn new_service(&self, _: &()) -> Self::Future { + Box::new( + self.service + .new_service(&()) + .map_err(|_| ()) + .and_then(|service| { + let service: BoxedRouteService<_, _> = + Box::new(RouteServiceWrapper { service }); + Ok(service) + }), + ) } } -/// Middlewares start executor -struct StartMiddlewares { - fut: Option, - _s: PhantomData, +struct RouteServiceWrapper>> { + service: T, } -type Fut = Box, Error = Error>>; +impl Service for RouteServiceWrapper +where + T::Future: 'static, + T: Service< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (Error, ServiceRequest

    ), + >, +{ + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type Future = Box>; -impl StartMiddlewares { - fn init(info: &mut ComposeInfo) -> ComposeState { - let len = info.mws.len(); - - loop { - if info.count == len { - let reply = info.handler.handle(&info.req); - return WaitingResponse::init(info, reply); - } else { - let result = info.mws[info.count].start(&info.req); - match result { - Ok(MiddlewareStarted::Done) => info.count += 1, - Ok(MiddlewareStarted::Response(resp)) => { - return RunMiddlewares::init(info, resp); - } - Ok(MiddlewareStarted::Future(fut)) => { - return ComposeState::Starting(StartMiddlewares { - fut: Some(fut), - _s: PhantomData, - }); - } - Err(err) => { - return RunMiddlewares::init(info, err.into()); - } - } - } - } + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready().map_err(|_| ()) } - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - let len = info.mws.len(); - - 'outer: loop { - match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => { - return None; - } - Ok(Async::Ready(resp)) => { - info.count += 1; - if let Some(resp) = resp { - return Some(RunMiddlewares::init(info, resp)); - } - loop { - if info.count == len { - let reply = info.handler.handle(&info.req); - return Some(WaitingResponse::init(info, reply)); - } else { - let result = info.mws[info.count].start(&info.req); - match result { - Ok(MiddlewareStarted::Done) => info.count += 1, - Ok(MiddlewareStarted::Response(resp)) => { - return Some(RunMiddlewares::init(info, resp)); - } - Ok(MiddlewareStarted::Future(fut)) => { - self.fut = Some(fut); - continue 'outer; - } - Err(err) => { - return Some(RunMiddlewares::init(info, err.into())); - } - } - } - } - } - Err(err) => { - return Some(RunMiddlewares::init(info, err.into())); - } - } - } - } -} - -type HandlerFuture = Future; - -// waiting for response -struct WaitingResponse { - fut: Box, - _s: PhantomData, -} - -impl WaitingResponse { - #[inline] - fn init( - info: &mut ComposeInfo, reply: AsyncResult, - ) -> ComposeState { - match reply.into() { - AsyncResultItem::Ok(resp) => RunMiddlewares::init(info, resp), - AsyncResultItem::Err(err) => RunMiddlewares::init(info, err.into()), - AsyncResultItem::Future(fut) => ComposeState::Handler(WaitingResponse { - fut, - _s: PhantomData, - }), - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - match self.fut.poll() { - Ok(Async::NotReady) => None, - Ok(Async::Ready(resp)) => Some(RunMiddlewares::init(info, resp)), - Err(err) => Some(RunMiddlewares::init(info, err.into())), - } - } -} - -/// Middlewares response executor -struct RunMiddlewares { - curr: usize, - fut: Option>>, - _s: PhantomData, -} - -impl RunMiddlewares { - fn init(info: &mut ComposeInfo, mut resp: HttpResponse) -> ComposeState { - let mut curr = 0; - let len = info.mws.len(); - - loop { - let state = info.mws[curr].response(&info.req, resp); - resp = match state { - Err(err) => { - info.count = curr + 1; - return FinishingMiddlewares::init(info, err.into()); - } - Ok(MiddlewareResponse::Done(r)) => { - curr += 1; - if curr == len { - return FinishingMiddlewares::init(info, r); - } else { - r - } - } - Ok(MiddlewareResponse::Future(fut)) => { - return ComposeState::RunMiddlewares(RunMiddlewares { - curr, - fut: Some(fut), - _s: PhantomData, - }); - } - }; - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - let len = info.mws.len(); - - loop { - // poll latest fut - let mut resp = match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => return None, - Ok(Async::Ready(resp)) => { - self.curr += 1; - resp - } - Err(err) => return Some(FinishingMiddlewares::init(info, err.into())), - }; - - loop { - if self.curr == len { - return Some(FinishingMiddlewares::init(info, resp)); - } else { - let state = info.mws[self.curr].response(&info.req, resp); - match state { - Err(err) => { - return Some(FinishingMiddlewares::init(info, err.into())) - } - Ok(MiddlewareResponse::Done(r)) => { - self.curr += 1; - resp = r - } - Ok(MiddlewareResponse::Future(fut)) => { - self.fut = Some(fut); - break; - } - } - } - } - } - } -} - -/// Middlewares start executor -struct FinishingMiddlewares { - resp: Option, - fut: Option>>, - _s: PhantomData, -} - -impl FinishingMiddlewares { - fn init(info: &mut ComposeInfo, resp: HttpResponse) -> ComposeState { - if info.count == 0 { - Response::init(resp) - } else { - let mut state = FinishingMiddlewares { - resp: Some(resp), - fut: None, - _s: PhantomData, - }; - if let Some(st) = state.poll(info) { - st - } else { - ComposeState::Finishing(state) - } - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - loop { - // poll latest fut - let not_ready = if let Some(ref mut fut) = self.fut { - match fut.poll() { - Ok(Async::NotReady) => true, - Ok(Async::Ready(())) => false, - Err(err) => { - error!("Middleware finish error: {}", err); - false - } - } - } else { - false - }; - if not_ready { - return None; - } - self.fut = None; - if info.count == 0 { - return Some(Response::init(self.resp.take().unwrap())); - } - - info.count -= 1; - - let state = info.mws[info.count as usize] - .finish(&info.req, self.resp.as_ref().unwrap()); - match state { - MiddlewareFinished::Done => { - if info.count == 0 { - return Some(Response::init(self.resp.take().unwrap())); - } - } - MiddlewareFinished::Future(fut) => { - self.fut = Some(fut); - } - } - } - } -} - -struct Response { - resp: Option, - _s: PhantomData, -} - -impl Response { - fn init(resp: HttpResponse) -> ComposeState { - ComposeState::Completed(Response { - resp: Some(resp), - _s: PhantomData, - }) + fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { + Box::new(self.service.call(req).then(|res| match res { + Ok(res) => Ok(res), + Err((err, req)) => Ok(req.error_response(err)), + })) } } diff --git a/src/router.rs b/src/router.rs deleted file mode 100644 index aa15e46d2..000000000 --- a/src/router.rs +++ /dev/null @@ -1,1247 +0,0 @@ -use std::cell::RefCell; -use std::cmp::min; -use std::collections::HashMap; -use std::hash::{Hash, Hasher}; -use std::rc::Rc; - -use regex::{escape, Regex}; -use url::Url; - -use error::UrlGenerationError; -use handler::{AsyncResult, FromRequest, Responder, RouteHandler}; -use http::{Method, StatusCode}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use param::{ParamItem, Params}; -use pred::Predicate; -use resource::{DefaultResource, Resource}; -use scope::Scope; -use server::Request; -use with::WithFactory; - -#[derive(Debug, Copy, Clone, PartialEq)] -pub(crate) enum ResourceId { - Default, - Normal(u16), -} - -enum ResourcePattern { - Resource(ResourceDef), - Handler(ResourceDef, Option>>>), - Scope(ResourceDef, Vec>>), -} - -enum ResourceItem { - Resource(Resource), - Handler(Box>), - Scope(Scope), -} - -/// Interface for application router. -pub struct Router { - rmap: Rc, - patterns: Vec>, - resources: Vec>, - default: Option>, -} - -/// Information about current resource -#[derive(Clone)] -pub struct ResourceInfo { - rmap: Rc, - resource: ResourceId, - params: Params, - prefix: u16, -} - -impl ResourceInfo { - /// Name os the resource - #[inline] - pub fn name(&self) -> &str { - if let ResourceId::Normal(idx) = self.resource { - self.rmap.patterns[idx as usize].0.name() - } else { - "" - } - } - - /// This method returns reference to matched `ResourceDef` object. - #[inline] - pub fn rdef(&self) -> Option<&ResourceDef> { - if let ResourceId::Normal(idx) = self.resource { - Some(&self.rmap.patterns[idx as usize].0) - } else { - None - } - } - - pub(crate) fn set_prefix(&mut self, prefix: u16) { - self.prefix = prefix; - } - - /// Get a reference to the Params object. - /// - /// Params is a container for url parameters. - /// A variable segment is specified in the form `{identifier}`, - /// where the identifier can be used later in a request handler to - /// access the matched value for that segment. - #[inline] - pub fn match_info(&self) -> &Params { - &self.params - } - - #[inline] - pub(crate) fn merge(&mut self, info: &ResourceInfo) { - let mut p = info.params.clone(); - p.set_tail(self.params.tail); - for item in &self.params.segments { - p.add(item.0.clone(), item.1.clone()); - } - - self.prefix = info.params.tail; - self.params = p; - } - - /// Generate url for named resource - /// - /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. - /// url_for) for detailed information. - pub fn url_for( - &self, req: &Request, name: &str, elements: U, - ) -> Result - where - U: IntoIterator, - I: AsRef, - { - let mut path = String::new(); - let mut elements = elements.into_iter(); - - if self - .rmap - .patterns_for(name, &mut path, &mut elements)? - .is_some() - { - if path.starts_with('/') { - let conn = req.connection_info(); - Ok(Url::parse(&format!( - "{}://{}{}", - conn.scheme(), - conn.host(), - path - ))?) - } else { - Ok(Url::parse(&path)?) - } - } else { - Err(UrlGenerationError::ResourceNotFound) - } - } - - /// Check if application contains matching resource. - /// - /// This method does not take `prefix` into account. - /// For example if prefix is `/test` and router contains route `/name`, - /// following path would be recognizable `/test/name` but `has_resource()` call - /// would return `false`. - pub fn has_resource(&self, path: &str) -> bool { - self.rmap.has_resource(path) - } - - /// Check if application contains matching resource. - /// - /// This method does take `prefix` into account - /// but behaves like `has_route` in case `prefix` is not set in the router. - /// - /// For example if prefix is `/test` and router contains route `/name`, the - /// following path would be recognizable `/test/name` and `has_prefixed_route()` call - /// would return `true`. - /// It will not match against prefix in case it's not given. For example for `/name` - /// with a `/test` prefix would return `false` - pub fn has_prefixed_resource(&self, path: &str) -> bool { - let prefix = self.prefix as usize; - if prefix >= path.len() { - return false; - } - self.rmap.has_resource(&path[prefix..]) - } -} - -pub(crate) struct ResourceMap { - root: ResourceDef, - parent: RefCell>>, - named: HashMap, - patterns: Vec<(ResourceDef, Option>)>, - nested: Vec>, -} - -impl ResourceMap { - fn has_resource(&self, path: &str) -> bool { - let path = if path.is_empty() { "/" } else { path }; - - for (pattern, rmap) in &self.patterns { - if let Some(ref rmap) = rmap { - if let Some(plen) = pattern.is_prefix_match(path) { - return rmap.has_resource(&path[plen..]); - } - } else if pattern.is_match(path) { - return true; - } - } - false - } - - fn patterns_for( - &self, name: &str, path: &mut String, elements: &mut U, - ) -> Result, UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if self.pattern_for(name, path, elements)?.is_some() { - Ok(Some(())) - } else { - self.parent_pattern_for(name, path, elements) - } - } - - fn pattern_for( - &self, name: &str, path: &mut String, elements: &mut U, - ) -> Result, UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if let Some(pattern) = self.named.get(name) { - self.fill_root(path, elements)?; - pattern.resource_path(path, elements)?; - Ok(Some(())) - } else { - for rmap in &self.nested { - if rmap.pattern_for(name, path, elements)?.is_some() { - return Ok(Some(())); - } - } - Ok(None) - } - } - - fn fill_root( - &self, path: &mut String, elements: &mut U, - ) -> Result<(), UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if let Some(ref parent) = *self.parent.borrow() { - parent.fill_root(path, elements)?; - } - self.root.resource_path(path, elements) - } - - fn parent_pattern_for( - &self, name: &str, path: &mut String, elements: &mut U, - ) -> Result, UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if let Some(ref parent) = *self.parent.borrow() { - if let Some(pattern) = parent.named.get(name) { - self.fill_root(path, elements)?; - pattern.resource_path(path, elements)?; - Ok(Some(())) - } else { - parent.parent_pattern_for(name, path, elements) - } - } else { - Ok(None) - } - } -} - -impl Default for Router { - fn default() -> Self { - Router::new(ResourceDef::new("")) - } -} - -impl Router { - pub(crate) fn new(root: ResourceDef) -> Self { - Router { - rmap: Rc::new(ResourceMap { - root, - parent: RefCell::new(None), - named: HashMap::new(), - patterns: Vec::new(), - nested: Vec::new(), - }), - resources: Vec::new(), - patterns: Vec::new(), - default: None, - } - } - - #[inline] - pub(crate) fn route_info_params(&self, idx: u16, params: Params) -> ResourceInfo { - ResourceInfo { - params, - prefix: 0, - rmap: self.rmap.clone(), - resource: ResourceId::Normal(idx), - } - } - - #[cfg(test)] - pub(crate) fn default_route_info(&self) -> ResourceInfo { - ResourceInfo { - params: Params::new(), - rmap: self.rmap.clone(), - resource: ResourceId::Default, - prefix: 0, - } - } - - pub(crate) fn set_prefix(&mut self, path: &str) { - Rc::get_mut(&mut self.rmap).unwrap().root = ResourceDef::new(path); - } - - pub(crate) fn register_resource(&mut self, resource: Resource) { - { - let rmap = Rc::get_mut(&mut self.rmap).unwrap(); - - let name = resource.get_name(); - if !name.is_empty() { - assert!( - !rmap.named.contains_key(name), - "Named resource {:?} is registered.", - name - ); - rmap.named.insert(name.to_owned(), resource.rdef().clone()); - } - rmap.patterns.push((resource.rdef().clone(), None)); - } - self.patterns - .push(ResourcePattern::Resource(resource.rdef().clone())); - self.resources.push(ResourceItem::Resource(resource)); - } - - pub(crate) fn register_scope(&mut self, mut scope: Scope) { - Rc::get_mut(&mut self.rmap) - .unwrap() - .patterns - .push((scope.rdef().clone(), Some(scope.router().rmap.clone()))); - Rc::get_mut(&mut self.rmap) - .unwrap() - .nested - .push(scope.router().rmap.clone()); - - let filters = scope.take_filters(); - self.patterns - .push(ResourcePattern::Scope(scope.rdef().clone(), filters)); - self.resources.push(ResourceItem::Scope(scope)); - } - - pub(crate) fn register_handler( - &mut self, path: &str, hnd: Box>, - filters: Option>>>, - ) { - let rdef = ResourceDef::prefix(path); - Rc::get_mut(&mut self.rmap) - .unwrap() - .patterns - .push((rdef.clone(), None)); - self.resources.push(ResourceItem::Handler(hnd)); - self.patterns.push(ResourcePattern::Handler(rdef, filters)); - } - - pub(crate) fn has_default_resource(&self) -> bool { - self.default.is_some() - } - - pub(crate) fn register_default_resource(&mut self, resource: DefaultResource) { - self.default = Some(resource); - } - - pub(crate) fn finish(&mut self) { - for resource in &mut self.resources { - match resource { - ResourceItem::Resource(_) => (), - ResourceItem::Scope(scope) => { - if !scope.has_default_resource() { - if let Some(ref default) = self.default { - scope.default_resource(default.clone()); - } - } - *scope.router().rmap.parent.borrow_mut() = Some(self.rmap.clone()); - scope.finish(); - } - ResourceItem::Handler(hnd) => { - if !hnd.has_default_resource() { - if let Some(ref default) = self.default { - hnd.default_resource(default.clone()); - } - } - hnd.finish() - } - } - } - } - - pub(crate) fn register_external(&mut self, name: &str, rdef: ResourceDef) { - let rmap = Rc::get_mut(&mut self.rmap).unwrap(); - assert!( - !rmap.named.contains_key(name), - "Named resource {:?} is registered.", - name - ); - rmap.named.insert(name.to_owned(), rdef); - } - - pub(crate) fn register_route(&mut self, path: &str, method: Method, f: F) - where - F: WithFactory, - R: Responder + 'static, - T: FromRequest + 'static, - { - let out = { - // get resource handler - let mut iterator = self.resources.iter_mut(); - - loop { - if let Some(ref mut resource) = iterator.next() { - if let ResourceItem::Resource(ref mut resource) = resource { - if resource.rdef().pattern() == path { - resource.method(method).with(f); - break None; - } - } - } else { - let mut resource = Resource::new(ResourceDef::new(path)); - resource.method(method).with(f); - break Some(resource); - } - } - }; - if let Some(out) = out { - self.register_resource(out); - } - } - - /// Handle request - pub fn handle(&self, req: &HttpRequest) -> AsyncResult { - let resource = match req.resource().resource { - ResourceId::Normal(idx) => &self.resources[idx as usize], - ResourceId::Default => { - if let Some(ref default) = self.default { - if let Some(id) = default.get_route_id(req) { - return default.handle(id, req); - } - } - return AsyncResult::ok(HttpResponse::new(StatusCode::NOT_FOUND)); - } - }; - match resource { - ResourceItem::Resource(ref resource) => { - if let Some(id) = resource.get_route_id(req) { - return resource.handle(id, req); - } - - if let Some(ref default) = self.default { - if let Some(id) = default.get_route_id(req) { - return default.handle(id, req); - } - } - } - ResourceItem::Handler(hnd) => return hnd.handle(req), - ResourceItem::Scope(hnd) => return hnd.handle(req), - } - AsyncResult::ok(HttpResponse::new(StatusCode::NOT_FOUND)) - } - - /// Query for matched resource - pub fn recognize(&self, req: &Request, state: &S, tail: usize) -> ResourceInfo { - if tail <= req.path().len() { - 'outer: for (idx, resource) in self.patterns.iter().enumerate() { - match resource { - ResourcePattern::Resource(rdef) => { - if let Some(params) = rdef.match_with_params(req, tail) { - return self.route_info_params(idx as u16, params); - } - } - ResourcePattern::Handler(rdef, filters) => { - if let Some(params) = rdef.match_prefix_with_params(req, tail) { - if let Some(ref filters) = filters { - for filter in filters { - if !filter.check(req, state) { - continue 'outer; - } - } - } - return self.route_info_params(idx as u16, params); - } - } - ResourcePattern::Scope(rdef, filters) => { - if let Some(params) = rdef.match_prefix_with_params(req, tail) { - for filter in filters { - if !filter.check(req, state) { - continue 'outer; - } - } - return self.route_info_params(idx as u16, params); - } - } - } - } - } - ResourceInfo { - prefix: tail as u16, - params: Params::new(), - rmap: self.rmap.clone(), - resource: ResourceId::Default, - } - } -} - -#[derive(Debug, Clone, PartialEq)] -enum PatternElement { - Str(String), - Var(String), -} - -#[derive(Clone, Debug)] -enum PatternType { - Static(String), - Prefix(String), - Dynamic(Regex, Vec>, usize), -} - -#[derive(Debug, Copy, Clone, PartialEq)] -/// Resource type -pub enum ResourceType { - /// Normal resource - Normal, - /// Resource for application default handler - Default, - /// External resource - External, - /// Unknown resource type - Unset, -} - -/// Resource type describes an entry in resources table -#[derive(Clone, Debug)] -pub struct ResourceDef { - tp: PatternType, - rtp: ResourceType, - name: String, - pattern: String, - elements: Vec, -} - -impl ResourceDef { - /// Parse path pattern and create new `ResourceDef` instance. - /// - /// Panics if path pattern is wrong. - pub fn new(path: &str) -> Self { - ResourceDef::with_prefix(path, false, !path.is_empty()) - } - - /// Parse path pattern and create new `ResourceDef` instance. - /// - /// Use `prefix` type instead of `static`. - /// - /// Panics if path regex pattern is wrong. - pub fn prefix(path: &str) -> Self { - ResourceDef::with_prefix(path, true, !path.is_empty()) - } - - /// Construct external resource def - /// - /// Panics if path pattern is wrong. - pub fn external(path: &str) -> Self { - let mut resource = ResourceDef::with_prefix(path, false, false); - resource.rtp = ResourceType::External; - resource - } - - /// Parse path pattern and create new `ResourceDef` instance with custom prefix - pub fn with_prefix(path: &str, for_prefix: bool, slash: bool) -> Self { - let mut path = path.to_owned(); - if slash && !path.starts_with('/') { - path.insert(0, '/'); - } - let (pattern, elements, is_dynamic, len) = ResourceDef::parse(&path, for_prefix); - - let tp = if is_dynamic { - let re = match Regex::new(&pattern) { - Ok(re) => re, - Err(err) => panic!("Wrong path pattern: \"{}\" {}", path, err), - }; - // actix creates one router per thread - let names = re - .capture_names() - .filter_map(|name| name.map(|name| Rc::new(name.to_owned()))) - .collect(); - PatternType::Dynamic(re, names, len) - } else if for_prefix { - PatternType::Prefix(pattern.clone()) - } else { - PatternType::Static(pattern.clone()) - }; - - ResourceDef { - tp, - elements, - name: "".to_string(), - rtp: ResourceType::Normal, - pattern: path.to_owned(), - } - } - - /// Resource type - pub fn rtype(&self) -> ResourceType { - self.rtp - } - - /// Resource name - pub fn name(&self) -> &str { - &self.name - } - - /// Resource name - pub(crate) fn set_name(&mut self, name: &str) { - self.name = name.to_owned(); - } - - /// Path pattern of the resource - pub fn pattern(&self) -> &str { - &self.pattern - } - - /// Is this path a match against this resource? - pub fn is_match(&self, path: &str) -> bool { - match self.tp { - PatternType::Static(ref s) => s == path, - PatternType::Dynamic(ref re, _, _) => re.is_match(path), - PatternType::Prefix(ref s) => path.starts_with(s), - } - } - - fn is_prefix_match(&self, path: &str) -> Option { - let plen = path.len(); - let path = if path.is_empty() { "/" } else { path }; - - match self.tp { - PatternType::Static(ref s) => if s == path { - Some(plen) - } else { - None - }, - PatternType::Dynamic(ref re, _, len) => { - if let Some(captures) = re.captures(path) { - let mut pos = 0; - let mut passed = false; - for capture in captures.iter() { - if let Some(ref m) = capture { - if !passed { - passed = true; - continue; - } - - pos = m.end(); - } - } - Some(plen + pos + len) - } else { - None - } - } - PatternType::Prefix(ref s) => { - let len = if path == s { - s.len() - } else if path.starts_with(s) - && (s.ends_with('/') || path.split_at(s.len()).1.starts_with('/')) - { - if s.ends_with('/') { - s.len() - 1 - } else { - s.len() - } - } else { - return None; - }; - Some(min(plen, len)) - } - } - } - - /// Are the given path and parameters a match against this resource? - pub fn match_with_params(&self, req: &Request, plen: usize) -> Option { - let path = &req.path()[plen..]; - - match self.tp { - PatternType::Static(ref s) => if s != path { - None - } else { - Some(Params::with_url(req.url())) - }, - PatternType::Dynamic(ref re, ref names, _) => { - if let Some(captures) = re.captures(path) { - let mut params = Params::with_url(req.url()); - let mut idx = 0; - let mut passed = false; - for capture in captures.iter() { - if let Some(ref m) = capture { - if !passed { - passed = true; - continue; - } - params.add( - names[idx].clone(), - ParamItem::UrlSegment( - (plen + m.start()) as u16, - (plen + m.end()) as u16, - ), - ); - idx += 1; - } - } - params.set_tail(req.path().len() as u16); - Some(params) - } else { - None - } - } - PatternType::Prefix(ref s) => if !path.starts_with(s) { - None - } else { - Some(Params::with_url(req.url())) - }, - } - } - - /// Is the given path a prefix match and do the parameters match against this resource? - pub fn match_prefix_with_params( - &self, req: &Request, plen: usize, - ) -> Option { - let path = &req.path()[plen..]; - let path = if path.is_empty() { "/" } else { path }; - - match self.tp { - PatternType::Static(ref s) => if s == path { - let mut params = Params::with_url(req.url()); - params.set_tail(req.path().len() as u16); - Some(params) - } else { - None - }, - PatternType::Dynamic(ref re, ref names, len) => { - if let Some(captures) = re.captures(path) { - let mut params = Params::with_url(req.url()); - let mut pos = 0; - let mut passed = false; - let mut idx = 0; - for capture in captures.iter() { - if let Some(ref m) = capture { - if !passed { - passed = true; - continue; - } - - params.add( - names[idx].clone(), - ParamItem::UrlSegment( - (plen + m.start()) as u16, - (plen + m.end()) as u16, - ), - ); - idx += 1; - pos = m.end(); - } - } - params.set_tail((plen + pos + len) as u16); - Some(params) - } else { - None - } - } - PatternType::Prefix(ref s) => { - let len = if path == s { - s.len() - } else if path.starts_with(s) - && (s.ends_with('/') || path.split_at(s.len()).1.starts_with('/')) - { - if s.ends_with('/') { - s.len() - 1 - } else { - s.len() - } - } else { - return None; - }; - let mut params = Params::with_url(req.url()); - params.set_tail(min(req.path().len(), plen + len) as u16); - Some(params) - } - } - } - - /// Build resource path. - pub fn resource_path( - &self, path: &mut String, elements: &mut U, - ) -> Result<(), UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - match self.tp { - PatternType::Prefix(ref p) => path.push_str(p), - PatternType::Static(ref p) => path.push_str(p), - PatternType::Dynamic(..) => { - for el in &self.elements { - match *el { - PatternElement::Str(ref s) => path.push_str(s), - PatternElement::Var(_) => { - if let Some(val) = elements.next() { - path.push_str(val.as_ref()) - } else { - return Err(UrlGenerationError::NotEnoughElements); - } - } - } - } - } - }; - Ok(()) - } - - fn parse_param(pattern: &str) -> (PatternElement, String, &str) { - const DEFAULT_PATTERN: &str = "[^/]+"; - let mut params_nesting = 0usize; - let close_idx = pattern - .find(|c| match c { - '{' => { - params_nesting += 1; - false - } - '}' => { - params_nesting -= 1; - params_nesting == 0 - } - _ => false, - }).expect("malformed param"); - let (mut param, rem) = pattern.split_at(close_idx + 1); - param = ¶m[1..param.len() - 1]; // Remove outer brackets - let (name, pattern) = match param.find(':') { - Some(idx) => { - let (name, pattern) = param.split_at(idx); - (name, &pattern[1..]) - } - None => (param, DEFAULT_PATTERN), - }; - ( - PatternElement::Var(name.to_string()), - format!(r"(?P<{}>{})", &name, &pattern), - rem, - ) - } - - fn parse( - mut pattern: &str, for_prefix: bool, - ) -> (String, Vec, bool, usize) { - if pattern.find('{').is_none() { - return ( - String::from(pattern), - vec![PatternElement::Str(String::from(pattern))], - false, - pattern.chars().count(), - ); - }; - - let mut elems = Vec::new(); - let mut re = String::from("^"); - - while let Some(idx) = pattern.find('{') { - let (prefix, rem) = pattern.split_at(idx); - elems.push(PatternElement::Str(String::from(prefix))); - re.push_str(&escape(prefix)); - let (param_pattern, re_part, rem) = Self::parse_param(rem); - elems.push(param_pattern); - re.push_str(&re_part); - pattern = rem; - } - - elems.push(PatternElement::Str(String::from(pattern))); - re.push_str(&escape(pattern)); - - if !for_prefix { - re.push_str("$"); - } - - (re, elems, true, pattern.chars().count()) - } -} - -impl PartialEq for ResourceDef { - fn eq(&self, other: &ResourceDef) -> bool { - self.pattern == other.pattern - } -} - -impl Eq for ResourceDef {} - -impl Hash for ResourceDef { - fn hash(&self, state: &mut H) { - self.pattern.hash(state); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use test::TestRequest; - - #[test] - fn test_recognizer10() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/name"))); - router.register_resource(Resource::new(ResourceDef::new("/name/{val}"))); - router.register_resource(Resource::new(ResourceDef::new( - "/name/{val}/index.html", - ))); - router.register_resource(Resource::new(ResourceDef::new("/file/{file}.{ext}"))); - router.register_resource(Resource::new(ResourceDef::new( - "/v{val}/{val2}/index.html", - ))); - router.register_resource(Resource::new(ResourceDef::new("/v/{tail:.*}"))); - router.register_resource(Resource::new(ResourceDef::new("/test2/{test}.html"))); - router.register_resource(Resource::new(ResourceDef::new("/{test}/index.html"))); - - let req = TestRequest::with_uri("/name").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(0)); - assert!(info.match_info().is_empty()); - - let req = TestRequest::with_uri("/name/value").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(1)); - assert_eq!(info.match_info().get("val").unwrap(), "value"); - assert_eq!(&info.match_info()["val"], "value"); - - let req = TestRequest::with_uri("/name/value2/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(2)); - assert_eq!(info.match_info().get("val").unwrap(), "value2"); - - let req = TestRequest::with_uri("/file/file.gz").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(3)); - assert_eq!(info.match_info().get("file").unwrap(), "file"); - assert_eq!(info.match_info().get("ext").unwrap(), "gz"); - - let req = TestRequest::with_uri("/vtest/ttt/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(4)); - assert_eq!(info.match_info().get("val").unwrap(), "test"); - assert_eq!(info.match_info().get("val2").unwrap(), "ttt"); - - let req = TestRequest::with_uri("/v/blah-blah/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(5)); - assert_eq!( - info.match_info().get("tail").unwrap(), - "blah-blah/index.html" - ); - - let req = TestRequest::with_uri("/test2/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(6)); - assert_eq!(info.match_info().get("test").unwrap(), "index"); - - let req = TestRequest::with_uri("/bbb/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(7)); - assert_eq!(info.match_info().get("test").unwrap(), "bbb"); - } - - #[test] - fn test_recognizer_2() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/index.json"))); - router.register_resource(Resource::new(ResourceDef::new("/{source}.json"))); - - let req = TestRequest::with_uri("/index.json").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(0)); - - let req = TestRequest::with_uri("/test.json").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(1)); - } - - #[test] - fn test_recognizer_with_prefix() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/name"))); - router.register_resource(Resource::new(ResourceDef::new("/name/{val}"))); - - let req = TestRequest::with_uri("/name").finish(); - let info = router.recognize(&req, &(), 5); - assert_eq!(info.resource, ResourceId::Default); - - let req = TestRequest::with_uri("/test/name").finish(); - let info = router.recognize(&req, &(), 5); - assert_eq!(info.resource, ResourceId::Normal(0)); - - let req = TestRequest::with_uri("/test/name/value").finish(); - let info = router.recognize(&req, &(), 5); - assert_eq!(info.resource, ResourceId::Normal(1)); - assert_eq!(info.match_info().get("val").unwrap(), "value"); - assert_eq!(&info.match_info()["val"], "value"); - - // same patterns - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/name"))); - router.register_resource(Resource::new(ResourceDef::new("/name/{val}"))); - - let req = TestRequest::with_uri("/name").finish(); - let info = router.recognize(&req, &(), 6); - assert_eq!(info.resource, ResourceId::Default); - - let req = TestRequest::with_uri("/test2/name").finish(); - let info = router.recognize(&req, &(), 6); - assert_eq!(info.resource, ResourceId::Normal(0)); - - let req = TestRequest::with_uri("/test2/name-test").finish(); - let info = router.recognize(&req, &(), 6); - assert_eq!(info.resource, ResourceId::Default); - - let req = TestRequest::with_uri("/test2/name/ttt").finish(); - let info = router.recognize(&req, &(), 6); - assert_eq!(info.resource, ResourceId::Normal(1)); - assert_eq!(&info.match_info()["val"], "ttt"); - } - - #[test] - fn test_parse_static() { - let re = ResourceDef::new("/"); - assert!(re.is_match("/")); - assert!(!re.is_match("/a")); - - let re = ResourceDef::new("/name"); - assert!(re.is_match("/name")); - assert!(!re.is_match("/name1")); - assert!(!re.is_match("/name/")); - assert!(!re.is_match("/name~")); - - let re = ResourceDef::new("/name/"); - assert!(re.is_match("/name/")); - assert!(!re.is_match("/name")); - assert!(!re.is_match("/name/gs")); - - let re = ResourceDef::new("/user/profile"); - assert!(re.is_match("/user/profile")); - assert!(!re.is_match("/user/profile/profile")); - } - - #[test] - fn test_parse_param() { - let re = ResourceDef::new("/user/{id}"); - assert!(re.is_match("/user/profile")); - assert!(re.is_match("/user/2345")); - assert!(!re.is_match("/user/2345/")); - assert!(!re.is_match("/user/2345/sdg")); - - let req = TestRequest::with_uri("/user/profile").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(info.get("id").unwrap(), "profile"); - - let req = TestRequest::with_uri("/user/1245125").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(info.get("id").unwrap(), "1245125"); - - let re = ResourceDef::new("/v{version}/resource/{id}"); - assert!(re.is_match("/v1/resource/320120")); - assert!(!re.is_match("/v/resource/1")); - assert!(!re.is_match("/resource")); - - let req = TestRequest::with_uri("/v151/resource/adahg32").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(info.get("version").unwrap(), "151"); - assert_eq!(info.get("id").unwrap(), "adahg32"); - - let re = ResourceDef::new("/{id:[[:digit:]]{6}}"); - assert!(re.is_match("/012345")); - assert!(!re.is_match("/012")); - assert!(!re.is_match("/01234567")); - assert!(!re.is_match("/XXXXXX")); - - let req = TestRequest::with_uri("/012345").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(info.get("id").unwrap(), "012345"); - } - - #[test] - fn test_resource_prefix() { - let re = ResourceDef::prefix("/name"); - assert!(re.is_match("/name")); - assert!(re.is_match("/name/")); - assert!(re.is_match("/name/test/test")); - assert!(re.is_match("/name1")); - assert!(re.is_match("/name~")); - - let re = ResourceDef::prefix("/name/"); - assert!(re.is_match("/name/")); - assert!(re.is_match("/name/gs")); - assert!(!re.is_match("/name")); - } - - #[test] - fn test_reousrce_prefix_dynamic() { - let re = ResourceDef::prefix("/{name}/"); - assert!(re.is_match("/name/")); - assert!(re.is_match("/name/gs")); - assert!(!re.is_match("/name")); - - let req = TestRequest::with_uri("/test2/").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(&info["name"], "test2"); - assert_eq!(&info[0], "test2"); - - let req = TestRequest::with_uri("/test2/subpath1/subpath2/index.html").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(&info["name"], "test2"); - assert_eq!(&info[0], "test2"); - } - - #[test] - fn test_request_resource() { - let mut router = Router::<()>::default(); - let mut resource = Resource::new(ResourceDef::new("/index.json")); - resource.name("r1"); - router.register_resource(resource); - let mut resource = Resource::new(ResourceDef::new("/test.json")); - resource.name("r2"); - router.register_resource(resource); - - let req = TestRequest::with_uri("/index.json").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(0)); - - assert_eq!(info.name(), "r1"); - - let req = TestRequest::with_uri("/test.json").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(1)); - assert_eq!(info.name(), "r2"); - } - - #[test] - fn test_has_resource() { - let mut router = Router::<()>::default(); - let scope = Scope::new("/test").resource("/name", |_| "done"); - router.register_scope(scope); - - { - let info = router.default_route_info(); - assert!(!info.has_resource("/test")); - assert!(info.has_resource("/test/name")); - } - - let scope = - Scope::new("/test2").nested("/test10", |s| s.resource("/name", |_| "done")); - router.register_scope(scope); - - let info = router.default_route_info(); - assert!(info.has_resource("/test2/test10/name")); - } - - #[test] - fn test_url_for() { - let mut router = Router::<()>::new(ResourceDef::prefix("")); - - let mut resource = Resource::new(ResourceDef::new("/tttt")); - resource.name("r0"); - router.register_resource(resource); - - let scope = Scope::new("/test").resource("/name", |r| { - r.name("r1"); - }); - router.register_scope(scope); - - let scope = Scope::new("/test2") - .nested("/test10", |s| s.resource("/name", |r| r.name("r2"))); - router.register_scope(scope); - router.finish(); - - let req = TestRequest::with_uri("/test").request(); - { - let info = router.default_route_info(); - - let res = info - .url_for(&req, "r0", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/tttt"); - - let res = info - .url_for(&req, "r1", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/test/name"); - - let res = info - .url_for(&req, "r2", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/test2/test10/name"); - } - - let req = TestRequest::with_uri("/test/name").request(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(1)); - - let res = info - .url_for(&req, "r0", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/tttt"); - - let res = info - .url_for(&req, "r1", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/test/name"); - - let res = info - .url_for(&req, "r2", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/test2/test10/name"); - } - - #[test] - fn test_url_for_dynamic() { - let mut router = Router::<()>::new(ResourceDef::prefix("")); - - let mut resource = Resource::new(ResourceDef::new("/{name}/test/index.{ext}")); - resource.name("r0"); - router.register_resource(resource); - - let scope = Scope::new("/{name1}").nested("/{name2}", |s| { - s.resource("/{name3}/test/index.{ext}", |r| r.name("r2")) - }); - router.register_scope(scope); - router.finish(); - - let req = TestRequest::with_uri("/test").request(); - { - let info = router.default_route_info(); - - let res = info.url_for(&req, "r0", vec!["sec1", "html"]).unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/sec1/test/index.html"); - - let res = info - .url_for(&req, "r2", vec!["sec1", "sec2", "sec3", "html"]) - .unwrap(); - assert_eq!( - res.as_str(), - "http://localhost:8080/sec1/sec2/sec3/test/index.html" - ); - } - } -} diff --git a/src/scope.rs b/src/scope.rs deleted file mode 100644 index fb9e7514a..000000000 --- a/src/scope.rs +++ /dev/null @@ -1,1236 +0,0 @@ -use std::marker::PhantomData; -use std::mem; -use std::rc::Rc; - -use futures::{Async, Future, Poll}; - -use error::Error; -use handler::{ - AsyncResult, AsyncResultItem, FromRequest, Handler, Responder, RouteHandler, - WrapHandler, -}; -use http::Method; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{ - Finished as MiddlewareFinished, Middleware, Response as MiddlewareResponse, - Started as MiddlewareStarted, -}; -use pred::Predicate; -use resource::{DefaultResource, Resource}; -use router::{ResourceDef, Router}; -use server::Request; -use with::WithFactory; - -/// Resources scope -/// -/// Scope is a set of resources with common root path. -/// Scopes collect multiple paths under a common path prefix. -/// Scope path can contain variable path segments as resources. -/// Scope prefix is always complete path segment, i.e `/app` would -/// be converted to a `/app/` and it would not match `/app` path. -/// -/// You can get variable path segments from `HttpRequest::match_info()`. -/// `Path` extractor also is able to extract scope level variable segments. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{http, App, HttpRequest, HttpResponse}; -/// -/// fn main() { -/// let app = App::new().scope("/{project_id}/", |scope| { -/// scope -/// .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) -/// .resource("/path2", |r| r.f(|_| HttpResponse::Ok())) -/// .resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed())) -/// }); -/// } -/// ``` -/// -/// In the above example three routes get registered: -/// * /{project_id}/path1 - reponds to all http method -/// * /{project_id}/path2 - `GET` requests -/// * /{project_id}/path3 - `HEAD` requests -/// -pub struct Scope { - rdef: ResourceDef, - router: Rc>, - filters: Vec>>, - middlewares: Rc>>>, -} - -#[cfg_attr( - feature = "cargo-clippy", - allow(new_without_default_derive) -)] -impl Scope { - /// Create a new scope - pub fn new(path: &str) -> Scope { - let rdef = ResourceDef::prefix(path); - Scope { - rdef: rdef.clone(), - router: Rc::new(Router::new(rdef)), - filters: Vec::new(), - middlewares: Rc::new(Vec::new()), - } - } - - #[inline] - pub(crate) fn rdef(&self) -> &ResourceDef { - &self.rdef - } - - pub(crate) fn router(&self) -> &Router { - self.router.as_ref() - } - - #[inline] - pub(crate) fn take_filters(&mut self) -> Vec>> { - mem::replace(&mut self.filters, Vec::new()) - } - - /// Add match predicate to scope. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, pred, App, HttpRequest, HttpResponse, Path}; - /// - /// fn index(data: Path<(String, String)>) -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope - /// .filter(pred::Header("content-type", "text/plain")) - /// .route("/test1", http::Method::GET, index) - /// .route("/test2", http::Method::POST, |_: HttpRequest| { - /// HttpResponse::MethodNotAllowed() - /// }) - /// }); - /// } - /// ``` - pub fn filter + 'static>(mut self, p: T) -> Self { - self.filters.push(Box::new(p)); - self - } - - /// Create nested scope with new state. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest}; - /// - /// struct AppState; - /// - /// fn index(req: &HttpRequest) -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope.with_state("/state2", AppState, |scope| { - /// scope.resource("/test1", |r| r.f(index)) - /// }) - /// }); - /// } - /// ``` - pub fn with_state(mut self, path: &str, state: T, f: F) -> Scope - where - F: FnOnce(Scope) -> Scope, - { - let rdef = ResourceDef::prefix(path); - let scope = Scope { - rdef: rdef.clone(), - filters: Vec::new(), - router: Rc::new(Router::new(rdef)), - middlewares: Rc::new(Vec::new()), - }; - let mut scope = f(scope); - - let state = Rc::new(state); - let filters: Vec>> = vec![Box::new(FiltersWrapper { - state: Rc::clone(&state), - filters: scope.take_filters(), - })]; - let handler = Box::new(Wrapper { scope, state }); - - Rc::get_mut(&mut self.router).unwrap().register_handler( - path, - handler, - Some(filters), - ); - - self - } - - /// Create nested scope. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest}; - /// - /// struct AppState; - /// - /// fn index(req: &HttpRequest) -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::with_state(AppState).scope("/app", |scope| { - /// scope.nested("/v1", |scope| scope.resource("/test1", |r| r.f(index))) - /// }); - /// } - /// ``` - pub fn nested(mut self, path: &str, f: F) -> Scope - where - F: FnOnce(Scope) -> Scope, - { - let rdef = ResourceDef::prefix(&insert_slash(path)); - let scope = Scope { - rdef: rdef.clone(), - filters: Vec::new(), - router: Rc::new(Router::new(rdef)), - middlewares: Rc::new(Vec::new()), - }; - Rc::get_mut(&mut self.router) - .unwrap() - .register_scope(f(scope)); - - self - } - - /// Configure route for a specific path. - /// - /// This is a simplified version of the `Scope::resource()` method. - /// Handler functions need to accept one request extractor - /// argument. - /// - /// This method could be called multiple times, in that case - /// multiple routes would be registered for same resource path. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse, Path}; - /// - /// fn index(data: Path<(String, String)>) -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope.route("/test1", http::Method::GET, index).route( - /// "/test2", - /// http::Method::POST, - /// |_: HttpRequest| HttpResponse::MethodNotAllowed(), - /// ) - /// }); - /// } - /// ``` - pub fn route(mut self, path: &str, method: Method, f: F) -> Scope - where - F: WithFactory, - R: Responder + 'static, - T: FromRequest + 'static, - { - Rc::get_mut(&mut self.router).unwrap().register_route( - &insert_slash(path), - method, - f, - ); - self - } - - /// Configure resource for a specific path. - /// - /// This method is similar to an `App::resource()` method. - /// Resources may have variable path segments. Resource path uses scope - /// path as a path prefix. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::*; - /// - /// fn main() { - /// let app = App::new().scope("/api", |scope| { - /// scope.resource("/users/{userid}/{friend}", |r| { - /// r.get().f(|_| HttpResponse::Ok()); - /// r.head().f(|_| HttpResponse::MethodNotAllowed()); - /// r.route() - /// .filter(pred::Any(pred::Get()).or(pred::Put())) - /// .filter(pred::Header("Content-Type", "text/plain")) - /// .f(|_| HttpResponse::Ok()) - /// }) - /// }); - /// } - /// ``` - pub fn resource(mut self, path: &str, f: F) -> Scope - where - F: FnOnce(&mut Resource) -> R + 'static, - { - // add resource - let mut resource = Resource::new(ResourceDef::new(&insert_slash(path))); - f(&mut resource); - - Rc::get_mut(&mut self.router) - .unwrap() - .register_resource(resource); - self - } - - /// Default resource to be used if no matching route could be found. - pub fn default_resource(mut self, f: F) -> Scope - where - F: FnOnce(&mut Resource) -> R + 'static, - { - // create and configure default resource - let mut resource = Resource::new(ResourceDef::new("")); - f(&mut resource); - - Rc::get_mut(&mut self.router) - .expect("Multiple copies of scope router") - .register_default_resource(resource.into()); - - self - } - - /// Configure handler for specific path prefix. - /// - /// A path prefix consists of valid path segments, i.e for the - /// prefix `/app` any request with the paths `/app`, `/app/` or - /// `/app/test` would match, but the path `/application` would - /// not. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().scope("/scope-prefix", |scope| { - /// scope.handler("/app", |req: &HttpRequest| match *req.method() { - /// http::Method::GET => HttpResponse::Ok(), - /// http::Method::POST => HttpResponse::MethodNotAllowed(), - /// _ => HttpResponse::NotFound(), - /// }) - /// }); - /// } - /// ``` - pub fn handler>(mut self, path: &str, handler: H) -> Scope { - let path = insert_slash(path.trim().trim_right_matches('/')); - Rc::get_mut(&mut self.router) - .expect("Multiple copies of scope router") - .register_handler(&path, Box::new(WrapHandler::new(handler)), None); - self - } - - /// Register a scope middleware - /// - /// This is similar to `App's` middlewares, but - /// middlewares get invoked on scope level. - /// - /// *Note* `Middleware::finish()` fires right after response get - /// prepared. It does not wait until body get sent to the peer. - pub fn middleware>(mut self, mw: M) -> Scope { - Rc::get_mut(&mut self.middlewares) - .expect("Can not use after configuration") - .push(Box::new(mw)); - self - } -} - -fn insert_slash(path: &str) -> String { - let mut path = path.to_owned(); - if !path.is_empty() && !path.starts_with('/') { - path.insert(0, '/'); - }; - path -} - -impl RouteHandler for Scope { - fn handle(&self, req: &HttpRequest) -> AsyncResult { - let tail = req.match_info().tail as usize; - - // recognize resources - let info = self.router.recognize(req, req.state(), tail); - let req2 = req.with_route_info(info); - if self.middlewares.is_empty() { - self.router.handle(&req2) - } else { - AsyncResult::future(Box::new(Compose::new( - req2, - Rc::clone(&self.router), - Rc::clone(&self.middlewares), - ))) - } - } - - fn has_default_resource(&self) -> bool { - self.router.has_default_resource() - } - - fn default_resource(&mut self, default: DefaultResource) { - Rc::get_mut(&mut self.router) - .expect("Can not use after configuration") - .register_default_resource(default); - } - - fn finish(&mut self) { - Rc::get_mut(&mut self.router) - .expect("Can not use after configuration") - .finish(); - } -} - -struct Wrapper { - state: Rc, - scope: Scope, -} - -impl RouteHandler for Wrapper { - fn handle(&self, req: &HttpRequest) -> AsyncResult { - let req = req.with_state(Rc::clone(&self.state)); - self.scope.handle(&req) - } -} - -struct FiltersWrapper { - state: Rc, - filters: Vec>>, -} - -impl Predicate for FiltersWrapper { - fn check(&self, req: &Request, _: &S2) -> bool { - for filter in &self.filters { - if !filter.check(&req, &self.state) { - return false; - } - } - true - } -} - -/// Compose resource level middlewares with route handler. -struct Compose { - info: ComposeInfo, - state: ComposeState, -} - -struct ComposeInfo { - count: usize, - req: HttpRequest, - router: Rc>, - mws: Rc>>>, -} - -enum ComposeState { - Starting(StartMiddlewares), - Handler(WaitingResponse), - RunMiddlewares(RunMiddlewares), - Finishing(FinishingMiddlewares), - Completed(Response), -} - -impl ComposeState { - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - match *self { - ComposeState::Starting(ref mut state) => state.poll(info), - ComposeState::Handler(ref mut state) => state.poll(info), - ComposeState::RunMiddlewares(ref mut state) => state.poll(info), - ComposeState::Finishing(ref mut state) => state.poll(info), - ComposeState::Completed(_) => None, - } - } -} - -impl Compose { - fn new( - req: HttpRequest, router: Rc>, mws: Rc>>>, - ) -> Self { - let mut info = ComposeInfo { - mws, - req, - router, - count: 0, - }; - let state = StartMiddlewares::init(&mut info); - - Compose { state, info } - } -} - -impl Future for Compose { - type Item = HttpResponse; - type Error = Error; - - fn poll(&mut self) -> Poll { - loop { - if let ComposeState::Completed(ref mut resp) = self.state { - let resp = resp.resp.take().unwrap(); - return Ok(Async::Ready(resp)); - } - if let Some(state) = self.state.poll(&mut self.info) { - self.state = state; - } else { - return Ok(Async::NotReady); - } - } - } -} - -/// Middlewares start executor -struct StartMiddlewares { - fut: Option, - _s: PhantomData, -} - -type Fut = Box, Error = Error>>; - -impl StartMiddlewares { - fn init(info: &mut ComposeInfo) -> ComposeState { - let len = info.mws.len(); - - loop { - if info.count == len { - let reply = info.router.handle(&info.req); - return WaitingResponse::init(info, reply); - } else { - let result = info.mws[info.count].start(&info.req); - match result { - Ok(MiddlewareStarted::Done) => info.count += 1, - Ok(MiddlewareStarted::Response(resp)) => { - return RunMiddlewares::init(info, resp); - } - Ok(MiddlewareStarted::Future(fut)) => { - return ComposeState::Starting(StartMiddlewares { - fut: Some(fut), - _s: PhantomData, - }); - } - Err(err) => { - return RunMiddlewares::init(info, err.into()); - } - } - } - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - let len = info.mws.len(); - - 'outer: loop { - match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => { - return None; - } - Ok(Async::Ready(resp)) => { - info.count += 1; - - if let Some(resp) = resp { - return Some(RunMiddlewares::init(info, resp)); - } - loop { - if info.count == len { - let reply = info.router.handle(&info.req); - return Some(WaitingResponse::init(info, reply)); - } else { - let result = info.mws[info.count].start(&info.req); - match result { - Ok(MiddlewareStarted::Done) => info.count += 1, - Ok(MiddlewareStarted::Response(resp)) => { - return Some(RunMiddlewares::init(info, resp)); - } - Ok(MiddlewareStarted::Future(fut)) => { - self.fut = Some(fut); - continue 'outer; - } - Err(err) => { - return Some(RunMiddlewares::init(info, err.into())); - } - } - } - } - } - Err(err) => { - return Some(RunMiddlewares::init(info, err.into())); - } - } - } - } -} - -// waiting for response -struct WaitingResponse { - fut: Box>, - _s: PhantomData, -} - -impl WaitingResponse { - #[inline] - fn init( - info: &mut ComposeInfo, reply: AsyncResult, - ) -> ComposeState { - match reply.into() { - AsyncResultItem::Ok(resp) => RunMiddlewares::init(info, resp), - AsyncResultItem::Err(err) => RunMiddlewares::init(info, err.into()), - AsyncResultItem::Future(fut) => ComposeState::Handler(WaitingResponse { - fut, - _s: PhantomData, - }), - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - match self.fut.poll() { - Ok(Async::NotReady) => None, - Ok(Async::Ready(resp)) => Some(RunMiddlewares::init(info, resp)), - Err(err) => Some(RunMiddlewares::init(info, err.into())), - } - } -} - -/// Middlewares response executor -struct RunMiddlewares { - curr: usize, - fut: Option>>, - _s: PhantomData, -} - -impl RunMiddlewares { - fn init(info: &mut ComposeInfo, mut resp: HttpResponse) -> ComposeState { - let mut curr = 0; - let len = info.mws.len(); - - loop { - let state = info.mws[curr].response(&info.req, resp); - resp = match state { - Err(err) => { - info.count = curr + 1; - return FinishingMiddlewares::init(info, err.into()); - } - Ok(MiddlewareResponse::Done(r)) => { - curr += 1; - if curr == len { - return FinishingMiddlewares::init(info, r); - } else { - r - } - } - Ok(MiddlewareResponse::Future(fut)) => { - return ComposeState::RunMiddlewares(RunMiddlewares { - curr, - fut: Some(fut), - _s: PhantomData, - }); - } - }; - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - let len = info.mws.len(); - - loop { - // poll latest fut - let mut resp = match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => return None, - Ok(Async::Ready(resp)) => { - self.curr += 1; - resp - } - Err(err) => return Some(FinishingMiddlewares::init(info, err.into())), - }; - - loop { - if self.curr == len { - return Some(FinishingMiddlewares::init(info, resp)); - } else { - let state = info.mws[self.curr].response(&info.req, resp); - match state { - Err(err) => { - return Some(FinishingMiddlewares::init(info, err.into())) - } - Ok(MiddlewareResponse::Done(r)) => { - self.curr += 1; - resp = r - } - Ok(MiddlewareResponse::Future(fut)) => { - self.fut = Some(fut); - break; - } - } - } - } - } - } -} - -/// Middlewares start executor -struct FinishingMiddlewares { - resp: Option, - fut: Option>>, - _s: PhantomData, -} - -impl FinishingMiddlewares { - fn init(info: &mut ComposeInfo, resp: HttpResponse) -> ComposeState { - if info.count == 0 { - Response::init(resp) - } else { - let mut state = FinishingMiddlewares { - resp: Some(resp), - fut: None, - _s: PhantomData, - }; - if let Some(st) = state.poll(info) { - st - } else { - ComposeState::Finishing(state) - } - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - loop { - // poll latest fut - let not_ready = if let Some(ref mut fut) = self.fut { - match fut.poll() { - Ok(Async::NotReady) => true, - Ok(Async::Ready(())) => false, - Err(err) => { - error!("Middleware finish error: {}", err); - false - } - } - } else { - false - }; - if not_ready { - return None; - } - self.fut = None; - if info.count == 0 { - return Some(Response::init(self.resp.take().unwrap())); - } - - info.count -= 1; - let state = info.mws[info.count as usize] - .finish(&info.req, self.resp.as_ref().unwrap()); - match state { - MiddlewareFinished::Done => { - if info.count == 0 { - return Some(Response::init(self.resp.take().unwrap())); - } - } - MiddlewareFinished::Future(fut) => { - self.fut = Some(fut); - } - } - } - } -} - -struct Response { - resp: Option, - _s: PhantomData, -} - -impl Response { - fn init(resp: HttpResponse) -> ComposeState { - ComposeState::Completed(Response { - resp: Some(resp), - _s: PhantomData, - }) - } -} - -#[cfg(test)] -mod tests { - use bytes::Bytes; - - use application::App; - use body::Body; - use http::{Method, StatusCode}; - use httprequest::HttpRequest; - use httpresponse::HttpResponse; - use pred; - use test::TestRequest; - - #[test] - fn test_scope() { - let app = App::new() - .scope("/app", |scope| { - scope.resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - }).finish(); - - let req = TestRequest::with_uri("/app/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_scope_root() { - let app = App::new() - .scope("/app", |scope| { - scope - .resource("", |r| r.f(|_| HttpResponse::Ok())) - .resource("/", |r| r.f(|_| HttpResponse::Created())) - }).finish(); - - let req = TestRequest::with_uri("/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_scope_root2() { - let app = App::new() - .scope("/app/", |scope| { - scope.resource("", |r| r.f(|_| HttpResponse::Ok())) - }).finish(); - - let req = TestRequest::with_uri("/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_scope_root3() { - let app = App::new() - .scope("/app/", |scope| { - scope.resource("/", |r| r.f(|_| HttpResponse::Ok())) - }).finish(); - - let req = TestRequest::with_uri("/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_route() { - let app = App::new() - .scope("app", |scope| { - scope - .route("/path1", Method::GET, |_: HttpRequest<_>| { - HttpResponse::Ok() - }).route("/path1", Method::DELETE, |_: HttpRequest<_>| { - HttpResponse::Ok() - }) - }).finish(); - - let req = TestRequest::with_uri("/app/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::DELETE) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_route_without_leading_slash() { - let app = App::new() - .scope("app", |scope| { - scope - .route("path1", Method::GET, |_: HttpRequest<_>| HttpResponse::Ok()) - .route("path1", Method::DELETE, |_: HttpRequest<_>| { - HttpResponse::Ok() - }) - }).finish(); - - let req = TestRequest::with_uri("/app/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::DELETE) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_filter() { - let app = App::new() - .scope("/app", |scope| { - scope - .filter(pred::Get()) - .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - }).finish(); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::GET) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_scope_variable_segment() { - let app = App::new() - .scope("/ab-{project}", |scope| { - scope.resource("/path1", |r| { - r.f(|r| { - HttpResponse::Ok() - .body(format!("project: {}", &r.match_info()["project"])) - }) - }) - }).finish(); - - let req = TestRequest::with_uri("/ab-project1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - match resp.as_msg().body() { - &Body::Binary(ref b) => { - let bytes: Bytes = b.clone().into(); - assert_eq!(bytes, Bytes::from_static(b"project: project1")); - } - _ => panic!(), - } - - let req = TestRequest::with_uri("/aa-project1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_with_state() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1", State, |scope| { - scope.resource("/path1", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_scope_with_state_root() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1", State, |scope| { - scope - .resource("", |r| r.f(|_| HttpResponse::Ok())) - .resource("/", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/t1/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_scope_with_state_root2() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1/", State, |scope| { - scope.resource("", |r| r.f(|_| HttpResponse::Ok())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/t1/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_scope_with_state_root3() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1/", State, |scope| { - scope.resource("/", |r| r.f(|_| HttpResponse::Ok())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/t1/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_with_state_filter() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1", State, |scope| { - scope - .filter(pred::Get()) - .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/t1/path1") - .method(Method::GET) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_nested_scope() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope.resource("/path1", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_nested_scope_no_slash() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("t1", |scope| { - scope.resource("/path1", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_nested_scope_root() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope - .resource("", |r| r.f(|_| HttpResponse::Ok())) - .resource("/", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/t1/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_nested_scope_filter() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope - .filter(pred::Get()) - .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/t1/path1") - .method(Method::GET) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_nested_scope_with_variable_segment() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/{project_id}", |scope| { - scope.resource("/path1", |r| { - r.f(|r| { - HttpResponse::Created().body(format!( - "project: {}", - &r.match_info()["project_id"] - )) - }) - }) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/project_1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - - match resp.as_msg().body() { - &Body::Binary(ref b) => { - let bytes: Bytes = b.clone().into(); - assert_eq!(bytes, Bytes::from_static(b"project: project_1")); - } - _ => panic!(), - } - } - - #[test] - fn test_nested2_scope_with_variable_segment() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/{project}", |scope| { - scope.nested("/{id}", |scope| { - scope.resource("/path1", |r| { - r.f(|r| { - HttpResponse::Created().body(format!( - "project: {} - {}", - &r.match_info()["project"], - &r.match_info()["id"], - )) - }) - }) - }) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/test/1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - - match resp.as_msg().body() { - &Body::Binary(ref b) => { - let bytes: Bytes = b.clone().into(); - assert_eq!(bytes, Bytes::from_static(b"project: test - 1")); - } - _ => panic!(), - } - - let req = TestRequest::with_uri("/app/test/1/path2").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_default_resource() { - let app = App::new() - .scope("/app", |scope| { - scope - .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - .default_resource(|r| r.f(|_| HttpResponse::BadRequest())) - }).finish(); - - let req = TestRequest::with_uri("/app/path2").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::BAD_REQUEST); - - let req = TestRequest::with_uri("/path2").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_default_resource_propagation() { - let app = App::new() - .scope("/app1", |scope| { - scope.default_resource(|r| r.f(|_| HttpResponse::BadRequest())) - }).scope("/app2", |scope| scope) - .default_resource(|r| r.f(|_| HttpResponse::MethodNotAllowed())) - .finish(); - - let req = TestRequest::with_uri("/non-exist").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::METHOD_NOT_ALLOWED); - - let req = TestRequest::with_uri("/app1/non-exist").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::BAD_REQUEST); - - let req = TestRequest::with_uri("/app2/non-exist").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::METHOD_NOT_ALLOWED); - } - - #[test] - fn test_handler() { - let app = App::new() - .scope("/scope", |scope| { - scope.handler("/test", |_: &_| HttpResponse::Ok()) - }).finish(); - - let req = TestRequest::with_uri("/scope/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/scope/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/scope/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/scope/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/scope/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } -} diff --git a/src/server/acceptor.rs b/src/server/acceptor.rs deleted file mode 100644 index 994b4b7bd..000000000 --- a/src/server/acceptor.rs +++ /dev/null @@ -1,383 +0,0 @@ -use std::time::Duration; -use std::{fmt, net}; - -use actix_net::server::ServerMessage; -use actix_net::service::{NewService, Service}; -use futures::future::{err, ok, Either, FutureResult}; -use futures::{Async, Future, Poll}; -use tokio_reactor::Handle; -use tokio_tcp::TcpStream; -use tokio_timer::{sleep, Delay}; - -use super::error::AcceptorError; -use super::IoStream; - -/// This trait indicates types that can create acceptor service for http server. -pub trait AcceptorServiceFactory: Send + Clone + 'static { - type Io: IoStream + Send; - type NewService: NewService; - - fn create(&self) -> Self::NewService; -} - -impl AcceptorServiceFactory for F -where - F: Fn() -> T + Send + Clone + 'static, - T::Response: IoStream + Send, - T: NewService, - T::InitError: fmt::Debug, -{ - type Io = T::Response; - type NewService = T; - - fn create(&self) -> T { - (self)() - } -} - -#[derive(Clone)] -/// Default acceptor service convert `TcpStream` to a `tokio_tcp::TcpStream` -pub(crate) struct DefaultAcceptor; - -impl AcceptorServiceFactory for DefaultAcceptor { - type Io = TcpStream; - type NewService = DefaultAcceptor; - - fn create(&self) -> Self::NewService { - DefaultAcceptor - } -} - -impl NewService for DefaultAcceptor { - type Request = TcpStream; - type Response = TcpStream; - type Error = (); - type InitError = (); - type Service = DefaultAcceptor; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(DefaultAcceptor) - } -} - -impl Service for DefaultAcceptor { - type Request = TcpStream; - type Response = TcpStream; - type Error = (); - type Future = FutureResult; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - ok(req) - } -} - -pub(crate) struct TcpAcceptor { - inner: T, -} - -impl TcpAcceptor -where - T: NewService>, - T::InitError: fmt::Debug, -{ - pub(crate) fn new(inner: T) -> Self { - TcpAcceptor { inner } - } -} - -impl NewService for TcpAcceptor -where - T: NewService>, - T::InitError: fmt::Debug, -{ - type Request = net::TcpStream; - type Response = T::Response; - type Error = AcceptorError; - type InitError = T::InitError; - type Service = TcpAcceptorService; - type Future = TcpAcceptorResponse; - - fn new_service(&self) -> Self::Future { - TcpAcceptorResponse { - fut: self.inner.new_service(), - } - } -} - -pub(crate) struct TcpAcceptorResponse -where - T: NewService, - T::InitError: fmt::Debug, -{ - fut: T::Future, -} - -impl Future for TcpAcceptorResponse -where - T: NewService, - T::InitError: fmt::Debug, -{ - type Item = TcpAcceptorService; - type Error = T::InitError; - - fn poll(&mut self) -> Poll { - match self.fut.poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(service)) => { - Ok(Async::Ready(TcpAcceptorService { inner: service })) - } - Err(e) => { - error!("Can not create accetor service: {:?}", e); - Err(e) - } - } - } -} - -pub(crate) struct TcpAcceptorService { - inner: T, -} - -impl Service for TcpAcceptorService -where - T: Service>, -{ - type Request = net::TcpStream; - type Response = T::Response; - type Error = AcceptorError; - type Future = Either>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.inner.poll_ready() - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - let stream = TcpStream::from_std(req, &Handle::default()).map_err(|e| { - error!("Can not convert to an async tcp stream: {}", e); - AcceptorError::Io(e) - }); - - match stream { - Ok(stream) => Either::A(self.inner.call(stream)), - Err(e) => Either::B(err(e)), - } - } -} - -#[doc(hidden)] -/// Acceptor timeout middleware -/// -/// Applies timeout to request prcoessing. -pub struct AcceptorTimeout { - inner: T, - timeout: Duration, -} - -impl AcceptorTimeout { - /// Create new `AcceptorTimeout` instance. timeout is in milliseconds. - pub fn new(timeout: u64, inner: T) -> Self { - Self { - inner, - timeout: Duration::from_millis(timeout), - } - } -} - -impl NewService for AcceptorTimeout { - type Request = T::Request; - type Response = T::Response; - type Error = AcceptorError; - type InitError = T::InitError; - type Service = AcceptorTimeoutService; - type Future = AcceptorTimeoutFut; - - fn new_service(&self) -> Self::Future { - AcceptorTimeoutFut { - fut: self.inner.new_service(), - timeout: self.timeout, - } - } -} - -#[doc(hidden)] -pub struct AcceptorTimeoutFut { - fut: T::Future, - timeout: Duration, -} - -impl Future for AcceptorTimeoutFut { - type Item = AcceptorTimeoutService; - type Error = T::InitError; - - fn poll(&mut self) -> Poll { - let inner = try_ready!(self.fut.poll()); - Ok(Async::Ready(AcceptorTimeoutService { - inner, - timeout: self.timeout, - })) - } -} - -#[doc(hidden)] -/// Acceptor timeout service -/// -/// Applies timeout to request prcoessing. -pub struct AcceptorTimeoutService { - inner: T, - timeout: Duration, -} - -impl Service for AcceptorTimeoutService { - type Request = T::Request; - type Response = T::Response; - type Error = AcceptorError; - type Future = AcceptorTimeoutResponse; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.inner.poll_ready().map_err(AcceptorError::Service) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - AcceptorTimeoutResponse { - fut: self.inner.call(req), - sleep: sleep(self.timeout), - } - } -} - -#[doc(hidden)] -pub struct AcceptorTimeoutResponse { - fut: T::Future, - sleep: Delay, -} - -impl Future for AcceptorTimeoutResponse { - type Item = T::Response; - type Error = AcceptorError; - - fn poll(&mut self) -> Poll { - match self.fut.poll().map_err(AcceptorError::Service)? { - Async::NotReady => match self.sleep.poll() { - Err(_) => Err(AcceptorError::Timeout), - Ok(Async::Ready(_)) => Err(AcceptorError::Timeout), - Ok(Async::NotReady) => Ok(Async::NotReady), - }, - Async::Ready(resp) => Ok(Async::Ready(resp)), - } - } -} - -pub(crate) struct ServerMessageAcceptor { - inner: T, -} - -impl ServerMessageAcceptor -where - T: NewService, -{ - pub(crate) fn new(inner: T) -> Self { - ServerMessageAcceptor { inner } - } -} - -impl NewService for ServerMessageAcceptor -where - T: NewService, -{ - type Request = ServerMessage; - type Response = (); - type Error = T::Error; - type InitError = T::InitError; - type Service = ServerMessageAcceptorService; - type Future = ServerMessageAcceptorResponse; - - fn new_service(&self) -> Self::Future { - ServerMessageAcceptorResponse { - fut: self.inner.new_service(), - } - } -} - -pub(crate) struct ServerMessageAcceptorResponse -where - T: NewService, -{ - fut: T::Future, -} - -impl Future for ServerMessageAcceptorResponse -where - T: NewService, -{ - type Item = ServerMessageAcceptorService; - type Error = T::InitError; - - fn poll(&mut self) -> Poll { - match self.fut.poll()? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(service) => Ok(Async::Ready(ServerMessageAcceptorService { - inner: service, - })), - } - } -} - -pub(crate) struct ServerMessageAcceptorService { - inner: T, -} - -impl Service for ServerMessageAcceptorService -where - T: Service, -{ - type Request = ServerMessage; - type Response = (); - type Error = T::Error; - type Future = - Either, FutureResult<(), Self::Error>>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.inner.poll_ready() - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - match req { - ServerMessage::Connect(stream) => { - Either::A(ServerMessageAcceptorServiceFut { - fut: self.inner.call(stream), - }) - } - ServerMessage::Shutdown(_) => Either::B(ok(())), - ServerMessage::ForceShutdown => { - // self.settings - // .head() - // .traverse(|proto: &mut HttpProtocol| proto.shutdown()); - Either::B(ok(())) - } - } - } -} - -pub(crate) struct ServerMessageAcceptorServiceFut { - fut: T::Future, -} - -impl Future for ServerMessageAcceptorServiceFut -where - T: Service, -{ - type Item = (); - type Error = T::Error; - - fn poll(&mut self) -> Poll { - match self.fut.poll()? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(_) => Ok(Async::Ready(())), - } - } -} diff --git a/src/server/builder.rs b/src/server/builder.rs deleted file mode 100644 index ea3638f10..000000000 --- a/src/server/builder.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::{fmt, net}; - -use actix_net::either::Either; -use actix_net::server::{Server, ServiceFactory}; -use actix_net::service::{NewService, NewServiceExt}; - -use super::acceptor::{ - AcceptorServiceFactory, AcceptorTimeout, ServerMessageAcceptor, TcpAcceptor, -}; -use super::error::AcceptorError; -use super::handler::IntoHttpHandler; -use super::service::{HttpService, StreamConfiguration}; -use super::settings::{ServerSettings, ServiceConfig}; -use super::KeepAlive; - -pub(crate) trait ServiceProvider { - fn register( - &self, - server: Server, - lst: net::TcpListener, - host: String, - addr: net::SocketAddr, - keep_alive: KeepAlive, - secure: bool, - client_timeout: u64, - client_shutdown: u64, - ) -> Server; -} - -/// Utility type that builds complete http pipeline -pub(crate) struct HttpServiceBuilder -where - F: Fn() -> H + Send + Clone, -{ - factory: F, - acceptor: A, -} - -impl HttpServiceBuilder -where - F: Fn() -> H + Send + Clone + 'static, - H: IntoHttpHandler, - A: AcceptorServiceFactory, - ::InitError: fmt::Debug, -{ - /// Create http service builder - pub fn new(factory: F, acceptor: A) -> Self { - Self { factory, acceptor } - } - - fn finish( - &self, - host: String, - addr: net::SocketAddr, - keep_alive: KeepAlive, - secure: bool, - client_timeout: u64, - client_shutdown: u64, - ) -> impl ServiceFactory { - let factory = self.factory.clone(); - let acceptor = self.acceptor.clone(); - move || { - let app = (factory)().into_handler(); - let settings = ServiceConfig::new( - app, - keep_alive, - client_timeout, - client_shutdown, - ServerSettings::new(addr, &host, false), - ); - - if secure { - Either::B(ServerMessageAcceptor::new( - TcpAcceptor::new(AcceptorTimeout::new( - client_timeout, - acceptor.create(), - )).map_err(|_| ()) - .map_init_err(|_| ()) - .and_then(StreamConfiguration::new().nodelay(true)) - .and_then( - HttpService::new(settings) - .map_init_err(|_| ()) - .map_err(|_| ()), - ), - )) - } else { - Either::A(ServerMessageAcceptor::new( - TcpAcceptor::new(acceptor.create().map_err(AcceptorError::Service)) - .map_err(|_| ()) - .map_init_err(|_| ()) - .and_then(StreamConfiguration::new().nodelay(true)) - .and_then( - HttpService::new(settings) - .map_init_err(|_| ()) - .map_err(|_| ()), - ), - )) - } - } - } -} - -impl ServiceProvider for HttpServiceBuilder -where - F: Fn() -> H + Send + Clone + 'static, - A: AcceptorServiceFactory, - ::InitError: fmt::Debug, - H: IntoHttpHandler, -{ - fn register( - &self, - server: Server, - lst: net::TcpListener, - host: String, - addr: net::SocketAddr, - keep_alive: KeepAlive, - secure: bool, - client_timeout: u64, - client_shutdown: u64, - ) -> Server { - server.listen2( - "actix-web", - lst, - self.finish( - host, - addr, - keep_alive, - secure, - client_timeout, - client_shutdown, - ), - ) - } -} diff --git a/src/server/channel.rs b/src/server/channel.rs deleted file mode 100644 index d65b05e85..000000000 --- a/src/server/channel.rs +++ /dev/null @@ -1,300 +0,0 @@ -use std::net::Shutdown; -use std::{io, mem, time}; - -use bytes::{Buf, BufMut, BytesMut}; -use futures::{Async, Future, Poll}; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_timer::Delay; - -use super::error::HttpDispatchError; -use super::settings::ServiceConfig; -use super::{h1, h2, HttpHandler, IoStream}; -use http::StatusCode; - -const HTTP2_PREFACE: [u8; 14] = *b"PRI * HTTP/2.0"; - -pub(crate) enum HttpProtocol { - H1(h1::Http1Dispatcher), - H2(h2::Http2), - Unknown(ServiceConfig, T, BytesMut), - None, -} - -// impl HttpProtocol { -// fn shutdown_(&mut self) { -// match self { -// HttpProtocol::H1(ref mut h1) => { -// let io = h1.io(); -// let _ = IoStream::set_linger(io, Some(time::Duration::new(0, 0))); -// let _ = IoStream::shutdown(io, Shutdown::Both); -// } -// HttpProtocol::H2(ref mut h2) => h2.shutdown(), -// HttpProtocol::Unknown(_, io, _) => { -// let _ = IoStream::set_linger(io, Some(time::Duration::new(0, 0))); -// let _ = IoStream::shutdown(io, Shutdown::Both); -// } -// HttpProtocol::None => (), -// } -// } -// } - -enum ProtocolKind { - Http1, - Http2, -} - -#[doc(hidden)] -pub struct HttpChannel -where - T: IoStream, - H: HttpHandler + 'static, -{ - proto: HttpProtocol, - ka_timeout: Option, -} - -impl HttpChannel -where - T: IoStream, - H: HttpHandler + 'static, -{ - pub(crate) fn new(settings: ServiceConfig, io: T) -> HttpChannel { - let ka_timeout = settings.client_timer(); - - HttpChannel { - ka_timeout, - proto: HttpProtocol::Unknown(settings, io, BytesMut::with_capacity(8192)), - } - } -} - -impl Future for HttpChannel -where - T: IoStream, - H: HttpHandler + 'static, -{ - type Item = (); - type Error = HttpDispatchError; - - fn poll(&mut self) -> Poll { - // keep-alive timer - if self.ka_timeout.is_some() { - match self.ka_timeout.as_mut().unwrap().poll() { - Ok(Async::Ready(_)) => { - trace!("Slow request timed out, close connection"); - let proto = mem::replace(&mut self.proto, HttpProtocol::None); - if let HttpProtocol::Unknown(settings, io, buf) = proto { - self.proto = HttpProtocol::H1(h1::Http1Dispatcher::for_error( - settings, - io, - StatusCode::REQUEST_TIMEOUT, - self.ka_timeout.take(), - buf, - )); - return self.poll(); - } - return Ok(Async::Ready(())); - } - Ok(Async::NotReady) => (), - Err(_) => panic!("Something is really wrong"), - } - } - - let mut is_eof = false; - let kind = match self.proto { - HttpProtocol::H1(ref mut h1) => return h1.poll(), - HttpProtocol::H2(ref mut h2) => return h2.poll(), - HttpProtocol::Unknown(_, ref mut io, ref mut buf) => { - let mut err = None; - let mut disconnect = false; - match io.read_available(buf) { - Ok(Async::Ready((read_some, stream_closed))) => { - is_eof = stream_closed; - // Only disconnect if no data was read. - if is_eof && !read_some { - disconnect = true; - } - } - Err(e) => { - err = Some(e.into()); - } - _ => (), - } - if disconnect { - debug!("Ignored premature client disconnection"); - return Ok(Async::Ready(())); - } else if let Some(e) = err { - return Err(e); - } - - if buf.len() >= 14 { - if buf[..14] == HTTP2_PREFACE[..] { - ProtocolKind::Http2 - } else { - ProtocolKind::Http1 - } - } else { - return Ok(Async::NotReady); - } - } - HttpProtocol::None => unreachable!(), - }; - - // upgrade to specific http protocol - let proto = mem::replace(&mut self.proto, HttpProtocol::None); - if let HttpProtocol::Unknown(settings, io, buf) = proto { - match kind { - ProtocolKind::Http1 => { - self.proto = HttpProtocol::H1(h1::Http1Dispatcher::new( - settings, - io, - buf, - is_eof, - self.ka_timeout.take(), - )); - return self.poll(); - } - ProtocolKind::Http2 => { - self.proto = HttpProtocol::H2(h2::Http2::new( - settings, - io, - buf.freeze(), - self.ka_timeout.take(), - )); - return self.poll(); - } - } - } - unreachable!() - } -} - -#[doc(hidden)] -pub struct H1Channel -where - T: IoStream, - H: HttpHandler + 'static, -{ - proto: HttpProtocol, -} - -impl H1Channel -where - T: IoStream, - H: HttpHandler + 'static, -{ - pub(crate) fn new(settings: ServiceConfig, io: T) -> H1Channel { - H1Channel { - proto: HttpProtocol::H1(h1::Http1Dispatcher::new( - settings, - io, - BytesMut::with_capacity(8192), - false, - None, - )), - } - } -} - -impl Future for H1Channel -where - T: IoStream, - H: HttpHandler + 'static, -{ - type Item = (); - type Error = HttpDispatchError; - - fn poll(&mut self) -> Poll { - match self.proto { - HttpProtocol::H1(ref mut h1) => h1.poll(), - _ => unreachable!(), - } - } -} - -/// Wrapper for `AsyncRead + AsyncWrite` types -pub(crate) struct WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - io: T, -} - -impl WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - pub fn new(io: T) -> Self { - WrapperStream { io } - } -} - -impl IoStream for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn shutdown(&mut self, _: Shutdown) -> io::Result<()> { - Ok(()) - } - #[inline] - fn set_nodelay(&mut self, _: bool) -> io::Result<()> { - Ok(()) - } - #[inline] - fn set_linger(&mut self, _: Option) -> io::Result<()> { - Ok(()) - } - #[inline] - fn set_keepalive(&mut self, _: Option) -> io::Result<()> { - Ok(()) - } -} - -impl io::Read for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.io.read(buf) - } -} - -impl io::Write for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn write(&mut self, buf: &[u8]) -> io::Result { - self.io.write(buf) - } - #[inline] - fn flush(&mut self) -> io::Result<()> { - self.io.flush() - } -} - -impl AsyncRead for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn read_buf(&mut self, buf: &mut B) -> Poll { - self.io.read_buf(buf) - } -} - -impl AsyncWrite for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn shutdown(&mut self) -> Poll<(), io::Error> { - self.io.shutdown() - } - #[inline] - fn write_buf(&mut self, buf: &mut B) -> Poll { - self.io.write_buf(buf) - } -} diff --git a/src/server/error.rs b/src/server/error.rs deleted file mode 100644 index 70f100998..000000000 --- a/src/server/error.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::io; - -use futures::{Async, Poll}; -use http2; - -use super::{helpers, HttpHandlerTask, Writer}; -use http::{StatusCode, Version}; -use Error; - -/// Errors produced by `AcceptorError` service. -#[derive(Debug)] -pub enum AcceptorError { - /// The inner service error - Service(T), - - /// Io specific error - Io(io::Error), - - /// The request did not complete within the specified timeout. - Timeout, -} - -#[derive(Fail, Debug)] -/// A set of errors that can occur during dispatching http requests -pub enum HttpDispatchError { - /// Application error - #[fail(display = "Application specific error: {}", _0)] - App(Error), - - /// An `io::Error` that occurred while trying to read or write to a network - /// stream. - #[fail(display = "IO error: {}", _0)] - Io(io::Error), - - /// The first request did not complete within the specified timeout. - #[fail(display = "The first request did not complete within the specified timeout")] - SlowRequestTimeout, - - /// Shutdown timeout - #[fail(display = "Connection shutdown timeout")] - ShutdownTimeout, - - /// HTTP2 error - #[fail(display = "HTTP2 error: {}", _0)] - Http2(http2::Error), - - /// Payload is not consumed - #[fail(display = "Task is completed but request's payload is not consumed")] - PayloadIsNotConsumed, - - /// Malformed request - #[fail(display = "Malformed request")] - MalformedRequest, - - /// Internal error - #[fail(display = "Internal error")] - InternalError, - - /// Unknown error - #[fail(display = "Unknown error")] - Unknown, -} - -impl From for HttpDispatchError { - fn from(err: Error) -> Self { - HttpDispatchError::App(err) - } -} - -impl From for HttpDispatchError { - fn from(err: io::Error) -> Self { - HttpDispatchError::Io(err) - } -} - -impl From for HttpDispatchError { - fn from(err: http2::Error) -> Self { - HttpDispatchError::Http2(err) - } -} - -pub(crate) struct ServerError(Version, StatusCode); - -impl ServerError { - pub fn err(ver: Version, status: StatusCode) -> Box { - Box::new(ServerError(ver, status)) - } -} - -impl HttpHandlerTask for ServerError { - fn poll_io(&mut self, io: &mut Writer) -> Poll { - { - let bytes = io.buffer(); - // Buffer should have sufficient capacity for status line - // and extra space - bytes.reserve(helpers::STATUS_LINE_BUF_SIZE + 1); - helpers::write_status_line(self.0, self.1.as_u16(), bytes); - } - // Convert Status Code to Reason. - let reason = self.1.canonical_reason().unwrap_or(""); - io.buffer().extend_from_slice(reason.as_bytes()); - // No response body. - io.buffer().extend_from_slice(b"\r\ncontent-length: 0\r\n"); - // date header - io.set_date(); - Ok(Async::Ready(true)) - } -} diff --git a/src/server/h1.rs b/src/server/h1.rs deleted file mode 100644 index fa7d2fda5..000000000 --- a/src/server/h1.rs +++ /dev/null @@ -1,1353 +0,0 @@ -use std::collections::VecDeque; -use std::net::{Shutdown, SocketAddr}; -use std::time::{Duration, Instant}; - -use bytes::BytesMut; -use futures::{Async, Future, Poll}; -use tokio_current_thread::spawn; -use tokio_timer::Delay; - -use body::Binary; -use error::{Error, PayloadError}; -use http::{StatusCode, Version}; -use payload::{Payload, PayloadStatus, PayloadWriter}; - -use super::error::{HttpDispatchError, ServerError}; -use super::h1decoder::{DecoderError, H1Decoder, Message}; -use super::h1writer::H1Writer; -use super::handler::{HttpHandler, HttpHandlerTask, HttpHandlerTaskFut}; -use super::input::PayloadType; -use super::settings::ServiceConfig; -use super::{IoStream, Writer}; - -const MAX_PIPELINED_MESSAGES: usize = 16; - -bitflags! { - pub struct Flags: u8 { - const STARTED = 0b0000_0001; - const KEEPALIVE_ENABLED = 0b0000_0010; - const KEEPALIVE = 0b0000_0100; - const SHUTDOWN = 0b0000_1000; - const READ_DISCONNECTED = 0b0001_0000; - const WRITE_DISCONNECTED = 0b0010_0000; - const POLLED = 0b0100_0000; - const FLUSHED = 0b1000_0000; - } -} - -/// Dispatcher for HTTP/1.1 protocol -pub struct Http1Dispatcher { - flags: Flags, - settings: ServiceConfig, - addr: Option, - stream: H1Writer, - decoder: H1Decoder, - payload: Option, - buf: BytesMut, - tasks: VecDeque>, - error: Option, - ka_expire: Instant, - ka_timer: Option, -} - -enum Entry { - Task(H::Task, Option<()>), - Error(Box), -} - -impl Entry { - fn into_task(self) -> H::Task { - match self { - Entry::Task(task, _) => task, - Entry::Error(_) => panic!(), - } - } - fn disconnected(&mut self) { - match *self { - Entry::Task(ref mut task, _) => task.disconnected(), - Entry::Error(ref mut task) => task.disconnected(), - } - } - fn poll_io(&mut self, io: &mut Writer) -> Poll { - match *self { - Entry::Task(ref mut task, ref mut except) => { - match except { - Some(_) => { - let _ = io.write(&Binary::from("HTTP/1.1 100 Continue\r\n\r\n")); - } - _ => (), - }; - task.poll_io(io) - } - Entry::Error(ref mut task) => task.poll_io(io), - } - } - fn poll_completed(&mut self) -> Poll<(), Error> { - match *self { - Entry::Task(ref mut task, _) => task.poll_completed(), - Entry::Error(ref mut task) => task.poll_completed(), - } - } -} - -impl Http1Dispatcher -where - T: IoStream, - H: HttpHandler + 'static, -{ - pub fn new( - settings: ServiceConfig, - stream: T, - buf: BytesMut, - is_eof: bool, - keepalive_timer: Option, - ) -> Self { - let addr = stream.peer_addr(); - let (ka_expire, ka_timer) = if let Some(delay) = keepalive_timer { - (delay.deadline(), Some(delay)) - } else if let Some(delay) = settings.keep_alive_timer() { - (delay.deadline(), Some(delay)) - } else { - (settings.now(), None) - }; - - let flags = if is_eof { - Flags::READ_DISCONNECTED | Flags::FLUSHED - } else if settings.keep_alive_enabled() { - Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED | Flags::FLUSHED - } else { - Flags::empty() - }; - - Http1Dispatcher { - stream: H1Writer::new(stream, settings.clone()), - decoder: H1Decoder::new(), - payload: None, - tasks: VecDeque::new(), - error: None, - flags, - addr, - buf, - settings, - ka_timer, - ka_expire, - } - } - - pub(crate) fn for_error( - settings: ServiceConfig, - stream: T, - status: StatusCode, - mut keepalive_timer: Option, - buf: BytesMut, - ) -> Self { - if let Some(deadline) = settings.client_timer_expire() { - let _ = keepalive_timer.as_mut().map(|delay| delay.reset(deadline)); - } - - let mut disp = Http1Dispatcher { - flags: Flags::STARTED | Flags::READ_DISCONNECTED | Flags::FLUSHED, - stream: H1Writer::new(stream, settings.clone()), - decoder: H1Decoder::new(), - payload: None, - tasks: VecDeque::new(), - error: None, - addr: None, - ka_timer: keepalive_timer, - ka_expire: settings.now(), - buf, - settings, - }; - disp.push_response_entry(status); - disp - } - - #[inline] - fn can_read(&self) -> bool { - if self.flags.contains(Flags::READ_DISCONNECTED) { - return false; - } - - if let Some(ref info) = self.payload { - info.need_read() == PayloadStatus::Read - } else { - true - } - } - - // if checked is set to true, delay disconnect until all tasks have finished. - fn client_disconnected(&mut self, checked: bool) { - self.flags.insert(Flags::READ_DISCONNECTED); - if let Some(mut payload) = self.payload.take() { - payload.set_error(PayloadError::Incomplete); - } - - if !checked || self.tasks.is_empty() { - self.flags - .insert(Flags::WRITE_DISCONNECTED | Flags::FLUSHED); - self.stream.disconnected(); - - // notify all tasks - for mut task in self.tasks.drain(..) { - task.disconnected(); - match task.poll_completed() { - Ok(Async::NotReady) => { - // spawn not completed task, it does not require access to io - // at this point - spawn(HttpHandlerTaskFut::new(task.into_task())); - } - Ok(Async::Ready(_)) => (), - Err(err) => { - error!("Unhandled application error: {}", err); - } - } - } - } - } - - #[inline] - pub fn poll(&mut self) -> Poll<(), HttpDispatchError> { - // check connection keep-alive - self.poll_keepalive()?; - - // shutdown - if self.flags.contains(Flags::SHUTDOWN) { - if self.flags.contains(Flags::WRITE_DISCONNECTED) { - return Ok(Async::Ready(())); - } - return self.poll_flush(true); - } - - // process incoming requests - if !self.flags.contains(Flags::WRITE_DISCONNECTED) { - self.poll_handler()?; - - // flush stream - self.poll_flush(false)?; - - // deal with keep-alive and stream eof (client-side write shutdown) - if self.tasks.is_empty() && self.flags.contains(Flags::FLUSHED) { - // handle stream eof - if self - .flags - .intersects(Flags::READ_DISCONNECTED | Flags::WRITE_DISCONNECTED) - { - return Ok(Async::Ready(())); - } - // no keep-alive - if self.flags.contains(Flags::STARTED) - && (!self.flags.contains(Flags::KEEPALIVE_ENABLED) - || !self.flags.contains(Flags::KEEPALIVE)) - { - self.flags.insert(Flags::SHUTDOWN); - return self.poll(); - } - } - Ok(Async::NotReady) - } else if let Some(err) = self.error.take() { - Err(err) - } else { - Ok(Async::Ready(())) - } - } - - /// Flush stream - fn poll_flush(&mut self, shutdown: bool) -> Poll<(), HttpDispatchError> { - if shutdown || self.flags.contains(Flags::STARTED) { - match self.stream.poll_completed(shutdown) { - Ok(Async::NotReady) => { - // mark stream - if !self.stream.flushed() { - self.flags.remove(Flags::FLUSHED); - } - Ok(Async::NotReady) - } - Err(err) => { - debug!("Error sending data: {}", err); - self.client_disconnected(false); - Err(err.into()) - } - Ok(Async::Ready(_)) => { - // if payload is not consumed we can not use connection - if self.payload.is_some() && self.tasks.is_empty() { - return Err(HttpDispatchError::PayloadIsNotConsumed); - } - self.flags.insert(Flags::FLUSHED); - Ok(Async::Ready(())) - } - } - } else { - Ok(Async::Ready(())) - } - } - - /// keep-alive timer. returns `true` is keep-alive, otherwise drop - fn poll_keepalive(&mut self) -> Result<(), HttpDispatchError> { - if let Some(ref mut timer) = self.ka_timer { - match timer.poll() { - Ok(Async::Ready(_)) => { - // if we get timer during shutdown, just drop connection - if self.flags.contains(Flags::SHUTDOWN) { - let io = self.stream.get_mut(); - let _ = IoStream::set_linger(io, Some(Duration::from_secs(0))); - let _ = IoStream::shutdown(io, Shutdown::Both); - return Err(HttpDispatchError::ShutdownTimeout); - } - if timer.deadline() >= self.ka_expire { - // check for any outstanding request handling - if self.tasks.is_empty() && self.flags.contains(Flags::FLUSHED) { - if !self.flags.contains(Flags::STARTED) { - // timeout on first request (slow request) return 408 - trace!("Slow request timeout"); - self.flags - .insert(Flags::STARTED | Flags::READ_DISCONNECTED); - self.tasks.push_back(Entry::Error(ServerError::err( - Version::HTTP_11, - StatusCode::REQUEST_TIMEOUT, - ))); - } else { - trace!("Keep-alive timeout, close connection"); - self.flags.insert(Flags::SHUTDOWN); - - // start shutdown timer - if let Some(deadline) = - self.settings.client_shutdown_timer() - { - timer.reset(deadline); - let _ = timer.poll(); - } else { - return Ok(()); - } - } - } else if let Some(dl) = self.settings.keep_alive_expire() { - timer.reset(dl); - let _ = timer.poll(); - } - } else { - timer.reset(self.ka_expire); - let _ = timer.poll(); - } - } - Ok(Async::NotReady) => (), - Err(e) => { - error!("Timer error {:?}", e); - return Err(HttpDispatchError::Unknown); - } - } - } - - Ok(()) - } - - #[inline] - /// read data from the stream - pub(self) fn poll_io(&mut self) -> Result { - if !self.flags.contains(Flags::POLLED) { - self.flags.insert(Flags::POLLED); - if !self.buf.is_empty() { - let updated = self.parse()?; - return Ok(updated); - } - } - - // read io from socket - let mut updated = false; - if self.can_read() && self.tasks.len() < MAX_PIPELINED_MESSAGES { - match self.stream.get_mut().read_available(&mut self.buf) { - Ok(Async::Ready((read_some, disconnected))) => { - if read_some && self.parse()? { - updated = true; - } - if disconnected { - self.client_disconnected(true); - } - } - Ok(Async::NotReady) => (), - Err(err) => { - self.client_disconnected(false); - return Err(err.into()); - } - } - } - Ok(updated) - } - - pub(self) fn poll_handler(&mut self) -> Result<(), HttpDispatchError> { - self.poll_io()?; - let mut retry = self.can_read(); - - // process first pipelined response, only first task can do io operation in http/1 - while !self.tasks.is_empty() { - match self.tasks[0].poll_io(&mut self.stream) { - Ok(Async::Ready(ready)) => { - // override keep-alive state - if self.stream.keepalive() { - self.flags.insert(Flags::KEEPALIVE); - } else { - self.flags.remove(Flags::KEEPALIVE); - } - // prepare stream for next response - self.stream.reset(); - - let task = self.tasks.pop_front().unwrap(); - if !ready { - // task is done with io operations but still needs to do more work - spawn(HttpHandlerTaskFut::new(task.into_task())); - } - } - Ok(Async::NotReady) => { - // check if we need timer - if self.ka_timer.is_some() && self.stream.upgrade() { - self.ka_timer.take(); - } - - // if read-backpressure is enabled and we consumed some data. - // we may read more dataand retry - if !retry && self.can_read() && self.poll_io()? { - retry = self.can_read(); - continue; - } - break; - } - Err(err) => { - error!("Unhandled error1: {}", err); - // it is not possible to recover from error - // during pipe handling, so just drop connection - self.client_disconnected(false); - return Err(err.into()); - } - } - } - - // check in-flight messages. all tasks must be alive, - // they need to produce response. if app returned error - // and we can not continue processing incoming requests. - let mut idx = 1; - while idx < self.tasks.len() { - let stop = match self.tasks[idx].poll_completed() { - Ok(Async::NotReady) => false, - Ok(Async::Ready(_)) => true, - Err(err) => { - self.error = Some(err.into()); - true - } - }; - if stop { - // error in task handling or task is completed, - // so no response for this task which means we can not read more requests - // because pipeline sequence is broken. - // but we can safely complete existing tasks - self.flags.insert(Flags::READ_DISCONNECTED); - - for mut task in self.tasks.drain(idx..) { - task.disconnected(); - match task.poll_completed() { - Ok(Async::NotReady) => { - // spawn not completed task, it does not require access to io - // at this point - spawn(HttpHandlerTaskFut::new(task.into_task())); - } - Ok(Async::Ready(_)) => (), - Err(err) => { - error!("Unhandled application error: {}", err); - } - } - } - break; - } else { - idx += 1; - } - } - - Ok(()) - } - - fn push_response_entry(&mut self, status: StatusCode) { - self.tasks - .push_back(Entry::Error(ServerError::err(Version::HTTP_11, status))); - } - - pub(self) fn parse(&mut self) -> Result { - let mut updated = false; - - 'outer: loop { - match self.decoder.decode(&mut self.buf, &self.settings) { - Ok(Some(Message::Message { - mut msg, - mut expect, - payload, - })) => { - updated = true; - self.flags.insert(Flags::STARTED); - - if payload { - let (ps, pl) = Payload::new(false); - *msg.inner.payload.borrow_mut() = Some(pl); - self.payload = Some(PayloadType::new(&msg.inner.headers, ps)); - } - - // stream extensions - msg.inner_mut().stream_extensions = - self.stream.get_mut().extensions(); - - // set remote addr - msg.inner_mut().addr = self.addr; - - // search handler for request - match self.settings.handler().handle(msg) { - Ok(mut task) => { - if self.tasks.is_empty() { - if expect { - expect = false; - let _ = self.stream.write(&Binary::from( - "HTTP/1.1 100 Continue\r\n\r\n", - )); - } - match task.poll_io(&mut self.stream) { - Ok(Async::Ready(ready)) => { - // override keep-alive state - if self.stream.keepalive() { - self.flags.insert(Flags::KEEPALIVE); - } else { - self.flags.remove(Flags::KEEPALIVE); - } - // prepare stream for next response - self.stream.reset(); - - if !ready { - // task is done with io operations - // but still needs to do more work - spawn(HttpHandlerTaskFut::new(task)); - } - continue 'outer; - } - Ok(Async::NotReady) => (), - Err(err) => { - error!("Unhandled error: {}", err); - self.client_disconnected(false); - return Err(err.into()); - } - } - } - self.tasks.push_back(Entry::Task( - task, - if expect { Some(()) } else { None }, - )); - continue 'outer; - } - Err(_) => { - // handler is not found - self.push_response_entry(StatusCode::NOT_FOUND); - } - } - } - Ok(Some(Message::Chunk(chunk))) => { - updated = true; - if let Some(ref mut payload) = self.payload { - payload.feed_data(chunk); - } else { - error!("Internal server error: unexpected payload chunk"); - self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); - self.push_response_entry(StatusCode::INTERNAL_SERVER_ERROR); - self.error = Some(HttpDispatchError::InternalError); - break; - } - } - Ok(Some(Message::Eof)) => { - updated = true; - if let Some(mut payload) = self.payload.take() { - payload.feed_eof(); - } else { - error!("Internal server error: unexpected eof"); - self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); - self.push_response_entry(StatusCode::INTERNAL_SERVER_ERROR); - self.error = Some(HttpDispatchError::InternalError); - break; - } - } - Ok(None) => { - if self.flags.contains(Flags::READ_DISCONNECTED) { - self.client_disconnected(true); - } - break; - } - Err(e) => { - if let Some(mut payload) = self.payload.take() { - let e = match e { - DecoderError::Io(e) => PayloadError::Io(e), - DecoderError::Error(_) => PayloadError::EncodingCorrupted, - }; - payload.set_error(e); - } - - // Malformed requests should be responded with 400 - self.push_response_entry(StatusCode::BAD_REQUEST); - self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); - self.error = Some(HttpDispatchError::MalformedRequest); - break; - } - } - } - - if self.ka_timer.is_some() && updated { - if let Some(expire) = self.settings.keep_alive_expire() { - self.ka_expire = expire; - } - } - Ok(updated) - } -} - -#[cfg(test)] -mod tests { - use std::net::Shutdown; - use std::{cmp, io, time}; - - use actix::System; - use bytes::{Buf, Bytes, BytesMut}; - use futures::future; - use http::{Method, Version}; - use tokio_io::{AsyncRead, AsyncWrite}; - - use super::*; - use application::{App, HttpApplication}; - use httpmessage::HttpMessage; - use server::h1decoder::Message; - use server::handler::IntoHttpHandler; - use server::settings::{ServerSettings, ServiceConfig}; - use server::{KeepAlive, Request}; - - fn wrk_settings() -> ServiceConfig { - ServiceConfig::::new( - App::new().into_handler(), - KeepAlive::Os, - 5000, - 2000, - ServerSettings::default(), - ) - } - - impl Message { - fn message(self) -> Request { - match self { - Message::Message { msg, .. } => msg, - _ => panic!("error"), - } - } - fn is_payload(&self) -> bool { - match *self { - Message::Message { payload, .. } => payload, - _ => panic!("error"), - } - } - fn chunk(self) -> Bytes { - match self { - Message::Chunk(chunk) => chunk, - _ => panic!("error"), - } - } - fn eof(&self) -> bool { - match *self { - Message::Eof => true, - _ => false, - } - } - } - - macro_rules! parse_ready { - ($e:expr) => {{ - let settings = wrk_settings(); - match H1Decoder::new().decode($e, &settings) { - Ok(Some(msg)) => msg.message(), - Ok(_) => unreachable!("Eof during parsing http request"), - Err(err) => unreachable!("Error during parsing http request: {:?}", err), - } - }}; - } - - macro_rules! expect_parse_err { - ($e:expr) => {{ - let settings = wrk_settings(); - - match H1Decoder::new().decode($e, &settings) { - Err(err) => match err { - DecoderError::Error(_) => (), - _ => unreachable!("Parse error expected"), - }, - _ => unreachable!("Error expected"), - } - }}; - } - - struct Buffer { - buf: Bytes, - err: Option, - } - - impl Buffer { - fn new(data: &'static str) -> Buffer { - Buffer { - buf: Bytes::from(data), - err: None, - } - } - } - - impl AsyncRead for Buffer {} - impl io::Read for Buffer { - fn read(&mut self, dst: &mut [u8]) -> Result { - if self.buf.is_empty() { - if self.err.is_some() { - Err(self.err.take().unwrap()) - } else { - Err(io::Error::new(io::ErrorKind::WouldBlock, "")) - } - } else { - let size = cmp::min(self.buf.len(), dst.len()); - let b = self.buf.split_to(size); - dst[..size].copy_from_slice(&b); - Ok(size) - } - } - } - - impl IoStream for Buffer { - fn shutdown(&mut self, _: Shutdown) -> io::Result<()> { - Ok(()) - } - fn set_nodelay(&mut self, _: bool) -> io::Result<()> { - Ok(()) - } - fn set_linger(&mut self, _: Option) -> io::Result<()> { - Ok(()) - } - fn set_keepalive(&mut self, _: Option) -> io::Result<()> { - Ok(()) - } - } - impl io::Write for Buffer { - fn write(&mut self, buf: &[u8]) -> io::Result { - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } - } - impl AsyncWrite for Buffer { - fn shutdown(&mut self) -> Poll<(), io::Error> { - Ok(Async::Ready(())) - } - fn write_buf(&mut self, _: &mut B) -> Poll { - Ok(Async::NotReady) - } - } - - #[test] - fn test_req_parse_err() { - let mut sys = System::new("test"); - let _ = sys.block_on(future::lazy(|| { - let buf = Buffer::new("GET /test HTTP/1\r\n\r\n"); - let readbuf = BytesMut::new(); - let settings = wrk_settings(); - - let mut h1 = - Http1Dispatcher::new(settings.clone(), buf, readbuf, false, None); - assert!(h1.poll_io().is_ok()); - assert!(h1.poll_io().is_ok()); - assert!(h1.flags.contains(Flags::READ_DISCONNECTED)); - assert_eq!(h1.tasks.len(), 1); - future::ok::<_, ()>(()) - })); - } - - #[test] - fn test_parse() { - let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { - Ok(Some(msg)) => { - let req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_partial() { - let mut buf = BytesMut::from("PUT /test HTTP/1"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { - Ok(None) => (), - _ => unreachable!("Error"), - } - - buf.extend(b".1\r\n\r\n"); - match reader.decode(&mut buf, &settings) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::PUT); - assert_eq!(req.path(), "/test"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_post() { - let mut buf = BytesMut::from("POST /test2 HTTP/1.0\r\n\r\n"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_10); - assert_eq!(*req.method(), Method::POST); - assert_eq!(req.path(), "/test2"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_body() { - let mut buf = - BytesMut::from("GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), - b"body" - ); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_body_crlf() { - let mut buf = - BytesMut::from("\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), - b"body" - ); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_partial_eof() { - let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); - let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); - - buf.extend(b"\r\n"); - match reader.decode(&mut buf, &settings) { - Ok(Some(msg)) => { - let req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_headers_split_field() { - let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - assert! { reader.decode(&mut buf, &settings).unwrap().is_none() } - - buf.extend(b"t"); - assert! { reader.decode(&mut buf, &settings).unwrap().is_none() } - - buf.extend(b"es"); - assert! { reader.decode(&mut buf, &settings).unwrap().is_none() } - - buf.extend(b"t: value\r\n\r\n"); - match reader.decode(&mut buf, &settings) { - Ok(Some(msg)) => { - let req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - assert_eq!(req.headers().get("test").unwrap().as_bytes(), b"value"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_headers_multi_value() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - Set-Cookie: c1=cookie1\r\n\ - Set-Cookie: c2=cookie2\r\n\r\n", - ); - let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - let req = msg.message(); - - let val: Vec<_> = req - .headers() - .get_all("Set-Cookie") - .iter() - .map(|v| v.to_str().unwrap().to_owned()) - .collect(); - assert_eq!(val[0], "c1=cookie1"); - assert_eq!(val[1], "c2=cookie2"); - } - - #[test] - fn test_conn_default_1_0() { - let mut buf = BytesMut::from("GET /test HTTP/1.0\r\n\r\n"); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - } - - #[test] - fn test_conn_default_1_1() { - let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); - let req = parse_ready!(&mut buf); - - assert!(req.keep_alive()); - } - - #[test] - fn test_conn_close() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - connection: close\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - connection: Close\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - } - - #[test] - fn test_conn_close_1_0() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.0\r\n\ - connection: close\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - - let mut buf = BytesMut::from( - "GET /test HTTP/1.0\r\n\ - connection: Close\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - } - - #[test] - fn test_conn_keep_alive_1_0() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.0\r\n\ - connection: Keep-Alive\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.keep_alive()); - - let mut buf = BytesMut::from( - "GET /test HTTP/1.0\r\n\ - connection: keep-alive\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.keep_alive()); - } - - #[test] - fn test_conn_keep_alive_1_1() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - connection: keep-alive\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.keep_alive()); - } - - #[test] - fn test_conn_other_1_0() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.0\r\n\ - connection: other\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - } - - #[test] - fn test_conn_other_1_1() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - connection: other\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.keep_alive()); - } - - #[test] - fn test_conn_upgrade() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - upgrade: websockets\r\n\ - connection: upgrade\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.upgrade()); - - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - upgrade: Websockets\r\n\ - connection: Upgrade\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.upgrade()); - } - - #[test] - fn test_conn_upgrade_connect_method() { - let mut buf = BytesMut::from( - "CONNECT /test HTTP/1.1\r\n\ - content-type: text/plain\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.upgrade()); - } - - #[test] - fn test_request_chunked() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - if let Ok(val) = req.chunked() { - assert!(val); - } else { - unreachable!("Error"); - } - - // type in chunked - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chnked\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - if let Ok(val) = req.chunked() { - assert!(!val); - } else { - unreachable!("Error"); - } - } - - #[test] - fn test_headers_content_length_err_1() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - content-length: line\r\n\r\n", - ); - - expect_parse_err!(&mut buf) - } - - #[test] - fn test_headers_content_length_err_2() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - content-length: -1\r\n\r\n", - ); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_invalid_header() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - test line\r\n\r\n", - ); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_invalid_name() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - test[]: line\r\n\r\n", - ); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_http_request_bad_status_line() { - let mut buf = BytesMut::from("getpath \r\n\r\n"); - expect_parse_err!(&mut buf); - } - - #[test] - fn test_http_request_upgrade() { - let settings = wrk_settings(); - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - connection: upgrade\r\n\ - upgrade: websocket\r\n\r\n\ - some raw data", - ); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); - assert!(!req.keep_alive()); - assert!(req.upgrade()); - assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), - b"some raw data" - ); - } - - #[test] - fn test_http_request_parser_utf8() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - x-test: теÑÑ‚\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert_eq!( - req.headers().get("x-test").unwrap().as_bytes(), - "теÑÑ‚".as_bytes() - ); - } - - #[test] - fn test_http_request_parser_two_slashes() { - let mut buf = BytesMut::from("GET //path HTTP/1.1\r\n\r\n"); - let req = parse_ready!(&mut buf); - - assert_eq!(req.path(), "//path"); - } - - #[test] - fn test_http_request_parser_bad_method() { - let mut buf = BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n"); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_http_request_parser_bad_version() { - let mut buf = BytesMut::from("GET //get HT/11\r\n\r\n"); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_http_request_chunked_payload() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); - assert!(req.chunked().unwrap()); - - buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); - assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), - b"data" - ); - assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), - b"line" - ); - assert!(reader.decode(&mut buf, &settings).unwrap().unwrap().eof()); - } - - #[test] - fn test_http_request_chunked_payload_and_next_message() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); - assert!(req.chunked().unwrap()); - - buf.extend( - b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\ - POST /test2 HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n" - .iter(), - ); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"data"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"line"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.eof()); - - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.is_payload()); - let req2 = msg.message(); - assert!(req2.chunked().unwrap()); - assert_eq!(*req2.method(), Method::POST); - assert!(req2.chunked().unwrap()); - } - - #[test] - fn test_http_request_chunked_payload_chunks() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); - assert!(req.chunked().unwrap()); - - buf.extend(b"4\r\n1111\r\n"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"1111"); - - buf.extend(b"4\r\ndata\r"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"data"); - - buf.extend(b"\n4"); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); - - buf.extend(b"\r"); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); - buf.extend(b"\n"); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); - - buf.extend(b"li"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"li"); - - //trailers - //buf.feed_data("test: test\r\n"); - //not_ready!(reader.parse(&mut buf, &mut readbuf)); - - buf.extend(b"ne\r\n0\r\n"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"ne"); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); - - buf.extend(b"\r\n"); - assert!(reader.decode(&mut buf, &settings).unwrap().unwrap().eof()); - } - - #[test] - fn test_parse_chunked_payload_chunk_extension() { - let mut buf = BytesMut::from( - &"GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n"[..], - ); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.is_payload()); - assert!(msg.message().chunked().unwrap()); - - buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n") - let chunk = reader.decode(&mut buf, &settings).unwrap().unwrap().chunk(); - assert_eq!(chunk, Bytes::from_static(b"data")); - let chunk = reader.decode(&mut buf, &settings).unwrap().unwrap().chunk(); - assert_eq!(chunk, Bytes::from_static(b"line")); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.eof()); - } -} diff --git a/src/server/h1decoder.rs b/src/server/h1decoder.rs deleted file mode 100644 index ece6b3cce..000000000 --- a/src/server/h1decoder.rs +++ /dev/null @@ -1,541 +0,0 @@ -use std::{io, mem}; - -use bytes::{Bytes, BytesMut}; -use futures::{Async, Poll}; -use httparse; - -use super::message::{MessageFlags, Request}; -use super::settings::ServiceConfig; -use error::ParseError; -use http::header::{HeaderName, HeaderValue}; -use http::{header, HttpTryFrom, Method, Uri, Version}; -use uri::Url; - -const MAX_BUFFER_SIZE: usize = 131_072; -const MAX_HEADERS: usize = 96; - -pub(crate) struct H1Decoder { - decoder: Option, -} - -#[derive(Debug)] -pub(crate) enum Message { - Message { - msg: Request, - payload: bool, - expect: bool, - }, - Chunk(Bytes), - Eof, -} - -#[derive(Debug)] -pub(crate) enum DecoderError { - Io(io::Error), - Error(ParseError), -} - -impl From for DecoderError { - fn from(err: io::Error) -> DecoderError { - DecoderError::Io(err) - } -} - -impl H1Decoder { - pub fn new() -> H1Decoder { - H1Decoder { decoder: None } - } - - pub fn decode( - &mut self, - src: &mut BytesMut, - settings: &ServiceConfig, - ) -> Result, DecoderError> { - // read payload - if self.decoder.is_some() { - match self.decoder.as_mut().unwrap().decode(src)? { - Async::Ready(Some(bytes)) => return Ok(Some(Message::Chunk(bytes))), - Async::Ready(None) => { - self.decoder.take(); - return Ok(Some(Message::Eof)); - } - Async::NotReady => return Ok(None), - } - } - - match self - .parse_message(src, settings) - .map_err(DecoderError::Error)? - { - Async::Ready((msg, expect, decoder)) => { - self.decoder = decoder; - Ok(Some(Message::Message { - msg, - expect, - payload: self.decoder.is_some(), - })) - } - Async::NotReady => { - if src.len() >= MAX_BUFFER_SIZE { - error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); - Err(DecoderError::Error(ParseError::TooLarge)) - } else { - Ok(None) - } - } - } - } - - fn parse_message( - &self, - buf: &mut BytesMut, - settings: &ServiceConfig, - ) -> Poll<(Request, bool, Option), ParseError> { - // Parse http message - let mut has_upgrade = false; - let mut chunked = false; - let mut content_length = None; - let mut expect_continue = false; - - let msg = { - // Unsafe: we read only this data only after httparse parses headers into. - // performance bump for pipeline benchmarks. - let mut headers: [HeaderIndex; MAX_HEADERS] = - unsafe { mem::uninitialized() }; - - let (len, method, path, version, headers_len) = { - let mut parsed: [httparse::Header; MAX_HEADERS] = - unsafe { mem::uninitialized() }; - - let mut req = httparse::Request::new(&mut parsed); - match req.parse(buf)? { - httparse::Status::Complete(len) => { - let method = Method::from_bytes(req.method.unwrap().as_bytes()) - .map_err(|_| ParseError::Method)?; - let path = Url::new(Uri::try_from(req.path.unwrap())?); - let version = if req.version.unwrap() == 1 { - Version::HTTP_11 - } else { - Version::HTTP_10 - }; - HeaderIndex::record(buf, req.headers, &mut headers); - - (len, method, path, version, req.headers.len()) - } - httparse::Status::Partial => return Ok(Async::NotReady), - } - }; - - let slice = buf.split_to(len).freeze(); - - // convert headers - let mut msg = settings.get_request(); - { - let inner = msg.inner_mut(); - inner - .flags - .get_mut() - .set(MessageFlags::KEEPALIVE, version != Version::HTTP_10); - - for idx in headers[..headers_len].iter() { - if let Ok(name) = - HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]) - { - // Unsafe: httparse check header value for valid utf-8 - let value = unsafe { - HeaderValue::from_shared_unchecked( - slice.slice(idx.value.0, idx.value.1), - ) - }; - match name { - header::CONTENT_LENGTH => { - if let Ok(s) = value.to_str() { - if let Ok(len) = s.parse::() { - content_length = Some(len); - } else { - debug!("illegal Content-Length: {:?}", len); - return Err(ParseError::Header); - } - } else { - debug!("illegal Content-Length: {:?}", len); - return Err(ParseError::Header); - } - } - // transfer-encoding - header::TRANSFER_ENCODING => { - if let Ok(s) = value.to_str().map(|s| s.trim()) { - chunked = s.eq_ignore_ascii_case("chunked"); - } else { - return Err(ParseError::Header); - } - } - // connection keep-alive state - header::CONNECTION => { - let ka = if let Ok(conn) = - value.to_str().map(|conn| conn.trim()) - { - if version == Version::HTTP_10 - && conn.eq_ignore_ascii_case("keep-alive") - { - true - } else { - version == Version::HTTP_11 - && !(conn.eq_ignore_ascii_case("close") - || conn.eq_ignore_ascii_case("upgrade")) - } - } else { - false - }; - inner.flags.get_mut().set(MessageFlags::KEEPALIVE, ka); - } - header::UPGRADE => { - has_upgrade = true; - // check content-length, some clients (dart) - // sends "content-length: 0" with websocket upgrade - if let Ok(val) = value.to_str().map(|val| val.trim()) { - if val.eq_ignore_ascii_case("websocket") { - content_length = None; - } - } - } - header::EXPECT => { - if value == "100-continue" { - expect_continue = true - } - } - _ => (), - } - - inner.headers.append(name, value); - } else { - return Err(ParseError::Header); - } - } - - inner.url = path; - inner.method = method; - inner.version = version; - } - msg - }; - - // https://tools.ietf.org/html/rfc7230#section-3.3.3 - let decoder = if chunked { - // Chunked encoding - Some(EncodingDecoder::chunked()) - } else if let Some(len) = content_length { - // Content-Length - Some(EncodingDecoder::length(len)) - } else if has_upgrade || msg.inner.method == Method::CONNECT { - // upgrade(websocket) or connect - Some(EncodingDecoder::eof()) - } else { - None - }; - - Ok(Async::Ready((msg, expect_continue, decoder))) - } -} - -#[derive(Clone, Copy)] -pub(crate) struct HeaderIndex { - pub(crate) name: (usize, usize), - pub(crate) value: (usize, usize), -} - -impl HeaderIndex { - pub(crate) fn record( - bytes: &[u8], - headers: &[httparse::Header], - indices: &mut [HeaderIndex], - ) { - let bytes_ptr = bytes.as_ptr() as usize; - for (header, indices) in headers.iter().zip(indices.iter_mut()) { - let name_start = header.name.as_ptr() as usize - bytes_ptr; - let name_end = name_start + header.name.len(); - indices.name = (name_start, name_end); - let value_start = header.value.as_ptr() as usize - bytes_ptr; - let value_end = value_start + header.value.len(); - indices.value = (value_start, value_end); - } - } -} - -/// Decoders to handle different Transfer-Encodings. -/// -/// If a message body does not include a Transfer-Encoding, it *should* -/// include a Content-Length header. -#[derive(Debug, Clone, PartialEq)] -pub struct EncodingDecoder { - kind: Kind, -} - -impl EncodingDecoder { - pub fn length(x: u64) -> EncodingDecoder { - EncodingDecoder { - kind: Kind::Length(x), - } - } - - pub fn chunked() -> EncodingDecoder { - EncodingDecoder { - kind: Kind::Chunked(ChunkedState::Size, 0), - } - } - - pub fn eof() -> EncodingDecoder { - EncodingDecoder { - kind: Kind::Eof(false), - } - } -} - -#[derive(Debug, Clone, PartialEq)] -enum Kind { - /// A Reader used when a Content-Length header is passed with a positive - /// integer. - Length(u64), - /// A Reader used when Transfer-Encoding is `chunked`. - Chunked(ChunkedState, u64), - /// A Reader used for responses that don't indicate a length or chunked. - /// - /// Note: This should only used for `Response`s. It is illegal for a - /// `Request` to be made with both `Content-Length` and - /// `Transfer-Encoding: chunked` missing, as explained from the spec: - /// - /// > If a Transfer-Encoding header field is present in a response and - /// > the chunked transfer coding is not the final encoding, the - /// > message body length is determined by reading the connection until - /// > it is closed by the server. If a Transfer-Encoding header field - /// > is present in a request and the chunked transfer coding is not - /// > the final encoding, the message body length cannot be determined - /// > reliably; the server MUST respond with the 400 (Bad Request) - /// > status code and then close the connection. - Eof(bool), -} - -#[derive(Debug, PartialEq, Clone)] -enum ChunkedState { - Size, - SizeLws, - Extension, - SizeLf, - Body, - BodyCr, - BodyLf, - EndCr, - EndLf, - End, -} - -impl EncodingDecoder { - pub fn decode(&mut self, body: &mut BytesMut) -> Poll, io::Error> { - match self.kind { - Kind::Length(ref mut remaining) => { - if *remaining == 0 { - Ok(Async::Ready(None)) - } else { - if body.is_empty() { - return Ok(Async::NotReady); - } - let len = body.len() as u64; - let buf; - if *remaining > len { - buf = body.take().freeze(); - *remaining -= len; - } else { - buf = body.split_to(*remaining as usize).freeze(); - *remaining = 0; - } - trace!("Length read: {}", buf.len()); - Ok(Async::Ready(Some(buf))) - } - } - Kind::Chunked(ref mut state, ref mut size) => { - loop { - let mut buf = None; - // advances the chunked state - *state = try_ready!(state.step(body, size, &mut buf)); - if *state == ChunkedState::End { - trace!("End of chunked stream"); - return Ok(Async::Ready(None)); - } - if let Some(buf) = buf { - return Ok(Async::Ready(Some(buf))); - } - if body.is_empty() { - return Ok(Async::NotReady); - } - } - } - Kind::Eof(ref mut is_eof) => { - if *is_eof { - Ok(Async::Ready(None)) - } else if !body.is_empty() { - Ok(Async::Ready(Some(body.take().freeze()))) - } else { - Ok(Async::NotReady) - } - } - } - } -} - -macro_rules! byte ( - ($rdr:ident) => ({ - if $rdr.len() > 0 { - let b = $rdr[0]; - $rdr.split_to(1); - b - } else { - return Ok(Async::NotReady) - } - }) -); - -impl ChunkedState { - fn step( - &self, - body: &mut BytesMut, - size: &mut u64, - buf: &mut Option, - ) -> Poll { - use self::ChunkedState::*; - match *self { - Size => ChunkedState::read_size(body, size), - SizeLws => ChunkedState::read_size_lws(body), - Extension => ChunkedState::read_extension(body), - SizeLf => ChunkedState::read_size_lf(body, size), - Body => ChunkedState::read_body(body, size, buf), - BodyCr => ChunkedState::read_body_cr(body), - BodyLf => ChunkedState::read_body_lf(body), - EndCr => ChunkedState::read_end_cr(body), - EndLf => ChunkedState::read_end_lf(body), - End => Ok(Async::Ready(ChunkedState::End)), - } - } - fn read_size(rdr: &mut BytesMut, size: &mut u64) -> Poll { - let radix = 16; - match byte!(rdr) { - b @ b'0'...b'9' => { - *size *= radix; - *size += u64::from(b - b'0'); - } - b @ b'a'...b'f' => { - *size *= radix; - *size += u64::from(b + 10 - b'a'); - } - b @ b'A'...b'F' => { - *size *= radix; - *size += u64::from(b + 10 - b'A'); - } - b'\t' | b' ' => return Ok(Async::Ready(ChunkedState::SizeLws)), - b';' => return Ok(Async::Ready(ChunkedState::Extension)), - b'\r' => return Ok(Async::Ready(ChunkedState::SizeLf)), - _ => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size line: Invalid Size", - )); - } - } - Ok(Async::Ready(ChunkedState::Size)) - } - fn read_size_lws(rdr: &mut BytesMut) -> Poll { - trace!("read_size_lws"); - match byte!(rdr) { - // LWS can follow the chunk size, but no more digits can come - b'\t' | b' ' => Ok(Async::Ready(ChunkedState::SizeLws)), - b';' => Ok(Async::Ready(ChunkedState::Extension)), - b'\r' => Ok(Async::Ready(ChunkedState::SizeLf)), - _ => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size linear white space", - )), - } - } - fn read_extension(rdr: &mut BytesMut) -> Poll { - match byte!(rdr) { - b'\r' => Ok(Async::Ready(ChunkedState::SizeLf)), - _ => Ok(Async::Ready(ChunkedState::Extension)), // no supported extensions - } - } - fn read_size_lf( - rdr: &mut BytesMut, - size: &mut u64, - ) -> Poll { - match byte!(rdr) { - b'\n' if *size > 0 => Ok(Async::Ready(ChunkedState::Body)), - b'\n' if *size == 0 => Ok(Async::Ready(ChunkedState::EndCr)), - _ => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size LF", - )), - } - } - - fn read_body( - rdr: &mut BytesMut, - rem: &mut u64, - buf: &mut Option, - ) -> Poll { - trace!("Chunked read, remaining={:?}", rem); - - let len = rdr.len() as u64; - if len == 0 { - Ok(Async::Ready(ChunkedState::Body)) - } else { - let slice; - if *rem > len { - slice = rdr.take().freeze(); - *rem -= len; - } else { - slice = rdr.split_to(*rem as usize).freeze(); - *rem = 0; - } - *buf = Some(slice); - if *rem > 0 { - Ok(Async::Ready(ChunkedState::Body)) - } else { - Ok(Async::Ready(ChunkedState::BodyCr)) - } - } - } - - fn read_body_cr(rdr: &mut BytesMut) -> Poll { - match byte!(rdr) { - b'\r' => Ok(Async::Ready(ChunkedState::BodyLf)), - _ => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk body CR", - )), - } - } - fn read_body_lf(rdr: &mut BytesMut) -> Poll { - match byte!(rdr) { - b'\n' => Ok(Async::Ready(ChunkedState::Size)), - _ => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk body LF", - )), - } - } - fn read_end_cr(rdr: &mut BytesMut) -> Poll { - match byte!(rdr) { - b'\r' => Ok(Async::Ready(ChunkedState::EndLf)), - _ => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk end CR", - )), - } - } - fn read_end_lf(rdr: &mut BytesMut) -> Poll { - match byte!(rdr) { - b'\n' => Ok(Async::Ready(ChunkedState::End)), - _ => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk end LF", - )), - } - } -} diff --git a/src/server/h1writer.rs b/src/server/h1writer.rs deleted file mode 100644 index 97ce6dff9..000000000 --- a/src/server/h1writer.rs +++ /dev/null @@ -1,364 +0,0 @@ -// #![cfg_attr(feature = "cargo-clippy", allow(redundant_field_names))] - -use std::io::{self, Write}; - -use bytes::{BufMut, BytesMut}; -use futures::{Async, Poll}; -use tokio_io::AsyncWrite; - -use super::helpers; -use super::output::{Output, ResponseInfo, ResponseLength}; -use super::settings::ServiceConfig; -use super::Request; -use super::{Writer, WriterState, MAX_WRITE_BUFFER_SIZE}; -use body::{Binary, Body}; -use header::ContentEncoding; -use http::header::{ - HeaderValue, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, -}; -use http::{Method, Version}; -use httpresponse::HttpResponse; - -const AVERAGE_HEADER_SIZE: usize = 30; // totally scientific - -bitflags! { - struct Flags: u8 { - const STARTED = 0b0000_0001; - const UPGRADE = 0b0000_0010; - const KEEPALIVE = 0b0000_0100; - const DISCONNECTED = 0b0000_1000; - } -} - -pub(crate) struct H1Writer { - flags: Flags, - stream: T, - written: u64, - headers_size: u32, - buffer: Output, - buffer_capacity: usize, - settings: ServiceConfig, -} - -impl H1Writer { - pub fn new(stream: T, settings: ServiceConfig) -> H1Writer { - H1Writer { - flags: Flags::KEEPALIVE, - written: 0, - headers_size: 0, - buffer: Output::Buffer(settings.get_bytes()), - buffer_capacity: 0, - stream, - settings, - } - } - - pub fn get_mut(&mut self) -> &mut T { - &mut self.stream - } - - pub fn reset(&mut self) { - self.written = 0; - self.flags = Flags::KEEPALIVE; - } - - pub fn flushed(&mut self) -> bool { - self.buffer.is_empty() - } - - pub fn disconnected(&mut self) { - self.flags.insert(Flags::DISCONNECTED); - } - - pub fn upgrade(&self) -> bool { - self.flags.contains(Flags::UPGRADE) - } - - pub fn keepalive(&self) -> bool { - self.flags.contains(Flags::KEEPALIVE) && !self.flags.contains(Flags::UPGRADE) - } - - fn write_data(stream: &mut T, data: &[u8]) -> io::Result { - let mut written = 0; - while written < data.len() { - match stream.write(&data[written..]) { - Ok(0) => { - return Err(io::Error::new(io::ErrorKind::WriteZero, "")); - } - Ok(n) => { - written += n; - } - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { - return Ok(written) - } - Err(err) => return Err(err), - } - } - Ok(written) - } -} - -impl Drop for H1Writer { - fn drop(&mut self) { - if let Some(bytes) = self.buffer.take_option() { - self.settings.release_bytes(bytes); - } - } -} - -impl Writer for H1Writer { - #[inline] - fn written(&self) -> u64 { - self.written - } - - #[inline] - fn set_date(&mut self) { - self.settings.set_date(self.buffer.as_mut(), true) - } - - #[inline] - fn buffer(&mut self) -> &mut BytesMut { - self.buffer.as_mut() - } - - fn start( - &mut self, req: &Request, msg: &mut HttpResponse, encoding: ContentEncoding, - ) -> io::Result { - // prepare task - let mut info = ResponseInfo::new(req.inner.method == Method::HEAD); - self.buffer.for_server(&mut info, &req.inner, msg, encoding); - if msg.keep_alive().unwrap_or_else(|| req.keep_alive()) { - self.flags = Flags::STARTED | Flags::KEEPALIVE; - } else { - self.flags = Flags::STARTED; - } - - // Connection upgrade - let version = msg.version().unwrap_or_else(|| req.inner.version); - if msg.upgrade() { - self.flags.insert(Flags::UPGRADE); - msg.headers_mut() - .insert(CONNECTION, HeaderValue::from_static("upgrade")); - } - // keep-alive - else if self.flags.contains(Flags::KEEPALIVE) { - if version < Version::HTTP_11 { - msg.headers_mut() - .insert(CONNECTION, HeaderValue::from_static("keep-alive")); - } - } else if version >= Version::HTTP_11 { - msg.headers_mut() - .insert(CONNECTION, HeaderValue::from_static("close")); - } - let body = msg.replace_body(Body::Empty); - - // render message - { - // output buffer - let mut buffer = self.buffer.as_mut(); - - let reason = msg.reason().as_bytes(); - if let Body::Binary(ref bytes) = body { - buffer.reserve( - 256 + msg.headers().len() * AVERAGE_HEADER_SIZE - + bytes.len() - + reason.len(), - ); - } else { - buffer.reserve( - 256 + msg.headers().len() * AVERAGE_HEADER_SIZE + reason.len(), - ); - } - - // status line - helpers::write_status_line(version, msg.status().as_u16(), &mut buffer); - buffer.extend_from_slice(reason); - - // content length - let mut len_is_set = true; - match info.length { - ResponseLength::Chunked => { - buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") - } - ResponseLength::Length(len) => { - helpers::write_content_length(len, &mut buffer) - } - ResponseLength::Length64(len) => { - buffer.extend_from_slice(b"\r\ncontent-length: "); - write!(buffer.writer(), "{}", len)?; - buffer.extend_from_slice(b"\r\n"); - } - ResponseLength::Zero => { - len_is_set = false; - buffer.extend_from_slice(b"\r\n"); - } - ResponseLength::None => buffer.extend_from_slice(b"\r\n"), - } - if let Some(ce) = info.content_encoding { - buffer.extend_from_slice(b"content-encoding: "); - buffer.extend_from_slice(ce.as_ref()); - buffer.extend_from_slice(b"\r\n"); - } - - // write headers - let mut pos = 0; - let mut has_date = false; - let mut remaining = buffer.remaining_mut(); - let mut buf = unsafe { &mut *(buffer.bytes_mut() as *mut [u8]) }; - for (key, value) in msg.headers() { - match *key { - TRANSFER_ENCODING => continue, - CONTENT_ENCODING => if encoding != ContentEncoding::Identity { - continue; - }, - CONTENT_LENGTH => match info.length { - ResponseLength::None => (), - ResponseLength::Zero => { - len_is_set = true; - } - _ => continue, - }, - DATE => { - has_date = true; - } - _ => (), - } - - let v = value.as_ref(); - let k = key.as_str().as_bytes(); - let len = k.len() + v.len() + 4; - if len > remaining { - unsafe { - buffer.advance_mut(pos); - } - pos = 0; - buffer.reserve(len); - remaining = buffer.remaining_mut(); - unsafe { - buf = &mut *(buffer.bytes_mut() as *mut _); - } - } - - buf[pos..pos + k.len()].copy_from_slice(k); - pos += k.len(); - buf[pos..pos + 2].copy_from_slice(b": "); - pos += 2; - buf[pos..pos + v.len()].copy_from_slice(v); - pos += v.len(); - buf[pos..pos + 2].copy_from_slice(b"\r\n"); - pos += 2; - remaining -= len; - } - unsafe { - buffer.advance_mut(pos); - } - if !len_is_set { - buffer.extend_from_slice(b"content-length: 0\r\n") - } - - // optimized date header, set_date writes \r\n - if !has_date { - self.settings.set_date(&mut buffer, true); - } else { - // msg eof - buffer.extend_from_slice(b"\r\n"); - } - self.headers_size = buffer.len() as u32; - } - - if let Body::Binary(bytes) = body { - self.written = bytes.len() as u64; - self.buffer.write(bytes.as_ref())?; - } else { - // capacity, makes sense only for streaming or actor - self.buffer_capacity = msg.write_buffer_capacity(); - - msg.replace_body(body); - } - Ok(WriterState::Done) - } - - fn write(&mut self, payload: &Binary) -> io::Result { - self.written += payload.len() as u64; - if !self.flags.contains(Flags::DISCONNECTED) { - if self.flags.contains(Flags::STARTED) { - // shortcut for upgraded connection - if self.flags.contains(Flags::UPGRADE) { - if self.buffer.is_empty() { - let pl: &[u8] = payload.as_ref(); - let n = match Self::write_data(&mut self.stream, pl) { - Err(err) => { - self.disconnected(); - return Err(err); - } - Ok(val) => val, - }; - if n < pl.len() { - self.buffer.write(&pl[n..])?; - return Ok(WriterState::Done); - } - } else { - self.buffer.write(payload.as_ref())?; - } - } else { - // TODO: add warning, write after EOF - self.buffer.write(payload.as_ref())?; - } - } else { - // could be response to EXCEPT header - self.buffer.write(payload.as_ref())?; - } - } - - if self.buffer.len() > self.buffer_capacity { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - fn write_eof(&mut self) -> io::Result { - if !self.buffer.write_eof()? { - Err(io::Error::new( - io::ErrorKind::Other, - "Last payload item, but eof is not reached", - )) - } else if self.buffer.len() > MAX_WRITE_BUFFER_SIZE { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - #[inline] - fn poll_completed(&mut self, shutdown: bool) -> Poll<(), io::Error> { - if self.flags.contains(Flags::DISCONNECTED) { - return Err(io::Error::new(io::ErrorKind::Other, "disconnected")); - } - - if !self.buffer.is_empty() { - let written = { - match Self::write_data(&mut self.stream, self.buffer.as_ref().as_ref()) { - Err(err) => { - self.disconnected(); - return Err(err); - } - Ok(val) => val, - } - }; - let _ = self.buffer.split_to(written); - if shutdown && !self.buffer.is_empty() - || (self.buffer.len() > self.buffer_capacity) - { - return Ok(Async::NotReady); - } - } - if shutdown { - self.stream.poll_flush()?; - self.stream.shutdown() - } else { - Ok(self.stream.poll_flush()?) - } - } -} diff --git a/src/server/h2.rs b/src/server/h2.rs deleted file mode 100644 index c9e968a39..000000000 --- a/src/server/h2.rs +++ /dev/null @@ -1,472 +0,0 @@ -use std::collections::VecDeque; -use std::io::{Read, Write}; -use std::net::SocketAddr; -use std::rc::Rc; -use std::time::Instant; -use std::{cmp, io, mem}; - -use bytes::{Buf, Bytes}; -use futures::{Async, Future, Poll, Stream}; -use http2::server::{self, Connection, Handshake, SendResponse}; -use http2::{Reason, RecvStream}; -use modhttp::request::Parts; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_timer::Delay; - -use error::{Error, PayloadError}; -use extensions::Extensions; -use http::{StatusCode, Version}; -use payload::{Payload, PayloadStatus, PayloadWriter}; -use uri::Url; - -use super::error::{HttpDispatchError, ServerError}; -use super::h2writer::H2Writer; -use super::input::PayloadType; -use super::settings::ServiceConfig; -use super::{HttpHandler, HttpHandlerTask, IoStream, Writer}; - -bitflags! { - struct Flags: u8 { - const DISCONNECTED = 0b0000_0001; - const SHUTDOWN = 0b0000_0010; - } -} - -/// HTTP/2 Transport -pub(crate) struct Http2 -where - T: AsyncRead + AsyncWrite + 'static, - H: HttpHandler + 'static, -{ - flags: Flags, - settings: ServiceConfig, - addr: Option, - state: State>, - tasks: VecDeque>, - extensions: Option>, - ka_expire: Instant, - ka_timer: Option, -} - -enum State { - Handshake(Handshake), - Connection(Connection), - Empty, -} - -impl Http2 -where - T: IoStream + 'static, - H: HttpHandler + 'static, -{ - pub fn new( - settings: ServiceConfig, - io: T, - buf: Bytes, - keepalive_timer: Option, - ) -> Self { - let addr = io.peer_addr(); - let extensions = io.extensions(); - - // keep-alive timeout - let (ka_expire, ka_timer) = if let Some(delay) = keepalive_timer { - (delay.deadline(), Some(delay)) - } else if let Some(delay) = settings.keep_alive_timer() { - (delay.deadline(), Some(delay)) - } else { - (settings.now(), None) - }; - - Http2 { - flags: Flags::empty(), - tasks: VecDeque::new(), - state: State::Handshake(server::handshake(IoWrapper { - unread: if buf.is_empty() { None } else { Some(buf) }, - inner: io, - })), - addr, - settings, - extensions, - ka_expire, - ka_timer, - } - } - - pub fn poll(&mut self) -> Poll<(), HttpDispatchError> { - self.poll_keepalive()?; - - // server - if let State::Connection(ref mut conn) = self.state { - loop { - // shutdown connection - if self.flags.contains(Flags::SHUTDOWN) { - return conn.poll_close().map_err(|e| e.into()); - } - - let mut not_ready = true; - let disconnected = self.flags.contains(Flags::DISCONNECTED); - - // check in-flight connections - for item in &mut self.tasks { - // read payload - if !disconnected { - item.poll_payload(); - } - - if !item.flags.contains(EntryFlags::EOF) { - if disconnected { - item.flags.insert(EntryFlags::EOF); - } else { - let retry = item.payload.need_read() == PayloadStatus::Read; - loop { - match item.task.poll_io(&mut item.stream) { - Ok(Async::Ready(ready)) => { - if ready { - item.flags.insert( - EntryFlags::EOF | EntryFlags::FINISHED, - ); - } else { - item.flags.insert(EntryFlags::EOF); - } - not_ready = false; - } - Ok(Async::NotReady) => { - if item.payload.need_read() - == PayloadStatus::Read - && !retry - { - continue; - } - } - Err(err) => { - error!("Unhandled error: {}", err); - item.flags.insert( - EntryFlags::EOF - | EntryFlags::ERROR - | EntryFlags::WRITE_DONE, - ); - item.stream.reset(Reason::INTERNAL_ERROR); - } - } - break; - } - } - } - - if item.flags.contains(EntryFlags::EOF) - && !item.flags.contains(EntryFlags::FINISHED) - { - match item.task.poll_completed() { - Ok(Async::NotReady) => (), - Ok(Async::Ready(_)) => { - item.flags.insert( - EntryFlags::FINISHED | EntryFlags::WRITE_DONE, - ); - } - Err(err) => { - item.flags.insert( - EntryFlags::ERROR - | EntryFlags::WRITE_DONE - | EntryFlags::FINISHED, - ); - error!("Unhandled error: {}", err); - } - } - } - - if item.flags.contains(EntryFlags::FINISHED) - && !item.flags.contains(EntryFlags::WRITE_DONE) - && !disconnected - { - match item.stream.poll_completed(false) { - Ok(Async::NotReady) => (), - Ok(Async::Ready(_)) => { - not_ready = false; - item.flags.insert(EntryFlags::WRITE_DONE); - } - Err(_) => { - item.flags.insert(EntryFlags::ERROR); - } - } - } - } - - // cleanup finished tasks - while !self.tasks.is_empty() { - if self.tasks[0].flags.contains(EntryFlags::FINISHED) - && self.tasks[0].flags.contains(EntryFlags::WRITE_DONE) - || self.tasks[0].flags.contains(EntryFlags::ERROR) - { - self.tasks.pop_front(); - } else { - break; - } - } - - // get request - if !self.flags.contains(Flags::DISCONNECTED) { - match conn.poll() { - Ok(Async::Ready(None)) => { - not_ready = false; - self.flags.insert(Flags::DISCONNECTED); - for entry in &mut self.tasks { - entry.task.disconnected() - } - } - Ok(Async::Ready(Some((req, resp)))) => { - not_ready = false; - let (parts, body) = req.into_parts(); - - // update keep-alive expire - if self.ka_timer.is_some() { - if let Some(expire) = self.settings.keep_alive_expire() { - self.ka_expire = expire; - } - } - - self.tasks.push_back(Entry::new( - parts, - body, - resp, - self.addr, - self.settings.clone(), - self.extensions.clone(), - )); - } - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(err) => { - trace!("Connection error: {}", err); - self.flags.insert(Flags::SHUTDOWN); - for entry in &mut self.tasks { - entry.task.disconnected() - } - continue; - } - } - } - - if not_ready { - if self.tasks.is_empty() && self.flags.contains(Flags::DISCONNECTED) - { - return conn.poll_close().map_err(|e| e.into()); - } else { - return Ok(Async::NotReady); - } - } - } - } - - // handshake - self.state = if let State::Handshake(ref mut handshake) = self.state { - match handshake.poll() { - Ok(Async::Ready(conn)) => State::Connection(conn), - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(err) => { - trace!("Error handling connection: {}", err); - return Err(err.into()); - } - } - } else { - mem::replace(&mut self.state, State::Empty) - }; - - self.poll() - } - - /// keep-alive timer. returns `true` is keep-alive, otherwise drop - fn poll_keepalive(&mut self) -> Result<(), HttpDispatchError> { - if let Some(ref mut timer) = self.ka_timer { - match timer.poll() { - Ok(Async::Ready(_)) => { - // if we get timer during shutdown, just drop connection - if self.flags.contains(Flags::SHUTDOWN) { - return Err(HttpDispatchError::ShutdownTimeout); - } - if timer.deadline() >= self.ka_expire { - // check for any outstanding request handling - if self.tasks.is_empty() { - return Err(HttpDispatchError::ShutdownTimeout); - } else if let Some(dl) = self.settings.keep_alive_expire() { - timer.reset(dl); - let _ = timer.poll(); - } - } else { - timer.reset(self.ka_expire); - let _ = timer.poll(); - } - } - Ok(Async::NotReady) => (), - Err(e) => { - error!("Timer error {:?}", e); - return Err(HttpDispatchError::Unknown); - } - } - } - - Ok(()) - } -} - -bitflags! { - struct EntryFlags: u8 { - const EOF = 0b0000_0001; - const REOF = 0b0000_0010; - const ERROR = 0b0000_0100; - const FINISHED = 0b0000_1000; - const WRITE_DONE = 0b0001_0000; - } -} - -enum EntryPipe { - Task(H::Task), - Error(Box), -} - -impl EntryPipe { - fn disconnected(&mut self) { - match *self { - EntryPipe::Task(ref mut task) => task.disconnected(), - EntryPipe::Error(ref mut task) => task.disconnected(), - } - } - fn poll_io(&mut self, io: &mut Writer) -> Poll { - match *self { - EntryPipe::Task(ref mut task) => task.poll_io(io), - EntryPipe::Error(ref mut task) => task.poll_io(io), - } - } - fn poll_completed(&mut self) -> Poll<(), Error> { - match *self { - EntryPipe::Task(ref mut task) => task.poll_completed(), - EntryPipe::Error(ref mut task) => task.poll_completed(), - } - } -} - -struct Entry { - task: EntryPipe, - payload: PayloadType, - recv: RecvStream, - stream: H2Writer, - flags: EntryFlags, -} - -impl Entry { - fn new( - parts: Parts, - recv: RecvStream, - resp: SendResponse, - addr: Option, - settings: ServiceConfig, - extensions: Option>, - ) -> Entry - where - H: HttpHandler + 'static, - { - // Payload and Content-Encoding - let (psender, payload) = Payload::new(false); - - let mut msg = settings.get_request(); - { - let inner = msg.inner_mut(); - inner.url = Url::new(parts.uri); - inner.method = parts.method; - inner.version = parts.version; - inner.headers = parts.headers; - inner.stream_extensions = extensions; - *inner.payload.borrow_mut() = Some(payload); - inner.addr = addr; - } - - // Payload sender - let psender = PayloadType::new(msg.headers(), psender); - - // start request processing - let task = match settings.handler().handle(msg) { - Ok(task) => EntryPipe::Task(task), - Err(_) => EntryPipe::Error(ServerError::err( - Version::HTTP_2, - StatusCode::NOT_FOUND, - )), - }; - - Entry { - task, - recv, - payload: psender, - stream: H2Writer::new(resp, settings), - flags: EntryFlags::empty(), - } - } - - fn poll_payload(&mut self) { - while !self.flags.contains(EntryFlags::REOF) - && self.payload.need_read() == PayloadStatus::Read - { - match self.recv.poll() { - Ok(Async::Ready(Some(chunk))) => { - let l = chunk.len(); - self.payload.feed_data(chunk); - if let Err(err) = self.recv.release_capacity().release_capacity(l) { - self.payload.set_error(PayloadError::Http2(err)); - break; - } - } - Ok(Async::Ready(None)) => { - self.flags.insert(EntryFlags::REOF); - self.payload.feed_eof(); - } - Ok(Async::NotReady) => break, - Err(err) => { - self.payload.set_error(PayloadError::Http2(err)); - break; - } - } - } - } -} - -struct IoWrapper { - unread: Option, - inner: T, -} - -impl Read for IoWrapper { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - if let Some(mut bytes) = self.unread.take() { - let size = cmp::min(buf.len(), bytes.len()); - buf[..size].copy_from_slice(&bytes[..size]); - if bytes.len() > size { - bytes.split_to(size); - self.unread = Some(bytes); - } - Ok(size) - } else { - self.inner.read(buf) - } - } -} - -impl Write for IoWrapper { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.inner.write(buf) - } - fn flush(&mut self) -> io::Result<()> { - self.inner.flush() - } -} - -impl AsyncRead for IoWrapper { - unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { - self.inner.prepare_uninitialized_buffer(buf) - } -} - -impl AsyncWrite for IoWrapper { - fn shutdown(&mut self) -> Poll<(), io::Error> { - self.inner.shutdown() - } - fn write_buf(&mut self, buf: &mut B) -> Poll { - self.inner.write_buf(buf) - } -} diff --git a/src/server/h2writer.rs b/src/server/h2writer.rs deleted file mode 100644 index fef6f889a..000000000 --- a/src/server/h2writer.rs +++ /dev/null @@ -1,268 +0,0 @@ -#![cfg_attr( - feature = "cargo-clippy", - allow(redundant_field_names) -)] - -use std::{cmp, io}; - -use bytes::{Bytes, BytesMut}; -use futures::{Async, Poll}; -use http2::server::SendResponse; -use http2::{Reason, SendStream}; -use modhttp::Response; - -use super::helpers; -use super::message::Request; -use super::output::{Output, ResponseInfo, ResponseLength}; -use super::settings::ServiceConfig; -use super::{Writer, WriterState, MAX_WRITE_BUFFER_SIZE}; -use body::{Binary, Body}; -use header::ContentEncoding; -use http::header::{ - HeaderValue, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, -}; -use http::{HttpTryFrom, Method, Version}; -use httpresponse::HttpResponse; - -const CHUNK_SIZE: usize = 16_384; - -bitflags! { - struct Flags: u8 { - const STARTED = 0b0000_0001; - const DISCONNECTED = 0b0000_0010; - const EOF = 0b0000_0100; - const RESERVED = 0b0000_1000; - } -} - -pub(crate) struct H2Writer { - respond: SendResponse, - stream: Option>, - flags: Flags, - written: u64, - buffer: Output, - buffer_capacity: usize, - settings: ServiceConfig, -} - -impl H2Writer { - pub fn new(respond: SendResponse, settings: ServiceConfig) -> H2Writer { - H2Writer { - stream: None, - flags: Flags::empty(), - written: 0, - buffer: Output::Buffer(settings.get_bytes()), - buffer_capacity: 0, - respond, - settings, - } - } - - pub fn reset(&mut self, reason: Reason) { - if let Some(mut stream) = self.stream.take() { - stream.send_reset(reason) - } - } -} - -impl Drop for H2Writer { - fn drop(&mut self) { - self.settings.release_bytes(self.buffer.take()); - } -} - -impl Writer for H2Writer { - fn written(&self) -> u64 { - self.written - } - - #[inline] - fn set_date(&mut self) { - self.settings.set_date(self.buffer.as_mut(), true) - } - - #[inline] - fn buffer(&mut self) -> &mut BytesMut { - self.buffer.as_mut() - } - - fn start( - &mut self, req: &Request, msg: &mut HttpResponse, encoding: ContentEncoding, - ) -> io::Result { - // prepare response - self.flags.insert(Flags::STARTED); - let mut info = ResponseInfo::new(req.inner.method == Method::HEAD); - self.buffer.for_server(&mut info, &req.inner, msg, encoding); - - let mut has_date = false; - let mut resp = Response::new(()); - let mut len_is_set = false; - *resp.status_mut() = msg.status(); - *resp.version_mut() = Version::HTTP_2; - for (key, value) in msg.headers().iter() { - match *key { - // http2 specific - CONNECTION | TRANSFER_ENCODING => continue, - CONTENT_ENCODING => if encoding != ContentEncoding::Identity { - continue; - }, - CONTENT_LENGTH => match info.length { - ResponseLength::None => (), - ResponseLength::Zero => { - len_is_set = true; - } - _ => continue, - }, - DATE => has_date = true, - _ => (), - } - resp.headers_mut().append(key, value.clone()); - } - - // set date header - if !has_date { - let mut bytes = BytesMut::with_capacity(29); - self.settings.set_date(&mut bytes, false); - resp.headers_mut() - .insert(DATE, HeaderValue::try_from(bytes.freeze()).unwrap()); - } - - // content length - match info.length { - ResponseLength::Zero => { - if !len_is_set { - resp.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::from_static("0")); - } - self.flags.insert(Flags::EOF); - } - ResponseLength::Length(len) => { - let mut val = BytesMut::new(); - helpers::convert_usize(len, &mut val); - let l = val.len(); - resp.headers_mut().insert( - CONTENT_LENGTH, - HeaderValue::try_from(val.split_to(l - 2).freeze()).unwrap(), - ); - } - ResponseLength::Length64(len) => { - let l = format!("{}", len); - resp.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::try_from(l.as_str()).unwrap()); - } - ResponseLength::None => { - self.flags.insert(Flags::EOF); - } - _ => (), - } - if let Some(ce) = info.content_encoding { - resp.headers_mut() - .insert(CONTENT_ENCODING, HeaderValue::try_from(ce).unwrap()); - } - - trace!("Response: {:?}", resp); - - match self - .respond - .send_response(resp, self.flags.contains(Flags::EOF)) - { - Ok(stream) => self.stream = Some(stream), - Err(_) => return Err(io::Error::new(io::ErrorKind::Other, "err")), - } - - let body = msg.replace_body(Body::Empty); - if let Body::Binary(bytes) = body { - if bytes.is_empty() { - Ok(WriterState::Done) - } else { - self.flags.insert(Flags::EOF); - self.buffer.write(bytes.as_ref())?; - if let Some(ref mut stream) = self.stream { - self.flags.insert(Flags::RESERVED); - stream.reserve_capacity(cmp::min(self.buffer.len(), CHUNK_SIZE)); - } - Ok(WriterState::Pause) - } - } else { - msg.replace_body(body); - self.buffer_capacity = msg.write_buffer_capacity(); - Ok(WriterState::Done) - } - } - - fn write(&mut self, payload: &Binary) -> io::Result { - if !self.flags.contains(Flags::DISCONNECTED) { - if self.flags.contains(Flags::STARTED) { - // TODO: add warning, write after EOF - self.buffer.write(payload.as_ref())?; - } else { - // might be response for EXCEPT - error!("Not supported"); - } - } - - if self.buffer.len() > self.buffer_capacity { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - fn write_eof(&mut self) -> io::Result { - self.flags.insert(Flags::EOF); - if !self.buffer.write_eof()? { - Err(io::Error::new( - io::ErrorKind::Other, - "Last payload item, but eof is not reached", - )) - } else if self.buffer.len() > MAX_WRITE_BUFFER_SIZE { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - fn poll_completed(&mut self, _shutdown: bool) -> Poll<(), io::Error> { - if !self.flags.contains(Flags::STARTED) { - return Ok(Async::NotReady); - } - - if let Some(ref mut stream) = self.stream { - // reserve capacity - if !self.flags.contains(Flags::RESERVED) && !self.buffer.is_empty() { - self.flags.insert(Flags::RESERVED); - stream.reserve_capacity(cmp::min(self.buffer.len(), CHUNK_SIZE)); - } - - loop { - match stream.poll_capacity() { - Ok(Async::NotReady) => return Ok(Async::NotReady), - Ok(Async::Ready(None)) => return Ok(Async::Ready(())), - Ok(Async::Ready(Some(cap))) => { - let len = self.buffer.len(); - let bytes = self.buffer.split_to(cmp::min(cap, len)); - let eof = - self.buffer.is_empty() && self.flags.contains(Flags::EOF); - self.written += bytes.len() as u64; - - if let Err(e) = stream.send_data(bytes.freeze(), eof) { - return Err(io::Error::new(io::ErrorKind::Other, e)); - } else if !self.buffer.is_empty() { - let cap = cmp::min(self.buffer.len(), CHUNK_SIZE); - stream.reserve_capacity(cap); - } else { - if eof { - stream.reserve_capacity(0); - continue; - } - self.flags.remove(Flags::RESERVED); - return Ok(Async::Ready(())); - } - } - Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)), - } - } - } - Ok(Async::Ready(())) - } -} diff --git a/src/server/handler.rs b/src/server/handler.rs deleted file mode 100644 index 33e50ac34..000000000 --- a/src/server/handler.rs +++ /dev/null @@ -1,208 +0,0 @@ -use futures::{Async, Future, Poll}; - -use super::message::Request; -use super::Writer; -use error::Error; - -/// Low level http request handler -#[allow(unused_variables)] -pub trait HttpHandler: 'static { - /// Request handling task - type Task: HttpHandlerTask; - - /// Handle request - fn handle(&self, req: Request) -> Result; -} - -impl HttpHandler for Box>> { - type Task = Box; - - fn handle(&self, req: Request) -> Result, Request> { - self.as_ref().handle(req) - } -} - -/// Low level http request handler -pub trait HttpHandlerTask { - /// Poll task, this method is used before or after *io* object is available - fn poll_completed(&mut self) -> Poll<(), Error> { - Ok(Async::Ready(())) - } - - /// Poll task when *io* object is available - fn poll_io(&mut self, io: &mut Writer) -> Poll; - - /// Connection is disconnected - fn disconnected(&mut self) {} -} - -impl HttpHandlerTask for Box { - fn poll_io(&mut self, io: &mut Writer) -> Poll { - self.as_mut().poll_io(io) - } -} - -pub(super) struct HttpHandlerTaskFut { - task: T, -} - -impl HttpHandlerTaskFut { - pub(crate) fn new(task: T) -> Self { - Self { task } - } -} - -impl Future for HttpHandlerTaskFut { - type Item = (); - type Error = (); - - fn poll(&mut self) -> Poll<(), ()> { - self.task.poll_completed().map_err(|_| ()) - } -} - -/// Conversion helper trait -pub trait IntoHttpHandler { - /// The associated type which is result of conversion. - type Handler: HttpHandler; - - /// Convert into `HttpHandler` object. - fn into_handler(self) -> Self::Handler; -} - -impl IntoHttpHandler for T { - type Handler = T; - - fn into_handler(self) -> Self::Handler { - self - } -} - -impl IntoHttpHandler for Vec { - type Handler = VecHttpHandler; - - fn into_handler(self) -> Self::Handler { - VecHttpHandler(self.into_iter().map(|item| item.into_handler()).collect()) - } -} - -#[doc(hidden)] -pub struct VecHttpHandler(Vec); - -impl HttpHandler for VecHttpHandler { - type Task = H::Task; - - fn handle(&self, mut req: Request) -> Result { - for h in &self.0 { - req = match h.handle(req) { - Ok(task) => return Ok(task), - Err(e) => e, - }; - } - Err(req) - } -} - -macro_rules! http_handler ({$EN:ident, $(($n:tt, $T:ident)),+} => { - impl<$($T: HttpHandler,)+> HttpHandler for ($($T,)+) { - type Task = $EN<$($T,)+>; - - fn handle(&self, mut req: Request) -> Result { - $( - req = match self.$n.handle(req) { - Ok(task) => return Ok($EN::$T(task)), - Err(e) => e, - }; - )+ - Err(req) - } - } - - #[doc(hidden)] - pub enum $EN<$($T: HttpHandler,)+> { - $($T ($T::Task),)+ - } - - impl<$($T: HttpHandler,)+> HttpHandlerTask for $EN<$($T,)+> - { - fn poll_completed(&mut self) -> Poll<(), Error> { - match self { - $($EN :: $T(ref mut task) => task.poll_completed(),)+ - } - } - - fn poll_io(&mut self, io: &mut Writer) -> Poll { - match self { - $($EN::$T(ref mut task) => task.poll_io(io),)+ - } - } - - /// Connection is disconnected - fn disconnected(&mut self) { - match self { - $($EN::$T(ref mut task) => task.disconnected(),)+ - } - } - } -}); - -http_handler!(HttpHandlerTask1, (0, A)); -http_handler!(HttpHandlerTask2, (0, A), (1, B)); -http_handler!(HttpHandlerTask3, (0, A), (1, B), (2, C)); -http_handler!(HttpHandlerTask4, (0, A), (1, B), (2, C), (3, D)); -http_handler!(HttpHandlerTask5, (0, A), (1, B), (2, C), (3, D), (4, E)); -http_handler!( - HttpHandlerTask6, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F) -); -http_handler!( - HttpHandlerTask7, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G) -); -http_handler!( - HttpHandlerTask8, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H) -); -http_handler!( - HttpHandlerTask9, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H), - (8, I) -); -http_handler!( - HttpHandlerTask10, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H), - (8, I), - (9, J) -); diff --git a/src/server/helpers.rs b/src/server/helpers.rs deleted file mode 100644 index e4ccd8aef..000000000 --- a/src/server/helpers.rs +++ /dev/null @@ -1,208 +0,0 @@ -use bytes::{BufMut, BytesMut}; -use http::Version; -use std::{mem, ptr, slice}; - -const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\ - 2021222324252627282930313233343536373839\ - 4041424344454647484950515253545556575859\ - 6061626364656667686970717273747576777879\ - 8081828384858687888990919293949596979899"; - -pub(crate) const STATUS_LINE_BUF_SIZE: usize = 13; - -pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesMut) { - let mut buf: [u8; STATUS_LINE_BUF_SIZE] = [ - b'H', b'T', b'T', b'P', b'/', b'1', b'.', b'1', b' ', b' ', b' ', b' ', b' ', - ]; - match version { - Version::HTTP_2 => buf[5] = b'2', - Version::HTTP_10 => buf[7] = b'0', - Version::HTTP_09 => { - buf[5] = b'0'; - buf[7] = b'9'; - } - _ => (), - } - - let mut curr: isize = 12; - let buf_ptr = buf.as_mut_ptr(); - let lut_ptr = DEC_DIGITS_LUT.as_ptr(); - let four = n > 999; - - // decode 2 more chars, if > 2 chars - let d1 = (n % 100) << 1; - n /= 100; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2); - } - - // decode last 1 or 2 chars - if n < 10 { - curr -= 1; - unsafe { - *buf_ptr.offset(curr) = (n as u8) + b'0'; - } - } else { - let d1 = n << 1; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping( - lut_ptr.offset(d1 as isize), - buf_ptr.offset(curr), - 2, - ); - } - } - - bytes.put_slice(&buf); - if four { - bytes.put(b' '); - } -} - -/// NOTE: bytes object has to contain enough space -pub fn write_content_length(mut n: usize, bytes: &mut BytesMut) { - if n < 10 { - let mut buf: [u8; 21] = [ - b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', b'e', - b'n', b'g', b't', b'h', b':', b' ', b'0', b'\r', b'\n', - ]; - buf[18] = (n as u8) + b'0'; - bytes.put_slice(&buf); - } else if n < 100 { - let mut buf: [u8; 22] = [ - b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', b'e', - b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'\r', b'\n', - ]; - let d1 = n << 1; - unsafe { - ptr::copy_nonoverlapping( - DEC_DIGITS_LUT.as_ptr().add(d1), - buf.as_mut_ptr().offset(18), - 2, - ); - } - bytes.put_slice(&buf); - } else if n < 1000 { - let mut buf: [u8; 23] = [ - b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', b'e', - b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'0', b'\r', b'\n', - ]; - // decode 2 more chars, if > 2 chars - let d1 = (n % 100) << 1; - n /= 100; - unsafe { - ptr::copy_nonoverlapping( - DEC_DIGITS_LUT.as_ptr().add(d1), - buf.as_mut_ptr().offset(19), - 2, - ) - }; - - // decode last 1 - buf[18] = (n as u8) + b'0'; - - bytes.put_slice(&buf); - } else { - bytes.put_slice(b"\r\ncontent-length: "); - convert_usize(n, bytes); - } -} - -pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) { - let mut curr: isize = 39; - let mut buf: [u8; 41] = unsafe { mem::uninitialized() }; - buf[39] = b'\r'; - buf[40] = b'\n'; - let buf_ptr = buf.as_mut_ptr(); - let lut_ptr = DEC_DIGITS_LUT.as_ptr(); - - // eagerly decode 4 characters at a time - while n >= 10_000 { - let rem = (n % 10_000) as isize; - n /= 10_000; - - let d1 = (rem / 100) << 1; - let d2 = (rem % 100) << 1; - curr -= 4; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); - ptr::copy_nonoverlapping(lut_ptr.offset(d2), buf_ptr.offset(curr + 2), 2); - } - } - - // if we reach here numbers are <= 9999, so at most 4 chars long - let mut n = n as isize; // possibly reduce 64bit math - - // decode 2 more chars, if > 2 chars - if n >= 100 { - let d1 = (n % 100) << 1; - n /= 100; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); - } - } - - // decode last 1 or 2 chars - if n < 10 { - curr -= 1; - unsafe { - *buf_ptr.offset(curr) = (n as u8) + b'0'; - } - } else { - let d1 = n << 1; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); - } - } - - unsafe { - bytes.extend_from_slice(slice::from_raw_parts( - buf_ptr.offset(curr), - 41 - curr as usize, - )); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_write_content_length() { - let mut bytes = BytesMut::new(); - bytes.reserve(50); - write_content_length(0, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 0\r\n"[..]); - bytes.reserve(50); - write_content_length(9, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 9\r\n"[..]); - bytes.reserve(50); - write_content_length(10, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 10\r\n"[..]); - bytes.reserve(50); - write_content_length(99, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 99\r\n"[..]); - bytes.reserve(50); - write_content_length(100, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 100\r\n"[..]); - bytes.reserve(50); - write_content_length(101, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 101\r\n"[..]); - bytes.reserve(50); - write_content_length(998, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 998\r\n"[..]); - bytes.reserve(50); - write_content_length(1000, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 1000\r\n"[..]); - bytes.reserve(50); - write_content_length(1001, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 1001\r\n"[..]); - bytes.reserve(50); - write_content_length(5909, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 5909\r\n"[..]); - } -} diff --git a/src/server/http.rs b/src/server/http.rs deleted file mode 100644 index 5ff621af2..000000000 --- a/src/server/http.rs +++ /dev/null @@ -1,579 +0,0 @@ -use std::{fmt, io, mem, net}; - -use actix::{Addr, System}; -use actix_net::server::Server; -use actix_net::service::NewService; -use actix_net::ssl; - -use net2::TcpBuilder; -use num_cpus; - -#[cfg(feature = "tls")] -use native_tls::TlsAcceptor; - -#[cfg(any(feature = "alpn", feature = "ssl"))] -use openssl::ssl::SslAcceptorBuilder; - -#[cfg(feature = "rust-tls")] -use rustls::ServerConfig; - -use super::acceptor::{AcceptorServiceFactory, DefaultAcceptor}; -use super::builder::{HttpServiceBuilder, ServiceProvider}; -use super::{IntoHttpHandler, KeepAlive}; - -struct Socket { - scheme: &'static str, - lst: net::TcpListener, - addr: net::SocketAddr, - handler: Box, -} - -/// An HTTP Server -/// -/// By default it serves HTTP2 when HTTPs is enabled, -/// in order to change it, use `ServerFlags` that can be provided -/// to acceptor service. -pub struct HttpServer -where - H: IntoHttpHandler + 'static, - F: Fn() -> H + Send + Clone, -{ - pub(super) factory: F, - pub(super) host: Option, - pub(super) keep_alive: KeepAlive, - pub(super) client_timeout: u64, - pub(super) client_shutdown: u64, - backlog: i32, - threads: usize, - exit: bool, - shutdown_timeout: u16, - no_http2: bool, - no_signals: bool, - maxconn: usize, - maxconnrate: usize, - sockets: Vec, -} - -impl HttpServer -where - H: IntoHttpHandler + 'static, - F: Fn() -> H + Send + Clone + 'static, -{ - /// Create new http server with application factory - pub fn new(factory: F) -> HttpServer { - HttpServer { - factory, - threads: num_cpus::get(), - host: None, - backlog: 2048, - keep_alive: KeepAlive::Timeout(5), - shutdown_timeout: 30, - exit: false, - no_http2: false, - no_signals: false, - maxconn: 25_600, - maxconnrate: 256, - client_timeout: 5000, - client_shutdown: 5000, - sockets: Vec::new(), - } - } - - /// Set number of workers to start. - /// - /// By default http server uses number of available logical cpu as threads - /// count. - pub fn workers(mut self, num: usize) -> Self { - self.threads = num; - self - } - - /// Set the maximum number of pending connections. - /// - /// This refers to the number of clients that can be waiting to be served. - /// Exceeding this number results in the client getting an error when - /// attempting to connect. It should only affect servers under significant - /// load. - /// - /// Generally set in the 64-2048 range. Default value is 2048. - /// - /// This method should be called before `bind()` method call. - pub fn backlog(mut self, num: i32) -> Self { - self.backlog = num; - self - } - - /// Sets the maximum per-worker number of concurrent connections. - /// - /// All socket listeners will stop accepting connections when this limit is reached - /// for each worker. - /// - /// By default max connections is set to a 25k. - pub fn maxconn(mut self, num: usize) -> Self { - self.maxconn = num; - self - } - - /// Sets the maximum per-worker concurrent connection establish process. - /// - /// All listeners will stop accepting connections when this limit is reached. It - /// can be used to limit the global SSL CPU usage. - /// - /// By default max connections is set to a 256. - pub fn maxconnrate(mut self, num: usize) -> Self { - self.maxconnrate = num; - self - } - - /// Set server keep-alive setting. - /// - /// By default keep alive is set to a 5 seconds. - pub fn keep_alive>(mut self, val: T) -> Self { - self.keep_alive = val.into(); - self - } - - /// Set server client timeout in milliseconds for first request. - /// - /// Defines a timeout for reading client request header. If a client does not transmit - /// the entire set headers within this time, the request is terminated with - /// the 408 (Request Time-out) error. - /// - /// To disable timeout set value to 0. - /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_timeout(mut self, val: u64) -> Self { - self.client_timeout = val; - self - } - - /// Set server connection shutdown timeout in milliseconds. - /// - /// Defines a timeout for shutdown connection. If a shutdown procedure does not complete - /// within this time, the request is dropped. - /// - /// To disable timeout set value to 0. - /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_shutdown(mut self, val: u64) -> Self { - self.client_shutdown = val; - self - } - - /// Set server host name. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - pub fn server_hostname(mut self, val: String) -> Self { - self.host = Some(val); - self - } - - /// Stop actix system. - /// - /// `SystemExit` message stops currently running system. - pub fn system_exit(mut self) -> Self { - self.exit = true; - self - } - - /// Disable signal handling - pub fn disable_signals(mut self) -> Self { - self.no_signals = true; - self - } - - /// Timeout for graceful workers shutdown. - /// - /// After receiving a stop signal, workers have this much time to finish - /// serving requests. Workers still alive after the timeout are force - /// dropped. - /// - /// By default shutdown timeout sets to 30 seconds. - pub fn shutdown_timeout(mut self, sec: u16) -> Self { - self.shutdown_timeout = sec; - self - } - - /// Disable `HTTP/2` support - pub fn no_http2(mut self) -> Self { - self.no_http2 = true; - self - } - - /// Get addresses of bound sockets. - pub fn addrs(&self) -> Vec { - self.sockets.iter().map(|s| s.addr).collect() - } - - /// Get addresses of bound sockets and the scheme for it. - /// - /// This is useful when the server is bound from different sources - /// with some sockets listening on http and some listening on https - /// and the user should be presented with an enumeration of which - /// socket requires which protocol. - pub fn addrs_with_scheme(&self) -> Vec<(net::SocketAddr, &str)> { - self.sockets.iter().map(|s| (s.addr, s.scheme)).collect() - } - - /// Use listener for accepting incoming connection requests - /// - /// HttpServer does not change any configuration for TcpListener, - /// it needs to be configured before passing it to listen() method. - pub fn listen(mut self, lst: net::TcpListener) -> Self { - let addr = lst.local_addr().unwrap(); - self.sockets.push(Socket { - lst, - addr, - scheme: "http", - handler: Box::new(HttpServiceBuilder::new( - self.factory.clone(), - DefaultAcceptor, - )), - }); - - self - } - - #[doc(hidden)] - /// Use listener for accepting incoming connection requests - pub fn listen_with(mut self, lst: net::TcpListener, acceptor: A) -> Self - where - A: AcceptorServiceFactory, - ::InitError: fmt::Debug, - { - let addr = lst.local_addr().unwrap(); - self.sockets.push(Socket { - lst, - addr, - scheme: "https", - handler: Box::new(HttpServiceBuilder::new(self.factory.clone(), acceptor)), - }); - - self - } - - #[cfg(feature = "tls")] - /// Use listener for accepting incoming tls connection requests - /// - /// HttpServer does not change any configuration for TcpListener, - /// it needs to be configured before passing it to listen() method. - pub fn listen_tls(self, lst: net::TcpListener, acceptor: TlsAcceptor) -> Self { - use actix_net::service::NewServiceExt; - - self.listen_with(lst, move || { - ssl::NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) - }) - } - - #[cfg(any(feature = "alpn", feature = "ssl"))] - /// Use listener for accepting incoming tls connection requests - /// - /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn listen_ssl( - self, lst: net::TcpListener, builder: SslAcceptorBuilder, - ) -> io::Result { - use super::{openssl_acceptor_with_flags, ServerFlags}; - use actix_net::service::NewServiceExt; - - let flags = if self.no_http2 { - ServerFlags::HTTP1 - } else { - ServerFlags::HTTP1 | ServerFlags::HTTP2 - }; - - let acceptor = openssl_acceptor_with_flags(builder, flags)?; - Ok(self.listen_with(lst, move || { - ssl::OpensslAcceptor::new(acceptor.clone()).map_err(|_| ()) - })) - } - - #[cfg(feature = "rust-tls")] - /// Use listener for accepting incoming tls connection requests - /// - /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn listen_rustls(self, lst: net::TcpListener, config: ServerConfig) -> Self { - use super::{RustlsAcceptor, ServerFlags}; - use actix_net::service::NewServiceExt; - - // alpn support - let flags = if self.no_http2 { - ServerFlags::HTTP1 - } else { - ServerFlags::HTTP1 | ServerFlags::HTTP2 - }; - - self.listen_with(lst, move || { - RustlsAcceptor::with_flags(config.clone(), flags).map_err(|_| ()) - }) - } - - /// The socket address to bind - /// - /// To bind multiple addresses this method can be called multiple times. - pub fn bind(mut self, addr: S) -> io::Result { - let sockets = self.bind2(addr)?; - - for lst in sockets { - self = self.listen(lst); - } - - Ok(self) - } - - /// Start listening for incoming connections with supplied acceptor. - #[doc(hidden)] - #[cfg_attr( - feature = "cargo-clippy", - allow(needless_pass_by_value) - )] - pub fn bind_with(mut self, addr: S, acceptor: A) -> io::Result - where - S: net::ToSocketAddrs, - A: AcceptorServiceFactory, - ::InitError: fmt::Debug, - { - let sockets = self.bind2(addr)?; - - for lst in sockets { - let addr = lst.local_addr().unwrap(); - self.sockets.push(Socket { - lst, - addr, - scheme: "https", - handler: Box::new(HttpServiceBuilder::new( - self.factory.clone(), - acceptor.clone(), - )), - }); - } - - Ok(self) - } - - fn bind2( - &self, addr: S, - ) -> io::Result> { - let mut err = None; - let mut succ = false; - let mut sockets = Vec::new(); - for addr in addr.to_socket_addrs()? { - match create_tcp_listener(addr, self.backlog) { - Ok(lst) => { - succ = true; - sockets.push(lst); - } - Err(e) => err = Some(e), - } - } - - if !succ { - if let Some(e) = err.take() { - Err(e) - } else { - Err(io::Error::new( - io::ErrorKind::Other, - "Can not bind to address.", - )) - } - } else { - Ok(sockets) - } - } - - #[cfg(feature = "tls")] - /// The ssl socket address to bind - /// - /// To bind multiple addresses this method can be called multiple times. - pub fn bind_tls( - self, addr: S, acceptor: TlsAcceptor, - ) -> io::Result { - use actix_net::service::NewServiceExt; - use actix_net::ssl::NativeTlsAcceptor; - - self.bind_with(addr, move || { - NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) - }) - } - - #[cfg(any(feature = "alpn", feature = "ssl"))] - /// Start listening for incoming tls connections. - /// - /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn bind_ssl(self, addr: S, builder: SslAcceptorBuilder) -> io::Result - where - S: net::ToSocketAddrs, - { - use super::{openssl_acceptor_with_flags, ServerFlags}; - use actix_net::service::NewServiceExt; - - // alpn support - let flags = if self.no_http2 { - ServerFlags::HTTP1 - } else { - ServerFlags::HTTP1 | ServerFlags::HTTP2 - }; - - let acceptor = openssl_acceptor_with_flags(builder, flags)?; - self.bind_with(addr, move || { - ssl::OpensslAcceptor::new(acceptor.clone()).map_err(|_| ()) - }) - } - - #[cfg(feature = "rust-tls")] - /// Start listening for incoming tls connections. - /// - /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn bind_rustls( - self, addr: S, builder: ServerConfig, - ) -> io::Result { - use super::{RustlsAcceptor, ServerFlags}; - use actix_net::service::NewServiceExt; - - // alpn support - let flags = if self.no_http2 { - ServerFlags::HTTP1 - } else { - ServerFlags::HTTP1 | ServerFlags::HTTP2 - }; - - self.bind_with(addr, move || { - RustlsAcceptor::with_flags(builder.clone(), flags).map_err(|_| ()) - }) - } -} - -impl H + Send + Clone> HttpServer { - /// Start listening for incoming connections. - /// - /// This method starts number of http workers in separate threads. - /// For each address this method starts separate thread which does - /// `accept()` in a loop. - /// - /// This methods panics if no socket address can be bound or an `Actix` system is not yet - /// configured. - /// - /// ```rust - /// extern crate actix_web; - /// extern crate actix; - /// use actix_web::{server, App, HttpResponse}; - /// - /// fn main() { - /// let sys = actix::System::new("example"); // <- create Actix system - /// - /// server::new(|| App::new().resource("/", |r| r.h(|_: &_| HttpResponse::Ok()))) - /// .bind("127.0.0.1:0") - /// .expect("Can not bind to 127.0.0.1:0") - /// .start(); - /// # actix::System::current().stop(); - /// sys.run(); // <- Run actix system, this method starts all async processes - /// } - /// ``` - pub fn start(mut self) -> Addr { - ssl::max_concurrent_ssl_connect(self.maxconnrate); - - let mut srv = Server::new() - .workers(self.threads) - .maxconn(self.maxconn) - .shutdown_timeout(self.shutdown_timeout); - - srv = if self.exit { srv.system_exit() } else { srv }; - srv = if self.no_signals { - srv.disable_signals() - } else { - srv - }; - - let sockets = mem::replace(&mut self.sockets, Vec::new()); - - for socket in sockets { - let host = self - .host - .as_ref() - .map(|h| h.to_owned()) - .unwrap_or_else(|| format!("{}", socket.addr)); - let (secure, client_shutdown) = if socket.scheme == "https" { - (true, self.client_shutdown) - } else { - (false, 0) - }; - srv = socket.handler.register( - srv, - socket.lst, - host, - socket.addr, - self.keep_alive, - secure, - self.client_timeout, - client_shutdown, - ); - } - srv.start() - } - - /// Spawn new thread and start listening for incoming connections. - /// - /// This method spawns new thread and starts new actix system. Other than - /// that it is similar to `start()` method. This method blocks. - /// - /// This methods panics if no socket addresses get bound. - /// - /// ```rust,ignore - /// # extern crate futures; - /// # extern crate actix_web; - /// # use futures::Future; - /// use actix_web::*; - /// - /// fn main() { - /// HttpServer::new(|| App::new().resource("/", |r| r.h(|_| HttpResponse::Ok()))) - /// .bind("127.0.0.1:0") - /// .expect("Can not bind to 127.0.0.1:0") - /// .run(); - /// } - /// ``` - pub fn run(self) { - let sys = System::new("http-server"); - self.start(); - sys.run(); - } - - /// Register current http server as actix-net's server service - pub fn register(self, mut srv: Server) -> Server { - for socket in self.sockets { - let host = self - .host - .as_ref() - .map(|h| h.to_owned()) - .unwrap_or_else(|| format!("{}", socket.addr)); - let (secure, client_shutdown) = if socket.scheme == "https" { - (true, self.client_shutdown) - } else { - (false, 0) - }; - srv = socket.handler.register( - srv, - socket.lst, - host, - socket.addr, - self.keep_alive, - secure, - self.client_timeout, - client_shutdown, - ); - } - srv - } -} - -fn create_tcp_listener( - addr: net::SocketAddr, backlog: i32, -) -> io::Result { - let builder = match addr { - net::SocketAddr::V4(_) => TcpBuilder::new_v4()?, - net::SocketAddr::V6(_) => TcpBuilder::new_v6()?, - }; - builder.reuse_address(true)?; - builder.bind(addr)?; - Ok(builder.listen(backlog)?) -} diff --git a/src/server/incoming.rs b/src/server/incoming.rs deleted file mode 100644 index b13bba2a7..000000000 --- a/src/server/incoming.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Support for `Stream`, deprecated! -use std::{io, net}; - -use actix::{Actor, Arbiter, AsyncContext, Context, Handler, Message}; -use futures::{Future, Stream}; -use tokio_io::{AsyncRead, AsyncWrite}; - -use super::channel::{HttpChannel, WrapperStream}; -use super::handler::{HttpHandler, IntoHttpHandler}; -use super::http::HttpServer; -use super::settings::{ServerSettings, ServiceConfig}; - -impl Message for WrapperStream { - type Result = (); -} - -impl HttpServer -where - H: IntoHttpHandler, - F: Fn() -> H + Send + Clone, -{ - #[doc(hidden)] - #[deprecated(since = "0.7.8")] - /// Start listening for incoming connections from a stream. - /// - /// This method uses only one thread for handling incoming connections. - pub fn start_incoming(self, stream: S, secure: bool) - where - S: Stream + 'static, - T: AsyncRead + AsyncWrite + 'static, - { - // set server settings - let addr: net::SocketAddr = "127.0.0.1:8080".parse().unwrap(); - let apps = (self.factory)().into_handler(); - let settings = ServiceConfig::new( - apps, - self.keep_alive, - self.client_timeout, - self.client_shutdown, - ServerSettings::new(addr, "127.0.0.1:8080", secure), - ); - - // start server - HttpIncoming::create(move |ctx| { - ctx.add_message_stream(stream.map_err(|_| ()).map(WrapperStream::new)); - HttpIncoming { settings } - }); - } -} - -struct HttpIncoming { - settings: ServiceConfig, -} - -impl Actor for HttpIncoming { - type Context = Context; -} - -impl Handler> for HttpIncoming -where - T: AsyncRead + AsyncWrite, - H: HttpHandler, -{ - type Result = (); - - fn handle(&mut self, msg: WrapperStream, _: &mut Context) -> Self::Result { - Arbiter::spawn(HttpChannel::new(self.settings.clone(), msg).map_err(|_| ())); - } -} diff --git a/src/server/input.rs b/src/server/input.rs deleted file mode 100644 index d23d1e991..000000000 --- a/src/server/input.rs +++ /dev/null @@ -1,288 +0,0 @@ -use std::io::{self, Write}; - -#[cfg(feature = "brotli")] -use brotli2::write::BrotliDecoder; -use bytes::{Bytes, BytesMut}; -use error::PayloadError; -#[cfg(feature = "flate2")] -use flate2::write::{GzDecoder, ZlibDecoder}; -use header::ContentEncoding; -use http::header::{HeaderMap, CONTENT_ENCODING}; -use payload::{PayloadSender, PayloadStatus, PayloadWriter}; - -pub(crate) enum PayloadType { - Sender(PayloadSender), - Encoding(Box), -} - -impl PayloadType { - #[cfg(any(feature = "brotli", feature = "flate2"))] - pub fn new(headers: &HeaderMap, sender: PayloadSender) -> PayloadType { - // check content-encoding - let enc = if let Some(enc) = headers.get(CONTENT_ENCODING) { - if let Ok(enc) = enc.to_str() { - ContentEncoding::from(enc) - } else { - ContentEncoding::Auto - } - } else { - ContentEncoding::Auto - }; - - match enc { - ContentEncoding::Auto | ContentEncoding::Identity => { - PayloadType::Sender(sender) - } - _ => PayloadType::Encoding(Box::new(EncodedPayload::new(sender, enc))), - } - } - - #[cfg(not(any(feature = "brotli", feature = "flate2")))] - pub fn new(headers: &HeaderMap, sender: PayloadSender) -> PayloadType { - PayloadType::Sender(sender) - } -} - -impl PayloadWriter for PayloadType { - #[inline] - fn set_error(&mut self, err: PayloadError) { - match *self { - PayloadType::Sender(ref mut sender) => sender.set_error(err), - PayloadType::Encoding(ref mut enc) => enc.set_error(err), - } - } - - #[inline] - fn feed_eof(&mut self) { - match *self { - PayloadType::Sender(ref mut sender) => sender.feed_eof(), - PayloadType::Encoding(ref mut enc) => enc.feed_eof(), - } - } - - #[inline] - fn feed_data(&mut self, data: Bytes) { - match *self { - PayloadType::Sender(ref mut sender) => sender.feed_data(data), - PayloadType::Encoding(ref mut enc) => enc.feed_data(data), - } - } - - #[inline] - fn need_read(&self) -> PayloadStatus { - match *self { - PayloadType::Sender(ref sender) => sender.need_read(), - PayloadType::Encoding(ref enc) => enc.need_read(), - } - } -} - -/// Payload wrapper with content decompression support -pub(crate) struct EncodedPayload { - inner: PayloadSender, - error: bool, - payload: PayloadStream, -} - -impl EncodedPayload { - pub fn new(inner: PayloadSender, enc: ContentEncoding) -> EncodedPayload { - EncodedPayload { - inner, - error: false, - payload: PayloadStream::new(enc), - } - } -} - -impl PayloadWriter for EncodedPayload { - fn set_error(&mut self, err: PayloadError) { - self.inner.set_error(err) - } - - fn feed_eof(&mut self) { - if !self.error { - match self.payload.feed_eof() { - Err(err) => { - self.error = true; - self.set_error(PayloadError::Io(err)); - } - Ok(value) => { - if let Some(b) = value { - self.inner.feed_data(b); - } - self.inner.feed_eof(); - } - } - } - } - - fn feed_data(&mut self, data: Bytes) { - if self.error { - return; - } - - match self.payload.feed_data(data) { - Ok(Some(b)) => self.inner.feed_data(b), - Ok(None) => (), - Err(e) => { - self.error = true; - self.set_error(e.into()); - } - } - } - - #[inline] - fn need_read(&self) -> PayloadStatus { - self.inner.need_read() - } -} - -pub(crate) enum Decoder { - #[cfg(feature = "flate2")] - Deflate(Box>), - #[cfg(feature = "flate2")] - Gzip(Box>), - #[cfg(feature = "brotli")] - Br(Box>), - Identity, -} - -pub(crate) struct Writer { - buf: BytesMut, -} - -impl Writer { - fn new() -> Writer { - Writer { - buf: BytesMut::with_capacity(8192), - } - } - fn take(&mut self) -> Bytes { - self.buf.take().freeze() - } -} - -impl io::Write for Writer { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.buf.extend_from_slice(buf); - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -/// Payload stream with decompression support -pub(crate) struct PayloadStream { - decoder: Decoder, -} - -impl PayloadStream { - pub fn new(enc: ContentEncoding) -> PayloadStream { - let decoder = match enc { - #[cfg(feature = "brotli")] - ContentEncoding::Br => { - Decoder::Br(Box::new(BrotliDecoder::new(Writer::new()))) - } - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => { - Decoder::Deflate(Box::new(ZlibDecoder::new(Writer::new()))) - } - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => { - Decoder::Gzip(Box::new(GzDecoder::new(Writer::new()))) - } - _ => Decoder::Identity, - }; - PayloadStream { decoder } - } -} - -impl PayloadStream { - pub fn feed_eof(&mut self) -> io::Result> { - match self.decoder { - #[cfg(feature = "brotli")] - Decoder::Br(ref mut decoder) => match decoder.finish() { - Ok(mut writer) => { - let b = writer.take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - #[cfg(feature = "flate2")] - Decoder::Gzip(ref mut decoder) => match decoder.try_finish() { - Ok(_) => { - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - #[cfg(feature = "flate2")] - Decoder::Deflate(ref mut decoder) => match decoder.try_finish() { - Ok(_) => { - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - Decoder::Identity => Ok(None), - } - } - - pub fn feed_data(&mut self, data: Bytes) -> io::Result> { - match self.decoder { - #[cfg(feature = "brotli")] - Decoder::Br(ref mut decoder) => match decoder.write_all(&data) { - Ok(_) => { - decoder.flush()?; - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - #[cfg(feature = "flate2")] - Decoder::Gzip(ref mut decoder) => match decoder.write_all(&data) { - Ok(_) => { - decoder.flush()?; - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - #[cfg(feature = "flate2")] - Decoder::Deflate(ref mut decoder) => match decoder.write_all(&data) { - Ok(_) => { - decoder.flush()?; - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - Decoder::Identity => Ok(Some(data)), - } - } -} diff --git a/src/server/message.rs b/src/server/message.rs deleted file mode 100644 index 9c4bc1ec4..000000000 --- a/src/server/message.rs +++ /dev/null @@ -1,284 +0,0 @@ -use std::cell::{Cell, Ref, RefCell, RefMut}; -use std::collections::VecDeque; -use std::fmt; -use std::net::SocketAddr; -use std::rc::Rc; - -use http::{header, HeaderMap, Method, Uri, Version}; - -use extensions::Extensions; -use httpmessage::HttpMessage; -use info::ConnectionInfo; -use payload::Payload; -use server::ServerSettings; -use uri::Url as InnerUrl; - -bitflags! { - pub(crate) struct MessageFlags: u8 { - const KEEPALIVE = 0b0000_0001; - const CONN_INFO = 0b0000_0010; - } -} - -/// Request's context -pub struct Request { - pub(crate) inner: Rc, -} - -pub(crate) struct InnerRequest { - pub(crate) version: Version, - pub(crate) method: Method, - pub(crate) url: InnerUrl, - pub(crate) flags: Cell, - pub(crate) headers: HeaderMap, - pub(crate) extensions: RefCell, - pub(crate) addr: Option, - pub(crate) info: RefCell, - pub(crate) payload: RefCell>, - pub(crate) settings: ServerSettings, - pub(crate) stream_extensions: Option>, - pool: &'static RequestPool, -} - -impl InnerRequest { - #[inline] - /// Reset request instance - pub fn reset(&mut self) { - self.headers.clear(); - self.extensions.borrow_mut().clear(); - self.flags.set(MessageFlags::empty()); - *self.payload.borrow_mut() = None; - } -} - -impl HttpMessage for Request { - type Stream = Payload; - - fn headers(&self) -> &HeaderMap { - &self.inner.headers - } - - #[inline] - fn payload(&self) -> Payload { - if let Some(payload) = self.inner.payload.borrow_mut().take() { - payload - } else { - Payload::empty() - } - } -} - -impl Request { - /// Create new RequestContext instance - pub(crate) fn new(pool: &'static RequestPool, settings: ServerSettings) -> Request { - Request { - inner: Rc::new(InnerRequest { - pool, - settings, - method: Method::GET, - url: InnerUrl::default(), - version: Version::HTTP_11, - headers: HeaderMap::with_capacity(16), - flags: Cell::new(MessageFlags::empty()), - addr: None, - info: RefCell::new(ConnectionInfo::default()), - payload: RefCell::new(None), - extensions: RefCell::new(Extensions::new()), - stream_extensions: None, - }), - } - } - - #[inline] - pub(crate) fn inner(&self) -> &InnerRequest { - self.inner.as_ref() - } - - #[inline] - pub(crate) fn inner_mut(&mut self) -> &mut InnerRequest { - Rc::get_mut(&mut self.inner).expect("Multiple copies exist") - } - - #[inline] - pub(crate) fn url(&self) -> &InnerUrl { - &self.inner().url - } - - /// Read the Request Uri. - #[inline] - pub fn uri(&self) -> &Uri { - self.inner().url.uri() - } - - /// Read the Request method. - #[inline] - pub fn method(&self) -> &Method { - &self.inner().method - } - - /// Read the Request Version. - #[inline] - pub fn version(&self) -> Version { - self.inner().version - } - - /// The target path of this Request. - #[inline] - pub fn path(&self) -> &str { - self.inner().url.path() - } - - #[inline] - /// Returns Request's headers. - pub fn headers(&self) -> &HeaderMap { - &self.inner().headers - } - - #[inline] - /// Returns mutable Request's headers. - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.inner_mut().headers - } - - /// Peer socket address - /// - /// Peer address is actual socket address, if proxy is used in front of - /// actix http server, then peer address would be address of this proxy. - /// - /// To get client connection information `connection_info()` method should - /// be used. - pub fn peer_addr(&self) -> Option { - self.inner().addr - } - - /// Checks if a connection should be kept alive. - #[inline] - pub fn keep_alive(&self) -> bool { - self.inner().flags.get().contains(MessageFlags::KEEPALIVE) - } - - /// Request extensions - #[inline] - pub fn extensions(&self) -> Ref { - self.inner().extensions.borrow() - } - - /// Mutable reference to a the request's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut { - self.inner().extensions.borrow_mut() - } - - /// Check if request requires connection upgrade - pub fn upgrade(&self) -> bool { - if let Some(conn) = self.inner().headers.get(header::CONNECTION) { - if let Ok(s) = conn.to_str() { - return s.to_lowercase().contains("upgrade"); - } - } - self.inner().method == Method::CONNECT - } - - /// Get *ConnectionInfo* for the correct request. - pub fn connection_info(&self) -> Ref { - if self.inner().flags.get().contains(MessageFlags::CONN_INFO) { - self.inner().info.borrow() - } else { - let mut flags = self.inner().flags.get(); - flags.insert(MessageFlags::CONN_INFO); - self.inner().flags.set(flags); - self.inner().info.borrow_mut().update(self); - self.inner().info.borrow() - } - } - - /// Io stream extensions - #[inline] - pub fn stream_extensions(&self) -> Option<&Extensions> { - self.inner().stream_extensions.as_ref().map(|e| e.as_ref()) - } - - /// Server settings - #[inline] - pub fn server_settings(&self) -> &ServerSettings { - &self.inner().settings - } - - pub(crate) fn clone(&self) -> Self { - Request { - inner: self.inner.clone(), - } - } - - pub(crate) fn release(self) { - let mut inner = self.inner; - if let Some(r) = Rc::get_mut(&mut inner) { - r.reset(); - } else { - return; - } - inner.pool.release(inner); - } -} - -impl fmt::Debug for Request { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!( - f, - "\nRequest {:?} {}:{}", - self.version(), - self.method(), - self.path() - )?; - if let Some(q) = self.uri().query().as_ref() { - writeln!(f, " query: ?{:?}", q)?; - } - writeln!(f, " headers:")?; - for (key, val) in self.headers().iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -pub(crate) struct RequestPool( - RefCell>>, - RefCell, -); - -thread_local!(static POOL: &'static RequestPool = RequestPool::create()); - -impl RequestPool { - fn create() -> &'static RequestPool { - let pool = RequestPool( - RefCell::new(VecDeque::with_capacity(128)), - RefCell::new(ServerSettings::default()), - ); - Box::leak(Box::new(pool)) - } - - pub fn pool(settings: ServerSettings) -> &'static RequestPool { - POOL.with(|p| { - *p.1.borrow_mut() = settings; - *p - }) - } - - #[inline] - pub fn get(pool: &'static RequestPool) -> Request { - if let Some(msg) = pool.0.borrow_mut().pop_front() { - Request { inner: msg } - } else { - Request::new(pool, pool.1.borrow().clone()) - } - } - - #[inline] - /// Release request instance - pub fn release(&self, msg: Rc) { - let v = &mut self.0.borrow_mut(); - if v.len() < 128 { - v.push_front(msg); - } - } -} diff --git a/src/server/mod.rs b/src/server/mod.rs deleted file mode 100644 index 641298542..000000000 --- a/src/server/mod.rs +++ /dev/null @@ -1,370 +0,0 @@ -//! Http server module -//! -//! The module contains everything necessary to setup -//! HTTP server. -//! -//! In order to start HTTP server, first you need to create and configure it -//! using factory that can be supplied to [new](fn.new.html). -//! -//! ## Factory -//! -//! Factory is a function that returns Application, describing how -//! to serve incoming HTTP requests. -//! -//! As the server uses worker pool, the factory function is restricted to trait bounds -//! `Send + Clone + 'static` so that each worker would be able to accept Application -//! without a need for synchronization. -//! -//! If you wish to share part of state among all workers you should -//! wrap it in `Arc` and potentially synchronization primitive like -//! [RwLock](https://doc.rust-lang.org/std/sync/struct.RwLock.html) -//! If the wrapped type is not thread safe. -//! -//! Note though that locking is not advisable for asynchronous programming -//! and you should minimize all locks in your request handlers -//! -//! ## HTTPS Support -//! -//! Actix-web provides support for major crates that provides TLS. -//! Each TLS implementation is provided with [AcceptorService](trait.AcceptorService.html) -//! that describes how HTTP Server accepts connections. -//! -//! For `bind` and `listen` there are corresponding `bind_ssl|tls|rustls` and `listen_ssl|tls|rustls` that accepts -//! these services. -//! -//! **NOTE:** `native-tls` doesn't support `HTTP2` yet -//! -//! ## Signal handling and shutdown -//! -//! By default HTTP Server listens for system signals -//! and, gracefully shuts down at most after 30 seconds. -//! -//! Both signal handling and shutdown timeout can be controlled -//! using corresponding methods. -//! -//! If worker, for some reason, unable to shut down within timeout -//! it is forcibly dropped. -//! -//! ## Example -//! -//! ```rust,ignore -//!extern crate actix; -//!extern crate actix_web; -//!extern crate rustls; -//! -//!use actix_web::{http, middleware, server, App, Error, HttpRequest, HttpResponse, Responder}; -//!use std::io::BufReader; -//!use rustls::internal::pemfile::{certs, rsa_private_keys}; -//!use rustls::{NoClientAuth, ServerConfig}; -//! -//!fn index(req: &HttpRequest) -> Result { -//! Ok(HttpResponse::Ok().content_type("text/plain").body("Welcome!")) -//!} -//! -//!fn load_ssl() -> ServerConfig { -//! use std::io::BufReader; -//! -//! const CERT: &'static [u8] = include_bytes!("../cert.pem"); -//! const KEY: &'static [u8] = include_bytes!("../key.pem"); -//! -//! let mut cert = BufReader::new(CERT); -//! let mut key = BufReader::new(KEY); -//! -//! let mut config = ServerConfig::new(NoClientAuth::new()); -//! let cert_chain = certs(&mut cert).unwrap(); -//! let mut keys = rsa_private_keys(&mut key).unwrap(); -//! config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); -//! -//! config -//!} -//! -//!fn main() { -//! let sys = actix::System::new("http-server"); -//! // load ssl keys -//! let config = load_ssl(); -//! -//! // create and start server at once -//! server::new(|| { -//! App::new() -//! // register simple handler, handle all methods -//! .resource("/index.html", |r| r.f(index)) -//! })) -//! }).bind_rustls("127.0.0.1:8443", config) -//! .unwrap() -//! .start(); -//! -//! println!("Started http server: 127.0.0.1:8080"); -//! //Run system so that server would start accepting connections -//! let _ = sys.run(); -//!} -//! ``` -use std::net::{Shutdown, SocketAddr}; -use std::rc::Rc; -use std::{io, time}; - -use bytes::{BufMut, BytesMut}; -use futures::{Async, Poll}; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_tcp::TcpStream; - -pub use actix_net::server::{PauseServer, ResumeServer, StopServer}; - -pub(crate) mod acceptor; -pub(crate) mod builder; -mod channel; -mod error; -pub(crate) mod h1; -pub(crate) mod h1decoder; -mod h1writer; -mod h2; -mod h2writer; -mod handler; -pub(crate) mod helpers; -mod http; -pub(crate) mod incoming; -pub(crate) mod input; -pub(crate) mod message; -pub(crate) mod output; -pub(crate) mod service; -pub(crate) mod settings; -mod ssl; - -pub use self::handler::*; -pub use self::http::HttpServer; -pub use self::message::Request; -pub use self::ssl::*; - -pub use self::error::{AcceptorError, HttpDispatchError}; -pub use self::settings::ServerSettings; - -#[doc(hidden)] -pub use self::acceptor::AcceptorTimeout; - -#[doc(hidden)] -pub use self::settings::{ServiceConfig, ServiceConfigBuilder}; - -#[doc(hidden)] -pub use self::service::{H1Service, HttpService, StreamConfiguration}; - -#[doc(hidden)] -pub use self::helpers::write_content_length; - -use body::Binary; -use extensions::Extensions; -use header::ContentEncoding; -use httpresponse::HttpResponse; - -/// max buffer size 64k -pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; - -const LW_BUFFER_SIZE: usize = 4096; -const HW_BUFFER_SIZE: usize = 32_768; - -/// Create new http server with application factory. -/// -/// This is shortcut for `server::HttpServer::new()` method. -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate actix; -/// use actix_web::{server, App, HttpResponse}; -/// -/// fn main() { -/// let sys = actix::System::new("example"); // <- create Actix system -/// -/// server::new( -/// || App::new() -/// .resource("/", |r| r.f(|_| HttpResponse::Ok()))) -/// .bind("127.0.0.1:59090").unwrap() -/// .start(); -/// -/// # actix::System::current().stop(); -/// sys.run(); -/// } -/// ``` -pub fn new(factory: F) -> HttpServer -where - F: Fn() -> H + Send + Clone + 'static, - H: IntoHttpHandler + 'static, -{ - HttpServer::new(factory) -} - -#[doc(hidden)] -bitflags! { - ///Flags that can be used to configure HTTP Server. - pub struct ServerFlags: u8 { - ///Use HTTP1 protocol - const HTTP1 = 0b0000_0001; - ///Use HTTP2 protocol - const HTTP2 = 0b0000_0010; - } -} - -#[derive(Debug, PartialEq, Clone, Copy)] -/// Server keep-alive setting -pub enum KeepAlive { - /// Keep alive in seconds - Timeout(usize), - /// Use `SO_KEEPALIVE` socket option, value in seconds - Tcp(usize), - /// Relay on OS to shutdown tcp connection - Os, - /// Disabled - Disabled, -} - -impl From for KeepAlive { - fn from(keepalive: usize) -> Self { - KeepAlive::Timeout(keepalive) - } -} - -impl From> for KeepAlive { - fn from(keepalive: Option) -> Self { - if let Some(keepalive) = keepalive { - KeepAlive::Timeout(keepalive) - } else { - KeepAlive::Disabled - } - } -} - -#[doc(hidden)] -#[derive(Debug)] -pub enum WriterState { - Done, - Pause, -} - -#[doc(hidden)] -/// Stream writer -pub trait Writer { - /// number of bytes written to the stream - fn written(&self) -> u64; - - #[doc(hidden)] - fn set_date(&mut self); - - #[doc(hidden)] - fn buffer(&mut self) -> &mut BytesMut; - - fn start( - &mut self, req: &Request, resp: &mut HttpResponse, encoding: ContentEncoding, - ) -> io::Result; - - fn write(&mut self, payload: &Binary) -> io::Result; - - fn write_eof(&mut self) -> io::Result; - - fn poll_completed(&mut self, shutdown: bool) -> Poll<(), io::Error>; -} - -#[doc(hidden)] -/// Low-level io stream operations -pub trait IoStream: AsyncRead + AsyncWrite + 'static { - fn shutdown(&mut self, how: Shutdown) -> io::Result<()>; - - /// Returns the socket address of the remote peer of this TCP connection. - fn peer_addr(&self) -> Option { - None - } - - /// Sets the value of the TCP_NODELAY option on this socket. - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()>; - - fn set_linger(&mut self, dur: Option) -> io::Result<()>; - - fn set_keepalive(&mut self, dur: Option) -> io::Result<()>; - - fn read_available(&mut self, buf: &mut BytesMut) -> Poll<(bool, bool), io::Error> { - let mut read_some = false; - loop { - if buf.remaining_mut() < LW_BUFFER_SIZE { - buf.reserve(HW_BUFFER_SIZE); - } - - let read = unsafe { self.read(buf.bytes_mut()) }; - match read { - Ok(n) => { - if n == 0 { - return Ok(Async::Ready((read_some, true))); - } else { - read_some = true; - unsafe { - buf.advance_mut(n); - } - } - } - Err(e) => { - return if e.kind() == io::ErrorKind::WouldBlock { - if read_some { - Ok(Async::Ready((read_some, false))) - } else { - Ok(Async::NotReady) - } - } else if e.kind() == io::ErrorKind::ConnectionReset && read_some { - Ok(Async::Ready((read_some, true))) - } else { - Err(e) - }; - } - } - } - } - - /// Extra io stream extensions - fn extensions(&self) -> Option> { - None - } -} - -#[cfg(all(unix, feature = "uds"))] -impl IoStream for ::tokio_uds::UnixStream { - #[inline] - fn shutdown(&mut self, how: Shutdown) -> io::Result<()> { - ::tokio_uds::UnixStream::shutdown(self, how) - } - - #[inline] - fn set_nodelay(&mut self, _nodelay: bool) -> io::Result<()> { - Ok(()) - } - - #[inline] - fn set_linger(&mut self, _dur: Option) -> io::Result<()> { - Ok(()) - } - - #[inline] - fn set_keepalive(&mut self, _dur: Option) -> io::Result<()> { - Ok(()) - } -} - -impl IoStream for TcpStream { - #[inline] - fn shutdown(&mut self, how: Shutdown) -> io::Result<()> { - TcpStream::shutdown(self, how) - } - - #[inline] - fn peer_addr(&self) -> Option { - TcpStream::peer_addr(self).ok() - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - TcpStream::set_nodelay(self, nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - TcpStream::set_linger(self, dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - TcpStream::set_keepalive(self, dur) - } -} diff --git a/src/server/output.rs b/src/server/output.rs deleted file mode 100644 index 4a86ffbb7..000000000 --- a/src/server/output.rs +++ /dev/null @@ -1,760 +0,0 @@ -use std::fmt::Write as FmtWrite; -use std::io::Write; -use std::str::FromStr; -use std::{cmp, fmt, io, mem}; - -#[cfg(feature = "brotli")] -use brotli2::write::BrotliEncoder; -use bytes::BytesMut; -#[cfg(feature = "flate2")] -use flate2::write::{GzEncoder, ZlibEncoder}; -#[cfg(feature = "flate2")] -use flate2::Compression; -use http::header::{ACCEPT_ENCODING, CONTENT_LENGTH}; -use http::{StatusCode, Version}; - -use super::message::InnerRequest; -use body::{Binary, Body}; -use header::ContentEncoding; -use httpresponse::HttpResponse; - -#[derive(Debug)] -pub(crate) enum ResponseLength { - Chunked, - Zero, - Length(usize), - Length64(u64), - None, -} - -#[derive(Debug)] -pub(crate) struct ResponseInfo { - head: bool, - pub length: ResponseLength, - pub content_encoding: Option<&'static str>, -} - -impl ResponseInfo { - pub fn new(head: bool) -> Self { - ResponseInfo { - head, - length: ResponseLength::None, - content_encoding: None, - } - } -} - -#[derive(Debug)] -pub(crate) enum Output { - Empty(BytesMut), - Buffer(BytesMut), - Encoder(ContentEncoder), - TE(TransferEncoding), - Done, -} - -impl Output { - pub fn take(&mut self) -> BytesMut { - match mem::replace(self, Output::Done) { - Output::Empty(bytes) => bytes, - Output::Buffer(bytes) => bytes, - Output::Encoder(mut enc) => enc.take_buf(), - Output::TE(mut te) => te.take(), - Output::Done => panic!(), - } - } - - pub fn take_option(&mut self) -> Option { - match mem::replace(self, Output::Done) { - Output::Empty(bytes) => Some(bytes), - Output::Buffer(bytes) => Some(bytes), - Output::Encoder(mut enc) => Some(enc.take_buf()), - Output::TE(mut te) => Some(te.take()), - Output::Done => None, - } - } - - pub fn as_ref(&mut self) -> &BytesMut { - match self { - Output::Empty(ref mut bytes) => bytes, - Output::Buffer(ref mut bytes) => bytes, - Output::Encoder(ref mut enc) => enc.buf_ref(), - Output::TE(ref mut te) => te.buf_ref(), - Output::Done => panic!(), - } - } - pub fn as_mut(&mut self) -> &mut BytesMut { - match self { - Output::Empty(ref mut bytes) => bytes, - Output::Buffer(ref mut bytes) => bytes, - Output::Encoder(ref mut enc) => enc.buf_mut(), - Output::TE(ref mut te) => te.buf_mut(), - Output::Done => panic!(), - } - } - pub fn split_to(&mut self, cap: usize) -> BytesMut { - match self { - Output::Empty(ref mut bytes) => bytes.split_to(cap), - Output::Buffer(ref mut bytes) => bytes.split_to(cap), - Output::Encoder(ref mut enc) => enc.buf_mut().split_to(cap), - Output::TE(ref mut te) => te.buf_mut().split_to(cap), - Output::Done => BytesMut::new(), - } - } - - pub fn len(&self) -> usize { - match self { - Output::Empty(ref bytes) => bytes.len(), - Output::Buffer(ref bytes) => bytes.len(), - Output::Encoder(ref enc) => enc.len(), - Output::TE(ref te) => te.len(), - Output::Done => 0, - } - } - - pub fn is_empty(&self) -> bool { - match self { - Output::Empty(ref bytes) => bytes.is_empty(), - Output::Buffer(ref bytes) => bytes.is_empty(), - Output::Encoder(ref enc) => enc.is_empty(), - Output::TE(ref te) => te.is_empty(), - Output::Done => true, - } - } - - pub fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { - match self { - Output::Buffer(ref mut bytes) => { - bytes.extend_from_slice(data); - Ok(()) - } - Output::Encoder(ref mut enc) => enc.write(data), - Output::TE(ref mut te) => te.encode(data).map(|_| ()), - Output::Empty(_) | Output::Done => Ok(()), - } - } - - pub fn write_eof(&mut self) -> Result { - match self { - Output::Buffer(_) => Ok(true), - Output::Encoder(ref mut enc) => enc.write_eof(), - Output::TE(ref mut te) => Ok(te.encode_eof()), - Output::Empty(_) | Output::Done => Ok(true), - } - } - - pub(crate) fn for_server( - &mut self, info: &mut ResponseInfo, req: &InnerRequest, resp: &mut HttpResponse, - response_encoding: ContentEncoding, - ) { - let buf = self.take(); - let version = resp.version().unwrap_or_else(|| req.version); - let mut len = 0; - - let has_body = match resp.body() { - Body::Empty => false, - Body::Binary(ref bin) => { - len = bin.len(); - !(response_encoding == ContentEncoding::Auto && len < 96) - } - _ => true, - }; - - // Enable content encoding only if response does not contain Content-Encoding - // header - #[cfg(any(feature = "brotli", feature = "flate2"))] - let mut encoding = if has_body { - let encoding = match response_encoding { - ContentEncoding::Auto => { - // negotiate content-encoding - if let Some(val) = req.headers.get(ACCEPT_ENCODING) { - if let Ok(enc) = val.to_str() { - AcceptEncoding::parse(enc) - } else { - ContentEncoding::Identity - } - } else { - ContentEncoding::Identity - } - } - encoding => encoding, - }; - if encoding.is_compression() { - info.content_encoding = Some(encoding.as_str()); - } - encoding - } else { - ContentEncoding::Identity - }; - #[cfg(not(any(feature = "brotli", feature = "flate2")))] - let mut encoding = ContentEncoding::Identity; - - let transfer = match resp.body() { - Body::Empty => { - info.length = match resp.status() { - StatusCode::NO_CONTENT - | StatusCode::CONTINUE - | StatusCode::SWITCHING_PROTOCOLS - | StatusCode::PROCESSING => ResponseLength::None, - _ => ResponseLength::Zero, - }; - *self = Output::Empty(buf); - return; - } - Body::Binary(_) => { - #[cfg(any(feature = "brotli", feature = "flate2"))] - { - if !(encoding == ContentEncoding::Identity - || encoding == ContentEncoding::Auto) - { - let mut tmp = BytesMut::new(); - let mut transfer = TransferEncoding::eof(tmp); - let mut enc = match encoding { - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => ContentEncoder::Deflate( - ZlibEncoder::new(transfer, Compression::fast()), - ), - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => ContentEncoder::Gzip( - GzEncoder::new(transfer, Compression::fast()), - ), - #[cfg(feature = "brotli")] - ContentEncoding::Br => { - ContentEncoder::Br(BrotliEncoder::new(transfer, 3)) - } - ContentEncoding::Identity | ContentEncoding::Auto => { - unreachable!() - } - }; - - let bin = resp.replace_body(Body::Empty).binary(); - - // TODO return error! - let _ = enc.write(bin.as_ref()); - let _ = enc.write_eof(); - let body = enc.buf_mut().take(); - len = body.len(); - resp.replace_body(Binary::from(body)); - } - } - - info.length = ResponseLength::Length(len); - if info.head { - *self = Output::Empty(buf); - } else { - *self = Output::Buffer(buf); - } - return; - } - Body::Streaming(_) | Body::Actor(_) => { - if resp.upgrade() { - if version == Version::HTTP_2 { - error!("Connection upgrade is forbidden for HTTP/2"); - } - if encoding != ContentEncoding::Identity { - encoding = ContentEncoding::Identity; - info.content_encoding.take(); - } - TransferEncoding::eof(buf) - } else { - if !(encoding == ContentEncoding::Identity - || encoding == ContentEncoding::Auto) - { - resp.headers_mut().remove(CONTENT_LENGTH); - } - Output::streaming_encoding(info, buf, version, resp) - } - } - }; - // check for head response - if info.head { - resp.set_body(Body::Empty); - *self = Output::Empty(transfer.buf.unwrap()); - return; - } - - let enc = match encoding { - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => { - ContentEncoder::Deflate(ZlibEncoder::new(transfer, Compression::fast())) - } - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => { - ContentEncoder::Gzip(GzEncoder::new(transfer, Compression::fast())) - } - #[cfg(feature = "brotli")] - ContentEncoding::Br => ContentEncoder::Br(BrotliEncoder::new(transfer, 3)), - ContentEncoding::Identity | ContentEncoding::Auto => { - *self = Output::TE(transfer); - return; - } - }; - *self = Output::Encoder(enc); - } - - fn streaming_encoding( - info: &mut ResponseInfo, buf: BytesMut, version: Version, - resp: &mut HttpResponse, - ) -> TransferEncoding { - match resp.chunked() { - Some(true) => { - // Enable transfer encoding - info.length = ResponseLength::Chunked; - if version == Version::HTTP_2 { - TransferEncoding::eof(buf) - } else { - TransferEncoding::chunked(buf) - } - } - Some(false) => TransferEncoding::eof(buf), - None => { - // if Content-Length is specified, then use it as length hint - let (len, chunked) = - if let Some(len) = resp.headers().get(CONTENT_LENGTH) { - // Content-Length - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - (Some(len), false) - } else { - error!("illegal Content-Length: {:?}", len); - (None, false) - } - } else { - error!("illegal Content-Length: {:?}", len); - (None, false) - } - } else { - (None, true) - }; - - if !chunked { - if let Some(len) = len { - info.length = ResponseLength::Length64(len); - TransferEncoding::length(len, buf) - } else { - TransferEncoding::eof(buf) - } - } else { - // Enable transfer encoding - info.length = ResponseLength::Chunked; - if version == Version::HTTP_2 { - TransferEncoding::eof(buf) - } else { - TransferEncoding::chunked(buf) - } - } - } - } - } -} - -pub(crate) enum ContentEncoder { - #[cfg(feature = "flate2")] - Deflate(ZlibEncoder), - #[cfg(feature = "flate2")] - Gzip(GzEncoder), - #[cfg(feature = "brotli")] - Br(BrotliEncoder), - Identity(TransferEncoding), -} - -impl fmt::Debug for ContentEncoder { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(_) => writeln!(f, "ContentEncoder(Brotli)"), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(_) => writeln!(f, "ContentEncoder(Deflate)"), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(_) => writeln!(f, "ContentEncoder(Gzip)"), - ContentEncoder::Identity(_) => writeln!(f, "ContentEncoder(Identity)"), - } - } -} - -impl ContentEncoder { - #[inline] - pub fn len(&self) -> usize { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref encoder) => encoder.get_ref().len(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref encoder) => encoder.get_ref().len(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref encoder) => encoder.get_ref().len(), - ContentEncoder::Identity(ref encoder) => encoder.len(), - } - } - - #[inline] - pub fn is_empty(&self) -> bool { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref encoder) => encoder.get_ref().is_empty(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref encoder) => encoder.get_ref().is_empty(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref encoder) => encoder.get_ref().is_empty(), - ContentEncoder::Identity(ref encoder) => encoder.is_empty(), - } - } - - #[inline] - pub(crate) fn take_buf(&mut self) -> BytesMut { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), - ContentEncoder::Identity(ref mut encoder) => encoder.take(), - } - } - - #[inline] - pub(crate) fn buf_mut(&mut self) -> &mut BytesMut { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref mut encoder) => encoder.get_mut().buf_mut(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().buf_mut(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().buf_mut(), - ContentEncoder::Identity(ref mut encoder) => encoder.buf_mut(), - } - } - - #[inline] - pub(crate) fn buf_ref(&mut self) -> &BytesMut { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref mut encoder) => encoder.get_mut().buf_ref(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().buf_ref(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().buf_ref(), - ContentEncoder::Identity(ref mut encoder) => encoder.buf_ref(), - } - } - - #[cfg_attr(feature = "cargo-clippy", allow(inline_always))] - #[inline(always)] - pub fn write_eof(&mut self) -> Result { - let encoder = - mem::replace(self, ContentEncoder::Identity(TransferEncoding::empty())); - - match encoder { - #[cfg(feature = "brotli")] - ContentEncoder::Br(encoder) => match encoder.finish() { - Ok(mut writer) => { - writer.encode_eof(); - *self = ContentEncoder::Identity(writer); - Ok(true) - } - Err(err) => Err(err), - }, - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(encoder) => match encoder.finish() { - Ok(mut writer) => { - writer.encode_eof(); - *self = ContentEncoder::Identity(writer); - Ok(true) - } - Err(err) => Err(err), - }, - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(encoder) => match encoder.finish() { - Ok(mut writer) => { - writer.encode_eof(); - *self = ContentEncoder::Identity(writer); - Ok(true) - } - Err(err) => Err(err), - }, - ContentEncoder::Identity(mut writer) => { - let res = writer.encode_eof(); - *self = ContentEncoder::Identity(writer); - Ok(res) - } - } - } - - #[cfg_attr(feature = "cargo-clippy", allow(inline_always))] - #[inline(always)] - pub fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { - Ok(_) => Ok(()), - Err(err) => { - trace!("Error decoding br encoding: {}", err); - Err(err) - } - }, - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { - Ok(_) => Ok(()), - Err(err) => { - trace!("Error decoding gzip encoding: {}", err); - Err(err) - } - }, - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { - Ok(_) => Ok(()), - Err(err) => { - trace!("Error decoding deflate encoding: {}", err); - Err(err) - } - }, - ContentEncoder::Identity(ref mut encoder) => { - encoder.encode(data)?; - Ok(()) - } - } - } -} - -/// Encoders to handle different Transfer-Encodings. -#[derive(Debug)] -pub(crate) struct TransferEncoding { - buf: Option, - kind: TransferEncodingKind, -} - -#[derive(Debug, PartialEq, Clone)] -enum TransferEncodingKind { - /// An Encoder for when Transfer-Encoding includes `chunked`. - Chunked(bool), - /// An Encoder for when Content-Length is set. - /// - /// Enforces that the body is not longer than the Content-Length header. - Length(u64), - /// An Encoder for when Content-Length is not known. - /// - /// Application decides when to stop writing. - Eof, -} - -impl TransferEncoding { - fn take(&mut self) -> BytesMut { - self.buf.take().unwrap() - } - - fn buf_ref(&mut self) -> &BytesMut { - self.buf.as_ref().unwrap() - } - - fn len(&self) -> usize { - self.buf.as_ref().unwrap().len() - } - - fn is_empty(&self) -> bool { - self.buf.as_ref().unwrap().is_empty() - } - - fn buf_mut(&mut self) -> &mut BytesMut { - self.buf.as_mut().unwrap() - } - - #[inline] - pub fn empty() -> TransferEncoding { - TransferEncoding { - buf: None, - kind: TransferEncodingKind::Eof, - } - } - - #[inline] - pub fn eof(buf: BytesMut) -> TransferEncoding { - TransferEncoding { - buf: Some(buf), - kind: TransferEncodingKind::Eof, - } - } - - #[inline] - pub fn chunked(buf: BytesMut) -> TransferEncoding { - TransferEncoding { - buf: Some(buf), - kind: TransferEncodingKind::Chunked(false), - } - } - - #[inline] - pub fn length(len: u64, buf: BytesMut) -> TransferEncoding { - TransferEncoding { - buf: Some(buf), - kind: TransferEncodingKind::Length(len), - } - } - - /// Encode message. Return `EOF` state of encoder - #[inline] - pub fn encode(&mut self, msg: &[u8]) -> io::Result { - match self.kind { - TransferEncodingKind::Eof => { - let eof = msg.is_empty(); - self.buf.as_mut().unwrap().extend_from_slice(msg); - Ok(eof) - } - TransferEncodingKind::Chunked(ref mut eof) => { - if *eof { - return Ok(true); - } - - if msg.is_empty() { - *eof = true; - self.buf.as_mut().unwrap().extend_from_slice(b"0\r\n\r\n"); - } else { - let mut buf = BytesMut::new(); - writeln!(&mut buf, "{:X}\r", msg.len()) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - - let b = self.buf.as_mut().unwrap(); - b.reserve(buf.len() + msg.len() + 2); - b.extend_from_slice(buf.as_ref()); - b.extend_from_slice(msg); - b.extend_from_slice(b"\r\n"); - } - Ok(*eof) - } - TransferEncodingKind::Length(ref mut remaining) => { - if *remaining > 0 { - if msg.is_empty() { - return Ok(*remaining == 0); - } - let len = cmp::min(*remaining, msg.len() as u64); - - self.buf - .as_mut() - .unwrap() - .extend_from_slice(&msg[..len as usize]); - - *remaining -= len as u64; - Ok(*remaining == 0) - } else { - Ok(true) - } - } - } - } - - /// Encode eof. Return `EOF` state of encoder - #[inline] - pub fn encode_eof(&mut self) -> bool { - match self.kind { - TransferEncodingKind::Eof => true, - TransferEncodingKind::Length(rem) => rem == 0, - TransferEncodingKind::Chunked(ref mut eof) => { - if !*eof { - *eof = true; - self.buf.as_mut().unwrap().extend_from_slice(b"0\r\n\r\n"); - } - true - } - } - } -} - -impl io::Write for TransferEncoding { - #[inline] - fn write(&mut self, buf: &[u8]) -> io::Result { - if self.buf.is_some() { - self.encode(buf)?; - } - Ok(buf.len()) - } - - #[inline] - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -struct AcceptEncoding { - encoding: ContentEncoding, - quality: f64, -} - -impl Eq for AcceptEncoding {} - -impl Ord for AcceptEncoding { - fn cmp(&self, other: &AcceptEncoding) -> cmp::Ordering { - if self.quality > other.quality { - cmp::Ordering::Less - } else if self.quality < other.quality { - cmp::Ordering::Greater - } else { - cmp::Ordering::Equal - } - } -} - -impl PartialOrd for AcceptEncoding { - fn partial_cmp(&self, other: &AcceptEncoding) -> Option { - Some(self.cmp(other)) - } -} - -impl PartialEq for AcceptEncoding { - fn eq(&self, other: &AcceptEncoding) -> bool { - self.quality == other.quality - } -} - -impl AcceptEncoding { - fn new(tag: &str) -> Option { - let parts: Vec<&str> = tag.split(';').collect(); - let encoding = match parts.len() { - 0 => return None, - _ => ContentEncoding::from(parts[0]), - }; - let quality = match parts.len() { - 1 => encoding.quality(), - _ => match f64::from_str(parts[1]) { - Ok(q) => q, - Err(_) => 0.0, - }, - }; - Some(AcceptEncoding { encoding, quality }) - } - - /// Parse a raw Accept-Encoding header value into an ordered list. - pub fn parse(raw: &str) -> ContentEncoding { - let mut encodings: Vec<_> = raw - .replace(' ', "") - .split(',') - .map(|l| AcceptEncoding::new(l)) - .collect(); - encodings.sort(); - - for enc in encodings { - if let Some(enc) = enc { - return enc.encoding; - } - } - ContentEncoding::Identity - } -} - -#[cfg(test)] -mod tests { - use super::*; - use bytes::Bytes; - - #[test] - fn test_chunked_te() { - let bytes = BytesMut::new(); - let mut enc = TransferEncoding::chunked(bytes); - { - assert!(!enc.encode(b"test").ok().unwrap()); - assert!(enc.encode(b"").ok().unwrap()); - } - assert_eq!( - enc.buf_mut().take().freeze(), - Bytes::from_static(b"4\r\ntest\r\n0\r\n\r\n") - ); - } -} diff --git a/src/server/service.rs b/src/server/service.rs deleted file mode 100644 index e3402e305..000000000 --- a/src/server/service.rs +++ /dev/null @@ -1,272 +0,0 @@ -use std::marker::PhantomData; -use std::time::Duration; - -use actix_net::service::{NewService, Service}; -use futures::future::{ok, FutureResult}; -use futures::{Async, Poll}; - -use super::channel::{H1Channel, HttpChannel}; -use super::error::HttpDispatchError; -use super::handler::HttpHandler; -use super::settings::ServiceConfig; -use super::IoStream; - -/// `NewService` implementation for HTTP1/HTTP2 transports -pub struct HttpService -where - H: HttpHandler, - Io: IoStream, -{ - settings: ServiceConfig, - _t: PhantomData, -} - -impl HttpService -where - H: HttpHandler, - Io: IoStream, -{ - /// Create new `HttpService` instance. - pub fn new(settings: ServiceConfig) -> Self { - HttpService { - settings, - _t: PhantomData, - } - } -} - -impl NewService for HttpService -where - H: HttpHandler, - Io: IoStream, -{ - type Request = Io; - type Response = (); - type Error = HttpDispatchError; - type InitError = (); - type Service = HttpServiceHandler; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(HttpServiceHandler::new(self.settings.clone())) - } -} - -pub struct HttpServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - settings: ServiceConfig, - _t: PhantomData, -} - -impl HttpServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - fn new(settings: ServiceConfig) -> HttpServiceHandler { - HttpServiceHandler { - settings, - _t: PhantomData, - } - } -} - -impl Service for HttpServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - type Request = Io; - type Response = (); - type Error = HttpDispatchError; - type Future = HttpChannel; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - HttpChannel::new(self.settings.clone(), req) - } -} - -/// `NewService` implementation for HTTP1 transport -pub struct H1Service -where - H: HttpHandler, - Io: IoStream, -{ - settings: ServiceConfig, - _t: PhantomData, -} - -impl H1Service -where - H: HttpHandler, - Io: IoStream, -{ - /// Create new `HttpService` instance. - pub fn new(settings: ServiceConfig) -> Self { - H1Service { - settings, - _t: PhantomData, - } - } -} - -impl NewService for H1Service -where - H: HttpHandler, - Io: IoStream, -{ - type Request = Io; - type Response = (); - type Error = HttpDispatchError; - type InitError = (); - type Service = H1ServiceHandler; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(H1ServiceHandler::new(self.settings.clone())) - } -} - -/// `Service` implementation for HTTP1 transport -pub struct H1ServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - settings: ServiceConfig, - _t: PhantomData, -} - -impl H1ServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - fn new(settings: ServiceConfig) -> H1ServiceHandler { - H1ServiceHandler { - settings, - _t: PhantomData, - } - } -} - -impl Service for H1ServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - type Request = Io; - type Response = (); - type Error = HttpDispatchError; - type Future = H1Channel; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - H1Channel::new(self.settings.clone(), req) - } -} - -/// `NewService` implementation for stream configuration service -/// -/// Stream configuration service allows to change some socket level -/// parameters. for example `tcp nodelay` or `tcp keep-alive`. -pub struct StreamConfiguration { - no_delay: Option, - tcp_ka: Option>, - _t: PhantomData<(T, E)>, -} - -impl Default for StreamConfiguration { - fn default() -> Self { - Self::new() - } -} - -impl StreamConfiguration { - /// Create new `StreamConfigurationService` instance. - pub fn new() -> Self { - Self { - no_delay: None, - tcp_ka: None, - _t: PhantomData, - } - } - - /// Sets the value of the `TCP_NODELAY` option on this socket. - pub fn nodelay(mut self, nodelay: bool) -> Self { - self.no_delay = Some(nodelay); - self - } - - /// Sets whether keepalive messages are enabled to be sent on this socket. - pub fn tcp_keepalive(mut self, keepalive: Option) -> Self { - self.tcp_ka = Some(keepalive); - self - } -} - -impl NewService for StreamConfiguration { - type Request = T; - type Response = T; - type Error = E; - type InitError = (); - type Service = StreamConfigurationService; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(StreamConfigurationService { - no_delay: self.no_delay, - tcp_ka: self.tcp_ka, - _t: PhantomData, - }) - } -} - -/// Stream configuration service -/// -/// Stream configuration service allows to change some socket level -/// parameters. for example `tcp nodelay` or `tcp keep-alive`. -pub struct StreamConfigurationService { - no_delay: Option, - tcp_ka: Option>, - _t: PhantomData<(T, E)>, -} - -impl Service for StreamConfigurationService -where - T: IoStream, -{ - type Request = T; - type Response = T; - type Error = E; - type Future = FutureResult; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, mut req: Self::Request) -> Self::Future { - if let Some(no_delay) = self.no_delay { - if req.set_nodelay(no_delay).is_err() { - error!("Can not set socket no-delay option"); - } - } - if let Some(keepalive) = self.tcp_ka { - if req.set_keepalive(keepalive).is_err() { - error!("Can not set socket keep-alive option"); - } - } - - ok(req) - } -} diff --git a/src/server/settings.rs b/src/server/settings.rs deleted file mode 100644 index 66a4eed88..000000000 --- a/src/server/settings.rs +++ /dev/null @@ -1,503 +0,0 @@ -use std::cell::{Cell, RefCell}; -use std::collections::VecDeque; -use std::fmt::Write; -use std::rc::Rc; -use std::time::{Duration, Instant}; -use std::{env, fmt, net}; - -use bytes::BytesMut; -use futures::{future, Future}; -use futures_cpupool::CpuPool; -use http::StatusCode; -use lazycell::LazyCell; -use parking_lot::Mutex; -use time; -use tokio_current_thread::spawn; -use tokio_timer::{sleep, Delay}; - -use super::message::{Request, RequestPool}; -use super::KeepAlive; -use body::Body; -use httpresponse::{HttpResponse, HttpResponseBuilder, HttpResponsePool}; - -/// Env variable for default cpu pool size -const ENV_CPU_POOL_VAR: &str = "ACTIX_CPU_POOL"; - -lazy_static! { - pub(crate) static ref DEFAULT_CPUPOOL: Mutex = { - let default = match env::var(ENV_CPU_POOL_VAR) { - Ok(val) => { - if let Ok(val) = val.parse() { - val - } else { - error!("Can not parse ACTIX_CPU_POOL value"); - 20 - } - } - Err(_) => 20, - }; - Mutex::new(CpuPool::new(default)) - }; -} - -/// Various server settings -pub struct ServerSettings { - addr: net::SocketAddr, - secure: bool, - host: String, - cpu_pool: LazyCell, - responses: &'static HttpResponsePool, -} - -impl Clone for ServerSettings { - fn clone(&self) -> Self { - ServerSettings { - addr: self.addr, - secure: self.secure, - host: self.host.clone(), - cpu_pool: LazyCell::new(), - responses: HttpResponsePool::get_pool(), - } - } -} - -impl Default for ServerSettings { - fn default() -> Self { - ServerSettings { - addr: "127.0.0.1:8080".parse().unwrap(), - secure: false, - host: "localhost:8080".to_owned(), - responses: HttpResponsePool::get_pool(), - cpu_pool: LazyCell::new(), - } - } -} - -impl ServerSettings { - /// Crate server settings instance - pub(crate) fn new( - addr: net::SocketAddr, host: &str, secure: bool, - ) -> ServerSettings { - let host = host.to_owned(); - let cpu_pool = LazyCell::new(); - let responses = HttpResponsePool::get_pool(); - ServerSettings { - addr, - secure, - host, - cpu_pool, - responses, - } - } - - /// Returns the socket address of the local half of this TCP connection - pub fn local_addr(&self) -> net::SocketAddr { - self.addr - } - - /// Returns true if connection is secure(https) - pub fn secure(&self) -> bool { - self.secure - } - - /// Returns host header value - pub fn host(&self) -> &str { - &self.host - } - - /// Returns default `CpuPool` for server - pub fn cpu_pool(&self) -> &CpuPool { - self.cpu_pool.borrow_with(|| DEFAULT_CPUPOOL.lock().clone()) - } - - #[inline] - pub(crate) fn get_response(&self, status: StatusCode, body: Body) -> HttpResponse { - HttpResponsePool::get_response(&self.responses, status, body) - } - - #[inline] - pub(crate) fn get_response_builder( - &self, status: StatusCode, - ) -> HttpResponseBuilder { - HttpResponsePool::get_builder(&self.responses, status) - } -} - -// "Sun, 06 Nov 1994 08:49:37 GMT".len() -const DATE_VALUE_LENGTH: usize = 29; - -/// Http service configuration -pub struct ServiceConfig(Rc>); - -struct Inner { - handler: H, - keep_alive: Option, - client_timeout: u64, - client_shutdown: u64, - ka_enabled: bool, - bytes: Rc, - messages: &'static RequestPool, - date: Cell>, -} - -impl Clone for ServiceConfig { - fn clone(&self) -> Self { - ServiceConfig(self.0.clone()) - } -} - -impl ServiceConfig { - /// Create instance of `ServiceConfig` - pub(crate) fn new( - handler: H, keep_alive: KeepAlive, client_timeout: u64, client_shutdown: u64, - settings: ServerSettings, - ) -> ServiceConfig { - let (keep_alive, ka_enabled) = match keep_alive { - KeepAlive::Timeout(val) => (val as u64, true), - KeepAlive::Os | KeepAlive::Tcp(_) => (0, true), - KeepAlive::Disabled => (0, false), - }; - let keep_alive = if ka_enabled && keep_alive > 0 { - Some(Duration::from_secs(keep_alive)) - } else { - None - }; - - ServiceConfig(Rc::new(Inner { - handler, - keep_alive, - ka_enabled, - client_timeout, - client_shutdown, - bytes: Rc::new(SharedBytesPool::new()), - messages: RequestPool::pool(settings), - date: Cell::new(None), - })) - } - - /// Create worker settings builder. - pub fn build(handler: H) -> ServiceConfigBuilder { - ServiceConfigBuilder::new(handler) - } - - pub(crate) fn handler(&self) -> &H { - &self.0.handler - } - - #[inline] - /// Keep alive duration if configured. - pub fn keep_alive(&self) -> Option { - self.0.keep_alive - } - - #[inline] - /// Return state of connection keep-alive funcitonality - pub fn keep_alive_enabled(&self) -> bool { - self.0.ka_enabled - } - - pub(crate) fn get_bytes(&self) -> BytesMut { - self.0.bytes.get_bytes() - } - - pub(crate) fn release_bytes(&self, bytes: BytesMut) { - self.0.bytes.release_bytes(bytes) - } - - pub(crate) fn get_request(&self) -> Request { - RequestPool::get(self.0.messages) - } -} - -impl ServiceConfig { - #[inline] - /// Client timeout for first request. - pub fn client_timer(&self) -> Option { - let delay = self.0.client_timeout; - if delay != 0 { - Some(Delay::new(self.now() + Duration::from_millis(delay))) - } else { - None - } - } - - /// Client timeout for first request. - pub fn client_timer_expire(&self) -> Option { - let delay = self.0.client_timeout; - if delay != 0 { - Some(self.now() + Duration::from_millis(delay)) - } else { - None - } - } - - /// Client shutdown timer - pub fn client_shutdown_timer(&self) -> Option { - let delay = self.0.client_shutdown; - if delay != 0 { - Some(self.now() + Duration::from_millis(delay)) - } else { - None - } - } - - #[inline] - /// Return keep-alive timer delay is configured. - pub fn keep_alive_timer(&self) -> Option { - if let Some(ka) = self.0.keep_alive { - Some(Delay::new(self.now() + ka)) - } else { - None - } - } - - /// Keep-alive expire time - pub fn keep_alive_expire(&self) -> Option { - if let Some(ka) = self.0.keep_alive { - Some(self.now() + ka) - } else { - None - } - } - - fn check_date(&self) { - if unsafe { &*self.0.date.as_ptr() }.is_none() { - self.0.date.set(Some(Date::new())); - - // periodic date update - let s = self.clone(); - spawn(sleep(Duration::from_millis(500)).then(move |_| { - s.0.date.set(None); - future::ok(()) - })); - } - } - - pub(crate) fn set_date(&self, dst: &mut BytesMut, full: bool) { - self.check_date(); - - let date = &unsafe { &*self.0.date.as_ptr() }.as_ref().unwrap().bytes; - - if full { - let mut buf: [u8; 39] = [0; 39]; - buf[..6].copy_from_slice(b"date: "); - buf[6..35].copy_from_slice(date); - buf[35..].copy_from_slice(b"\r\n\r\n"); - dst.extend_from_slice(&buf); - } else { - dst.extend_from_slice(date); - } - } - - #[inline] - pub(crate) fn now(&self) -> Instant { - self.check_date(); - unsafe { &*self.0.date.as_ptr() }.as_ref().unwrap().current - } -} - -/// A service config builder -/// -/// This type can be used to construct an instance of `ServiceConfig` through a -/// builder-like pattern. -pub struct ServiceConfigBuilder { - handler: H, - keep_alive: KeepAlive, - client_timeout: u64, - client_shutdown: u64, - host: String, - addr: net::SocketAddr, - secure: bool, -} - -impl ServiceConfigBuilder { - /// Create instance of `ServiceConfigBuilder` - pub fn new(handler: H) -> ServiceConfigBuilder { - ServiceConfigBuilder { - handler, - keep_alive: KeepAlive::Timeout(5), - client_timeout: 5000, - client_shutdown: 5000, - secure: false, - host: "localhost".to_owned(), - addr: "127.0.0.1:8080".parse().unwrap(), - } - } - - /// Enable secure flag for current server. - /// - /// By default this flag is set to false. - pub fn secure(mut self) -> Self { - self.secure = true; - self - } - - /// Set server keep-alive setting. - /// - /// By default keep alive is set to a 5 seconds. - pub fn keep_alive>(mut self, val: T) -> Self { - self.keep_alive = val.into(); - self - } - - /// Set server client timeout in milliseconds for first request. - /// - /// Defines a timeout for reading client request header. If a client does not transmit - /// the entire set headers within this time, the request is terminated with - /// the 408 (Request Time-out) error. - /// - /// To disable timeout set value to 0. - /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_timeout(mut self, val: u64) -> Self { - self.client_timeout = val; - self - } - - /// Set server connection shutdown timeout in milliseconds. - /// - /// Defines a timeout for shutdown connection. If a shutdown procedure does not complete - /// within this time, the request is dropped. This timeout affects only secure connections. - /// - /// To disable timeout set value to 0. - /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_shutdown(mut self, val: u64) -> Self { - self.client_shutdown = val; - self - } - - /// Set server host name. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - /// - /// By default host name is set to a "localhost" value. - pub fn server_hostname(mut self, val: &str) -> Self { - self.host = val.to_owned(); - self - } - - /// Set server ip address. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - /// - /// By default server address is set to a "127.0.0.1:8080" - pub fn server_address(mut self, addr: S) -> Self { - match addr.to_socket_addrs() { - Err(err) => error!("Can not convert to SocketAddr: {}", err), - Ok(mut addrs) => if let Some(addr) = addrs.next() { - self.addr = addr; - }, - } - self - } - - /// Finish service configuration and create `ServiceConfig` object. - pub fn finish(self) -> ServiceConfig { - let settings = ServerSettings::new(self.addr, &self.host, self.secure); - let client_shutdown = if self.secure { self.client_shutdown } else { 0 }; - - ServiceConfig::new( - self.handler, - self.keep_alive, - self.client_timeout, - client_shutdown, - settings, - ) - } -} - -#[derive(Copy, Clone)] -struct Date { - current: Instant, - bytes: [u8; DATE_VALUE_LENGTH], - pos: usize, -} - -impl Date { - fn new() -> Date { - let mut date = Date { - current: Instant::now(), - bytes: [0; DATE_VALUE_LENGTH], - pos: 0, - }; - date.update(); - date - } - fn update(&mut self) { - self.pos = 0; - self.current = Instant::now(); - write!(self, "{}", time::at_utc(time::get_time()).rfc822()).unwrap(); - } -} - -impl fmt::Write for Date { - fn write_str(&mut self, s: &str) -> fmt::Result { - let len = s.len(); - self.bytes[self.pos..self.pos + len].copy_from_slice(s.as_bytes()); - self.pos += len; - Ok(()) - } -} - -#[derive(Debug)] -pub(crate) struct SharedBytesPool(RefCell>); - -impl SharedBytesPool { - pub fn new() -> SharedBytesPool { - SharedBytesPool(RefCell::new(VecDeque::with_capacity(128))) - } - - pub fn get_bytes(&self) -> BytesMut { - if let Some(bytes) = self.0.borrow_mut().pop_front() { - bytes - } else { - BytesMut::new() - } - } - - pub fn release_bytes(&self, mut bytes: BytesMut) { - let v = &mut self.0.borrow_mut(); - if v.len() < 128 { - bytes.clear(); - v.push_front(bytes); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use futures::future; - use tokio::runtime::current_thread; - - #[test] - fn test_date_len() { - assert_eq!(DATE_VALUE_LENGTH, "Sun, 06 Nov 1994 08:49:37 GMT".len()); - } - - #[test] - fn test_date() { - let mut rt = current_thread::Runtime::new().unwrap(); - - let _ = rt.block_on(future::lazy(|| { - let settings = ServiceConfig::<()>::new( - (), - KeepAlive::Os, - 0, - 0, - ServerSettings::default(), - ); - let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); - settings.set_date(&mut buf1, true); - let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); - settings.set_date(&mut buf2, true); - assert_eq!(buf1, buf2); - future::ok::<_, ()>(()) - })); - } -} diff --git a/src/server/ssl/mod.rs b/src/server/ssl/mod.rs deleted file mode 100644 index c09573fe3..000000000 --- a/src/server/ssl/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[cfg(any(feature = "alpn", feature = "ssl"))] -mod openssl; -#[cfg(any(feature = "alpn", feature = "ssl"))] -pub use self::openssl::{openssl_acceptor_with_flags, OpensslAcceptor}; - -#[cfg(feature = "tls")] -mod nativetls; - -#[cfg(feature = "rust-tls")] -mod rustls; -#[cfg(feature = "rust-tls")] -pub use self::rustls::RustlsAcceptor; diff --git a/src/server/ssl/nativetls.rs b/src/server/ssl/nativetls.rs deleted file mode 100644 index a9797ffb3..000000000 --- a/src/server/ssl/nativetls.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::net::{Shutdown, SocketAddr}; -use std::{io, time}; - -use actix_net::ssl::TlsStream; - -use server::IoStream; - -impl IoStream for TlsStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = self.get_mut().shutdown(); - Ok(()) - } - - #[inline] - fn peer_addr(&self) -> Option { - self.get_ref().get_ref().peer_addr() - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().get_mut().set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_keepalive(dur) - } -} diff --git a/src/server/ssl/openssl.rs b/src/server/ssl/openssl.rs deleted file mode 100644 index 9d370f8be..000000000 --- a/src/server/ssl/openssl.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::net::{Shutdown, SocketAddr}; -use std::{io, time}; - -use actix_net::ssl; -use openssl::ssl::{AlpnError, SslAcceptor, SslAcceptorBuilder}; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_openssl::SslStream; - -use server::{IoStream, ServerFlags}; - -/// Support `SSL` connections via openssl package -/// -/// `ssl` feature enables `OpensslAcceptor` type -pub struct OpensslAcceptor { - _t: ssl::OpensslAcceptor, -} - -impl OpensslAcceptor { - /// Create `OpensslAcceptor` with enabled `HTTP/2` and `HTTP1.1` support. - pub fn new(builder: SslAcceptorBuilder) -> io::Result> { - OpensslAcceptor::with_flags(builder, ServerFlags::HTTP1 | ServerFlags::HTTP2) - } - - /// Create `OpensslAcceptor` with custom server flags. - pub fn with_flags( - builder: SslAcceptorBuilder, flags: ServerFlags, - ) -> io::Result> { - let acceptor = openssl_acceptor_with_flags(builder, flags)?; - - Ok(ssl::OpensslAcceptor::new(acceptor)) - } -} - -/// Configure `SslAcceptorBuilder` with custom server flags. -pub fn openssl_acceptor_with_flags( - mut builder: SslAcceptorBuilder, flags: ServerFlags, -) -> io::Result { - let mut protos = Vec::new(); - if flags.contains(ServerFlags::HTTP1) { - protos.extend(b"\x08http/1.1"); - } - if flags.contains(ServerFlags::HTTP2) { - protos.extend(b"\x02h2"); - builder.set_alpn_select_callback(|_, protos| { - const H2: &[u8] = b"\x02h2"; - if protos.windows(3).any(|window| window == H2) { - Ok(b"h2") - } else { - Err(AlpnError::NOACK) - } - }); - } - - if !protos.is_empty() { - builder.set_alpn_protos(&protos)?; - } - - Ok(builder.build()) -} - -impl IoStream for SslStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = self.get_mut().shutdown(); - Ok(()) - } - - #[inline] - fn peer_addr(&self) -> Option { - self.get_ref().get_ref().peer_addr() - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().get_mut().set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_keepalive(dur) - } -} diff --git a/src/server/ssl/rustls.rs b/src/server/ssl/rustls.rs deleted file mode 100644 index a53a53a98..000000000 --- a/src/server/ssl/rustls.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::net::{Shutdown, SocketAddr}; -use std::{io, time}; - -use actix_net::ssl; //::RustlsAcceptor; -use rustls::{ClientSession, ServerConfig, ServerSession}; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_rustls::TlsStream; - -use server::{IoStream, ServerFlags}; - -/// Support `SSL` connections via rustls package -/// -/// `rust-tls` feature enables `RustlsAcceptor` type -pub struct RustlsAcceptor { - _t: ssl::RustlsAcceptor, -} - -impl RustlsAcceptor { - /// Create `RustlsAcceptor` with custom server flags. - pub fn with_flags( - mut config: ServerConfig, flags: ServerFlags, - ) -> ssl::RustlsAcceptor { - let mut protos = Vec::new(); - if flags.contains(ServerFlags::HTTP2) { - protos.push("h2".to_string()); - } - if flags.contains(ServerFlags::HTTP1) { - protos.push("http/1.1".to_string()); - } - if !protos.is_empty() { - config.set_protocols(&protos); - } - - ssl::RustlsAcceptor::new(config) - } -} - -impl IoStream for TlsStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = ::shutdown(self); - Ok(()) - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().0.set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().0.set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().0.set_keepalive(dur) - } -} - -impl IoStream for TlsStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = ::shutdown(self); - Ok(()) - } - - #[inline] - fn peer_addr(&self) -> Option { - self.get_ref().0.peer_addr() - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().0.set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().0.set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().0.set_keepalive(dur) - } -} diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 000000000..775bb6113 --- /dev/null +++ b/src/service.rs @@ -0,0 +1,155 @@ +use std::rc::Rc; + +use actix_http::body::{Body, MessageBody, ResponseBody}; +use actix_http::http::HeaderMap; +use actix_http::{ + Error, Extensions, HttpMessage, Payload, Request, Response, ResponseHead, +}; +use actix_router::{Path, Url}; + +use crate::request::HttpRequest; + +pub struct ServiceRequest

    { + req: HttpRequest, + payload: Payload

    , +} + +impl

    ServiceRequest

    { + pub(crate) fn new( + path: Path, + request: Request

    , + extensions: Rc, + ) -> Self { + let (head, payload) = request.into_parts(); + ServiceRequest { + payload, + req: HttpRequest::new(head, path, extensions), + } + } + + #[inline] + pub fn request(&self) -> &HttpRequest { + &self.req + } + + #[inline] + pub fn into_request(self) -> HttpRequest { + self.req + } + + /// Create service response + #[inline] + pub fn into_response(self, res: Response) -> ServiceResponse { + ServiceResponse::new(self.req, res) + } + + /// Create service response for error + #[inline] + pub fn error_response>(self, err: E) -> ServiceResponse { + ServiceResponse::new(self.req, err.into().into()) + } + + #[inline] + pub fn match_info_mut(&mut self) -> &mut Path { + &mut self.req.path + } +} + +impl

    HttpMessage for ServiceRequest

    { + type Stream = P; + + #[inline] + fn headers(&self) -> &HeaderMap { + self.req.headers() + } + + #[inline] + fn take_payload(&mut self) -> Payload { + std::mem::replace(&mut self.payload, Payload::None) + } +} + +impl

    std::ops::Deref for ServiceRequest

    { + type Target = HttpRequest; + + fn deref(&self) -> &HttpRequest { + self.request() + } +} + +pub struct ServiceResponse { + request: HttpRequest, + response: Response, +} + +impl ServiceResponse { + /// Create service response instance + pub fn new(request: HttpRequest, response: Response) -> Self { + ServiceResponse { request, response } + } + + /// Get reference to original request + #[inline] + pub fn request(&self) -> &HttpRequest { + &self.request + } + + /// Get reference to response + #[inline] + pub fn response(&self) -> &Response { + &self.response + } + + /// Get mutable reference to response + #[inline] + pub fn response_mut(&mut self) -> &mut Response { + &mut self.response + } + + /// Get the headers from the response + #[inline] + pub fn headers(&self) -> &HeaderMap { + self.response.headers() + } + + /// Get a mutable reference to the headers + #[inline] + pub fn headers_mut(&mut self) -> &mut HeaderMap { + self.response.headers_mut() + } +} + +impl ServiceResponse { + /// Set a new body + pub fn map_body(self, f: F) -> ServiceResponse + where + F: FnOnce(&mut ResponseHead, ResponseBody) -> ResponseBody, + { + let response = self.response.map_body(f); + + ServiceResponse { + response, + request: self.request, + } + } +} + +impl std::ops::Deref for ServiceResponse { + type Target = Response; + + fn deref(&self) -> &Response { + self.response() + } +} + +impl std::ops::DerefMut for ServiceResponse { + fn deref_mut(&mut self) -> &mut Response { + self.response_mut() + } +} + +impl Into> for ServiceResponse { + fn into(self) -> Response { + self.response + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 000000000..82aecc6e1 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,120 @@ +use std::ops::Deref; +use std::rc::Rc; + +use actix_http::error::{Error, ErrorInternalServerError}; +use actix_http::Extensions; +use futures::future::{err, ok, FutureResult}; +use futures::{Async, Future, IntoFuture, Poll}; + +use crate::handler::FromRequest; +use crate::service::ServiceRequest; + +/// Application state factory +pub(crate) trait StateFactory { + fn construct(&self) -> Box; +} + +pub(crate) trait StateFactoryResult { + fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()>; +} + +/// Application state +pub struct State(Rc); + +impl State { + pub fn new(state: S) -> State { + State(Rc::new(state)) + } + + pub fn get_ref(&self) -> &S { + self.0.as_ref() + } +} + +impl Deref for State { + type Target = S; + + fn deref(&self) -> &S { + self.0.as_ref() + } +} + +impl Clone for State { + fn clone(&self) -> State { + State(self.0.clone()) + } +} + +impl FromRequest

    for State { + type Error = Error; + type Future = FutureResult; + + #[inline] + fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + if let Some(st) = req.app_extensions().get::>() { + ok(st.clone()) + } else { + err(ErrorInternalServerError( + "State is not configured, use App::state()", + )) + } + } +} + +impl StateFactory for State { + fn construct(&self) -> Box { + Box::new(StateFut { st: self.clone() }) + } +} + +struct StateFut { + st: State, +} + +impl StateFactoryResult for StateFut { + fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()> { + extensions.insert(self.st.clone()); + Ok(Async::Ready(())) + } +} + +impl StateFactory for F +where + F: Fn() -> Out + 'static, + Out: IntoFuture + 'static, + Out::Error: std::fmt::Debug, +{ + fn construct(&self) -> Box { + Box::new(StateFactoryFut { + fut: (*self)().into_future(), + }) + } +} + +struct StateFactoryFut +where + F: Future, + F::Error: std::fmt::Debug, +{ + fut: F, +} + +impl StateFactoryResult for StateFactoryFut +where + F: Future, + F::Error: std::fmt::Debug, +{ + fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()> { + match self.fut.poll() { + Ok(Async::Ready(s)) => { + extensions.insert(State::new(s)); + Ok(Async::Ready(())) + } + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(e) => { + log::error!("Can not construct application state: {:?}", e); + Err(()) + } + } + } +} diff --git a/src/test.rs b/src/test.rs index 1d86db9ff..e13dcd16c 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,449 +1,23 @@ //! Various helpers for Actix applications to use during testing. -use std::rc::Rc; use std::str::FromStr; -use std::sync::mpsc; -use std::{net, thread}; -use actix::{Actor, Addr, System}; -use actix::actors::signal; +use bytes::Bytes; +use futures::IntoFuture; +use tokio_current_thread::Runtime; -use actix_net::server::Server; -use cookie::Cookie; -use futures::Future; -use http::header::HeaderName; -use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; -use net2::TcpBuilder; -use tokio::runtime::current_thread::Runtime; +use actix_http::dev::Payload; +use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; +use actix_http::http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; +use actix_http::Request as HttpRequest; +use actix_router::{Path, Url}; -#[cfg(any(feature = "alpn", feature = "ssl"))] -use openssl::ssl::SslAcceptorBuilder; -#[cfg(feature = "rust-tls")] -use rustls::ServerConfig; +use crate::app::State; +use crate::request::Request; +use crate::service::ServiceRequest; -use application::{App, HttpApplication}; -use body::Binary; -use client::{ClientConnector, ClientRequest, ClientRequestBuilder}; -use error::Error; -use handler::{AsyncResult, AsyncResultItem, Handler, Responder}; -use header::{Header, IntoHeaderValue}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::Middleware; -use param::Params; -use payload::Payload; -use resource::Resource; -use router::Router; -use server::message::{Request, RequestPool}; -use server::{HttpServer, IntoHttpHandler, ServerSettings}; -use uri::Url as InnerUrl; -use ws; - -/// The `TestServer` type. +/// Test `Request` builder /// -/// `TestServer` is very simple test server that simplify process of writing -/// integration tests cases for actix web applications. -/// -/// # Examples -/// -/// ```rust -/// # extern crate actix_web; -/// # use actix_web::*; -/// # -/// # fn my_handler(req: &HttpRequest) -> HttpResponse { -/// # HttpResponse::Ok().into() -/// # } -/// # -/// # fn main() { -/// use actix_web::test::TestServer; -/// -/// let mut srv = TestServer::new(|app| app.handler(my_handler)); -/// -/// let req = srv.get().finish().unwrap(); -/// let response = srv.execute(req.send()).unwrap(); -/// assert!(response.status().is_success()); -/// # } -/// ``` -pub struct TestServer { - addr: net::SocketAddr, - ssl: bool, - conn: Addr, - rt: Runtime, - backend: Addr, -} - -impl TestServer { - /// Start new test server - /// - /// This method accepts configuration method. You can add - /// middlewares or set handlers for test application. - pub fn new(config: F) -> Self - where - F: Clone + Send + 'static + Fn(&mut TestApp<()>), - { - TestServerBuilder::new(|| ()).start(config) - } - - /// Create test server builder - pub fn build() -> TestServerBuilder<(), impl Fn() -> () + Clone + Send + 'static> { - TestServerBuilder::new(|| ()) - } - - /// Create test server builder with specific state factory - /// - /// This method can be used for constructing application state. - /// Also it can be used for external dependency initialization, - /// like creating sync actors for diesel integration. - pub fn build_with_state(state: F) -> TestServerBuilder - where - F: Fn() -> S + Clone + Send + 'static, - S: 'static, - { - TestServerBuilder::new(state) - } - - /// Start new test server with application factory - pub fn with_factory(factory: F) -> Self - where - F: Fn() -> H + Send + Clone + 'static, - H: IntoHttpHandler + 'static, - { - let (tx, rx) = mpsc::channel(); - - // run server in separate thread - thread::spawn(move || { - let sys = System::new("actix-test-server"); - let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); - let local_addr = tcp.local_addr().unwrap(); - - let srv = HttpServer::new(factory) - .disable_signals() - .listen(tcp) - .keep_alive(5) - .start(); - - tx.send((System::current(), local_addr, TestServer::get_conn(), srv)) - .unwrap(); - sys.run(); - }); - - let (system, addr, conn, backend) = rx.recv().unwrap(); - System::set_current(system); - TestServer { - addr, - conn, - ssl: false, - rt: Runtime::new().unwrap(), - backend, - } - } - - fn get_conn() -> Addr { - #[cfg(any(feature = "alpn", feature = "ssl"))] - { - use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; - - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - ClientConnector::with_connector(builder.build()).start() - } - #[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "ssl")) - ))] - { - use rustls::ClientConfig; - use std::fs::File; - use std::io::BufReader; - let mut config = ClientConfig::new(); - let pem_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); - config.root_store.add_pem_file(pem_file).unwrap(); - ClientConnector::with_connector(config).start() - } - #[cfg(not(any(feature = "alpn", feature = "ssl", feature = "rust-tls")))] - { - ClientConnector::default().start() - } - } - - /// Get firat available unused address - pub fn unused_addr() -> net::SocketAddr { - let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); - let socket = TcpBuilder::new_v4().unwrap(); - socket.bind(&addr).unwrap(); - socket.reuse_address(true).unwrap(); - let tcp = socket.to_tcp_listener().unwrap(); - tcp.local_addr().unwrap() - } - - /// Construct test server url - pub fn addr(&self) -> net::SocketAddr { - self.addr - } - - /// Construct test server url - pub fn url(&self, uri: &str) -> String { - if uri.starts_with('/') { - format!( - "{}://localhost:{}{}", - if self.ssl { "https" } else { "http" }, - self.addr.port(), - uri - ) - } else { - format!( - "{}://localhost:{}/{}", - if self.ssl { "https" } else { "http" }, - self.addr.port(), - uri - ) - } - } - - /// Stop http server - fn stop(&mut self) { - let _ = self.backend.send(signal::Signal(signal::SignalType::Term)).wait(); - System::current().stop(); - } - - /// Execute future on current core - pub fn execute(&mut self, fut: F) -> Result - where - F: Future, - { - self.rt.block_on(fut) - } - - /// Connect to websocket server at a given path - pub fn ws_at( - &mut self, path: &str, - ) -> Result<(ws::ClientReader, ws::ClientWriter), ws::ClientError> { - let url = self.url(path); - self.rt - .block_on(ws::Client::with_connector(url, self.conn.clone()).connect()) - } - - /// Connect to a websocket server - pub fn ws( - &mut self, - ) -> Result<(ws::ClientReader, ws::ClientWriter), ws::ClientError> { - self.ws_at("/") - } - - /// Create `GET` request - pub fn get(&self) -> ClientRequestBuilder { - ClientRequest::get(self.url("/").as_str()) - } - - /// Create `POST` request - pub fn post(&self) -> ClientRequestBuilder { - ClientRequest::post(self.url("/").as_str()) - } - - /// Create `HEAD` request - pub fn head(&self) -> ClientRequestBuilder { - ClientRequest::head(self.url("/").as_str()) - } - - /// Connect to test http server - pub fn client(&self, meth: Method, path: &str) -> ClientRequestBuilder { - ClientRequest::build() - .method(meth) - .uri(self.url(path).as_str()) - .with_connector(self.conn.clone()) - .take() - } -} - -impl Drop for TestServer { - fn drop(&mut self) { - self.stop() - } -} - -/// An `TestServer` builder -/// -/// This type can be used to construct an instance of `TestServer` through a -/// builder-like pattern. -pub struct TestServerBuilder -where - F: Fn() -> S + Send + Clone + 'static, -{ - state: F, - #[cfg(any(feature = "alpn", feature = "ssl"))] - ssl: Option, - #[cfg(feature = "rust-tls")] - rust_ssl: Option, -} - -impl TestServerBuilder -where - F: Fn() -> S + Send + Clone + 'static, -{ - /// Create a new test server - pub fn new(state: F) -> TestServerBuilder { - TestServerBuilder { - state, - #[cfg(any(feature = "alpn", feature = "ssl"))] - ssl: None, - #[cfg(feature = "rust-tls")] - rust_ssl: None, - } - } - - #[cfg(any(feature = "alpn", feature = "ssl"))] - /// Create ssl server - pub fn ssl(mut self, ssl: SslAcceptorBuilder) -> Self { - self.ssl = Some(ssl); - self - } - - #[cfg(feature = "rust-tls")] - /// Create rust tls server - pub fn rustls(mut self, ssl: ServerConfig) -> Self { - self.rust_ssl = Some(ssl); - self - } - - #[allow(unused_mut)] - /// Configure test application and run test server - pub fn start(mut self, config: C) -> TestServer - where - C: Fn(&mut TestApp) + Clone + Send + 'static, - { - let (tx, rx) = mpsc::channel(); - - let mut has_ssl = false; - - #[cfg(any(feature = "alpn", feature = "ssl"))] - { - has_ssl = has_ssl || self.ssl.is_some(); - } - - #[cfg(feature = "rust-tls")] - { - has_ssl = has_ssl || self.rust_ssl.is_some(); - } - - // run server in separate thread - thread::spawn(move || { - let addr = TestServer::unused_addr(); - - let sys = System::new("actix-test-server"); - let state = self.state; - let mut srv = HttpServer::new(move || { - let mut app = TestApp::new(state()); - config(&mut app); - app - }).workers(1) - .keep_alive(5) - .disable_signals(); - - - - #[cfg(any(feature = "alpn", feature = "ssl"))] - { - let ssl = self.ssl.take(); - if let Some(ssl) = ssl { - let tcp = net::TcpListener::bind(addr).unwrap(); - srv = srv.listen_ssl(tcp, ssl).unwrap(); - } - } - #[cfg(feature = "rust-tls")] - { - let ssl = self.rust_ssl.take(); - if let Some(ssl) = ssl { - let tcp = net::TcpListener::bind(addr).unwrap(); - srv = srv.listen_rustls(tcp, ssl); - } - } - if !has_ssl { - let tcp = net::TcpListener::bind(addr).unwrap(); - srv = srv.listen(tcp); - } - let backend = srv.start(); - - tx.send((System::current(), addr, TestServer::get_conn(), backend)) - .unwrap(); - - sys.run(); - }); - - let (system, addr, conn, backend) = rx.recv().unwrap(); - System::set_current(system); - TestServer { - addr, - conn, - ssl: has_ssl, - rt: Runtime::new().unwrap(), - backend, - } - } -} - -/// Test application helper for testing request handlers. -pub struct TestApp { - app: Option>, -} - -impl TestApp { - fn new(state: S) -> TestApp { - let app = App::with_state(state); - TestApp { app: Some(app) } - } - - /// Register handler for "/" - pub fn handler(&mut self, handler: F) - where - F: Fn(&HttpRequest) -> R + 'static, - R: Responder + 'static, - { - self.app = Some(self.app.take().unwrap().resource("/", |r| r.f(handler))); - } - - /// Register middleware - pub fn middleware(&mut self, mw: T) -> &mut TestApp - where - T: Middleware + 'static, - { - self.app = Some(self.app.take().unwrap().middleware(mw)); - self - } - - /// Register resource. This method is similar - /// to `App::resource()` method. - pub fn resource(&mut self, path: &str, f: F) -> &mut TestApp - where - F: FnOnce(&mut Resource) -> R + 'static, - { - self.app = Some(self.app.take().unwrap().resource(path, f)); - self - } -} - -impl IntoHttpHandler for TestApp { - type Handler = HttpApplication; - - fn into_handler(mut self) -> HttpApplication { - self.app.take().unwrap().into_handler() - } -} - -#[doc(hidden)] -impl Iterator for TestApp { - type Item = HttpApplication; - - fn next(&mut self) -> Option { - if let Some(mut app) = self.app.take() { - Some(app.finish()) - } else { - None - } - } -} - -/// Test `HttpRequest` builder -/// -/// ```rust +/// ```rust,ignore /// # extern crate http; /// # extern crate actix_web; /// # use http::{header, StatusCode}; @@ -474,10 +48,8 @@ pub struct TestRequest { method: Method, uri: Uri, headers: HeaderMap, - params: Params, - cookies: Option>>, + params: Path, payload: Option, - prefix: u16, } impl Default for TestRequest<()> { @@ -488,10 +60,8 @@ impl Default for TestRequest<()> { uri: Uri::from_str("/").unwrap(), version: Version::HTTP_11, headers: HeaderMap::new(), - params: Params::new(), - cookies: None, + params: Path::new(Url::default()), payload: None, - prefix: 0, } } } @@ -515,11 +85,6 @@ impl TestRequest<()> { { TestRequest::default().header(key, value) } - - /// Create TestRequest and set request cookie - pub fn with_cookie(cookie: Cookie<'static>) -> TestRequest<()> { - TestRequest::default().cookie(cookie) - } } impl TestRequest { @@ -531,10 +96,8 @@ impl TestRequest { uri: Uri::from_str("/").unwrap(), version: Version::HTTP_11, headers: HeaderMap::new(), - params: Params::new(), - cookies: None, + params: Path::new(Url::default()), payload: None, - prefix: 0, } } @@ -556,25 +119,6 @@ impl TestRequest { self } - /// set cookie of this request - pub fn cookie(mut self, cookie: Cookie<'static>) -> Self { - if self.cookies.is_some() { - let mut should_insert = true; - let old_cookies = self.cookies.as_mut().unwrap(); - for old_cookie in old_cookies.iter() { - if old_cookie == &cookie { - should_insert = false - }; - }; - if should_insert { - old_cookies.push(cookie); - }; - } else { - self.cookies = Some(vec![cookie]); - }; - self - } - /// Set a header pub fn set(mut self, hdr: H) -> Self { if let Ok(value) = hdr.try_into() { @@ -606,22 +150,15 @@ impl TestRequest { } /// Set request payload - pub fn set_payload>(mut self, data: B) -> Self { - let mut data = data.into(); + pub fn set_payload>(mut self, data: B) -> Self { let mut payload = Payload::empty(); - payload.unread_data(data.take()); + payload.unread_data(data.into()); self.payload = Some(payload); self } - /// Set request's prefix - pub fn prefix(mut self, prefix: u16) -> Self { - self.prefix = prefix; - self - } - /// Complete request creation and generate `HttpRequest` instance - pub fn finish(self) -> HttpRequest { + pub fn finish(self) -> ServiceRequest { let TestRequest { state, method, @@ -629,172 +166,31 @@ impl TestRequest { version, headers, mut params, - cookies, payload, - prefix, - } = self; - let router = Router::<()>::default(); - - let pool = RequestPool::pool(ServerSettings::default()); - let mut req = RequestPool::get(pool); - { - let inner = req.inner_mut(); - inner.method = method; - inner.url = InnerUrl::new(uri); - inner.version = version; - inner.headers = headers; - *inner.payload.borrow_mut() = payload; - } - params.set_url(req.url().clone()); - let mut info = router.route_info_params(0, params); - info.set_prefix(prefix); - - let mut req = HttpRequest::new(req, Rc::new(state), info); - req.set_cookies(cookies); - req - } - - #[cfg(test)] - /// Complete request creation and generate `HttpRequest` instance - pub(crate) fn finish_with_router(self, router: Router) -> HttpRequest { - let TestRequest { - state, - method, - uri, - version, - headers, - mut params, - cookies, - payload, - prefix, } = self; - let pool = RequestPool::pool(ServerSettings::default()); - let mut req = RequestPool::get(pool); + params.get_mut().update(&uri); + + let mut req = HttpRequest::new(); { let inner = req.inner_mut(); - inner.method = method; - inner.url = InnerUrl::new(uri); - inner.version = version; - inner.headers = headers; + inner.head.uri = uri; + inner.head.method = method; + inner.head.version = version; + inner.head.headers = headers; *inner.payload.borrow_mut() = payload; } - params.set_url(req.url().clone()); - let mut info = router.route_info_params(0, params); - info.set_prefix(prefix); - let mut req = HttpRequest::new(req, Rc::new(state), info); - req.set_cookies(cookies); - req - } - /// Complete request creation and generate server `Request` instance - pub fn request(self) -> Request { - let TestRequest { - method, - uri, - version, - headers, - payload, - .. - } = self; - - let pool = RequestPool::pool(ServerSettings::default()); - let mut req = RequestPool::get(pool); - { - let inner = req.inner_mut(); - inner.method = method; - inner.url = InnerUrl::new(uri); - inner.version = version; - inner.headers = headers; - *inner.payload.borrow_mut() = payload; - } - req - } - - /// This method generates `HttpRequest` instance and runs handler - /// with generated request. - pub fn run>(self, h: &H) -> Result { - let req = self.finish(); - let resp = h.handle(&req); - - match resp.respond_to(&req) { - Ok(resp) => match resp.into().into() { - AsyncResultItem::Ok(resp) => Ok(resp), - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Future(fut) => { - let mut sys = System::new("test"); - sys.block_on(fut) - } - }, - Err(err) => Err(err.into()), - } - } - - /// This method generates `HttpRequest` instance and runs handler - /// with generated request. - /// - /// This method panics is handler returns actor. - pub fn run_async(self, h: H) -> Result - where - H: Fn(HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - { - let req = self.finish(); - let fut = h(req.clone()); - - let mut sys = System::new("test"); - match sys.block_on(fut) { - Ok(r) => match r.respond_to(&req) { - Ok(reply) => match reply.into().into() { - AsyncResultItem::Ok(resp) => Ok(resp), - _ => panic!("Nested async replies are not supported"), - }, - Err(e) => Err(e), - }, - Err(err) => Err(err), - } + Request::new(State::new(state), req, params) } /// This method generates `HttpRequest` instance and executes handler - pub fn run_async_result(self, f: F) -> Result + pub fn run_async(self, f: F) -> Result where - F: FnOnce(&HttpRequest) -> R, - R: Into>, + F: FnOnce(&Request) -> R, + R: IntoFuture, { - let req = self.finish(); - let res = f(&req); - - match res.into().into() { - AsyncResultItem::Ok(resp) => Ok(resp), - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Future(fut) => { - let mut sys = System::new("test"); - sys.block_on(fut) - } - } - } - - /// This method generates `HttpRequest` instance and executes handler - pub fn execute(self, f: F) -> Result - where - F: FnOnce(&HttpRequest) -> R, - R: Responder + 'static, - { - let req = self.finish(); - let resp = f(&req); - - match resp.respond_to(&req) { - Ok(resp) => match resp.into().into() { - AsyncResultItem::Ok(resp) => Ok(resp), - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Future(fut) => { - let mut sys = System::new("test"); - sys.block_on(fut) - } - }, - Err(err) => Err(err.into()), - } + let mut rt = Runtime::new().unwrap(); + rt.block_on(f(&self.finish()).into_future()) } } diff --git a/src/uri.rs b/src/uri.rs deleted file mode 100644 index c87cb3d5b..000000000 --- a/src/uri.rs +++ /dev/null @@ -1,177 +0,0 @@ -use http::Uri; -use std::rc::Rc; - -// https://tools.ietf.org/html/rfc3986#section-2.2 -const RESERVED_PLUS_EXTRA: &[u8] = b":/?#[]@!$&'()*,+?;=%^ <>\"\\`{}|"; - -// https://tools.ietf.org/html/rfc3986#section-2.3 -const UNRESERVED: &[u8] = - b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-._~"; - -#[inline] -fn bit_at(array: &[u8], ch: u8) -> bool { - array[(ch >> 3) as usize] & (1 << (ch & 7)) != 0 -} - -#[inline] -fn set_bit(array: &mut [u8], ch: u8) { - array[(ch >> 3) as usize] |= 1 << (ch & 7) -} - -lazy_static! { - static ref UNRESERVED_QUOTER: Quoter = { Quoter::new(UNRESERVED) }; - pub(crate) static ref RESERVED_QUOTER: Quoter = { Quoter::new(RESERVED_PLUS_EXTRA) }; -} - -#[derive(Default, Clone, Debug)] -pub(crate) struct Url { - uri: Uri, - path: Option>, -} - -impl Url { - pub fn new(uri: Uri) -> Url { - let path = UNRESERVED_QUOTER.requote(uri.path().as_bytes()); - - Url { uri, path } - } - - pub fn uri(&self) -> &Uri { - &self.uri - } - - pub fn path(&self) -> &str { - if let Some(ref s) = self.path { - s - } else { - self.uri.path() - } - } -} - -pub(crate) struct Quoter { - safe_table: [u8; 16], -} - -impl Quoter { - pub fn new(safe: &[u8]) -> Quoter { - let mut q = Quoter { - safe_table: [0; 16], - }; - - // prepare safe table - for ch in safe { - set_bit(&mut q.safe_table, *ch) - } - - q - } - - pub fn requote(&self, val: &[u8]) -> Option> { - let mut has_pct = 0; - let mut pct = [b'%', 0, 0]; - let mut idx = 0; - let mut cloned: Option> = None; - - let len = val.len(); - while idx < len { - let ch = val[idx]; - - if has_pct != 0 { - pct[has_pct] = val[idx]; - has_pct += 1; - if has_pct == 3 { - has_pct = 0; - let buf = cloned.as_mut().unwrap(); - - if let Some(ch) = restore_ch(pct[1], pct[2]) { - if ch < 128 { - if bit_at(&self.safe_table, ch) { - buf.push(ch); - idx += 1; - continue; - } - - buf.extend_from_slice(&pct); - } else { - // Not ASCII, decode it - buf.push(ch); - } - } else { - buf.extend_from_slice(&pct[..]); - } - } - } else if ch == b'%' { - has_pct = 1; - if cloned.is_none() { - let mut c = Vec::with_capacity(len); - c.extend_from_slice(&val[..idx]); - cloned = Some(c); - } - } else if let Some(ref mut cloned) = cloned { - cloned.push(ch) - } - idx += 1; - } - - if let Some(data) = cloned { - // Unsafe: we get data from http::Uri, which does utf-8 checks already - // this code only decodes valid pct encoded values - Some(Rc::new(unsafe { String::from_utf8_unchecked(data) })) - } else { - None - } - } -} - -#[inline] -fn from_hex(v: u8) -> Option { - if v >= b'0' && v <= b'9' { - Some(v - 0x30) // ord('0') == 0x30 - } else if v >= b'A' && v <= b'F' { - Some(v - 0x41 + 10) // ord('A') == 0x41 - } else if v > b'a' && v <= b'f' { - Some(v - 0x61 + 10) // ord('a') == 0x61 - } else { - None - } -} - -#[inline] -fn restore_ch(d1: u8, d2: u8) -> Option { - from_hex(d1).and_then(|d1| from_hex(d2).and_then(move |d2| Some(d1 << 4 | d2))) -} - - -#[cfg(test)] -mod tests { - use std::rc::Rc; - - use super::*; - - #[test] - fn decode_path() { - assert_eq!(UNRESERVED_QUOTER.requote(b"https://localhost:80/foo"), None); - - assert_eq!( - Rc::try_unwrap(UNRESERVED_QUOTER.requote( - b"https://localhost:80/foo%25" - ).unwrap()).unwrap(), - "https://localhost:80/foo%25".to_string() - ); - - assert_eq!( - Rc::try_unwrap(UNRESERVED_QUOTER.requote( - b"http://cache-service/http%3A%2F%2Flocalhost%3A80%2Ffoo" - ).unwrap()).unwrap(), - "http://cache-service/http%3A%2F%2Flocalhost%3A80%2Ffoo".to_string() - ); - - assert_eq!( - Rc::try_unwrap(UNRESERVED_QUOTER.requote( - b"http://cache/http%3A%2F%2Flocal%3A80%2Ffile%2F%252Fvar%252Flog%0A" - ).unwrap()).unwrap(), - "http://cache/http%3A%2F%2Flocal%3A80%2Ffile%2F%252Fvar%252Flog%0A".to_string() - ); - } -} \ No newline at end of file diff --git a/src/with.rs b/src/with.rs deleted file mode 100644 index 140e086e1..000000000 --- a/src/with.rs +++ /dev/null @@ -1,383 +0,0 @@ -use futures::{Async, Future, Poll}; -use std::marker::PhantomData; -use std::rc::Rc; - -use error::Error; -use handler::{AsyncResult, AsyncResultItem, FromRequest, Handler, Responder}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; - -trait FnWith: 'static { - fn call_with(self: &Self, T) -> R; -} - -impl R + 'static> FnWith for F { - fn call_with(self: &Self, arg: T) -> R { - (*self)(arg) - } -} - -#[doc(hidden)] -pub trait WithFactory: 'static -where - T: FromRequest, - R: Responder, -{ - fn create(self) -> With; - - fn create_with_config(self, T::Config) -> With; -} - -#[doc(hidden)] -pub trait WithAsyncFactory: 'static -where - T: FromRequest, - R: Future, - I: Responder, - E: Into, -{ - fn create(self) -> WithAsync; - - fn create_with_config(self, T::Config) -> WithAsync; -} - -#[doc(hidden)] -pub struct With -where - T: FromRequest, - S: 'static, -{ - hnd: Rc>, - cfg: Rc, - _s: PhantomData, -} - -impl With -where - T: FromRequest, - S: 'static, -{ - pub fn new R + 'static>(f: F, cfg: T::Config) -> Self { - With { - cfg: Rc::new(cfg), - hnd: Rc::new(f), - _s: PhantomData, - } - } -} - -impl Handler for With -where - R: Responder + 'static, - T: FromRequest + 'static, - S: 'static, -{ - type Result = AsyncResult; - - fn handle(&self, req: &HttpRequest) -> Self::Result { - let mut fut = WithHandlerFut { - req: req.clone(), - started: false, - hnd: Rc::clone(&self.hnd), - cfg: self.cfg.clone(), - fut1: None, - fut2: None, - }; - - match fut.poll() { - Ok(Async::Ready(resp)) => AsyncResult::ok(resp), - Ok(Async::NotReady) => AsyncResult::future(Box::new(fut)), - Err(e) => AsyncResult::err(e), - } - } -} - -struct WithHandlerFut -where - R: Responder, - T: FromRequest + 'static, - S: 'static, -{ - started: bool, - hnd: Rc>, - cfg: Rc, - req: HttpRequest, - fut1: Option>>, - fut2: Option>>, -} - -impl Future for WithHandlerFut -where - R: Responder + 'static, - T: FromRequest + 'static, - S: 'static, -{ - type Item = HttpResponse; - type Error = Error; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut2 { - return fut.poll(); - } - - let item = if !self.started { - self.started = true; - let reply = T::from_request(&self.req, self.cfg.as_ref()).into(); - match reply.into() { - AsyncResultItem::Err(err) => return Err(err), - AsyncResultItem::Ok(msg) => msg, - AsyncResultItem::Future(fut) => { - self.fut1 = Some(fut); - return self.poll(); - } - } - } else { - match self.fut1.as_mut().unwrap().poll()? { - Async::Ready(item) => item, - Async::NotReady => return Ok(Async::NotReady), - } - }; - - let item = match self.hnd.as_ref().call_with(item).respond_to(&self.req) { - Ok(item) => item.into(), - Err(e) => return Err(e.into()), - }; - - match item.into() { - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Ok(resp) => Ok(Async::Ready(resp)), - AsyncResultItem::Future(fut) => { - self.fut2 = Some(fut); - self.poll() - } - } - } -} - -#[doc(hidden)] -pub struct WithAsync -where - R: Future, - I: Responder, - E: Into, - T: FromRequest, - S: 'static, -{ - hnd: Rc>, - cfg: Rc, - _s: PhantomData, -} - -impl WithAsync -where - R: Future, - I: Responder, - E: Into, - T: FromRequest, - S: 'static, -{ - pub fn new R + 'static>(f: F, cfg: T::Config) -> Self { - WithAsync { - cfg: Rc::new(cfg), - hnd: Rc::new(f), - _s: PhantomData, - } - } -} - -impl Handler for WithAsync -where - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, - S: 'static, -{ - type Result = AsyncResult; - - fn handle(&self, req: &HttpRequest) -> Self::Result { - let mut fut = WithAsyncHandlerFut { - req: req.clone(), - started: false, - hnd: Rc::clone(&self.hnd), - cfg: Rc::clone(&self.cfg), - fut1: None, - fut2: None, - fut3: None, - }; - - match fut.poll() { - Ok(Async::Ready(resp)) => AsyncResult::ok(resp), - Ok(Async::NotReady) => AsyncResult::future(Box::new(fut)), - Err(e) => AsyncResult::err(e), - } - } -} - -struct WithAsyncHandlerFut -where - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, - S: 'static, -{ - started: bool, - hnd: Rc>, - cfg: Rc, - req: HttpRequest, - fut1: Option>>, - fut2: Option, - fut3: Option>>, -} - -impl Future for WithAsyncHandlerFut -where - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, - S: 'static, -{ - type Item = HttpResponse; - type Error = Error; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut3 { - return fut.poll(); - } - - if self.fut2.is_some() { - return match self.fut2.as_mut().unwrap().poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(r)) => match r.respond_to(&self.req) { - Ok(r) => match r.into().into() { - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Ok(resp) => Ok(Async::Ready(resp)), - AsyncResultItem::Future(fut) => { - self.fut3 = Some(fut); - self.poll() - } - }, - Err(e) => Err(e.into()), - }, - Err(e) => Err(e.into()), - }; - } - - let item = if !self.started { - self.started = true; - let reply = T::from_request(&self.req, self.cfg.as_ref()).into(); - match reply.into() { - AsyncResultItem::Err(err) => return Err(err), - AsyncResultItem::Ok(msg) => msg, - AsyncResultItem::Future(fut) => { - self.fut1 = Some(fut); - return self.poll(); - } - } - } else { - match self.fut1.as_mut().unwrap().poll()? { - Async::Ready(item) => item, - Async::NotReady => return Ok(Async::NotReady), - } - }; - - self.fut2 = Some(self.hnd.as_ref().call_with(item)); - self.poll() - } -} - -macro_rules! with_factory_tuple ({$(($n:tt, $T:ident)),+} => { - impl<$($T,)+ State, Func, Res> WithFactory<($($T,)+), State, Res> for Func - where Func: Fn($($T,)+) -> Res + 'static, - $($T: FromRequest + 'static,)+ - Res: Responder + 'static, - State: 'static, - { - fn create(self) -> With<($($T,)+), State, Res> { - With::new(move |($($n,)+)| (self)($($n,)+), ($($T::Config::default(),)+)) - } - - fn create_with_config(self, cfg: ($($T::Config,)+)) -> With<($($T,)+), State, Res> { - With::new(move |($($n,)+)| (self)($($n,)+), cfg) - } - } -}); - -macro_rules! with_async_factory_tuple ({$(($n:tt, $T:ident)),+} => { - impl<$($T,)+ State, Func, Res, Item, Err> WithAsyncFactory<($($T,)+), State, Res, Item, Err> for Func - where Func: Fn($($T,)+) -> Res + 'static, - $($T: FromRequest + 'static,)+ - Res: Future, - Item: Responder + 'static, - Err: Into, - State: 'static, - { - fn create(self) -> WithAsync<($($T,)+), State, Res, Item, Err> { - WithAsync::new(move |($($n,)+)| (self)($($n,)+), ($($T::Config::default(),)+)) - } - - fn create_with_config(self, cfg: ($($T::Config,)+)) -> WithAsync<($($T,)+), State, Res, Item, Err> { - WithAsync::new(move |($($n,)+)| (self)($($n,)+), cfg) - } - } -}); - -with_factory_tuple!((a, A)); -with_factory_tuple!((a, A), (b, B)); -with_factory_tuple!((a, A), (b, B), (c, C)); -with_factory_tuple!((a, A), (b, B), (c, C), (d, D)); -with_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E)); -with_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E), (f, F)); -with_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E), (f, F), (g, G)); -with_factory_tuple!( - (a, A), - (b, B), - (c, C), - (d, D), - (e, E), - (f, F), - (g, G), - (h, H) -); -with_factory_tuple!( - (a, A), - (b, B), - (c, C), - (d, D), - (e, E), - (f, F), - (g, G), - (h, H), - (i, I) -); - -with_async_factory_tuple!((a, A)); -with_async_factory_tuple!((a, A), (b, B)); -with_async_factory_tuple!((a, A), (b, B), (c, C)); -with_async_factory_tuple!((a, A), (b, B), (c, C), (d, D)); -with_async_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E)); -with_async_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E), (f, F)); -with_async_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E), (f, F), (g, G)); -with_async_factory_tuple!( - (a, A), - (b, B), - (c, C), - (d, D), - (e, E), - (f, F), - (g, G), - (h, H) -); -with_async_factory_tuple!( - (a, A), - (b, B), - (c, C), - (d, D), - (e, E), - (f, F), - (g, G), - (h, H), - (i, I) -); diff --git a/src/ws/client.rs b/src/ws/client.rs deleted file mode 100644 index 18789fef8..000000000 --- a/src/ws/client.rs +++ /dev/null @@ -1,602 +0,0 @@ -//! Http client request -use std::cell::RefCell; -use std::rc::Rc; -use std::time::Duration; -use std::{fmt, io, str}; - -use base64; -use bytes::Bytes; -use cookie::Cookie; -use futures::sync::mpsc::{unbounded, UnboundedSender}; -use futures::{Async, Future, Poll, Stream}; -use http::header::{self, HeaderName, HeaderValue}; -use http::{Error as HttpError, HttpTryFrom, StatusCode}; -use rand; -use sha1::Sha1; - -use actix::{Addr, SystemService}; - -use body::{Binary, Body}; -use error::{Error, UrlParseError}; -use header::IntoHeaderValue; -use httpmessage::HttpMessage; -use payload::PayloadBuffer; - -use client::{ - ClientConnector, ClientRequest, ClientRequestBuilder, HttpResponseParserError, - Pipeline, SendRequest, SendRequestError, -}; - -use super::frame::{Frame, FramedMessage}; -use super::proto::{CloseReason, OpCode}; -use super::{Message, ProtocolError, WsWriter}; - -/// Websocket client error -#[derive(Fail, Debug)] -pub enum ClientError { - /// Invalid url - #[fail(display = "Invalid url")] - InvalidUrl, - /// Invalid response status - #[fail(display = "Invalid response status")] - InvalidResponseStatus(StatusCode), - /// Invalid upgrade header - #[fail(display = "Invalid upgrade header")] - InvalidUpgradeHeader, - /// Invalid connection header - #[fail(display = "Invalid connection header")] - InvalidConnectionHeader(HeaderValue), - /// Missing CONNECTION header - #[fail(display = "Missing CONNECTION header")] - MissingConnectionHeader, - /// Missing SEC-WEBSOCKET-ACCEPT header - #[fail(display = "Missing SEC-WEBSOCKET-ACCEPT header")] - MissingWebSocketAcceptHeader, - /// Invalid challenge response - #[fail(display = "Invalid challenge response")] - InvalidChallengeResponse(String, HeaderValue), - /// Http parsing error - #[fail(display = "Http parsing error")] - Http(Error), - /// Url parsing error - #[fail(display = "Url parsing error")] - Url(UrlParseError), - /// Response parsing error - #[fail(display = "Response parsing error")] - ResponseParseError(HttpResponseParserError), - /// Send request error - #[fail(display = "{}", _0)] - SendRequest(SendRequestError), - /// Protocol error - #[fail(display = "{}", _0)] - Protocol(#[cause] ProtocolError), - /// IO Error - #[fail(display = "{}", _0)] - Io(io::Error), - /// "Disconnected" - #[fail(display = "Disconnected")] - Disconnected, -} - -impl From for ClientError { - fn from(err: Error) -> ClientError { - ClientError::Http(err) - } -} - -impl From for ClientError { - fn from(err: UrlParseError) -> ClientError { - ClientError::Url(err) - } -} - -impl From for ClientError { - fn from(err: SendRequestError) -> ClientError { - ClientError::SendRequest(err) - } -} - -impl From for ClientError { - fn from(err: ProtocolError) -> ClientError { - ClientError::Protocol(err) - } -} - -impl From for ClientError { - fn from(err: io::Error) -> ClientError { - ClientError::Io(err) - } -} - -impl From for ClientError { - fn from(err: HttpResponseParserError) -> ClientError { - ClientError::ResponseParseError(err) - } -} - -/// `WebSocket` client -/// -/// Example of `WebSocket` client usage is available in -/// [websocket example]( -/// https://github.com/actix/examples/blob/master/websocket/src/client.rs#L24) -pub struct Client { - request: ClientRequestBuilder, - err: Option, - http_err: Option, - origin: Option, - protocols: Option, - conn: Addr, - max_size: usize, - no_masking: bool, -} - -impl Client { - /// Create new websocket connection - pub fn new>(uri: S) -> Client { - Client::with_connector(uri, ClientConnector::from_registry()) - } - - /// Create new websocket connection with custom `ClientConnector` - pub fn with_connector>(uri: S, conn: Addr) -> Client { - let mut cl = Client { - request: ClientRequest::build(), - err: None, - http_err: None, - origin: None, - protocols: None, - max_size: 65_536, - no_masking: false, - conn, - }; - cl.request.uri(uri.as_ref()); - cl - } - - /// Set supported websocket protocols - pub fn protocols(mut self, protos: U) -> Self - where - U: IntoIterator + 'static, - V: AsRef, - { - let mut protos = protos - .into_iter() - .fold(String::new(), |acc, s| acc + s.as_ref() + ","); - protos.pop(); - self.protocols = Some(protos); - self - } - - /// Set cookie for handshake request - pub fn cookie(mut self, cookie: Cookie) -> Self { - self.request.cookie(cookie); - self - } - - /// Set request Origin - pub fn origin(mut self, origin: V) -> Self - where - HeaderValue: HttpTryFrom, - { - match HeaderValue::try_from(origin) { - Ok(value) => self.origin = Some(value), - Err(e) => self.http_err = Some(e.into()), - } - self - } - - /// Set max frame size - /// - /// By default max size is set to 64kb - pub fn max_frame_size(mut self, size: usize) -> Self { - self.max_size = size; - self - } - - /// Set write buffer capacity - /// - /// Default buffer capacity is 32kb - pub fn write_buffer_capacity(mut self, cap: usize) -> Self { - self.request.write_buffer_capacity(cap); - self - } - - /// Disable payload masking. By default ws client masks frame payload. - pub fn no_masking(mut self) -> Self { - self.no_masking = true; - self - } - - /// Set request header - pub fn header(mut self, key: K, value: V) -> Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - self.request.header(key, value); - self - } - - /// Set websocket handshake timeout - /// - /// Handshake timeout is a total time for successful handshake. - /// Default value is 5 seconds. - pub fn timeout(mut self, timeout: Duration) -> Self { - self.request.timeout(timeout); - self - } - - /// Connect to websocket server and do ws handshake - pub fn connect(&mut self) -> ClientHandshake { - if let Some(e) = self.err.take() { - ClientHandshake::error(e) - } else if let Some(e) = self.http_err.take() { - ClientHandshake::error(Error::from(e).into()) - } else { - // origin - if let Some(origin) = self.origin.take() { - self.request.set_header(header::ORIGIN, origin); - } - - self.request.upgrade(); - self.request.set_header(header::UPGRADE, "websocket"); - self.request.set_header(header::CONNECTION, "upgrade"); - self.request.set_header(header::SEC_WEBSOCKET_VERSION, "13"); - self.request.with_connector(self.conn.clone()); - - if let Some(protocols) = self.protocols.take() { - self.request - .set_header(header::SEC_WEBSOCKET_PROTOCOL, protocols.as_str()); - } - let request = match self.request.finish() { - Ok(req) => req, - Err(err) => return ClientHandshake::error(err.into()), - }; - - if request.uri().host().is_none() { - return ClientHandshake::error(ClientError::InvalidUrl); - } - if let Some(scheme) = request.uri().scheme_part() { - if scheme != "http" - && scheme != "https" - && scheme != "ws" - && scheme != "wss" - { - return ClientHandshake::error(ClientError::InvalidUrl); - } - } else { - return ClientHandshake::error(ClientError::InvalidUrl); - } - - // start handshake - ClientHandshake::new(request, self.max_size, self.no_masking) - } - } -} - -struct Inner { - tx: UnboundedSender, - rx: PayloadBuffer>, - closed: bool, -} - -/// Future that implementes client websocket handshake process. -/// -/// It resolves to a pair of `ClientReader` and `ClientWriter` that -/// can be used for reading and writing websocket frames. -pub struct ClientHandshake { - request: Option, - tx: Option>, - key: String, - error: Option, - max_size: usize, - no_masking: bool, -} - -impl ClientHandshake { - fn new( - mut request: ClientRequest, max_size: usize, no_masking: bool, - ) -> ClientHandshake { - // Generate a random key for the `Sec-WebSocket-Key` header. - // a base64-encoded (see Section 4 of [RFC4648]) value that, - // when decoded, is 16 bytes in length (RFC 6455) - let sec_key: [u8; 16] = rand::random(); - let key = base64::encode(&sec_key); - - request.headers_mut().insert( - header::SEC_WEBSOCKET_KEY, - HeaderValue::try_from(key.as_str()).unwrap(), - ); - - let (tx, rx) = unbounded(); - request.set_body(Body::Streaming(Box::new(rx.map_err(|_| { - io::Error::new(io::ErrorKind::Other, "disconnected").into() - })))); - - ClientHandshake { - key, - max_size, - no_masking, - request: Some(request.send()), - tx: Some(tx), - error: None, - } - } - - fn error(err: ClientError) -> ClientHandshake { - ClientHandshake { - key: String::new(), - request: None, - tx: None, - error: Some(err), - max_size: 0, - no_masking: false, - } - } - - /// Set handshake timeout - /// - /// Handshake timeout is a total time before handshake should be completed. - /// Default value is 5 seconds. - pub fn timeout(mut self, timeout: Duration) -> Self { - if let Some(request) = self.request.take() { - self.request = Some(request.timeout(timeout)); - } - self - } - - /// Set connection timeout - /// - /// Connection timeout includes resolving hostname and actual connection to - /// the host. - /// Default value is 1 second. - pub fn conn_timeout(mut self, timeout: Duration) -> Self { - if let Some(request) = self.request.take() { - self.request = Some(request.conn_timeout(timeout)); - } - self - } -} - -impl Future for ClientHandshake { - type Item = (ClientReader, ClientWriter); - type Error = ClientError; - - fn poll(&mut self) -> Poll { - if let Some(err) = self.error.take() { - return Err(err); - } - - let resp = match self.request.as_mut().unwrap().poll()? { - Async::Ready(response) => { - self.request.take(); - response - } - Async::NotReady => return Ok(Async::NotReady), - }; - - // verify response - if resp.status() != StatusCode::SWITCHING_PROTOCOLS { - return Err(ClientError::InvalidResponseStatus(resp.status())); - } - // Check for "UPGRADE" to websocket header - let has_hdr = if let Some(hdr) = resp.headers().get(header::UPGRADE) { - if let Ok(s) = hdr.to_str() { - s.to_lowercase().contains("websocket") - } else { - false - } - } else { - false - }; - if !has_hdr { - trace!("Invalid upgrade header"); - return Err(ClientError::InvalidUpgradeHeader); - } - // Check for "CONNECTION" header - if let Some(conn) = resp.headers().get(header::CONNECTION) { - if let Ok(s) = conn.to_str() { - if !s.to_lowercase().contains("upgrade") { - trace!("Invalid connection header: {}", s); - return Err(ClientError::InvalidConnectionHeader(conn.clone())); - } - } else { - trace!("Invalid connection header: {:?}", conn); - return Err(ClientError::InvalidConnectionHeader(conn.clone())); - } - } else { - trace!("Missing connection header"); - return Err(ClientError::MissingConnectionHeader); - } - - if let Some(key) = resp.headers().get(header::SEC_WEBSOCKET_ACCEPT) { - // field is constructed by concatenating /key/ - // with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (RFC 6455) - const WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - let mut sha1 = Sha1::new(); - sha1.update(self.key.as_ref()); - sha1.update(WS_GUID); - let encoded = base64::encode(&sha1.digest().bytes()); - if key.as_bytes() != encoded.as_bytes() { - trace!( - "Invalid challenge response: expected: {} received: {:?}", - encoded, - key - ); - return Err(ClientError::InvalidChallengeResponse(encoded, key.clone())); - } - } else { - trace!("Missing SEC-WEBSOCKET-ACCEPT header"); - return Err(ClientError::MissingWebSocketAcceptHeader); - }; - - let inner = Inner { - tx: self.tx.take().unwrap(), - rx: PayloadBuffer::new(resp.payload()), - closed: false, - }; - - let inner = Rc::new(RefCell::new(inner)); - Ok(Async::Ready(( - ClientReader { - inner: Rc::clone(&inner), - max_size: self.max_size, - no_masking: self.no_masking, - }, - ClientWriter { inner }, - ))) - } -} - -/// Websocket reader client -pub struct ClientReader { - inner: Rc>, - max_size: usize, - no_masking: bool, -} - -impl fmt::Debug for ClientReader { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "ws::ClientReader()") - } -} - -impl Stream for ClientReader { - type Item = Message; - type Error = ProtocolError; - - fn poll(&mut self) -> Poll, Self::Error> { - let max_size = self.max_size; - let no_masking = self.no_masking; - let mut inner = self.inner.borrow_mut(); - if inner.closed { - return Ok(Async::Ready(None)); - } - - // read - match Frame::parse(&mut inner.rx, no_masking, max_size) { - Ok(Async::Ready(Some(frame))) => { - let (_finished, opcode, payload) = frame.unpack(); - - match opcode { - // continuation is not supported - OpCode::Continue => { - inner.closed = true; - Err(ProtocolError::NoContinuation) - } - OpCode::Bad => { - inner.closed = true; - Err(ProtocolError::BadOpCode) - } - OpCode::Close => { - inner.closed = true; - let close_reason = Frame::parse_close_payload(&payload); - Ok(Async::Ready(Some(Message::Close(close_reason)))) - } - OpCode::Ping => Ok(Async::Ready(Some(Message::Ping( - String::from_utf8_lossy(payload.as_ref()).into(), - )))), - OpCode::Pong => Ok(Async::Ready(Some(Message::Pong( - String::from_utf8_lossy(payload.as_ref()).into(), - )))), - OpCode::Binary => Ok(Async::Ready(Some(Message::Binary(payload)))), - OpCode::Text => { - let tmp = Vec::from(payload.as_ref()); - match String::from_utf8(tmp) { - Ok(s) => Ok(Async::Ready(Some(Message::Text(s)))), - Err(_) => { - inner.closed = true; - Err(ProtocolError::BadEncoding) - } - } - } - } - } - Ok(Async::Ready(None)) => Ok(Async::Ready(None)), - Ok(Async::NotReady) => Ok(Async::NotReady), - Err(e) => { - inner.closed = true; - Err(e) - } - } - } -} - -/// Websocket writer client -pub struct ClientWriter { - inner: Rc>, -} - -impl ClientWriter { - /// Write payload - #[inline] - fn write(&mut self, mut data: FramedMessage) { - let inner = self.inner.borrow_mut(); - if !inner.closed { - let _ = inner.tx.unbounded_send(data.0.take()); - } else { - warn!("Trying to write to disconnected response"); - } - } - - /// Send text frame - #[inline] - pub fn text>(&mut self, text: T) { - self.write(Frame::message(text.into(), OpCode::Text, true, true)); - } - - /// Send binary frame - #[inline] - pub fn binary>(&mut self, data: B) { - self.write(Frame::message(data, OpCode::Binary, true, true)); - } - - /// Send ping frame - #[inline] - pub fn ping(&mut self, message: &str) { - self.write(Frame::message(Vec::from(message), OpCode::Ping, true, true)); - } - - /// Send pong frame - #[inline] - pub fn pong(&mut self, message: &str) { - self.write(Frame::message(Vec::from(message), OpCode::Pong, true, true)); - } - - /// Send close frame - #[inline] - pub fn close(&mut self, reason: Option) { - self.write(Frame::close(reason, true)); - } -} - -impl WsWriter for ClientWriter { - /// Send text frame - #[inline] - fn send_text>(&mut self, text: T) { - self.text(text) - } - - /// Send binary frame - #[inline] - fn send_binary>(&mut self, data: B) { - self.binary(data) - } - - /// Send ping frame - #[inline] - fn send_ping(&mut self, message: &str) { - self.ping(message) - } - - /// Send pong frame - #[inline] - fn send_pong(&mut self, message: &str) { - self.pong(message) - } - - /// Send close frame - #[inline] - fn send_close(&mut self, reason: Option) { - self.close(reason); - } -} diff --git a/src/ws/context.rs b/src/ws/context.rs deleted file mode 100644 index 5e207d43e..000000000 --- a/src/ws/context.rs +++ /dev/null @@ -1,341 +0,0 @@ -extern crate actix; - -use bytes::Bytes; -use futures::sync::oneshot::{self, Sender}; -use futures::{Async, Future, Poll, Stream}; -use smallvec::SmallVec; - -use self::actix::dev::{ - AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, - ToEnvelope, -}; -use self::actix::fut::ActorFuture; -use self::actix::{ - Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, - Message as ActixMessage, SpawnHandle, -}; - -use body::{Binary, Body}; -use context::{ActorHttpContext, Drain, Frame as ContextFrame}; -use error::{Error, ErrorInternalServerError, PayloadError}; -use httprequest::HttpRequest; - -use ws::frame::{Frame, FramedMessage}; -use ws::proto::{CloseReason, OpCode}; -use ws::{Message, ProtocolError, WsStream, WsWriter}; - -/// Execution context for `WebSockets` actors -pub struct WebsocketContext -where - A: Actor>, -{ - inner: ContextParts, - stream: Option>, - request: HttpRequest, - disconnected: bool, -} - -impl ActorContext for WebsocketContext -where - A: Actor, -{ - fn stop(&mut self) { - self.inner.stop(); - } - fn terminate(&mut self) { - self.inner.terminate() - } - fn state(&self) -> ActorState { - self.inner.state() - } -} - -impl AsyncContext for WebsocketContext -where - A: Actor, -{ - fn spawn(&mut self, fut: F) -> SpawnHandle - where - F: ActorFuture + 'static, - { - self.inner.spawn(fut) - } - - fn wait(&mut self, fut: F) - where - F: ActorFuture + 'static, - { - self.inner.wait(fut) - } - - #[doc(hidden)] - #[inline] - fn waiting(&self) -> bool { - self.inner.waiting() - || self.inner.state() == ActorState::Stopping - || self.inner.state() == ActorState::Stopped - } - - fn cancel_future(&mut self, handle: SpawnHandle) -> bool { - self.inner.cancel_future(handle) - } - - #[inline] - fn address(&self) -> Addr { - self.inner.address() - } -} - -impl WebsocketContext -where - A: Actor, -{ - #[inline] - /// Create a new Websocket context from a request and an actor - pub fn create

    (req: HttpRequest, actor: A, stream: WsStream

    ) -> Body - where - A: StreamHandler, - P: Stream + 'static, - { - let mb = Mailbox::default(); - let mut ctx = WebsocketContext { - inner: ContextParts::new(mb.sender_producer()), - stream: None, - request: req, - disconnected: false, - }; - ctx.add_stream(stream); - - Body::Actor(Box::new(WebsocketContextFut::new(ctx, actor, mb))) - } - - /// Create a new Websocket context - pub fn with_factory(req: HttpRequest, f: F) -> Body - where - F: FnOnce(&mut Self) -> A + 'static, - { - let mb = Mailbox::default(); - let mut ctx = WebsocketContext { - inner: ContextParts::new(mb.sender_producer()), - stream: None, - request: req, - disconnected: false, - }; - - let act = f(&mut ctx); - Body::Actor(Box::new(WebsocketContextFut::new(ctx, act, mb))) - } -} - -impl WebsocketContext -where - A: Actor, -{ - /// Write payload - /// - /// This is a low-level function that accepts framed messages that should - /// be created using `Frame::message()`. If you want to send text or binary - /// data you should prefer the `text()` or `binary()` convenience functions - /// that handle the framing for you. - #[inline] - pub fn write_raw(&mut self, data: FramedMessage) { - if !self.disconnected { - if self.stream.is_none() { - self.stream = Some(SmallVec::new()); - } - let stream = self.stream.as_mut().unwrap(); - stream.push(ContextFrame::Chunk(Some(data.0))); - } else { - warn!("Trying to write to disconnected response"); - } - } - - /// Shared application state - #[inline] - pub fn state(&self) -> &S { - self.request.state() - } - - /// Incoming request - #[inline] - pub fn request(&mut self) -> &mut HttpRequest { - &mut self.request - } - - /// Returns drain future - pub fn drain(&mut self) -> Drain { - let (tx, rx) = oneshot::channel(); - self.add_frame(ContextFrame::Drain(tx)); - Drain::new(rx) - } - - /// Send text frame - #[inline] - pub fn text>(&mut self, text: T) { - self.write_raw(Frame::message(text.into(), OpCode::Text, true, false)); - } - - /// Send binary frame - #[inline] - pub fn binary>(&mut self, data: B) { - self.write_raw(Frame::message(data, OpCode::Binary, true, false)); - } - - /// Send ping frame - #[inline] - pub fn ping(&mut self, message: &str) { - self.write_raw(Frame::message( - Vec::from(message), - OpCode::Ping, - true, - false, - )); - } - - /// Send pong frame - #[inline] - pub fn pong(&mut self, message: &str) { - self.write_raw(Frame::message( - Vec::from(message), - OpCode::Pong, - true, - false, - )); - } - - /// Send close frame - #[inline] - pub fn close(&mut self, reason: Option) { - self.write_raw(Frame::close(reason, false)); - } - - /// Check if connection still open - #[inline] - pub fn connected(&self) -> bool { - !self.disconnected - } - - #[inline] - fn add_frame(&mut self, frame: ContextFrame) { - if self.stream.is_none() { - self.stream = Some(SmallVec::new()); - } - if let Some(s) = self.stream.as_mut() { - s.push(frame) - } - } - - /// Handle of the running future - /// - /// SpawnHandle is the handle returned by `AsyncContext::spawn()` method. - pub fn handle(&self) -> SpawnHandle { - self.inner.curr_handle() - } - - /// Set mailbox capacity - /// - /// By default mailbox capacity is 16 messages. - pub fn set_mailbox_capacity(&mut self, cap: usize) { - self.inner.set_mailbox_capacity(cap) - } -} - -impl WsWriter for WebsocketContext -where - A: Actor, - S: 'static, -{ - /// Send text frame - #[inline] - fn send_text>(&mut self, text: T) { - self.text(text) - } - - /// Send binary frame - #[inline] - fn send_binary>(&mut self, data: B) { - self.binary(data) - } - - /// Send ping frame - #[inline] - fn send_ping(&mut self, message: &str) { - self.ping(message) - } - - /// Send pong frame - #[inline] - fn send_pong(&mut self, message: &str) { - self.pong(message) - } - - /// Send close frame - #[inline] - fn send_close(&mut self, reason: Option) { - self.close(reason) - } -} - -impl AsyncContextParts for WebsocketContext -where - A: Actor, -{ - fn parts(&mut self) -> &mut ContextParts { - &mut self.inner - } -} - -struct WebsocketContextFut -where - A: Actor>, -{ - fut: ContextFut>, -} - -impl WebsocketContextFut -where - A: Actor>, -{ - fn new(ctx: WebsocketContext, act: A, mailbox: Mailbox) -> Self { - let fut = ContextFut::new(ctx, act, mailbox); - WebsocketContextFut { fut } - } -} - -impl ActorHttpContext for WebsocketContextFut -where - A: Actor>, - S: 'static, -{ - #[inline] - fn disconnected(&mut self) { - self.fut.ctx().disconnected = true; - self.fut.ctx().stop(); - } - - fn poll(&mut self) -> Poll>, Error> { - if self.fut.alive() && self.fut.poll().is_err() { - return Err(ErrorInternalServerError("error")); - } - - // frames - if let Some(data) = self.fut.ctx().stream.take() { - Ok(Async::Ready(Some(data))) - } else if self.fut.alive() { - Ok(Async::NotReady) - } else { - Ok(Async::Ready(None)) - } - } -} - -impl ToEnvelope for WebsocketContext -where - A: Actor> + Handler, - M: ActixMessage + Send + 'static, - M::Result: Send, -{ - fn pack(msg: M, tx: Option>) -> Envelope { - Envelope::new(msg, tx) - } -} diff --git a/src/ws/frame.rs b/src/ws/frame.rs deleted file mode 100644 index 5e4fd8290..000000000 --- a/src/ws/frame.rs +++ /dev/null @@ -1,538 +0,0 @@ -use byteorder::{ByteOrder, LittleEndian, NetworkEndian}; -use bytes::{BufMut, Bytes, BytesMut}; -use futures::{Async, Poll, Stream}; -use rand; -use std::fmt; - -use body::Binary; -use error::PayloadError; -use payload::PayloadBuffer; - -use ws::mask::apply_mask; -use ws::proto::{CloseCode, CloseReason, OpCode}; -use ws::ProtocolError; - -/// A struct representing a `WebSocket` frame. -#[derive(Debug)] -pub struct Frame { - finished: bool, - opcode: OpCode, - payload: Binary, -} - -impl Frame { - /// Destruct frame - pub fn unpack(self) -> (bool, OpCode, Binary) { - (self.finished, self.opcode, self.payload) - } - - /// Create a new Close control frame. - #[inline] - pub fn close(reason: Option, genmask: bool) -> FramedMessage { - let payload = match reason { - None => Vec::new(), - Some(reason) => { - let mut code_bytes = [0; 2]; - NetworkEndian::write_u16(&mut code_bytes, reason.code.into()); - - let mut payload = Vec::from(&code_bytes[..]); - if let Some(description) = reason.description { - payload.extend(description.as_bytes()); - } - payload - } - }; - - Frame::message(payload, OpCode::Close, true, genmask) - } - - #[cfg_attr(feature = "cargo-clippy", allow(type_complexity))] - fn read_copy_md( - pl: &mut PayloadBuffer, server: bool, max_size: usize, - ) -> Poll)>, ProtocolError> - where - S: Stream, - { - let mut idx = 2; - let buf = match pl.copy(2)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => return Ok(Async::NotReady), - }; - let first = buf[0]; - let second = buf[1]; - let finished = first & 0x80 != 0; - - // check masking - let masked = second & 0x80 != 0; - if !masked && server { - return Err(ProtocolError::UnmaskedFrame); - } else if masked && !server { - return Err(ProtocolError::MaskedFrame); - } - - // Op code - let opcode = OpCode::from(first & 0x0F); - - if let OpCode::Bad = opcode { - return Err(ProtocolError::InvalidOpcode(first & 0x0F)); - } - - let len = second & 0x7F; - let length = if len == 126 { - let buf = match pl.copy(4)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => return Ok(Async::NotReady), - }; - let len = NetworkEndian::read_uint(&buf[idx..], 2) as usize; - idx += 2; - len - } else if len == 127 { - let buf = match pl.copy(10)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => return Ok(Async::NotReady), - }; - let len = NetworkEndian::read_uint(&buf[idx..], 8); - if len > max_size as u64 { - return Err(ProtocolError::Overflow); - } - idx += 8; - len as usize - } else { - len as usize - }; - - // check for max allowed size - if length > max_size { - return Err(ProtocolError::Overflow); - } - - let mask = if server { - let buf = match pl.copy(idx + 4)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => return Ok(Async::NotReady), - }; - - let mask: &[u8] = &buf[idx..idx + 4]; - let mask_u32 = LittleEndian::read_u32(mask); - idx += 4; - Some(mask_u32) - } else { - None - }; - - Ok(Async::Ready(Some((idx, finished, opcode, length, mask)))) - } - - fn read_chunk_md( - chunk: &[u8], server: bool, max_size: usize, - ) -> Poll<(usize, bool, OpCode, usize, Option), ProtocolError> { - let chunk_len = chunk.len(); - - let mut idx = 2; - if chunk_len < 2 { - return Ok(Async::NotReady); - } - - let first = chunk[0]; - let second = chunk[1]; - let finished = first & 0x80 != 0; - - // check masking - let masked = second & 0x80 != 0; - if !masked && server { - return Err(ProtocolError::UnmaskedFrame); - } else if masked && !server { - return Err(ProtocolError::MaskedFrame); - } - - // Op code - let opcode = OpCode::from(first & 0x0F); - - if let OpCode::Bad = opcode { - return Err(ProtocolError::InvalidOpcode(first & 0x0F)); - } - - let len = second & 0x7F; - let length = if len == 126 { - if chunk_len < 4 { - return Ok(Async::NotReady); - } - let len = NetworkEndian::read_uint(&chunk[idx..], 2) as usize; - idx += 2; - len - } else if len == 127 { - if chunk_len < 10 { - return Ok(Async::NotReady); - } - let len = NetworkEndian::read_uint(&chunk[idx..], 8); - if len > max_size as u64 { - return Err(ProtocolError::Overflow); - } - idx += 8; - len as usize - } else { - len as usize - }; - - // check for max allowed size - if length > max_size { - return Err(ProtocolError::Overflow); - } - - let mask = if server { - if chunk_len < idx + 4 { - return Ok(Async::NotReady); - } - - let mask: &[u8] = &chunk[idx..idx + 4]; - let mask_u32 = LittleEndian::read_u32(mask); - idx += 4; - Some(mask_u32) - } else { - None - }; - - Ok(Async::Ready((idx, finished, opcode, length, mask))) - } - - /// Parse the input stream into a frame. - pub fn parse( - pl: &mut PayloadBuffer, server: bool, max_size: usize, - ) -> Poll, ProtocolError> - where - S: Stream, - { - // try to parse ws frame md from one chunk - let result = match pl.get_chunk()? { - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::Ready(Some(chunk)) => Frame::read_chunk_md(chunk, server, max_size)?, - }; - - let (idx, finished, opcode, length, mask) = match result { - // we may need to join several chunks - Async::NotReady => match Frame::read_copy_md(pl, server, max_size)? { - Async::Ready(Some(item)) => item, - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(None) => return Ok(Async::Ready(None)), - }, - Async::Ready(item) => item, - }; - - match pl.can_read(idx + length)? { - Async::Ready(Some(true)) => (), - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::Ready(Some(false)) | Async::NotReady => return Ok(Async::NotReady), - } - - // remove prefix - pl.drop_bytes(idx); - - // no need for body - if length == 0 { - return Ok(Async::Ready(Some(Frame { - finished, - opcode, - payload: Binary::from(""), - }))); - } - - let data = match pl.read_exact(length)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => panic!(), - }; - - // control frames must have length <= 125 - match opcode { - OpCode::Ping | OpCode::Pong if length > 125 => { - return Err(ProtocolError::InvalidLength(length)) - } - OpCode::Close if length > 125 => { - debug!("Received close frame with payload length exceeding 125. Morphing to protocol close frame."); - return Ok(Async::Ready(Some(Frame::default()))); - } - _ => (), - } - - // unmask - let data = if let Some(mask) = mask { - let mut buf = BytesMut::new(); - buf.extend_from_slice(&data); - apply_mask(&mut buf, mask); - buf.freeze() - } else { - data - }; - - Ok(Async::Ready(Some(Frame { - finished, - opcode, - payload: data.into(), - }))) - } - - /// Parse the payload of a close frame. - pub fn parse_close_payload(payload: &Binary) -> Option { - if payload.len() >= 2 { - let raw_code = NetworkEndian::read_u16(payload.as_ref()); - let code = CloseCode::from(raw_code); - let description = if payload.len() > 2 { - Some(String::from_utf8_lossy(&payload.as_ref()[2..]).into()) - } else { - None - }; - Some(CloseReason { code, description }) - } else { - None - } - } - - /// Generate binary representation - pub fn message>( - data: B, code: OpCode, finished: bool, genmask: bool, - ) -> FramedMessage { - let payload = data.into(); - let one: u8 = if finished { - 0x80 | Into::::into(code) - } else { - code.into() - }; - let payload_len = payload.len(); - let (two, p_len) = if genmask { - (0x80, payload_len + 4) - } else { - (0, payload_len) - }; - - let mut buf = if payload_len < 126 { - let mut buf = BytesMut::with_capacity(p_len + 2); - buf.put_slice(&[one, two | payload_len as u8]); - buf - } else if payload_len <= 65_535 { - let mut buf = BytesMut::with_capacity(p_len + 4); - buf.put_slice(&[one, two | 126]); - buf.put_u16_be(payload_len as u16); - buf - } else { - let mut buf = BytesMut::with_capacity(p_len + 10); - buf.put_slice(&[one, two | 127]); - buf.put_u64_be(payload_len as u64); - buf - }; - - let binary = if genmask { - let mask = rand::random::(); - buf.put_u32_le(mask); - buf.extend_from_slice(payload.as_ref()); - let pos = buf.len() - payload_len; - apply_mask(&mut buf[pos..], mask); - buf.into() - } else { - buf.put_slice(payload.as_ref()); - buf.into() - }; - - FramedMessage(binary) - } -} - -impl Default for Frame { - fn default() -> Frame { - Frame { - finished: true, - opcode: OpCode::Close, - payload: Binary::from(&b""[..]), - } - } -} - -impl fmt::Display for Frame { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - " - - final: {} - opcode: {} - payload length: {} - payload: 0x{} -", - self.finished, - self.opcode, - self.payload.len(), - self.payload - .as_ref() - .iter() - .map(|byte| format!("{:x}", byte)) - .collect::() - ) - } -} - -/// `WebSocket` message with framing. -#[derive(Debug)] -pub struct FramedMessage(pub(crate) Binary); - -#[cfg(test)] -mod tests { - use super::*; - use futures::stream::once; - - fn is_none(frm: &Poll, ProtocolError>) -> bool { - match *frm { - Ok(Async::Ready(None)) => true, - _ => false, - } - } - - fn extract(frm: Poll, ProtocolError>) -> Frame { - match frm { - Ok(Async::Ready(Some(frame))) => frame, - _ => unreachable!("error"), - } - } - - #[test] - fn test_parse() { - let mut buf = PayloadBuffer::new(once(Ok(BytesMut::from( - &[0b0000_0001u8, 0b0000_0001u8][..], - ).freeze()))); - assert!(is_none(&Frame::parse(&mut buf, false, 1024))); - - let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]); - buf.extend(b"1"); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - - let frame = extract(Frame::parse(&mut buf, false, 1024)); - assert!(!frame.finished); - assert_eq!(frame.opcode, OpCode::Text); - assert_eq!(frame.payload.as_ref(), &b"1"[..]); - } - - #[test] - fn test_parse_length0() { - let buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0000u8][..]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - - let frame = extract(Frame::parse(&mut buf, false, 1024)); - assert!(!frame.finished); - assert_eq!(frame.opcode, OpCode::Text); - assert!(frame.payload.is_empty()); - } - - #[test] - fn test_parse_length2() { - let buf = BytesMut::from(&[0b0000_0001u8, 126u8][..]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - assert!(is_none(&Frame::parse(&mut buf, false, 1024))); - - let mut buf = BytesMut::from(&[0b0000_0001u8, 126u8][..]); - buf.extend(&[0u8, 4u8][..]); - buf.extend(b"1234"); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - - let frame = extract(Frame::parse(&mut buf, false, 1024)); - assert!(!frame.finished); - assert_eq!(frame.opcode, OpCode::Text); - assert_eq!(frame.payload.as_ref(), &b"1234"[..]); - } - - #[test] - fn test_parse_length4() { - let buf = BytesMut::from(&[0b0000_0001u8, 127u8][..]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - assert!(is_none(&Frame::parse(&mut buf, false, 1024))); - - let mut buf = BytesMut::from(&[0b0000_0001u8, 127u8][..]); - buf.extend(&[0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 4u8][..]); - buf.extend(b"1234"); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - - let frame = extract(Frame::parse(&mut buf, false, 1024)); - assert!(!frame.finished); - assert_eq!(frame.opcode, OpCode::Text); - assert_eq!(frame.payload.as_ref(), &b"1234"[..]); - } - - #[test] - fn test_parse_frame_mask() { - let mut buf = BytesMut::from(&[0b0000_0001u8, 0b1000_0001u8][..]); - buf.extend(b"0001"); - buf.extend(b"1"); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - - assert!(Frame::parse(&mut buf, false, 1024).is_err()); - - let frame = extract(Frame::parse(&mut buf, true, 1024)); - assert!(!frame.finished); - assert_eq!(frame.opcode, OpCode::Text); - assert_eq!(frame.payload, vec![1u8].into()); - } - - #[test] - fn test_parse_frame_no_mask() { - let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]); - buf.extend(&[1u8]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - - assert!(Frame::parse(&mut buf, true, 1024).is_err()); - - let frame = extract(Frame::parse(&mut buf, false, 1024)); - assert!(!frame.finished); - assert_eq!(frame.opcode, OpCode::Text); - assert_eq!(frame.payload, vec![1u8].into()); - } - - #[test] - fn test_parse_frame_max_size() { - let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0010u8][..]); - buf.extend(&[1u8, 1u8]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - - assert!(Frame::parse(&mut buf, true, 1).is_err()); - - if let Err(ProtocolError::Overflow) = Frame::parse(&mut buf, false, 0) { - } else { - unreachable!("error"); - } - } - - #[test] - fn test_ping_frame() { - let frame = Frame::message(Vec::from("data"), OpCode::Ping, true, false); - - let mut v = vec![137u8, 4u8]; - v.extend(b"data"); - assert_eq!(frame.0, v.into()); - } - - #[test] - fn test_pong_frame() { - let frame = Frame::message(Vec::from("data"), OpCode::Pong, true, false); - - let mut v = vec![138u8, 4u8]; - v.extend(b"data"); - assert_eq!(frame.0, v.into()); - } - - #[test] - fn test_close_frame() { - let reason = (CloseCode::Normal, "data"); - let frame = Frame::close(Some(reason.into()), false); - - let mut v = vec![136u8, 6u8, 3u8, 232u8]; - v.extend(b"data"); - assert_eq!(frame.0, v.into()); - } - - #[test] - fn test_empty_close_frame() { - let frame = Frame::close(None, false); - assert_eq!(frame.0, vec![0x88, 0x00].into()); - } -} diff --git a/src/ws/mask.rs b/src/ws/mask.rs deleted file mode 100644 index 18ce57bb7..000000000 --- a/src/ws/mask.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! This is code from [Tungstenite project](https://github.com/snapview/tungstenite-rs) -#![cfg_attr(feature = "cargo-clippy", allow(cast_ptr_alignment))] -use std::ptr::copy_nonoverlapping; -use std::slice; - -// Holds a slice guaranteed to be shorter than 8 bytes -struct ShortSlice<'a>(&'a mut [u8]); - -impl<'a> ShortSlice<'a> { - unsafe fn new(slice: &'a mut [u8]) -> Self { - // Sanity check for debug builds - debug_assert!(slice.len() < 8); - ShortSlice(slice) - } - fn len(&self) -> usize { - self.0.len() - } -} - -/// Faster version of `apply_mask()` which operates on 8-byte blocks. -#[inline] -#[cfg_attr(feature = "cargo-clippy", allow(cast_lossless))] -pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) { - // Extend the mask to 64 bits - let mut mask_u64 = ((mask_u32 as u64) << 32) | (mask_u32 as u64); - // Split the buffer into three segments - let (head, mid, tail) = align_buf(buf); - - // Initial unaligned segment - let head_len = head.len(); - if head_len > 0 { - xor_short(head, mask_u64); - if cfg!(target_endian = "big") { - mask_u64 = mask_u64.rotate_left(8 * head_len as u32); - } else { - mask_u64 = mask_u64.rotate_right(8 * head_len as u32); - } - } - // Aligned segment - for v in mid { - *v ^= mask_u64; - } - // Final unaligned segment - if tail.len() > 0 { - xor_short(tail, mask_u64); - } -} - -#[inline] -// TODO: copy_nonoverlapping here compiles to call memcpy. While it is not so -// inefficient, it could be done better. The compiler does not understand that -// a `ShortSlice` must be smaller than a u64. -#[cfg_attr( - feature = "cargo-clippy", - allow(needless_pass_by_value) -)] -fn xor_short(buf: ShortSlice, mask: u64) { - // Unsafe: we know that a `ShortSlice` fits in a u64 - unsafe { - let (ptr, len) = (buf.0.as_mut_ptr(), buf.0.len()); - let mut b: u64 = 0; - #[allow(trivial_casts)] - copy_nonoverlapping(ptr, &mut b as *mut _ as *mut u8, len); - b ^= mask; - #[allow(trivial_casts)] - copy_nonoverlapping(&b as *const _ as *const u8, ptr, len); - } -} - -#[inline] -// Unsafe: caller must ensure the buffer has the correct size and alignment -unsafe fn cast_slice(buf: &mut [u8]) -> &mut [u64] { - // Assert correct size and alignment in debug builds - debug_assert!(buf.len().trailing_zeros() >= 3); - debug_assert!((buf.as_ptr() as usize).trailing_zeros() >= 3); - - slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u64, buf.len() >> 3) -} - -#[inline] -// Splits a slice into three parts: an unaligned short head and tail, plus an aligned -// u64 mid section. -fn align_buf(buf: &mut [u8]) -> (ShortSlice, &mut [u64], ShortSlice) { - let start_ptr = buf.as_ptr() as usize; - let end_ptr = start_ptr + buf.len(); - - // Round *up* to next aligned boundary for start - let start_aligned = (start_ptr + 7) & !0x7; - // Round *down* to last aligned boundary for end - let end_aligned = end_ptr & !0x7; - - if end_aligned >= start_aligned { - // We have our three segments (head, mid, tail) - let (tmp, tail) = buf.split_at_mut(end_aligned - start_ptr); - let (head, mid) = tmp.split_at_mut(start_aligned - start_ptr); - - // Unsafe: we know the middle section is correctly aligned, and the outer - // sections are smaller than 8 bytes - unsafe { (ShortSlice::new(head), cast_slice(mid), ShortSlice(tail)) } - } else { - // We didn't cross even one aligned boundary! - - // Unsafe: The outer sections are smaller than 8 bytes - unsafe { (ShortSlice::new(buf), &mut [], ShortSlice::new(&mut [])) } - } -} - -#[cfg(test)] -mod tests { - use super::apply_mask; - use byteorder::{ByteOrder, LittleEndian}; - - /// A safe unoptimized mask application. - fn apply_mask_fallback(buf: &mut [u8], mask: &[u8; 4]) { - for (i, byte) in buf.iter_mut().enumerate() { - *byte ^= mask[i & 3]; - } - } - - #[test] - fn test_apply_mask() { - let mask = [0x6d, 0xb6, 0xb2, 0x80]; - let mask_u32: u32 = LittleEndian::read_u32(&mask); - - let unmasked = vec![ - 0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, - 0x74, 0xf9, 0x12, 0x03, - ]; - - // Check masking with proper alignment. - { - let mut masked = unmasked.clone(); - apply_mask_fallback(&mut masked, &mask); - - let mut masked_fast = unmasked.clone(); - apply_mask(&mut masked_fast, mask_u32); - - assert_eq!(masked, masked_fast); - } - - // Check masking without alignment. - { - let mut masked = unmasked.clone(); - apply_mask_fallback(&mut masked[1..], &mask); - - let mut masked_fast = unmasked.clone(); - apply_mask(&mut masked_fast[1..], mask_u32); - - assert_eq!(masked, masked_fast); - } - } -} diff --git a/src/ws/mod.rs b/src/ws/mod.rs deleted file mode 100644 index b0942c0d3..000000000 --- a/src/ws/mod.rs +++ /dev/null @@ -1,477 +0,0 @@ -//! `WebSocket` support for Actix -//! -//! To setup a `WebSocket`, first do web socket handshake then on success -//! convert `Payload` into a `WsStream` stream and then use `WsWriter` to -//! communicate with the peer. -//! -//! ## Example -//! -//! ```rust -//! # extern crate actix_web; -//! # extern crate actix; -//! # use actix::prelude::*; -//! # use actix_web::*; -//! use actix_web::{ws, HttpRequest, HttpResponse}; -//! -//! // do websocket handshake and start actor -//! fn ws_index(req: &HttpRequest) -> Result { -//! ws::start(req, Ws) -//! } -//! -//! struct Ws; -//! -//! impl Actor for Ws { -//! type Context = ws::WebsocketContext; -//! } -//! -//! // Handler for ws::Message messages -//! impl StreamHandler for Ws { -//! fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { -//! match msg { -//! ws::Message::Ping(msg) => ctx.pong(&msg), -//! ws::Message::Text(text) => ctx.text(text), -//! ws::Message::Binary(bin) => ctx.binary(bin), -//! _ => (), -//! } -//! } -//! } -//! # -//! # fn main() { -//! # App::new() -//! # .resource("/ws/", |r| r.f(ws_index)) // <- register websocket route -//! # .finish(); -//! # } -//! ``` -use bytes::Bytes; -use futures::{Async, Poll, Stream}; -use http::{header, Method, StatusCode}; - -use super::actix::{Actor, StreamHandler}; - -use body::Binary; -use error::{Error, PayloadError, ResponseError}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::{ConnectionType, HttpResponse, HttpResponseBuilder}; -use payload::PayloadBuffer; - -mod client; -mod context; -mod frame; -mod mask; -mod proto; - -pub use self::client::{ - Client, ClientError, ClientHandshake, ClientReader, ClientWriter, -}; -pub use self::context::WebsocketContext; -pub use self::frame::{Frame, FramedMessage}; -pub use self::proto::{CloseCode, CloseReason, OpCode}; - -/// Websocket protocol errors -#[derive(Fail, Debug)] -pub enum ProtocolError { - /// Received an unmasked frame from client - #[fail(display = "Received an unmasked frame from client")] - UnmaskedFrame, - /// Received a masked frame from server - #[fail(display = "Received a masked frame from server")] - MaskedFrame, - /// Encountered invalid opcode - #[fail(display = "Invalid opcode: {}", _0)] - InvalidOpcode(u8), - /// Invalid control frame length - #[fail(display = "Invalid control frame length: {}", _0)] - InvalidLength(usize), - /// Bad web socket op code - #[fail(display = "Bad web socket op code")] - BadOpCode, - /// A payload reached size limit. - #[fail(display = "A payload reached size limit.")] - Overflow, - /// Continuation is not supported - #[fail(display = "Continuation is not supported.")] - NoContinuation, - /// Bad utf-8 encoding - #[fail(display = "Bad utf-8 encoding.")] - BadEncoding, - /// Payload error - #[fail(display = "Payload error: {}", _0)] - Payload(#[cause] PayloadError), -} - -impl ResponseError for ProtocolError {} - -impl From for ProtocolError { - fn from(err: PayloadError) -> ProtocolError { - ProtocolError::Payload(err) - } -} - -/// Websocket handshake errors -#[derive(Fail, PartialEq, Debug)] -pub enum HandshakeError { - /// Only get method is allowed - #[fail(display = "Method not allowed")] - GetMethodRequired, - /// Upgrade header if not set to websocket - #[fail(display = "Websocket upgrade is expected")] - NoWebsocketUpgrade, - /// Connection header is not set to upgrade - #[fail(display = "Connection upgrade is expected")] - NoConnectionUpgrade, - /// Websocket version header is not set - #[fail(display = "Websocket version header is required")] - NoVersionHeader, - /// Unsupported websocket version - #[fail(display = "Unsupported version")] - UnsupportedVersion, - /// Websocket key is not set or wrong - #[fail(display = "Unknown websocket key")] - BadWebsocketKey, -} - -impl ResponseError for HandshakeError { - fn error_response(&self) -> HttpResponse { - match *self { - HandshakeError::GetMethodRequired => HttpResponse::MethodNotAllowed() - .header(header::ALLOW, "GET") - .finish(), - HandshakeError::NoWebsocketUpgrade => HttpResponse::BadRequest() - .reason("No WebSocket UPGRADE header found") - .finish(), - HandshakeError::NoConnectionUpgrade => HttpResponse::BadRequest() - .reason("No CONNECTION upgrade") - .finish(), - HandshakeError::NoVersionHeader => HttpResponse::BadRequest() - .reason("Websocket version header is required") - .finish(), - HandshakeError::UnsupportedVersion => HttpResponse::BadRequest() - .reason("Unsupported version") - .finish(), - HandshakeError::BadWebsocketKey => HttpResponse::BadRequest() - .reason("Handshake error") - .finish(), - } - } -} - -/// `WebSocket` Message -#[derive(Debug, PartialEq, Message)] -pub enum Message { - /// Text message - Text(String), - /// Binary message - Binary(Binary), - /// Ping message - Ping(String), - /// Pong message - Pong(String), - /// Close message with optional reason - Close(Option), -} - -/// Do websocket handshake and start actor -pub fn start(req: &HttpRequest, actor: A) -> Result -where - A: Actor> + StreamHandler, - S: 'static, -{ - let mut resp = handshake(req)?; - let stream = WsStream::new(req.payload()); - - let body = WebsocketContext::create(req.clone(), actor, stream); - Ok(resp.body(body)) -} - -/// Prepare `WebSocket` handshake response. -/// -/// This function returns handshake `HttpResponse`, ready to send to peer. -/// It does not perform any IO. -/// -// /// `protocols` is a sequence of known protocols. On successful handshake, -// /// the returned response headers contain the first protocol in this list -// /// which the server also knows. -pub fn handshake( - req: &HttpRequest, -) -> Result { - // WebSocket accepts only GET - if *req.method() != Method::GET { - return Err(HandshakeError::GetMethodRequired); - } - - // Check for "UPGRADE" to websocket header - let has_hdr = if let Some(hdr) = req.headers().get(header::UPGRADE) { - if let Ok(s) = hdr.to_str() { - s.to_lowercase().contains("websocket") - } else { - false - } - } else { - false - }; - if !has_hdr { - return Err(HandshakeError::NoWebsocketUpgrade); - } - - // Upgrade connection - if !req.upgrade() { - return Err(HandshakeError::NoConnectionUpgrade); - } - - // check supported version - if !req.headers().contains_key(header::SEC_WEBSOCKET_VERSION) { - return Err(HandshakeError::NoVersionHeader); - } - let supported_ver = { - if let Some(hdr) = req.headers().get(header::SEC_WEBSOCKET_VERSION) { - hdr == "13" || hdr == "8" || hdr == "7" - } else { - false - } - }; - if !supported_ver { - return Err(HandshakeError::UnsupportedVersion); - } - - // check client handshake for validity - if !req.headers().contains_key(header::SEC_WEBSOCKET_KEY) { - return Err(HandshakeError::BadWebsocketKey); - } - let key = { - let key = req.headers().get(header::SEC_WEBSOCKET_KEY).unwrap(); - proto::hash_key(key.as_ref()) - }; - - Ok(HttpResponse::build(StatusCode::SWITCHING_PROTOCOLS) - .connection_type(ConnectionType::Upgrade) - .header(header::UPGRADE, "websocket") - .header(header::TRANSFER_ENCODING, "chunked") - .header(header::SEC_WEBSOCKET_ACCEPT, key.as_str()) - .take()) -} - -/// Maps `Payload` stream into stream of `ws::Message` items -pub struct WsStream { - rx: PayloadBuffer, - closed: bool, - max_size: usize, -} - -impl WsStream -where - S: Stream, -{ - /// Create new websocket frames stream - pub fn new(stream: S) -> WsStream { - WsStream { - rx: PayloadBuffer::new(stream), - closed: false, - max_size: 65_536, - } - } - - /// Set max frame size - /// - /// By default max size is set to 64kb - pub fn max_size(mut self, size: usize) -> Self { - self.max_size = size; - self - } -} - -impl Stream for WsStream -where - S: Stream, -{ - type Item = Message; - type Error = ProtocolError; - - fn poll(&mut self) -> Poll, Self::Error> { - if self.closed { - return Ok(Async::Ready(None)); - } - - match Frame::parse(&mut self.rx, true, self.max_size) { - Ok(Async::Ready(Some(frame))) => { - let (finished, opcode, payload) = frame.unpack(); - - // continuation is not supported - if !finished { - self.closed = true; - return Err(ProtocolError::NoContinuation); - } - - match opcode { - OpCode::Continue => Err(ProtocolError::NoContinuation), - OpCode::Bad => { - self.closed = true; - Err(ProtocolError::BadOpCode) - } - OpCode::Close => { - self.closed = true; - let close_reason = Frame::parse_close_payload(&payload); - Ok(Async::Ready(Some(Message::Close(close_reason)))) - } - OpCode::Ping => Ok(Async::Ready(Some(Message::Ping( - String::from_utf8_lossy(payload.as_ref()).into(), - )))), - OpCode::Pong => Ok(Async::Ready(Some(Message::Pong( - String::from_utf8_lossy(payload.as_ref()).into(), - )))), - OpCode::Binary => Ok(Async::Ready(Some(Message::Binary(payload)))), - OpCode::Text => { - let tmp = Vec::from(payload.as_ref()); - match String::from_utf8(tmp) { - Ok(s) => Ok(Async::Ready(Some(Message::Text(s)))), - Err(_) => { - self.closed = true; - Err(ProtocolError::BadEncoding) - } - } - } - } - } - Ok(Async::Ready(None)) => Ok(Async::Ready(None)), - Ok(Async::NotReady) => Ok(Async::NotReady), - Err(e) => { - self.closed = true; - Err(e) - } - } - } -} - -/// Common writing methods for a websocket. -pub trait WsWriter { - /// Send a text - fn send_text>(&mut self, text: T); - /// Send a binary - fn send_binary>(&mut self, data: B); - /// Send a ping message - fn send_ping(&mut self, message: &str); - /// Send a pong message - fn send_pong(&mut self, message: &str); - /// Close the connection - fn send_close(&mut self, reason: Option); -} - -#[cfg(test)] -mod tests { - use super::*; - use http::{header, Method}; - use test::TestRequest; - - #[test] - fn test_handshake() { - let req = TestRequest::default().method(Method::POST).finish(); - assert_eq!( - HandshakeError::GetMethodRequired, - handshake(&req).err().unwrap() - ); - - let req = TestRequest::default().finish(); - assert_eq!( - HandshakeError::NoWebsocketUpgrade, - handshake(&req).err().unwrap() - ); - - let req = TestRequest::default() - .header(header::UPGRADE, header::HeaderValue::from_static("test")) - .finish(); - assert_eq!( - HandshakeError::NoWebsocketUpgrade, - handshake(&req).err().unwrap() - ); - - let req = TestRequest::default() - .header( - header::UPGRADE, - header::HeaderValue::from_static("websocket"), - ).finish(); - assert_eq!( - HandshakeError::NoConnectionUpgrade, - handshake(&req).err().unwrap() - ); - - let req = TestRequest::default() - .header( - header::UPGRADE, - header::HeaderValue::from_static("websocket"), - ).header( - header::CONNECTION, - header::HeaderValue::from_static("upgrade"), - ).finish(); - assert_eq!( - HandshakeError::NoVersionHeader, - handshake(&req).err().unwrap() - ); - - let req = TestRequest::default() - .header( - header::UPGRADE, - header::HeaderValue::from_static("websocket"), - ).header( - header::CONNECTION, - header::HeaderValue::from_static("upgrade"), - ).header( - header::SEC_WEBSOCKET_VERSION, - header::HeaderValue::from_static("5"), - ).finish(); - assert_eq!( - HandshakeError::UnsupportedVersion, - handshake(&req).err().unwrap() - ); - - let req = TestRequest::default() - .header( - header::UPGRADE, - header::HeaderValue::from_static("websocket"), - ).header( - header::CONNECTION, - header::HeaderValue::from_static("upgrade"), - ).header( - header::SEC_WEBSOCKET_VERSION, - header::HeaderValue::from_static("13"), - ).finish(); - assert_eq!( - HandshakeError::BadWebsocketKey, - handshake(&req).err().unwrap() - ); - - let req = TestRequest::default() - .header( - header::UPGRADE, - header::HeaderValue::from_static("websocket"), - ).header( - header::CONNECTION, - header::HeaderValue::from_static("upgrade"), - ).header( - header::SEC_WEBSOCKET_VERSION, - header::HeaderValue::from_static("13"), - ).header( - header::SEC_WEBSOCKET_KEY, - header::HeaderValue::from_static("13"), - ).finish(); - assert_eq!( - StatusCode::SWITCHING_PROTOCOLS, - handshake(&req).unwrap().finish().status() - ); - } - - #[test] - fn test_wserror_http_response() { - let resp: HttpResponse = HandshakeError::GetMethodRequired.error_response(); - assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - let resp: HttpResponse = HandshakeError::NoWebsocketUpgrade.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: HttpResponse = HandshakeError::NoConnectionUpgrade.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: HttpResponse = HandshakeError::NoVersionHeader.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: HttpResponse = HandshakeError::UnsupportedVersion.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: HttpResponse = HandshakeError::BadWebsocketKey.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - } -} diff --git a/src/ws/proto.rs b/src/ws/proto.rs deleted file mode 100644 index 35fbbe3ee..000000000 --- a/src/ws/proto.rs +++ /dev/null @@ -1,318 +0,0 @@ -use base64; -use sha1; -use std::convert::{From, Into}; -use std::fmt; - -use self::OpCode::*; -/// Operation codes as part of rfc6455. -#[derive(Debug, Eq, PartialEq, Clone, Copy)] -pub enum OpCode { - /// Indicates a continuation frame of a fragmented message. - Continue, - /// Indicates a text data frame. - Text, - /// Indicates a binary data frame. - Binary, - /// Indicates a close control frame. - Close, - /// Indicates a ping control frame. - Ping, - /// Indicates a pong control frame. - Pong, - /// Indicates an invalid opcode was received. - Bad, -} - -impl fmt::Display for OpCode { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Continue => write!(f, "CONTINUE"), - Text => write!(f, "TEXT"), - Binary => write!(f, "BINARY"), - Close => write!(f, "CLOSE"), - Ping => write!(f, "PING"), - Pong => write!(f, "PONG"), - Bad => write!(f, "BAD"), - } - } -} - -impl Into for OpCode { - fn into(self) -> u8 { - match self { - Continue => 0, - Text => 1, - Binary => 2, - Close => 8, - Ping => 9, - Pong => 10, - Bad => { - debug_assert!( - false, - "Attempted to convert invalid opcode to u8. This is a bug." - ); - 8 // if this somehow happens, a close frame will help us tear down quickly - } - } - } -} - -impl From for OpCode { - fn from(byte: u8) -> OpCode { - match byte { - 0 => Continue, - 1 => Text, - 2 => Binary, - 8 => Close, - 9 => Ping, - 10 => Pong, - _ => Bad, - } - } -} - -use self::CloseCode::*; -/// Status code used to indicate why an endpoint is closing the `WebSocket` -/// connection. -#[derive(Debug, Eq, PartialEq, Clone, Copy)] -pub enum CloseCode { - /// Indicates a normal closure, meaning that the purpose for - /// which the connection was established has been fulfilled. - Normal, - /// Indicates that an endpoint is "going away", such as a server - /// going down or a browser having navigated away from a page. - Away, - /// Indicates that an endpoint is terminating the connection due - /// to a protocol error. - Protocol, - /// Indicates that an endpoint is terminating the connection - /// because it has received a type of data it cannot accept (e.g., an - /// endpoint that understands only text data MAY send this if it - /// receives a binary message). - Unsupported, - /// Indicates an abnormal closure. If the abnormal closure was due to an - /// error, this close code will not be used. Instead, the `on_error` method - /// of the handler will be called with the error. However, if the connection - /// is simply dropped, without an error, this close code will be sent to the - /// handler. - Abnormal, - /// Indicates that an endpoint is terminating the connection - /// because it has received data within a message that was not - /// consistent with the type of the message (e.g., non-UTF-8 [RFC3629] - /// data within a text message). - Invalid, - /// Indicates that an endpoint is terminating the connection - /// because it has received a message that violates its policy. This - /// is a generic status code that can be returned when there is no - /// other more suitable status code (e.g., Unsupported or Size) or if there - /// is a need to hide specific details about the policy. - Policy, - /// Indicates that an endpoint is terminating the connection - /// because it has received a message that is too big for it to - /// process. - Size, - /// Indicates that an endpoint (client) is terminating the - /// connection because it has expected the server to negotiate one or - /// more extension, but the server didn't return them in the response - /// message of the WebSocket handshake. The list of extensions that - /// are needed should be given as the reason for closing. - /// Note that this status code is not used by the server, because it - /// can fail the WebSocket handshake instead. - Extension, - /// Indicates that a server is terminating the connection because - /// it encountered an unexpected condition that prevented it from - /// fulfilling the request. - Error, - /// Indicates that the server is restarting. A client may choose to - /// reconnect, and if it does, it should use a randomized delay of 5-30 - /// seconds between attempts. - Restart, - /// Indicates that the server is overloaded and the client should either - /// connect to a different IP (when multiple targets exist), or - /// reconnect to the same IP when a user has performed an action. - Again, - #[doc(hidden)] - Tls, - #[doc(hidden)] - Other(u16), -} - -impl Into for CloseCode { - fn into(self) -> u16 { - match self { - Normal => 1000, - Away => 1001, - Protocol => 1002, - Unsupported => 1003, - Abnormal => 1006, - Invalid => 1007, - Policy => 1008, - Size => 1009, - Extension => 1010, - Error => 1011, - Restart => 1012, - Again => 1013, - Tls => 1015, - Other(code) => code, - } - } -} - -impl From for CloseCode { - fn from(code: u16) -> CloseCode { - match code { - 1000 => Normal, - 1001 => Away, - 1002 => Protocol, - 1003 => Unsupported, - 1006 => Abnormal, - 1007 => Invalid, - 1008 => Policy, - 1009 => Size, - 1010 => Extension, - 1011 => Error, - 1012 => Restart, - 1013 => Again, - 1015 => Tls, - _ => Other(code), - } - } -} - -#[derive(Debug, Eq, PartialEq, Clone)] -/// Reason for closing the connection -pub struct CloseReason { - /// Exit code - pub code: CloseCode, - /// Optional description of the exit code - pub description: Option, -} - -impl From for CloseReason { - fn from(code: CloseCode) -> Self { - CloseReason { - code, - description: None, - } - } -} - -impl> From<(CloseCode, T)> for CloseReason { - fn from(info: (CloseCode, T)) -> Self { - CloseReason { - code: info.0, - description: Some(info.1.into()), - } - } -} - -static WS_GUID: &'static str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - -// TODO: hash is always same size, we dont need String -pub(crate) fn hash_key(key: &[u8]) -> String { - let mut hasher = sha1::Sha1::new(); - - hasher.update(key); - hasher.update(WS_GUID.as_bytes()); - - base64::encode(&hasher.digest().bytes()) -} - -#[cfg(test)] -mod test { - #![allow(unused_imports, unused_variables, dead_code)] - use super::*; - - macro_rules! opcode_into { - ($from:expr => $opcode:pat) => { - match OpCode::from($from) { - e @ $opcode => (), - e => unreachable!("{:?}", e), - } - }; - } - - macro_rules! opcode_from { - ($from:expr => $opcode:pat) => { - let res: u8 = $from.into(); - match res { - e @ $opcode => (), - e => unreachable!("{:?}", e), - } - }; - } - - #[test] - fn test_to_opcode() { - opcode_into!(0 => OpCode::Continue); - opcode_into!(1 => OpCode::Text); - opcode_into!(2 => OpCode::Binary); - opcode_into!(8 => OpCode::Close); - opcode_into!(9 => OpCode::Ping); - opcode_into!(10 => OpCode::Pong); - opcode_into!(99 => OpCode::Bad); - } - - #[test] - fn test_from_opcode() { - opcode_from!(OpCode::Continue => 0); - opcode_from!(OpCode::Text => 1); - opcode_from!(OpCode::Binary => 2); - opcode_from!(OpCode::Close => 8); - opcode_from!(OpCode::Ping => 9); - opcode_from!(OpCode::Pong => 10); - } - - #[test] - #[should_panic] - fn test_from_opcode_debug() { - opcode_from!(OpCode::Bad => 99); - } - - #[test] - fn test_from_opcode_display() { - assert_eq!(format!("{}", OpCode::Continue), "CONTINUE"); - assert_eq!(format!("{}", OpCode::Text), "TEXT"); - assert_eq!(format!("{}", OpCode::Binary), "BINARY"); - assert_eq!(format!("{}", OpCode::Close), "CLOSE"); - assert_eq!(format!("{}", OpCode::Ping), "PING"); - assert_eq!(format!("{}", OpCode::Pong), "PONG"); - assert_eq!(format!("{}", OpCode::Bad), "BAD"); - } - - #[test] - fn closecode_from_u16() { - assert_eq!(CloseCode::from(1000u16), CloseCode::Normal); - assert_eq!(CloseCode::from(1001u16), CloseCode::Away); - assert_eq!(CloseCode::from(1002u16), CloseCode::Protocol); - assert_eq!(CloseCode::from(1003u16), CloseCode::Unsupported); - assert_eq!(CloseCode::from(1006u16), CloseCode::Abnormal); - assert_eq!(CloseCode::from(1007u16), CloseCode::Invalid); - assert_eq!(CloseCode::from(1008u16), CloseCode::Policy); - assert_eq!(CloseCode::from(1009u16), CloseCode::Size); - assert_eq!(CloseCode::from(1010u16), CloseCode::Extension); - assert_eq!(CloseCode::from(1011u16), CloseCode::Error); - assert_eq!(CloseCode::from(1012u16), CloseCode::Restart); - assert_eq!(CloseCode::from(1013u16), CloseCode::Again); - assert_eq!(CloseCode::from(1015u16), CloseCode::Tls); - assert_eq!(CloseCode::from(2000u16), CloseCode::Other(2000)); - } - - #[test] - fn closecode_into_u16() { - assert_eq!(1000u16, Into::::into(CloseCode::Normal)); - assert_eq!(1001u16, Into::::into(CloseCode::Away)); - assert_eq!(1002u16, Into::::into(CloseCode::Protocol)); - assert_eq!(1003u16, Into::::into(CloseCode::Unsupported)); - assert_eq!(1006u16, Into::::into(CloseCode::Abnormal)); - assert_eq!(1007u16, Into::::into(CloseCode::Invalid)); - assert_eq!(1008u16, Into::::into(CloseCode::Policy)); - assert_eq!(1009u16, Into::::into(CloseCode::Size)); - assert_eq!(1010u16, Into::::into(CloseCode::Extension)); - assert_eq!(1011u16, Into::::into(CloseCode::Error)); - assert_eq!(1012u16, Into::::into(CloseCode::Restart)); - assert_eq!(1013u16, Into::::into(CloseCode::Again)); - assert_eq!(1015u16, Into::::into(CloseCode::Tls)); - assert_eq!(2000u16, Into::::into(CloseCode::Other(2000))); - } -} diff --git a/tests/identity.pfx b/tests/identity.pfx deleted file mode 100644 index 946e3b8b8ae10e19a11e7ac6eead66b12fff0014..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5549 zcmY+GRZtv&vTbn*Fa-DD5S(Fv;O=h0gF_fx!{8nycyM=jcXvr}cXtMNxH(n#z4P8j zS68jQyT2EE0A2|kEIfMvo;?yO<4>8N_ZYCqu-O54MhF3T`v0&tdjMMWe%lL7KUFAFV5{u;owkU`~uKqm}O*fBSo5RVYIG` zPAKF6ePO1*(FhW)-QpFAvtO?cVWGD0hs0WO4>qy>9T48E19Q`LuD0r#V<$Vj_c2yp z)4}gHs(AF<^Bo{R_`YId+=%k!c;why`_I0GXxv)FEmD)RG7Vs?g`z@xC^k*0+`Ps8 zZQH$QkvVVeOE(P8=g*0kvj|2!A! zCaUuJ^XO0NPlE$=oQRK9ti`Ic7JJnq;osnZ4OA`G7m{`HKP{cH&`YLS z&7cXaq1TKaOG&uTJqqY25!-%6M82yrFSuJv{`JhJUOE4ZMT1J%h8u3pSuQ$qle0}C z2}0Lk)3OpsFm1yeJ1|XW9gt0oX1PIiJ+HG_ccQbIH}C6hb|aPpRO(@Rt|u~DJIX%l zC+^x;>&_>s!^v1hwDp-biu2rspT|oc!3;MUJ>ui2i(Lamzq&B9OCcN`j&N`^wTcd1 z`DFH$T83h&1Ws&{N(khR##{-N3RSG@_ZPyM3SQ_TAXRRqyV#LUkE0&kEvE8Y61{fKJ+Y= z$eQ*1oOaeF^Z9A#-r@E7LmI)arBxsyT>V+(Ruq)&tBqzOi1sjBwNjPe_jraq(=2r^ zB|%@2bxrAnZr&d!&MNXZn&!iHPAs)aINufHy*7>&8Ap}7vY+Ji590{9Gq+KBpjkN0 zeCWDYHbCrNc%062ljm(+!6eM>ku}@T8$$TYjIT%~Y3z>YH2FxhEJ6lUL_#W3!=?ks zFo969*bgk}a!GRqXMSjjgd&?6Y%hBVuhL~74jyLlXNnJzvi?lWEo_HD7ZkXXB+vYf z**da2rqVV>3tDz^j`grt{G5Ye8`Q@KgQelPxGF%zLkZ=m7N>eor9pQmK6B$F&s9TaFeP0w?b5Q2p`A;be~m;x!mwj;I4ye%g!Op2Z^tb4S}i z#o}zOP0;F#mw3YEF_5DmxuBJQ{MZN!`^i6PwkgfA(n$bs9PQx6aZB5p$2&d$+`VjO zsH)tO=SExUR825&T#?}~JqRDb1{y6YsTMn zYINH#Ru0n1kxJc|=rVjd@`uh*nzIv3n@lt8^$P6GC5xIW1?7XLu5&GtX$Ho%rp5@a z<;*{8D{Rd@ocNX}STxWU(2moCw*4dI^{&HccPIwaXF^%V^P@gTFF*ow(`z_5$lNR7B+?<1E> zULlb}pDx~mAAb5{pg@(IH|PW09Pz6r7!}<9>(fAn8=3nRL|EV^f5o-!1D@`uY_E0= zXWciaPcKF60&?K+MAnU0gz}SuZb>wVAeRY>Lz9(&Zu_rB_JiKSV8HOqO3;vSD41G~ zIYpO4JcJ5K0+;O6x0VlNvNY%;v*XCt2V>-|%pxixP0jfH+xE^8^D3ZXs zS4AQ;$|oz)t4F{7(j7F7l{R}lJhwq)ekCh$|7`g4CXbIghAm6OSZ1L&K5-Y1w$mQa z_MBdP-vu45dbf@y=ls^2$D{4YS(f+0N}W49&$hLSRWl_WZB;uN`i7GQSU2{> z%tc5FX16D4Hc~M^2Q-}r|Th>@2!HT<`AbzLpW6XWl;-{}i)bscjNo9~Z zt>K*BFR$SlC#sBVi?$v)Y?HUyl)E2s`8W6ZXD9n+OhlM3@csT_+o|7G5+{-kChNBE zGoihxT`ld2`0D_n8Kx;B37>D}D7${tb4|NGV4*rXJY< zrP!d-L?ATkL~#^D#VB&5mZjsJsQ(E>4kqY74x%)7DdB0tEho2OB+t;4_6jhwgA|cO znS?z?-;5Y{h$UXY8kALQOM~}sKX{KB4C#6j4aK&Qk9w{6xr6GoWSrQBZ-V^FUe|IuEu|!Ho zP~_-_Q3K`aH^E3h7qd#rjmRUmA*ZPMaLSD!4zIf>se1cE%=z*bSCvx#B3|QUR$0#m9GAZ3L&y-$F1DIC& zT8gc<4SckFy#eY%&9U^Kz5>cD`L{z)t6x+lxMS@K!T}DzX*QtVExm3-I%(wjkMP#> z1(Cx)JgRslsAW|p`13(`X}#T<>eipuNW4PA4R2-@N+Vc&(y87pk_BSeS+d6)P$^TP z`?$x%WPFPWh9eb*vW#gb#gty{p~gAkXo)>T5Uf}c#r*~bw?jGhA9M7i8I5Xoby}f^ z^p4Qzi_ghn7*)HR6CmO5t+f(DYnCA3GX_!BFzYWFViu9Jn~Rh{W4lS>Ien~yruGbu z>(;O4jayyFSMEP1yQ9A{eQ^Hh(X@0+auu!ON=}-efn0d?U5LMh$zI#vo9nqvQ$058 z0%`dSn#OpIe|FctVo1fv^b^C(=MqC(usOYfc9zoBNAJEY_F!S;aKK8MEaXZ2?J0*q z%9PBkwW53|md4e|UjFDs!BwW$KTztoZy9^)QA!(U)itkV!h$GWP*eYJRt+x@x5V%i z1J7`#SEWSE_@>C3i32lz2g3U@e}8X+mAz+L`pS5%*YB7Dxr&GE4t*>KB6I zE-@2=x9^2U&Ux`Z5<+-#U+{a@#CzMU?Leqv9AhmP6iaWp>Oq!NsxHbS=EW1!zJLz{ zMUGYEB7n6q3Er#o<|Z`u0MwrUN4&EGP-_taP%Ho8(tlHkg!X?l`~xi9ztHXDeK*m&V~nDC|pLVujbH(b=fk1EfnQhXPjX1C48B*(7^BO>cFO~ zAH*SPpf#PD###N!E9oD!h87(U5h5>lHJLd?LymUDW$S z^HqD#EbaAcgn)aq3MVH(f*KmTMjHO5dW|_+katLspc=S!v^H3wli*n6ojf^qf3K=y zKr%Yay%^*Xvh0;}J@-TW@bcRzax<}Q{(@FjI+hfy@)|L*hscS(TttTvz_%Nv+(z8GGr?Ic>si}%oD zjS`B@EE+;f!(VuH>B7DBvpXijW%<{YjhQlo9fhwvuO{nH%@)BNiq<1`YY&2FCcm1v z)V&H!Kcj^3Bt0y%<I%;IH=P zJ41oky-lS{I9?XOY~Id^5UX<7RGwt!+GLyLENxHkj-snrrD9wg(faIi6I{jBh1go{ zM*ViN9ui7wip-=5;2)#r3!%>8z?BacE<1_@ALhkFNVIX%-ABNFWSr=jiLQ6%-{KRS zWwq<$wd1t!5~>t;g)EsTUt{vq;Fq-VE-~ml6ZXxgTNfUz_@@T7 zjsLv05k4~6I(7sOoX;Me9Mx*!2n`*&G62*4lL#eRXMj)}# zKr96@p5g@T(q@%oV#NRfPt?05WUo1wQk&aO{Pmwj7rdpbgcv6`Z78Sujv^Cw!}t9r`^Lc8t$WvLza7iBfkmc zDDY9Tit~6VzqS0L=Ayqpm%buj8Ag(=8<}4__o+jk*7sRDS~t>ecLxvu9W`08b;!5* zVykt!s+BvMXq)=q=y6Z4d=}r*-a;}j-*Qyy?4M4cOLy-!>N4}f4H%^$+ju=$d;iJ7 zPw8!Js)`{-TTglxv%Sm^K45gm#!5u2ni5W%dv9_+kEJG*<)*>LnTIO3 zP6HOD&&FrUjtF+byLCKHD-ACb%_vi{l0xPj|E3=MNnc)wn90{jb#I)vL$jsxtHlPu zCN(WOd)y*If;(Pc2D_T*H8TGC6#FjyA&L=1eB$U3^IK?5o$tR7-J`4e7hlpb7w~C+Agyessj{qzudSzQHf)p zE3bG;_o5JE@|R;^XOzYSMRqL`ey& z7cxd^&=J@~sxSdNJ~4`WKG3Pf5(-q%1&@Bp@h^T2p}7IVJL}{Ir#}ZYrY(`qOdx+V z!4-keXG9fPwZ72Z>0InsQ!U51)YwT!#5(o7Q73y)O7Vj^xant+Mw#45BH$9?B1vV- zK-S5Bmx_T9S^3JTc)fx@Q&R72RSU{u74roB{&3hlzknbj{dy%m?DxD5GH*40Tk$q; zDxcEj{-~XXePdPnzs!=S2lkV$IweaW8lBM@z7iZlAu5N%pYus=+D*DQyFs=GYfXl#{yYa^Ur z^wdzws}mB6Y~FeN1FIrbx)Pe=LH~}x^wgbl7Mnn)HlS|~MKWejJ*=#vF+_%p7!)FC z*>GliQit=X=~)<^UO-kl>$hKAhy@PbN%T6E8AFDRIPqP?3nKbu9SC1*x-_q%oa>un z)jigLjSg;Ie=Sf4&{X0(O6asc*}f}KP)&dmbcEmjIu<#h|51e3-$TXpM$ACpy4wx^ z7~&b%^6mSNlP@DXnZzWuf4O5+N;qfkl$8#UJw?Z-lL=nl=k*Qj5K!X_jNe5*ceN|1 zJO(G;HL??jza#IvW7CL&O%M}9wD`Q<7M?twO`H?+^WCB~JG8Y5`NHaesh58pW(2{G0QEPc8si#*pBZg zbmb#%Lx#-6A2aE}_`5AxI8xhZ#FL@NXPYMwK!#-*abrgDL>W=V)NBx_S5!i=ATh~B zoT!rujGkse$2dK9!A$E8ILE})=TNqjus$Y5VZgwNlvHx9Q@B&7X378MKMHcNc9XmD z{1dA1N8T7+{i%3V@mO!?q$Lgg=zTVwG9vk)dN9s4cwe diff --git a/tests/test space.binary b/tests/test space.binary deleted file mode 100644 index ef8ff0245..000000000 --- a/tests/test space.binary +++ /dev/null @@ -1 +0,0 @@ -ÂTǑɂVù2þvI ª–\ÇRË™–ˆæeÞvDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ.– û®õpA !ûߦÙx j+Uc÷±©X”c%Û;ï"yì­AI \ No newline at end of file diff --git a/tests/test.binary b/tests/test.binary deleted file mode 100644 index ef8ff0245..000000000 --- a/tests/test.binary +++ /dev/null @@ -1 +0,0 @@ -ÂTǑɂVù2þvI ª–\ÇRË™–ˆæeÞvDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ.– û®õpA !ûߦÙx j+Uc÷±©X”c%Û;ï"yì­AI \ No newline at end of file diff --git a/tests/test.png b/tests/test.png deleted file mode 100644 index 6b7cdc0b8fb5a439d3bd19f210e89a37a2354e61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1SFYWcSQjy&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=h((rL&%EBHDibIqn;8;O;+&tGo0?Yw HttpResponse::Ok().finish(), - None => HttpResponse::BadRequest().finish(), - }) - }); - - let request = srv.get().uri(srv.url("/?qp=5").as_str()).finish().unwrap(); - - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_no_decompress() { - let mut srv = - test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); - - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - - let mut e = GzDecoder::new(&bytes[..]); - let mut dec = Vec::new(); - e.read_to_end(&mut dec).unwrap(); - assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); - - // POST - let request = srv.post().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - - let bytes = srv.execute(response.body()).unwrap(); - let mut e = GzDecoder::new(&bytes[..]); - let mut dec = Vec::new(); - e.read_to_end(&mut dec).unwrap(); - assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_client_gzip_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Deflate) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Gzip) - .body(STR) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_client_gzip_encoding_large() { - let data = STR.repeat(10); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Deflate) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Gzip) - .body(data.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_client_gzip_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(100_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Deflate) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Gzip) - .body(data.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(all(unix, feature = "uds"))] -#[test] -fn test_compatible_with_unix_socket_stream() { - let (stream, _) = tokio_uds::UnixStream::pair().unwrap(); - let _ = client::Connection::from_stream(stream); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_client_brotli_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .client(http::Method::POST, "/") - .content_encoding(http::ContentEncoding::Br) - .body(STR) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_client_brotli_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(70_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(move |bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .client(http::Method::POST, "/") - .content_encoding(http::ContentEncoding::Br) - .body(data.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_client_deflate_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Br) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Deflate) - .body(STR) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_client_deflate_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(70_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Br) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Deflate) - .body(data.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_client_streaming_explicit() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .map_err(Error::from) - .and_then(|body| { - Ok(HttpResponse::Ok() - .chunked() - .content_encoding(http::ContentEncoding::Identity) - .body(body)) - }).responder() - }) - }); - - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - - let request = srv.get().body(Body::Streaming(Box::new(body))).unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_body_streaming_implicit() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(Body::Streaming(Box::new(body))) - }) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_client_cookie_handling() { - use actix_web::http::Cookie; - fn err() -> Error { - use std::io::{Error as IoError, ErrorKind}; - // stub some generic error - Error::from(IoError::from(ErrorKind::NotFound)) - } - let cookie1 = Cookie::build("cookie1", "value1").finish(); - let cookie2 = Cookie::build("cookie2", "value2") - .domain("www.example.org") - .path("/") - .secure(true) - .http_only(true) - .finish(); - // Q: are all these clones really necessary? A: Yes, possibly - let cookie1b = cookie1.clone(); - let cookie2b = cookie2.clone(); - let mut srv = test::TestServer::new(move |app| { - let cookie1 = cookie1b.clone(); - let cookie2 = cookie2b.clone(); - app.handler(move |req: &HttpRequest| { - // Check cookies were sent correctly - req.cookie("cookie1") - .ok_or_else(err) - .and_then(|c1| { - if c1.value() == "value1" { - Ok(()) - } else { - Err(err()) - } - }).and_then(|()| req.cookie("cookie2").ok_or_else(err)) - .and_then(|c2| { - if c2.value() == "value2" { - Ok(()) - } else { - Err(err()) - } - }) - // Send some cookies back - .map(|_| { - HttpResponse::Ok() - .cookie(cookie1.clone()) - .cookie(cookie2.clone()) - .finish() - }) - }) - }); - - let request = srv - .get() - .cookie(cookie1.clone()) - .cookie(cookie2.clone()) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - let c1 = response.cookie("cookie1").expect("Missing cookie1"); - assert_eq!(c1, cookie1); - let c2 = response.cookie("cookie2").expect("Missing cookie2"); - assert_eq!(c2, cookie2); -} - -#[test] -fn test_default_headers() { - let srv = test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); - - let request = srv.get().finish().unwrap(); - let repr = format!("{:?}", request); - assert!(repr.contains("\"accept-encoding\": \"gzip, deflate\"")); - assert!(repr.contains(concat!( - "\"user-agent\": \"actix-web/", - env!("CARGO_PKG_VERSION"), - "\"" - ))); - - let request_override = srv - .get() - .header("User-Agent", "test") - .header("Accept-Encoding", "over_test") - .finish() - .unwrap(); - let repr_override = format!("{:?}", request_override); - assert!(repr_override.contains("\"user-agent\": \"test\"")); - assert!(repr_override.contains("\"accept-encoding\": \"over_test\"")); - assert!(!repr_override.contains("\"accept-encoding\": \"gzip, deflate\"")); - assert!(!repr_override.contains(concat!( - "\"user-agent\": \"Actix-web/", - env!("CARGO_PKG_VERSION"), - "\"" - ))); -} - -#[test] -fn client_read_until_eof() { - let addr = test::TestServer::unused_addr(); - - thread::spawn(move || { - let lst = net::TcpListener::bind(addr).unwrap(); - - for stream in lst.incoming() { - let mut stream = stream.unwrap(); - let mut b = [0; 1000]; - let _ = stream.read(&mut b).unwrap(); - let _ = stream - .write_all(b"HTTP/1.1 200 OK\r\nconnection: close\r\n\r\nwelcome!"); - } - }); - - let mut sys = actix::System::new("test"); - - // client request - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = sys.block_on(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"welcome!")); -} diff --git a/tests/test_custom_pipeline.rs b/tests/test_custom_pipeline.rs deleted file mode 100644 index 6b5df00e3..000000000 --- a/tests/test_custom_pipeline.rs +++ /dev/null @@ -1,81 +0,0 @@ -extern crate actix; -extern crate actix_net; -extern crate actix_web; - -use std::{thread, time}; - -use actix::System; -use actix_net::server::Server; -use actix_net::service::NewServiceExt; -use actix_web::server::{HttpService, KeepAlive, ServiceConfig, StreamConfiguration}; -use actix_web::{client, http, test, App, HttpRequest}; - -#[test] -fn test_custom_pipeline() { - let addr = test::TestServer::unused_addr(); - - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - let app = App::new() - .route("/", http::Method::GET, |_: HttpRequest| "OK") - .finish(); - let settings = ServiceConfig::build(app) - .keep_alive(KeepAlive::Disabled) - .client_timeout(1000) - .client_shutdown(1000) - .server_hostname("localhost") - .server_address(addr) - .finish(); - - StreamConfiguration::new() - .nodelay(true) - .tcp_keepalive(Some(time::Duration::from_secs(10))) - .and_then(HttpService::new(settings)) - }).unwrap() - .run(); - }); - - let mut sys = System::new("test"); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } -} - -#[test] -fn test_h1() { - use actix_web::server::H1Service; - - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - let app = App::new() - .route("/", http::Method::GET, |_: HttpRequest| "OK") - .finish(); - let settings = ServiceConfig::build(app) - .keep_alive(KeepAlive::Disabled) - .client_timeout(1000) - .client_shutdown(1000) - .server_hostname("localhost") - .server_address(addr) - .finish(); - - H1Service::new(settings) - }).unwrap() - .run(); - }); - - let mut sys = System::new("test"); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } -} diff --git a/tests/test_handlers.rs b/tests/test_handlers.rs deleted file mode 100644 index debc1626a..000000000 --- a/tests/test_handlers.rs +++ /dev/null @@ -1,677 +0,0 @@ -extern crate actix; -extern crate actix_web; -extern crate bytes; -extern crate futures; -extern crate h2; -extern crate http; -extern crate tokio_timer; -#[macro_use] -extern crate serde_derive; -extern crate serde_json; - -use std::io; -use std::time::{Duration, Instant}; - -use actix_web::*; -use bytes::Bytes; -use futures::Future; -use http::StatusCode; -use serde_json::Value; -use tokio_timer::Delay; - -#[derive(Deserialize)] -struct PParam { - username: String, -} - -#[test] -fn test_path_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.with(|p: Path| format!("Welcome {}!", p.username)) - }); - }); - - // client request - let request = srv.get().uri(srv.url("/test/index.html")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test!")); -} - -#[test] -fn test_async_handler() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|p: Path| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| Ok(format!("Welcome {}!", p.username))) - .responder() - }) - }); - }); - - // client request - let request = srv.get().uri(srv.url("/test/index.html")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test!")); -} - -#[test] -fn test_query_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/index.html", |r| { - r.with(|p: Query| format!("Welcome {}!", p.username)) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/index.html?username=test")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test!")); - - // client request - let request = srv.get().uri(srv.url("/index.html")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[derive(Deserialize, Debug)] -pub enum ResponseType { - Token, - Code, -} - -#[derive(Debug, Deserialize)] -pub struct AuthRequest { - id: u64, - response_type: ResponseType, -} - -#[test] -fn test_query_enum_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/index.html", |r| { - r.with(|p: Query| format!("{:?}", p.into_inner())) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/index.html?id=64&response_type=Code")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!( - bytes, - Bytes::from_static(b"AuthRequest { id: 64, response_type: Code }") - ); - - let request = srv - .get() - .uri(srv.url("/index.html?id=64&response_type=Co")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - - let request = srv - .get() - .uri(srv.url("/index.html?response_type=Code")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_async_extractor_async() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|data: Json| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| Ok(format!("{}", data.0))) - .responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"{\"test\":1}")); -} - -#[derive(Deserialize, Serialize)] -struct FormData { - username: String, -} - -#[test] -fn test_form_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route() - .with(|form: Form| format!("{}", form.username)) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .form(FormData { - username: "test".to_string(), - }).unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"test")); -} - -#[test] -fn test_form_extractor2() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with_config( - |form: Form| format!("{}", form.username), - |cfg| { - cfg.0.error_handler(|err, _| { - error::InternalError::from_response( - err, - HttpResponse::Conflict().finish(), - ).into() - }); - }, - ); - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .header("content-type", "application/x-www-form-urlencoded") - .body("918237129hdk:D:D:D:D:D:DjASHDKJhaswkjeq") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_client_error()); -} - -#[test] -fn test_path_and_query_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|(p, q): (Path, Query)| { - format!("Welcome {} - {}!", p.username, q.username) - }) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html?username=test2")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - test2!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_path_and_query_extractor2() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route() - .with(|(_r, p, q): (HttpRequest, Path, Query)| { - format!("Welcome {} - {}!", p.username, q.username) - }) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html?username=test2")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - test2!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_path_and_query_extractor2_async() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with( - |(p, _q, data): (Path, Query, Json)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }, - ) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); -} - -#[test] -fn test_path_and_query_extractor3_async() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|(p, data): (Path, Json)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_path_and_query_extractor4_async() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|(data, p): (Json, Path)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_path_and_query_extractor2_async2() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with( - |(p, data, _q): (Path, Json, Query)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }, - ) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_path_and_query_extractor2_async3() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route() - .with(|data: Json, p: Path, _: Query| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_path_and_query_extractor2_async4() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route() - .with(|data: (Json, Path, Query)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", data.1.username, (data.0).0)) - }).responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_scope_and_path_extractor() { - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/sc", |scope| { - scope.resource("/{num}/index.html", |r| { - r.route() - .with(|p: Path<(usize,)>| format!("Welcome {}!", p.0)) - }) - }) - }); - - // client request - let request = srv - .get() - .uri(srv.url("/sc/10/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome 10!")); - - // client request - let request = srv - .get() - .uri(srv.url("/sc/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); -} - -#[test] -fn test_nested_scope_and_path_extractor() { - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/sc", |scope| { - scope.nested("/{num}", |scope| { - scope.resource("/{num}/index.html", |r| { - r.route().with(|p: Path<(usize, usize)>| { - format!("Welcome {} {}!", p.0, p.1) - }) - }) - }) - }) - }); - - // client request - let request = srv - .get() - .uri(srv.url("/sc/10/12/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome 10 12!")); - - // client request - let request = srv - .get() - .uri(srv.url("/sc/10/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); -} - -#[cfg(actix_impl_trait)] -fn test_impl_trait( - data: (Json, Path, Query), -) -> impl Future { - Delay::new(Instant::now() + Duration::from_millis(10)) - .map_err(|_| io::Error::new(io::ErrorKind::Other, "timeout")) - .and_then(move |_| Ok(format!("Welcome {} - {}!", data.1.username, (data.0).0))) -} - -#[cfg(actix_impl_trait)] -fn test_impl_trait_err( - _data: (Json, Path, Query), -) -> impl Future { - Delay::new(Instant::now() + Duration::from_millis(10)) - .map_err(|_| io::Error::new(io::ErrorKind::Other, "timeout")) - .and_then(move |_| Err(io::Error::new(io::ErrorKind::Other, "other"))) -} - -#[cfg(actix_impl_trait)] -#[test] -fn test_path_and_query_extractor2_async4_impl_trait() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with_async(test_impl_trait) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[cfg(actix_impl_trait)] -#[test] -fn test_path_and_query_extractor2_async4_impl_trait_err() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with_async(test_impl_trait_err) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); -} - -#[test] -fn test_non_ascii_route() { - let mut srv = test::TestServer::new(|app| { - app.resource("/中文/index.html", |r| r.f(|_| "success")); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/中文/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"success")); -} - -#[test] -fn test_unsafe_path_route() { - let mut srv = test::TestServer::new(|app| { - app.resource("/test/{url}", |r| { - r.f(|r| format!("success: {}", &r.match_info()["url"])) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/test/http%3A%2F%2Fexample.com")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!( - bytes, - Bytes::from_static(b"success: http%3A%2F%2Fexample.com") - ); -} diff --git a/tests/test_middleware.rs b/tests/test_middleware.rs deleted file mode 100644 index 6cb6ee363..000000000 --- a/tests/test_middleware.rs +++ /dev/null @@ -1,1055 +0,0 @@ -extern crate actix; -extern crate actix_web; -extern crate futures; -extern crate tokio_timer; - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::thread; -use std::time::{Duration, Instant}; - -use actix_web::error::{Error, ErrorInternalServerError}; -use actix_web::*; -use futures::{future, Future}; -use tokio_timer::Delay; - -struct MiddlewareTest { - start: Arc, - response: Arc, - finish: Arc, -} - -impl middleware::Middleware for MiddlewareTest { - fn start(&self, _: &HttpRequest) -> Result { - self.start - .store(self.start.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - Ok(middleware::Started::Done) - } - - fn response( - &self, _: &HttpRequest, resp: HttpResponse, - ) -> Result { - self.response - .store(self.response.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - Ok(middleware::Response::Done(resp)) - } - - fn finish(&self, _: &HttpRequest, _: &HttpResponse) -> middleware::Finished { - self.finish - .store(self.finish.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - middleware::Finished::Done - } -} - -#[test] -fn test_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_resource_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_scope_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_middleware_async_handler() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new() - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/", |r| { - r.route().a(|_| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(|_| Ok(HttpResponse::Ok())) - }) - }) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_async_handler() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", |r| { - r.middleware(mw); - r.route().a(|_| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(|_| Ok(HttpResponse::Ok())) - }) - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_async_handler() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| { - r.route().a(|_| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(|_| Ok(HttpResponse::Ok())) - }) - }) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -fn index_test_middleware_async_error(_: &HttpRequest) -> FutureResponse { - future::result(Err(error::ErrorBadRequest("TEST"))).responder() -} - -#[test] -fn test_middleware_async_error() { - let req = Arc::new(AtomicUsize::new(0)); - let resp = Arc::new(AtomicUsize::new(0)); - let fin = Arc::new(AtomicUsize::new(0)); - - let act_req = Arc::clone(&req); - let act_resp = Arc::clone(&resp); - let act_fin = Arc::clone(&fin); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_req), - response: Arc::clone(&act_resp), - finish: Arc::clone(&act_fin), - }).handler(index_test_middleware_async_error) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), http::StatusCode::BAD_REQUEST); - - assert_eq!(req.load(Ordering::Relaxed), 1); - assert_eq!(resp.load(Ordering::Relaxed), 1); - assert_eq!(fin.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_async_error() { - let req = Arc::new(AtomicUsize::new(0)); - let resp = Arc::new(AtomicUsize::new(0)); - let fin = Arc::new(AtomicUsize::new(0)); - - let act_req = Arc::clone(&req); - let act_resp = Arc::clone(&resp); - let act_fin = Arc::clone(&fin); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_req), - response: Arc::clone(&act_resp), - finish: Arc::clone(&act_fin), - }).resource("/test", |r| r.f(index_test_middleware_async_error)) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), http::StatusCode::BAD_REQUEST); - - assert_eq!(req.load(Ordering::Relaxed), 1); - assert_eq!(resp.load(Ordering::Relaxed), 1); - assert_eq!(fin.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_async_error() { - let req = Arc::new(AtomicUsize::new(0)); - let resp = Arc::new(AtomicUsize::new(0)); - let fin = Arc::new(AtomicUsize::new(0)); - - let act_req = Arc::clone(&req); - let act_resp = Arc::clone(&resp); - let act_fin = Arc::clone(&fin); - - let mut srv = test::TestServer::with_factory(move || { - let mw = MiddlewareAsyncTest { - start: Arc::clone(&act_req), - response: Arc::clone(&act_resp), - finish: Arc::clone(&act_fin), - }; - - App::new().resource("/test", move |r| { - r.middleware(mw); - r.f(index_test_middleware_async_error); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), http::StatusCode::BAD_REQUEST); - - assert_eq!(req.load(Ordering::Relaxed), 1); - assert_eq!(resp.load(Ordering::Relaxed), 1); - assert_eq!(fin.load(Ordering::Relaxed), 1); -} - -struct MiddlewareAsyncTest { - start: Arc, - response: Arc, - finish: Arc, -} - -impl middleware::Middleware for MiddlewareAsyncTest { - fn start(&self, _: &HttpRequest) -> Result { - let to = Delay::new(Instant::now() + Duration::from_millis(10)); - - let start = Arc::clone(&self.start); - Ok(middleware::Started::Future(Box::new( - to.from_err().and_then(move |_| { - start.fetch_add(1, Ordering::Relaxed); - Ok(None) - }), - ))) - } - - fn response( - &self, _: &HttpRequest, resp: HttpResponse, - ) -> Result { - let to = Delay::new(Instant::now() + Duration::from_millis(10)); - - let response = Arc::clone(&self.response); - Ok(middleware::Response::Future(Box::new( - to.from_err().and_then(move |_| { - response.fetch_add(1, Ordering::Relaxed); - Ok(resp) - }), - ))) - } - - fn finish(&self, _: &HttpRequest, _: &HttpResponse) -> middleware::Finished { - let to = Delay::new(Instant::now() + Duration::from_millis(10)); - - let finish = Arc::clone(&self.finish); - middleware::Finished::Future(Box::new(to.from_err().and_then(move |_| { - finish.fetch_add(1, Ordering::Relaxed); - Ok(()) - }))) - } -} - -#[test] -fn test_async_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_async_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new() - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(50)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_sync_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new() - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(50)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_scope_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_async_scope_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_async_scope_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_resource_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - - thread::sleep(Duration::from_millis(40)); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_async_resource_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - let mw2 = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw1); - r.middleware(mw2); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(40)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_sync_resource_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - let mw2 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw1); - r.middleware(mw2); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(40)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -struct MiddlewareWithErr; - -impl middleware::Middleware for MiddlewareWithErr { - fn start(&self, _: &HttpRequest) -> Result { - Err(ErrorInternalServerError("middleware error")) - } -} - -struct MiddlewareAsyncWithErr; - -impl middleware::Middleware for MiddlewareAsyncWithErr { - fn start(&self, _: &HttpRequest) -> Result { - Ok(middleware::Started::Future(Box::new(future::err( - ErrorInternalServerError("middleware error"), - )))) - } -} - -#[test] -fn test_middleware_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new() - .middleware(mw1) - .middleware(MiddlewareWithErr) - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_middleware_async_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new() - .middleware(mw1) - .middleware(MiddlewareAsyncWithErr) - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().scope("/scope", |scope| { - scope - .middleware(mw1) - .middleware(MiddlewareWithErr) - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_async_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().scope("/scope", |scope| { - scope - .middleware(mw1) - .middleware(MiddlewareAsyncWithErr) - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw1); - r.middleware(MiddlewareWithErr); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_async_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw1); - r.middleware(MiddlewareAsyncWithErr); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[cfg(feature = "session")] -#[test] -fn test_session_storage_middleware() { - use actix_web::middleware::session::{ - CookieSessionBackend, RequestSession, SessionStorage, - }; - - const SIMPLE_NAME: &'static str = "simple"; - const SIMPLE_PAYLOAD: &'static str = "kantan"; - const COMPLEX_NAME: &'static str = "test"; - const COMPLEX_PAYLOAD: &'static str = "url=https://test.com&generate_204"; - //TODO: investigate how to handle below input - //const COMPLEX_PAYLOAD: &'static str = "FJc%26continue_url%3Dhttp%253A%252F%252Fconnectivitycheck.gstatic.com%252Fgenerate_204"; - - let mut srv = test::TestServer::with_factory(move || { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/index", move |r| { - r.f(|req| { - let res = req.session().set(COMPLEX_NAME, COMPLEX_PAYLOAD); - assert!(res.is_ok()); - let value = req.session().get::(COMPLEX_NAME); - assert!(value.is_ok()); - let value = value.unwrap(); - assert!(value.is_some()); - assert_eq!(value.unwrap(), COMPLEX_PAYLOAD); - - let res = req.session().set(SIMPLE_NAME, SIMPLE_PAYLOAD); - assert!(res.is_ok()); - let value = req.session().get::(SIMPLE_NAME); - assert!(value.is_ok()); - let value = value.unwrap(); - assert!(value.is_some()); - assert_eq!(value.unwrap(), SIMPLE_PAYLOAD); - - HttpResponse::Ok() - }) - }).resource("/expect_cookie", move |r| { - r.f(|req| { - let _cookies = req.cookies().expect("To get cookies"); - - let value = req.session().get::(SIMPLE_NAME); - assert!(value.is_ok()); - let value = value.unwrap(); - assert!(value.is_some()); - assert_eq!(value.unwrap(), SIMPLE_PAYLOAD); - - let value = req.session().get::(COMPLEX_NAME); - assert!(value.is_ok()); - let value = value.unwrap(); - assert!(value.is_some()); - assert_eq!(value.unwrap(), COMPLEX_PAYLOAD); - - HttpResponse::Ok() - }) - }) - }); - - let request = srv.get().uri(srv.url("/index")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - - assert!(response.headers().contains_key("set-cookie")); - let set_cookie = response.headers().get("set-cookie"); - assert!(set_cookie.is_some()); - let set_cookie = set_cookie.unwrap().to_str().expect("Convert to str"); - - let request = srv - .get() - .uri(srv.url("/expect_cookie")) - .header("cookie", set_cookie.split(';').next().unwrap()) - .finish() - .unwrap(); - - srv.execute(request.send()).unwrap(); -} diff --git a/tests/test_server.rs b/tests/test_server.rs index f3c9bf9dd..2d01d270c 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1,48 +1,18 @@ -extern crate actix; -extern crate actix_net; -extern crate actix_web; -#[cfg(feature = "brotli")] -extern crate brotli2; -extern crate bytes; -extern crate flate2; -extern crate futures; -extern crate h2; -extern crate http as modhttp; -extern crate rand; -extern crate tokio; -extern crate tokio_current_thread; -extern crate tokio_current_thread as current_thread; -extern crate tokio_reactor; -extern crate tokio_tcp; - -#[cfg(feature = "tls")] -extern crate native_tls; -#[cfg(feature = "ssl")] -extern crate openssl; -#[cfg(feature = "rust-tls")] -extern crate rustls; - use std::io::{Read, Write}; -use std::sync::Arc; -use std::{thread, time}; -#[cfg(feature = "brotli")] -use brotli2::write::{BrotliDecoder, BrotliEncoder}; -use bytes::{Bytes, BytesMut}; +use actix_http::http::header::{ + ContentEncoding, ACCEPT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING, +}; +use actix_http::{h1, Error, HttpMessage, Response}; +use actix_http_test::TestServer; +use brotli2::write::BrotliDecoder; +use bytes::Bytes; use flate2::read::GzDecoder; -use flate2::write::{GzEncoder, ZlibDecoder, ZlibEncoder}; -use flate2::Compression; -use futures::stream::once; -use futures::{Future, Stream}; -use h2::client as h2client; -use modhttp::Request; -use rand::distributions::Alphanumeric; -use rand::Rng; -use tokio::runtime::current_thread::Runtime; -use tokio_current_thread::spawn; -use tokio_tcp::TcpStream; +use flate2::write::ZlibDecoder; +use futures::stream::once; //Future, Stream +use rand::{distributions::Alphanumeric, Rng}; -use actix_web::*; +use actix_web2::{middleware, App}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -66,240 +36,35 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World"; -#[test] -#[cfg(unix)] -fn test_start() { - use actix::System; - use std::sync::mpsc; - - let _ = test::TestServer::unused_addr(); - let (tx, rx) = mpsc::channel(); - - thread::spawn(|| { - System::run(move || { - let srv = server::new(|| { - vec![App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - })] - }); - - let srv = srv.bind("127.0.0.1:0").unwrap(); - let addr = srv.addrs()[0]; - let srv_addr = srv.start(); - let _ = tx.send((addr, srv_addr, System::current())); - }); - }); - let (addr, srv_addr, sys) = rx.recv().unwrap(); - System::set_current(sys.clone()); - - let mut rt = Runtime::new().unwrap(); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } - - // pause - let _ = srv_addr.send(server::PauseServer).wait(); - thread::sleep(time::Duration::from_millis(200)); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .timeout(time::Duration::from_millis(200)) - .finish() - .unwrap(); - assert!(rt.block_on(req.send()).is_err()); - } - - // resume - let _ = srv_addr.send(server::ResumeServer).wait(); - thread::sleep(time::Duration::from_millis(200)); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } - - let _ = sys.stop(); -} - -#[test] -#[cfg(unix)] -fn test_shutdown() { - use actix::System; - use std::net; - use std::sync::mpsc; - - let _ = test::TestServer::unused_addr(); - let (tx, rx) = mpsc::channel(); - - thread::spawn(|| { - System::run(move || { - let srv = server::new(|| { - vec![App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - })] - }); - - let srv = srv.bind("127.0.0.1:0").unwrap(); - let addr = srv.addrs()[0]; - let srv_addr = srv.shutdown_timeout(1).start(); - let _ = tx.send((addr, srv_addr, System::current())); - }); - }); - let (addr, srv_addr, sys) = rx.recv().unwrap(); - System::set_current(sys.clone()); - - let mut rt = Runtime::new().unwrap(); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()).unwrap(); - srv_addr.do_send(server::StopServer { graceful: true }); - assert!(response.status().is_success()); - } - - thread::sleep(time::Duration::from_millis(1000)); - assert!(net::TcpStream::connect(addr).is_err()); - - let _ = sys.stop(); -} - -#[test] -#[cfg(unix)] -fn test_panic() { - use actix::System; - use std::sync::mpsc; - - let _ = test::TestServer::unused_addr(); - let (tx, rx) = mpsc::channel(); - - thread::spawn(|| { - System::run(move || { - let srv = server::new(|| { - App::new() - .resource("/panic", |r| { - r.method(http::Method::GET).f(|_| -> &'static str { - panic!("error"); - }); - }).resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - }) - }).workers(1); - - let srv = srv.bind("127.0.0.1:0").unwrap(); - let addr = srv.addrs()[0]; - srv.start(); - let _ = tx.send((addr, System::current())); - }); - }); - let (addr, sys) = rx.recv().unwrap(); - System::set_current(sys.clone()); - - let mut rt = Runtime::new().unwrap(); - { - let req = client::ClientRequest::get(format!("http://{}/panic", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()); - assert!(response.is_err()); - } - - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()); - assert!(response.is_err()); - } - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } - - let _ = sys.stop(); -} - -#[test] -fn test_simple() { - let mut srv = test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok())); - let req = srv.get().finish().unwrap(); - let response = srv.execute(req.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_headers() { - let data = STR.repeat(10); - let srv_data = Arc::new(data.clone()); - let mut srv = test::TestServer::new(move |app| { - let data = srv_data.clone(); - app.handler(move |_| { - let mut builder = HttpResponse::Ok(); - for idx in 0..90 { - builder.header( - format!("X-TEST-{}", idx).as_str(), - "TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", - ); - } - builder.body(data.as_ref()) - }) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - #[test] fn test_body() { - let mut srv = - test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); + let mut srv = TestServer::new(|| { + h1::H1Service::new( + App::new().resource("/", |r| r.get(|| Response::Ok().body(STR))), + ) + }); let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } #[test] fn test_body_gzip() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(STR) - }) + let mut srv = TestServer::new(|| { + h1::H1Service::new( + App::new() + .middleware(middleware::Compress::new(ContentEncoding::Gzip)) + .resource("/", |r| r.get(|| Response::Ok().body(STR))), + ) }); - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let request = srv.get().finish().unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); // read response @@ -315,19 +80,19 @@ fn test_body_gzip() { #[test] fn test_body_gzip_large() { let data = STR.repeat(10); - let srv_data = Arc::new(data.clone()); + let srv_data = data.clone(); - let mut srv = test::TestServer::new(move |app| { + let mut srv = TestServer::new(move || { let data = srv_data.clone(); - app.handler(move |_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(data.as_ref()) - }) + h1::H1Service::new( + App::new() + .middleware(middleware::Compress::new(ContentEncoding::Gzip)) + .resource("/", |r| r.get(move || Response::Ok().body(data.clone()))), + ) }); - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let request = srv.get().finish().unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); // read response @@ -346,19 +111,19 @@ fn test_body_gzip_large_random() { .sample_iter(&Alphanumeric) .take(70_000) .collect::(); - let srv_data = Arc::new(data.clone()); + let srv_data = data.clone(); - let mut srv = test::TestServer::new(move |app| { + let mut srv = TestServer::new(move || { let data = srv_data.clone(); - app.handler(move |_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(data.as_ref()) - }) + h1::H1Service::new( + App::new() + .middleware(middleware::Compress::new(ContentEncoding::Gzip)) + .resource("/", |r| r.get(move || Response::Ok().body(data.clone()))), + ) }); - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let request = srv.get().finish().unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); // read response @@ -374,18 +139,27 @@ fn test_body_gzip_large_random() { #[test] fn test_body_chunked_implicit() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(Body::Streaming(Box::new(body))) - }) + let mut srv = TestServer::new(move || { + h1::H1Service::new( + App::new() + .middleware(middleware::Compress::new(ContentEncoding::Gzip)) + .resource("/", |r| { + r.get(move || { + Response::Ok().streaming(once(Ok::<_, Error>( + Bytes::from_static(STR.as_ref()), + ))) + }) + }), + ) }); - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let request = srv.get().finish().unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); + assert_eq!( + response.headers().get(TRANSFER_ENCODING).unwrap(), + &b"chunked"[..] + ); // read response let bytes = srv.execute(response.body()).unwrap(); @@ -397,20 +171,24 @@ fn test_body_chunked_implicit() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } -#[cfg(feature = "brotli")] #[test] fn test_body_br_streaming() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Br) - .body(Body::Streaming(Box::new(body))) - }) + let mut srv = TestServer::new(move || { + h1::H1Service::new( + App::new() + .middleware(middleware::Compress::new(ContentEncoding::Br)) + .resource("/", |r| { + r.get(move || { + Response::Ok().streaming(once(Ok::<_, Error>( + Bytes::from_static(STR.as_ref()), + ))) + }) + }), + ) }); - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let request = srv.get().header(ACCEPT_ENCODING, "br").finish().unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); // read response @@ -423,49 +201,20 @@ fn test_body_br_streaming() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } -#[test] -fn test_head_empty() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| HttpResponse::Ok().content_length(STR.len() as u64).finish()) - }); - - let request = srv.head().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - { - let len = response - .headers() - .get(http::header::CONTENT_LENGTH) - .unwrap(); - assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); - } - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert!(bytes.is_empty()); -} - #[test] fn test_head_binary() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .content_length(100) - .body(STR) - }) + let mut srv = TestServer::new(move || { + h1::H1Service::new(App::new().resource("/", |r| { + r.head(move || Response::Ok().content_length(100).body(STR)) + })) }); let request = srv.head().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); { - let len = response - .headers() - .get(http::header::CONTENT_LENGTH) - .unwrap(); + let len = response.headers().get(CONTENT_LENGTH).unwrap(); assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); } @@ -475,121 +224,41 @@ fn test_head_binary() { } #[test] -fn test_head_binary2() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(STR) - }) - }); - - let request = srv.head().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - { - let len = response - .headers() - .get(http::header::CONTENT_LENGTH) - .unwrap(); - assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); - } -} - -#[test] -fn test_body_length() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .content_length(STR.len() as u64) - .content_encoding(http::ContentEncoding::Identity) - .body(Body::Streaming(Box::new(body))) - }) +fn test_no_chunking() { + let mut srv = TestServer::new(move || { + h1::H1Service::new(App::new().resource("/", |r| { + r.get(move || { + Response::Ok() + .no_chunking() + .content_length(STR.len() as u64) + .streaming(once(Ok::<_, Error>(Bytes::from_static(STR.as_ref())))) + }) + })) }); let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); + assert!(!response.headers().contains_key(TRANSFER_ENCODING)); // read response let bytes = srv.execute(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } -#[test] -fn test_body_chunked_explicit() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .chunked() - .content_encoding(http::ContentEncoding::Gzip) - .body(Body::Streaming(Box::new(body))) - }) - }); - - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - - // decode - let mut e = GzDecoder::new(&bytes[..]); - let mut dec = Vec::new(); - e.read_to_end(&mut dec).unwrap(); - assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_body_identity() { - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(STR.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - let enc2 = enc.clone(); - - let mut srv = test::TestServer::new(move |app| { - let enc3 = enc2.clone(); - app.handler(move |_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .header(http::header::CONTENT_ENCODING, "deflate") - .body(enc3.clone()) - }) - }); - - // client request - let request = srv - .get() - .header("accept-encoding", "deflate") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - - // decode deflate - assert_eq!(bytes, Bytes::from(STR)); -} - #[test] fn test_body_deflate() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Deflate) - .body(STR) - }) + let mut srv = TestServer::new(move || { + h1::H1Service::new( + App::new() + .middleware(middleware::Compress::new(ContentEncoding::Deflate)) + .resource("/", |r| r.get(move || Response::Ok().body(STR))), + ) }); // client request - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let request = srv.get().finish().unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); // read response @@ -602,20 +271,19 @@ fn test_body_deflate() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } -#[cfg(feature = "brotli")] #[test] fn test_body_brotli() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Br) - .body(STR) - }) + let mut srv = TestServer::new(move || { + h1::H1Service::new( + App::new() + .middleware(middleware::Compress::new(ContentEncoding::Br)) + .resource("/", |r| r.get(move || Response::Ok().body(STR))), + ) }); // client request - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let request = srv.get().header(ACCEPT_ENCODING, "br").finish().unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); // read response @@ -628,773 +296,623 @@ fn test_body_brotli() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } -#[test] -fn test_gzip_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - // client request - let mut e = GzEncoder::new(Vec::new(), Compression::default()); - e.write_all(STR.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "gzip") - .body(enc.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_gzip_encoding_large() { - let data = STR.repeat(10); - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - // client request - let mut e = GzEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "gzip") - .body(enc.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_reading_gzip_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(60_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - // client request - let mut e = GzEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "gzip") - .body(enc.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_reading_deflate_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(STR.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "deflate") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_reading_deflate_encoding_large() { - let data = STR.repeat(10); - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "deflate") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_reading_deflate_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(160_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "deflate") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_brotli_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = BrotliEncoder::new(Vec::new(), 5); - e.write_all(STR.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "br") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_brotli_encoding_large() { - let data = STR.repeat(10); - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = BrotliEncoder::new(Vec::new(), 5); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "br") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(all(feature = "brotli", feature = "ssl"))] -#[test] -fn test_brotli_encoding_large_ssl() { - use actix::{Actor, System}; - use openssl::ssl::{ - SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode, - }; - // load ssl keys - let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); - builder - .set_private_key_file("tests/key.pem", SslFiletype::PEM) - .unwrap(); - builder - .set_certificate_chain_file("tests/cert.pem") - .unwrap(); - - let data = STR.repeat(10); - let srv = test::TestServer::build().ssl(builder).start(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - let mut rt = System::new("test"); - - // client connector - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - let conn = client::ClientConnector::with_connector(builder.build()).start(); - - // body - let mut e = BrotliEncoder::new(Vec::new(), 5); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = client::ClientRequest::build() - .uri(srv.url("/")) - .method(http::Method::POST) - .header(http::header::CONTENT_ENCODING, "br") - .with_connector(conn) - .body(enc) - .unwrap(); - let response = rt.block_on(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = rt.block_on(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(all(feature = "rust-tls", feature = "ssl"))] -#[test] -fn test_reading_deflate_encoding_large_random_ssl() { - use actix::{Actor, System}; - use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; - use rustls::internal::pemfile::{certs, rsa_private_keys}; - use rustls::{NoClientAuth, ServerConfig}; - use std::fs::File; - use std::io::BufReader; - - // load ssl keys - let mut config = ServerConfig::new(NoClientAuth::new()); - let cert_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); - let key_file = &mut BufReader::new(File::open("tests/key.pem").unwrap()); - let cert_chain = certs(cert_file).unwrap(); - let mut keys = rsa_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); - - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(160_000) - .collect::(); - - let srv = test::TestServer::build().rustls(config).start(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut rt = System::new("test"); - - // client connector - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - let conn = client::ClientConnector::with_connector(builder.build()).start(); - - // encode data - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = client::ClientRequest::build() - .uri(srv.url("/")) - .method(http::Method::POST) - .header(http::header::CONTENT_ENCODING, "deflate") - .with_connector(conn) - .body(enc) - .unwrap(); - let response = rt.block_on(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = rt.block_on(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(all(feature = "tls", feature = "ssl"))] -#[test] -fn test_reading_deflate_encoding_large_random_tls() { - use native_tls::{Identity, TlsAcceptor}; - use openssl::ssl::{ - SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode, - }; - use std::fs::File; - use std::sync::mpsc; - - use actix::{Actor, System}; - let (tx, rx) = mpsc::channel(); - - // load ssl keys - let mut file = File::open("tests/identity.pfx").unwrap(); - let mut identity = vec![]; - file.read_to_end(&mut identity).unwrap(); - let identity = Identity::from_pkcs12(&identity, "1").unwrap(); - let acceptor = TlsAcceptor::new(identity).unwrap(); - - // load ssl keys - let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); - builder - .set_private_key_file("tests/key.pem", SslFiletype::PEM) - .unwrap(); - builder - .set_certificate_chain_file("tests/cert.pem") - .unwrap(); - - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(160_000) - .collect::(); - - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - System::run(move || { - server::new(|| { - App::new().handler("/", |req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }).bind_tls(addr, acceptor) - .unwrap() - .start(); - let _ = tx.send(System::current()); - }); - }); - let sys = rx.recv().unwrap(); - - let mut rt = System::new("test"); - - // client connector - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - let conn = client::ClientConnector::with_connector(builder.build()).start(); - - // encode data - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = client::ClientRequest::build() - .uri(format!("https://{}/", addr)) - .method(http::Method::POST) - .header(http::header::CONTENT_ENCODING, "deflate") - .with_connector(conn) - .body(enc) - .unwrap(); - let response = rt.block_on(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = rt.block_on(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); - - let _ = sys.stop(); -} - -#[test] -fn test_h2() { - let srv = test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); - let addr = srv.addr(); - thread::sleep(time::Duration::from_millis(500)); - - let mut core = Runtime::new().unwrap(); - let tcp = TcpStream::connect(&addr); - - let tcp = tcp - .then(|res| h2client::handshake(res.unwrap())) - .then(move |res| { - let (mut client, h2) = res.unwrap(); - - let request = Request::builder() - .uri(format!("https://{}/", addr).as_str()) - .body(()) - .unwrap(); - let (response, _) = client.send_request(request, false).unwrap(); - - // Spawn a task to run the conn... - spawn(h2.map_err(|e| println!("GOT ERR={:?}", e))); - - response.and_then(|response| { - assert_eq!(response.status(), http::StatusCode::OK); - - let (_, body) = response.into_parts(); - - body.fold(BytesMut::new(), |mut b, c| -> Result<_, h2::Error> { - b.extend(c); - Ok(b) - }) - }) - }); - let _res = core.block_on(tcp); - // assert_eq!(_res.unwrap(), Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_application() { - let mut srv = test::TestServer::with_factory(|| { - App::new().resource("/", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_default_404_handler_response() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .prefix("/app") - .resource("", |r| r.f(|_| HttpResponse::Ok())) - .resource("/", |r| r.f(|_| HttpResponse::Ok())) - }); - let addr = srv.addr(); - - let mut buf = [0; 24]; - let request = TcpStream::connect(&addr) - .and_then(|sock| { - tokio::io::write_all(sock, "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n") - .and_then(|(sock, _)| tokio::io::read_exact(sock, &mut buf)) - .and_then(|(_, buf)| Ok(buf)) - }).map_err(|e| panic!("{:?}", e)); - let response = srv.execute(request).unwrap(); - let rep = String::from_utf8_lossy(&response[..]); - assert!(rep.contains("HTTP/1.1 404 Not Found")); -} - -#[test] -fn test_server_cookies() { - use actix_web::http; - - let mut srv = test::TestServer::with_factory(|| { - App::new().resource("/", |r| { - r.f(|_| { - HttpResponse::Ok() - .cookie( - http::CookieBuilder::new("first", "first_value") - .http_only(true) - .finish(), - ).cookie(http::Cookie::new("second", "first_value")) - .cookie(http::Cookie::new("second", "second_value")) - .finish() - }) - }) - }); - - let first_cookie = http::CookieBuilder::new("first", "first_value") - .http_only(true) - .finish(); - let second_cookie = http::Cookie::new("second", "second_value"); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - let cookies = response.cookies().expect("To have cookies"); - assert_eq!(cookies.len(), 2); - if cookies[0] == first_cookie { - assert_eq!(cookies[1], second_cookie); - } else { - assert_eq!(cookies[0], second_cookie); - assert_eq!(cookies[1], first_cookie); - } - - let first_cookie = first_cookie.to_string(); - let second_cookie = second_cookie.to_string(); - //Check that we have exactly two instances of raw cookie headers - let cookies = response - .headers() - .get_all(http::header::SET_COOKIE) - .iter() - .map(|header| header.to_str().expect("To str").to_string()) - .collect::>(); - assert_eq!(cookies.len(), 2); - if cookies[0] == first_cookie { - assert_eq!(cookies[1], second_cookie); - } else { - assert_eq!(cookies[0], second_cookie); - assert_eq!(cookies[1], first_cookie); - } -} - -#[test] -fn test_slow_request() { - use actix::System; - use std::net; - use std::sync::mpsc; - let (tx, rx) = mpsc::channel(); - - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - System::run(move || { - let srv = server::new(|| { - vec![App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - })] - }); - - let srv = srv.bind(addr).unwrap(); - srv.client_timeout(200).start(); - let _ = tx.send(System::current()); - }); - }); - let sys = rx.recv().unwrap(); - - thread::sleep(time::Duration::from_millis(200)); - - let mut stream = net::TcpStream::connect(addr).unwrap(); - let mut data = String::new(); - let _ = stream.read_to_string(&mut data); - assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); - - let mut stream = net::TcpStream::connect(addr).unwrap(); - let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n"); - let mut data = String::new(); - let _ = stream.read_to_string(&mut data); - assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); - - sys.stop(); -} - -#[test] -fn test_malformed_request() { - use actix::System; - use std::net; - use std::sync::mpsc; - let (tx, rx) = mpsc::channel(); - - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - System::run(move || { - let srv = server::new(|| { - App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - }) - }); - - let _ = srv.bind(addr).unwrap().start(); - let _ = tx.send(System::current()); - }); - }); - let sys = rx.recv().unwrap(); - thread::sleep(time::Duration::from_millis(200)); - - let mut stream = net::TcpStream::connect(addr).unwrap(); - let _ = stream.write_all(b"GET /test/tests/test HTTP1.1\r\n"); - let mut data = String::new(); - let _ = stream.read_to_string(&mut data); - assert!(data.starts_with("HTTP/1.1 400 Bad Request")); - - sys.stop(); -} - -#[test] -fn test_app_404() { - let mut srv = test::TestServer::with_factory(|| { - App::new().prefix("/prefix").resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - }) - }); - - let request = srv.client(http::Method::GET, "/prefix/").finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - let request = srv.client(http::Method::GET, "/").finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), http::StatusCode::NOT_FOUND); -} - -#[test] -#[cfg(feature = "ssl")] -fn test_ssl_handshake_timeout() { - use actix::System; - use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; - use std::net; - use std::sync::mpsc; - - let (tx, rx) = mpsc::channel(); - let addr = test::TestServer::unused_addr(); - - // load ssl keys - let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); - builder - .set_private_key_file("tests/key.pem", SslFiletype::PEM) - .unwrap(); - builder - .set_certificate_chain_file("tests/cert.pem") - .unwrap(); - - thread::spawn(move || { - System::run(move || { - let srv = server::new(|| { - App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - }) - }); - - srv.bind_ssl(addr, builder) - .unwrap() - .workers(1) - .client_timeout(200) - .start(); - let _ = tx.send(System::current()); - }); - }); - let sys = rx.recv().unwrap(); - - let mut stream = net::TcpStream::connect(addr).unwrap(); - let mut data = String::new(); - let _ = stream.read_to_string(&mut data); - assert!(data.is_empty()); - - let _ = sys.stop(); -} - -#[test] -fn test_content_length() { - use actix_web::http::header::{HeaderName, HeaderValue}; - use http::StatusCode; - - let mut srv = test::TestServer::new(move |app| { - app.resource("/{status}", |r| { - r.f(|req: &HttpRequest| { - let indx: usize = - req.match_info().get("status").unwrap().parse().unwrap(); - let statuses = [ - StatusCode::NO_CONTENT, - StatusCode::CONTINUE, - StatusCode::SWITCHING_PROTOCOLS, - StatusCode::PROCESSING, - StatusCode::OK, - StatusCode::NOT_FOUND, - ]; - HttpResponse::new(statuses[indx]) - }) - }); - }); - - let addr = srv.addr(); - let mut get_resp = |i| { - let url = format!("http://{}/{}", addr, i); - let req = srv.get().uri(url).finish().unwrap(); - srv.execute(req.send()).unwrap() - }; - - let header = HeaderName::from_static("content-length"); - let value = HeaderValue::from_static("0"); - - for i in 0..4 { - let response = get_resp(i); - assert_eq!(response.headers().get(&header), None); - } - for i in 4..6 { - let response = get_resp(i); - assert_eq!(response.headers().get(&header), Some(&value)); - } -} +// #[test] +// fn test_gzip_encoding() { +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// // client request +// let mut e = GzEncoder::new(Vec::new(), Compression::default()); +// e.write_all(STR.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "gzip") +// .body(enc.clone()) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +// } + +// #[test] +// fn test_gzip_encoding_large() { +// let data = STR.repeat(10); +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// // client request +// let mut e = GzEncoder::new(Vec::new(), Compression::default()); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "gzip") +// .body(enc.clone()) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[test] +// fn test_reading_gzip_encoding_large_random() { +// let data = rand::thread_rng() +// .sample_iter(&Alphanumeric) +// .take(60_000) +// .collect::(); + +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// // client request +// let mut e = GzEncoder::new(Vec::new(), Compression::default()); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "gzip") +// .body(enc.clone()) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes.len(), data.len()); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[test] +// fn test_reading_deflate_encoding() { +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); +// e.write_all(STR.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "deflate") +// .body(enc) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +// } + +// #[test] +// fn test_reading_deflate_encoding_large() { +// let data = STR.repeat(10); +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "deflate") +// .body(enc) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[test] +// fn test_reading_deflate_encoding_large_random() { +// let data = rand::thread_rng() +// .sample_iter(&Alphanumeric) +// .take(160_000) +// .collect::(); + +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "deflate") +// .body(enc) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes.len(), data.len()); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[cfg(feature = "brotli")] +// #[test] +// fn test_brotli_encoding() { +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// let mut e = BrotliEncoder::new(Vec::new(), 5); +// e.write_all(STR.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "br") +// .body(enc) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +// } + +// #[cfg(feature = "brotli")] +// #[test] +// fn test_brotli_encoding_large() { +// let data = STR.repeat(10); +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// let mut e = BrotliEncoder::new(Vec::new(), 5); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "br") +// .body(enc) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[cfg(all(feature = "brotli", feature = "ssl"))] +// #[test] +// fn test_brotli_encoding_large_ssl() { +// use actix::{Actor, System}; +// use openssl::ssl::{ +// SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode, +// }; +// // load ssl keys +// let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); +// builder +// .set_private_key_file("tests/key.pem", SslFiletype::PEM) +// .unwrap(); +// builder +// .set_certificate_chain_file("tests/cert.pem") +// .unwrap(); + +// let data = STR.repeat(10); +// let srv = test::TestServer::build().ssl(builder).start(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); +// let mut rt = System::new("test"); + +// // client connector +// let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); +// builder.set_verify(SslVerifyMode::NONE); +// let conn = client::ClientConnector::with_connector(builder.build()).start(); + +// // body +// let mut e = BrotliEncoder::new(Vec::new(), 5); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = client::ClientRequest::build() +// .uri(srv.url("/")) +// .method(http::Method::POST) +// .header(http::header::CONTENT_ENCODING, "br") +// .with_connector(conn) +// .body(enc) +// .unwrap(); +// let response = rt.block_on(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = rt.block_on(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[cfg(all(feature = "rust-tls", feature = "ssl"))] +// #[test] +// fn test_reading_deflate_encoding_large_random_ssl() { +// use actix::{Actor, System}; +// use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; +// use rustls::internal::pemfile::{certs, rsa_private_keys}; +// use rustls::{NoClientAuth, ServerConfig}; +// use std::fs::File; +// use std::io::BufReader; + +// // load ssl keys +// let mut config = ServerConfig::new(NoClientAuth::new()); +// let cert_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); +// let key_file = &mut BufReader::new(File::open("tests/key.pem").unwrap()); +// let cert_chain = certs(cert_file).unwrap(); +// let mut keys = rsa_private_keys(key_file).unwrap(); +// config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); + +// let data = rand::thread_rng() +// .sample_iter(&Alphanumeric) +// .take(160_000) +// .collect::(); + +// let srv = test::TestServer::build().rustls(config).start(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// let mut rt = System::new("test"); + +// // client connector +// let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); +// builder.set_verify(SslVerifyMode::NONE); +// let conn = client::ClientConnector::with_connector(builder.build()).start(); + +// // encode data +// let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = client::ClientRequest::build() +// .uri(srv.url("/")) +// .method(http::Method::POST) +// .header(http::header::CONTENT_ENCODING, "deflate") +// .with_connector(conn) +// .body(enc) +// .unwrap(); +// let response = rt.block_on(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = rt.block_on(response.body()).unwrap(); +// assert_eq!(bytes.len(), data.len()); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[cfg(all(feature = "tls", feature = "ssl"))] +// #[test] +// fn test_reading_deflate_encoding_large_random_tls() { +// use native_tls::{Identity, TlsAcceptor}; +// use openssl::ssl::{ +// SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode, +// }; +// use std::fs::File; +// use std::sync::mpsc; + +// use actix::{Actor, System}; +// let (tx, rx) = mpsc::channel(); + +// // load ssl keys +// let mut file = File::open("tests/identity.pfx").unwrap(); +// let mut identity = vec![]; +// file.read_to_end(&mut identity).unwrap(); +// let identity = Identity::from_pkcs12(&identity, "1").unwrap(); +// let acceptor = TlsAcceptor::new(identity).unwrap(); + +// // load ssl keys +// let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); +// builder +// .set_private_key_file("tests/key.pem", SslFiletype::PEM) +// .unwrap(); +// builder +// .set_certificate_chain_file("tests/cert.pem") +// .unwrap(); + +// let data = rand::thread_rng() +// .sample_iter(&Alphanumeric) +// .take(160_000) +// .collect::(); + +// let addr = test::TestServer::unused_addr(); +// thread::spawn(move || { +// System::run(move || { +// server::new(|| { +// App::new().handler("/", |req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }) +// .bind_tls(addr, acceptor) +// .unwrap() +// .start(); +// let _ = tx.send(System::current()); +// }); +// }); +// let sys = rx.recv().unwrap(); + +// let mut rt = System::new("test"); + +// // client connector +// let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); +// builder.set_verify(SslVerifyMode::NONE); +// let conn = client::ClientConnector::with_connector(builder.build()).start(); + +// // encode data +// let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = client::ClientRequest::build() +// .uri(format!("https://{}/", addr)) +// .method(http::Method::POST) +// .header(http::header::CONTENT_ENCODING, "deflate") +// .with_connector(conn) +// .body(enc) +// .unwrap(); +// let response = rt.block_on(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = rt.block_on(response.body()).unwrap(); +// assert_eq!(bytes.len(), data.len()); +// assert_eq!(bytes, Bytes::from(data)); + +// let _ = sys.stop(); +// } + +// #[test] +// fn test_server_cookies() { +// use actix_web::http; + +// let mut srv = test::TestServer::with_factory(|| { +// App::new().resource("/", |r| { +// r.f(|_| { +// HttpResponse::Ok() +// .cookie( +// http::CookieBuilder::new("first", "first_value") +// .http_only(true) +// .finish(), +// ) +// .cookie(http::Cookie::new("second", "first_value")) +// .cookie(http::Cookie::new("second", "second_value")) +// .finish() +// }) +// }) +// }); + +// let first_cookie = http::CookieBuilder::new("first", "first_value") +// .http_only(true) +// .finish(); +// let second_cookie = http::Cookie::new("second", "second_value"); + +// let request = srv.get().finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// let cookies = response.cookies().expect("To have cookies"); +// assert_eq!(cookies.len(), 2); +// if cookies[0] == first_cookie { +// assert_eq!(cookies[1], second_cookie); +// } else { +// assert_eq!(cookies[0], second_cookie); +// assert_eq!(cookies[1], first_cookie); +// } + +// let first_cookie = first_cookie.to_string(); +// let second_cookie = second_cookie.to_string(); +// //Check that we have exactly two instances of raw cookie headers +// let cookies = response +// .headers() +// .get_all(http::header::SET_COOKIE) +// .iter() +// .map(|header| header.to_str().expect("To str").to_string()) +// .collect::>(); +// assert_eq!(cookies.len(), 2); +// if cookies[0] == first_cookie { +// assert_eq!(cookies[1], second_cookie); +// } else { +// assert_eq!(cookies[0], second_cookie); +// assert_eq!(cookies[1], first_cookie); +// } +// } + +// #[test] +// fn test_slow_request() { +// use actix::System; +// use std::net; +// use std::sync::mpsc; +// let (tx, rx) = mpsc::channel(); + +// let addr = test::TestServer::unused_addr(); +// thread::spawn(move || { +// System::run(move || { +// let srv = server::new(|| { +// vec![App::new().resource("/", |r| { +// r.method(http::Method::GET).f(|_| HttpResponse::Ok()) +// })] +// }); + +// let srv = srv.bind(addr).unwrap(); +// srv.client_timeout(200).start(); +// let _ = tx.send(System::current()); +// }); +// }); +// let sys = rx.recv().unwrap(); + +// thread::sleep(time::Duration::from_millis(200)); + +// let mut stream = net::TcpStream::connect(addr).unwrap(); +// let mut data = String::new(); +// let _ = stream.read_to_string(&mut data); +// assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); + +// let mut stream = net::TcpStream::connect(addr).unwrap(); +// let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n"); +// let mut data = String::new(); +// let _ = stream.read_to_string(&mut data); +// assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); + +// sys.stop(); +// } + +// #[test] +// #[cfg(feature = "ssl")] +// fn test_ssl_handshake_timeout() { +// use actix::System; +// use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; +// use std::net; +// use std::sync::mpsc; + +// let (tx, rx) = mpsc::channel(); +// let addr = test::TestServer::unused_addr(); + +// // load ssl keys +// let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); +// builder +// .set_private_key_file("tests/key.pem", SslFiletype::PEM) +// .unwrap(); +// builder +// .set_certificate_chain_file("tests/cert.pem") +// .unwrap(); + +// thread::spawn(move || { +// System::run(move || { +// let srv = server::new(|| { +// App::new().resource("/", |r| { +// r.method(http::Method::GET).f(|_| HttpResponse::Ok()) +// }) +// }); + +// srv.bind_ssl(addr, builder) +// .unwrap() +// .workers(1) +// .client_timeout(200) +// .start(); +// let _ = tx.send(System::current()); +// }); +// }); +// let sys = rx.recv().unwrap(); + +// let mut stream = net::TcpStream::connect(addr).unwrap(); +// let mut data = String::new(); +// let _ = stream.read_to_string(&mut data); +// assert!(data.is_empty()); + +// let _ = sys.stop(); +// } diff --git a/tests/test_ws.rs b/tests/test_ws.rs deleted file mode 100644 index cb46bc7e1..000000000 --- a/tests/test_ws.rs +++ /dev/null @@ -1,395 +0,0 @@ -extern crate actix; -extern crate actix_web; -extern crate bytes; -extern crate futures; -extern crate http; -extern crate rand; - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::{thread, time}; - -use bytes::Bytes; -use futures::Stream; -use rand::distributions::Alphanumeric; -use rand::Rng; - -#[cfg(feature = "ssl")] -extern crate openssl; -#[cfg(feature = "rust-tls")] -extern crate rustls; - -use actix::prelude::*; -use actix_web::*; - -struct Ws; - -impl Actor for Ws { - type Context = ws::WebsocketContext; -} - -impl StreamHandler for Ws { - fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { - match msg { - ws::Message::Ping(msg) => ctx.pong(&msg), - ws::Message::Text(text) => ctx.text(text), - ws::Message::Binary(bin) => ctx.binary(bin), - ws::Message::Close(reason) => ctx.close(reason), - _ => (), - } - } -} - -#[test] -fn test_simple() { - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (reader, mut writer) = srv.ws().unwrap(); - - writer.text("text"); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Text("text".to_owned()))); - - writer.binary(b"text".as_ref()); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(ws::Message::Binary(Bytes::from_static(b"text").into())) - ); - - writer.ping("ping"); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Pong("ping".to_owned()))); - - writer.close(Some(ws::CloseCode::Normal.into())); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(ws::Message::Close(Some(ws::CloseCode::Normal.into()))) - ); -} - -// websocket resource helper function -fn start_ws_resource(req: &HttpRequest) -> Result { - ws::start(req, Ws) -} - -#[test] -fn test_simple_path() { - const PATH: &str = "/v1/ws/"; - - // Create a websocket at a specific path. - let mut srv = test::TestServer::new(|app| { - app.resource(PATH, |r| r.route().f(start_ws_resource)); - }); - // fetch the sockets for the resource at a given path. - let (reader, mut writer) = srv.ws_at(PATH).unwrap(); - - writer.text("text"); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Text("text".to_owned()))); - - writer.binary(b"text".as_ref()); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(ws::Message::Binary(Bytes::from_static(b"text").into())) - ); - - writer.ping("ping"); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Pong("ping".to_owned()))); - - writer.close(Some(ws::CloseCode::Normal.into())); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(ws::Message::Close(Some(ws::CloseCode::Normal.into()))) - ); -} - -#[test] -fn test_empty_close_code() { - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (reader, mut writer) = srv.ws().unwrap(); - - writer.close(None); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Close(None))); -} - -#[test] -fn test_close_description() { - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (reader, mut writer) = srv.ws().unwrap(); - - let close_reason: ws::CloseReason = - (ws::CloseCode::Normal, "close description").into(); - writer.close(Some(close_reason.clone())); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Close(Some(close_reason)))); -} - -#[test] -fn test_large_text() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(65_536) - .collect::(); - - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (mut reader, mut writer) = srv.ws().unwrap(); - - for _ in 0..100 { - writer.text(data.clone()); - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, Some(ws::Message::Text(data.clone()))); - } -} - -#[test] -fn test_large_bin() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(65_536) - .collect::(); - - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (mut reader, mut writer) = srv.ws().unwrap(); - - for _ in 0..100 { - writer.binary(data.clone()); - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, Some(ws::Message::Binary(Binary::from(data.clone())))); - } -} - -#[test] -fn test_client_frame_size() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(131_072) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req| -> Result { - let mut resp = ws::handshake(req)?; - let stream = ws::WsStream::new(req.payload()).max_size(131_072); - - let body = ws::WebsocketContext::create(req.clone(), Ws, stream); - Ok(resp.body(body)) - }) - }); - let (reader, mut writer) = srv.ws().unwrap(); - - writer.binary(data.clone()); - match srv.execute(reader.into_future()).err().unwrap().0 { - ws::ProtocolError::Overflow => (), - _ => panic!(), - } -} - -struct Ws2 { - count: usize, - bin: bool, -} - -impl Actor for Ws2 { - type Context = ws::WebsocketContext; - - fn started(&mut self, ctx: &mut Self::Context) { - self.send(ctx); - } -} - -impl Ws2 { - fn send(&mut self, ctx: &mut ws::WebsocketContext) { - if self.bin { - ctx.binary(Vec::from("0".repeat(65_536))); - } else { - ctx.text("0".repeat(65_536)); - } - ctx.drain() - .and_then(|_, act, ctx| { - act.count += 1; - if act.count != 10_000 { - act.send(ctx); - } - actix::fut::ok(()) - }).wait(ctx); - } -} - -impl StreamHandler for Ws2 { - fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { - match msg { - ws::Message::Ping(msg) => ctx.pong(&msg), - ws::Message::Text(text) => ctx.text(text), - ws::Message::Binary(bin) => ctx.binary(bin), - ws::Message::Close(reason) => ctx.close(reason), - _ => (), - } - } -} - -#[test] -fn test_server_send_text() { - let data = Some(ws::Message::Text("0".repeat(65_536))); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req| { - ws::start( - req, - Ws2 { - count: 0, - bin: false, - }, - ) - }) - }); - let (mut reader, _writer) = srv.ws().unwrap(); - - for _ in 0..10_000 { - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, data); - } -} - -#[test] -fn test_server_send_bin() { - let data = Some(ws::Message::Binary(Binary::from("0".repeat(65_536)))); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req| { - ws::start( - req, - Ws2 { - count: 0, - bin: true, - }, - ) - }) - }); - let (mut reader, _writer) = srv.ws().unwrap(); - - for _ in 0..10_000 { - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, data); - } -} - -#[test] -#[cfg(feature = "ssl")] -fn test_ws_server_ssl() { - use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; - - // load ssl keys - let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); - builder - .set_private_key_file("tests/key.pem", SslFiletype::PEM) - .unwrap(); - builder - .set_certificate_chain_file("tests/cert.pem") - .unwrap(); - - let mut srv = test::TestServer::build().ssl(builder).start(|app| { - app.handler(|req| { - ws::start( - req, - Ws2 { - count: 0, - bin: false, - }, - ) - }) - }); - let (mut reader, _writer) = srv.ws().unwrap(); - - let data = Some(ws::Message::Text("0".repeat(65_536))); - for _ in 0..10_000 { - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, data); - } -} - -#[test] -#[cfg(feature = "rust-tls")] -fn test_ws_server_rust_tls() { - use rustls::internal::pemfile::{certs, rsa_private_keys}; - use rustls::{NoClientAuth, ServerConfig}; - use std::fs::File; - use std::io::BufReader; - - // load ssl keys - let mut config = ServerConfig::new(NoClientAuth::new()); - let cert_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); - let key_file = &mut BufReader::new(File::open("tests/key.pem").unwrap()); - let cert_chain = certs(cert_file).unwrap(); - let mut keys = rsa_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); - - let mut srv = test::TestServer::build().rustls(config).start(|app| { - app.handler(|req| { - ws::start( - req, - Ws2 { - count: 0, - bin: false, - }, - ) - }) - }); - - let (mut reader, _writer) = srv.ws().unwrap(); - - let data = Some(ws::Message::Text("0".repeat(65_536))); - for _ in 0..10_000 { - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, data); - } -} - -struct WsStopped(Arc); - -impl Actor for WsStopped { - type Context = ws::WebsocketContext; - - fn stopped(&mut self, _: &mut Self::Context) { - self.0.fetch_add(1, Ordering::Relaxed); - } -} - -impl StreamHandler for WsStopped { - fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { - match msg { - ws::Message::Text(text) => ctx.text(text), - _ => (), - } - } -} - -#[test] -fn test_ws_stopped() { - let num = Arc::new(AtomicUsize::new(0)); - let num2 = num.clone(); - - let mut srv = test::TestServer::new(move |app| { - let num3 = num2.clone(); - app.handler(move |req| ws::start(req, WsStopped(num3.clone()))) - }); - { - let (reader, mut writer) = srv.ws().unwrap(); - writer.text("text"); - writer.close(None); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Text("text".to_owned()))); - } - thread::sleep(time::Duration::from_millis(100)); - - assert_eq!(num.load(Ordering::Relaxed), 1); -} From e6d04d24cce00d4f0d08da518325f0ecdf195b5e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 1 Mar 2019 23:59:44 -0800 Subject: [PATCH 153/427] move fs to separate crate --- Cargo.toml | 6 + Makefile | 14 - actix-web-fs/CHANGES.md | 5 + actix-web-fs/Cargo.toml | 42 +++ actix-web-fs/README.md | 82 +++++ src/fs.rs => actix-web-fs/src/lib.rs | 30 +- examples/basic.rs | 2 +- src/app.rs | 118 ++----- src/framed_app.rs | 240 -------------- src/framed_handler.rs | 379 ---------------------- src/framed_route.rs | 448 --------------------------- src/helpers.rs | 180 ----------- src/lib.rs | 3 +- src/resource.rs | 39 +-- tests/test_server.rs | 2 +- 15 files changed, 184 insertions(+), 1406 deletions(-) delete mode 100644 Makefile create mode 100644 actix-web-fs/CHANGES.md create mode 100644 actix-web-fs/Cargo.toml create mode 100644 actix-web-fs/README.md rename src/fs.rs => actix-web-fs/src/lib.rs (99%) delete mode 100644 src/framed_app.rs delete mode 100644 src/framed_handler.rs delete mode 100644 src/framed_route.rs delete mode 100644 src/helpers.rs diff --git a/Cargo.toml b/Cargo.toml index 6a61c7801..e9c298fad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,12 @@ codecov = { repository = "actix/actix-web2", branch = "master", service = "githu name = "actix_web" path = "src/lib.rs" +[workspace] +members = [ + ".", + "actix-web-fs", +] + [features] default = ["brotli", "flate2-c"] diff --git a/Makefile b/Makefile deleted file mode 100644 index e3b8b2cf1..000000000 --- a/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -.PHONY: default build test doc book clean - -CARGO_FLAGS := --features "$(FEATURES) alpn tls" - -default: test - -build: - cargo build $(CARGO_FLAGS) - -test: build clippy - cargo test $(CARGO_FLAGS) - -doc: build - cargo doc --no-deps $(CARGO_FLAGS) diff --git a/actix-web-fs/CHANGES.md b/actix-web-fs/CHANGES.md new file mode 100644 index 000000000..b93e282ac --- /dev/null +++ b/actix-web-fs/CHANGES.md @@ -0,0 +1,5 @@ +# Changes + +## [0.1.0] - 2018-10-x + +* Initial impl diff --git a/actix-web-fs/Cargo.toml b/actix-web-fs/Cargo.toml new file mode 100644 index 000000000..5900a9365 --- /dev/null +++ b/actix-web-fs/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "actix-web-fs" +version = "0.1.0" +authors = ["Nikolay Kim "] +description = "Static files support for Actix web." +readme = "README.md" +keywords = ["actix", "http", "async", "futures"] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web.git" +documentation = "https://actix.rs/api/actix-web/stable/actix_web/" +categories = ["asynchronous", "web-programming::http-server"] +license = "MIT/Apache-2.0" +edition = "2018" +workspace = ".." + +[lib] +name = "actix_web_fs" +path = "src/lib.rs" + +[dependencies] +actix-web = { path=".." } +actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-service = { git = "https://github.com/actix/actix-net.git" } + +bytes = "0.4" +futures = "0.1" +derive_more = "0.14" +log = "0.4" +mime = "0.3" +mime_guess = "2.0.0-alpha" +percent-encoding = "1.0" +v_htmlescape = "0.4" + +[dev-dependencies] +actix-rt = "0.1.0" +#actix-server = { version="0.2", features=["ssl"] } +actix-server = { git = "https://github.com/actix/actix-net.git", features=["ssl"] } +actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +rand = "0.6" +env_logger = "0.6" +serde_derive = "1.0" diff --git a/actix-web-fs/README.md b/actix-web-fs/README.md new file mode 100644 index 000000000..c7e195de9 --- /dev/null +++ b/actix-web-fs/README.md @@ -0,0 +1,82 @@ +# Actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +Actix web is a simple, pragmatic and extremely fast web framework for Rust. + +* Supported *HTTP/1.x* and [*HTTP/2.0*](https://actix.rs/docs/http2/) protocols +* Streaming and pipelining +* Keep-alive and slow requests handling +* Client/server [WebSockets](https://actix.rs/docs/websockets/) support +* Transparent content compression/decompression (br, gzip, deflate) +* Configurable [request routing](https://actix.rs/docs/url-dispatch/) +* Multipart streams +* Static assets +* SSL support with OpenSSL or `native-tls` +* Middlewares ([Logger, Session, CORS, CSRF, etc](https://actix.rs/docs/middleware/)) +* Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) +* Built on top of [Actix actor framework](https://github.com/actix/actix) +* Experimental [Async/Await](https://github.com/mehcode/actix-web-async-await) support. + +## Documentation & community resources + +* [User Guide](https://actix.rs/docs/) +* [API Documentation (Development)](https://actix.rs/actix-web/actix_web/) +* [API Documentation (Releases)](https://actix.rs/api/actix-web/stable/actix_web/) +* [Chat on gitter](https://gitter.im/actix/actix) +* Cargo package: [actix-web](https://crates.io/crates/actix-web) +* Minimum supported Rust version: 1.31 or later + +## Example + +```rust +extern crate actix_web; +use actix_web::{http, server, App, Path, Responder}; + +fn index(info: Path<(u32, String)>) -> impl Responder { + format!("Hello {}! id:{}", info.1, info.0) +} + +fn main() { + server::new( + || App::new() + .route("/{id}/{name}/index.html", http::Method::GET, index)) + .bind("127.0.0.1:8080").unwrap() + .run(); +} +``` + +### More examples + +* [Basics](https://github.com/actix/examples/tree/master/basics/) +* [Stateful](https://github.com/actix/examples/tree/master/state/) +* [Protobuf support](https://github.com/actix/examples/tree/master/protobuf/) +* [Multipart streams](https://github.com/actix/examples/tree/master/multipart/) +* [Simple websocket](https://github.com/actix/examples/tree/master/websocket/) +* [Tera](https://github.com/actix/examples/tree/master/template_tera/) / + [Askama](https://github.com/actix/examples/tree/master/template_askama/) templates +* [Diesel integration](https://github.com/actix/examples/tree/master/diesel/) +* [r2d2](https://github.com/actix/examples/tree/master/r2d2/) +* [SSL / HTTP/2.0](https://github.com/actix/examples/tree/master/tls/) +* [Tcp/Websocket chat](https://github.com/actix/examples/tree/master/websocket-chat/) +* [Json](https://github.com/actix/examples/tree/master/json/) + +You may consider checking out +[this directory](https://github.com/actix/examples/tree/master/) for more examples. + +## Benchmarks + +* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r16&hw=ph&test=plaintext) + +## License + +This project is licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) + +at your option. + +## Code of Conduct + +Contribution to the actix-web crate is organized under the terms of the +Contributor Covenant, the maintainer of actix-web, @fafhrd91, promises to +intervene to uphold that code of conduct. diff --git a/src/fs.rs b/actix-web-fs/src/lib.rs similarity index 99% rename from src/fs.rs rename to actix-web-fs/src/lib.rs index 3c83af6eb..c2ac5f3af 100644 --- a/src/fs.rs +++ b/actix-web-fs/src/lib.rs @@ -27,15 +27,14 @@ use actix_http::http::header::{ }; use actix_http::http::{ContentEncoding, Method, StatusCode}; use actix_http::{HttpMessage, Response}; +use actix_service::boxed::BoxedNewService; use actix_service::{NewService, Service}; +use actix_web::{ + blocking, FromRequest, HttpRequest, Responder, ServiceRequest, ServiceResponse, +}; use futures::future::{err, ok, FutureResult}; -use crate::blocking; -use crate::handler::FromRequest; -use crate::helpers::HttpDefaultNewService; -use crate::request::HttpRequest; -use crate::responder::Responder; -use crate::service::{ServiceRequest, ServiceResponse}; +type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; ///Describes `StaticFiles` configiration /// @@ -101,6 +100,7 @@ pub trait StaticFileConfig: Default { ///[StaticFileConfig](trait.StaticFileConfig.html) #[derive(Default)] pub struct DefaultConfig; + impl StaticFileConfig for DefaultConfig {} /// Return the MIME type associated with a filename extension (case-insensitive). @@ -716,9 +716,7 @@ pub struct StaticFiles { directory: PathBuf, index: Option, show_index: bool, - default: Rc< - RefCell, ServiceResponse>>>>, - >, + default: Rc>>>>, renderer: Rc, _chunk_size: usize, _follow_symlinks: bool, @@ -817,9 +815,7 @@ pub struct StaticFilesService { directory: PathBuf, index: Option, show_index: bool, - default: Rc< - RefCell, ServiceResponse>>>>, - >, + default: Rc>>>>, renderer: Rc, _chunk_size: usize, _follow_symlinks: bool, @@ -838,8 +834,8 @@ impl Service for StaticFilesService { fn call(&mut self, req: Self::Request) -> Self::Future { let mut req = req; - let real_path = match PathBuf::from_request(&mut req).poll() { - Ok(Async::Ready(item)) => item, + let real_path = match PathBufWrp::from_request(&mut req).poll() { + Ok(Async::Ready(item)) => item.0, Ok(Async::NotReady) => unreachable!(), Err(e) => return err(Error::from(e)), }; @@ -888,7 +884,9 @@ impl Service for StaticFilesService { } } -impl

    FromRequest

    for PathBuf { +struct PathBufWrp(PathBuf); + +impl

    FromRequest

    for PathBufWrp { type Error = UriSegmentError; type Future = FutureResult; @@ -917,7 +915,7 @@ impl

    FromRequest

    for PathBuf { } } - ok(buf) + ok(PathBufWrp(buf)) } } diff --git a/examples/basic.rs b/examples/basic.rs index 9f4701ebc..a18581f90 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -2,7 +2,7 @@ use futures::IntoFuture; use actix_http::{h1, http::Method, Response}; use actix_server::Server; -use actix_web2::{middleware, App, Error, HttpRequest, Resource}; +use actix_web::{middleware, App, Error, HttpRequest, Resource}; fn index(req: HttpRequest) -> &'static str { println!("REQ: {:?}", req); diff --git a/src/app.rs b/src/app.rs index 6ed9b1440..7c0777060 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,6 +5,7 @@ use std::rc::Rc; use actix_http::body::{Body, MessageBody}; use actix_http::{Extensions, PayloadStream, Request, Response}; use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; +use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ AndThenNewService, ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, @@ -12,13 +13,12 @@ use actix_service::{ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; -use crate::helpers::{ - BoxedHttpNewService, BoxedHttpService, DefaultNewService, HttpDefaultNewService, -}; use crate::resource::Resource; use crate::service::{ServiceRequest, ServiceResponse}; use crate::state::{State, StateFactory, StateFactoryResult}; +type HttpService

    = BoxedService, ServiceResponse, ()>; +type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; type BoxedResponse = Box>; pub trait HttpServiceFactory { @@ -31,18 +31,9 @@ pub trait HttpServiceFactory { /// Application builder pub struct App { - services: Vec<( - ResourceDef, - BoxedHttpNewService, ServiceResponse>, - )>, - default: Option, ServiceResponse>>>, - defaults: Vec< - Rc< - RefCell< - Option, ServiceResponse>>>, - >, - >, - >, + services: Vec<(ResourceDef, HttpNewService

    )>, + default: Option>>, + defaults: Vec>>>>>, endpoint: T, factory_ref: Rc>>>, extensions: Extensions, @@ -181,10 +172,8 @@ where let rdef = ResourceDef::new(path); let resource = f(Resource::new()); self.defaults.push(resource.get_default()); - self.services.push(( - rdef, - Box::new(HttpNewService::new(resource.into_new_service())), - )); + self.services + .push((rdef, boxed::new_service(resource.into_new_service()))); self } @@ -203,9 +192,9 @@ where > + 'static, { // create and configure default resource - self.default = Some(Rc::new(Box::new(DefaultNewService::new( - f(Resource::new()).into_new_service(), - )))); + self.default = Some(Rc::new(boxed::new_service( + f(Resource::new()).into_new_service().map_init_err(|_| ()), + ))); self } @@ -223,7 +212,7 @@ where { self.services.push(( rdef.into(), - Box::new(HttpNewService::new(factory.into_new_service())), + boxed::new_service(factory.into_new_service().map_init_err(|_| ())), )); self } @@ -422,15 +411,10 @@ impl

    Service for AppStateService

    { } pub struct AppFactory

    { - services: Rc< - Vec<( - ResourceDef, - BoxedHttpNewService, ServiceResponse>, - )>, - >, + services: Rc)>>, } -impl

    NewService for AppFactory

    { +impl NewService for AppFactory

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); @@ -454,8 +438,7 @@ impl

    NewService for AppFactory

    { } } -type HttpServiceFut

    = - Box, ServiceResponse>, Error = ()>>; +type HttpServiceFut

    = Box, Error = ()>>; /// Create app service #[doc(hidden)] @@ -465,10 +448,7 @@ pub struct CreateAppService

    { enum CreateAppServiceItem

    { Future(Option, HttpServiceFut

    ), - Service( - ResourceDef, - BoxedHttpService, ServiceResponse>, - ), + Service(ResourceDef, HttpService

    ), } impl

    Future for CreateAppService

    { @@ -522,7 +502,7 @@ impl

    Future for CreateAppService

    { } pub struct AppService

    { - router: Router, ServiceResponse>>, + router: Router>, ready: Option<(ServiceRequest

    , ResourceInfo)>, } @@ -561,68 +541,6 @@ impl Future for AppServiceResponse { } } -struct HttpNewService>>(T); - -impl HttpNewService -where - T: NewService, Response = ServiceResponse, Error = ()>, - T::Future: 'static, - ::Future: 'static, -{ - pub fn new(service: T) -> Self { - HttpNewService(service) - } -} - -impl NewService for HttpNewService -where - T: NewService, Response = ServiceResponse, Error = ()>, - T::Future: 'static, - T::Service: 'static, - ::Future: 'static, -{ - type Request = ServiceRequest

    ; - type Response = ServiceResponse; - type Error = (); - type InitError = (); - type Service = BoxedHttpService, Self::Response>; - type Future = Box>; - - fn new_service(&self, _: &()) -> Self::Future { - Box::new(self.0.new_service(&()).map_err(|_| ()).and_then(|service| { - let service: BoxedHttpService<_, _> = Box::new(HttpServiceWrapper { - service, - _t: PhantomData, - }); - Ok(service) - })) - } -} - -struct HttpServiceWrapper { - service: T, - _t: PhantomData<(P,)>, -} - -impl Service for HttpServiceWrapper -where - T::Future: 'static, - T: Service, Response = ServiceResponse, Error = ()>, -{ - type Request = ServiceRequest

    ; - type Response = ServiceResponse; - type Error = (); - type Future = BoxedResponse; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.service.poll_ready().map_err(|_| ()) - } - - fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { - Box::new(self.service.call(req)) - } -} - #[doc(hidden)] pub struct AppEntry

    { factory: Rc>>>, @@ -634,7 +552,7 @@ impl

    AppEntry

    { } } -impl

    NewService for AppEntry

    { +impl NewService for AppEntry

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); diff --git a/src/framed_app.rs b/src/framed_app.rs deleted file mode 100644 index ba9254146..000000000 --- a/src/framed_app.rs +++ /dev/null @@ -1,240 +0,0 @@ -use std::marker::PhantomData; -use std::rc::Rc; - -use actix_codec::Framed; -use actix_http::h1::Codec; -use actix_http::{Request, Response, SendResponse}; -use actix_router::{Path, Router, Url}; -use actix_service::{IntoNewService, NewService, Service}; -use actix_utils::cloneable::CloneableService; -use futures::{Async, Future, Poll}; -use tokio_io::{AsyncRead, AsyncWrite}; - -use crate::app::{HttpServiceFactory, State}; -use crate::framed_handler::FramedRequest; -use crate::helpers::{BoxedHttpNewService, BoxedHttpService, HttpNewService}; -use crate::request::Request as WebRequest; - -pub type FRequest = (Request, Framed); -type BoxedResponse = Box>; - -/// Application builder -pub struct FramedApp { - services: Vec<(String, BoxedHttpNewService, ()>)>, - state: State, -} - -impl FramedApp { - pub fn new() -> Self { - FramedApp { - services: Vec::new(), - state: State::new(()), - } - } -} - -impl FramedApp { - pub fn with(state: S) -> FramedApp { - FramedApp { - services: Vec::new(), - state: State::new(state), - } - } - - pub fn service(mut self, factory: U) -> Self - where - U: HttpServiceFactory, - U::Factory: NewService, Response = ()> + 'static, - ::Future: 'static, - ::Service: Service>, - <::Service as Service>::Future: 'static, - { - let path = factory.path().to_string(); - self.services.push(( - path, - Box::new(HttpNewService::new(factory.create(self.state.clone()))), - )); - self - } - - pub fn register_service(&mut self, factory: U) - where - U: HttpServiceFactory, - U::Factory: NewService, Response = ()> + 'static, - ::Future: 'static, - ::Service: Service>, - <::Service as Service>::Future: 'static, - { - let path = factory.path().to_string(); - self.services.push(( - path, - Box::new(HttpNewService::new(factory.create(self.state.clone()))), - )); - } -} - -impl IntoNewService> for FramedApp -where - T: AsyncRead + AsyncWrite, -{ - fn into_new_service(self) -> FramedAppFactory { - FramedAppFactory { - state: self.state, - services: Rc::new(self.services), - _t: PhantomData, - } - } -} - -#[derive(Clone)] -pub struct FramedAppFactory { - state: State, - services: Rc, ()>)>>, - _t: PhantomData, -} - -impl NewService for FramedAppFactory -where - T: AsyncRead + AsyncWrite, -{ - type Request = FRequest; - type Response = (); - type Error = (); - type InitError = (); - type Service = CloneableService>; - type Future = CreateService; - - fn new_service(&self) -> Self::Future { - CreateService { - fut: self - .services - .iter() - .map(|(path, service)| { - CreateServiceItem::Future(Some(path.clone()), service.new_service()) - }) - .collect(), - state: self.state.clone(), - } - } -} - -#[doc(hidden)] -pub struct CreateService { - fut: Vec>, - state: State, -} - -enum CreateServiceItem { - Future( - Option, - Box, ()>, Error = ()>>, - ), - Service(String, BoxedHttpService, ()>), -} - -impl Future for CreateService -where - T: AsyncRead + AsyncWrite, -{ - type Item = CloneableService>; - type Error = (); - - fn poll(&mut self) -> Poll { - let mut done = true; - - // poll http services - for item in &mut self.fut { - let res = match item { - CreateServiceItem::Future(ref mut path, ref mut fut) => { - match fut.poll()? { - Async::Ready(service) => Some((path.take().unwrap(), service)), - Async::NotReady => { - done = false; - None - } - } - } - CreateServiceItem::Service(_, _) => continue, - }; - - if let Some((path, service)) = res { - *item = CreateServiceItem::Service(path, service); - } - } - - if done { - let router = self - .fut - .drain(..) - .fold(Router::build(), |mut router, item| { - match item { - CreateServiceItem::Service(path, service) => { - router.path(&path, service) - } - CreateServiceItem::Future(_, _) => unreachable!(), - } - router - }); - Ok(Async::Ready(CloneableService::new(FramedAppService { - router: router.finish(), - state: self.state.clone(), - // default: self.default.take().expect("something is wrong"), - }))) - } else { - Ok(Async::NotReady) - } - } -} - -pub struct FramedAppService { - state: State, - router: Router, ()>>, -} - -impl Service for FramedAppService -where - T: AsyncRead + AsyncWrite, -{ - type Request = FRequest; - type Response = (); - type Error = (); - type Future = BoxedResponse; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - // let mut ready = true; - // for service in &mut self.services { - // if let Async::NotReady = service.poll_ready()? { - // ready = false; - // } - // } - // if ready { - // Ok(Async::Ready(())) - // } else { - // Ok(Async::NotReady) - // } - Ok(Async::Ready(())) - } - - fn call(&mut self, (req, framed): (Request, Framed)) -> Self::Future { - let mut path = Path::new(Url::new(req.uri().clone())); - - if let Some((srv, _info)) = self.router.recognize_mut(&mut path) { - return srv.call(FramedRequest::new( - WebRequest::new(self.state.clone(), req, path), - framed, - )); - } - // for item in &mut self.services { - // req = match item.handle(req) { - // Ok(fut) => return fut, - // Err(req) => req, - // }; - // } - // self.default.call(req) - Box::new( - SendResponse::send(framed, Response::NotFound().finish().into()) - .map(|_| ()) - .map_err(|_| ()), - ) - } -} diff --git a/src/framed_handler.rs b/src/framed_handler.rs deleted file mode 100644 index 109b5f0a2..000000000 --- a/src/framed_handler.rs +++ /dev/null @@ -1,379 +0,0 @@ -use std::marker::PhantomData; -use std::rc::Rc; - -use actix_codec::Framed; -use actix_http::{h1::Codec, Error}; -use actix_service::{NewService, Service}; -use futures::future::{ok, FutureResult}; -use futures::{Async, Future, IntoFuture, Poll}; -use log::error; - -use crate::handler::FromRequest; -use crate::request::Request; - -pub struct FramedError { - pub err: Error, - pub framed: Framed, -} - -pub struct FramedRequest { - req: Request, - framed: Framed, - param: Ex, -} - -impl FramedRequest { - pub fn new(req: Request, framed: Framed) -> Self { - Self { - req, - framed, - param: (), - } - } -} - -impl FramedRequest { - pub fn request(&self) -> &Request { - &self.req - } - - pub fn request_mut(&mut self) -> &mut Request { - &mut self.req - } - - pub fn into_parts(self) -> (Request, Framed, Ex) { - (self.req, self.framed, self.param) - } - - pub fn map(self, op: F) -> FramedRequest - where - F: FnOnce(Ex) -> Ex2, - { - FramedRequest { - req: self.req, - framed: self.framed, - param: op(self.param), - } - } -} - -/// T handler converter factory -pub trait FramedFactory: Clone + 'static -where - R: IntoFuture, - E: Into, -{ - fn call(&self, framed: Framed, param: T, extra: Ex) -> R; -} - -#[doc(hidden)] -pub struct FramedHandle -where - F: FramedFactory, - R: IntoFuture, - E: Into, -{ - hnd: F, - _t: PhantomData<(S, Io, Ex, T, R, E)>, -} - -impl FramedHandle -where - F: FramedFactory, - R: IntoFuture, - E: Into, -{ - pub fn new(hnd: F) -> Self { - FramedHandle { - hnd, - _t: PhantomData, - } - } -} -impl NewService for FramedHandle -where - F: FramedFactory, - R: IntoFuture, - E: Into, -{ - type Request = (T, FramedRequest); - type Response = (); - type Error = FramedError; - type InitError = (); - type Service = FramedHandleService; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(FramedHandleService { - hnd: self.hnd.clone(), - _t: PhantomData, - }) - } -} - -#[doc(hidden)] -pub struct FramedHandleService -where - F: FramedFactory, - R: IntoFuture, - E: Into, -{ - hnd: F, - _t: PhantomData<(S, Io, Ex, T, R, E)>, -} - -impl Service for FramedHandleService -where - F: FramedFactory, - R: IntoFuture, - E: Into, -{ - type Request = (T, FramedRequest); - type Response = (); - type Error = FramedError; - type Future = FramedHandleServiceResponse; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, (param, framed): (T, FramedRequest)) -> Self::Future { - let (_, framed, ex) = framed.into_parts(); - FramedHandleServiceResponse { - fut: self.hnd.call(framed, param, ex).into_future(), - _t: PhantomData, - } - } -} - -#[doc(hidden)] -pub struct FramedHandleServiceResponse { - fut: F, - _t: PhantomData, -} - -impl Future for FramedHandleServiceResponse -where - F: Future, - F::Error: Into, -{ - type Item = (); - type Error = FramedError; - - fn poll(&mut self) -> Poll { - match self.fut.poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(res)) => Ok(Async::Ready(res.into())), - Err(e) => { - let e: Error = e.into(); - error!("Error in handler: {:?}", e); - Ok(Async::Ready(())) - } - } - } -} - -pub struct FramedExtract -where - T: FromRequest, -{ - cfg: Rc, - _t: PhantomData<(Io, Ex)>, -} - -impl FramedExtract -where - T: FromRequest + 'static, -{ - pub fn new(cfg: T::Config) -> FramedExtract { - FramedExtract { - cfg: Rc::new(cfg), - _t: PhantomData, - } - } -} -impl NewService for FramedExtract -where - T: FromRequest + 'static, -{ - type Request = FramedRequest; - type Response = (T, FramedRequest); - type Error = FramedError; - type InitError = (); - type Service = FramedExtractService; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(FramedExtractService { - cfg: self.cfg.clone(), - _t: PhantomData, - }) - } -} - -pub struct FramedExtractService -where - T: FromRequest, -{ - cfg: Rc, - _t: PhantomData<(Io, Ex)>, -} - -impl Service for FramedExtractService -where - T: FromRequest + 'static, -{ - type Request = FramedRequest; - type Response = (T, FramedRequest); - type Error = FramedError; - type Future = FramedExtractResponse; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: FramedRequest) -> Self::Future { - FramedExtractResponse { - fut: T::from_request(&req.request(), self.cfg.as_ref()), - req: Some(req), - } - } -} - -pub struct FramedExtractResponse -where - T: FromRequest + 'static, -{ - req: Option>, - fut: T::Future, -} - -impl Future for FramedExtractResponse -where - T: FromRequest + 'static, -{ - type Item = (T, FramedRequest); - type Error = FramedError; - - fn poll(&mut self) -> Poll { - match self.fut.poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(item)) => Ok(Async::Ready((item, self.req.take().unwrap()))), - Err(err) => Err(FramedError { - err: err.into(), - framed: self.req.take().unwrap().into_parts().1, - }), - } - } -} - -macro_rules! factory_tuple ({ ($(($nex:tt, $Ex:ident)),+), $(($n:tt, $T:ident)),+} => { - impl FramedFactory for Func - where Func: Fn(Framed, $($Ex,)+ $($T,)+) -> Res + Clone + 'static, - $($T: FromRequest + 'static,)+ - Res: IntoFuture + 'static, - Err: Into, - { - fn call(&self, framed: Framed, param: ($($T,)+), extra: ($($Ex,)+)) -> Res { - (self)(framed, $(extra.$nex,)+ $(param.$n,)+) - } - } -}); - -macro_rules! factory_tuple_unit ({$(($n:tt, $T:ident)),+} => { - impl FramedFactory for Func - where Func: Fn(Framed, $($T,)+) -> Res + Clone + 'static, - $($T: FromRequest + 'static,)+ - Res: IntoFuture + 'static, - Err: Into, - { - fn call(&self, framed: Framed, param: ($($T,)+), _extra: () ) -> Res { - (self)(framed, $(param.$n,)+) - } - } -}); - -#[cfg_attr(rustfmt, rustfmt_skip)] -mod m { - use super::*; - -factory_tuple_unit!((0, A)); -factory_tuple!(((0, Aex)), (0, A)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A)); - -factory_tuple_unit!((0, A), (1, B)); -factory_tuple!(((0, Aex)), (0, A), (1, B)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B)); - -factory_tuple_unit!((0, A), (1, B), (2, C)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C)); - -factory_tuple_unit!((0, A), (1, B), (2, C), (3, D)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D)); - -factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E)); - -factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); - -factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); - -factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); - -factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); - -factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -} diff --git a/src/framed_route.rs b/src/framed_route.rs deleted file mode 100644 index 90555a9c8..000000000 --- a/src/framed_route.rs +++ /dev/null @@ -1,448 +0,0 @@ -use std::marker::PhantomData; - -use actix_http::http::{HeaderName, HeaderValue, Method}; -use actix_http::Error; -use actix_service::{IntoNewService, NewService, NewServiceExt, Service}; -use futures::{try_ready, Async, Future, IntoFuture, Poll}; -use log::{debug, error}; -use tokio_io::{AsyncRead, AsyncWrite}; - -use crate::app::{HttpServiceFactory, State}; -use crate::framed_handler::{ - FramedError, FramedExtract, FramedFactory, FramedHandle, FramedRequest, -}; -use crate::handler::FromRequest; - -/// Resource route definition -/// -/// Route uses builder-like pattern for configuration. -/// If handler is not explicitly set, default *404 Not Found* handler is used. -pub struct FramedRoute { - service: T, - pattern: String, - methods: Vec, - headers: Vec<(HeaderName, HeaderValue)>, - state: PhantomData<(S, Io)>, -} - -impl FramedRoute { - pub fn build(path: &str) -> FramedRoutePatternBuilder { - FramedRoutePatternBuilder::new(path) - } - - pub fn get(path: &str) -> FramedRoutePatternBuilder { - FramedRoutePatternBuilder::new(path).method(Method::GET) - } - - pub fn post(path: &str) -> FramedRoutePatternBuilder { - FramedRoutePatternBuilder::new(path).method(Method::POST) - } - - pub fn put(path: &str) -> FramedRoutePatternBuilder { - FramedRoutePatternBuilder::new(path).method(Method::PUT) - } - - pub fn delete(path: &str) -> FramedRoutePatternBuilder { - FramedRoutePatternBuilder::new(path).method(Method::DELETE) - } -} - -impl FramedRoute -where - T: NewService< - Request = FramedRequest, - Response = (), - Error = FramedError, - > + 'static, -{ - pub fn new>(pattern: &str, factory: F) -> Self { - FramedRoute { - pattern: pattern.to_string(), - service: factory.into_new_service(), - headers: Vec::new(), - methods: Vec::new(), - state: PhantomData, - } - } - - pub fn method(mut self, method: Method) -> Self { - self.methods.push(method); - self - } - - pub fn header(mut self, name: HeaderName, value: HeaderValue) -> Self { - self.headers.push((name, value)); - self - } -} - -impl HttpServiceFactory for FramedRoute -where - Io: AsyncRead + AsyncWrite + 'static, - T: NewService< - Request = FramedRequest, - Response = (), - Error = FramedError, - > + 'static, - T::Service: 'static, -{ - type Factory = FramedRouteFactory; - - fn path(&self) -> &str { - &self.pattern - } - - fn create(self, state: State) -> Self::Factory { - FramedRouteFactory { - state, - service: self.service, - pattern: self.pattern, - methods: self.methods, - headers: self.headers, - _t: PhantomData, - } - } -} - -pub struct FramedRouteFactory { - service: T, - pattern: String, - methods: Vec, - headers: Vec<(HeaderName, HeaderValue)>, - state: State, - _t: PhantomData, -} - -impl NewService for FramedRouteFactory -where - Io: AsyncRead + AsyncWrite + 'static, - T: NewService< - Request = FramedRequest, - Response = (), - Error = FramedError, - > + 'static, - T::Service: 'static, -{ - type Request = FramedRequest; - type Response = T::Response; - type Error = (); - type InitError = T::InitError; - type Service = FramedRouteService; - type Future = CreateRouteService; - - fn new_service(&self) -> Self::Future { - CreateRouteService { - fut: self.service.new_service(), - pattern: self.pattern.clone(), - methods: self.methods.clone(), - headers: self.headers.clone(), - state: self.state.clone(), - _t: PhantomData, - } - } -} - -pub struct CreateRouteService { - fut: T::Future, - pattern: String, - methods: Vec, - headers: Vec<(HeaderName, HeaderValue)>, - state: State, - _t: PhantomData, -} - -impl Future for CreateRouteService -where - T: NewService< - Request = FramedRequest, - Response = (), - Error = FramedError, - >, -{ - type Item = FramedRouteService; - type Error = T::InitError; - - fn poll(&mut self) -> Poll { - let service = try_ready!(self.fut.poll()); - - Ok(Async::Ready(FramedRouteService { - service, - state: self.state.clone(), - pattern: self.pattern.clone(), - methods: self.methods.clone(), - headers: self.headers.clone(), - _t: PhantomData, - })) - } -} - -pub struct FramedRouteService { - service: T, - pattern: String, - methods: Vec, - headers: Vec<(HeaderName, HeaderValue)>, - state: State, - _t: PhantomData, -} - -impl Service for FramedRouteService -where - Io: AsyncRead + AsyncWrite + 'static, - T: Service, Response = (), Error = FramedError> - + 'static, -{ - type Request = FramedRequest; - type Response = (); - type Error = (); - type Future = FramedRouteServiceResponse; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.service.poll_ready().map_err(|e| { - debug!("Service not available: {}", e.err); - () - }) - } - - fn call(&mut self, req: FramedRequest) -> Self::Future { - FramedRouteServiceResponse { - fut: self.service.call(req), - send: None, - _t: PhantomData, - } - } -} - -// impl HttpService<(Request, Framed)> for FramedRouteService -// where -// Io: AsyncRead + AsyncWrite + 'static, -// S: 'static, -// T: Service, Response = (), Error = FramedError> + 'static, -// { -// fn handle( -// &mut self, -// (req, framed): (Request, Framed), -// ) -> Result)> { -// if self.methods.is_empty() -// || !self.methods.is_empty() && self.methods.contains(req.method()) -// { -// if let Some(params) = self.pattern.match_with_params(&req, 0) { -// return Ok(FramedRouteServiceResponse { -// fut: self.service.call(FramedRequest::new( -// WebRequest::new(self.state.clone(), req, params), -// framed, -// )), -// send: None, -// _t: PhantomData, -// }); -// } -// } -// Err((req, framed)) -// } -// } - -#[doc(hidden)] -pub struct FramedRouteServiceResponse { - fut: F, - send: Option>>, - _t: PhantomData, -} - -impl Future for FramedRouteServiceResponse -where - F: Future>, - Io: AsyncRead + AsyncWrite + 'static, -{ - type Item = (); - type Error = (); - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.send { - return match fut.poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(_)) => Ok(Async::Ready(())), - Err(e) => { - debug!("Error during error response send: {}", e); - Err(()) - } - }; - }; - - match self.fut.poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(_)) => Ok(Async::Ready(())), - Err(e) => { - error!("Error occurred during request handling: {}", e.err); - Err(()) - } - } - } -} - -pub struct FramedRoutePatternBuilder { - pattern: String, - methods: Vec, - headers: Vec<(HeaderName, HeaderValue)>, - state: PhantomData<(Io, S)>, -} - -impl FramedRoutePatternBuilder { - fn new(path: &str) -> FramedRoutePatternBuilder { - FramedRoutePatternBuilder { - pattern: path.to_string(), - methods: Vec::new(), - headers: Vec::new(), - state: PhantomData, - } - } - - pub fn method(mut self, method: Method) -> Self { - self.methods.push(method); - self - } - - pub fn map>( - self, - md: F, - ) -> FramedRouteBuilder - where - T: NewService< - Request = FramedRequest, - Response = FramedRequest, - Error = FramedError, - InitError = (), - >, - { - FramedRouteBuilder { - service: md.into_new_service(), - pattern: self.pattern, - methods: self.methods, - headers: self.headers, - state: PhantomData, - } - } - - pub fn with( - self, - handler: F, - ) -> FramedRoute< - Io, - impl NewService< - Request = FramedRequest, - Response = (), - Error = FramedError, - InitError = (), - >, - S, - > - where - F: FramedFactory, - P: FromRequest + 'static, - R: IntoFuture, - E: Into, - { - FramedRoute { - service: FramedExtract::new(P::Config::default()) - .and_then(FramedHandle::new(handler)), - pattern: self.pattern, - methods: self.methods, - headers: self.headers, - state: PhantomData, - } - } -} - -pub struct FramedRouteBuilder { - service: T, - pattern: String, - methods: Vec, - headers: Vec<(HeaderName, HeaderValue)>, - state: PhantomData<(Io, S, U1, U2)>, -} - -impl FramedRouteBuilder -where - T: NewService< - Request = FramedRequest, - Response = FramedRequest, - Error = FramedError, - InitError = (), - >, -{ - pub fn new>(path: &str, factory: F) -> Self { - FramedRouteBuilder { - service: factory.into_new_service(), - pattern: path.to_string(), - methods: Vec::new(), - headers: Vec::new(), - state: PhantomData, - } - } - - pub fn method(mut self, method: Method) -> Self { - self.methods.push(method); - self - } - - pub fn map>( - self, - md: F, - ) -> FramedRouteBuilder< - Io, - S, - impl NewService< - Request = FramedRequest, - Response = FramedRequest, - Error = FramedError, - InitError = (), - >, - U1, - U3, - > - where - K: NewService< - Request = FramedRequest, - Response = FramedRequest, - Error = FramedError, - InitError = (), - >, - { - FramedRouteBuilder { - service: self.service.from_err().and_then(md.into_new_service()), - pattern: self.pattern, - methods: self.methods, - headers: self.headers, - state: PhantomData, - } - } - - pub fn with( - self, - handler: F, - ) -> FramedRoute< - Io, - impl NewService< - Request = FramedRequest, - Response = (), - Error = FramedError, - InitError = (), - >, - S, - > - where - F: FramedFactory, - P: FromRequest + 'static, - R: IntoFuture, - E: Into, - { - FramedRoute { - service: self - .service - .and_then(FramedExtract::new(P::Config::default())) - .and_then(FramedHandle::new(handler)), - pattern: self.pattern, - methods: self.methods, - headers: self.headers, - state: PhantomData, - } - } -} diff --git a/src/helpers.rs b/src/helpers.rs deleted file mode 100644 index 860a02a4d..000000000 --- a/src/helpers.rs +++ /dev/null @@ -1,180 +0,0 @@ -use actix_http::Response; -use actix_service::{NewService, Service}; -use futures::future::{ok, FutureResult}; -use futures::{Future, Poll}; - -pub(crate) type BoxedHttpService = Box< - Service< - Request = Req, - Response = Res, - Error = (), - Future = Box>, - >, ->; - -pub(crate) type BoxedHttpNewService = Box< - NewService< - Request = Req, - Response = Res, - Error = (), - InitError = (), - Service = BoxedHttpService, - Future = Box, Error = ()>>, - >, ->; - -pub(crate) struct HttpNewService(T); - -impl HttpNewService -where - T: NewService, - T::Response: 'static, - T::Future: 'static, - T::Service: Service, - ::Future: 'static, -{ - pub fn new(service: T) -> Self { - HttpNewService(service) - } -} - -impl NewService for HttpNewService -where - T: NewService, - T::Request: 'static, - T::Response: 'static, - T::Future: 'static, - T::Service: Service + 'static, - ::Future: 'static, -{ - type Request = T::Request; - type Response = T::Response; - type Error = (); - type InitError = (); - type Service = BoxedHttpService; - type Future = Box>; - - fn new_service(&self, _: &()) -> Self::Future { - Box::new(self.0.new_service(&()).map_err(|_| ()).and_then(|service| { - let service: BoxedHttpService<_, _> = - Box::new(HttpServiceWrapper { service }); - Ok(service) - })) - } -} - -struct HttpServiceWrapper { - service: T, -} - -impl Service for HttpServiceWrapper -where - T: Service, - T::Request: 'static, - T::Response: 'static, - T::Future: 'static, -{ - type Request = T::Request; - type Response = T::Response; - type Error = (); - type Future = Box>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.service.poll_ready().map_err(|_| ()) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - Box::new(self.service.call(req).map_err(|_| ())) - } -} - -pub(crate) fn not_found(_: Req) -> FutureResult { - ok(Response::NotFound().finish()) -} - -pub(crate) type HttpDefaultService = Box< - Service< - Request = Req, - Response = Res, - Error = (), - Future = Box>, - >, ->; - -pub(crate) type HttpDefaultNewService = Box< - NewService< - Request = Req, - Response = Res, - Error = (), - InitError = (), - Service = HttpDefaultService, - Future = Box, Error = ()>>, - >, ->; - -pub(crate) struct DefaultNewService { - service: T, -} - -impl DefaultNewService -where - T: NewService + 'static, - T::Future: 'static, - ::Future: 'static, -{ - pub fn new(service: T) -> Self { - DefaultNewService { service } - } -} - -impl NewService for DefaultNewService -where - T: NewService + 'static, - T::Request: 'static, - T::Future: 'static, - T::Service: 'static, - ::Future: 'static, -{ - type Request = T::Request; - type Response = T::Response; - type Error = (); - type InitError = (); - type Service = HttpDefaultService; - type Future = Box>; - - fn new_service(&self, _: &()) -> Self::Future { - Box::new( - self.service - .new_service(&()) - .map_err(|_| ()) - .and_then(|service| { - let service: HttpDefaultService<_, _> = - Box::new(DefaultServiceWrapper { service }); - Ok(service) - }), - ) - } -} - -struct DefaultServiceWrapper { - service: T, -} - -impl Service for DefaultServiceWrapper -where - T: Service + 'static, - T::Future: 'static, -{ - type Request = T::Request; - type Response = T::Response; - type Error = (); - type Future = Box>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.service.poll_ready().map_err(|_| ()) - } - - fn call(&mut self, req: T::Request) -> Self::Future { - Box::new(self.service.call(req).map_err(|_| ())) - } -} diff --git a/src/lib.rs b/src/lib.rs index f09c11ced..b4b12eb14 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,11 +3,10 @@ mod app; mod extractor; pub mod handler; -mod helpers; +// mod helpers; // mod info; pub mod blocking; pub mod filter; -pub mod fs; pub mod middleware; mod request; mod resource; diff --git a/src/resource.rs b/src/resource.rs index 88f7ae5a9..80ac8d83b 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -2,6 +2,7 @@ use std::cell::RefCell; use std::rc::Rc; use actix_http::{http::Method, Error, Response}; +use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, }; @@ -9,11 +10,13 @@ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; use crate::handler::{AsyncFactory, Factory, FromRequest}; -use crate::helpers::{DefaultNewService, HttpDefaultNewService, HttpDefaultService}; use crate::responder::Responder; use crate::route::{CreateRouteService, Route, RouteBuilder, RouteService}; use crate::service::{ServiceRequest, ServiceResponse}; +type HttpService

    = BoxedService, ServiceResponse, ()>; +type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; + /// Resource route definition /// /// Route uses builder-like pattern for configuration. @@ -21,9 +24,7 @@ use crate::service::{ServiceRequest, ServiceResponse}; pub struct Resource> { routes: Vec>, endpoint: T, - default: Rc< - RefCell, ServiceResponse>>>>, - >, + default: Rc>>>>, factory_ref: Rc>>>, } @@ -277,17 +278,14 @@ where > + 'static, { // create and configure default resource - self.default = Rc::new(RefCell::new(Some(Rc::new(Box::new( - DefaultNewService::new(f(Resource::new()).into_new_service()), + self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::new_service( + f(Resource::new()).into_new_service().map_init_err(|_| ()), ))))); self } - pub(crate) fn get_default( - &self, - ) -> Rc, ServiceResponse>>>>> - { + pub(crate) fn get_default(&self) -> Rc>>>> { self.default.clone() } } @@ -313,12 +311,10 @@ where pub struct ResourceFactory

    { routes: Vec>, - default: Rc< - RefCell, ServiceResponse>>>>, - >, + default: Rc>>>>, } -impl

    NewService for ResourceFactory

    { +impl NewService for ResourceFactory

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); @@ -352,15 +348,8 @@ enum CreateRouteServiceItem

    { pub struct CreateResourceService

    { fut: Vec>, - default: Option, ServiceResponse>>, - default_fut: Option< - Box< - Future< - Item = HttpDefaultService, ServiceResponse>, - Error = (), - >, - >, - >, + default: Option>, + default_fut: Option, Error = ()>>>, } impl

    Future for CreateResourceService

    { @@ -413,7 +402,7 @@ impl

    Future for CreateResourceService

    { pub struct ResourceService

    { routes: Vec>, - default: Option, ServiceResponse>>, + default: Option>, } impl

    Service for ResourceService

    { @@ -461,7 +450,7 @@ impl

    ResourceEndpoint

    { } } -impl

    NewService for ResourceEndpoint

    { +impl NewService for ResourceEndpoint

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); diff --git a/tests/test_server.rs b/tests/test_server.rs index 2d01d270c..590cc0e7b 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -12,7 +12,7 @@ use flate2::write::ZlibDecoder; use futures::stream::once; //Future, Stream use rand::{distributions::Alphanumeric, Rng}; -use actix_web2::{middleware, App}; +use actix_web::{middleware, App}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ From bc3c29c39868bf940f19de1960e0f876b15c5636 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 00:04:39 -0800 Subject: [PATCH 154/427] update version --- .travis.yml | 10 ++++------ Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9b1bcff54..077989d27 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,15 +32,13 @@ script: - | if [[ "$TRAVIS_RUST_VERSION" != "nightly" ]]; then cargo clean - cargo check --features rust-tls - cargo check --features ssl - cargo check --features tls - cargo test --features="ssl,tls,rust-tls,uds" -- --nocapture + cargo check + cargo test -- --nocapture fi - | if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install -f cargo-tarpaulin - RUST_BACKTRACE=1 cargo tarpaulin --features="ssl,tls,rust-tls" --out Xml + RUST_BACKTRACE=1 cargo tarpaulin --out Xml bash <(curl -s https://codecov.io/bash) echo "Uploaded code coverage" fi @@ -49,7 +47,7 @@ script: after_success: - | if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then - cargo doc --features "ssl,tls,rust-tls,session" --no-deps && + cargo doc --no-deps && echo "" > target/doc/index.html && git clone https://github.com/davisp/ghp-import.git && ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc && diff --git a/Cargo.toml b/Cargo.toml index e9c298fad..3cc42d801 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "0.1.0" +version = "1.0.0-alpha.1" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" From fdf30118378f0b84004d39624daa7ba36ac67535 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 09:05:07 -0800 Subject: [PATCH 155/427] add responder for unit type --- Cargo.toml | 1 + src/application.rs | 785 --------------------------------------------- src/lib.rs | 1 - src/responder.rs | 26 +- 4 files changed, 15 insertions(+), 798 deletions(-) delete mode 100644 src/application.rs diff --git a/Cargo.toml b/Cargo.toml index 3cc42d801..bfd061015 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ actix-router = { git = "https://github.com/actix/actix-net.git" } bytes = "0.4" futures = "0.1" derive_more = "0.14" +either = "1.5.1" log = "0.4" lazy_static = "1.2" mime = "0.3" diff --git a/src/application.rs b/src/application.rs deleted file mode 100644 index 6ca4ce285..000000000 --- a/src/application.rs +++ /dev/null @@ -1,785 +0,0 @@ -use std::rc::Rc; - -use actix_http::http::ContentEncoding; -use actix_http::{Error, Request, Response}; -use actix_service::Service; -use futures::{Async, Future, Poll}; - -use handler::{AsyncResult, FromRequest, Handler, Responder, WrapHandler}; -use http::Method; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -// use middleware::Middleware; -// use pipeline::{Pipeline, PipelineHandler}; -use pred::Predicate; -use resource::Resource; -use router::{ResourceDef, Router}; -// use scope::Scope; -// use server::{HttpHandler, HttpHandlerTask, IntoHttpHandler, Request}; -use with::WithFactory; - -/// Application -pub struct HttpApplication { - state: Rc, - prefix: String, - prefix_len: usize, - inner: Rc>, - filters: Option>>>, - // middlewares: Rc>>>, -} - -#[doc(hidden)] -pub struct Inner { - router: Router, - encoding: ContentEncoding, -} - -// impl PipelineHandler for Inner { -// #[inline] -// fn encoding(&self) -> ContentEncoding { -// self.encoding -// } - -// fn handle(&self, req: &HttpRequest) -> AsyncResult { -// self.router.handle(req) -// } -// } - -impl HttpApplication { - #[cfg(test)] - pub(crate) fn run(&self, req: Request) -> AsyncResult { - let info = self - .inner - .router - .recognize(&req, &self.state, self.prefix_len); - let req = HttpRequest::new(req, Rc::clone(&self.state), info); - - self.inner.handle(&req) - } -} - -impl Service for HttpApplication { - // type Task = Pipeline>; - type Request = actix_http::Request; - type Response = actix_http::Response; - type Error = Error; - type Future = Box>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, msg: actix_http::Request) -> Self::Future { - let m = { - if self.prefix_len == 0 { - true - } else { - let path = msg.path(); - path.starts_with(&self.prefix) - && (path.len() == self.prefix_len - || path.split_at(self.prefix_len).1.starts_with('/')) - } - }; - if m { - if let Some(ref filters) = self.filters { - //for filter in filters { - // if !filter.check(&msg, &self.state) { - //return Err(msg); - unimplemented!() - // } - //} - } - - let info = self - .inner - .router - .recognize(&msg, &self.state, self.prefix_len); - - let inner = Rc::clone(&self.inner); - // let req = HttpRequest::new(msg, Rc::clone(&self.state), info); - // Ok(Pipeline::new(req, inner)) - unimplemented!() - } else { - // Err(msg) - unimplemented!() - } - } -} - -struct ApplicationParts { - state: S, - prefix: String, - router: Router, - encoding: ContentEncoding, - // middlewares: Vec>>, - filters: Vec>>, -} - -/// Structure that follows the builder pattern for building application -/// instances. -pub struct App { - parts: Option>, -} - -impl App -where - S: 'static, -{ - /// Set application prefix. - /// - /// Only requests that match the application's prefix get - /// processed by this application. - /// - /// The application prefix always contains a leading slash (`/`). - /// If the supplied prefix does not contain leading slash, it is - /// inserted. - /// - /// Prefix should consist of valid path segments. i.e for an - /// application with the prefix `/app` any request with the paths - /// `/app`, `/app/` or `/app/test` would match, but the path - /// `/application` would not. - /// - /// In the following example only requests with an `/app/` path - /// prefix get handled. Requests with path `/app/test/` would be - /// handled, while requests with the paths `/application` or - /// `/other/...` would return `NOT FOUND`. It is also possible to - /// handle `/app` path, to do this you can register resource for - /// empty string `""` - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new() - /// .prefix("/app") - /// .resource("", |r| r.f(|_| HttpResponse::Ok())) // <- handle `/app` path - /// .resource("/", |r| r.f(|_| HttpResponse::Ok())) // <- handle `/app/` path - /// .resource("/test", |r| { - /// r.get().f(|_| HttpResponse::Ok()); - /// r.head().f(|_| HttpResponse::MethodNotAllowed()); - /// }) - /// .finish(); - /// } - /// ``` - pub fn prefix>(mut self, prefix: P) -> App { - { - let parts = self.parts.as_mut().expect("Use after finish"); - let mut prefix = prefix.into(); - if !prefix.starts_with('/') { - prefix.insert(0, '/') - } - parts.router.set_prefix(&prefix); - parts.prefix = prefix; - } - self - } - - /// Configure route for a specific path. - /// - /// This is a simplified version of the `App::resource()` method. - /// Handler functions need to accept one request extractor - /// argument. - /// - /// This method could be called multiple times, in that case - /// multiple routes would be registered for same resource path. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// let app = App::new() - /// .route("/test", http::Method::GET, |_: HttpRequest| { - /// HttpResponse::Ok() - /// }) - /// .route("/test", http::Method::POST, |_: HttpRequest| { - /// HttpResponse::MethodNotAllowed() - /// }); - /// } - /// ``` - pub fn route(mut self, path: &str, method: Method, f: F) -> App - where - F: WithFactory, - R: Responder + 'static, - T: FromRequest + 'static, - { - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_route(path, method, f); - - self - } - - // /// Configure scope for common root path. - // /// - // /// Scopes collect multiple paths under a common path prefix. - // /// Scope path can contain variable path segments as resources. - // /// - // /// ```rust - // /// # extern crate actix_web; - // /// use actix_web::{http, App, HttpRequest, HttpResponse}; - // /// - // /// fn main() { - // /// let app = App::new().scope("/{project_id}", |scope| { - // /// scope - // /// .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - // /// .resource("/path2", |r| r.f(|_| HttpResponse::Ok())) - // /// .resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed())) - // /// }); - // /// } - // /// ``` - // /// - // /// In the above example, three routes get added: - // /// * /{project_id}/path1 - // /// * /{project_id}/path2 - // /// * /{project_id}/path3 - // /// - // pub fn scope(mut self, path: &str, f: F) -> App - // where - // F: FnOnce(Scope) -> Scope, - // { - // let scope = f(Scope::new(path)); - // self.parts - // .as_mut() - // .expect("Use after finish") - // .router - // .register_scope(scope); - // self - // } - - /// Configure resource for a specific path. - /// - /// Resources may have variable path segments. For example, a - /// resource with the path `/a/{name}/c` would match all incoming - /// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. - /// - /// A variable segment is specified in the form `{identifier}`, - /// where the identifier can be used later in a request handler to - /// access the matched value for that segment. This is done by - /// looking up the identifier in the `Params` object returned by - /// `HttpRequest.match_info()` method. - /// - /// By default, each segment matches the regular expression `[^{}/]+`. - /// - /// You can also specify a custom regex in the form `{identifier:regex}`: - /// - /// For instance, to route `GET`-requests on any route matching - /// `/users/{userid}/{friend}` and store `userid` and `friend` in - /// the exposed `Params` object: - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().resource("/users/{userid}/{friend}", |r| { - /// r.get().f(|_| HttpResponse::Ok()); - /// r.head().f(|_| HttpResponse::MethodNotAllowed()); - /// }); - /// } - /// ``` - pub fn resource(mut self, path: &str, f: F) -> App - where - F: FnOnce(&mut Resource) -> R + 'static, - { - { - let parts = self.parts.as_mut().expect("Use after finish"); - - // create resource - let mut resource = Resource::new(ResourceDef::new(path)); - - // configure - f(&mut resource); - - parts.router.register_resource(resource); - } - self - } - - /// Configure resource for a specific path. - #[doc(hidden)] - pub fn register_resource(&mut self, resource: Resource) { - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_resource(resource); - } - - /// Default resource to be used if no matching route could be found. - pub fn default_resource(mut self, f: F) -> App - where - F: FnOnce(&mut Resource) -> R + 'static, - { - // create and configure default resource - let mut resource = Resource::new(ResourceDef::new("")); - f(&mut resource); - - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_default_resource(resource.into()); - - self - } - - /// Configure handler for specific path prefix. - /// - /// A path prefix consists of valid path segments, i.e for the - /// prefix `/app` any request with the paths `/app`, `/app/` or - /// `/app/test` would match, but the path `/application` would - /// not. - /// - /// Path tail is available as `tail` parameter in request's match_dict. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().handler("/app", |req: &HttpRequest| match *req.method() { - /// http::Method::GET => HttpResponse::Ok(), - /// http::Method::POST => HttpResponse::MethodNotAllowed(), - /// _ => HttpResponse::NotFound(), - /// }); - /// } - /// ``` - pub fn handler>(mut self, path: &str, handler: H) -> App { - { - let mut path = path.trim().trim_right_matches('/').to_owned(); - if !path.is_empty() && !path.starts_with('/') { - path.insert(0, '/'); - }; - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_handler(&path, Box::new(WrapHandler::new(handler)), None); - } - self - } - - // /// Register a middleware. - // pub fn middleware>(mut self, mw: M) -> App { - // self.parts - // .as_mut() - // .expect("Use after finish") - // .middlewares - // .push(Box::new(mw)); - // self - // } - - /// Run external configuration as part of the application building - /// process - /// - /// This function is useful for moving parts of configuration to a - /// different module or event library. For example we can move - /// some of the resources' configuration to different module. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{fs, middleware, App, HttpResponse}; - /// - /// // this function could be located in different module - /// fn config(app: App) -> App { - /// app.resource("/test", |r| { - /// r.get().f(|_| HttpResponse::Ok()); - /// r.head().f(|_| HttpResponse::MethodNotAllowed()); - /// }) - /// } - /// - /// fn main() { - /// let app = App::new() - /// .middleware(middleware::Logger::default()) - /// .configure(config) // <- register resources - /// .handler("/static", fs::StaticFiles::new(".").unwrap()); - /// } - /// ``` - pub fn configure(self, cfg: F) -> App - where - F: Fn(App) -> App, - { - cfg(self) - } - - /// Finish application configuration and create `HttpHandler` object. - pub fn finish(&mut self) -> HttpApplication { - let mut parts = self.parts.take().expect("Use after finish"); - let prefix = parts.prefix.trim().trim_right_matches('/'); - parts.router.finish(); - - let inner = Rc::new(Inner { - router: parts.router, - encoding: parts.encoding, - }); - let filters = if parts.filters.is_empty() { - None - } else { - Some(parts.filters) - }; - - HttpApplication { - inner, - filters, - state: Rc::new(parts.state), - // middlewares: Rc::new(parts.middlewares), - prefix: prefix.to_owned(), - prefix_len: prefix.len(), - } - } - - // /// Convenience method for creating `Box` instances. - // /// - // /// This method is useful if you need to register multiple - // /// application instances with different state. - // /// - // /// ```rust - // /// # use std::thread; - // /// # extern crate actix_web; - // /// use actix_web::{server, App, HttpResponse}; - // /// - // /// struct State1; - // /// - // /// struct State2; - // /// - // /// fn main() { - // /// # thread::spawn(|| { - // /// server::new(|| { - // /// vec![ - // /// App::with_state(State1) - // /// .prefix("/app1") - // /// .resource("/", |r| r.f(|r| HttpResponse::Ok())) - // /// .boxed(), - // /// App::with_state(State2) - // /// .prefix("/app2") - // /// .resource("/", |r| r.f(|r| HttpResponse::Ok())) - // /// .boxed(), - // /// ] - // /// }).bind("127.0.0.1:8080") - // /// .unwrap() - // /// .run() - // /// # }); - // /// } - // /// ``` - // pub fn boxed(mut self) -> Box>> { - // Box::new(BoxedApplication { app: self.finish() }) - // } -} - -// struct BoxedApplication { -// app: HttpApplication, -// } - -// impl HttpHandler for BoxedApplication { -// type Task = Box; - -// fn handle(&self, req: Request) -> Result { -// self.app.handle(req).map(|t| { -// let task: Self::Task = Box::new(t); -// task -// }) -// } -// } - -// impl IntoHttpHandler for App { -// type Handler = HttpApplication; - -// fn into_handler(mut self) -> HttpApplication { -// self.finish() -// } -// } - -// impl<'a, S: 'static> IntoHttpHandler for &'a mut App { -// type Handler = HttpApplication; - -// fn into_handler(self) -> HttpApplication { -// self.finish() -// } -// } - -// #[doc(hidden)] -// impl Iterator for App { -// type Item = HttpApplication; - -// fn next(&mut self) -> Option { -// if self.parts.is_some() { -// Some(self.finish()) -// } else { -// None -// } -// } -// } - -#[cfg(test)] -mod tests { - use super::*; - use body::{Binary, Body}; - use http::StatusCode; - use httprequest::HttpRequest; - use httpresponse::HttpResponse; - use pred; - use test::{TestRequest, TestServer}; - - #[test] - fn test_default_resource() { - let app = App::new() - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .finish(); - - let req = TestRequest::with_uri("/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let app = App::new() - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .default_resource(|r| r.f(|_| HttpResponse::MethodNotAllowed())) - .finish(); - let req = TestRequest::with_uri("/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::METHOD_NOT_ALLOWED); - } - - #[test] - fn test_unhandled_prefix() { - let app = App::new() - .prefix("/test") - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .finish(); - let ctx = TestRequest::default().request(); - assert!(app.handle(ctx).is_err()); - } - - #[test] - fn test_state() { - let app = App::with_state(10) - .resource("/", |r| r.f(|_| HttpResponse::Ok())) - .finish(); - let req = TestRequest::with_state(10).request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_prefix() { - let app = App::new() - .prefix("/test") - .resource("/blah", |r| r.f(|_| HttpResponse::Ok())) - .finish(); - let req = TestRequest::with_uri("/test").request(); - let resp = app.handle(req); - assert!(resp.is_ok()); - - let req = TestRequest::with_uri("/test/").request(); - let resp = app.handle(req); - assert!(resp.is_ok()); - - let req = TestRequest::with_uri("/test/blah").request(); - let resp = app.handle(req); - assert!(resp.is_ok()); - - let req = TestRequest::with_uri("/testing").request(); - let resp = app.handle(req); - assert!(resp.is_err()); - } - - #[test] - fn test_handler() { - let app = App::new() - .handler("/test", |_: &_| HttpResponse::Ok()) - .finish(); - - let req = TestRequest::with_uri("/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_handler2() { - let app = App::new() - .handler("test", |_: &_| HttpResponse::Ok()) - .finish(); - - let req = TestRequest::with_uri("/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_handler_with_prefix() { - let app = App::new() - .prefix("prefix") - .handler("/test", |_: &_| HttpResponse::Ok()) - .finish(); - - let req = TestRequest::with_uri("/prefix/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/prefix/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/prefix/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/prefix/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/prefix/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_route() { - let app = App::new() - .route("/test", Method::GET, |_: HttpRequest| HttpResponse::Ok()) - .route("/test", Method::POST, |_: HttpRequest| { - HttpResponse::Created() - }) - .finish(); - - let req = TestRequest::with_uri("/test").method(Method::GET).request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - - let req = TestRequest::with_uri("/test") - .method(Method::HEAD) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_handler_prefix() { - let app = App::new() - .prefix("/app") - .handler("/test", |_: &_| HttpResponse::Ok()) - .finish(); - - let req = TestRequest::with_uri("/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_option_responder() { - let app = App::new() - .resource("/none", |r| r.f(|_| -> Option<&'static str> { None })) - .resource("/some", |r| r.f(|_| Some("some"))) - .finish(); - - let req = TestRequest::with_uri("/none").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/some").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - assert_eq!(resp.as_msg().body(), &Body::Binary(Binary::Slice(b"some"))); - } - - #[test] - fn test_filter() { - let mut srv = TestServer::with_factory(|| { - App::new() - .filter(pred::Get()) - .handler("/test", |_: &_| HttpResponse::Ok()) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - let request = srv.post().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_prefix_root() { - let mut srv = TestServer::with_factory(|| { - App::new() - .prefix("/test") - .resource("/", |r| r.f(|_| HttpResponse::Ok())) - .resource("", |r| r.f(|_| HttpResponse::Created())) - }); - - let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::CREATED); - } - -} diff --git a/src/lib.rs b/src/lib.rs index b4b12eb14..74fa0a94e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ mod app; mod extractor; pub mod handler; -// mod helpers; // mod info; pub mod blocking; pub mod filter; diff --git a/src/responder.rs b/src/responder.rs index 5520c610c..b3ec7ec73 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,13 +1,11 @@ -use actix_http::dev::ResponseBuilder; -use actix_http::http::StatusCode; -use actix_http::{Error, Response}; +use actix_http::{dev::ResponseBuilder, http::StatusCode, Error, Response}; use bytes::{Bytes, BytesMut}; use futures::future::{err, ok, Either as EitherFuture, FutureResult}; use futures::{Future, Poll}; use crate::request::HttpRequest; -/// Trait implemented by types that generate http responses. +/// Trait implemented by types that can be converted to a http response. /// /// Types that implement this trait can be used as the return type of a handler. pub trait Responder { @@ -72,6 +70,15 @@ impl Responder for ResponseBuilder { } } +impl Responder for () { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(Response::build(StatusCode::OK).finish()) + } +} + impl Responder for &'static str { type Error = Error; type Future = FutureResult; @@ -167,12 +174,7 @@ impl Responder for BytesMut { /// # fn is_a_variant() -> bool { true } /// # fn main() {} /// ``` -pub enum Either { - /// First branch of the type - A(A), - /// Second branch of the type - B(B), -} +pub type Either = either::Either; impl Responder for Either where @@ -184,8 +186,8 @@ where fn respond_to(self, req: &HttpRequest) -> Self::Future { match self { - Either::A(a) => EitherResponder::A(a.respond_to(req)), - Either::B(b) => EitherResponder::B(b.respond_to(req)), + either::Either::Left(a) => EitherResponder::A(a.respond_to(req)), + either::Either::Right(b) => EitherResponder::B(b.respond_to(req)), } } } From cc20fee62884d70990fbf0d0b4013172d8e2759d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 11:53:05 -0800 Subject: [PATCH 156/427] add request chain services --- src/app.rs | 480 ++++++++++++++++++++++++++----------- src/blocking.rs | 12 +- src/extractor.rs | 1 + src/lib.rs | 4 +- src/middleware/compress.rs | 3 - src/route.rs | 10 +- 6 files changed, 360 insertions(+), 150 deletions(-) diff --git a/src/app.rs b/src/app.rs index 7c0777060..78f718db7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::marker::PhantomData; use std::rc::Rc; -use actix_http::body::{Body, MessageBody}; +use actix_http::body::MessageBody; use actix_http::{Extensions, PayloadStream, Request, Response}; use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; @@ -30,32 +30,45 @@ pub trait HttpServiceFactory { } /// Application builder -pub struct App { - services: Vec<(ResourceDef, HttpNewService

    )>, - default: Option>>, - defaults: Vec>>>>>, - endpoint: T, - factory_ref: Rc>>>, +pub struct App +where + T: NewService, Response = ServiceRequest

    >, +{ + chain: T, extensions: Extensions, state: Vec>, - _t: PhantomData<(P, B)>, + _t: PhantomData<(P,)>, } -impl App> { - /// Create application with empty state. Application can +impl App { + /// Create application builder with empty state. Application can /// be configured with a builder-like pattern. pub fn new() -> Self { - App::create() + App { + chain: AppChain, + extensions: Extensions::new(), + state: Vec::new(), + _t: PhantomData, + } } } -impl Default for App> { +impl Default for App { fn default() -> Self { App::new() } } -impl App> { +impl App +where + P: 'static, + T: NewService< + Request = ServiceRequest, + Response = ServiceRequest

    , + Error = (), + InitError = (), + >, +{ /// Create application with specified state. Application can be /// configured with a builder-like pattern. /// @@ -86,38 +99,172 @@ impl App> { self } - fn create() -> Self { + /// Configure resource for a specific path. + /// + /// Resources may have variable path segments. For example, a + /// resource with the path `/a/{name}/c` would match all incoming + /// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. + /// + /// A variable segment is specified in the form `{identifier}`, + /// where the identifier can be used later in a request handler to + /// access the matched value for that segment. This is done by + /// looking up the identifier in the `Params` object returned by + /// `HttpRequest.match_info()` method. + /// + /// By default, each segment matches the regular expression `[^{}/]+`. + /// + /// You can also specify a custom regex in the form `{identifier:regex}`: + /// + /// For instance, to route `GET`-requests on any route matching + /// `/users/{userid}/{friend}` and store `userid` and `friend` in + /// the exposed `Params` object: + /// + /// ```rust,ignore + /// # extern crate actix_web; + /// use actix_web::{http, App, HttpResponse}; + /// + /// fn main() { + /// let app = App::new().resource("/users/{userid}/{friend}", |r| { + /// r.get(|r| r.to(|_| HttpResponse::Ok())); + /// r.head(|r| r.to(|_| HttpResponse::MethodNotAllowed())) + /// }); + /// } + /// ``` + pub fn resource(self, path: &str, f: F) -> AppRouter> + where + F: FnOnce(Resource

    ) -> Resource, + U: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, + { + let rdef = ResourceDef::new(path); + let resource = f(Resource::new()); + let default = resource.get_default(); + let fref = Rc::new(RefCell::new(None)); + AppRouter { + chain: self.chain, + services: vec![(rdef, boxed::new_service(resource.into_new_service()))], + default: None, + defaults: vec![default], + endpoint: AppEntry::new(fref.clone()), + factory_ref: fref, + extensions: self.extensions, + state: self.state, + _t: PhantomData, + } + } + + /// Register a middleware. + pub fn middleware( + self, + mw: F, + ) -> AppRouter< + T, + P, + B, + impl NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, + > + where + M: NewTransform< + AppService

    , + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, + B: MessageBody, + F: IntoNewTransform>, + { + let fref = Rc::new(RefCell::new(None)); + let endpoint = ApplyNewService::new(mw, AppEntry::new(fref.clone())); + AppRouter { + endpoint, + chain: self.chain, + state: self.state, + services: Vec::new(), + default: None, + defaults: Vec::new(), + factory_ref: fref, + extensions: self.extensions, + _t: PhantomData, + } + } + + /// Register a request modifier. It can modify any request parameters + /// including payload stream. + pub fn chain( + self, + chain: C, + ) -> App< + P1, + impl NewService< + Request = ServiceRequest, + Response = ServiceRequest, + Error = (), + InitError = (), + >, + > + where + C: NewService< + (), + Request = ServiceRequest

    , + Response = ServiceRequest, + Error = (), + InitError = (), + >, + F: IntoNewService, + { + let chain = self.chain.and_then(chain.into_new_service()); App { + chain, + state: self.state, + extensions: self.extensions, + _t: PhantomData, + } + } + + /// Complete applicatin chain configuration and start resource + /// configuration. + pub fn router(self) -> AppRouter> { + let fref = Rc::new(RefCell::new(None)); + AppRouter { + chain: self.chain, services: Vec::new(), default: None, defaults: Vec::new(), endpoint: AppEntry::new(fref.clone()), factory_ref: fref, - extensions: Extensions::new(), - state: Vec::new(), + extensions: self.extensions, + state: self.state, _t: PhantomData, } } } -// /// Application router builder -// pub struct AppRouter { -// services: Vec<( -// ResourceDef, -// BoxedHttpNewService, Response>, -// )>, -// default: Option, Response>>>, -// defaults: -// Vec, Response>>>>>>, -// state: AppState, -// endpoint: T, -// factory_ref: Rc>>>, -// extensions: Extensions, -// _t: PhantomData

    , -// } +/// Structure that follows the builder pattern for building application +/// instances. +pub struct AppRouter { + chain: C, + services: Vec<(ResourceDef, HttpNewService

    )>, + default: Option>>, + defaults: Vec>>>>>, + endpoint: T, + factory_ref: Rc>>>, + extensions: Extensions, + state: Vec>, + _t: PhantomData<(P, B)>, +} -impl App +impl AppRouter where P: 'static, B: MessageBody, @@ -221,7 +368,8 @@ where pub fn middleware( self, mw: F, - ) -> App< + ) -> AppRouter< + C, P, B1, impl NewService< @@ -243,14 +391,15 @@ where F: IntoNewTransform, { let endpoint = ApplyNewService::new(mw, self.endpoint); - App { + AppRouter { endpoint, + chain: self.chain, state: self.state, services: self.services, default: self.default, - defaults: Vec::new(), + defaults: self.defaults, factory_ref: self.factory_ref, - extensions: Extensions::new(), + extensions: self.extensions, _t: PhantomData, } } @@ -292,8 +441,8 @@ where } } -impl - IntoNewService, T, ()>> for App +impl + IntoNewService, T, ()>> for AppRouter where T: NewService< Request = ServiceRequest

    , @@ -301,8 +450,14 @@ where Error = (), InitError = (), >, + C: NewService< + Request = ServiceRequest, + Response = ServiceRequest

    , + Error = (), + InitError = (), + >, { - fn into_new_service(self) -> AndThenNewService, T, ()> { + fn into_new_service(self) -> AndThenNewService, T, ()> { // update resource default service if self.default.is_some() { for default in &self.defaults { @@ -317,99 +472,15 @@ where services: Rc::new(self.services), }); - AppStateFactory { + AppInit { + chain: self.chain, state: self.state, extensions: Rc::new(RefCell::new(Rc::new(self.extensions))), - _t: PhantomData, } .and_then(self.endpoint) } } -/// Service factory to convert `Request` to a `ServiceRequest` -pub struct AppStateFactory

    { - state: Vec>, - extensions: Rc>>, - _t: PhantomData

    , -} - -impl NewService for AppStateFactory

    { - type Request = Request

    ; - type Response = ServiceRequest

    ; - type Error = (); - type InitError = (); - type Service = AppStateService

    ; - type Future = AppStateFactoryResult

    ; - - fn new_service(&self, _: &()) -> Self::Future { - AppStateFactoryResult { - state: self.state.iter().map(|s| s.construct()).collect(), - extensions: self.extensions.clone(), - _t: PhantomData, - } - } -} - -#[doc(hidden)] -pub struct AppStateFactoryResult

    { - state: Vec>, - extensions: Rc>>, - _t: PhantomData

    , -} - -impl

    Future for AppStateFactoryResult

    { - type Item = AppStateService

    ; - type Error = (); - - fn poll(&mut self) -> Poll { - if let Some(extensions) = Rc::get_mut(&mut *self.extensions.borrow_mut()) { - let mut idx = 0; - while idx < self.state.len() { - if let Async::Ready(_) = self.state[idx].poll_result(extensions)? { - self.state.remove(idx); - } else { - idx += 1; - } - } - if !self.state.is_empty() { - return Ok(Async::NotReady); - } - } else { - log::warn!("Multiple copies of app extensions exists"); - } - - Ok(Async::Ready(AppStateService { - extensions: self.extensions.borrow().clone(), - _t: PhantomData, - })) - } -} - -/// Service to convert `Request` to a `ServiceRequest` -pub struct AppStateService

    { - extensions: Rc, - _t: PhantomData

    , -} - -impl

    Service for AppStateService

    { - type Request = Request

    ; - type Response = ServiceRequest

    ; - type Error = (); - type Future = FutureResult; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: Request

    ) -> Self::Future { - ok(ServiceRequest::new( - Path::new(Url::new(req.uri().clone())), - req, - self.extensions.clone(), - )) - } -} - pub struct AppFactory

    { services: Rc)>>, } @@ -530,17 +601,6 @@ impl

    Service for AppService

    { } } -pub struct AppServiceResponse(Box>); - -impl Future for AppServiceResponse { - type Item = ServiceResponse; - type Error = (); - - fn poll(&mut self) -> Poll { - self.0.poll().map_err(|_| panic!()) - } -} - #[doc(hidden)] pub struct AppEntry

    { factory: Rc>>>, @@ -564,3 +624,151 @@ impl NewService for AppEntry

    { self.factory.borrow_mut().as_mut().unwrap().new_service(&()) } } + +#[doc(hidden)] +pub struct AppChain; + +impl NewService<()> for AppChain { + type Request = ServiceRequest; + type Response = ServiceRequest; + type Error = (); + type InitError = (); + type Service = AppChain; + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(AppChain) + } +} + +impl Service for AppChain { + type Request = ServiceRequest; + type Response = ServiceRequest; + type Error = (); + type Future = FutureResult; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: Self::Request) -> Self::Future { + ok(req) + } +} + +/// Service factory to convert `Request` to a `ServiceRequest` +pub struct AppInit +where + C: NewService, Response = ServiceRequest

    >, +{ + chain: C, + state: Vec>, + extensions: Rc>>, +} + +impl NewService for AppInit +where + C: NewService< + Request = ServiceRequest, + Response = ServiceRequest

    , + InitError = (), + >, +{ + type Request = Request; + type Response = ServiceRequest

    ; + type Error = C::Error; + type InitError = C::InitError; + type Service = AppInitService; + type Future = AppInitResult; + + fn new_service(&self, _: &()) -> Self::Future { + AppInitResult { + chain: self.chain.new_service(&()), + state: self.state.iter().map(|s| s.construct()).collect(), + extensions: self.extensions.clone(), + } + } +} + +#[doc(hidden)] +pub struct AppInitResult +where + C: NewService< + Request = ServiceRequest, + Response = ServiceRequest

    , + InitError = (), + >, +{ + chain: C::Future, + state: Vec>, + extensions: Rc>>, +} + +impl Future for AppInitResult +where + C: NewService< + Request = ServiceRequest, + Response = ServiceRequest

    , + InitError = (), + >, +{ + type Item = AppInitService; + type Error = C::InitError; + + fn poll(&mut self) -> Poll { + if let Some(extensions) = Rc::get_mut(&mut *self.extensions.borrow_mut()) { + let mut idx = 0; + while idx < self.state.len() { + if let Async::Ready(_) = self.state[idx].poll_result(extensions)? { + self.state.remove(idx); + } else { + idx += 1; + } + } + if !self.state.is_empty() { + return Ok(Async::NotReady); + } + } else { + log::warn!("Multiple copies of app extensions exists"); + } + + let chain = futures::try_ready!(self.chain.poll()); + + Ok(Async::Ready(AppInitService { + chain, + extensions: self.extensions.borrow().clone(), + })) + } +} + +/// Service to convert `Request` to a `ServiceRequest` +pub struct AppInitService +where + C: Service, Response = ServiceRequest

    >, +{ + chain: C, + extensions: Rc, +} + +impl Service for AppInitService +where + C: Service, Response = ServiceRequest

    >, +{ + type Request = Request; + type Response = ServiceRequest

    ; + type Error = C::Error; + type Future = C::Future; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.chain.poll_ready() + } + + fn call(&mut self, req: Request) -> Self::Future { + let req = ServiceRequest::new( + Path::new(Url::new(req.uri().clone())), + req, + self.extensions.clone(), + ); + self.chain.call(req) + } +} diff --git a/src/blocking.rs b/src/blocking.rs index fc2624f6d..abf4282cf 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -24,7 +24,7 @@ lazy_static::lazy_static! { Mutex::new( threadpool::Builder::new() .thread_name("actix-web".to_owned()) - .num_threads(8) + .num_threads(default) .build(), ) }; @@ -45,11 +45,15 @@ pub enum BlockingError { /// to result of the function execution. pub fn run(f: F) -> CpuFuture where - F: FnOnce() -> Result, + F: FnOnce() -> Result + Send + 'static, + I: Send + 'static, + E: Send + 'static, { let (tx, rx) = oneshot::channel(); - POOL.with(move |pool| { - let _ = tx.send(f()); + POOL.with(|pool| { + pool.execute(move || { + let _ = tx.send(f()); + }) }); CpuFuture { rx } diff --git a/src/extractor.rs b/src/extractor.rs index 53209ad00..48c70b861 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] use std::ops::{Deref, DerefMut}; use std::rc::Rc; use std::{fmt, str}; diff --git a/src/lib.rs b/src/lib.rs index 74fa0a94e..c70f47e80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::type_complexity, dead_code, unused_variables)] +#![allow(clippy::type_complexity)] mod app; mod extractor; @@ -28,7 +28,7 @@ pub use crate::service::{ServiceRequest, ServiceResponse}; pub use crate::state::State; pub mod dev { - pub use crate::app::AppService; + pub use crate::app::AppRouter; pub use crate::handler::{AsyncFactory, Extract, Factory, Handle}; pub use crate::route::{Route, RouteBuilder}; // pub use crate::info::ConnectionInfo; diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index fee17ce13..5d5586cf7 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -117,7 +117,6 @@ where enum EncoderBody { Body(B), Other(Box), - None, } pub struct Encoder { @@ -131,7 +130,6 @@ impl MessageBody for Encoder { match self.body { EncoderBody::Body(ref b) => b.length(), EncoderBody::Other(ref b) => b.length(), - EncoderBody::None => BodyLength::Empty, } } else { BodyLength::Stream @@ -143,7 +141,6 @@ impl MessageBody for Encoder { let result = match self.body { EncoderBody::Body(ref mut b) => b.poll_next()?, EncoderBody::Other(ref mut b) => b.poll_next()?, - EncoderBody::None => return Ok(Async::Ready(None)), }; match result { Async::NotReady => return Ok(Async::NotReady), diff --git a/src/route.rs b/src/route.rs index 574e8e34c..4abb2f1de 100644 --- a/src/route.rs +++ b/src/route.rs @@ -320,11 +320,11 @@ impl RouteBuilder

    { } } -pub struct RouteServiceBuilder { - service: T, - filters: Vec>, - _t: PhantomData<(P, U1, U2)>, -} +// pub struct RouteServiceBuilder { +// service: T, +// filters: Vec>, +// _t: PhantomData<(P, U1, U2)>, +// } // impl RouteServiceBuilder // where From 75fbb9748051db5fdb70f8cbc0cb5d1e111ded15 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 13:57:00 -0800 Subject: [PATCH 157/427] update new transform trait --- src/handler.rs | 9 ++++----- src/middleware/mod.rs | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index e957d15e7..8076d4d42 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,8 +1,7 @@ use std::marker::PhantomData; use actix_http::{Error, Response}; -use actix_service::{NewService, Service}; -use actix_utils::Never; +use actix_service::{NewService, Service, Void}; use futures::future::{ok, FutureResult}; use futures::{try_ready, Async, Future, IntoFuture, Poll}; @@ -71,7 +70,7 @@ where { type Request = (T, HttpRequest); type Response = ServiceResponse; - type Error = Never; + type Error = Void; type InitError = (); type Service = HandleService; type Future = FutureResult; @@ -101,7 +100,7 @@ where { type Request = (T, HttpRequest); type Response = ServiceResponse; - type Error = Never; + type Error = Void; type Future = HandleServiceResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { @@ -128,7 +127,7 @@ where T::Error: Into, { type Item = ServiceResponse; - type Error = Never; + type Error = Void; fn poll(&mut self) -> Poll { match self.fut.poll() { diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index a8b4b3c67..8ef316b4d 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -47,7 +47,7 @@ where } } -impl NewTransform for MiddlewareFactory +impl NewTransform for MiddlewareFactory where T: Transform + Clone, S: Service, @@ -59,7 +59,7 @@ where type InitError = (); type Future = FutureResult; - fn new_transform(&self) -> Self::Future { + fn new_transform(&self, _: &C) -> Self::Future { ok(self.tr.clone()) } } From 3454812b687d2d22e4f1148d624574cf829aef32 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 13:59:12 -0800 Subject: [PATCH 158/427] rename actix-web-fs crate --- Cargo.toml | 2 +- {actix-web-fs => staticfiles}/CHANGES.md | 0 {actix-web-fs => staticfiles}/Cargo.toml | 4 ++-- {actix-web-fs => staticfiles}/README.md | 0 {actix-web-fs => staticfiles}/src/lib.rs | 0 5 files changed, 3 insertions(+), 3 deletions(-) rename {actix-web-fs => staticfiles}/CHANGES.md (100%) rename {actix-web-fs => staticfiles}/Cargo.toml (95%) rename {actix-web-fs => staticfiles}/README.md (100%) rename {actix-web-fs => staticfiles}/src/lib.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index bfd061015..fa6546864 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ path = "src/lib.rs" [workspace] members = [ ".", - "actix-web-fs", + "staticfiles", ] [features] diff --git a/actix-web-fs/CHANGES.md b/staticfiles/CHANGES.md similarity index 100% rename from actix-web-fs/CHANGES.md rename to staticfiles/CHANGES.md diff --git a/actix-web-fs/Cargo.toml b/staticfiles/Cargo.toml similarity index 95% rename from actix-web-fs/Cargo.toml rename to staticfiles/Cargo.toml index 5900a9365..c25163025 100644 --- a/actix-web-fs/Cargo.toml +++ b/staticfiles/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "actix-web-fs" +name = "actix-staticfiles" version = "0.1.0" authors = ["Nikolay Kim "] description = "Static files support for Actix web." @@ -14,7 +14,7 @@ edition = "2018" workspace = ".." [lib] -name = "actix_web_fs" +name = "actix_staticfiles" path = "src/lib.rs" [dependencies] diff --git a/actix-web-fs/README.md b/staticfiles/README.md similarity index 100% rename from actix-web-fs/README.md rename to staticfiles/README.md diff --git a/actix-web-fs/src/lib.rs b/staticfiles/src/lib.rs similarity index 100% rename from actix-web-fs/src/lib.rs rename to staticfiles/src/lib.rs From 9394a4e2a5513de678d17fd2669d5810254bcd71 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 14:07:21 -0800 Subject: [PATCH 159/427] cleanup dependencies --- Cargo.toml | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fa6546864..d5b2fa3f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" -documentation = "https://actix.rs/api/actix-web/stable/actix_web/" +documentation = "https://docs.rs/actix-web/" categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] @@ -16,8 +16,9 @@ exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] edition = "2018" [badges] -travis-ci = { repository = "actix/actix-web2", branch = "master" } -codecov = { repository = "actix/actix-web2", branch = "master", service = "github" } +travis-ci = { repository = "actix/actix-web", branch = "master" } +appveyor = { repository = "fafhrd91/actix-web-hdy9d" } +codecov = { repository = "actix/actix-web", branch = "master", service = "github" } [lib] name = "actix_web" @@ -46,34 +47,24 @@ actix-codec = "0.1.0" #actix-service = "0.2.1" #actix-server = "0.2.1" #actix-utils = "0.2.1" -actix-service = { git = "https://github.com/actix/actix-net.git" } -actix-server = { git = "https://github.com/actix/actix-net.git" } actix-utils = { git = "https://github.com/actix/actix-net.git" } - -actix-rt = "0.1.0" actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } +actix-service = { git = "https://github.com/actix/actix-net.git" } bytes = "0.4" -futures = "0.1" derive_more = "0.14" either = "1.5.1" +encoding = "0.2" +futures = "0.1" log = "0.4" lazy_static = "1.2" mime = "0.3" -mime_guess = "2.0.0-alpha" num_cpus = "1.10" -percent-encoding = "1.0" -cookie = { version="0.11", features=["percent-encode"] } -v_htmlescape = "0.4" +parking_lot = "0.7" serde = "1.0" serde_json = "1.0" -encoding = "0.2" serde_urlencoded = "^0.5.3" -parking_lot = "0.7" -hashbrown = "0.1" -regex = "1" -time = "0.1" threadpool = "1.7" # compression From de9b38295f2778e1f5a62e6971727257727679ab Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 15:08:10 -0800 Subject: [PATCH 160/427] update deps --- Cargo.toml | 20 +++++--------------- test-server/Cargo.toml | 10 +++------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index edf572af4..403f3303c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,18 +37,10 @@ session = ["cookie/secure"] ssl = ["openssl", "actix-connector/ssl"] [dependencies] -#actix-service = "0.2.1" +actix-service = "0.3.0" actix-codec = "0.1.0" -#actix-connector = "0.2.0" -#actix-utils = "0.2.2" - -actix-service = { git = "https://github.com/actix/actix-net" } -actix-connector = { git = "https://github.com/actix/actix-net" } -actix-utils = { git = "https://github.com/actix/actix-net" } - -#actix-service = { path = "../actix-net/actix-service" } -#actix-connector = { path = "../actix-net/actix-connector" } -#actix-utils = { path = "../actix-net/actix-utils" } +actix-connector = "0.3.0" +actix-utils = "0.3.0" base64 = "0.10" backtrace = "0.3" @@ -86,10 +78,8 @@ openssl = { version="0.10", optional = true } [dev-dependencies] actix-rt = "0.1.0" -actix-server = { git = "https://github.com/actix/actix-net", features=["ssl"] } -#actix-server = { path = "../actix-net/actix-server", features=["ssl"] } -#actix-connector = { path = "../actix-net/actix-connector", features=["ssl"] } -actix-connector = { git = "https://github.com/actix/actix-net", features=["ssl"] } +actix-server = { version = "0.3.0", features=["ssl"] } +actix-connector = { version = "0.3.0", features=["ssl"] } actix-http-test = { path="test-server", features=["ssl"] } env_logger = "0.6" serde_derive = "1.0" diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index f5e8afd22..cf4364c49 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -35,13 +35,9 @@ ssl = ["openssl", "actix-http/ssl", "actix-server/ssl"] actix-codec = "0.1" actix-rt = "0.1.0" actix-http = { path=".." } - -#actix-service = "0.2.0" -#actix-server = "0.2.0" -#actix-utils = "0.2.0" -actix-service = { git = "https://github.com/actix/actix-net" } -actix-server = { git = "https://github.com/actix/actix-net" } -actix-utils = { git = "https://github.com/actix/actix-net" } +actix-service = "0.3.0" +actix-server = "0.3.0" +actix-utils = "0.3.0" base64 = "0.10" bytes = "0.4" From 0081b9d4467e04a719e1715e8d141e05960ebc40 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 15:59:05 -0800 Subject: [PATCH 161/427] improve ergomonics of TestRequest --- src/response.rs | 1 - src/test.rs | 55 +++++++++++++++++++++++++++---------------------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/response.rs b/src/response.rs index aab881062..807487460 100644 --- a/src/response.rs +++ b/src/response.rs @@ -669,7 +669,6 @@ impl ResponseBuilder { } #[inline] -#[allow(clippy::borrowed_box)] fn parts<'a>( parts: &'a mut Option>, err: &Option, diff --git a/src/test.rs b/src/test.rs index b0e308728..74e5da71d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -37,7 +37,9 @@ use crate::Request; /// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); /// } /// ``` -pub struct TestRequest { +pub struct TestRequest(Option); + +struct Inner { version: Version, method: Method, uri: Uri, @@ -49,7 +51,7 @@ pub struct TestRequest { impl Default for TestRequest { fn default() -> TestRequest { - TestRequest { + TestRequest(Some(Inner { method: Method::GET, uri: Uri::from_str("/").unwrap(), version: Version::HTTP_11, @@ -57,19 +59,19 @@ impl Default for TestRequest { _cookies: None, payload: None, prefix: 0, - } + })) } } impl TestRequest { /// Create TestRequest and set request uri pub fn with_uri(path: &str) -> TestRequest { - TestRequest::default().uri(path) + TestRequest::default().uri(path).take() } /// Create TestRequest and set header pub fn with_hdr(hdr: H) -> TestRequest { - TestRequest::default().set(hdr) + TestRequest::default().set(hdr).take() } /// Create TestRequest and set header @@ -78,45 +80,45 @@ impl TestRequest { HeaderName: HttpTryFrom, V: IntoHeaderValue, { - TestRequest::default().header(key, value) + TestRequest::default().header(key, value).take() } /// Set HTTP version of this request - pub fn version(mut self, ver: Version) -> Self { - self.version = ver; + pub fn version(&mut self, ver: Version) -> &mut Self { + parts(&mut self.0).version = ver; self } /// Set HTTP method of this request - pub fn method(mut self, meth: Method) -> Self { - self.method = meth; + pub fn method(&mut self, meth: Method) -> &mut Self { + parts(&mut self.0).method = meth; self } /// Set HTTP Uri of this request - pub fn uri(mut self, path: &str) -> Self { - self.uri = Uri::from_str(path).unwrap(); + pub fn uri(&mut self, path: &str) -> &mut Self { + parts(&mut self.0).uri = Uri::from_str(path).unwrap(); self } /// Set a header - pub fn set(mut self, hdr: H) -> Self { + pub fn set(&mut self, hdr: H) -> &mut Self { if let Ok(value) = hdr.try_into() { - self.headers.append(H::name(), value); + parts(&mut self.0).headers.append(H::name(), value); return self; } panic!("Can not set header"); } /// Set a header - pub fn header(mut self, key: K, value: V) -> Self + pub fn header(&mut self, key: K, value: V) -> &mut Self where HeaderName: HttpTryFrom, V: IntoHeaderValue, { if let Ok(key) = HeaderName::try_from(key) { if let Ok(value) = value.try_into() { - self.headers.append(key, value); + parts(&mut self.0).headers.append(key, value); return self; } } @@ -124,29 +126,27 @@ impl TestRequest { } /// Set request payload - pub fn set_payload>(mut self, data: B) -> Self { + pub fn set_payload>(&mut self, data: B) -> &mut Self { let mut payload = crate::h1::Payload::empty(); payload.unread_data(data.into()); - self.payload = Some(payload.into()); + parts(&mut self.0).payload = Some(payload.into()); self } - /// Set request's prefix - pub fn prefix(mut self, prefix: u16) -> Self { - self.prefix = prefix; - self + pub(crate) fn take(&mut self) -> TestRequest { + TestRequest(self.0.take()) } /// Complete request creation and generate `Request` instance - pub fn finish(self) -> Request { - let TestRequest { + pub fn finish(&mut self) -> Request { + let Inner { method, uri, version, headers, payload, .. - } = self; + } = self.0.take().expect("cannot reuse test request builder");; let mut req = if let Some(pl) = payload { Request::with_payload(pl) @@ -251,3 +251,8 @@ impl TestRequest { // } // } } + +#[inline] +fn parts<'a>(parts: &'a mut Option) -> &'a mut Inner { + parts.as_mut().expect("cannot reuse test request builder") +} From 00ea195601d9d1daf2336329c18154038097c6dc Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 16:04:43 -0800 Subject: [PATCH 162/427] TestRequest::take public --- src/test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test.rs b/src/test.rs index 74e5da71d..4b925d020 100644 --- a/src/test.rs +++ b/src/test.rs @@ -133,7 +133,7 @@ impl TestRequest { self } - pub(crate) fn take(&mut self) -> TestRequest { + pub fn take(&mut self) -> TestRequest { TestRequest(self.0.take()) } From e4198a037a9d42d1546791c62f3605dec5909dab Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 16:24:14 -0800 Subject: [PATCH 163/427] add TestServiceRequest builder --- Cargo.toml | 11 +- src/app.rs | 13 ++- src/filter.rs | 288 ++++++++++++++++++++++++++------------------------ src/lib.rs | 1 + src/route.rs | 2 +- src/test.rs | 173 ++++++++++++------------------ 6 files changed, 229 insertions(+), 259 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d5b2fa3f0..30d23d027 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,13 +44,11 @@ flate2-rust = ["flate2/rust_backend"] [dependencies] actix-codec = "0.1.0" -#actix-service = "0.2.1" -#actix-server = "0.2.1" -#actix-utils = "0.2.1" -actix-utils = { git = "https://github.com/actix/actix-net.git" } +actix-service = "0.3.0" +actix-utils = "0.3.0" + actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } -actix-service = { git = "https://github.com/actix/actix-net.git" } bytes = "0.4" derive_more = "0.14" @@ -73,8 +71,7 @@ flate2 = { version="^1.0.2", optional = true, default-features = false } [dev-dependencies] actix-rt = "0.1.0" -#actix-server = { version="0.2", features=["ssl"] } -actix-server = { git = "https://github.com/actix/actix-net.git", features=["ssl"] } +actix-server = { version="0.3.0", features=["ssl"] } actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } rand = "0.6" diff --git a/src/app.rs b/src/app.rs index 78f718db7..2e45f2d3c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::marker::PhantomData; use std::rc::Rc; -use actix_http::body::MessageBody; +use actix_http::body::{Body, MessageBody}; use actix_http::{Extensions, PayloadStream, Request, Response}; use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; @@ -130,7 +130,7 @@ where /// }); /// } /// ``` - pub fn resource(self, path: &str, f: F) -> AppRouter> + pub fn resource(self, path: &str, f: F) -> AppRouter> where F: FnOnce(Resource

    ) -> Resource, U: NewService< @@ -159,16 +159,16 @@ where } /// Register a middleware. - pub fn middleware( + pub fn middleware( self, mw: F, ) -> AppRouter< T, P, - B, + Body, impl NewService< Request = ServiceRequest

    , - Response = ServiceResponse, + Response = ServiceResponse, Error = (), InitError = (), >, @@ -177,11 +177,10 @@ where M: NewTransform< AppService

    , Request = ServiceRequest

    , - Response = ServiceResponse, + Response = ServiceResponse, Error = (), InitError = (), >, - B: MessageBody, F: IntoNewTransform>, { let fref = Rc::new(RefCell::new(None)); diff --git a/src/filter.rs b/src/filter.rs index a0566092e..37c23d732 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -16,14 +16,13 @@ pub trait Filter { /// Return filter that matches if any of supplied filters. /// /// ```rust,ignore -/// # extern crate actix_web; -/// use actix_web2::{filter, App, HttpResponse}; +/// use actix_web::{filter, App, HttpResponse}; /// /// fn main() { /// App::new().resource("/index.html", |r| { /// r.route() /// .filter(pred::Any(pred::Get()).or(pred::Post())) -/// .f(|r| HttpResponse::MethodNotAllowed()) +/// .to(|| HttpResponse::MethodNotAllowed()) /// }); /// } /// ``` @@ -55,18 +54,18 @@ impl Filter for AnyFilter { /// Return filter that matches if all of supplied filters match. /// -/// ```rust,ignore +/// ```rust /// # extern crate actix_web; -/// use actix_web::{pred, App, HttpResponse}; +/// use actix_web::{filter, App, HttpResponse}; /// /// fn main() { /// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter( -/// pred::All(pred::Get()) -/// .and(pred::Header("content-type", "text/plain")), +/// r.route( +/// |r| r.filter( +/// filter::All(filter::Get()) +/// .and(filter::Header("content-type", "text/plain")), /// ) -/// .f(|_| HttpResponse::MethodNotAllowed()) +/// .to(|| HttpResponse::MethodNotAllowed())) /// }); /// } /// ``` @@ -191,137 +190,146 @@ impl Filter for HeaderFilter { } } -/// Return predicate that matches if request contains specified Host name. -/// -/// ```rust,ignore -/// # extern crate actix_web; -/// use actix_web::{pred, App, HttpResponse}; -/// -/// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter(pred::Host("www.rust-lang.org")) -/// .f(|_| HttpResponse::MethodNotAllowed()) -/// }); -/// } -/// ``` -pub fn Host>(host: H) -> HostFilter { - HostFilter(host.as_ref().to_string(), None) -} +// /// Return predicate that matches if request contains specified Host name. +// /// +// /// ```rust,ignore +// /// # extern crate actix_web; +// /// use actix_web::{pred, App, HttpResponse}; +// /// +// /// fn main() { +// /// App::new().resource("/index.html", |r| { +// /// r.route() +// /// .filter(pred::Host("www.rust-lang.org")) +// /// .f(|_| HttpResponse::MethodNotAllowed()) +// /// }); +// /// } +// /// ``` +// pub fn Host>(host: H) -> HostFilter { +// HostFilter(host.as_ref().to_string(), None) +// } -#[doc(hidden)] -pub struct HostFilter(String, Option); +// #[doc(hidden)] +// pub struct HostFilter(String, Option); -impl HostFilter { - /// Set reuest scheme to match - pub fn scheme>(&mut self, scheme: H) { - self.1 = Some(scheme.as_ref().to_string()) - } -} - -impl Filter for HostFilter { - fn check(&self, _req: &HttpRequest) -> bool { - // let info = req.connection_info(); - // if let Some(ref scheme) = self.1 { - // self.0 == info.host() && scheme == info.scheme() - // } else { - // self.0 == info.host() - // } - false - } -} - -// #[cfg(test)] -// mod tests { -// use actix_http::http::{header, Method}; -// use actix_http::test::TestRequest; - -// use super::*; - -// #[test] -// fn test_header() { -// let req = TestRequest::with_header( -// header::TRANSFER_ENCODING, -// header::HeaderValue::from_static("chunked"), -// ) -// .finish(); - -// let pred = Header("transfer-encoding", "chunked"); -// assert!(pred.check(&req, req.state())); - -// let pred = Header("transfer-encoding", "other"); -// assert!(!pred.check(&req, req.state())); - -// let pred = Header("content-type", "other"); -// assert!(!pred.check(&req, req.state())); -// } - -// #[test] -// fn test_host() { -// let req = TestRequest::default() -// .header( -// header::HOST, -// header::HeaderValue::from_static("www.rust-lang.org"), -// ) -// .finish(); - -// let pred = Host("www.rust-lang.org"); -// assert!(pred.check(&req, req.state())); - -// let pred = Host("localhost"); -// assert!(!pred.check(&req, req.state())); -// } - -// #[test] -// fn test_methods() { -// let req = TestRequest::default().finish(); -// let req2 = TestRequest::default().method(Method::POST).finish(); - -// assert!(Get().check(&req, req.state())); -// assert!(!Get().check(&req2, req2.state())); -// assert!(Post().check(&req2, req2.state())); -// assert!(!Post().check(&req, req.state())); - -// let r = TestRequest::default().method(Method::PUT).finish(); -// assert!(Put().check(&r, r.state())); -// assert!(!Put().check(&req, req.state())); - -// let r = TestRequest::default().method(Method::DELETE).finish(); -// assert!(Delete().check(&r, r.state())); -// assert!(!Delete().check(&req, req.state())); - -// let r = TestRequest::default().method(Method::HEAD).finish(); -// assert!(Head().check(&r, r.state())); -// assert!(!Head().check(&req, req.state())); - -// let r = TestRequest::default().method(Method::OPTIONS).finish(); -// assert!(Options().check(&r, r.state())); -// assert!(!Options().check(&req, req.state())); - -// let r = TestRequest::default().method(Method::CONNECT).finish(); -// assert!(Connect().check(&r, r.state())); -// assert!(!Connect().check(&req, req.state())); - -// let r = TestRequest::default().method(Method::PATCH).finish(); -// assert!(Patch().check(&r, r.state())); -// assert!(!Patch().check(&req, req.state())); - -// let r = TestRequest::default().method(Method::TRACE).finish(); -// assert!(Trace().check(&r, r.state())); -// assert!(!Trace().check(&req, req.state())); -// } - -// #[test] -// fn test_preds() { -// let r = TestRequest::default().method(Method::TRACE).finish(); - -// assert!(Not(Get()).check(&r, r.state())); -// assert!(!Not(Trace()).check(&r, r.state())); - -// assert!(All(Trace()).and(Trace()).check(&r, r.state())); -// assert!(!All(Get()).and(Trace()).check(&r, r.state())); - -// assert!(Any(Get()).or(Trace()).check(&r, r.state())); -// assert!(!Any(Get()).or(Get()).check(&r, r.state())); +// impl HostFilter { +// /// Set reuest scheme to match +// pub fn scheme>(&mut self, scheme: H) { +// self.1 = Some(scheme.as_ref().to_string()) // } // } + +// impl Filter for HostFilter { +// fn check(&self, _req: &HttpRequest) -> bool { +// // let info = req.connection_info(); +// // if let Some(ref scheme) = self.1 { +// // self.0 == info.host() && scheme == info.scheme() +// // } else { +// // self.0 == info.host() +// // } +// false +// } +// } + +#[cfg(test)] +mod tests { + use crate::test::TestServiceRequest; + use actix_http::http::{header, Method}; + + use super::*; + + #[test] + fn test_header() { + let req = TestServiceRequest::with_header(header::TRANSFER_ENCODING, "chunked") + .finish() + .into_request(); + + let pred = Header("transfer-encoding", "chunked"); + assert!(pred.check(&req)); + + let pred = Header("transfer-encoding", "other"); + assert!(!pred.check(&req)); + + let pred = Header("content-type", "other"); + assert!(!pred.check(&req)); + } + + // #[test] + // fn test_host() { + // let req = TestServiceRequest::default() + // .header( + // header::HOST, + // header::HeaderValue::from_static("www.rust-lang.org"), + // ) + // .request(); + + // let pred = Host("www.rust-lang.org"); + // assert!(pred.check(&req)); + + // let pred = Host("localhost"); + // assert!(!pred.check(&req)); + // } + + #[test] + fn test_methods() { + let req = TestServiceRequest::default().finish().into_request(); + let req2 = TestServiceRequest::default() + .method(Method::POST) + .finish() + .into_request(); + + assert!(Get().check(&req)); + assert!(!Get().check(&req2)); + assert!(Post().check(&req2)); + assert!(!Post().check(&req)); + + let r = TestServiceRequest::default().method(Method::PUT).finish(); + assert!(Put().check(&r,)); + assert!(!Put().check(&req,)); + + let r = TestServiceRequest::default() + .method(Method::DELETE) + .finish(); + assert!(Delete().check(&r,)); + assert!(!Delete().check(&req,)); + + let r = TestServiceRequest::default().method(Method::HEAD).finish(); + assert!(Head().check(&r,)); + assert!(!Head().check(&req,)); + + let r = TestServiceRequest::default() + .method(Method::OPTIONS) + .finish(); + assert!(Options().check(&r,)); + assert!(!Options().check(&req,)); + + let r = TestServiceRequest::default() + .method(Method::CONNECT) + .finish(); + assert!(Connect().check(&r,)); + assert!(!Connect().check(&req,)); + + let r = TestServiceRequest::default().method(Method::PATCH).finish(); + assert!(Patch().check(&r,)); + assert!(!Patch().check(&req,)); + + let r = TestServiceRequest::default().method(Method::TRACE).finish(); + assert!(Trace().check(&r,)); + assert!(!Trace().check(&req,)); + } + + #[test] + fn test_preds() { + let r = TestServiceRequest::default() + .method(Method::TRACE) + .request(); + + assert!(Not(Get()).check(&r,)); + assert!(!Not(Trace()).check(&r,)); + + assert!(All(Trace()).and(Trace()).check(&r,)); + assert!(!All(Get()).and(Trace()).check(&r,)); + + assert!(Any(Get()).or(Trace()).check(&r,)); + assert!(!Any(Get()).or(Get()).check(&r,)); + } +} diff --git a/src/lib.rs b/src/lib.rs index c70f47e80..70ce96073 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ mod responder; mod route; mod service; mod state; +pub mod test; // re-export for convenience pub use actix_http::Response as HttpResponse; diff --git a/src/route.rs b/src/route.rs index 4abb2f1de..d5e114246 100644 --- a/src/route.rs +++ b/src/route.rs @@ -180,7 +180,7 @@ impl RouteBuilder

    { /// # .finish(); /// # } /// ``` - pub fn filter(&mut self, f: F) -> &mut Self { + pub fn filter(mut self, f: F) -> Self { self.filters.push(Box::new(f)); self } diff --git a/src/test.rs b/src/test.rs index e13dcd16c..d67696b11 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,18 +1,15 @@ //! Various helpers for Actix applications to use during testing. -use std::str::FromStr; +use std::ops::{Deref, DerefMut}; +use std::rc::Rc; -use bytes::Bytes; -use futures::IntoFuture; -use tokio_current_thread::Runtime; - -use actix_http::dev::Payload; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; -use actix_http::http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; -use actix_http::Request as HttpRequest; +use actix_http::http::{HttpTryFrom, Method, Version}; +use actix_http::test::TestRequest; +use actix_http::{Extensions, PayloadStream}; use actix_router::{Path, Url}; +use bytes::Bytes; -use crate::app::State; -use crate::request::Request; +use crate::request::HttpRequest; use crate::service::ServiceRequest; /// Test `Request` builder @@ -42,90 +39,71 @@ use crate::service::ServiceRequest; /// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); /// } /// ``` -pub struct TestRequest { - state: S, - version: Version, - method: Method, - uri: Uri, - headers: HeaderMap, - params: Path, - payload: Option, +pub struct TestServiceRequest { + req: TestRequest, + extensions: Extensions, } -impl Default for TestRequest<()> { - fn default() -> TestRequest<()> { - TestRequest { - state: (), - method: Method::GET, - uri: Uri::from_str("/").unwrap(), - version: Version::HTTP_11, - headers: HeaderMap::new(), - params: Path::new(Url::default()), - payload: None, +impl Default for TestServiceRequest { + fn default() -> TestServiceRequest { + TestServiceRequest { + req: TestRequest::default(), + extensions: Extensions::new(), } } } -impl TestRequest<()> { +impl TestServiceRequest { /// Create TestRequest and set request uri - pub fn with_uri(path: &str) -> TestRequest<()> { - TestRequest::default().uri(path) + pub fn with_uri(path: &str) -> TestServiceRequest { + TestServiceRequest { + req: TestRequest::default().uri(path).take(), + extensions: Extensions::new(), + } } /// Create TestRequest and set header - pub fn with_hdr(hdr: H) -> TestRequest<()> { - TestRequest::default().set(hdr) + pub fn with_hdr(hdr: H) -> TestServiceRequest { + TestServiceRequest { + req: TestRequest::default().set(hdr).take(), + extensions: Extensions::new(), + } } /// Create TestRequest and set header - pub fn with_header(key: K, value: V) -> TestRequest<()> + pub fn with_header(key: K, value: V) -> TestServiceRequest where HeaderName: HttpTryFrom, V: IntoHeaderValue, { - TestRequest::default().header(key, value) - } -} - -impl TestRequest { - /// Start HttpRequest build process with application state - pub fn with_state(state: S) -> TestRequest { - TestRequest { - state, - method: Method::GET, - uri: Uri::from_str("/").unwrap(), - version: Version::HTTP_11, - headers: HeaderMap::new(), - params: Path::new(Url::default()), - payload: None, + TestServiceRequest { + req: TestRequest::default().header(key, value).take(), + extensions: Extensions::new(), } } /// Set HTTP version of this request pub fn version(mut self, ver: Version) -> Self { - self.version = ver; + self.req.version(ver); self } /// Set HTTP method of this request pub fn method(mut self, meth: Method) -> Self { - self.method = meth; + self.req.method(meth); self } /// Set HTTP Uri of this request pub fn uri(mut self, path: &str) -> Self { - self.uri = Uri::from_str(path).unwrap(); + self.req.uri(path); self } /// Set a header pub fn set(mut self, hdr: H) -> Self { - if let Ok(value) = hdr.try_into() { - self.headers.append(H::name(), value); - return self; - } - panic!("Can not set header"); + self.req.set(hdr); + self } /// Set a header @@ -134,63 +112,50 @@ impl TestRequest { HeaderName: HttpTryFrom, V: IntoHeaderValue, { - if let Ok(key) = HeaderName::try_from(key) { - if let Ok(value) = value.try_into() { - self.headers.append(key, value); - return self; - } - } - panic!("Can not create header"); - } - - /// Set request path pattern parameter - pub fn param(mut self, name: &'static str, value: &'static str) -> Self { - self.params.add_static(name, value); + self.req.header(key, value); self } /// Set request payload pub fn set_payload>(mut self, data: B) -> Self { - let mut payload = Payload::empty(); - payload.unread_data(data.into()); - self.payload = Some(payload); + self.req.set_payload(data); self } - /// Complete request creation and generate `HttpRequest` instance - pub fn finish(self) -> ServiceRequest { - let TestRequest { - state, - method, - uri, - version, - headers, - mut params, - payload, - } = self; + /// Complete request creation and generate `ServiceRequest` instance + pub fn finish(mut self) -> ServiceRequest { + let req = self.req.finish(); - params.get_mut().update(&uri); - - let mut req = HttpRequest::new(); - { - let inner = req.inner_mut(); - inner.head.uri = uri; - inner.head.method = method; - inner.head.version = version; - inner.head.headers = headers; - *inner.payload.borrow_mut() = payload; - } - - Request::new(State::new(state), req, params) + ServiceRequest::new( + Path::new(Url::new(req.uri().clone())), + req, + Rc::new(self.extensions), + ) } - /// This method generates `HttpRequest` instance and executes handler - pub fn run_async(self, f: F) -> Result - where - F: FnOnce(&Request) -> R, - R: IntoFuture, - { - let mut rt = Runtime::new().unwrap(); - rt.block_on(f(&self.finish()).into_future()) + /// Complete request creation and generate `HttpRequest` instance + pub fn request(mut self) -> HttpRequest { + let req = self.req.finish(); + + ServiceRequest::new( + Path::new(Url::new(req.uri().clone())), + req, + Rc::new(self.extensions), + ) + .into_request() + } +} + +impl Deref for TestServiceRequest { + type Target = TestRequest; + + fn deref(&self) -> &TestRequest { + &self.req + } +} + +impl DerefMut for TestServiceRequest { + fn deref_mut(&mut self) -> &mut TestRequest { + &mut self.req } } From 2d0495093c29b63daeed0a88aba3c7856ccb5733 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 18:37:09 -0800 Subject: [PATCH 164/427] add Payload::take method --- src/payload.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/payload.rs b/src/payload.rs index bc40fe807..8f96bab95 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -39,6 +39,13 @@ impl From for Payload { } } +impl Payload { + /// Takes current payload and replaces it with `None` value + fn take(&mut self) -> Payload { + std::mem::replace(self, Payload::None) + } +} + impl Stream for Payload where S: Stream, From 8103d3327068d6ce4de977abbe007b0f104c65de Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 19:19:56 -0800 Subject: [PATCH 165/427] use custom request for FromRequest trait --- examples/basic.rs | 10 +-- src/app.rs | 8 +-- src/extractor.rs | 26 ++++---- src/filter.rs | 46 +++++++------ src/handler.rs | 15 +++-- src/lib.rs | 81 ++++++++++++++++++++++- src/request.rs | 6 +- src/resource.rs | 96 ++------------------------- src/route.rs | 83 +++++++++++------------- src/service.rs | 151 ++++++++++++++++++++++++++++++++++++++++--- src/state.rs | 4 +- tests/test_server.rs | 32 +++++---- 12 files changed, 342 insertions(+), 216 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index a18581f90..886efb7b4 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -2,7 +2,7 @@ use futures::IntoFuture; use actix_http::{h1, http::Method, Response}; use actix_server::Server; -use actix_web::{middleware, App, Error, HttpRequest, Resource}; +use actix_web::{middleware, web, App, Error, HttpRequest, Resource, Route}; fn index(req: HttpRequest) -> &'static str { println!("REQ: {:?}", req); @@ -31,7 +31,7 @@ fn main() { middleware::DefaultHeaders::new().header("X-Version", "0.2"), ) .middleware(middleware::Compress::default()) - .resource("/resource1/index.html", |r| r.get(index)) + .resource("/resource1/index.html", |r| r.route(web::get().to(index))) .service( "/resource2/index.html", Resource::new() @@ -39,8 +39,10 @@ fn main() { middleware::DefaultHeaders::new() .header("X-Version-R2", "0.3"), ) - .default_resource(|r| r.to(|| Response::MethodNotAllowed())) - .method(Method::GET, |r| r.to_async(index_async)), + .default_resource(|r| { + r.route(Route::new().to(|| Response::MethodNotAllowed())) + }) + .route(web::method(Method::GET).to_async(index_async)), ) .service("/test1.html", Resource::new().to(|| "Test\r\n")) .service("/", Resource::new().to(no_params)), diff --git a/src/app.rs b/src/app.rs index 2e45f2d3c..d92190915 100644 --- a/src/app.rs +++ b/src/app.rs @@ -159,16 +159,16 @@ where } /// Register a middleware. - pub fn middleware( + pub fn middleware( self, mw: F, ) -> AppRouter< T, P, - Body, + B, impl NewService< Request = ServiceRequest

    , - Response = ServiceResponse, + Response = ServiceResponse, Error = (), InitError = (), >, @@ -177,7 +177,7 @@ where M: NewTransform< AppService

    , Request = ServiceRequest

    , - Response = ServiceResponse, + Response = ServiceResponse, Error = (), InitError = (), >, diff --git a/src/extractor.rs b/src/extractor.rs index 48c70b861..522ce721f 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -26,7 +26,7 @@ use actix_router::PathDeserializer; use crate::handler::FromRequest; use crate::request::HttpRequest; use crate::responder::Responder; -use crate::service::ServiceRequest; +use crate::service::ServiceFromRequest; #[derive(PartialEq, Eq, PartialOrd, Ord)] /// Extract typed information from the request's path. @@ -112,7 +112,7 @@ impl Path { } /// Extract path information from a request - pub fn extract

    (req: &ServiceRequest

    ) -> Result, de::value::Error> + pub fn extract

    (req: &ServiceFromRequest

    ) -> Result, de::value::Error> where T: DeserializeOwned, { @@ -135,7 +135,7 @@ where type Future = FutureResult; #[inline] - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { Self::extract(req).map_err(ErrorNotFound).into_future() } } @@ -221,7 +221,7 @@ where type Future = FutureResult; #[inline] - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { serde_urlencoded::from_str::(req.query_string()) .map(|val| ok(Query(val))) .unwrap_or_else(|e| err(e.into())) @@ -301,7 +301,7 @@ where type Future = Box>; #[inline] - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { let cfg = FormConfig::default(); let req2 = req.clone(); @@ -511,7 +511,7 @@ where type Future = Box>; #[inline] - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { let cfg = JsonConfig::default(); let req2 = req.clone(); @@ -619,7 +619,7 @@ where Either>, FutureResult>; #[inline] - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { let cfg = PayloadConfig::default(); if let Err(e) = cfg.check_mimetype(req) { @@ -666,7 +666,7 @@ where Either>, FutureResult>; #[inline] - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { let cfg = PayloadConfig::default(); // check content-type @@ -755,7 +755,7 @@ where type Future = Box, Error = Error>>; #[inline] - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { Box::new(T::from_request(req).then(|r| match r { Ok(v) => future::ok(Some(v)), Err(_) => future::ok(None), @@ -818,7 +818,7 @@ where type Future = Box, Error = Error>>; #[inline] - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { Box::new(T::from_request(req).then(|res| match res { Ok(v) => ok(Ok(v)), Err(e) => ok(Err(e)), @@ -846,7 +846,7 @@ impl PayloadConfig { self } - fn check_mimetype

    (&self, req: &ServiceRequest

    ) -> Result<(), Error> { + fn check_mimetype

    (&self, req: &ServiceFromRequest

    ) -> Result<(), Error> { // check content-type if let Some(ref mt) = self.mimetype { match req.mime_type() { @@ -884,7 +884,7 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { type Error = Error; type Future = $fut_type; - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { $fut_type { items: <($(Option<$T>,)+)>::default(), futs: ($($T::from_request(req),)+), @@ -933,7 +933,7 @@ impl

    FromRequest

    for () { type Error = Error; type Future = FutureResult<(), Error>; - fn from_request(_req: &mut ServiceRequest

    ) -> Self::Future { + fn from_request(_req: &mut ServiceFromRequest

    ) -> Self::Future { ok(()) } } diff --git a/src/filter.rs b/src/filter.rs index 37c23d732..e3d87b766 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -1,8 +1,7 @@ //! Route match predicates #![allow(non_snake_case)] use actix_http::http::{self, header, HttpTryFrom}; - -use crate::request::HttpRequest; +use actix_http::RequestHead; /// Trait defines resource predicate. /// Predicate can modify request object. It is also possible to @@ -10,20 +9,21 @@ use crate::request::HttpRequest; /// Extensions container available via `HttpRequest::extensions()` method. pub trait Filter { /// Check if request matches predicate - fn check(&self, request: &HttpRequest) -> bool; + fn check(&self, request: &RequestHead) -> bool; } /// Return filter that matches if any of supplied filters. /// -/// ```rust,ignore -/// use actix_web::{filter, App, HttpResponse}; +/// ```rust +/// use actix_web::{web, filter, App, HttpResponse}; /// /// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter(pred::Any(pred::Get()).or(pred::Post())) -/// .to(|| HttpResponse::MethodNotAllowed()) -/// }); +/// App::new().resource("/index.html", |r| +/// r.route( +/// web::route() +/// .filter(filter::Any(filter::Get()).or(filter::Post())) +/// .to(|| HttpResponse::MethodNotAllowed())) +/// ); /// } /// ``` pub fn Any(filter: F) -> AnyFilter { @@ -42,7 +42,7 @@ impl AnyFilter { } impl Filter for AnyFilter { - fn check(&self, req: &HttpRequest) -> bool { + fn check(&self, req: &RequestHead) -> bool { for p in &self.0 { if p.check(req) { return true; @@ -56,15 +56,13 @@ impl Filter for AnyFilter { /// /// ```rust /// # extern crate actix_web; -/// use actix_web::{filter, App, HttpResponse}; +/// use actix_web::{filter, web, App, HttpResponse}; /// /// fn main() { /// App::new().resource("/index.html", |r| { -/// r.route( -/// |r| r.filter( -/// filter::All(filter::Get()) -/// .and(filter::Header("content-type", "text/plain")), -/// ) +/// r.route(web::route() +/// .filter( +/// filter::All(filter::Get()).and(filter::Header("content-type", "text/plain"))) /// .to(|| HttpResponse::MethodNotAllowed())) /// }); /// } @@ -85,7 +83,7 @@ impl AllFilter { } impl Filter for AllFilter { - fn check(&self, request: &HttpRequest) -> bool { + fn check(&self, request: &RequestHead) -> bool { for p in &self.0 { if !p.check(request) { return false; @@ -104,7 +102,7 @@ pub fn Not(filter: F) -> NotFilter { pub struct NotFilter(Box); impl Filter for NotFilter { - fn check(&self, request: &HttpRequest) -> bool { + fn check(&self, request: &RequestHead) -> bool { !self.0.check(request) } } @@ -114,8 +112,8 @@ impl Filter for NotFilter { pub struct MethodFilter(http::Method); impl Filter for MethodFilter { - fn check(&self, request: &HttpRequest) -> bool { - request.method() == self.0 + fn check(&self, request: &RequestHead) -> bool { + request.method == self.0 } } @@ -182,8 +180,8 @@ pub fn Header(name: &'static str, value: &'static str) -> HeaderFilter { pub struct HeaderFilter(header::HeaderName, header::HeaderValue); impl Filter for HeaderFilter { - fn check(&self, req: &HttpRequest) -> bool { - if let Some(val) = req.headers().get(&self.0) { + fn check(&self, req: &RequestHead) -> bool { + if let Some(val) = req.headers.get(&self.0) { return val == self.1; } false @@ -219,7 +217,7 @@ impl Filter for HeaderFilter { // } // impl Filter for HostFilter { -// fn check(&self, _req: &HttpRequest) -> bool { +// fn check(&self, _req: &RequestHead) -> bool { // // let info = req.connection_info(); // // if let Some(ref scheme) = self.1 { // // self.0 == info.host() && scheme == info.scheme() diff --git a/src/handler.rs b/src/handler.rs index 8076d4d42..31ae796b2 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -7,7 +7,7 @@ use futures::{try_ready, Async, Future, IntoFuture, Poll}; use crate::request::HttpRequest; use crate::responder::Responder; -use crate::service::{ServiceRequest, ServiceResponse}; +use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; /// Trait implemented by types that can be extracted from request. /// @@ -20,7 +20,7 @@ pub trait FromRequest

    : Sized { type Future: Future; /// Convert request to a Self - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future; + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future; } /// Handler converter factory @@ -306,7 +306,7 @@ impl> Default for Extract { impl> NewService for Extract { type Request = ServiceRequest

    ; type Response = (T, HttpRequest); - type Error = (Error, ServiceRequest

    ); + type Error = (Error, ServiceFromRequest

    ); type InitError = (); type Service = ExtractService; type Future = FutureResult; @@ -323,14 +323,15 @@ pub struct ExtractService> { impl> Service for ExtractService { type Request = ServiceRequest

    ; type Response = (T, HttpRequest); - type Error = (Error, ServiceRequest

    ); + type Error = (Error, ServiceFromRequest

    ); type Future = ExtractResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } - fn call(&mut self, mut req: ServiceRequest

    ) -> Self::Future { + fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { + let mut req = req.into(); ExtractResponse { fut: T::from_request(&mut req), req: Some(req), @@ -339,13 +340,13 @@ impl> Service for ExtractService { } pub struct ExtractResponse> { - req: Option>, + req: Option>, fut: T::Future, } impl> Future for ExtractResponse { type Item = (T, HttpRequest); - type Error = (Error, ServiceRequest

    ); + type Error = (Error, ServiceFromRequest

    ); fn poll(&mut self) -> Poll { let item = try_ready!(self diff --git a/src/lib.rs b/src/lib.rs index 70ce96073..0e81b65ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,12 +25,91 @@ pub use crate::handler::FromRequest; pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::{Either, Responder}; +pub use crate::route::Route; pub use crate::service::{ServiceRequest, ServiceResponse}; pub use crate::state::State; +pub mod web { + use actix_http::{http::Method, Error, Response}; + use futures::IntoFuture; + + use crate::handler::{AsyncFactory, Factory, FromRequest}; + use crate::responder::Responder; + use crate::Route; + + pub fn route() -> Route

    { + Route::new() + } + + pub fn get() -> Route

    { + Route::get() + } + + pub fn post() -> Route

    { + Route::post() + } + + pub fn put() -> Route

    { + Route::put() + } + + pub fn delete() -> Route

    { + Route::delete() + } + + pub fn head() -> Route

    { + Route::new().method(Method::HEAD) + } + + pub fn method(method: Method) -> Route

    { + Route::new().method(method) + } + + /// Create a new route and add handler. + /// + /// ```rust + /// use actix_web::{web, App, HttpResponse}; + /// + /// fn index() -> HttpResponse { + /// unimplemented!() + /// } + /// + /// App::new().resource("/", |r| r.route(web::to(index))); + /// ``` + pub fn to(handler: F) -> Route

    + where + F: Factory + 'static, + I: FromRequest

    + 'static, + R: Responder + 'static, + { + Route::new().to(handler) + } + + /// Create a new route and add async handler. + /// + /// ```rust + /// use actix_web::{web, App, HttpResponse, Error}; + /// + /// fn index() -> impl futures::Future { + /// futures::future::ok(HttpResponse::Ok().finish()) + /// } + /// + /// App::new().resource("/", |r| r.route(web::to_async(index))); + /// ``` + pub fn to_async(handler: F) -> Route

    + where + F: AsyncFactory, + I: FromRequest

    + 'static, + R: IntoFuture + 'static, + R::Item: Into, + R::Error: Into, + { + Route::new().to_async(handler) + } +} + pub mod dev { pub use crate::app::AppRouter; pub use crate::handler::{AsyncFactory, Extract, Factory, Handle}; - pub use crate::route::{Route, RouteBuilder}; // pub use crate::info::ConnectionInfo; } diff --git a/src/request.rs b/src/request.rs index 571431cc2..538064f19 100644 --- a/src/request.rs +++ b/src/request.rs @@ -9,11 +9,11 @@ use actix_router::{Path, Url}; use futures::future::{ok, FutureResult}; use crate::handler::FromRequest; -use crate::service::ServiceRequest; +use crate::service::ServiceFromRequest; #[derive(Clone)] pub struct HttpRequest { - head: Message, + pub(crate) head: Message, pub(crate) path: Path, extensions: Rc, } @@ -145,7 +145,7 @@ impl

    FromRequest

    for HttpRequest { type Future = FutureResult; #[inline] - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { ok(req.clone()) } } diff --git a/src/resource.rs b/src/resource.rs index 80ac8d83b..ab6096c52 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::rc::Rc; -use actix_http::{http::Method, Error, Response}; +use actix_http::{Error, Response}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, @@ -11,7 +11,7 @@ use futures::{Async, Future, IntoFuture, Poll}; use crate::handler::{AsyncFactory, Factory, FromRequest}; use crate::responder::Responder; -use crate::route::{CreateRouteService, Route, RouteBuilder, RouteService}; +use crate::route::{CreateRouteService, Route, RouteService}; use crate::service::{ServiceRequest, ServiceResponse}; type HttpService

    = BoxedService, ServiceResponse, ()>; @@ -74,92 +74,8 @@ where /// .finish(); /// } /// ``` - pub fn route(mut self, f: F) -> Self - where - F: FnOnce(RouteBuilder

    ) -> Route

    , - { - self.routes.push(f(Route::build())); - self - } - - /// Register a new `GET` route. - pub fn get(mut self, f: F) -> Self - where - F: Factory + 'static, - I: FromRequest

    + 'static, - R: Responder + 'static, - { - self.routes.push(Route::get().to(f)); - self - } - - /// Register a new `POST` route. - pub fn post(mut self, f: F) -> Self - where - F: Factory + 'static, - I: FromRequest

    + 'static, - R: Responder + 'static, - { - self.routes.push(Route::post().to(f)); - self - } - - /// Register a new `PUT` route. - pub fn put(mut self, f: F) -> Self - where - F: Factory + 'static, - I: FromRequest

    + 'static, - R: Responder + 'static, - { - self.routes.push(Route::put().to(f)); - self - } - - /// Register a new `DELETE` route. - pub fn delete(mut self, f: F) -> Self - where - F: Factory + 'static, - I: FromRequest

    + 'static, - R: Responder + 'static, - { - self.routes.push(Route::delete().to(f)); - self - } - - /// Register a new `HEAD` route. - pub fn head(mut self, f: F) -> Self - where - F: Factory + 'static, - I: FromRequest

    + 'static, - R: Responder + 'static, - { - self.routes.push(Route::build().method(Method::HEAD).to(f)); - self - } - - /// Register a new route and add method check to route. - /// - /// ```rust,ignore - /// # extern crate actix_web; - /// use actix_web::*; - /// fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// - /// App::new().resource("/", |r| r.method(http::Method::GET).f(index)); - /// ``` - /// - /// This is shortcut for: - /// - /// ```rust,ignore - /// # extern crate actix_web; - /// # use actix_web::*; - /// # fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// App::new().resource("/", |r| r.route().filter(pred::Get()).f(index)); - /// ``` - pub fn method(mut self, method: Method, f: F) -> Self - where - F: FnOnce(RouteBuilder

    ) -> Route

    , - { - self.routes.push(f(Route::build().method(method))); + pub fn route(mut self, route: Route

    ) -> Self { + self.routes.push(route); self } @@ -187,7 +103,7 @@ where I: FromRequest

    + 'static, R: Responder + 'static, { - self.routes.push(Route::build().to(handler)); + self.routes.push(Route::new().to(handler)); self } @@ -227,7 +143,7 @@ where R::Item: Into, R::Error: Into, { - self.routes.push(Route::build().to_async(handler)); + self.routes.push(Route::new().to_async(handler)); self } diff --git a/src/route.rs b/src/route.rs index d5e114246..99117afed 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,4 +1,3 @@ -use std::marker::PhantomData; use std::rc::Rc; use actix_http::{http::Method, Error, Response}; @@ -8,7 +7,8 @@ use futures::{Async, Future, IntoFuture, Poll}; use crate::filter::{self, Filter}; use crate::handler::{AsyncFactory, AsyncHandle, Extract, Factory, FromRequest, Handle}; use crate::responder::Responder; -use crate::service::{ServiceRequest, ServiceResponse}; +use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; +use crate::HttpResponse; type BoxedRouteService = Box< Service< @@ -40,24 +40,29 @@ pub struct Route

    { } impl Route

    { - pub fn build() -> RouteBuilder

    { - RouteBuilder::new() + pub fn new() -> Route

    { + Route { + service: Box::new(RouteNewService::new(Extract::new().and_then( + Handle::new(|| HttpResponse::NotFound()).map_err(|_| panic!()), + ))), + filters: Rc::new(Vec::new()), + } } - pub fn get() -> RouteBuilder

    { - RouteBuilder::new().method(Method::GET) + pub fn get() -> Route

    { + Route::new().method(Method::GET) } - pub fn post() -> RouteBuilder

    { - RouteBuilder::new().method(Method::POST) + pub fn post() -> Route

    { + Route::new().method(Method::POST) } - pub fn put() -> RouteBuilder

    { - RouteBuilder::new().method(Method::PUT) + pub fn put() -> Route

    { + Route::new().method(Method::PUT) } - pub fn delete() -> RouteBuilder

    { - RouteBuilder::new().method(Method::DELETE) + pub fn delete() -> Route

    { + Route::new().method(Method::DELETE) } } @@ -109,7 +114,7 @@ pub struct RouteService

    { impl

    RouteService

    { pub fn check(&self, req: &mut ServiceRequest

    ) -> bool { for f in self.filters.iter() { - if !f.check(req.request()) { + if !f.check(req.head()) { return false; } } @@ -132,19 +137,7 @@ impl

    Service for RouteService

    { } } -pub struct RouteBuilder

    { - filters: Vec>, - _t: PhantomData

    , -} - -impl RouteBuilder

    { - fn new() -> RouteBuilder

    { - RouteBuilder { - filters: Vec::new(), - _t: PhantomData, - } - } - +impl Route

    { /// Add method match filter to the route. /// /// ```rust,ignore @@ -161,7 +154,9 @@ impl RouteBuilder

    { /// # } /// ``` pub fn method(mut self, method: Method) -> Self { - self.filters.push(Box::new(filter::Method(method))); + Rc::get_mut(&mut self.filters) + .unwrap() + .push(Box::new(filter::Method(method))); self } @@ -181,7 +176,7 @@ impl RouteBuilder

    { /// # } /// ``` pub fn filter(mut self, f: F) -> Self { - self.filters.push(Box::new(f)); + Rc::get_mut(&mut self.filters).unwrap().push(Box::new(f)); self } @@ -259,18 +254,16 @@ impl RouteBuilder

    { /// ); // <- use `with` extractor /// } /// ``` - pub fn to(self, handler: F) -> Route

    + pub fn to(mut self, handler: F) -> Route

    where F: Factory + 'static, T: FromRequest

    + 'static, R: Responder + 'static, { - Route { - service: Box::new(RouteNewService::new( - Extract::new().and_then(Handle::new(handler).map_err(|_| panic!())), - )), - filters: Rc::new(self.filters), - } + self.service = Box::new(RouteNewService::new( + Extract::new().and_then(Handle::new(handler).map_err(|_| panic!())), + )); + self } /// Set async handler function, use request extractor for parameters. @@ -303,7 +296,7 @@ impl RouteBuilder

    { /// } /// ``` #[allow(clippy::wrong_self_convention)] - pub fn to_async(self, handler: F) -> Route

    + pub fn to_async(mut self, handler: F) -> Self where F: AsyncFactory, T: FromRequest

    + 'static, @@ -311,12 +304,10 @@ impl RouteBuilder

    { R::Item: Into, R::Error: Into, { - Route { - service: Box::new(RouteNewService::new( - Extract::new().and_then(AsyncHandle::new(handler).map_err(|_| panic!())), - )), - filters: Rc::new(self.filters), - } + self.service = Box::new(RouteNewService::new( + Extract::new().and_then(AsyncHandle::new(handler).map_err(|_| panic!())), + )); + self } } @@ -450,7 +441,7 @@ impl RouteBuilder

    { struct RouteNewService where - T: NewService, Error = (Error, ServiceRequest

    )>, + T: NewService, Error = (Error, ServiceFromRequest

    )>, { service: T, } @@ -460,7 +451,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (Error, ServiceRequest

    ), + Error = (Error, ServiceFromRequest

    ), >, T::Future: 'static, T::Service: 'static, @@ -476,7 +467,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (Error, ServiceRequest

    ), + Error = (Error, ServiceFromRequest

    ), >, T::Future: 'static, T::Service: 'static, @@ -513,7 +504,7 @@ where T: Service< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (Error, ServiceRequest

    ), + Error = (Error, ServiceFromRequest

    ), >, { type Request = ServiceRequest

    ; diff --git a/src/service.rs b/src/service.rs index 775bb6113..078cb2236 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,9 +1,11 @@ +use std::cell::{Ref, RefMut}; use std::rc::Rc; use actix_http::body::{Body, MessageBody, ResponseBody}; -use actix_http::http::HeaderMap; +use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{ - Error, Extensions, HttpMessage, Payload, Request, Response, ResponseHead, + Error, Extensions, HttpMessage, Payload, Request, RequestHead, Response, + ResponseHead, }; use actix_router::{Path, Url}; @@ -27,11 +29,6 @@ impl

    ServiceRequest

    { } } - #[inline] - pub fn request(&self) -> &HttpRequest { - &self.req - } - #[inline] pub fn into_request(self) -> HttpRequest { self.req @@ -49,10 +46,93 @@ impl

    ServiceRequest

    { ServiceResponse::new(self.req, err.into().into()) } + /// This method returns reference to the request head + #[inline] + pub fn head(&self) -> &RequestHead { + &self.req.head + } + + /// This method returns reference to the request head + #[inline] + pub fn head_mut(&mut self) -> &mut RequestHead { + &mut self.req.head + } + + /// Request's uri. + #[inline] + pub fn uri(&self) -> &Uri { + &self.head().uri + } + + /// Read the Request method. + #[inline] + pub fn method(&self) -> &Method { + &self.head().method + } + + /// Read the Request Version. + #[inline] + pub fn version(&self) -> Version { + self.head().version + } + + /// The target path of this Request. + #[inline] + pub fn path(&self) -> &str { + self.head().uri.path() + } + + #[inline] + /// Returns Request's headers. + pub fn headers(&self) -> &HeaderMap { + &self.head().headers + } + + /// The query string in the URL. + /// + /// E.g., id=10 + #[inline] + pub fn query_string(&self) -> &str { + if let Some(query) = self.uri().query().as_ref() { + query + } else { + "" + } + } + + /// Get a reference to the Path parameters. + /// + /// Params is a container for url parameters. + /// A variable segment is specified in the form `{identifier}`, + /// where the identifier can be used later in a request handler to + /// access the matched value for that segment. + #[inline] + pub fn match_info(&self) -> &Path { + &self.req.path + } + #[inline] pub fn match_info_mut(&mut self) -> &mut Path { &mut self.req.path } + + /// Request extensions + #[inline] + pub fn extensions(&self) -> Ref { + self.req.head.extensions() + } + + /// Mutable reference to a the request's extensions + #[inline] + pub fn extensions_mut(&self) -> RefMut { + self.req.head.extensions_mut() + } + + /// Application extensions + #[inline] + pub fn app_extensions(&self) -> &Extensions { + self.req.app_extensions() + } } impl

    HttpMessage for ServiceRequest

    { @@ -70,10 +150,65 @@ impl

    HttpMessage for ServiceRequest

    { } impl

    std::ops::Deref for ServiceRequest

    { + type Target = RequestHead; + + fn deref(&self) -> &RequestHead { + self.req.head() + } +} + +impl

    std::ops::DerefMut for ServiceRequest

    { + fn deref_mut(&mut self) -> &mut RequestHead { + self.head_mut() + } +} + +pub struct ServiceFromRequest

    { + req: HttpRequest, + payload: Payload

    , +} + +impl

    ServiceFromRequest

    { + #[inline] + pub fn into_request(self) -> HttpRequest { + self.req + } + + /// Create service response for error + #[inline] + pub fn error_response>(self, err: E) -> ServiceResponse { + ServiceResponse::new(self.req, err.into().into()) + } +} + +impl

    std::ops::Deref for ServiceFromRequest

    { type Target = HttpRequest; fn deref(&self) -> &HttpRequest { - self.request() + &self.req + } +} + +impl

    HttpMessage for ServiceFromRequest

    { + type Stream = P; + + #[inline] + fn headers(&self) -> &HeaderMap { + self.req.headers() + } + + #[inline] + fn take_payload(&mut self) -> Payload { + std::mem::replace(&mut self.payload, Payload::None) + } +} + +impl

    From> for ServiceFromRequest

    { + fn from(req: ServiceRequest

    ) -> Self { + Self { + req: req.req, + payload: req.payload, + } } } diff --git a/src/state.rs b/src/state.rs index 82aecc6e1..db2637775 100644 --- a/src/state.rs +++ b/src/state.rs @@ -7,7 +7,7 @@ use futures::future::{err, ok, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; use crate::handler::FromRequest; -use crate::service::ServiceRequest; +use crate::service::ServiceFromRequest; /// Application state factory pub(crate) trait StateFactory { @@ -50,7 +50,7 @@ impl FromRequest

    for State { type Future = FutureResult; #[inline] - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { if let Some(st) = req.app_extensions().get::>() { ok(st.clone()) } else { diff --git a/tests/test_server.rs b/tests/test_server.rs index 590cc0e7b..d28f207c1 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -12,7 +12,7 @@ use flate2::write::ZlibDecoder; use futures::stream::once; //Future, Stream use rand::{distributions::Alphanumeric, Rng}; -use actix_web::{middleware, App}; +use actix_web::{middleware, web, App}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -40,7 +40,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ fn test_body() { let mut srv = TestServer::new(|| { h1::H1Service::new( - App::new().resource("/", |r| r.get(|| Response::Ok().body(STR))), + App::new().resource("/", |r| r.route(web::to(|| Response::Ok().body(STR)))), ) }); @@ -59,7 +59,7 @@ fn test_body_gzip() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) - .resource("/", |r| r.get(|| Response::Ok().body(STR))), + .resource("/", |r| r.route(web::to(|| Response::Ok().body(STR)))), ) }); @@ -87,7 +87,9 @@ fn test_body_gzip_large() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) - .resource("/", |r| r.get(move || Response::Ok().body(data.clone()))), + .resource("/", |r| { + r.route(web::to(move || Response::Ok().body(data.clone()))) + }), ) }); @@ -118,7 +120,9 @@ fn test_body_gzip_large_random() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) - .resource("/", |r| r.get(move || Response::Ok().body(data.clone()))), + .resource("/", |r| { + r.route(web::to(move || Response::Ok().body(data.clone()))) + }), ) }); @@ -144,11 +148,11 @@ fn test_body_chunked_implicit() { App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) .resource("/", |r| { - r.get(move || { + r.route(web::get().to(move || { Response::Ok().streaming(once(Ok::<_, Error>( Bytes::from_static(STR.as_ref()), ))) - }) + })) }), ) }); @@ -178,11 +182,11 @@ fn test_body_br_streaming() { App::new() .middleware(middleware::Compress::new(ContentEncoding::Br)) .resource("/", |r| { - r.get(move || { + r.route(web::to(move || { Response::Ok().streaming(once(Ok::<_, Error>( Bytes::from_static(STR.as_ref()), ))) - }) + })) }), ) }); @@ -205,7 +209,7 @@ fn test_body_br_streaming() { fn test_head_binary() { let mut srv = TestServer::new(move || { h1::H1Service::new(App::new().resource("/", |r| { - r.head(move || Response::Ok().content_length(100).body(STR)) + r.route(web::head().to(move || Response::Ok().content_length(100).body(STR))) })) }); @@ -227,12 +231,12 @@ fn test_head_binary() { fn test_no_chunking() { let mut srv = TestServer::new(move || { h1::H1Service::new(App::new().resource("/", |r| { - r.get(move || { + r.route(web::to(move || { Response::Ok() .no_chunking() .content_length(STR.len() as u64) .streaming(once(Ok::<_, Error>(Bytes::from_static(STR.as_ref())))) - }) + })) })) }); @@ -252,7 +256,7 @@ fn test_body_deflate() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Deflate)) - .resource("/", |r| r.get(move || Response::Ok().body(STR))), + .resource("/", |r| r.route(web::to(move || Response::Ok().body(STR)))), ) }); @@ -277,7 +281,7 @@ fn test_body_brotli() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Br)) - .resource("/", |r| r.get(move || Response::Ok().body(STR))), + .resource("/", |r| r.route(web::to(move || Response::Ok().body(STR)))), ) }); From b535adf637ea85176bfaabc78e7f6eed7358ef41 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 21:22:01 -0800 Subject: [PATCH 166/427] add IntoFuture impl for Response and ResponseBuilder --- src/header/common/mod.rs | 6 +++--- src/response.rs | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs index adc7484a9..30dfcaa6d 100644 --- a/src/header/common/mod.rs +++ b/src/header/common/mod.rs @@ -81,7 +81,7 @@ macro_rules! test_header { let a: Vec> = raw.iter().map(|x| x.to_vec()).collect(); let mut req = test::TestRequest::default(); for item in a { - req = req.header(HeaderField::name(), item); + req = req.header(HeaderField::name(), item).take(); } let req = req.finish(); let value = HeaderField::parse(&req); @@ -104,11 +104,11 @@ macro_rules! test_header { #[test] fn $id() { use $crate::test; - + let a: Vec> = $raw.iter().map(|x| x.to_vec()).collect(); let mut req = test::TestRequest::default(); for item in a { - req = req.header(HeaderField::name(), item); + req.header(HeaderField::name(), item); } let req = req.finish(); let val = HeaderField::parse(&req); diff --git a/src/response.rs b/src/response.rs index 807487460..9b503de1b 100644 --- a/src/response.rs +++ b/src/response.rs @@ -4,6 +4,7 @@ use std::{fmt, str}; use bytes::{BufMut, Bytes, BytesMut}; use cookie::{Cookie, CookieJar}; +use futures::future::{ok, FutureResult, IntoFuture}; use futures::Stream; use http::header::{self, HeaderName, HeaderValue}; use http::{Error as HttpError, HeaderMap, HttpTryFrom, StatusCode}; @@ -276,6 +277,16 @@ impl fmt::Debug for Response { } } +impl IntoFuture for Response { + type Item = Response; + type Error = Error; + type Future = FutureResult; + + fn into_future(self) -> Self::Future { + ok(self) + } +} + pub struct CookieIter<'a> { iter: header::ValueIter<'a, HeaderValue>, } @@ -679,6 +690,16 @@ fn parts<'a>( parts.as_mut() } +impl IntoFuture for ResponseBuilder { + type Item = Response; + type Error = Error; + type Future = FutureResult; + + fn into_future(mut self) -> Self::Future { + ok(self.finish()) + } +} + /// Helper converters impl, E: Into> From> for Response { fn from(res: Result) -> Self { From 352e7b7a754cb48e09922b8cd3a7c883369d2128 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 21:35:31 -0800 Subject: [PATCH 167/427] update tests for defaultheaders middleware --- src/middleware/defaultheaders.rs | 74 +++++++++++++++++++------------- src/service.rs | 11 +++++ 2 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index 1ace34fe8..83bb94c68 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -13,17 +13,15 @@ use crate::service::{ServiceRequest, ServiceResponse}; /// /// This middleware does not set header if response headers already contains it. /// -/// ```rust,ignore -/// # extern crate actix_web; -/// use actix_web::{http, middleware, App, HttpResponse}; +/// ```rust +/// use actix_web::{web, http, middleware, App, HttpResponse}; /// /// fn main() { /// let app = App::new() /// .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) /// .resource("/test", |r| { -/// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -/// r.method(http::Method::HEAD) -/// .f(|_| HttpResponse::MethodNotAllowed()); +/// r.route(web::get().to(|| HttpResponse::Ok())) +/// .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed())) /// }); /// } /// ``` @@ -134,30 +132,48 @@ where } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use actix_http::http::header::CONTENT_TYPE; -// use actix_http::test::TestRequest; +#[cfg(test)] +mod tests { + use actix_http::http::header::CONTENT_TYPE; + use actix_service::FnService; -// #[test] -// fn test_default_headers() { -// let mw = DefaultHeaders::new().header(CONTENT_TYPE, "0001"); + use super::*; + use crate::test::TestServiceRequest; + use crate::{HttpResponse, ServiceRequest}; -// let req = TestRequest::default().finish(); + #[test] + fn test_default_headers() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let mut mw = DefaultHeaders::new().header(CONTENT_TYPE, "0001"); + let mut srv = FnService::new(|req: ServiceRequest<_>| { + req.into_response(HttpResponse::Ok().finish()) + }); -// let resp = Response::Ok().finish(); -// let resp = match mw.response(&req, resp) { -// Ok(Response::Done(resp)) => resp, -// _ => panic!(), -// }; -// assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); + let req = TestServiceRequest::default().finish(); + let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); -// let resp = Response::Ok().header(CONTENT_TYPE, "0002").finish(); -// let resp = match mw.response(&req, resp) { -// Ok(Response::Done(resp)) => resp, -// _ => panic!(), -// }; -// assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); -// } -// } + let req = TestServiceRequest::default().finish(); + let mut srv = FnService::new(|req: ServiceRequest<_>| { + req.into_response(HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish()) + }); + let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); + } + + #[test] + fn test_content_type() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let mut mw = DefaultHeaders::new().content_type(); + let mut srv = FnService::new(|req: ServiceRequest<_>| { + req.into_response(HttpResponse::Ok().finish()) + }); + + let req = TestServiceRequest::default().finish(); + let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + "application/octet-stream" + ); + } +} diff --git a/src/service.rs b/src/service.rs index 078cb2236..99af73c16 100644 --- a/src/service.rs +++ b/src/service.rs @@ -8,6 +8,7 @@ use actix_http::{ ResponseHead, }; use actix_router::{Path, Url}; +use futures::future::{ok, FutureResult, IntoFuture}; use crate::request::HttpRequest; @@ -288,3 +289,13 @@ impl Into> for ServiceResponse { self.response } } + +impl IntoFuture for ServiceResponse { + type Item = ServiceResponse; + type Error = Error; + type Future = FutureResult, Error>; + + fn into_future(self) -> Self::Future { + ok(self) + } +} From d5c54a18675f2ed7159307ea616ebbcce85d3444 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 22:03:45 -0800 Subject: [PATCH 168/427] update extractor tests --- src/app.rs | 36 +++++------ src/extractor.rs | 105 ++++++++++++++----------------- src/filter.rs | 32 ++++------ src/middleware/defaultheaders.rs | 8 +-- src/middleware/mod.rs | 13 ---- src/test.rs | 49 +++++---------- 6 files changed, 97 insertions(+), 146 deletions(-) diff --git a/src/app.rs b/src/app.rs index d92190915..ae510621c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -29,7 +29,8 @@ pub trait HttpServiceFactory { fn create(self) -> Self::Factory; } -/// Application builder +/// Application builder - structure that follows the builder pattern +/// for building application instances. pub struct App where T: NewService, Response = ServiceRequest

    >, @@ -69,11 +70,8 @@ where InitError = (), >, { - /// Create application with specified state. Application can be - /// configured with a builder-like pattern. - /// - /// State is shared with all resources within same application and - /// could be accessed with `HttpRequest::state()` method. + /// Set application state. Applicatin state could be accessed + /// by using `State` extractor where `T` is state type. /// /// **Note**: http server accepts an application factory rather than /// an application instance. Http server constructs an application @@ -86,7 +84,7 @@ where self } - /// Set application state. This function is + /// Set application state factory. This function is /// similar to `.state()` but it accepts state factory. State get /// constructed asynchronously during application initialization. pub fn state_factory(mut self, state: F) -> Self @@ -119,14 +117,14 @@ where /// `/users/{userid}/{friend}` and store `userid` and `friend` in /// the exposed `Params` object: /// - /// ```rust,ignore + /// ```rust /// # extern crate actix_web; - /// use actix_web::{http, App, HttpResponse}; + /// use actix_web::{web, http, App, HttpResponse}; /// /// fn main() { /// let app = App::new().resource("/users/{userid}/{friend}", |r| { - /// r.get(|r| r.to(|_| HttpResponse::Ok())); - /// r.head(|r| r.to(|_| HttpResponse::MethodNotAllowed())) + /// r.route(web::get().to(|| HttpResponse::Ok())) + /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) /// }); /// } /// ``` @@ -294,15 +292,17 @@ where /// `/users/{userid}/{friend}` and store `userid` and `friend` in /// the exposed `Params` object: /// - /// ```rust,ignore - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpResponse}; + /// ```rust + /// use actix_web::{web, http, App, HttpResponse}; /// /// fn main() { - /// let app = App::new().resource("/users/{userid}/{friend}", |r| { - /// r.get(|r| r.to(|_| HttpResponse::Ok())); - /// r.head(|r| r.to(|_| HttpResponse::MethodNotAllowed())) - /// }); + /// let app = App::new() + /// .resource("/users/{userid}/{friend}", |r| { + /// r.route(web::to(|| HttpResponse::Ok())) + /// }) + /// .resource("/index.html", |r| { + /// r.route(web::head().to(|| HttpResponse::MethodNotAllowed())) + /// }); /// } /// ``` pub fn resource(mut self, path: &str, f: F) -> Self diff --git a/src/extractor.rs b/src/extractor.rs index 522ce721f..04dfb81ae 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -999,73 +999,60 @@ tuple_from_req!( (9, J) ); -// #[cfg(test)] -// mod tests { -// use super::*; -// use actix_http::http::header; -// use actix_http::test::TestRequest; -// use bytes::Bytes; -// use futures::{Async, Future}; -// use mime; -// use serde::{Deserialize, Serialize}; +#[cfg(test)] +mod tests { + use actix_http::http::header; + use bytes::Bytes; + use serde_derive::Deserialize; -// use crate::resource::Resource; -// // use crate::router::{ResourceDef, Router}; + use super::*; + use crate::test::TestRequest; -// #[derive(Deserialize, Debug, PartialEq)] -// struct Info { -// hello: String, -// } + #[derive(Deserialize, Debug, PartialEq)] + struct Info { + hello: String, + } -// #[test] -// fn test_bytes() { -// let cfg = PayloadConfig::default(); -// let req = TestRequest::with_header(header::CONTENT_LENGTH, "11") -// .set_payload(Bytes::from_static(b"hello=world")) -// .finish(); + #[test] + fn test_bytes() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .finish() + .into(); -// match Bytes::from_request(&req, &cfg).unwrap().poll().unwrap() { -// Async::Ready(s) => { -// assert_eq!(s, Bytes::from_static(b"hello=world")); -// } -// _ => unreachable!(), -// } -// } + let s = rt.block_on(Bytes::from_request(&mut req)).unwrap(); + assert_eq!(s, Bytes::from_static(b"hello=world")); + } -// #[test] -// fn test_string() { -// let cfg = PayloadConfig::default(); -// let req = TestRequest::with_header(header::CONTENT_LENGTH, "11") -// .set_payload(Bytes::from_static(b"hello=world")) -// .finish(); + #[test] + fn test_string() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .finish() + .into(); -// match String::from_request(&req, &cfg).unwrap().poll().unwrap() { -// Async::Ready(s) => { -// assert_eq!(s, "hello=world"); -// } -// _ => unreachable!(), -// } -// } + let s = rt.block_on(String::from_request(&mut req)).unwrap(); + assert_eq!(s, "hello=world"); + } -// #[test] -// fn test_form() { -// let req = TestRequest::with_header( -// header::CONTENT_TYPE, -// "application/x-www-form-urlencoded", -// ) -// .header(header::CONTENT_LENGTH, "11") -// .set_payload(Bytes::from_static(b"hello=world")) -// .finish(); + #[test] + fn test_form() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let mut req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .header(header::CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .finish() + .into(); -// let mut cfg = FormConfig::default(); -// cfg.limit(4096); -// match Form::::from_request(&req, &cfg).poll().unwrap() { -// Async::Ready(s) => { -// assert_eq!(s.hello, "world"); -// } -// _ => unreachable!(), -// } -// } + let s = rt.block_on(Form::::from_request(&mut req)).unwrap(); + assert_eq!(s.hello, "world"); + } +} // #[test] // fn test_option() { diff --git a/src/filter.rs b/src/filter.rs index e3d87b766..9b49c9dd3 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -230,14 +230,14 @@ impl Filter for HeaderFilter { #[cfg(test)] mod tests { - use crate::test::TestServiceRequest; use actix_http::http::{header, Method}; use super::*; + use crate::test::TestRequest; #[test] fn test_header() { - let req = TestServiceRequest::with_header(header::TRANSFER_ENCODING, "chunked") + let req = TestRequest::with_header(header::TRANSFER_ENCODING, "chunked") .finish() .into_request(); @@ -269,8 +269,8 @@ mod tests { #[test] fn test_methods() { - let req = TestServiceRequest::default().finish().into_request(); - let req2 = TestServiceRequest::default() + let req = TestRequest::default().finish().into_request(); + let req2 = TestRequest::default() .method(Method::POST) .finish() .into_request(); @@ -280,46 +280,38 @@ mod tests { assert!(Post().check(&req2)); assert!(!Post().check(&req)); - let r = TestServiceRequest::default().method(Method::PUT).finish(); + let r = TestRequest::default().method(Method::PUT).finish(); assert!(Put().check(&r,)); assert!(!Put().check(&req,)); - let r = TestServiceRequest::default() - .method(Method::DELETE) - .finish(); + let r = TestRequest::default().method(Method::DELETE).finish(); assert!(Delete().check(&r,)); assert!(!Delete().check(&req,)); - let r = TestServiceRequest::default().method(Method::HEAD).finish(); + let r = TestRequest::default().method(Method::HEAD).finish(); assert!(Head().check(&r,)); assert!(!Head().check(&req,)); - let r = TestServiceRequest::default() - .method(Method::OPTIONS) - .finish(); + let r = TestRequest::default().method(Method::OPTIONS).finish(); assert!(Options().check(&r,)); assert!(!Options().check(&req,)); - let r = TestServiceRequest::default() - .method(Method::CONNECT) - .finish(); + let r = TestRequest::default().method(Method::CONNECT).finish(); assert!(Connect().check(&r,)); assert!(!Connect().check(&req,)); - let r = TestServiceRequest::default().method(Method::PATCH).finish(); + let r = TestRequest::default().method(Method::PATCH).finish(); assert!(Patch().check(&r,)); assert!(!Patch().check(&req,)); - let r = TestServiceRequest::default().method(Method::TRACE).finish(); + let r = TestRequest::default().method(Method::TRACE).finish(); assert!(Trace().check(&r,)); assert!(!Trace().check(&req,)); } #[test] fn test_preds() { - let r = TestServiceRequest::default() - .method(Method::TRACE) - .request(); + let r = TestRequest::default().method(Method::TRACE).request(); assert!(Not(Get()).check(&r,)); assert!(!Not(Trace()).check(&r,)); diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index 83bb94c68..fa287b288 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -138,7 +138,7 @@ mod tests { use actix_service::FnService; use super::*; - use crate::test::TestServiceRequest; + use crate::test::TestRequest; use crate::{HttpResponse, ServiceRequest}; #[test] @@ -149,11 +149,11 @@ mod tests { req.into_response(HttpResponse::Ok().finish()) }); - let req = TestServiceRequest::default().finish(); + let req = TestRequest::default().finish(); let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - let req = TestServiceRequest::default().finish(); + let req = TestRequest::default().finish(); let mut srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish()) }); @@ -169,7 +169,7 @@ mod tests { req.into_response(HttpResponse::Ok().finish()) }); - let req = TestServiceRequest::default().finish(); + let req = TestRequest::default().finish(); let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 8ef316b4d..85127ee28 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -34,19 +34,6 @@ where } } -impl Clone for MiddlewareFactory -where - T: Transform + Clone, - S: Service, -{ - fn clone(&self) -> Self { - Self { - tr: self.tr.clone(), - _t: PhantomData, - } - } -} - impl NewTransform for MiddlewareFactory where T: Transform + Clone, diff --git a/src/test.rs b/src/test.rs index d67696b11..d6caf8973 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,10 +1,9 @@ //! Various helpers for Actix applications to use during testing. -use std::ops::{Deref, DerefMut}; use std::rc::Rc; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, Version}; -use actix_http::test::TestRequest; +use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{Extensions, PayloadStream}; use actix_router::{Path, Url}; use bytes::Bytes; @@ -39,45 +38,45 @@ use crate::service::ServiceRequest; /// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); /// } /// ``` -pub struct TestServiceRequest { - req: TestRequest, +pub struct TestRequest { + req: HttpTestRequest, extensions: Extensions, } -impl Default for TestServiceRequest { - fn default() -> TestServiceRequest { - TestServiceRequest { - req: TestRequest::default(), +impl Default for TestRequest { + fn default() -> TestRequest { + TestRequest { + req: HttpTestRequest::default(), extensions: Extensions::new(), } } } -impl TestServiceRequest { +impl TestRequest { /// Create TestRequest and set request uri - pub fn with_uri(path: &str) -> TestServiceRequest { - TestServiceRequest { - req: TestRequest::default().uri(path).take(), + pub fn with_uri(path: &str) -> TestRequest { + TestRequest { + req: HttpTestRequest::default().uri(path).take(), extensions: Extensions::new(), } } /// Create TestRequest and set header - pub fn with_hdr(hdr: H) -> TestServiceRequest { - TestServiceRequest { - req: TestRequest::default().set(hdr).take(), + pub fn with_hdr(hdr: H) -> TestRequest { + TestRequest { + req: HttpTestRequest::default().set(hdr).take(), extensions: Extensions::new(), } } /// Create TestRequest and set header - pub fn with_header(key: K, value: V) -> TestServiceRequest + pub fn with_header(key: K, value: V) -> TestRequest where HeaderName: HttpTryFrom, V: IntoHeaderValue, { - TestServiceRequest { - req: TestRequest::default().header(key, value).take(), + TestRequest { + req: HttpTestRequest::default().header(key, value).take(), extensions: Extensions::new(), } } @@ -145,17 +144,3 @@ impl TestServiceRequest { .into_request() } } - -impl Deref for TestServiceRequest { - type Target = TestRequest; - - fn deref(&self) -> &TestRequest { - &self.req - } -} - -impl DerefMut for TestServiceRequest { - fn deref_mut(&mut self) -> &mut TestRequest { - &mut self.req - } -} From 115b30d9ccf52c9f19609bf13862dfdd58b99d0b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 22:11:24 -0800 Subject: [PATCH 169/427] add state example --- src/app.rs | 21 ++++++++++++++++ src/extractor.rs | 65 ++++++++---------------------------------------- 2 files changed, 31 insertions(+), 55 deletions(-) diff --git a/src/app.rs b/src/app.rs index ae510621c..e9b10081b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -79,6 +79,27 @@ where /// multiple times. If you want to share state between different /// threads, a shared object should be used, e.g. `Arc`. Application /// state does not need to be `Send` or `Sync`. + /// + /// ```rust + /// use std::cell::Cell; + /// use actix_web::{web, State, App}; + /// + /// struct MyState { + /// counter: Cell, + /// } + /// + /// fn index(state: State) { + /// state.counter.set(state.counter.get() + 1); + /// } + /// + /// fn main() { + /// let app = App::new() + /// .state(MyState{ counter: Cell::new(0) }) + /// .resource( + /// "/index.html", + /// |r| r.route(web::get().to(index))); + /// } + /// ``` pub fn state(mut self, state: S) -> Self { self.state.push(Box::new(State::new(state))); self diff --git a/src/extractor.rs b/src/extractor.rs index 04dfb81ae..ea7681b0e 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -938,66 +938,21 @@ impl

    FromRequest

    for () { } } +#[rustfmt::skip] +mod m { + use super::*; + tuple_from_req!(TupleFromRequest1, (0, A)); tuple_from_req!(TupleFromRequest2, (0, A), (1, B)); tuple_from_req!(TupleFromRequest3, (0, A), (1, B), (2, C)); tuple_from_req!(TupleFromRequest4, (0, A), (1, B), (2, C), (3, D)); tuple_from_req!(TupleFromRequest5, (0, A), (1, B), (2, C), (3, D), (4, E)); -tuple_from_req!( - TupleFromRequest6, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F) -); -tuple_from_req!( - TupleFromRequest7, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G) -); -tuple_from_req!( - TupleFromRequest8, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H) -); -tuple_from_req!( - TupleFromRequest9, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H), - (8, I) -); -tuple_from_req!( - TupleFromRequest10, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H), - (8, I), - (9, J) -); +tuple_from_req!(TupleFromRequest6, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +tuple_from_req!(TupleFromRequest7, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +tuple_from_req!(TupleFromRequest8, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +tuple_from_req!(TupleFromRequest9, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +tuple_from_req!(TupleFromRequest10, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +} #[cfg(test)] mod tests { From b320dc127a3aa9a64b6b59808138b56edbb1ea56 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 22:22:45 -0800 Subject: [PATCH 170/427] remove unused code --- .travis.yml | 7 ++++++- src/app.rs | 23 ----------------------- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/.travis.yml b/.travis.yml index 077989d27..f0dc9e900 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ matrix: include: - rust: stable - rust: beta - - rust: nightly + - rust: nightly-2019-03-02 allow_failures: - rust: nightly @@ -24,6 +24,11 @@ before_install: - sudo apt-get update -qq - sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev +before_cache: | + if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then + RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin -f + fi + # Add clippy before_script: - export PATH=$PATH:~/.cargo/bin diff --git a/src/app.rs b/src/app.rs index e9b10081b..2a2380b21 100644 --- a/src/app.rs +++ b/src/app.rs @@ -54,12 +54,6 @@ impl App { } } -impl Default for App { - fn default() -> Self { - App::new() - } -} - impl App where P: 'static, @@ -249,23 +243,6 @@ where _t: PhantomData, } } - - /// Complete applicatin chain configuration and start resource - /// configuration. - pub fn router(self) -> AppRouter> { - let fref = Rc::new(RefCell::new(None)); - AppRouter { - chain: self.chain, - services: Vec::new(), - default: None, - defaults: Vec::new(), - endpoint: AppEntry::new(fref.clone()), - factory_ref: fref, - extensions: self.extensions, - state: self.state, - _t: PhantomData, - } - } } /// Structure that follows the builder pattern for building application From 08fcb6891e5f29cd615c7c839aaa004dee652959 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 22:33:46 -0800 Subject: [PATCH 171/427] use specific nightly version for travis --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index f0dc9e900..15a0b04e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,8 +25,8 @@ before_install: - sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev before_cache: | - if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then - RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin -f + if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then + RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin fi # Add clippy @@ -35,13 +35,13 @@ before_script: script: - | - if [[ "$TRAVIS_RUST_VERSION" != "nightly" ]]; then + if [[ "$TRAVIS_RUST_VERSION" != "nightly-2019-03-02" ]]; then cargo clean cargo check cargo test -- --nocapture fi - | - if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then + if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install -f cargo-tarpaulin RUST_BACKTRACE=1 cargo tarpaulin --out Xml bash <(curl -s https://codecov.io/bash) From 6df85e32dfa2ef7c2c3d587c65c8a3602406e772 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 00:57:48 -0800 Subject: [PATCH 172/427] added extractor configuration system --- src/extractor.rs | 83 ++++++++++++++++++++++++++++++++++++++---------- src/handler.rs | 58 ++++++++++++++++++++++++++------- src/lib.rs | 4 +-- src/request.rs | 1 + src/resource.rs | 2 +- src/route.rs | 66 ++++++++++++++++++++++++++++++++++---- src/service.rs | 29 +++++++++++------ src/state.rs | 1 + src/test.rs | 16 ++++++++-- 9 files changed, 210 insertions(+), 50 deletions(-) diff --git a/src/extractor.rs b/src/extractor.rs index ea7681b0e..24e4c8afa 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -23,7 +23,7 @@ use actix_http::http::StatusCode; use actix_http::{HttpMessage, Response}; use actix_router::PathDeserializer; -use crate::handler::FromRequest; +use crate::handler::{ConfigStorage, ExtractorConfig, FromRequest}; use crate::request::HttpRequest; use crate::responder::Responder; use crate::service::ServiceFromRequest; @@ -133,6 +133,7 @@ where { type Error = Error; type Future = FutureResult; + type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { @@ -219,6 +220,7 @@ where { type Error = Error; type Future = FutureResult; + type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { @@ -299,16 +301,18 @@ where { type Error = Error; type Future = Box>; + type Config = FormConfig; #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - let cfg = FormConfig::default(); - let req2 = req.clone(); + let cfg = req.load_config::(); + + let limit = cfg.limit; let err = Rc::clone(&cfg.ehandler); Box::new( UrlEncoded::new(req) - .limit(cfg.limit) + .limit(limit) .map_err(move |e| (*err)(e, &req2)) .map(Form), ) @@ -356,6 +360,7 @@ impl fmt::Display for Form { /// ); /// } /// ``` +#[derive(Clone)] pub struct FormConfig { limit: usize, ehandler: Rc Error>, @@ -363,13 +368,13 @@ pub struct FormConfig { impl FormConfig { /// Change max size of payload. By default max size is 256Kb - pub fn limit(&mut self, limit: usize) -> &mut Self { + pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self } /// Set custom error handler - pub fn error_handler(&mut self, f: F) -> &mut Self + pub fn error_handler(mut self, f: F) -> Self where F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static, { @@ -378,6 +383,8 @@ impl FormConfig { } } +impl ExtractorConfig for FormConfig {} + impl Default for FormConfig { fn default() -> Self { FormConfig { @@ -509,16 +516,18 @@ where { type Error = Error; type Future = Box>; + type Config = JsonConfig; #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - let cfg = JsonConfig::default(); - let req2 = req.clone(); + let cfg = req.load_config::(); + + let limit = cfg.limit; let err = Rc::clone(&cfg.ehandler); Box::new( JsonBody::new(req) - .limit(cfg.limit) + .limit(limit) .map_err(move |e| (*err)(e, &req2)) .map(Json), ) @@ -555,6 +564,7 @@ where /// }); /// } /// ``` +#[derive(Clone)] pub struct JsonConfig { limit: usize, ehandler: Rc Error>, @@ -562,13 +572,13 @@ pub struct JsonConfig { impl JsonConfig { /// Change max size of payload. By default max size is 256Kb - pub fn limit(&mut self, limit: usize) -> &mut Self { + pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self } /// Set custom error handler - pub fn error_handler(&mut self, f: F) -> &mut Self + pub fn error_handler(mut self, f: F) -> Self where F: Fn(JsonPayloadError, &HttpRequest) -> Error + 'static, { @@ -577,6 +587,8 @@ impl JsonConfig { } } +impl ExtractorConfig for JsonConfig {} + impl Default for JsonConfig { fn default() -> Self { JsonConfig { @@ -617,16 +629,18 @@ where type Error = Error; type Future = Either>, FutureResult>; + type Config = PayloadConfig; #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - let cfg = PayloadConfig::default(); + let cfg = req.load_config::(); if let Err(e) = cfg.check_mimetype(req) { return Either::B(err(e)); } - Either::A(Box::new(MessageBody::new(req).limit(cfg.limit).from_err())) + let limit = cfg.limit; + Either::A(Box::new(MessageBody::new(req).limit(limit).from_err())) } } @@ -664,10 +678,11 @@ where type Error = Error; type Future = Either>, FutureResult>; + type Config = PayloadConfig; #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - let cfg = PayloadConfig::default(); + let cfg = req.load_config::(); // check content-type if let Err(e) = cfg.check_mimetype(req) { @@ -679,10 +694,11 @@ where Ok(enc) => enc, Err(e) => return Either::B(err(e.into())), }; + let limit = cfg.limit; Either::A(Box::new( MessageBody::new(req) - .limit(cfg.limit) + .limit(limit) .from_err() .and_then(move |body| { let enc: *const Encoding = encoding as *const Encoding; @@ -753,6 +769,7 @@ where { type Error = Error; type Future = Box, Error = Error>>; + type Config = T::Config; #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { @@ -816,6 +833,7 @@ where { type Error = Error; type Future = Box, Error = Error>>; + type Config = T::Config; #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { @@ -827,21 +845,27 @@ where } /// Payload configuration for request's payload. +#[derive(Clone)] pub struct PayloadConfig { limit: usize, mimetype: Option, } impl PayloadConfig { + /// Create `PayloadConfig` instance and set max size of payload. + pub fn new(limit: usize) -> Self { + Self::default().limit(limit) + } + /// Change max size of payload. By default max size is 256Kb - pub fn limit(&mut self, limit: usize) -> &mut Self { + pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self } /// Set required mime-type of the request. By default mime type is not /// enforced. - pub fn mimetype(&mut self, mt: Mime) -> &mut Self { + pub fn mimetype(mut self, mt: Mime) -> Self { self.mimetype = Some(mt); self } @@ -867,6 +891,8 @@ impl PayloadConfig { } } +impl ExtractorConfig for PayloadConfig {} + impl Default for PayloadConfig { fn default() -> Self { PayloadConfig { @@ -876,6 +902,16 @@ impl Default for PayloadConfig { } } +macro_rules! tuple_config ({ $($T:ident),+} => { + impl<$($T,)+> ExtractorConfig for ($($T,)+) + where $($T: ExtractorConfig + Clone,)+ + { + fn store_default(ext: &mut ConfigStorage) { + $($T::store_default(ext);)+ + } + } +}); + macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { /// FromRequest implementation for tuple @@ -883,6 +919,7 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { { type Error = Error; type Future = $fut_type; + type Config = ($($T::Config,)+); fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { $fut_type { @@ -932,6 +969,7 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { impl

    FromRequest

    for () { type Error = Error; type Future = FutureResult<(), Error>; + type Config = (); fn from_request(_req: &mut ServiceFromRequest

    ) -> Self::Future { ok(()) @@ -942,6 +980,17 @@ impl

    FromRequest

    for () { mod m { use super::*; +tuple_config!(A); +tuple_config!(A, B); +tuple_config!(A, B, C); +tuple_config!(A, B, C, D); +tuple_config!(A, B, C, D, E); +tuple_config!(A, B, C, D, E, F); +tuple_config!(A, B, C, D, E, F, G); +tuple_config!(A, B, C, D, E, F, G, H); +tuple_config!(A, B, C, D, E, F, G, H, I); +tuple_config!(A, B, C, D, E, F, G, H, I, J); + tuple_from_req!(TupleFromRequest1, (0, A)); tuple_from_req!(TupleFromRequest2, (0, A), (1, B)); tuple_from_req!(TupleFromRequest3, (0, A), (1, B), (2, C)); diff --git a/src/handler.rs b/src/handler.rs index 31ae796b2..313422ed5 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,6 +1,8 @@ +use std::cell::RefCell; use std::marker::PhantomData; +use std::rc::Rc; -use actix_http::{Error, Response}; +use actix_http::{Error, Extensions, Response}; use actix_service::{NewService, Service, Void}; use futures::future::{ok, FutureResult}; use futures::{try_ready, Async, Future, IntoFuture, Poll}; @@ -19,10 +21,41 @@ pub trait FromRequest

    : Sized { /// Future that resolves to a Self type Future: Future; + /// Configuration for the extractor + type Config: ExtractorConfig; + /// Convert request to a Self fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future; } +/// Storage for extractor configs +#[derive(Default)] +pub struct ConfigStorage { + pub(crate) storage: Option>, +} + +impl ConfigStorage { + pub fn store(&mut self, config: C) { + if self.storage.is_none() { + self.storage = Some(Rc::new(Extensions::new())); + } + if let Some(ref mut ext) = self.storage { + Rc::get_mut(ext).unwrap().insert(config); + } + } +} + +pub trait ExtractorConfig: Default + Clone + 'static { + /// Set default configuration to config storage + fn store_default(ext: &mut ConfigStorage) { + ext.store(Self::default()) + } +} + +impl ExtractorConfig for () { + fn store_default(_: &mut ConfigStorage) {} +} + /// Handler converter factory pub trait Factory: Clone where @@ -288,18 +321,16 @@ where /// Extract arguments from request pub struct Extract> { + config: Rc>>>, _t: PhantomData<(P, T)>, } impl> Extract { - pub fn new() -> Self { - Extract { _t: PhantomData } - } -} - -impl> Default for Extract { - fn default() -> Self { - Self::new() + pub fn new(config: Rc>>>) -> Self { + Extract { + config, + _t: PhantomData, + } } } @@ -312,11 +343,15 @@ impl> NewService for Extract { type Future = FutureResult; fn new_service(&self, _: &()) -> Self::Future { - ok(ExtractService { _t: PhantomData }) + ok(ExtractService { + _t: PhantomData, + config: self.config.borrow().clone(), + }) } } pub struct ExtractService> { + config: Option>, _t: PhantomData<(P, T)>, } @@ -331,7 +366,7 @@ impl> Service for ExtractService { } fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { - let mut req = req.into(); + let mut req = ServiceFromRequest::new(req, self.config.clone()); ExtractResponse { fut: T::from_request(&mut req), req: Some(req), @@ -365,7 +400,6 @@ impl> Future for ExtractResponse { macro_rules! factory_tuple ({ $(($n:tt, $T:ident)),+} => { impl Factory<($($T,)+), Res> for Func where Func: Fn($($T,)+) -> Res + Clone + 'static, - //$($T,)+ Res: Responder + 'static, { fn call(&self, param: ($($T,)+)) -> Res { diff --git a/src/lib.rs b/src/lib.rs index 0e81b65ab..8ad689aa9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #![allow(clippy::type_complexity)] mod app; -mod extractor; +pub mod extractor; pub mod handler; // mod info; pub mod blocking; @@ -20,7 +20,7 @@ pub use actix_http::Response as HttpResponse; pub use actix_http::{http, Error, HttpMessage, ResponseError}; pub use crate::app::App; -pub use crate::extractor::{Form, Json, Path, Query}; +pub use crate::extractor::{Form, Json, Path, PayloadConfig, Query}; pub use crate::handler::FromRequest; pub use crate::request::HttpRequest; pub use crate::resource::Resource; diff --git a/src/request.rs b/src/request.rs index 538064f19..a7c84b534 100644 --- a/src/request.rs +++ b/src/request.rs @@ -143,6 +143,7 @@ impl HttpMessage for HttpRequest { impl

    FromRequest

    for HttpRequest { type Error = Error; type Future = FutureResult; + type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { diff --git a/src/resource.rs b/src/resource.rs index ab6096c52..0bee0ecd4 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -75,7 +75,7 @@ where /// } /// ``` pub fn route(mut self, route: Route

    ) -> Self { - self.routes.push(route); + self.routes.push(route.finish()); self } diff --git a/src/route.rs b/src/route.rs index 99117afed..578ba79ef 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,11 +1,15 @@ +use std::cell::RefCell; use std::rc::Rc; -use actix_http::{http::Method, Error, Response}; +use actix_http::{http::Method, Error, Extensions, Response}; use actix_service::{NewService, Service}; use futures::{Async, Future, IntoFuture, Poll}; use crate::filter::{self, Filter}; -use crate::handler::{AsyncFactory, AsyncHandle, Extract, Factory, FromRequest, Handle}; +use crate::handler::{ + AsyncFactory, AsyncHandle, ConfigStorage, Extract, ExtractorConfig, Factory, + FromRequest, Handle, +}; use crate::responder::Responder; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; use crate::HttpResponse; @@ -37,33 +41,50 @@ type BoxedRouteNewService = Box< pub struct Route

    { service: BoxedRouteNewService, ServiceResponse>, filters: Rc>>, + config: ConfigStorage, + config_ref: Rc>>>, } impl Route

    { + /// Create new route which matches any request. pub fn new() -> Route

    { + let config_ref = Rc::new(RefCell::new(None)); Route { - service: Box::new(RouteNewService::new(Extract::new().and_then( - Handle::new(|| HttpResponse::NotFound()).map_err(|_| panic!()), - ))), + service: Box::new(RouteNewService::new( + Extract::new(config_ref.clone()).and_then( + Handle::new(|| HttpResponse::NotFound()).map_err(|_| panic!()), + ), + )), filters: Rc::new(Vec::new()), + config: ConfigStorage::default(), + config_ref, } } + /// Create new `GET` route. pub fn get() -> Route

    { Route::new().method(Method::GET) } + /// Create new `POST` route. pub fn post() -> Route

    { Route::new().method(Method::POST) } + /// Create new `PUT` route. pub fn put() -> Route

    { Route::new().method(Method::PUT) } + /// Create new `DELETE` route. pub fn delete() -> Route

    { Route::new().method(Method::DELETE) } + + pub(crate) fn finish(self) -> Self { + *self.config_ref.borrow_mut() = self.config.storage.clone(); + self + } } impl

    NewService for Route

    { @@ -260,8 +281,10 @@ impl Route

    { T: FromRequest

    + 'static, R: Responder + 'static, { + T::Config::store_default(&mut self.config); self.service = Box::new(RouteNewService::new( - Extract::new().and_then(Handle::new(handler).map_err(|_| panic!())), + Extract::new(self.config_ref.clone()) + .and_then(Handle::new(handler).map_err(|_| panic!())), )); self } @@ -305,10 +328,39 @@ impl Route

    { R::Error: Into, { self.service = Box::new(RouteNewService::new( - Extract::new().and_then(AsyncHandle::new(handler).map_err(|_| panic!())), + Extract::new(self.config_ref.clone()) + .and_then(AsyncHandle::new(handler).map_err(|_| panic!())), )); self } + + /// This method allows to add extractor configuration + /// for specific route. + /// + /// ```rust + /// use actix_web::{web, extractor, App}; + /// + /// /// extract text data from request + /// fn index(body: String) -> String { + /// format!("Body {}!", body) + /// } + /// + /// fn main() { + /// let app = App::new().resource("/index.html", |r| { + /// r.route( + /// web::get() + /// // limit size of the payload + /// .config(extractor::PayloadConfig::new(4096)) + /// // register handler + /// .to(index) + /// ) + /// }); + /// } + /// ``` + pub fn config(mut self, config: C) -> Self { + self.config.store(config); + self + } } // pub struct RouteServiceBuilder { diff --git a/src/service.rs b/src/service.rs index 99af73c16..5602a6133 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::cell::{Ref, RefMut}; use std::rc::Rc; @@ -167,9 +168,18 @@ impl

    std::ops::DerefMut for ServiceRequest

    { pub struct ServiceFromRequest

    { req: HttpRequest, payload: Payload

    , + config: Option>, } impl

    ServiceFromRequest

    { + pub(crate) fn new(req: ServiceRequest

    , config: Option>) -> Self { + Self { + req: req.req, + payload: req.payload, + config, + } + } + #[inline] pub fn into_request(self) -> HttpRequest { self.req @@ -180,6 +190,16 @@ impl

    ServiceFromRequest

    { pub fn error_response>(self, err: E) -> ServiceResponse { ServiceResponse::new(self.req, err.into().into()) } + + /// Load extractor configuration + pub fn load_config(&self) -> Cow { + if let Some(ref ext) = self.config { + if let Some(cfg) = ext.get::() { + return Cow::Borrowed(cfg); + } + } + Cow::Owned(T::default()) + } } impl

    std::ops::Deref for ServiceFromRequest

    { @@ -204,15 +224,6 @@ impl

    HttpMessage for ServiceFromRequest

    { } } -impl

    From> for ServiceFromRequest

    { - fn from(req: ServiceRequest

    ) -> Self { - Self { - req: req.req, - payload: req.payload, - } - } -} - pub struct ServiceResponse { request: HttpRequest, response: Response, diff --git a/src/state.rs b/src/state.rs index db2637775..4a450245a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -48,6 +48,7 @@ impl Clone for State { impl FromRequest

    for State { type Error = Error; type Future = FutureResult; + type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { diff --git a/src/test.rs b/src/test.rs index d6caf8973..4899cfe41 100644 --- a/src/test.rs +++ b/src/test.rs @@ -9,7 +9,7 @@ use actix_router::{Path, Url}; use bytes::Bytes; use crate::request::HttpRequest; -use crate::service::ServiceRequest; +use crate::service::{ServiceFromRequest, ServiceRequest}; /// Test `Request` builder /// @@ -133,7 +133,7 @@ impl TestRequest { } /// Complete request creation and generate `HttpRequest` instance - pub fn request(mut self) -> HttpRequest { + pub fn to_request(mut self) -> HttpRequest { let req = self.req.finish(); ServiceRequest::new( @@ -143,4 +143,16 @@ impl TestRequest { ) .into_request() } + + /// Complete request creation and generate `ServiceFromRequest` instance + pub fn to_from(mut self) -> ServiceFromRequest { + let req = self.req.finish(); + + let req = ServiceRequest::new( + Path::new(Url::new(req.uri().clone())), + req, + Rc::new(self.extensions), + ); + ServiceFromRequest::new(req, None) + } } From a8f3dec527dd3ce3d0aef1139e23a367bec5f7f4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 01:03:28 -0800 Subject: [PATCH 173/427] use tarpaulin from cache --- .travis.yml | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 15a0b04e9..da8f33bee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,19 +34,9 @@ before_script: - export PATH=$PATH:~/.cargo/bin script: - - | - if [[ "$TRAVIS_RUST_VERSION" != "nightly-2019-03-02" ]]; then - cargo clean - cargo check - cargo test -- --nocapture - fi - - | - if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then - RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install -f cargo-tarpaulin - RUST_BACKTRACE=1 cargo tarpaulin --out Xml - bash <(curl -s https://codecov.io/bash) - echo "Uploaded code coverage" - fi + - cargo clean + - cargo check + - cargo test -- --nocapture # Upload docs after_success: @@ -58,3 +48,9 @@ after_success: ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc && echo "Uploaded documentation" fi + - | + if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then + cargo tarpaulin --features="ssl" --out Xml + bash <(curl -s https://codecov.io/bash) + echo "Uploaded code coverage" + fi From f90ca868ca76f8e3d63475f3f661558f1e96330f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 01:12:06 -0800 Subject: [PATCH 174/427] update tests --- src/extractor.rs | 9 +++------ src/filter.rs | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/extractor.rs b/src/extractor.rs index 24e4c8afa..5fa9af61c 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -1022,8 +1022,7 @@ mod tests { let mut rt = actix_rt::Runtime::new().unwrap(); let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) - .finish() - .into(); + .to_from(); let s = rt.block_on(Bytes::from_request(&mut req)).unwrap(); assert_eq!(s, Bytes::from_static(b"hello=world")); @@ -1034,8 +1033,7 @@ mod tests { let mut rt = actix_rt::Runtime::new().unwrap(); let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) - .finish() - .into(); + .to_from(); let s = rt.block_on(String::from_request(&mut req)).unwrap(); assert_eq!(s, "hello=world"); @@ -1050,8 +1048,7 @@ mod tests { ) .header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) - .finish() - .into(); + .to_from(); let s = rt.block_on(Form::::from_request(&mut req)).unwrap(); assert_eq!(s.hello, "world"); diff --git a/src/filter.rs b/src/filter.rs index 9b49c9dd3..501c60d83 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -311,7 +311,7 @@ mod tests { #[test] fn test_preds() { - let r = TestRequest::default().method(Method::TRACE).request(); + let r = TestRequest::default().method(Method::TRACE).to_request(); assert!(Not(Get()).check(&r,)); assert!(!Not(Trace()).check(&r,)); From 015364edf841d37a4a4ddd977934aa87fd77ede8 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 08:00:12 -0800 Subject: [PATCH 175/427] fix travis config --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index da8f33bee..d994a80d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,7 +50,7 @@ after_success: fi - | if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then - cargo tarpaulin --features="ssl" --out Xml + cargo tarpaulin --out Xml bash <(curl -s https://codecov.io/bash) echo "Uploaded code coverage" fi From b81ae899f6472d58eb525715308ed9ad3f59b241 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 08:24:09 -0800 Subject: [PATCH 176/427] better naming --- .travis.yml | 1 - src/app.rs | 60 ++++++++++++++++++++++++++++------------------------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/.travis.yml b/.travis.yml index d994a80d4..1d3c227a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,6 @@ before_script: script: - cargo clean - - cargo check - cargo test -- --nocapture # Upload docs diff --git a/src/app.rs b/src/app.rs index 2a2380b21..c9c23d9cb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -188,13 +188,13 @@ where > where M: NewTransform< - AppService

    , + AppRouting

    , Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoNewTransform>, + F: IntoNewTransform>, { let fref = Rc::new(RefCell::new(None)); let endpoint = ApplyNewService::new(mw, AppEntry::new(fref.clone())); @@ -253,7 +253,7 @@ pub struct AppRouter { default: Option>>, defaults: Vec>>>>>, endpoint: T, - factory_ref: Rc>>>, + factory_ref: Rc>>>, extensions: Extensions, state: Vec>, _t: PhantomData<(P, B)>, @@ -465,7 +465,7 @@ where } // set factory - *self.factory_ref.borrow_mut() = Some(AppFactory { + *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { services: Rc::new(self.services), }); @@ -478,25 +478,25 @@ where } } -pub struct AppFactory

    { +pub struct AppRoutingFactory

    { services: Rc)>>, } -impl NewService for AppFactory

    { +impl NewService for AppRoutingFactory

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type InitError = (); - type Service = AppService

    ; - type Future = CreateAppService

    ; + type Service = AppRouting

    ; + type Future = AppRoutingFactoryResponse

    ; fn new_service(&self, _: &()) -> Self::Future { - CreateAppService { + AppRoutingFactoryResponse { fut: self .services .iter() .map(|(path, service)| { - CreateAppServiceItem::Future( + CreateAppRoutingItem::Future( Some(path.clone()), service.new_service(&()), ) @@ -510,17 +510,17 @@ type HttpServiceFut

    = Box, Error = ()>>; /// Create app service #[doc(hidden)] -pub struct CreateAppService

    { - fut: Vec>, +pub struct AppRoutingFactoryResponse

    { + fut: Vec>, } -enum CreateAppServiceItem

    { +enum CreateAppRoutingItem

    { Future(Option, HttpServiceFut

    ), Service(ResourceDef, HttpService

    ), } -impl

    Future for CreateAppService

    { - type Item = AppService

    ; +impl

    Future for AppRoutingFactoryResponse

    { + type Item = AppRouting

    ; type Error = (); fn poll(&mut self) -> Poll { @@ -529,7 +529,7 @@ impl

    Future for CreateAppService

    { // poll http services for item in &mut self.fut { let res = match item { - CreateAppServiceItem::Future(ref mut path, ref mut fut) => { + CreateAppRoutingItem::Future(ref mut path, ref mut fut) => { match fut.poll()? { Async::Ready(service) => Some((path.take().unwrap(), service)), Async::NotReady => { @@ -538,11 +538,11 @@ impl

    Future for CreateAppService

    { } } } - CreateAppServiceItem::Service(_, _) => continue, + CreateAppRoutingItem::Service(_, _) => continue, }; if let Some((path, service)) = res { - *item = CreateAppServiceItem::Service(path, service); + *item = CreateAppRoutingItem::Service(path, service); } } @@ -552,14 +552,14 @@ impl

    Future for CreateAppService

    { .drain(..) .fold(Router::build(), |mut router, item| { match item { - CreateAppServiceItem::Service(path, service) => { + CreateAppRoutingItem::Service(path, service) => { router.rdef(path, service) } - CreateAppServiceItem::Future(_, _) => unreachable!(), + CreateAppRoutingItem::Future(_, _) => unreachable!(), } router }); - Ok(Async::Ready(AppService { + Ok(Async::Ready(AppRouting { router: router.finish(), ready: None, })) @@ -569,12 +569,12 @@ impl

    Future for CreateAppService

    { } } -pub struct AppService

    { +pub struct AppRouting

    { router: Router>, ready: Option<(ServiceRequest

    , ResourceInfo)>, } -impl

    Service for AppService

    { +impl

    Service for AppRouting

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); @@ -599,12 +599,13 @@ impl

    Service for AppService

    { } #[doc(hidden)] +/// Wrapper service for routing pub struct AppEntry

    { - factory: Rc>>>, + factory: Rc>>>, } impl

    AppEntry

    { - fn new(factory: Rc>>>) -> Self { + fn new(factory: Rc>>>) -> Self { AppEntry { factory } } } @@ -614,8 +615,8 @@ impl NewService for AppEntry

    { type Response = ServiceResponse; type Error = (); type InitError = (); - type Service = AppService

    ; - type Future = CreateAppService

    ; + type Service = AppRouting

    ; + type Future = AppRoutingFactoryResponse

    ; fn new_service(&self, _: &()) -> Self::Future { self.factory.borrow_mut().as_mut().unwrap().new_service(&()) @@ -644,16 +645,19 @@ impl Service for AppChain { type Error = (); type Future = FutureResult; + #[inline] fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } + #[inline] fn call(&mut self, req: Self::Request) -> Self::Future { ok(req) } } -/// Service factory to convert `Request` to a `ServiceRequest` +/// Service factory to convert `Request` to a `ServiceRequest`. +/// It also executes state factories. pub struct AppInit where C: NewService, Response = ServiceRequest

    >, From 237677be15d00b6074b18eb214790ccbd21eb349 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 12:09:38 -0800 Subject: [PATCH 177/427] rename filter to guard --- src/{filter.rs => guard.rs} | 139 +++++++++++---------- src/lib.rs | 2 +- src/resource.rs | 51 ++++---- src/route.rs | 243 ++++++++---------------------------- 4 files changed, 145 insertions(+), 290 deletions(-) rename src/{filter.rs => guard.rs} (67%) diff --git a/src/filter.rs b/src/guard.rs similarity index 67% rename from src/filter.rs rename to src/guard.rs index 501c60d83..10a56921a 100644 --- a/src/filter.rs +++ b/src/guard.rs @@ -1,47 +1,48 @@ -//! Route match predicates +//! Route match guards. #![allow(non_snake_case)] use actix_http::http::{self, header, HttpTryFrom}; use actix_http::RequestHead; -/// Trait defines resource predicate. -/// Predicate can modify request object. It is also possible to +/// Trait defines resource guards. Guards are used for routes selection. +/// +/// Guard can not modify request object. But it is possible to /// to store extra attributes on request by using `Extensions` container, -/// Extensions container available via `HttpRequest::extensions()` method. -pub trait Filter { +/// Extensions container available via `RequestHead::extensions()` method. +pub trait Guard { /// Check if request matches predicate fn check(&self, request: &RequestHead) -> bool; } -/// Return filter that matches if any of supplied filters. +/// Return guard that matches if any of supplied guards. /// /// ```rust -/// use actix_web::{web, filter, App, HttpResponse}; +/// use actix_web::{web, guard, App, HttpResponse}; /// /// fn main() { /// App::new().resource("/index.html", |r| /// r.route( /// web::route() -/// .filter(filter::Any(filter::Get()).or(filter::Post())) +/// .guard(guard::Any(guard::Get()).or(guard::Post())) /// .to(|| HttpResponse::MethodNotAllowed())) /// ); /// } /// ``` -pub fn Any(filter: F) -> AnyFilter { - AnyFilter(vec![Box::new(filter)]) +pub fn Any(guard: F) -> AnyGuard { + AnyGuard(vec![Box::new(guard)]) } -/// Matches if any of supplied filters matche. -pub struct AnyFilter(Vec>); +/// Matches if any of supplied guards matche. +pub struct AnyGuard(Vec>); -impl AnyFilter { - /// Add filter to a list of filters to check - pub fn or(mut self, filter: F) -> Self { - self.0.push(Box::new(filter)); +impl AnyGuard { + /// Add guard to a list of guards to check + pub fn or(mut self, guard: F) -> Self { + self.0.push(Box::new(guard)); self } } -impl Filter for AnyFilter { +impl Guard for AnyGuard { fn check(&self, req: &RequestHead) -> bool { for p in &self.0 { if p.check(req) { @@ -52,37 +53,37 @@ impl Filter for AnyFilter { } } -/// Return filter that matches if all of supplied filters match. +/// Return guard that matches if all of the supplied guards. /// /// ```rust /// # extern crate actix_web; -/// use actix_web::{filter, web, App, HttpResponse}; +/// use actix_web::{guard, web, App, HttpResponse}; /// /// fn main() { /// App::new().resource("/index.html", |r| { /// r.route(web::route() -/// .filter( -/// filter::All(filter::Get()).and(filter::Header("content-type", "text/plain"))) +/// .guard( +/// guard::All(guard::Get()).and(guard::Header("content-type", "text/plain"))) /// .to(|| HttpResponse::MethodNotAllowed())) /// }); /// } /// ``` -pub fn All(filter: F) -> AllFilter { - AllFilter(vec![Box::new(filter)]) +pub fn All(guard: F) -> AllGuard { + AllGuard(vec![Box::new(guard)]) } -/// Matches if all of supplied filters matche. -pub struct AllFilter(Vec>); +/// Matches if all of supplied guards. +pub struct AllGuard(Vec>); -impl AllFilter { - /// Add new predicate to list of predicates to check - pub fn and(mut self, filter: F) -> Self { - self.0.push(Box::new(filter)); +impl AllGuard { + /// Add new guard to the list of guards to check + pub fn and(mut self, guard: F) -> Self { + self.0.push(Box::new(guard)); self } } -impl Filter for AllFilter { +impl Guard for AllGuard { fn check(&self, request: &RequestHead) -> bool { for p in &self.0 { if !p.check(request) { @@ -93,93 +94,93 @@ impl Filter for AllFilter { } } -/// Return predicate that matches if supplied predicate does not match. -pub fn Not(filter: F) -> NotFilter { - NotFilter(Box::new(filter)) +/// Return guard that matches if supplied guard does not match. +pub fn Not(guard: F) -> NotGuard { + NotGuard(Box::new(guard)) } #[doc(hidden)] -pub struct NotFilter(Box); +pub struct NotGuard(Box); -impl Filter for NotFilter { +impl Guard for NotGuard { fn check(&self, request: &RequestHead) -> bool { !self.0.check(request) } } -/// Http method predicate +/// Http method guard #[doc(hidden)] -pub struct MethodFilter(http::Method); +pub struct MethodGuard(http::Method); -impl Filter for MethodFilter { +impl Guard for MethodGuard { fn check(&self, request: &RequestHead) -> bool { request.method == self.0 } } -/// Predicate to match *GET* http method -pub fn Get() -> MethodFilter { - MethodFilter(http::Method::GET) +/// Guard to match *GET* http method +pub fn Get() -> MethodGuard { + MethodGuard(http::Method::GET) } /// Predicate to match *POST* http method -pub fn Post() -> MethodFilter { - MethodFilter(http::Method::POST) +pub fn Post() -> MethodGuard { + MethodGuard(http::Method::POST) } /// Predicate to match *PUT* http method -pub fn Put() -> MethodFilter { - MethodFilter(http::Method::PUT) +pub fn Put() -> MethodGuard { + MethodGuard(http::Method::PUT) } /// Predicate to match *DELETE* http method -pub fn Delete() -> MethodFilter { - MethodFilter(http::Method::DELETE) +pub fn Delete() -> MethodGuard { + MethodGuard(http::Method::DELETE) } /// Predicate to match *HEAD* http method -pub fn Head() -> MethodFilter { - MethodFilter(http::Method::HEAD) +pub fn Head() -> MethodGuard { + MethodGuard(http::Method::HEAD) } /// Predicate to match *OPTIONS* http method -pub fn Options() -> MethodFilter { - MethodFilter(http::Method::OPTIONS) +pub fn Options() -> MethodGuard { + MethodGuard(http::Method::OPTIONS) } /// Predicate to match *CONNECT* http method -pub fn Connect() -> MethodFilter { - MethodFilter(http::Method::CONNECT) +pub fn Connect() -> MethodGuard { + MethodGuard(http::Method::CONNECT) } /// Predicate to match *PATCH* http method -pub fn Patch() -> MethodFilter { - MethodFilter(http::Method::PATCH) +pub fn Patch() -> MethodGuard { + MethodGuard(http::Method::PATCH) } /// Predicate to match *TRACE* http method -pub fn Trace() -> MethodFilter { - MethodFilter(http::Method::TRACE) +pub fn Trace() -> MethodGuard { + MethodGuard(http::Method::TRACE) } /// Predicate to match specified http method -pub fn Method(method: http::Method) -> MethodFilter { - MethodFilter(method) +pub fn Method(method: http::Method) -> MethodGuard { + MethodGuard(method) } /// Return predicate that matches if request contains specified header and /// value. -pub fn Header(name: &'static str, value: &'static str) -> HeaderFilter { - HeaderFilter( +pub fn Header(name: &'static str, value: &'static str) -> HeaderGuard { + HeaderGuard( header::HeaderName::try_from(name).unwrap(), header::HeaderValue::from_static(value), ) } #[doc(hidden)] -pub struct HeaderFilter(header::HeaderName, header::HeaderValue); +pub struct HeaderGuard(header::HeaderName, header::HeaderValue); -impl Filter for HeaderFilter { +impl Guard for HeaderGuard { fn check(&self, req: &RequestHead) -> bool { if let Some(val) = req.headers.get(&self.0) { return val == self.1; @@ -197,26 +198,26 @@ impl Filter for HeaderFilter { // /// fn main() { // /// App::new().resource("/index.html", |r| { // /// r.route() -// /// .filter(pred::Host("www.rust-lang.org")) +// /// .guard(pred::Host("www.rust-lang.org")) // /// .f(|_| HttpResponse::MethodNotAllowed()) // /// }); // /// } // /// ``` -// pub fn Host>(host: H) -> HostFilter { -// HostFilter(host.as_ref().to_string(), None) +// pub fn Host>(host: H) -> HostGuard { +// HostGuard(host.as_ref().to_string(), None) // } // #[doc(hidden)] -// pub struct HostFilter(String, Option); +// pub struct HostGuard(String, Option); -// impl HostFilter { +// impl HostGuard { // /// Set reuest scheme to match // pub fn scheme>(&mut self, scheme: H) { // self.1 = Some(scheme.as_ref().to_string()) // } // } -// impl Filter for HostFilter { +// impl Guard for HostGuard { // fn check(&self, _req: &RequestHead) -> bool { // // let info = req.connection_info(); // // if let Some(ref scheme) = self.1 { diff --git a/src/lib.rs b/src/lib.rs index 8ad689aa9..e876a7ea4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ pub mod extractor; pub mod handler; // mod info; pub mod blocking; -pub mod filter; +pub mod guard; pub mod middleware; mod request; mod resource; diff --git a/src/resource.rs b/src/resource.rs index 0bee0ecd4..98c2dc114 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -56,22 +56,21 @@ where InitError = (), >, { - /// Register a new route and return mutable reference to *Route* object. - /// *Route* is used for route configuration, i.e. adding predicates, + /// Register a new route. + /// *Route* is used for route configuration, i.e. adding guards, /// setting up handler. /// - /// ```rust,ignore - /// use actix_web::*; + /// ```rust + /// use actix_web::{web, guard, App, HttpResponse}; /// /// fn main() { /// let app = App::new() /// .resource("/", |r| { - /// r.route() - /// .filter(pred::Any(pred::Get()).or(pred::Put())) - /// .filter(pred::Header("Content-Type", "text/plain")) - /// .f(|r| HttpResponse::Ok()) - /// }) - /// .finish(); + /// r.route(web::route() + /// .guard(guard::Any(guard::Get()).or(guard::Put())) + /// .guard(guard::Header("Content-Type", "text/plain")) + /// .to(|| HttpResponse::Ok())) + /// }); /// } /// ``` pub fn route(mut self, route: Route

    ) -> Self { @@ -81,21 +80,23 @@ where /// Register a new route and add handler. /// - /// ```rust,ignore - /// # extern crate actix_web; + /// ```rust /// use actix_web::*; - /// fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } /// - /// App::new().resource("/", |r| r.with(index)); + /// fn index(req: HttpRequest) -> HttpResponse { + /// unimplemented!() + /// } + /// + /// App::new().resource("/", |r| r.to(index)); /// ``` /// /// This is shortcut for: /// - /// ```rust,ignore + /// ```rust /// # extern crate actix_web; /// # use actix_web::*; /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } - /// App::new().resource("/", |r| r.route().with(index)); + /// App::new().resource("/", |r| r.route(web::route().to(index))); /// ``` pub fn to(mut self, handler: F) -> Self where @@ -109,30 +110,26 @@ where /// Register a new route and add async handler. /// - /// ```rust,ignore - /// # extern crate actix_web; - /// # extern crate futures; + /// ```rust /// use actix_web::*; - /// use futures::future::Future; + /// use futures::future::{ok, Future}; /// - /// fn index(req: HttpRequest) -> Box> { - /// unimplemented!() + /// fn index(req: HttpRequest) -> impl Future { + /// ok(HttpResponse::Ok().finish()) /// } /// - /// App::new().resource("/", |r| r.with_async(index)); + /// App::new().resource("/", |r| r.to_async(index)); /// ``` /// /// This is shortcut for: /// - /// ```rust,ignore - /// # extern crate actix_web; - /// # extern crate futures; + /// ```rust /// # use actix_web::*; /// # use futures::future::Future; /// # fn index(req: HttpRequest) -> Box> { /// # unimplemented!() /// # } - /// App::new().resource("/", |r| r.route().with_async(index)); + /// App::new().resource("/", |r| r.route(web::route().to_async(index))); /// ``` #[allow(clippy::wrong_self_convention)] pub fn to_async(mut self, handler: F) -> Self diff --git a/src/route.rs b/src/route.rs index 578ba79ef..16a4fc5ba 100644 --- a/src/route.rs +++ b/src/route.rs @@ -5,7 +5,7 @@ use actix_http::{http::Method, Error, Extensions, Response}; use actix_service::{NewService, Service}; use futures::{Async, Future, IntoFuture, Poll}; -use crate::filter::{self, Filter}; +use crate::guard::{self, Guard}; use crate::handler::{ AsyncFactory, AsyncHandle, ConfigStorage, Extract, ExtractorConfig, Factory, FromRequest, Handle, @@ -40,7 +40,7 @@ type BoxedRouteNewService = Box< /// If handler is not explicitly set, default *404 Not Found* handler is used. pub struct Route

    { service: BoxedRouteNewService, ServiceResponse>, - filters: Rc>>, + guards: Rc>>, config: ConfigStorage, config_ref: Rc>>>, } @@ -55,7 +55,7 @@ impl Route

    { Handle::new(|| HttpResponse::NotFound()).map_err(|_| panic!()), ), )), - filters: Rc::new(Vec::new()), + guards: Rc::new(Vec::new()), config: ConfigStorage::default(), config_ref, } @@ -98,7 +98,7 @@ impl

    NewService for Route

    { fn new_service(&self, _: &()) -> Self::Future { CreateRouteService { fut: self.service.new_service(&()), - filters: self.filters.clone(), + guards: self.guards.clone(), } } } @@ -109,7 +109,7 @@ type RouteFuture

    = Box< pub struct CreateRouteService

    { fut: RouteFuture

    , - filters: Rc>>, + guards: Rc>>, } impl

    Future for CreateRouteService

    { @@ -120,7 +120,7 @@ impl

    Future for CreateRouteService

    { match self.fut.poll()? { Async::Ready(service) => Ok(Async::Ready(RouteService { service, - filters: self.filters.clone(), + guards: self.guards.clone(), })), Async::NotReady => Ok(Async::NotReady), } @@ -129,12 +129,12 @@ impl

    Future for CreateRouteService

    { pub struct RouteService

    { service: BoxedRouteService, ServiceResponse>, - filters: Rc>>, + guards: Rc>>, } impl

    RouteService

    { pub fn check(&self, req: &mut ServiceRequest

    ) -> bool { - for f in self.filters.iter() { + for f in self.guards.iter() { if !f.check(req.head()) { return false; } @@ -159,45 +159,41 @@ impl

    Service for RouteService

    { } impl Route

    { - /// Add method match filter to the route. + /// Add method guard to the route. /// - /// ```rust,ignore - /// # extern crate actix_web; + /// ```rust /// # use actix_web::*; /// # fn main() { /// App::new().resource("/path", |r| { - /// r.route() - /// .filter(pred::Get()) - /// .filter(pred::Header("content-type", "text/plain")) - /// .f(|req| HttpResponse::Ok()) - /// }) - /// # .finish(); + /// r.route(web::get() + /// .guard(guard::Get()) + /// .guard(guard::Header("content-type", "text/plain")) + /// .to(|req: HttpRequest| HttpResponse::Ok())) + /// }); /// # } /// ``` pub fn method(mut self, method: Method) -> Self { - Rc::get_mut(&mut self.filters) + Rc::get_mut(&mut self.guards) .unwrap() - .push(Box::new(filter::Method(method))); + .push(Box::new(guard::Method(method))); self } - /// Add filter to the route. + /// Add guard to the route. /// - /// ```rust,ignore - /// # extern crate actix_web; + /// ```rust /// # use actix_web::*; /// # fn main() { /// App::new().resource("/path", |r| { - /// r.route() - /// .filter(pred::Get()) - /// .filter(pred::Header("content-type", "text/plain")) - /// .f(|req| HttpResponse::Ok()) - /// }) - /// # .finish(); + /// r.route(web::route() + /// .guard(guard::Get()) + /// .guard(guard::Header("content-type", "text/plain")) + /// .to(|req: HttpRequest| HttpResponse::Ok())) + /// }); /// # } /// ``` - pub fn filter(mut self, f: F) -> Self { - Rc::get_mut(&mut self.filters).unwrap().push(Box::new(f)); + pub fn guard(mut self, f: F) -> Self { + Rc::get_mut(&mut self.guards).unwrap().push(Box::new(f)); self } @@ -214,19 +210,16 @@ impl Route

    { // { // RouteServiceBuilder { // service: md.into_new_service(), - // filters: self.filters, + // guards: self.guards, // _t: PhantomData, // } // } - /// Set handler function, use request extractor for parameters. + /// Set handler function, use request extractors for parameters. /// - /// ```rust,ignore - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; + /// ```rust /// #[macro_use] extern crate serde_derive; - /// use actix_web::{http, App, Path, Result}; + /// use actix_web::{web, http, App, Path}; /// /// #[derive(Deserialize)] /// struct Info { @@ -234,27 +227,24 @@ impl Route

    { /// } /// /// /// extract path info using serde - /// fn index(info: Path) -> Result { - /// Ok(format!("Welcome {}!", info.username)) + /// fn index(info: Path) -> String { + /// format!("Welcome {}!", info.username) /// } /// /// fn main() { /// let app = App::new().resource( /// "/{username}/index.html", // <- define path parameters - /// |r| r.method(http::Method::GET).with(index), - /// ); // <- use `with` extractor + /// |r| r.route(web::get().to(index)), // <- register handler + /// ); /// } /// ``` /// /// It is possible to use multiple extractors for one handler function. /// - /// ```rust,ignore - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; - /// #[macro_use] extern crate serde_derive; + /// ```rust /// # use std::collections::HashMap; - /// use actix_web::{http, App, Json, Path, Query, Result}; + /// # use serde_derive::Deserialize; + /// use actix_web::{web, http, App, Json, Path, Query}; /// /// #[derive(Deserialize)] /// struct Info { @@ -262,17 +252,15 @@ impl Route

    { /// } /// /// /// extract path info using serde - /// fn index( - /// path: Path, query: Query>, body: Json, - /// ) -> Result { - /// Ok(format!("Welcome {}!", path.username)) + /// fn index(path: Path, query: Query>, body: Json) -> String { + /// format!("Welcome {}!", path.username) /// } /// /// fn main() { /// let app = App::new().resource( /// "/{username}/index.html", // <- define path parameters - /// |r| r.method(http::Method::GET).with(index), - /// ); // <- use `with` extractor + /// |r| r.route(web::method(http::Method::GET).to(index)), + /// ); /// } /// ``` pub fn to(mut self, handler: F) -> Route

    @@ -289,16 +277,13 @@ impl Route

    { self } - /// Set async handler function, use request extractor for parameters. - /// Also this method needs to be used if your handler function returns - /// `impl Future<>` + /// Set async handler function, use request extractors for parameters. + /// This method has to be used if your handler function returns `impl Future<>` /// - /// ```rust,ignore - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; + /// ```rust + /// # use futures::future::ok; /// #[macro_use] extern crate serde_derive; - /// use actix_web::{http, App, Error, Path}; + /// use actix_web::{web, http, App, Error, Path}; /// use futures::Future; /// /// #[derive(Deserialize)] @@ -307,15 +292,15 @@ impl Route

    { /// } /// /// /// extract path info using serde - /// fn index(info: Path) -> Box> { - /// unimplemented!() + /// fn index(info: Path) -> impl Future { + /// ok("Hello World!") /// } /// /// fn main() { /// let app = App::new().resource( /// "/{username}/index.html", // <- define path parameters - /// |r| r.method(http::Method::GET).with_async(index), - /// ); // <- use `with` extractor + /// |r| r.route(web::get().to_async(index)), // <- register async handler + /// ); /// } /// ``` #[allow(clippy::wrong_self_convention)] @@ -363,134 +348,6 @@ impl Route

    { } } -// pub struct RouteServiceBuilder { -// service: T, -// filters: Vec>, -// _t: PhantomData<(P, U1, U2)>, -// } - -// impl RouteServiceBuilder -// where -// T: NewService< -// Request = HandlerRequest, -// Response = HandlerRequest, -// Error = Error, -// InitError = (), -// >, -// { -// pub fn new>(factory: F) -> Self { -// RouteServiceBuilder { -// service: factory.into_new_service(), -// filters: Vec::new(), -// _t: PhantomData, -// } -// } - -// /// Add method match filter to the route. -// /// -// /// ```rust -// /// # extern crate actix_web; -// /// # use actix_web::*; -// /// # fn main() { -// /// App::new().resource("/path", |r| { -// /// r.route() -// /// .filter(pred::Get()) -// /// .filter(pred::Header("content-type", "text/plain")) -// /// .f(|req| HttpResponse::Ok()) -// /// }) -// /// # .finish(); -// /// # } -// /// ``` -// pub fn method(mut self, method: Method) -> Self { -// self.filters.push(Box::new(filter::Method(method))); -// self -// } - -// /// Add filter to the route. -// /// -// /// ```rust -// /// # extern crate actix_web; -// /// # use actix_web::*; -// /// # fn main() { -// /// App::new().resource("/path", |r| { -// /// r.route() -// /// .filter(pred::Get()) -// /// .filter(pred::Header("content-type", "text/plain")) -// /// .f(|req| HttpResponse::Ok()) -// /// }) -// /// # .finish(); -// /// # } -// /// ``` -// pub fn filter + 'static>(&mut self, f: F) -> &mut Self { -// self.filters.push(Box::new(f)); -// self -// } - -// pub fn map>( -// self, -// md: F, -// ) -> RouteServiceBuilder< -// impl NewService< -// Request = HandlerRequest, -// Response = HandlerRequest, -// Error = Error, -// InitError = (), -// >, -// S, -// U1, -// U2, -// > -// where -// T1: NewService< -// Request = HandlerRequest, -// Response = HandlerRequest, -// InitError = (), -// >, -// T1::Error: Into, -// { -// RouteServiceBuilder { -// service: self -// .service -// .and_then(md.into_new_service().map_err(|e| e.into())), -// filters: self.filters, -// _t: PhantomData, -// } -// } - -// pub fn to_async(self, handler: F) -> Route -// where -// F: AsyncFactory, -// P: FromRequest + 'static, -// R: IntoFuture, -// R::Item: Into, -// R::Error: Into, -// { -// Route { -// service: self -// .service -// .and_then(Extract::new(P::Config::default())) -// .then(AsyncHandle::new(handler)), -// filters: Rc::new(self.filters), -// } -// } - -// pub fn to(self, handler: F) -> Route -// where -// F: Factory + 'static, -// P: FromRequest + 'static, -// R: Responder + 'static, -// { -// Route { -// service: Box::new(RouteNewService::new( -// self.service -// .and_then(Extract::new(P::Config::default())) -// .and_then(Handle::new(handler)), -// )), -// filters: Rc::new(self.filters), -// } -// } -// } - struct RouteNewService where T: NewService, Error = (Error, ServiceFromRequest

    )>, From e50d4c5e0e7bcaaf50031ff1aaebc222910e3517 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 13:53:31 -0800 Subject: [PATCH 178/427] rename extractor module to extract, re-enable doc tests --- Cargo.toml | 1 - src/{extractor.rs => extract.rs} | 279 +++++++++++++++++-------------- src/handler.rs | 54 +----- src/lib.rs | 18 +- src/request.rs | 2 +- src/resource.rs | 3 +- src/responder.rs | 61 ++++--- src/route.rs | 18 +- src/state.rs | 2 +- src/test.rs | 5 +- 10 files changed, 215 insertions(+), 228 deletions(-) rename src/{extractor.rs => extract.rs} (83%) diff --git a/Cargo.toml b/Cargo.toml index 30d23d027..03b1794ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,6 @@ actix-router = { git = "https://github.com/actix/actix-net.git" } bytes = "0.4" derive_more = "0.14" -either = "1.5.1" encoding = "0.2" futures = "0.1" log = "0.4" diff --git a/src/extractor.rs b/src/extract.rs similarity index 83% rename from src/extractor.rs rename to src/extract.rs index 5fa9af61c..d6e533276 100644 --- a/src/extractor.rs +++ b/src/extract.rs @@ -20,65 +20,103 @@ use actix_http::error::{ UrlencodedError, }; use actix_http::http::StatusCode; -use actix_http::{HttpMessage, Response}; +use actix_http::{Extensions, HttpMessage, Response}; use actix_router::PathDeserializer; -use crate::handler::{ConfigStorage, ExtractorConfig, FromRequest}; use crate::request::HttpRequest; use crate::responder::Responder; use crate::service::ServiceFromRequest; +/// Trait implemented by types that can be extracted from request. +/// +/// Types that implement this trait can be used with `Route` handlers. +pub trait FromRequest

    : Sized { + /// The associated error which can be returned. + type Error: Into; + + /// Future that resolves to a Self + type Future: IntoFuture; + + /// Configuration for the extractor + type Config: ExtractorConfig; + + /// Convert request to a Self + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future; +} + +/// Storage for extractor configs +#[derive(Default)] +pub struct ConfigStorage { + pub(crate) storage: Option>, +} + +impl ConfigStorage { + pub fn store(&mut self, config: C) { + if self.storage.is_none() { + self.storage = Some(Rc::new(Extensions::new())); + } + if let Some(ref mut ext) = self.storage { + Rc::get_mut(ext).unwrap().insert(config); + } + } +} + +pub trait ExtractorConfig: Default + Clone + 'static { + /// Set default configuration to config storage + fn store_default(ext: &mut ConfigStorage) { + ext.store(Self::default()) + } +} + +impl ExtractorConfig for () { + fn store_default(_: &mut ConfigStorage) {} +} + #[derive(PartialEq, Eq, PartialOrd, Ord)] /// Extract typed information from the request's path. /// /// ## Example /// -/// ```rust,ignore -/// # extern crate bytes; -/// # extern crate actix_web; -/// # extern crate futures; -/// use actix_web::{http, App, Path, Result}; +/// ```rust +/// use actix_web::{web, http, App, extract::Path}; /// /// /// extract path info from "/{username}/{count}/index.html" url /// /// {username} - deserializes to a String /// /// {count} - - deserializes to a u32 -/// fn index(info: Path<(String, u32)>) -> Result { -/// Ok(format!("Welcome {}! {}", info.0, info.1)) +/// fn index(info: Path<(String, u32)>) -> String { +/// format!("Welcome {}! {}", info.0, info.1) /// } /// /// fn main() { /// let app = App::new().resource( /// "/{username}/{count}/index.html", // <- define path parameters -/// |r| r.method(http::Method::GET).with(index), -/// ); // <- use `with` extractor +/// |r| r.route(web::get().to(index)) // <- register handler with `Path` extractor +/// ); /// } /// ``` /// /// It is possible to extract path information to a specific type that /// implements `Deserialize` trait from *serde*. /// -/// ```rust,ignore -/// # extern crate bytes; -/// # extern crate actix_web; -/// # extern crate futures; +/// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Path, Result}; +/// use actix_web::{web, App, extract::Path, Error}; /// /// #[derive(Deserialize)] /// struct Info { /// username: String, /// } /// -/// /// extract path info using serde -/// fn index(info: Path) -> Result { +/// /// extract `Info` from a path using serde +/// fn index(info: Path) -> Result { /// Ok(format!("Welcome {}!", info.username)) /// } /// /// fn main() { /// let app = App::new().resource( /// "/{username}/index.html", // <- define path parameters -/// |r| r.method(http::Method::GET).with(index), -/// ); // <- use `with` extractor +/// |r| r.route(web::get().to(index)) // <- use handler with Path` extractor +/// ); /// } /// ``` pub struct Path { @@ -112,7 +150,7 @@ impl Path { } /// Extract path information from a request - pub fn extract

    (req: &ServiceFromRequest

    ) -> Result, de::value::Error> + pub fn extract(req: &HttpRequest) -> Result, de::value::Error> where T: DeserializeOwned, { @@ -158,13 +196,9 @@ impl fmt::Display for Path { /// /// ## Example /// -/// ```rust,ignore -/// # extern crate bytes; -/// # extern crate actix_web; -/// # extern crate futures; +/// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{App, Query, http}; -/// +/// use actix_web::{web, extract, App}; /// ///#[derive(Debug, Deserialize)] ///pub enum ResponseType { @@ -178,17 +212,17 @@ impl fmt::Display for Path { /// response_type: ResponseType, ///} /// -/// // use `with` extractor for query info -/// // this handler get called only if request's query contains `username` field +/// // Use `Query` extractor for query information. +/// // This handler get called only if request's query contains `username` field /// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` -/// fn index(info: Query) -> String { +/// fn index(info: extract::Query) -> String { /// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) /// } /// /// fn main() { /// let app = App::new().resource( /// "/index.html", -/// |r| r.method(http::Method::GET).with(index)); // <- use `with` extractor +/// |r| r.route(web::get().to(index))); // <- use `Query` extractor /// } /// ``` pub struct Query(T); @@ -253,21 +287,21 @@ impl fmt::Display for Query { /// /// ## Example /// -/// ```rust,ignore +/// ```rust /// # extern crate actix_web; /// #[macro_use] extern crate serde_derive; -/// use actix_web::{App, Form, Result}; +/// use actix_web::{web, App, extract::Form}; /// /// #[derive(Deserialize)] /// struct FormData { /// username: String, /// } /// -/// /// extract form data using serde -/// /// this handler get called only if content type is *x-www-form-urlencoded* +/// /// Extract form data using serde. +/// /// This handler get called only if content type is *x-www-form-urlencoded* /// /// and content of the request could be deserialized to a `FormData` struct -/// fn index(form: Form) -> Result { -/// Ok(format!("Welcome {}!", form.username)) +/// fn index(form: Form) -> String { +/// format!("Welcome {}!", form.username) /// } /// # fn main() {} /// ``` @@ -333,19 +367,18 @@ impl fmt::Display for Form { /// Form extractor configuration /// -/// ```rust,ignore -/// # extern crate actix_web; +/// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Form, Result}; +/// use actix_web::{web, extract, App, Result}; /// /// #[derive(Deserialize)] /// struct FormData { /// username: String, /// } /// -/// /// extract form data using serde. -/// /// custom configuration is used for this handler, max payload size is 4k -/// fn index(form: Form) -> Result { +/// /// Extract form data using serde. +/// /// Custom configuration is used for this handler, max payload size is 4k +/// fn index(form: extract::Form) -> Result { /// Ok(format!("Welcome {}!", form.username)) /// } /// @@ -353,11 +386,11 @@ impl fmt::Display for Form { /// let app = App::new().resource( /// "/index.html", /// |r| { -/// r.method(http::Method::GET) -/// // register form handler and change form extractor configuration -/// .with_config(index, |cfg| {cfg.0.limit(4096);}) -/// }, -/// ); +/// r.route(web::get() +/// // change `Form` extractor configuration +/// .config(extract::FormConfig::default().limit(4097)) +/// .to(index)) +/// }); /// } /// ``` #[derive(Clone)] @@ -408,10 +441,9 @@ impl Default for FormConfig { /// /// ## Example /// -/// ```rust,ignore -/// # extern crate actix_web; +/// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{App, Json, Result, http}; +/// use actix_web::{web, extract, App}; /// /// #[derive(Deserialize)] /// struct Info { @@ -419,14 +451,14 @@ impl Default for FormConfig { /// } /// /// /// deserialize `Info` from request's body -/// fn index(info: Json) -> Result { -/// Ok(format!("Welcome {}!", info.username)) +/// fn index(info: extract::Json) -> String { +/// format!("Welcome {}!", info.username) /// } /// /// fn main() { /// let app = App::new().resource( /// "/index.html", -/// |r| r.method(http::Method::POST).with(index)); // <- use `with` extractor +/// |r| r.route(web::post().to(index))); /// } /// ``` /// @@ -435,8 +467,7 @@ impl Default for FormConfig { /// to serialize into *JSON*. The type `T` must implement the `Serialize` /// trait from *serde*. /// -/// ```rust,ignore -/// # extern crate actix_web; +/// ```rust /// # #[macro_use] extern crate serde_derive; /// # use actix_web::*; /// # @@ -447,7 +478,7 @@ impl Default for FormConfig { /// /// fn index(req: HttpRequest) -> Result> { /// Ok(Json(MyObj { -/// name: req.match_info().query("name")?, +/// name: req.match_info().get("name").unwrap().to_string(), /// })) /// } /// # fn main() {} @@ -536,10 +567,9 @@ where /// Json extractor configuration /// -/// ```rust,ignore -/// # extern crate actix_web; +/// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{error, http, App, HttpResponse, Json, Result}; +/// use actix_web::{error, extract, web, App, HttpResponse, Json}; /// /// #[derive(Deserialize)] /// struct Info { @@ -547,20 +577,20 @@ where /// } /// /// /// deserialize `Info` from request's body, max payload size is 4kb -/// fn index(info: Json) -> Result { -/// Ok(format!("Welcome {}!", info.username)) +/// fn index(info: Json) -> String { +/// format!("Welcome {}!", info.username) /// } /// /// fn main() { /// let app = App::new().resource("/index.html", |r| { -/// r.method(http::Method::POST) -/// .with_config(index, |cfg| { -/// cfg.0.limit(4096) // <- change json extractor configuration -/// .error_handler(|err, req| { // <- create custom error response -/// error::InternalError::from_response( -/// err, HttpResponse::Conflict().finish()).into() -/// }); -/// }) +/// r.route(web::post().config( +/// // change json extractor configuration +/// extract::JsonConfig::default().limit(4096) +/// .error_handler(|err, req| { // <- create custom error response +/// error::InternalError::from_response( +/// err, HttpResponse::Conflict().finish()).into() +/// })) +/// .to(index)) /// }); /// } /// ``` @@ -598,7 +628,7 @@ impl Default for JsonConfig { } } -/// Request payload extractor. +/// Request binary data from a request's payload. /// /// Loads request's payload and construct Bytes instance. /// @@ -607,19 +637,18 @@ impl Default for JsonConfig { /// /// ## Example /// -/// ```rust,ignore -/// extern crate bytes; -/// # extern crate actix_web; -/// use actix_web::{http, App, Result}; +/// ```rust +/// use bytes::Bytes; +/// use actix_web::{web, App}; /// -/// /// extract text data from request -/// fn index(body: bytes::Bytes) -> Result { -/// Ok(format!("Body {:?}!", body)) +/// /// extract binary data from request +/// fn index(body: Bytes) -> String { +/// format!("Body {:?}!", body) /// } /// /// fn main() { /// let app = App::new() -/// .resource("/index.html", |r| r.method(http::Method::GET).with(index)); +/// .resource("/index.html", |r| r.route(web::get().to(index))); /// } /// ``` impl

    FromRequest

    for Bytes @@ -644,7 +673,7 @@ where } } -/// Extract text information from the request's body. +/// Extract text information from a request's body. /// /// Text extractor automatically decode body according to the request's charset. /// @@ -653,21 +682,20 @@ where /// /// ## Example /// -/// ```rust,ignore -/// # extern crate actix_web; -/// use actix_web::{http, App, Result}; +/// ```rust +/// use actix_web::{web, extract, App}; /// /// /// extract text data from request -/// fn index(body: String) -> Result { -/// Ok(format!("Body {}!", body)) +/// fn index(text: String) -> String { +/// format!("Body {}!", text) /// } /// /// fn main() { /// let app = App::new().resource("/index.html", |r| { -/// r.method(http::Method::GET) -/// .with_config(index, |cfg| { // <- register handler with extractor params -/// cfg.0.limit(4096); // <- limit size of the payload -/// }) +/// r.route( +/// web::get() +/// .config(extract::PayloadConfig::new(4096)) // <- limit size of the payload +/// .to(index)) // <- register handler with extractor params /// }); /// } /// ``` @@ -722,22 +750,23 @@ where /// /// ## Example /// -/// ```rust,ignore -/// # extern crate actix_web; -/// extern crate rand; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Result, HttpRequest, Error, FromRequest}; +/// ```rust +/// # #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App, Error, FromRequest, ServiceFromRequest}; /// use actix_web::error::ErrorBadRequest; +/// use rand; /// /// #[derive(Debug, Deserialize)] -/// struct Thing { name: String } +/// struct Thing { +/// name: String +/// } /// -/// impl FromRequest for Thing { +/// impl

    FromRequest

    for Thing { +/// type Error = Error; +/// type Future = Result; /// type Config = (); -/// type Result = Result; /// -/// #[inline] -/// fn from_request(req: &HttpRequest, _cfg: &Self::Config) -> Self::Result { +/// fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { /// if rand::random() { /// Ok(Thing { name: "thingy".into() }) /// } else { @@ -747,18 +776,18 @@ where /// } /// } /// -/// /// extract text data from request -/// fn index(supplied_thing: Option) -> Result { +/// /// extract `Thing` from request +/// fn index(supplied_thing: Option) -> String { /// match supplied_thing { /// // Puns not intended -/// Some(thing) => Ok(format!("Got something: {:?}", thing)), -/// None => Ok(format!("No thing!")) +/// Some(thing) => format!("Got something: {:?}", thing), +/// None => format!("No thing!") /// } /// } /// /// fn main() { /// let app = App::new().resource("/users/:first", |r| { -/// r.method(http::Method::POST).with(index) +/// r.route(web::post().to(index)) /// }); /// } /// ``` @@ -773,7 +802,7 @@ where #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - Box::new(T::from_request(req).then(|r| match r { + Box::new(T::from_request(req).into_future().then(|r| match r { Ok(v) => future::ok(Some(v)), Err(_) => future::ok(None), })) @@ -782,46 +811,46 @@ where /// Optionally extract a field from the request or extract the Error if unsuccessful /// -/// If the FromRequest for T fails, inject Err into handler rather than returning an error response +/// If the `FromRequest` for T fails, inject Err into handler rather than returning an error response /// /// ## Example /// -/// ```rust,ignore -/// # extern crate actix_web; -/// extern crate rand; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Result, HttpRequest, Error, FromRequest}; +/// ```rust +/// # #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App, Result, Error, FromRequest, ServiceFromRequest}; /// use actix_web::error::ErrorBadRequest; +/// use rand; /// /// #[derive(Debug, Deserialize)] -/// struct Thing { name: String } +/// struct Thing { +/// name: String +/// } /// -/// impl FromRequest for Thing { +/// impl

    FromRequest

    for Thing { +/// type Error = Error; +/// type Future = Result; /// type Config = (); -/// type Result = Result; /// -/// #[inline] -/// fn from_request(req: &Request, _cfg: &Self::Config) -> Self::Result { +/// fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { /// if rand::random() { /// Ok(Thing { name: "thingy".into() }) /// } else { /// Err(ErrorBadRequest("no luck")) /// } -/// /// } /// } /// -/// /// extract text data from request -/// fn index(supplied_thing: Result) -> Result { +/// /// extract `Thing` from request +/// fn index(supplied_thing: Result) -> String { /// match supplied_thing { -/// Ok(thing) => Ok(format!("Got thing: {:?}", thing)), -/// Err(e) => Ok(format!("Error extracting thing: {}", e)) +/// Ok(thing) => format!("Got thing: {:?}", thing), +/// Err(e) => format!("Error extracting thing: {}", e) /// } /// } /// /// fn main() { /// let app = App::new().resource("/users/:first", |r| { -/// r.method(http::Method::POST).with(index) +/// r.route(web::post().to(index)) /// }); /// } /// ``` @@ -837,7 +866,7 @@ where #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - Box::new(T::from_request(req).then(|res| match res { + Box::new(T::from_request(req).into_future().then(|res| match res { Ok(v) => ok(Ok(v)), Err(e) => ok(Err(e)), })) @@ -924,7 +953,7 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { $fut_type { items: <($(Option<$T>,)+)>::default(), - futs: ($($T::from_request(req),)+), + futs: ($($T::from_request(req).into_future(),)+), } } } @@ -932,7 +961,7 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { #[doc(hidden)] pub struct $fut_type),+> { items: ($(Option<$T>,)+), - futs: ($($T::Future,)+), + futs: ($(<$T::Future as futures::IntoFuture>::Future,)+), } impl),+> Future for $fut_type diff --git a/src/handler.rs b/src/handler.rs index 313422ed5..98a36c562 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -7,55 +7,11 @@ use actix_service::{NewService, Service, Void}; use futures::future::{ok, FutureResult}; use futures::{try_ready, Async, Future, IntoFuture, Poll}; +use crate::extract::FromRequest; use crate::request::HttpRequest; use crate::responder::Responder; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; -/// Trait implemented by types that can be extracted from request. -/// -/// Types that implement this trait can be used with `Route` handlers. -pub trait FromRequest

    : Sized { - /// The associated error which can be returned. - type Error: Into; - - /// Future that resolves to a Self - type Future: Future; - - /// Configuration for the extractor - type Config: ExtractorConfig; - - /// Convert request to a Self - fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future; -} - -/// Storage for extractor configs -#[derive(Default)] -pub struct ConfigStorage { - pub(crate) storage: Option>, -} - -impl ConfigStorage { - pub fn store(&mut self, config: C) { - if self.storage.is_none() { - self.storage = Some(Rc::new(Extensions::new())); - } - if let Some(ref mut ext) = self.storage { - Rc::get_mut(ext).unwrap().insert(config); - } - } -} - -pub trait ExtractorConfig: Default + Clone + 'static { - /// Set default configuration to config storage - fn store_default(ext: &mut ConfigStorage) { - ext.store(Self::default()) - } -} - -impl ExtractorConfig for () { - fn store_default(_: &mut ConfigStorage) {} -} - /// Handler converter factory pub trait Factory: Clone where @@ -134,14 +90,14 @@ where type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = Void; - type Future = HandleServiceResponse; + type Future = HandleServiceResponse<::Future>; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } fn call(&mut self, (param, req): (T, HttpRequest)) -> Self::Future { - let fut = self.hnd.call(param).respond_to(&req); + let fut = self.hnd.call(param).respond_to(&req).into_future(); HandleServiceResponse { fut, req: Some(req), @@ -368,7 +324,7 @@ impl> Service for ExtractService { fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { let mut req = ServiceFromRequest::new(req, self.config.clone()); ExtractResponse { - fut: T::from_request(&mut req), + fut: T::from_request(&mut req).into_future(), req: Some(req), } } @@ -376,7 +332,7 @@ impl> Service for ExtractService { pub struct ExtractResponse> { req: Option>, - fut: T::Future, + fut: ::Future, } impl> Future for ExtractResponse { diff --git a/src/lib.rs b/src/lib.rs index e876a7ea4..f61bf0ace 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #![allow(clippy::type_complexity)] mod app; -pub mod extractor; +pub mod extract; pub mod handler; // mod info; pub mod blocking; @@ -17,23 +17,23 @@ pub mod test; // re-export for convenience pub use actix_http::Response as HttpResponse; -pub use actix_http::{http, Error, HttpMessage, ResponseError}; +pub use actix_http::{error, http, Error, HttpMessage, ResponseError, Result}; pub use crate::app::App; -pub use crate::extractor::{Form, Json, Path, PayloadConfig, Query}; -pub use crate::handler::FromRequest; +pub use crate::extract::{FromRequest, Json}; pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::{Either, Responder}; pub use crate::route::Route; -pub use crate::service::{ServiceRequest, ServiceResponse}; +pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; pub use crate::state::State; pub mod web { use actix_http::{http::Method, Error, Response}; use futures::IntoFuture; - use crate::handler::{AsyncFactory, Factory, FromRequest}; + use crate::extract::FromRequest; + use crate::handler::{AsyncFactory, Factory}; use crate::responder::Responder; use crate::Route; @@ -107,9 +107,3 @@ pub mod web { Route::new().to_async(handler) } } - -pub mod dev { - pub use crate::app::AppRouter; - pub use crate::handler::{AsyncFactory, Extract, Factory, Handle}; - // pub use crate::info::ConnectionInfo; -} diff --git a/src/request.rs b/src/request.rs index a7c84b534..48a2dd833 100644 --- a/src/request.rs +++ b/src/request.rs @@ -8,7 +8,7 @@ use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; use actix_router::{Path, Url}; use futures::future::{ok, FutureResult}; -use crate::handler::FromRequest; +use crate::extract::FromRequest; use crate::service::ServiceFromRequest; #[derive(Clone)] diff --git a/src/resource.rs b/src/resource.rs index 98c2dc114..f05e998fa 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -9,7 +9,8 @@ use actix_service::{ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; -use crate::handler::{AsyncFactory, Factory, FromRequest}; +use crate::extract::FromRequest; +use crate::handler::{AsyncFactory, Factory}; use crate::responder::Responder; use crate::route::{CreateRouteService, Route, RouteService}; use crate::service::{ServiceRequest, ServiceResponse}; diff --git a/src/responder.rs b/src/responder.rs index b3ec7ec73..22588a9cd 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,7 +1,7 @@ use actix_http::{dev::ResponseBuilder, http::StatusCode, Error, Response}; use bytes::{Bytes, BytesMut}; use futures::future::{err, ok, Either as EitherFuture, FutureResult}; -use futures::{Future, Poll}; +use futures::{Future, IntoFuture, Poll}; use crate::request::HttpRequest; @@ -13,7 +13,7 @@ pub trait Responder { type Error: Into; /// The future response value. - type Future: Future; + type Future: IntoFuture; /// Convert itself to `AsyncResult` or `Error`. fn respond_to(self, req: &HttpRequest) -> Self::Future; @@ -34,11 +34,14 @@ where T: Responder, { type Error = T::Error; - type Future = EitherFuture>; + type Future = EitherFuture< + ::Future, + FutureResult, + >; fn respond_to(self, req: &HttpRequest) -> Self::Future { match self { - Some(t) => EitherFuture::A(t.respond_to(req)), + Some(t) => EitherFuture::A(t.respond_to(req).into_future()), None => EitherFuture::B(ok(Response::build(StatusCode::NOT_FOUND).finish())), } } @@ -50,11 +53,16 @@ where E: Into, { type Error = Error; - type Future = EitherFuture, FutureResult>; + type Future = EitherFuture< + ResponseFuture<::Future>, + FutureResult, + >; fn respond_to(self, req: &HttpRequest) -> Self::Future { match self { - Ok(val) => EitherFuture::A(ResponseFuture::new(val.respond_to(req))), + Ok(val) => { + EitherFuture::A(ResponseFuture::new(val.respond_to(req).into_future())) + } Err(e) => EitherFuture::B(err(e.into())), } } @@ -147,34 +155,36 @@ impl Responder for BytesMut { /// Combines two different responder types into a single type /// -/// ```rust,ignore -/// # extern crate actix_web; -/// # extern crate futures; -/// # use futures::future::Future; -/// use actix_web::{AsyncResponder, Either, Error, Request, Response}; -/// use futures::future::result; +/// ```rust +/// # use futures::future::{ok, Future}; +/// use actix_web::{Either, Error, HttpResponse}; /// /// type RegisterResult = -/// Either>>; +/// Either>>; /// -/// fn index(req: Request) -> RegisterResult { +/// fn index() -> RegisterResult { /// if is_a_variant() { -/// // <- choose variant A -/// Either::A(Response::BadRequest().body("Bad data")) +/// // <- choose left variant +/// Either::A(HttpResponse::BadRequest().body("Bad data")) /// } else { /// Either::B( -/// // <- variant B -/// result(Ok(Response::Ok() +/// // <- Right variant +/// Box::new(ok(HttpResponse::Ok() /// .content_type("text/html") /// .body("Hello!"))) -/// .responder(), /// ) /// } /// } /// # fn is_a_variant() -> bool { true } /// # fn main() {} /// ``` -pub type Either = either::Either; +#[derive(Debug, PartialEq)] +pub enum Either { + /// First branch of the type + A(A), + /// Second branch of the type + B(B), +} impl Responder for Either where @@ -182,12 +192,15 @@ where B: Responder, { type Error = Error; - type Future = EitherResponder; + type Future = EitherResponder< + ::Future, + ::Future, + >; fn respond_to(self, req: &HttpRequest) -> Self::Future { match self { - either::Either::Left(a) => EitherResponder::A(a.respond_to(req)), - either::Either::Right(b) => EitherResponder::B(b.respond_to(req)), + Either::A(a) => EitherResponder::A(a.respond_to(req).into_future()), + Either::B(b) => EitherResponder::B(b.respond_to(req).into_future()), } } } @@ -234,7 +247,7 @@ where let req = req.clone(); Box::new( self.map_err(|e| e.into()) - .and_then(move |r| ResponseFuture(r.respond_to(&req))), + .and_then(move |r| ResponseFuture(r.respond_to(&req).into_future())), ) } } diff --git a/src/route.rs b/src/route.rs index 16a4fc5ba..72abeb324 100644 --- a/src/route.rs +++ b/src/route.rs @@ -5,11 +5,9 @@ use actix_http::{http::Method, Error, Extensions, Response}; use actix_service::{NewService, Service}; use futures::{Async, Future, IntoFuture, Poll}; +use crate::extract::{ConfigStorage, ExtractorConfig, FromRequest}; use crate::guard::{self, Guard}; -use crate::handler::{ - AsyncFactory, AsyncHandle, ConfigStorage, Extract, ExtractorConfig, Factory, - FromRequest, Handle, -}; +use crate::handler::{AsyncFactory, AsyncHandle, Extract, Factory, Handle}; use crate::responder::Responder; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; use crate::HttpResponse; @@ -219,7 +217,7 @@ impl Route

    { /// /// ```rust /// #[macro_use] extern crate serde_derive; - /// use actix_web::{web, http, App, Path}; + /// use actix_web::{web, http, App, extract::Path}; /// /// #[derive(Deserialize)] /// struct Info { @@ -244,7 +242,7 @@ impl Route

    { /// ```rust /// # use std::collections::HashMap; /// # use serde_derive::Deserialize; - /// use actix_web::{web, http, App, Json, Path, Query}; + /// use actix_web::{web, App, Json, extract::Path, extract::Query}; /// /// #[derive(Deserialize)] /// struct Info { @@ -259,7 +257,7 @@ impl Route

    { /// fn main() { /// let app = App::new().resource( /// "/{username}/index.html", // <- define path parameters - /// |r| r.route(web::method(http::Method::GET).to(index)), + /// |r| r.route(web::get().to(index)), /// ); /// } /// ``` @@ -283,7 +281,7 @@ impl Route

    { /// ```rust /// # use futures::future::ok; /// #[macro_use] extern crate serde_derive; - /// use actix_web::{web, http, App, Error, Path}; + /// use actix_web::{web, App, Error, extract::Path}; /// use futures::Future; /// /// #[derive(Deserialize)] @@ -323,7 +321,7 @@ impl Route

    { /// for specific route. /// /// ```rust - /// use actix_web::{web, extractor, App}; + /// use actix_web::{web, extract, App}; /// /// /// extract text data from request /// fn index(body: String) -> String { @@ -335,7 +333,7 @@ impl Route

    { /// r.route( /// web::get() /// // limit size of the payload - /// .config(extractor::PayloadConfig::new(4096)) + /// .config(extract::PayloadConfig::new(4096)) /// // register handler /// .to(index) /// ) diff --git a/src/state.rs b/src/state.rs index 4a450245a..168a8c899 100644 --- a/src/state.rs +++ b/src/state.rs @@ -6,7 +6,7 @@ use actix_http::Extensions; use futures::future::{err, ok, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; -use crate::handler::FromRequest; +use crate::extract::FromRequest; use crate::service::ServiceFromRequest; /// Application state factory diff --git a/src/test.rs b/src/test.rs index 4899cfe41..b4447f8b3 100644 --- a/src/test.rs +++ b/src/test.rs @@ -14,13 +14,10 @@ use crate::service::{ServiceFromRequest, ServiceRequest}; /// Test `Request` builder /// /// ```rust,ignore -/// # extern crate http; -/// # extern crate actix_web; -/// # use http::{header, StatusCode}; /// # use actix_web::*; /// use actix_web::test::TestRequest; /// -/// fn index(req: &HttpRequest) -> HttpResponse { +/// fn index(req: HttpRequest) -> HttpResponse { /// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { /// HttpResponse::Ok().into() /// } else { From 360082f99ffed06d19388905bf3023b81a6c80aa Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 14:45:56 -0800 Subject: [PATCH 179/427] update api docs --- src/app.rs | 4 +- src/extract.rs | 168 ++++++++++++++++++++++++++++++++++++++++-------- src/lib.rs | 11 +++- src/request.rs | 27 ++++++-- src/resource.rs | 46 +++++++++++-- src/state.rs | 48 +++++++------- 6 files changed, 237 insertions(+), 67 deletions(-) diff --git a/src/app.rs b/src/app.rs index c9c23d9cb..34a663ae7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -245,8 +245,8 @@ where } } -/// Structure that follows the builder pattern for building application -/// instances. +/// Application router builder - Structure that follows the builder pattern +/// for building application instances. pub struct AppRouter { chain: C, services: Vec<(ResourceDef, HttpNewService

    )>, diff --git a/src/extract.rs b/src/extract.rs index d6e533276..c0af6016d 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -165,17 +165,63 @@ impl From for Path { } } +/// Extract typed information from the request's path. +/// +/// ## Example +/// +/// ```rust +/// use actix_web::{web, http, App, extract::Path}; +/// +/// /// extract path info from "/{username}/{count}/index.html" url +/// /// {username} - deserializes to a String +/// /// {count} - - deserializes to a u32 +/// fn index(info: Path<(String, u32)>) -> String { +/// format!("Welcome {}! {}", info.0, info.1) +/// } +/// +/// fn main() { +/// let app = App::new().resource( +/// "/{username}/{count}/index.html", // <- define path parameters +/// |r| r.route(web::get().to(index)) // <- register handler with `Path` extractor +/// ); +/// } +/// ``` +/// +/// It is possible to extract path information to a specific type that +/// implements `Deserialize` trait from *serde*. +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App, extract::Path, Error}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// extract `Info` from a path using serde +/// fn index(info: Path) -> Result { +/// Ok(format!("Welcome {}!", info.username)) +/// } +/// +/// fn main() { +/// let app = App::new().resource( +/// "/{username}/index.html", // <- define path parameters +/// |r| r.route(web::get().to(index)) // <- use handler with Path` extractor +/// ); +/// } +/// ``` impl FromRequest

    for Path where T: DeserializeOwned, { type Error = Error; - type Future = FutureResult; + type Future = Result; type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - Self::extract(req).map_err(ErrorNotFound).into_future() + Self::extract(req).map_err(ErrorNotFound) } } @@ -200,17 +246,17 @@ impl fmt::Display for Path { /// #[macro_use] extern crate serde_derive; /// use actix_web::{web, extract, App}; /// -///#[derive(Debug, Deserialize)] -///pub enum ResponseType { +/// #[derive(Debug, Deserialize)] +/// pub enum ResponseType { /// Token, /// Code -///} +/// } /// -///#[derive(Deserialize)] -///pub struct AuthRequest { +/// #[derive(Deserialize)] +/// pub struct AuthRequest { /// id: u64, /// response_type: ResponseType, -///} +/// } /// /// // Use `Query` extractor for query information. /// // This handler get called only if request's query contains `username` field @@ -248,19 +294,52 @@ impl Query { } } +/// Extract typed information from from the request's query. +/// +/// ## Example +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, extract, App}; +/// +/// #[derive(Debug, Deserialize)] +/// pub enum ResponseType { +/// Token, +/// Code +/// } +/// +/// #[derive(Deserialize)] +/// pub struct AuthRequest { +/// id: u64, +/// response_type: ResponseType, +/// } +/// +/// // Use `Query` extractor for query information. +/// // This handler get called only if request's query contains `username` field +/// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` +/// fn index(info: extract::Query) -> String { +/// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) +/// } +/// +/// fn main() { +/// let app = App::new().resource( +/// "/index.html", +/// |r| r.route(web::get().to(index))); // <- use `Query` extractor +/// } +/// ``` impl FromRequest

    for Query where T: de::DeserializeOwned, { type Error = Error; - type Future = FutureResult; + type Future = Result; type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { serde_urlencoded::from_str::(req.query_string()) - .map(|val| ok(Query(val))) - .unwrap_or_else(|e| err(e.into())) + .map(|val| Ok(Query(val))) + .unwrap_or_else(|e| Err(e.into())) } } @@ -282,7 +361,7 @@ impl fmt::Display for Query { /// To extract typed information from request's body, the type `T` must /// implement the `Deserialize` trait from *serde*. /// -/// [**FormConfig**](dev/struct.FormConfig.html) allows to configure extraction +/// [**FormConfig**](struct.FormConfig.html) allows to configure extraction /// process. /// /// ## Example @@ -436,7 +515,7 @@ impl Default for FormConfig { /// To extract typed information from request's body, the type `T` must /// implement the `Deserialize` trait from *serde*. /// -/// [**JsonConfig**](dev/struct.JsonConfig.html) allows to configure extraction +/// [**JsonConfig**](struct.JsonConfig.html) allows to configure extraction /// process. /// /// ## Example @@ -526,20 +605,51 @@ where impl Responder for Json { type Error = Error; - type Future = FutureResult; + type Future = Result; fn respond_to(self, _: &HttpRequest) -> Self::Future { let body = match serde_json::to_string(&self.0) { Ok(body) => body, - Err(e) => return err(e.into()), + Err(e) => return Err(e.into()), }; - ok(Response::build(StatusCode::OK) + Ok(Response::build(StatusCode::OK) .content_type("application/json") .body(body)) } } +/// Json extractor. Allow to extract typed information from request's +/// payload. +/// +/// To extract typed information from request's body, the type `T` must +/// implement the `Deserialize` trait from *serde*. +/// +/// [**JsonConfig**](struct.JsonConfig.html) allows to configure extraction +/// process. +/// +/// ## Example +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, extract, App}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// deserialize `Info` from request's body +/// fn index(info: extract::Json) -> String { +/// format!("Welcome {}!", info.username) +/// } +/// +/// fn main() { +/// let app = App::new().resource( +/// "/index.html", +/// |r| r.route(web::post().to(index))); +/// } +/// ``` impl FromRequest

    for Json where T: DeserializeOwned + 'static, @@ -632,7 +742,7 @@ impl Default for JsonConfig { /// /// Loads request's payload and construct Bytes instance. /// -/// [**PayloadConfig**](dev/struct.PayloadConfig.html) allows to configure +/// [**PayloadConfig**](struct.PayloadConfig.html) allows to configure /// extraction process. /// /// ## Example @@ -677,7 +787,7 @@ where /// /// Text extractor automatically decode body according to the request's charset. /// -/// [**PayloadConfig**](dev/struct.PayloadConfig.html) allows to configure +/// [**PayloadConfig**](struct.PayloadConfig.html) allows to configure /// extraction process. /// /// ## Example @@ -931,6 +1041,17 @@ impl Default for PayloadConfig { } } +#[doc(hidden)] +impl

    FromRequest

    for () { + type Error = Error; + type Future = FutureResult<(), Error>; + type Config = (); + + fn from_request(_req: &mut ServiceFromRequest

    ) -> Self::Future { + ok(()) + } +} + macro_rules! tuple_config ({ $($T:ident),+} => { impl<$($T,)+> ExtractorConfig for ($($T,)+) where $($T: ExtractorConfig + Clone,)+ @@ -944,6 +1065,7 @@ macro_rules! tuple_config ({ $($T:ident),+} => { macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { /// FromRequest implementation for tuple + #[doc(hidden)] impl + 'static),+> FromRequest

    for ($($T,)+) { type Error = Error; @@ -995,16 +1117,6 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { } }); -impl

    FromRequest

    for () { - type Error = Error; - type Future = FutureResult<(), Error>; - type Config = (); - - fn from_request(_req: &mut ServiceFromRequest

    ) -> Self::Future { - ok(()) - } -} - #[rustfmt::skip] mod m { use super::*; diff --git a/src/lib.rs b/src/lib.rs index f61bf0ace..37fd75912 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ mod app; pub mod extract; -pub mod handler; +mod handler; // mod info; pub mod blocking; pub mod guard; @@ -19,7 +19,7 @@ pub mod test; pub use actix_http::Response as HttpResponse; pub use actix_http::{error, http, Error, HttpMessage, ResponseError, Result}; -pub use crate::app::App; +pub use crate::app::{App, AppRouter}; pub use crate::extract::{FromRequest, Json}; pub use crate::request::HttpRequest; pub use crate::resource::Resource; @@ -37,30 +37,37 @@ pub mod web { use crate::responder::Responder; use crate::Route; + /// Create **route** without configuration. pub fn route() -> Route

    { Route::new() } + /// Create **route** with `GET` method guard. pub fn get() -> Route

    { Route::get() } + /// Create **route** with `POST` method guard. pub fn post() -> Route

    { Route::post() } + /// Create **route** with `PUT` method guard. pub fn put() -> Route

    { Route::put() } + /// Create **route** with `DELETE` method guard. pub fn delete() -> Route

    { Route::delete() } + /// Create **route** with `HEAD` method guard. pub fn head() -> Route

    { Route::new().method(Method::HEAD) } + /// Create **route** and add method guard. pub fn method(method: Method) -> Route

    { Route::new().method(method) } diff --git a/src/request.rs b/src/request.rs index 48a2dd833..d90627f52 100644 --- a/src/request.rs +++ b/src/request.rs @@ -6,12 +6,12 @@ use std::rc::Rc; use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; use actix_router::{Path, Url}; -use futures::future::{ok, FutureResult}; use crate::extract::FromRequest; use crate::service::ServiceFromRequest; #[derive(Clone)] +/// An HTTP Request pub struct HttpRequest { pub(crate) head: Message, pub(crate) path: Path, @@ -20,7 +20,7 @@ pub struct HttpRequest { impl HttpRequest { #[inline] - pub fn new( + pub(crate) fn new( head: Message, path: Path, extensions: Rc, @@ -140,14 +140,33 @@ impl HttpMessage for HttpRequest { } } +/// It is possible to get `HttpRequest` as an extractor handler parameter +/// +/// ## Example +/// +/// ```rust +/// # #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App, HttpRequest}; +/// +/// /// extract `Thing` from request +/// fn index(req: HttpRequest) -> String { +/// format!("Got thing: {:?}", req) +/// } +/// +/// fn main() { +/// let app = App::new().resource("/users/:first", |r| { +/// r.route(web::get().to(index)) +/// }); +/// } +/// ``` impl

    FromRequest

    for HttpRequest { type Error = Error; - type Future = FutureResult; + type Future = Result; type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - ok(req.clone()) + Ok(req.clone()) } } diff --git a/src/resource.rs b/src/resource.rs index f05e998fa..342d801d4 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -18,10 +18,24 @@ use crate::service::{ServiceRequest, ServiceResponse}; type HttpService

    = BoxedService, ServiceResponse, ()>; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; -/// Resource route definition +/// *Resource* is an entry in route table which corresponds to requested URL. /// -/// Route uses builder-like pattern for configuration. -/// If handler is not explicitly set, default *404 Not Found* handler is used. +/// Resource in turn has at least one route. +/// Route consists of an handlers objects and list of guards +/// (objects that implement `Guard` trait). +/// Resources and rouets uses builder-like pattern for configuration. +/// During request handling, resource object iterate through all routes +/// and check guards for specific route, if request matches all +/// guards, route considered matched and route handler get called. +/// +/// ```rust +/// use actix_web::{web, App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new() +/// .resource( +/// "/", |r| r.route(web::get().to(|| HttpResponse::Ok()))); +/// } pub struct Resource> { routes: Vec>, endpoint: T, @@ -58,8 +72,6 @@ where >, { /// Register a new route. - /// *Route* is used for route configuration, i.e. adding guards, - /// setting up handler. /// /// ```rust /// use actix_web::{web, guard, App, HttpResponse}; @@ -74,12 +86,31 @@ where /// }); /// } /// ``` + /// + /// Multiple routes could be added to a resource. + /// + /// ```rust + /// use actix_web::{web, guard, App, HttpResponse}; + /// + /// fn main() { + /// let app = App::new() + /// .resource("/container/", |r| { + /// r.route(web::get().to(get_handler)) + /// .route(web::post().to(post_handler)) + /// .route(web::delete().to(delete_handler)) + /// }); + /// } + /// # fn get_handler() {} + /// # fn post_handler() {} + /// # fn delete_handler() {} + /// ``` pub fn route(mut self, route: Route

    ) -> Self { self.routes.push(route.finish()); self } - /// Register a new route and add handler. + /// Register a new route and add handler. This route get called for all + /// requests. /// /// ```rust /// use actix_web::*; @@ -148,7 +179,8 @@ where /// Register a resource middleware /// /// This is similar to `App's` middlewares, but - /// middlewares get invoked on resource level. + /// middleware is not allowed to change response type (i.e modify response's body). + /// Middleware get invoked on resource level. pub fn middleware( self, mw: F, diff --git a/src/state.rs b/src/state.rs index 168a8c899..d4e4c894e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -3,7 +3,6 @@ use std::rc::Rc; use actix_http::error::{Error, ErrorInternalServerError}; use actix_http::Extensions; -use futures::future::{err, ok, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; use crate::extract::FromRequest; @@ -19,60 +18,61 @@ pub(crate) trait StateFactoryResult { } /// Application state -pub struct State(Rc); +pub struct State(Rc); -impl State { - pub fn new(state: S) -> State { +impl State { + pub(crate) fn new(state: T) -> State { State(Rc::new(state)) } - pub fn get_ref(&self) -> &S { + /// Get referecnce to inner state type. + pub fn get_ref(&self) -> &T { self.0.as_ref() } } -impl Deref for State { - type Target = S; +impl Deref for State { + type Target = T; - fn deref(&self) -> &S { + fn deref(&self) -> &T { self.0.as_ref() } } -impl Clone for State { - fn clone(&self) -> State { +impl Clone for State { + fn clone(&self) -> State { State(self.0.clone()) } } -impl FromRequest

    for State { +impl FromRequest

    for State { type Error = Error; - type Future = FutureResult; + type Future = Result; type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - if let Some(st) = req.app_extensions().get::>() { - ok(st.clone()) + if let Some(st) = req.app_extensions().get::>() { + Ok(st.clone()) } else { - err(ErrorInternalServerError( - "State is not configured, use App::state()", + Err(ErrorInternalServerError( + "State is not configured, to configure use App::state()", )) } } } -impl StateFactory for State { +impl StateFactory for State { fn construct(&self) -> Box { Box::new(StateFut { st: self.clone() }) } } -struct StateFut { - st: State, +struct StateFut { + st: State, } -impl StateFactoryResult for StateFut { +impl StateFactoryResult for StateFut { fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()> { extensions.insert(self.st.clone()); Ok(Async::Ready(())) @@ -92,17 +92,17 @@ where } } -struct StateFactoryFut +struct StateFactoryFut where - F: Future, + F: Future, F::Error: std::fmt::Debug, { fut: F, } -impl StateFactoryResult for StateFactoryFut +impl StateFactoryResult for StateFactoryFut where - F: Future, + F: Future, F::Error: std::fmt::Debug, { fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()> { From 8502c32a3c576463e2b24d47244d543210c86d74 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 15:32:47 -0800 Subject: [PATCH 180/427] re-enable extractor tests --- src/extract.rs | 411 +++++++++++++++++++++++-------------------------- src/service.rs | 5 + src/test.rs | 6 + 3 files changed, 206 insertions(+), 216 deletions(-) diff --git a/src/extract.rs b/src/extract.rs index c0af6016d..3b5c7e742 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -1044,11 +1044,11 @@ impl Default for PayloadConfig { #[doc(hidden)] impl

    FromRequest

    for () { type Error = Error; - type Future = FutureResult<(), Error>; + type Future = Result<(), Error>; type Config = (); fn from_request(_req: &mut ServiceFromRequest

    ) -> Self::Future { - ok(()) + Ok(()) } } @@ -1147,6 +1147,7 @@ tuple_from_req!(TupleFromRequest10, (0, A), (1, B), (2, C), (3, D), (4, E), (5, #[cfg(test)] mod tests { use actix_http::http::header; + use actix_router::ResourceDef; use bytes::Bytes; use serde_derive::Deserialize; @@ -1194,218 +1195,196 @@ mod tests { let s = rt.block_on(Form::::from_request(&mut req)).unwrap(); assert_eq!(s.hello, "world"); } + + #[test] + fn test_option() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let mut req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .config(FormConfig::default().limit(4096)) + .to_from(); + + let r = rt + .block_on(Option::>::from_request(&mut req)) + .unwrap(); + assert_eq!(r, None); + + let mut req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .header(header::CONTENT_LENGTH, "9") + .set_payload(Bytes::from_static(b"hello=world")) + .to_from(); + + let r = rt + .block_on(Option::>::from_request(&mut req)) + .unwrap(); + assert_eq!( + r, + Some(Form(Info { + hello: "world".into() + })) + ); + + let mut req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .header(header::CONTENT_LENGTH, "9") + .set_payload(Bytes::from_static(b"bye=world")) + .to_from(); + + let r = rt + .block_on(Option::>::from_request(&mut req)) + .unwrap(); + assert_eq!(r, None); + } + + #[test] + fn test_result() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let mut req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .header(header::CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .to_from(); + + let r = rt + .block_on(Result::, Error>::from_request(&mut req)) + .unwrap() + .unwrap(); + assert_eq!( + r, + Form(Info { + hello: "world".into() + }) + ); + + let mut req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .header(header::CONTENT_LENGTH, "9") + .set_payload(Bytes::from_static(b"bye=world")) + .to_from(); + + let r = rt + .block_on(Result::, Error>::from_request(&mut req)) + .unwrap(); + assert!(r.is_err()); + } + + #[test] + fn test_payload_config() { + let req = TestRequest::default().to_from(); + let cfg = PayloadConfig::default().mimetype(mime::APPLICATION_JSON); + assert!(cfg.check_mimetype(&req).is_err()); + + let req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .to_from(); + assert!(cfg.check_mimetype(&req).is_err()); + + let req = + TestRequest::with_header(header::CONTENT_TYPE, "application/json").to_from(); + assert!(cfg.check_mimetype(&req).is_ok()); + } + + #[derive(Deserialize)] + struct MyStruct { + key: String, + value: String, + } + + #[derive(Deserialize)] + struct Id { + id: String, + } + + #[derive(Deserialize)] + struct Test2 { + key: String, + value: u32, + } + + #[test] + fn test_request_extract() { + let mut req = TestRequest::with_uri("/name/user1/?id=test").to_from(); + + let resource = ResourceDef::new("/{key}/{value}/"); + resource.match_path(req.match_info_mut()); + + let s = Path::::from_request(&mut req).unwrap(); + assert_eq!(s.key, "name"); + assert_eq!(s.value, "user1"); + + let s = Path::<(String, String)>::from_request(&mut req).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, "user1"); + + let s = Query::::from_request(&mut req).unwrap(); + assert_eq!(s.id, "test"); + + let mut req = TestRequest::with_uri("/name/32/").to_from(); + let resource = ResourceDef::new("/{key}/{value}/"); + resource.match_path(req.match_info_mut()); + + let s = Path::::from_request(&mut req).unwrap(); + assert_eq!(s.as_ref().key, "name"); + assert_eq!(s.value, 32); + + let s = Path::<(String, u8)>::from_request(&mut req).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, 32); + + let res = Path::>::from_request(&mut req).unwrap(); + assert_eq!(res[0], "name".to_owned()); + assert_eq!(res[1], "32".to_owned()); + } + + #[test] + fn test_extract_path_single() { + let resource = ResourceDef::new("/{value}/"); + + let mut req = TestRequest::with_uri("/32/").to_from(); + resource.match_path(req.match_info_mut()); + + assert_eq!(*Path::::from_request(&mut req).unwrap(), 32); + } + + #[test] + fn test_tuple_extract() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let resource = ResourceDef::new("/{key}/{value}/"); + + let mut req = TestRequest::with_uri("/name/user1/?id=test").to_from(); + resource.match_path(req.match_info_mut()); + + let res = rt + .block_on(<(Path<(String, String)>,)>::from_request(&mut req)) + .unwrap(); + assert_eq!((res.0).0, "name"); + assert_eq!((res.0).1, "user1"); + + let res = rt + .block_on( + <(Path<(String, String)>, Path<(String, String)>)>::from_request( + &mut req, + ), + ) + .unwrap(); + assert_eq!((res.0).0, "name"); + assert_eq!((res.0).1, "user1"); + assert_eq!((res.1).0, "name"); + assert_eq!((res.1).1, "user1"); + + let () = <()>::from_request(&mut req).unwrap(); + } } - -// #[test] -// fn test_option() { -// let req = TestRequest::with_header( -// header::CONTENT_TYPE, -// "application/x-www-form-urlencoded", -// ) -// .finish(); - -// let mut cfg = FormConfig::default(); -// cfg.limit(4096); - -// match Option::>::from_request(&req, &cfg) -// .poll() -// .unwrap() -// { -// Async::Ready(r) => assert_eq!(r, None), -// _ => unreachable!(), -// } - -// let req = TestRequest::with_header( -// header::CONTENT_TYPE, -// "application/x-www-form-urlencoded", -// ) -// .header(header::CONTENT_LENGTH, "9") -// .set_payload(Bytes::from_static(b"hello=world")) -// .finish(); - -// match Option::>::from_request(&req, &cfg) -// .poll() -// .unwrap() -// { -// Async::Ready(r) => assert_eq!( -// r, -// Some(Form(Info { -// hello: "world".into() -// })) -// ), -// _ => unreachable!(), -// } - -// let req = TestRequest::with_header( -// header::CONTENT_TYPE, -// "application/x-www-form-urlencoded", -// ) -// .header(header::CONTENT_LENGTH, "9") -// .set_payload(Bytes::from_static(b"bye=world")) -// .finish(); - -// match Option::>::from_request(&req, &cfg) -// .poll() -// .unwrap() -// { -// Async::Ready(r) => assert_eq!(r, None), -// _ => unreachable!(), -// } -// } - -// #[test] -// fn test_result() { -// let req = TestRequest::with_header( -// header::CONTENT_TYPE, -// "application/x-www-form-urlencoded", -// ) -// .header(header::CONTENT_LENGTH, "11") -// .set_payload(Bytes::from_static(b"hello=world")) -// .finish(); - -// match Result::, Error>::from_request(&req, &FormConfig::default()) -// .poll() -// .unwrap() -// { -// Async::Ready(Ok(r)) => assert_eq!( -// r, -// Form(Info { -// hello: "world".into() -// }) -// ), -// _ => unreachable!(), -// } - -// let req = TestRequest::with_header( -// header::CONTENT_TYPE, -// "application/x-www-form-urlencoded", -// ) -// .header(header::CONTENT_LENGTH, "9") -// .set_payload(Bytes::from_static(b"bye=world")) -// .finish(); - -// match Result::, Error>::from_request(&req, &FormConfig::default()) -// .poll() -// .unwrap() -// { -// Async::Ready(r) => assert!(r.is_err()), -// _ => unreachable!(), -// } -// } - -// #[test] -// fn test_payload_config() { -// let req = TestRequest::default().finish(); -// let mut cfg = PayloadConfig::default(); -// cfg.mimetype(mime::APPLICATION_JSON); -// assert!(cfg.check_mimetype(&req).is_err()); - -// let req = TestRequest::with_header( -// header::CONTENT_TYPE, -// "application/x-www-form-urlencoded", -// ) -// .finish(); -// assert!(cfg.check_mimetype(&req).is_err()); - -// let req = -// TestRequest::with_header(header::CONTENT_TYPE, "application/json").finish(); -// assert!(cfg.check_mimetype(&req).is_ok()); -// } - -// #[derive(Deserialize)] -// struct MyStruct { -// key: String, -// value: String, -// } - -// #[derive(Deserialize)] -// struct Id { -// id: String, -// } - -// #[derive(Deserialize)] -// struct Test2 { -// key: String, -// value: u32, -// } - -// #[test] -// fn test_request_extract() { -// let req = TestRequest::with_uri("/name/user1/?id=test").finish(); - -// let mut router = Router::<()>::default(); -// router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); -// let info = router.recognize(&req, &(), 0); -// let req = req.with_route_info(info); - -// let s = Path::::from_request(&req, &()).unwrap(); -// assert_eq!(s.key, "name"); -// assert_eq!(s.value, "user1"); - -// let s = Path::<(String, String)>::from_request(&req, &()).unwrap(); -// assert_eq!(s.0, "name"); -// assert_eq!(s.1, "user1"); - -// let s = Query::::from_request(&req, &()).unwrap(); -// assert_eq!(s.id, "test"); - -// let mut router = Router::<()>::default(); -// router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); -// let req = TestRequest::with_uri("/name/32/").finish(); -// let info = router.recognize(&req, &(), 0); -// let req = req.with_route_info(info); - -// let s = Path::::from_request(&req, &()).unwrap(); -// assert_eq!(s.as_ref().key, "name"); -// assert_eq!(s.value, 32); - -// let s = Path::<(String, u8)>::from_request(&req, &()).unwrap(); -// assert_eq!(s.0, "name"); -// assert_eq!(s.1, 32); - -// let res = Path::>::extract(&req).unwrap(); -// assert_eq!(res[0], "name".to_owned()); -// assert_eq!(res[1], "32".to_owned()); -// } - -// #[test] -// fn test_extract_path_single() { -// let mut router = Router::<()>::default(); -// router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); - -// let req = TestRequest::with_uri("/32/").finish(); -// let info = router.recognize(&req, &(), 0); -// let req = req.with_route_info(info); -// assert_eq!(*Path::::from_request(&req, &()).unwrap(), 32); -// } - -// #[test] -// fn test_tuple_extract() { -// let mut router = Router::<()>::default(); -// router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - -// let req = TestRequest::with_uri("/name/user1/?id=test").finish(); -// let info = router.recognize(&req, &(), 0); -// let req = req.with_route_info(info); - -// let res = match <(Path<(String, String)>,)>::extract(&req).poll() { -// Ok(Async::Ready(res)) => res, -// _ => panic!("error"), -// }; -// assert_eq!((res.0).0, "name"); -// assert_eq!((res.0).1, "user1"); - -// let res = match <(Path<(String, String)>, Path<(String, String)>)>::extract(&req) -// .poll() -// { -// Ok(Async::Ready(res)) => res, -// _ => panic!("error"), -// }; -// assert_eq!((res.0).0, "name"); -// assert_eq!((res.0).1, "user1"); -// assert_eq!((res.1).0, "name"); -// assert_eq!((res.1).1, "user1"); - -// let () = <()>::extract(&req); -// } -// } diff --git a/src/service.rs b/src/service.rs index 5602a6133..a515300a4 100644 --- a/src/service.rs +++ b/src/service.rs @@ -185,6 +185,11 @@ impl

    ServiceFromRequest

    { self.req } + #[inline] + pub fn match_info_mut(&mut self) -> &mut Path { + &mut self.req.path + } + /// Create service response for error #[inline] pub fn error_response>(self, err: E) -> ServiceResponse { diff --git a/src/test.rs b/src/test.rs index b4447f8b3..46fd45d5b 100644 --- a/src/test.rs +++ b/src/test.rs @@ -118,6 +118,12 @@ impl TestRequest { self } + /// Set request config + pub fn config(mut self, data: T) -> Self { + self.extensions.insert(data); + self + } + /// Complete request creation and generate `ServiceRequest` instance pub fn finish(mut self) -> ServiceRequest { let req = self.req.finish(); From 34171fa7f570a8188491445fec86a2d427e48bb0 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 21:02:01 -0800 Subject: [PATCH 181/427] add scopes --- Cargo.toml | 1 + src/app.rs | 120 ++++++- src/guard.rs | 4 +- src/lib.rs | 2 + src/scope.rs | 872 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/test.rs | 9 +- 6 files changed, 1004 insertions(+), 4 deletions(-) create mode 100644 src/scope.rs diff --git a/Cargo.toml b/Cargo.toml index 03b1794ff..370089e7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ actix-utils = "0.3.0" actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } +#actix-router = { path = "../actix-net/router" } bytes = "0.4" derive_more = "0.14" diff --git a/src/app.rs b/src/app.rs index 34a663ae7..276580110 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,6 +14,7 @@ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; use crate::resource::Resource; +use crate::scope::Scope; use crate::service::{ServiceRequest, ServiceResponse}; use crate::state::{State, StateFactory, StateFactoryResult}; @@ -112,6 +113,52 @@ where self } + /// Configure scope for common root path. + /// + /// Scopes collect multiple paths under a common path prefix. + /// Scope path can contain variable path segments as resources. + /// + /// ```rust + /// # extern crate actix_web; + /// use actix_web::{App, HttpRequest, HttpResponse}; + /// + /// fn main() { + /// let app = App::new().scope("/{project_id}", |scope| { + /// scope + /// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + /// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) + /// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) + /// }); + /// } + /// ``` + /// + /// In the above example, three routes get added: + /// * /{project_id}/path1 + /// * /{project_id}/path2 + /// * /{project_id}/path3 + /// + pub fn scope(self, path: &str, f: F) -> AppRouter> + where + F: FnOnce(Scope

    ) -> Scope

    , + { + let scope = f(Scope::new(path)); + let rdef = scope.rdef().clone(); + let default = scope.get_default(); + + let fref = Rc::new(RefCell::new(None)); + AppRouter { + chain: self.chain, + services: vec![(rdef, boxed::new_service(scope.into_new_service()))], + default: None, + defaults: vec![default], + endpoint: AppEntry::new(fref.clone()), + factory_ref: fref, + extensions: self.extensions, + state: self.state, + _t: PhantomData, + } + } + /// Configure resource for a specific path. /// /// Resources may have variable path segments. For example, a @@ -243,6 +290,18 @@ where _t: PhantomData, } } + + /// Set server host name. + /// + /// Host name is used by application router aa a hostname for url + /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. + /// html#method.host) documentation for more information. + /// + /// By default host name is set to a "localhost" value. + pub fn hostname(self, _val: &str) -> Self { + // self.host = val.to_owned(); + self + } } /// Application router builder - Structure that follows the builder pattern @@ -270,6 +329,42 @@ where InitError = (), >, { + /// Configure scope for common root path. + /// + /// Scopes collect multiple paths under a common path prefix. + /// Scope path can contain variable path segments as resources. + /// + /// ```rust + /// # extern crate actix_web; + /// use actix_web::{App, HttpRequest, HttpResponse}; + /// + /// fn main() { + /// let app = App::new().scope("/{project_id}", |scope| { + /// scope + /// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + /// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) + /// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) + /// }); + /// } + /// ``` + /// + /// In the above example, three routes get added: + /// * /{project_id}/path1 + /// * /{project_id}/path2 + /// * /{project_id}/path3 + /// + pub fn scope(mut self, path: &str, f: F) -> Self + where + F: FnOnce(Scope

    ) -> Scope

    , + { + let scope = f(Scope::new(path)); + let rdef = scope.rdef().clone(); + self.defaults.push(scope.get_default()); + self.services + .push((rdef, boxed::new_service(scope.into_new_service()))); + self + } + /// Configure resource for a specific path. /// /// Resources may have variable path segments. For example, a @@ -466,6 +561,7 @@ where // set factory *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { + default: self.default.clone(), services: Rc::new(self.services), }); @@ -480,6 +576,7 @@ where pub struct AppRoutingFactory

    { services: Rc)>>, + default: Option>>, } impl NewService for AppRoutingFactory

    { @@ -491,6 +588,12 @@ impl NewService for AppRoutingFactory

    { type Future = AppRoutingFactoryResponse

    ; fn new_service(&self, _: &()) -> Self::Future { + let default_fut = if let Some(ref default) = self.default { + Some(default.new_service(&())) + } else { + None + }; + AppRoutingFactoryResponse { fut: self .services @@ -502,6 +605,8 @@ impl NewService for AppRoutingFactory

    { ) }) .collect(), + default: None, + default_fut, } } } @@ -512,6 +617,8 @@ type HttpServiceFut

    = Box, Error = ()>>; #[doc(hidden)] pub struct AppRoutingFactoryResponse

    { fut: Vec>, + default: Option>, + default_fut: Option, Error = ()>>>, } enum CreateAppRoutingItem

    { @@ -526,6 +633,13 @@ impl

    Future for AppRoutingFactoryResponse

    { fn poll(&mut self) -> Poll { let mut done = true; + if let Some(ref mut fut) = self.default_fut { + match fut.poll()? { + Async::Ready(default) => self.default = Some(default), + Async::NotReady => done = false, + } + } + // poll http services for item in &mut self.fut { let res = match item { @@ -560,8 +674,9 @@ impl

    Future for AppRoutingFactoryResponse

    { router }); Ok(Async::Ready(AppRouting { - router: router.finish(), ready: None, + router: router.finish(), + default: self.default.take(), })) } else { Ok(Async::NotReady) @@ -572,6 +687,7 @@ impl

    Future for AppRoutingFactoryResponse

    { pub struct AppRouting

    { router: Router>, ready: Option<(ServiceRequest

    , ResourceInfo)>, + default: Option>, } impl

    Service for AppRouting

    { @@ -591,6 +707,8 @@ impl

    Service for AppRouting

    { fn call(&mut self, mut req: ServiceRequest

    ) -> Self::Future { if let Some((srv, _info)) = self.router.recognize_mut(req.match_info_mut()) { Either::A(srv.call(req)) + } else if let Some(ref mut default) = self.default { + Either::A(default.call(req)) } else { let req = req.into_request(); Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) diff --git a/src/guard.rs b/src/guard.rs index 10a56921a..c8948b3b2 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -312,7 +312,9 @@ mod tests { #[test] fn test_preds() { - let r = TestRequest::default().method(Method::TRACE).to_request(); + let r = TestRequest::default() + .method(Method::TRACE) + .to_http_request(); assert!(Not(Get()).check(&r,)); assert!(!Not(Trace()).check(&r,)); diff --git a/src/lib.rs b/src/lib.rs index 37fd75912..18a6de067 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ mod request; mod resource; mod responder; mod route; +mod scope; mod service; mod state; pub mod test; @@ -25,6 +26,7 @@ pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::{Either, Responder}; pub use crate::route::Route; +pub use crate::scope::Scope; pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; pub use crate::state::State; diff --git a/src/scope.rs b/src/scope.rs new file mode 100644 index 000000000..5327f953e --- /dev/null +++ b/src/scope.rs @@ -0,0 +1,872 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use actix_http::Response; +use actix_router::{ResourceDef, ResourceInfo, Router}; +use actix_service::boxed::{self, BoxedNewService, BoxedService}; +use actix_service::{ + ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, +}; +use futures::future::{ok, Either, Future, FutureResult}; +use futures::{Async, Poll}; + +use crate::guard::Guard; +use crate::resource::Resource; +use crate::route::Route; +use crate::service::{ServiceRequest, ServiceResponse}; + +type HttpService

    = BoxedService, ServiceResponse, ()>; +type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; +type BoxedResponse = Box>; + +/// Resources scope +/// +/// Scope is a set of resources with common root path. +/// Scopes collect multiple paths under a common path prefix. +/// Scope path can contain variable path segments as resources. +/// Scope prefix is always complete path segment, i.e `/app` would +/// be converted to a `/app/` and it would not match `/app` path. +/// +/// You can get variable path segments from `HttpRequest::match_info()`. +/// `Path` extractor also is able to extract scope level variable segments. +/// +/// ```rust +/// use actix_web::{App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new().scope("/{project_id}/", |scope| { +/// scope +/// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) +/// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) +/// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) +/// }); +/// } +/// ``` +/// +/// In the above example three routes get registered: +/// * /{project_id}/path1 - reponds to all http method +/// * /{project_id}/path2 - `GET` requests +/// * /{project_id}/path3 - `HEAD` requests +/// +pub struct Scope> { + endpoint: T, + rdef: ResourceDef, + services: Vec<(ResourceDef, HttpNewService

    )>, + guards: Rc>>, + default: Rc>>>>, + defaults: Vec>>>>>, + factory_ref: Rc>>>, +} + +impl Scope

    { + /// Create a new scope + pub fn new(path: &str) -> Scope

    { + let fref = Rc::new(RefCell::new(None)); + let rdef = ResourceDef::prefix(&insert_slash(path)); + Scope { + endpoint: ScopeEndpoint::new(fref.clone()), + rdef: rdef.clone(), + guards: Rc::new(Vec::new()), + services: Vec::new(), + default: Rc::new(RefCell::new(None)), + defaults: Vec::new(), + factory_ref: fref, + } + } +} + +impl Scope +where + T: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + #[inline] + pub(crate) fn rdef(&self) -> &ResourceDef { + &self.rdef + } + + /// Add guard to a scope. + /// + /// ```rust + /// use actix_web::{web, guard, App, HttpRequest, HttpResponse, extract::Path}; + /// + /// fn index(data: Path<(String, String)>) -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new().scope("/app", |scope| { + /// scope + /// .guard(guard::Header("content-type", "text/plain")) + /// .route("/test1",web::get().to(index)) + /// .route("/test2", web::post().to(|r: HttpRequest| { + /// HttpResponse::MethodNotAllowed() + /// })) + /// }); + /// } + /// ``` + pub fn guard(mut self, guard: G) -> Self { + Rc::get_mut(&mut self.guards).unwrap().push(Box::new(guard)); + self + } + + /// Create nested scope. + /// + /// ```rust + /// use actix_web::{App, HttpRequest}; + /// + /// struct AppState; + /// + /// fn index(req: HttpRequest) -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new().scope("/app", |scope| { + /// scope.nested("/v1", |scope| scope.resource("/test1", |r| r.to(index))) + /// }); + /// } + /// ``` + pub fn nested(mut self, path: &str, f: F) -> Self + where + F: FnOnce(Scope

    ) -> Scope

    , + { + let scope = f(Scope::new(path)); + let rdef = scope.rdef().clone(); + self.defaults.push(scope.get_default()); + self.services + .push((rdef, boxed::new_service(scope.into_new_service()))); + + self + } + + /// Configure route for a specific path. + /// + /// This is a simplified version of the `Scope::resource()` method. + /// This method can not be could multiple times, in that case + /// multiple resources with one route would be registered for same resource path. + /// + /// ```rust + /// use actix_web::{web, App, HttpResponse, extract::Path}; + /// + /// fn index(data: Path<(String, String)>) -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new().scope("/app", |scope| { + /// scope.route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())) + /// }); + /// } + /// ``` + pub fn route(self, path: &str, route: Route

    ) -> Self { + self.resource(path, move |r| r.route(route)) + } + + /// configure resource for a specific path. + /// + /// This method is similar to an `App::resource()` method. + /// Resources may have variable path segments. Resource path uses scope + /// path as a path prefix. + /// + /// ```rust + /// use actix_web::*; + /// + /// fn main() { + /// let app = App::new().scope("/api", |scope| { + /// scope.resource("/users/{userid}/{friend}", |r| { + /// r.route(web::get().to(|| HttpResponse::Ok())) + /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) + /// .route(web::route() + /// .guard(guard::Any(guard::Get()).or(guard::Put())) + /// .guard(guard::Header("Content-Type", "text/plain")) + /// .to(|| HttpResponse::Ok())) + /// }) + /// }); + /// } + /// ``` + pub fn resource(mut self, path: &str, f: F) -> Self + where + F: FnOnce(Resource

    ) -> Resource, + U: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, + { + // add resource + let rdef = ResourceDef::new(&insert_slash(path)); + let resource = f(Resource::new()); + self.defaults.push(resource.get_default()); + self.services + .push((rdef, boxed::new_service(resource.into_new_service()))); + self + } + + /// Default resource to be used if no matching route could be found. + pub fn default_resource(mut self, f: F) -> Self + where + F: FnOnce(Resource

    ) -> Resource, + U: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, + { + // create and configure default resource + self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::new_service( + f(Resource::new()).into_new_service().map_init_err(|_| ()), + ))))); + + self + } + + /// Register a scope middleware + /// + /// This is similar to `App's` middlewares, but + /// middleware is not allowed to change response type (i.e modify response's body). + /// Middleware get invoked on scope level. + pub fn middleware( + self, + mw: F, + ) -> Scope< + P, + impl NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, + > + where + M: NewTransform< + T::Service, + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, + F: IntoNewTransform, + { + let endpoint = ApplyNewService::new(mw, self.endpoint); + Scope { + endpoint, + rdef: self.rdef, + guards: self.guards, + services: self.services, + default: self.default, + defaults: self.defaults, + factory_ref: self.factory_ref, + } + } + + pub(crate) fn get_default(&self) -> Rc>>>> { + self.default.clone() + } +} + +fn insert_slash(path: &str) -> String { + let mut path = path.to_owned(); + if !path.is_empty() && !path.starts_with('/') { + path.insert(0, '/'); + }; + path +} + +impl IntoNewService for Scope +where + T: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + fn into_new_service(self) -> T { + // update resource default service + if let Some(ref d) = *self.default.as_ref().borrow() { + for default in &self.defaults { + if default.borrow_mut().is_none() { + *default.borrow_mut() = Some(d.clone()); + } + } + } + + *self.factory_ref.borrow_mut() = Some(ScopeFactory { + default: self.default.clone(), + services: Rc::new(self.services), + }); + + self.endpoint + } +} + +pub struct ScopeFactory

    { + services: Rc)>>, + default: Rc>>>>, +} + +impl NewService for ScopeFactory

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = ScopeService

    ; + type Future = ScopeFactoryResponse

    ; + + fn new_service(&self, _: &()) -> Self::Future { + let default_fut = if let Some(ref default) = *self.default.borrow() { + Some(default.new_service(&())) + } else { + None + }; + + ScopeFactoryResponse { + fut: self + .services + .iter() + .map(|(path, service)| { + CreateScopeServiceItem::Future( + Some(path.clone()), + service.new_service(&()), + ) + }) + .collect(), + default: None, + default_fut, + } + } +} + +/// Create app service +#[doc(hidden)] +pub struct ScopeFactoryResponse

    { + fut: Vec>, + default: Option>, + default_fut: Option, Error = ()>>>, +} + +type HttpServiceFut

    = Box, Error = ()>>; + +enum CreateScopeServiceItem

    { + Future(Option, HttpServiceFut

    ), + Service(ResourceDef, HttpService

    ), +} + +impl

    Future for ScopeFactoryResponse

    { + type Item = ScopeService

    ; + type Error = (); + + fn poll(&mut self) -> Poll { + let mut done = true; + + if let Some(ref mut fut) = self.default_fut { + match fut.poll()? { + Async::Ready(default) => self.default = Some(default), + Async::NotReady => done = false, + } + } + + // poll http services + for item in &mut self.fut { + let res = match item { + CreateScopeServiceItem::Future(ref mut path, ref mut fut) => { + match fut.poll()? { + Async::Ready(service) => Some((path.take().unwrap(), service)), + Async::NotReady => { + done = false; + None + } + } + } + CreateScopeServiceItem::Service(_, _) => continue, + }; + + if let Some((path, service)) = res { + *item = CreateScopeServiceItem::Service(path, service); + } + } + + if done { + let router = self + .fut + .drain(..) + .fold(Router::build(), |mut router, item| { + match item { + CreateScopeServiceItem::Service(path, service) => { + router.rdef(path, service) + } + CreateScopeServiceItem::Future(_, _) => unreachable!(), + } + router + }); + Ok(Async::Ready(ScopeService { + router: router.finish(), + default: self.default.take(), + _ready: None, + })) + } else { + Ok(Async::NotReady) + } + } +} + +pub struct ScopeService

    { + router: Router>, + default: Option>, + _ready: Option<(ServiceRequest

    , ResourceInfo)>, +} + +impl

    Service for ScopeService

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type Future = Either>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, mut req: ServiceRequest

    ) -> Self::Future { + if let Some((srv, _info)) = self.router.recognize_mut(req.match_info_mut()) { + Either::A(srv.call(req)) + } else if let Some(ref mut default) = self.default { + Either::A(default.call(req)) + } else { + let req = req.into_request(); + Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) + } + } +} + +#[doc(hidden)] +pub struct ScopeEndpoint

    { + factory: Rc>>>, +} + +impl

    ScopeEndpoint

    { + fn new(factory: Rc>>>) -> Self { + ScopeEndpoint { factory } + } +} + +impl NewService for ScopeEndpoint

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = ScopeService

    ; + type Future = ScopeFactoryResponse

    ; + + fn new_service(&self, _: &()) -> Self::Future { + self.factory.borrow_mut().as_mut().unwrap().new_service(&()) + } +} + +#[cfg(test)] +mod tests { + use actix_http::body::{Body, ResponseBody}; + use actix_http::http::{Method, StatusCode}; + use actix_service::{IntoNewService, NewService, Service}; + use bytes::Bytes; + + use crate::test::TestRequest; + use crate::{web, App, HttpRequest, HttpResponse}; + + #[test] + fn test_scope() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.resource("/path1", |r| r.to(|| HttpResponse::Ok())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn test_scope_root() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope + .resource("", |r| r.to(|| HttpResponse::Ok())) + .resource("/", |r| r.to(|| HttpResponse::Created())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + } + + #[test] + fn test_scope_root2() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app/", |scope| { + scope.resource("", |r| r.to(|| HttpResponse::Ok())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let req = TestRequest::with_uri("/app/").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn test_scope_root3() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app/", |scope| { + scope.resource("/", |r| r.to(|| HttpResponse::Ok())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let req = TestRequest::with_uri("/app/").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_scope_route() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("app", |scope| { + scope.resource("/path1", |r| { + r.route(web::get().to(|| HttpResponse::Ok())) + .route(web::delete().to(|| HttpResponse::Ok())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/path1") + .method(Method::DELETE) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/path1") + .method(Method::POST) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_scope_route_without_leading_slash() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("app", |scope| { + scope.resource("path1", |r| { + r.route(web::get().to(|| HttpResponse::Ok())) + .route(web::delete().to(|| HttpResponse::Ok())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/path1") + .method(Method::DELETE) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/path1") + .method(Method::POST) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + // #[test] + // fn test_scope_guard() { + // let mut rt = actix_rt::Runtime::new().unwrap(); + // let app = App::new() + // .scope("/app", |scope| { + // scope + // .guard(guard::Get()) + // .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + // }) + // .into_new_service(); + // let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + // let req = TestRequest::with_uri("/app/path1") + // .method(Method::POST) + // .to_request(); + // let resp = rt.block_on(srv.call(req)).unwrap(); + // assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + // let req = TestRequest::with_uri("/app/path1") + // .method(Method::GET) + // .to_request(); + // let resp = rt.block_on(srv.call(req)).unwrap(); + // assert_eq!(resp.status(), StatusCode::OK); + // } + + #[test] + fn test_scope_variable_segment() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/ab-{project}", |scope| { + scope.resource("/path1", |r| { + r.to(|r: HttpRequest| { + HttpResponse::Ok() + .body(format!("project: {}", &r.match_info()["project"])) + }) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/ab-project1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + match resp.body() { + ResponseBody::Body(Body::Bytes(ref b)) => { + let bytes: Bytes = b.clone().into(); + assert_eq!(bytes, Bytes::from_static(b"project: project1")); + } + _ => panic!(), + } + + let req = TestRequest::with_uri("/aa-project1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_nested_scope() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("/t1", |scope| { + scope.resource("/path1", |r| r.to(|| HttpResponse::Created())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/t1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + } + + #[test] + fn test_nested_scope_no_slash() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("t1", |scope| { + scope.resource("/path1", |r| r.to(|| HttpResponse::Created())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/t1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + } + + #[test] + fn test_nested_scope_root() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("/t1", |scope| { + scope + .resource("", |r| r.to(|| HttpResponse::Ok())) + .resource("/", |r| r.to(|| HttpResponse::Created())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/t1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/t1/").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + } + + // #[test] + // fn test_nested_scope_filter() { + // let app = App::new() + // .scope("/app", |scope| { + // scope.nested("/t1", |scope| { + // scope + // .filter(pred::Get()) + // .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) + // }) + // }) + // .finish(); + + // let req = TestRequest::with_uri("/app/t1/path1") + // .method(Method::POST) + // .request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + + // let req = TestRequest::with_uri("/app/t1/path1") + // .method(Method::GET) + // .request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + // } + + #[test] + fn test_nested_scope_with_variable_segment() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("/{project_id}", |scope| { + scope.resource("/path1", |r| { + r.to(|r: HttpRequest| { + HttpResponse::Created().body(format!( + "project: {}", + &r.match_info()["project_id"] + )) + }) + }) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/project_1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + + match resp.body() { + ResponseBody::Body(Body::Bytes(ref b)) => { + let bytes: Bytes = b.clone().into(); + assert_eq!(bytes, Bytes::from_static(b"project: project_1")); + } + _ => panic!(), + } + } + + #[test] + fn test_nested2_scope_with_variable_segment() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("/{project}", |scope| { + scope.nested("/{id}", |scope| { + scope.resource("/path1", |r| { + r.to(|r: HttpRequest| { + HttpResponse::Created().body(format!( + "project: {} - {}", + &r.match_info()["project"], + &r.match_info()["id"], + )) + }) + }) + }) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/test/1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + + match resp.body() { + ResponseBody::Body(Body::Bytes(ref b)) => { + let bytes: Bytes = b.clone().into(); + assert_eq!(bytes, Bytes::from_static(b"project: test - 1")); + } + _ => panic!(), + } + + let req = TestRequest::with_uri("/app/test/1/path2").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_default_resource() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope + .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + .default_resource(|r| r.to(|| HttpResponse::BadRequest())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/path2").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/path2").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_default_resource_propagation() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app1", |scope| { + scope.default_resource(|r| r.to(|| HttpResponse::BadRequest())) + }) + .scope("/app2", |scope| scope) + .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/non-exist").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + + let req = TestRequest::with_uri("/app1/non-exist").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/app2/non-exist").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + } +} diff --git a/src/test.rs b/src/test.rs index 46fd45d5b..684817ec1 100644 --- a/src/test.rs +++ b/src/test.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, Version}; use actix_http::test::TestRequest as HttpTestRequest; -use actix_http::{Extensions, PayloadStream}; +use actix_http::{Extensions, PayloadStream, Request}; use actix_router::{Path, Url}; use bytes::Bytes; @@ -135,8 +135,13 @@ impl TestRequest { ) } + /// Complete request creation and generate `Request` instance + pub fn to_request(mut self) -> Request { + self.req.finish() + } + /// Complete request creation and generate `HttpRequest` instance - pub fn to_request(mut self) -> HttpRequest { + pub fn to_http_request(mut self) -> HttpRequest { let req = self.req.finish(); ServiceRequest::new( From 5c61321565d02f8c08b73d31a90676a48ab08694 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 21:40:03 -0800 Subject: [PATCH 182/427] fix state factory support, tests for state and state factory --- src/app.rs | 175 +++++++++++++++++++++++++++++++++++++++++++++-- src/responder.rs | 37 ++++++++++ src/scope.rs | 2 +- 3 files changed, 208 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index 276580110..3940b9fc5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,7 +14,7 @@ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; use crate::resource::Resource; -use crate::scope::Scope; +use crate::scope::{insert_slash, Scope}; use crate::service::{ServiceRequest, ServiceResponse}; use crate::state::{State, StateFactory, StateFactoryResult}; @@ -103,13 +103,13 @@ where /// Set application state factory. This function is /// similar to `.state()` but it accepts state factory. State get /// constructed asynchronously during application initialization. - pub fn state_factory(mut self, state: F) -> Self + pub fn state_factory(mut self, state: F) -> Self where F: Fn() -> Out + 'static, Out: IntoFuture + 'static, Out::Error: std::fmt::Debug, { - self.state.push(Box::new(State::new(state))); + self.state.push(Box::new(state)); self } @@ -200,7 +200,7 @@ where InitError = (), > + 'static, { - let rdef = ResourceDef::new(path); + let rdef = ResourceDef::new(&insert_slash(path)); let resource = f(Resource::new()); let default = resource.get_default(); @@ -408,7 +408,7 @@ where InitError = (), > + 'static, { - let rdef = ResourceDef::new(path); + let rdef = ResourceDef::new(&insert_slash(path)); let resource = f(Resource::new()); self.defaults.push(resource.get_default()); self.services @@ -891,3 +891,168 @@ where self.chain.call(req) } } + +#[cfg(test)] +mod tests { + use actix_http::http::StatusCode; + + use super::*; + use crate::test::TestRequest; + use crate::{HttpResponse, State}; + + #[test] + fn test_default_resource() { + let mut rt = actix_rt::Runtime::new().unwrap(); + + let app = App::new() + .resource("/test", |r| r.to(|| HttpResponse::Ok())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/test").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/blah").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let app = App::new() + .resource("/test", |r| r.to(|| HttpResponse::Ok())) + .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/blah").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + } + + #[test] + fn test_state() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .state(10usize) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::default().to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let app = App::new() + .state(10u32) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::default().to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + } + + #[test] + fn test_state_factory() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .state_factory(|| Ok::<_, ()>(10usize)) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::default().to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let app = App::new() + .state_factory(|| Ok::<_, ()>(10u32)) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::default().to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + } + + // #[test] + // fn test_handler() { + // let app = App::new() + // .handler("/test", |_: &_| HttpResponse::Ok()) + // .finish(); + + // let req = TestRequest::with_uri("/test").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + + // let req = TestRequest::with_uri("/test/").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + + // let req = TestRequest::with_uri("/test/app").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + + // let req = TestRequest::with_uri("/testapp").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + + // let req = TestRequest::with_uri("/blah").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + // } + + // #[test] + // fn test_handler2() { + // let app = App::new() + // .handler("test", |_: &_| HttpResponse::Ok()) + // .finish(); + + // let req = TestRequest::with_uri("/test").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + + // let req = TestRequest::with_uri("/test/").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + + // let req = TestRequest::with_uri("/test/app").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + + // let req = TestRequest::with_uri("/testapp").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + + // let req = TestRequest::with_uri("/blah").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + // } + + // #[test] + // fn test_route() { + // let app = App::new() + // .route("/test", Method::GET, |_: HttpRequest| HttpResponse::Ok()) + // .route("/test", Method::POST, |_: HttpRequest| { + // HttpResponse::Created() + // }) + // .finish(); + + // let req = TestRequest::with_uri("/test").method(Method::GET).request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + + // let req = TestRequest::with_uri("/test") + // .method(Method::POST) + // .request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::CREATED); + + // let req = TestRequest::with_uri("/test") + // .method(Method::HEAD) + // .request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + // } +} diff --git a/src/responder.rs b/src/responder.rs index 22588a9cd..8e7f66b43 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -272,3 +272,40 @@ where Ok(self.0.poll().map_err(|e| e.into())?) } } + +#[cfg(test)] +mod tests { + // use actix_http::body::Body; + use actix_http::body::{Body, ResponseBody}; + use actix_http::http::StatusCode; + use actix_service::{IntoNewService, NewService, Service}; + use bytes::Bytes; + + use crate::test::TestRequest; + use crate::App; + + #[test] + fn test_option_responder() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .resource("/none", |r| r.to(|| -> Option<&'static str> { None })) + .resource("/some", |r| r.to(|| Some("some"))) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/none").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let req = TestRequest::with_uri("/some").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + match resp.body() { + ResponseBody::Body(Body::Bytes(ref b)) => { + let bytes: Bytes = b.clone().into(); + assert_eq!(bytes, Bytes::from_static(b"some")); + } + _ => panic!(), + } + } +} diff --git a/src/scope.rs b/src/scope.rs index 5327f953e..ec6bc0354 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -272,7 +272,7 @@ where } } -fn insert_slash(path: &str) -> String { +pub(crate) fn insert_slash(path: &str) -> String { let mut path = path.to_owned(); if !path.is_empty() && !path.starts_with('/') { path.insert(0, '/'); From e442ddb1671ad9fb1644c3e7f150d6f834cccf78 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 4 Mar 2019 11:47:53 -0800 Subject: [PATCH 183/427] allow scope level guards --- Cargo.toml | 1 - src/app.rs | 92 +++++++++++++++--------- src/scope.rs | 185 +++++++++++++++++++++++++++++-------------------- src/service.rs | 8 ++- 4 files changed, 179 insertions(+), 107 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 370089e7e..03b1794ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,6 @@ actix-utils = "0.3.0" actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } -#actix-router = { path = "../actix-net/router" } bytes = "0.4" derive_more = "0.14" diff --git a/src/app.rs b/src/app.rs index 3940b9fc5..119f1a21e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,11 +13,13 @@ use actix_service::{ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; +use crate::guard::Guard; use crate::resource::Resource; use crate::scope::{insert_slash, Scope}; use crate::service::{ServiceRequest, ServiceResponse}; use crate::state::{State, StateFactory, StateFactoryResult}; +type Guards = Vec>; type HttpService

    = BoxedService, ServiceResponse, ()>; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; type BoxedResponse = Box>; @@ -141,14 +143,15 @@ where where F: FnOnce(Scope

    ) -> Scope

    , { - let scope = f(Scope::new(path)); + let mut scope = f(Scope::new(path)); let rdef = scope.rdef().clone(); let default = scope.get_default(); + let guards = scope.take_guards(); let fref = Rc::new(RefCell::new(None)); AppRouter { chain: self.chain, - services: vec![(rdef, boxed::new_service(scope.into_new_service()))], + services: vec![(rdef, boxed::new_service(scope.into_new_service()), guards)], default: None, defaults: vec![default], endpoint: AppEntry::new(fref.clone()), @@ -201,13 +204,13 @@ where > + 'static, { let rdef = ResourceDef::new(&insert_slash(path)); - let resource = f(Resource::new()); - let default = resource.get_default(); + let res = f(Resource::new()); + let default = res.get_default(); let fref = Rc::new(RefCell::new(None)); AppRouter { chain: self.chain, - services: vec![(rdef, boxed::new_service(resource.into_new_service()))], + services: vec![(rdef, boxed::new_service(res.into_new_service()), None)], default: None, defaults: vec![default], endpoint: AppEntry::new(fref.clone()), @@ -308,7 +311,7 @@ where /// for building application instances. pub struct AppRouter { chain: C, - services: Vec<(ResourceDef, HttpNewService

    )>, + services: Vec<(ResourceDef, HttpNewService

    , Option)>, default: Option>>, defaults: Vec>>>>>, endpoint: T, @@ -357,11 +360,12 @@ where where F: FnOnce(Scope

    ) -> Scope

    , { - let scope = f(Scope::new(path)); + let mut scope = f(Scope::new(path)); let rdef = scope.rdef().clone(); + let guards = scope.take_guards(); self.defaults.push(scope.get_default()); self.services - .push((rdef, boxed::new_service(scope.into_new_service()))); + .push((rdef, boxed::new_service(scope.into_new_service()), guards)); self } @@ -411,8 +415,11 @@ where let rdef = ResourceDef::new(&insert_slash(path)); let resource = f(Resource::new()); self.defaults.push(resource.get_default()); - self.services - .push((rdef, boxed::new_service(resource.into_new_service()))); + self.services.push(( + rdef, + boxed::new_service(resource.into_new_service()), + None, + )); self } @@ -452,6 +459,7 @@ where self.services.push(( rdef.into(), boxed::new_service(factory.into_new_service().map_init_err(|_| ())), + None, )); self } @@ -562,7 +570,12 @@ where // set factory *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { default: self.default.clone(), - services: Rc::new(self.services), + services: Rc::new( + self.services + .into_iter() + .map(|(rdef, srv, guards)| (rdef, srv, RefCell::new(guards))) + .collect(), + ), }); AppInit { @@ -575,7 +588,7 @@ where } pub struct AppRoutingFactory

    { - services: Rc)>>, + services: Rc, RefCell>)>>, default: Option>>, } @@ -598,9 +611,10 @@ impl NewService for AppRoutingFactory

    { fut: self .services .iter() - .map(|(path, service)| { + .map(|(path, service, guards)| { CreateAppRoutingItem::Future( Some(path.clone()), + guards.borrow_mut().take(), service.new_service(&()), ) }) @@ -622,8 +636,8 @@ pub struct AppRoutingFactoryResponse

    { } enum CreateAppRoutingItem

    { - Future(Option, HttpServiceFut

    ), - Service(ResourceDef, HttpService

    ), + Future(Option, Option, HttpServiceFut

    ), + Service(ResourceDef, Option, HttpService

    ), } impl

    Future for AppRoutingFactoryResponse

    { @@ -643,20 +657,24 @@ impl

    Future for AppRoutingFactoryResponse

    { // poll http services for item in &mut self.fut { let res = match item { - CreateAppRoutingItem::Future(ref mut path, ref mut fut) => { - match fut.poll()? { - Async::Ready(service) => Some((path.take().unwrap(), service)), - Async::NotReady => { - done = false; - None - } + CreateAppRoutingItem::Future( + ref mut path, + ref mut guards, + ref mut fut, + ) => match fut.poll()? { + Async::Ready(service) => { + Some((path.take().unwrap(), guards.take(), service)) } - } - CreateAppRoutingItem::Service(_, _) => continue, + Async::NotReady => { + done = false; + None + } + }, + CreateAppRoutingItem::Service(_, _, _) => continue, }; - if let Some((path, service)) = res { - *item = CreateAppRoutingItem::Service(path, service); + if let Some((path, guards, service)) = res { + *item = CreateAppRoutingItem::Service(path, guards, service); } } @@ -666,10 +684,11 @@ impl

    Future for AppRoutingFactoryResponse

    { .drain(..) .fold(Router::build(), |mut router, item| { match item { - CreateAppRoutingItem::Service(path, service) => { - router.rdef(path, service) + CreateAppRoutingItem::Service(path, guards, service) => { + router.rdef(path, service); + router.set_user_data(guards); } - CreateAppRoutingItem::Future(_, _) => unreachable!(), + CreateAppRoutingItem::Future(_, _, _) => unreachable!(), } router }); @@ -685,7 +704,7 @@ impl

    Future for AppRoutingFactoryResponse

    { } pub struct AppRouting

    { - router: Router>, + router: Router, Guards>, ready: Option<(ServiceRequest

    , ResourceInfo)>, default: Option>, } @@ -705,7 +724,18 @@ impl

    Service for AppRouting

    { } fn call(&mut self, mut req: ServiceRequest

    ) -> Self::Future { - if let Some((srv, _info)) = self.router.recognize_mut(req.match_info_mut()) { + let res = self.router.recognize_mut_checked(&mut req, |req, guards| { + if let Some(ref guards) = guards { + for f in guards { + if !f.check(req.head()) { + return false; + } + } + } + true + }); + + if let Some((srv, _info)) = res { Either::A(srv.call(req)) } else if let Some(ref mut default) = self.default { Either::A(default.call(req)) diff --git a/src/scope.rs b/src/scope.rs index ec6bc0354..2ed18a423 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -15,6 +15,7 @@ use crate::resource::Resource; use crate::route::Route; use crate::service::{ServiceRequest, ServiceResponse}; +type Guards = Vec>; type HttpService

    = BoxedService, ServiceResponse, ()>; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; type BoxedResponse = Box>; @@ -51,8 +52,8 @@ type BoxedResponse = Box>; pub struct Scope> { endpoint: T, rdef: ResourceDef, - services: Vec<(ResourceDef, HttpNewService

    )>, - guards: Rc>>, + services: Vec<(ResourceDef, HttpNewService

    , Option)>, + guards: Vec>, default: Rc>>>>, defaults: Vec>>>>>, factory_ref: Rc>>>, @@ -66,7 +67,7 @@ impl Scope

    { Scope { endpoint: ScopeEndpoint::new(fref.clone()), rdef: rdef.clone(), - guards: Rc::new(Vec::new()), + guards: Vec::new(), services: Vec::new(), default: Rc::new(RefCell::new(None)), defaults: Vec::new(), @@ -110,7 +111,7 @@ where /// } /// ``` pub fn guard(mut self, guard: G) -> Self { - Rc::get_mut(&mut self.guards).unwrap().push(Box::new(guard)); + self.guards.push(Box::new(guard)); self } @@ -135,11 +136,12 @@ where where F: FnOnce(Scope

    ) -> Scope

    , { - let scope = f(Scope::new(path)); + let mut scope = f(Scope::new(path)); let rdef = scope.rdef().clone(); + let guards = scope.take_guards(); self.defaults.push(scope.get_default()); self.services - .push((rdef, boxed::new_service(scope.into_new_service()))); + .push((rdef, boxed::new_service(scope.into_new_service()), guards)); self } @@ -204,8 +206,11 @@ where let rdef = ResourceDef::new(&insert_slash(path)); let resource = f(Resource::new()); self.defaults.push(resource.get_default()); - self.services - .push((rdef, boxed::new_service(resource.into_new_service()))); + self.services.push(( + rdef, + boxed::new_service(resource.into_new_service()), + None, + )); self } @@ -270,6 +275,14 @@ where pub(crate) fn get_default(&self) -> Rc>>>> { self.default.clone() } + + pub(crate) fn take_guards(&mut self) -> Option>> { + if self.guards.is_empty() { + None + } else { + Some(std::mem::replace(&mut self.guards, Vec::new())) + } + } } pub(crate) fn insert_slash(path: &str) -> String { @@ -301,7 +314,12 @@ where *self.factory_ref.borrow_mut() = Some(ScopeFactory { default: self.default.clone(), - services: Rc::new(self.services), + services: Rc::new( + self.services + .into_iter() + .map(|(rdef, srv, guards)| (rdef, srv, RefCell::new(guards))) + .collect(), + ), }); self.endpoint @@ -309,7 +327,7 @@ where } pub struct ScopeFactory

    { - services: Rc)>>, + services: Rc, RefCell>)>>, default: Rc>>>>, } @@ -332,9 +350,10 @@ impl NewService for ScopeFactory

    { fut: self .services .iter() - .map(|(path, service)| { + .map(|(path, service, guards)| { CreateScopeServiceItem::Future( Some(path.clone()), + guards.borrow_mut().take(), service.new_service(&()), ) }) @@ -356,8 +375,8 @@ pub struct ScopeFactoryResponse

    { type HttpServiceFut

    = Box, Error = ()>>; enum CreateScopeServiceItem

    { - Future(Option, HttpServiceFut

    ), - Service(ResourceDef, HttpService

    ), + Future(Option, Option, HttpServiceFut

    ), + Service(ResourceDef, Option, HttpService

    ), } impl

    Future for ScopeFactoryResponse

    { @@ -377,20 +396,24 @@ impl

    Future for ScopeFactoryResponse

    { // poll http services for item in &mut self.fut { let res = match item { - CreateScopeServiceItem::Future(ref mut path, ref mut fut) => { - match fut.poll()? { - Async::Ready(service) => Some((path.take().unwrap(), service)), - Async::NotReady => { - done = false; - None - } + CreateScopeServiceItem::Future( + ref mut path, + ref mut guards, + ref mut fut, + ) => match fut.poll()? { + Async::Ready(service) => { + Some((path.take().unwrap(), guards.take(), service)) } - } - CreateScopeServiceItem::Service(_, _) => continue, + Async::NotReady => { + done = false; + None + } + }, + CreateScopeServiceItem::Service(_, _, _) => continue, }; - if let Some((path, service)) = res { - *item = CreateScopeServiceItem::Service(path, service); + if let Some((path, guards, service)) = res { + *item = CreateScopeServiceItem::Service(path, guards, service); } } @@ -400,10 +423,11 @@ impl

    Future for ScopeFactoryResponse

    { .drain(..) .fold(Router::build(), |mut router, item| { match item { - CreateScopeServiceItem::Service(path, service) => { - router.rdef(path, service) + CreateScopeServiceItem::Service(path, guards, service) => { + router.rdef(path, service); + router.set_user_data(guards); } - CreateScopeServiceItem::Future(_, _) => unreachable!(), + CreateScopeServiceItem::Future(_, _, _) => unreachable!(), } router }); @@ -419,7 +443,7 @@ impl

    Future for ScopeFactoryResponse

    { } pub struct ScopeService

    { - router: Router>, + router: Router, Vec>>, default: Option>, _ready: Option<(ServiceRequest

    , ResourceInfo)>, } @@ -435,7 +459,18 @@ impl

    Service for ScopeService

    { } fn call(&mut self, mut req: ServiceRequest

    ) -> Self::Future { - if let Some((srv, _info)) = self.router.recognize_mut(req.match_info_mut()) { + let res = self.router.recognize_mut_checked(&mut req, |req, guards| { + if let Some(ref guards) = guards { + for f in guards { + if !f.check(req.head()) { + return false; + } + } + } + true + }); + + if let Some((srv, _info)) = res { Either::A(srv.call(req)) } else if let Some(ref mut default) = self.default { Either::A(default.call(req)) @@ -478,7 +513,7 @@ mod tests { use bytes::Bytes; use crate::test::TestRequest; - use crate::{web, App, HttpRequest, HttpResponse}; + use crate::{guard, web, App, HttpRequest, HttpResponse}; #[test] fn test_scope() { @@ -614,30 +649,30 @@ mod tests { assert_eq!(resp.status(), StatusCode::NOT_FOUND); } - // #[test] - // fn test_scope_guard() { - // let mut rt = actix_rt::Runtime::new().unwrap(); - // let app = App::new() - // .scope("/app", |scope| { - // scope - // .guard(guard::Get()) - // .resource("/path1", |r| r.to(|| HttpResponse::Ok())) - // }) - // .into_new_service(); - // let mut srv = rt.block_on(app.new_service(&())).unwrap(); + #[test] + fn test_scope_guard() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope + .guard(guard::Get()) + .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); - // let req = TestRequest::with_uri("/app/path1") - // .method(Method::POST) - // .to_request(); - // let resp = rt.block_on(srv.call(req)).unwrap(); - // assert_eq!(resp.status(), StatusCode::NOT_FOUND); + let req = TestRequest::with_uri("/app/path1") + .method(Method::POST) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); - // let req = TestRequest::with_uri("/app/path1") - // .method(Method::GET) - // .to_request(); - // let resp = rt.block_on(srv.call(req)).unwrap(); - // assert_eq!(resp.status(), StatusCode::OK); - // } + let req = TestRequest::with_uri("/app/path1") + .method(Method::GET) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } #[test] fn test_scope_variable_segment() { @@ -728,30 +763,32 @@ mod tests { assert_eq!(resp.status(), StatusCode::CREATED); } - // #[test] - // fn test_nested_scope_filter() { - // let app = App::new() - // .scope("/app", |scope| { - // scope.nested("/t1", |scope| { - // scope - // .filter(pred::Get()) - // .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - // }) - // }) - // .finish(); + #[test] + fn test_nested_scope_filter() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("/t1", |scope| { + scope + .guard(guard::Get()) + .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); - // let req = TestRequest::with_uri("/app/t1/path1") - // .method(Method::POST) - // .request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + let req = TestRequest::with_uri("/app/t1/path1") + .method(Method::POST) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); - // let req = TestRequest::with_uri("/app/t1/path1") - // .method(Method::GET) - // .request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - // } + let req = TestRequest::with_uri("/app/t1/path1") + .method(Method::GET) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } #[test] fn test_nested_scope_with_variable_segment() { diff --git a/src/service.rs b/src/service.rs index a515300a4..637a8668a 100644 --- a/src/service.rs +++ b/src/service.rs @@ -8,7 +8,7 @@ use actix_http::{ Error, Extensions, HttpMessage, Payload, Request, RequestHead, Response, ResponseHead, }; -use actix_router::{Path, Url}; +use actix_router::{Path, Resource, Url}; use futures::future::{ok, FutureResult, IntoFuture}; use crate::request::HttpRequest; @@ -137,6 +137,12 @@ impl

    ServiceRequest

    { } } +impl

    Resource for ServiceRequest

    { + fn resource_path(&mut self) -> &mut Path { + self.match_info_mut() + } +} + impl

    HttpMessage for ServiceRequest

    { type Stream = P; From bd4124587a1fae9e14a31d5ecaf050f7b454d186 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 4 Mar 2019 13:25:35 -0800 Subject: [PATCH 184/427] provide block_on function for testing purpose --- Cargo.toml | 2 +- src/app.rs | 50 +++++++------ src/extract.rs | 48 ++++--------- src/guard.rs | 74 ++++++++++--------- src/middleware/defaultheaders.rs | 16 ++--- src/responder.rs | 7 +- src/scope.rs | 118 +++++++++++++------------------ src/test.rs | 67 +++++++++++++++--- 8 files changed, 207 insertions(+), 175 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 03b1794ff..3b88c3001 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ flate2-rust = ["flate2/rust_backend"] actix-codec = "0.1.0" actix-service = "0.3.0" actix-utils = "0.3.0" +actix-rt = "0.1.0" actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } @@ -69,7 +70,6 @@ brotli2 = { version="^0.3.2", optional = true } flate2 = { version="^1.0.2", optional = true, default-features = false } [dev-dependencies] -actix-rt = "0.1.0" actix-server = { version="0.3.0", features=["ssl"] } actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } diff --git a/src/app.rs b/src/app.rs index 119f1a21e..e1479080a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -924,85 +924,95 @@ where #[cfg(test)] mod tests { - use actix_http::http::StatusCode; + use actix_http::http::{Method, StatusCode}; use super::*; - use crate::test::TestRequest; - use crate::{HttpResponse, State}; + use crate::test::{block_on, TestRequest}; + use crate::{web, HttpResponse, State}; #[test] fn test_default_resource() { - let mut rt = actix_rt::Runtime::new().unwrap(); - let app = App::new() .resource("/test", |r| r.to(|| HttpResponse::Ok())) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/test").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/blah").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let app = App::new() .resource("/test", |r| r.to(|| HttpResponse::Ok())) + .resource("/test2", |r| { + r.default_resource(|r| r.to(|| HttpResponse::Created())) + .route(web::get().to(|| HttpResponse::Ok())) + }) .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/blah").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + + let req = TestRequest::with_uri("/test2").to_request(); + let resp = block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/test2") + .method(Method::POST) + .to_request(); + let resp = block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); } #[test] fn test_state() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .state(10usize) .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::default().to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let app = App::new() .state(10u32) .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::default().to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } #[test] fn test_state_factory() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .state_factory(|| Ok::<_, ()>(10usize)) .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::default().to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let app = App::new() .state_factory(|| Ok::<_, ()>(10u32)) .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::default().to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } diff --git a/src/extract.rs b/src/extract.rs index 3b5c7e742..7350d7d95 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -1152,7 +1152,7 @@ mod tests { use serde_derive::Deserialize; use super::*; - use crate::test::TestRequest; + use crate::test::{block_on, TestRequest}; #[derive(Deserialize, Debug, PartialEq)] struct Info { @@ -1161,29 +1161,26 @@ mod tests { #[test] fn test_bytes() { - let mut rt = actix_rt::Runtime::new().unwrap(); let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) .to_from(); - let s = rt.block_on(Bytes::from_request(&mut req)).unwrap(); + let s = block_on(Bytes::from_request(&mut req)).unwrap(); assert_eq!(s, Bytes::from_static(b"hello=world")); } #[test] fn test_string() { - let mut rt = actix_rt::Runtime::new().unwrap(); let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) .to_from(); - let s = rt.block_on(String::from_request(&mut req)).unwrap(); + let s = block_on(String::from_request(&mut req)).unwrap(); assert_eq!(s, "hello=world"); } #[test] fn test_form() { - let mut rt = actix_rt::Runtime::new().unwrap(); let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", @@ -1192,13 +1189,12 @@ mod tests { .set_payload(Bytes::from_static(b"hello=world")) .to_from(); - let s = rt.block_on(Form::::from_request(&mut req)).unwrap(); + let s = block_on(Form::::from_request(&mut req)).unwrap(); assert_eq!(s.hello, "world"); } #[test] fn test_option() { - let mut rt = actix_rt::Runtime::new().unwrap(); let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", @@ -1206,9 +1202,7 @@ mod tests { .config(FormConfig::default().limit(4096)) .to_from(); - let r = rt - .block_on(Option::>::from_request(&mut req)) - .unwrap(); + let r = block_on(Option::>::from_request(&mut req)).unwrap(); assert_eq!(r, None); let mut req = TestRequest::with_header( @@ -1219,9 +1213,7 @@ mod tests { .set_payload(Bytes::from_static(b"hello=world")) .to_from(); - let r = rt - .block_on(Option::>::from_request(&mut req)) - .unwrap(); + let r = block_on(Option::>::from_request(&mut req)).unwrap(); assert_eq!( r, Some(Form(Info { @@ -1237,15 +1229,12 @@ mod tests { .set_payload(Bytes::from_static(b"bye=world")) .to_from(); - let r = rt - .block_on(Option::>::from_request(&mut req)) - .unwrap(); + let r = block_on(Option::>::from_request(&mut req)).unwrap(); assert_eq!(r, None); } #[test] fn test_result() { - let mut rt = actix_rt::Runtime::new().unwrap(); let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", @@ -1254,8 +1243,7 @@ mod tests { .set_payload(Bytes::from_static(b"hello=world")) .to_from(); - let r = rt - .block_on(Result::, Error>::from_request(&mut req)) + let r = block_on(Result::, Error>::from_request(&mut req)) .unwrap() .unwrap(); assert_eq!( @@ -1273,9 +1261,7 @@ mod tests { .set_payload(Bytes::from_static(b"bye=world")) .to_from(); - let r = rt - .block_on(Result::, Error>::from_request(&mut req)) - .unwrap(); + let r = block_on(Result::, Error>::from_request(&mut req)).unwrap(); assert!(r.is_err()); } @@ -1361,25 +1347,19 @@ mod tests { #[test] fn test_tuple_extract() { - let mut rt = actix_rt::Runtime::new().unwrap(); let resource = ResourceDef::new("/{key}/{value}/"); let mut req = TestRequest::with_uri("/name/user1/?id=test").to_from(); resource.match_path(req.match_info_mut()); - let res = rt - .block_on(<(Path<(String, String)>,)>::from_request(&mut req)) - .unwrap(); + let res = block_on(<(Path<(String, String)>,)>::from_request(&mut req)).unwrap(); assert_eq!((res.0).0, "name"); assert_eq!((res.0).1, "user1"); - let res = rt - .block_on( - <(Path<(String, String)>, Path<(String, String)>)>::from_request( - &mut req, - ), - ) - .unwrap(); + let res = block_on( + <(Path<(String, String)>, Path<(String, String)>)>::from_request(&mut req), + ) + .unwrap(); assert_eq!((res.0).0, "name"); assert_eq!((res.0).1, "user1"); assert_eq!((res.1).0, "name"); diff --git a/src/guard.rs b/src/guard.rs index c8948b3b2..93b6e1325 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -239,8 +239,7 @@ mod tests { #[test] fn test_header() { let req = TestRequest::with_header(header::TRANSFER_ENCODING, "chunked") - .finish() - .into_request(); + .to_http_request(); let pred = Header("transfer-encoding", "chunked"); assert!(pred.check(&req)); @@ -270,44 +269,55 @@ mod tests { #[test] fn test_methods() { - let req = TestRequest::default().finish().into_request(); + let req = TestRequest::default().to_http_request(); let req2 = TestRequest::default() .method(Method::POST) - .finish() - .into_request(); + .to_http_request(); assert!(Get().check(&req)); assert!(!Get().check(&req2)); assert!(Post().check(&req2)); assert!(!Post().check(&req)); - let r = TestRequest::default().method(Method::PUT).finish(); - assert!(Put().check(&r,)); - assert!(!Put().check(&req,)); + let r = TestRequest::default().method(Method::PUT).to_http_request(); + assert!(Put().check(&r)); + assert!(!Put().check(&req)); - let r = TestRequest::default().method(Method::DELETE).finish(); - assert!(Delete().check(&r,)); - assert!(!Delete().check(&req,)); + let r = TestRequest::default() + .method(Method::DELETE) + .to_http_request(); + assert!(Delete().check(&r)); + assert!(!Delete().check(&req)); - let r = TestRequest::default().method(Method::HEAD).finish(); - assert!(Head().check(&r,)); - assert!(!Head().check(&req,)); + let r = TestRequest::default() + .method(Method::HEAD) + .to_http_request(); + assert!(Head().check(&r)); + assert!(!Head().check(&req)); - let r = TestRequest::default().method(Method::OPTIONS).finish(); - assert!(Options().check(&r,)); - assert!(!Options().check(&req,)); + let r = TestRequest::default() + .method(Method::OPTIONS) + .to_http_request(); + assert!(Options().check(&r)); + assert!(!Options().check(&req)); - let r = TestRequest::default().method(Method::CONNECT).finish(); - assert!(Connect().check(&r,)); - assert!(!Connect().check(&req,)); + let r = TestRequest::default() + .method(Method::CONNECT) + .to_http_request(); + assert!(Connect().check(&r)); + assert!(!Connect().check(&req)); - let r = TestRequest::default().method(Method::PATCH).finish(); - assert!(Patch().check(&r,)); - assert!(!Patch().check(&req,)); + let r = TestRequest::default() + .method(Method::PATCH) + .to_http_request(); + assert!(Patch().check(&r)); + assert!(!Patch().check(&req)); - let r = TestRequest::default().method(Method::TRACE).finish(); - assert!(Trace().check(&r,)); - assert!(!Trace().check(&req,)); + let r = TestRequest::default() + .method(Method::TRACE) + .to_http_request(); + assert!(Trace().check(&r)); + assert!(!Trace().check(&req)); } #[test] @@ -316,13 +326,13 @@ mod tests { .method(Method::TRACE) .to_http_request(); - assert!(Not(Get()).check(&r,)); - assert!(!Not(Trace()).check(&r,)); + assert!(Not(Get()).check(&r)); + assert!(!Not(Trace()).check(&r)); - assert!(All(Trace()).and(Trace()).check(&r,)); - assert!(!All(Get()).and(Trace()).check(&r,)); + assert!(All(Trace()).and(Trace()).check(&r)); + assert!(!All(Get()).and(Trace()).check(&r)); - assert!(Any(Get()).or(Trace()).check(&r,)); - assert!(!Any(Get()).or(Get()).check(&r,)); + assert!(Any(Get()).or(Trace()).check(&r)); + assert!(!Any(Get()).or(Get()).check(&r)); } } diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index fa287b288..40bf9f1cc 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -138,39 +138,37 @@ mod tests { use actix_service::FnService; use super::*; - use crate::test::TestRequest; + use crate::test::{block_on, TestRequest}; use crate::{HttpResponse, ServiceRequest}; #[test] fn test_default_headers() { - let mut rt = actix_rt::Runtime::new().unwrap(); let mut mw = DefaultHeaders::new().header(CONTENT_TYPE, "0001"); let mut srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().finish()) }); - let req = TestRequest::default().finish(); - let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); + let req = TestRequest::default().to_service(); + let resp = block_on(mw.call(req, &mut srv)).unwrap(); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - let req = TestRequest::default().finish(); + let req = TestRequest::default().to_service(); let mut srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish()) }); - let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); + let resp = block_on(mw.call(req, &mut srv)).unwrap(); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); } #[test] fn test_content_type() { - let mut rt = actix_rt::Runtime::new().unwrap(); let mut mw = DefaultHeaders::new().content_type(); let mut srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().finish()) }); - let req = TestRequest::default().finish(); - let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); + let req = TestRequest::default().to_service(); + let resp = block_on(mw.call(req, &mut srv)).unwrap(); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), "application/octet-stream" diff --git a/src/responder.rs b/src/responder.rs index 8e7f66b43..b2fd848f0 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -286,19 +286,18 @@ mod tests { #[test] fn test_option_responder() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .resource("/none", |r| r.to(|| -> Option<&'static str> { None })) .resource("/some", |r| r.to(|| Some("some"))) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = TestRequest::block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/none").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = TestRequest::block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let req = TestRequest::with_uri("/some").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = TestRequest::block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); match resp.body() { ResponseBody::Body(Body::Bytes(ref b)) => { diff --git a/src/scope.rs b/src/scope.rs index 2ed18a423..7aeb50412 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -32,14 +32,14 @@ type BoxedResponse = Box>; /// `Path` extractor also is able to extract scope level variable segments. /// /// ```rust -/// use actix_web::{App, HttpResponse}; +/// use actix_web::{web, App, HttpResponse}; /// /// fn main() { /// let app = App::new().scope("/{project_id}/", |scope| { /// scope /// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) -/// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) -/// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) +/// .resource("/path2", |r| r.route(web::get().to(|| HttpResponse::Ok()))) +/// .resource("/path3", |r| r.route(web::head().to(|| HttpResponse::MethodNotAllowed()))) /// }); /// } /// ``` @@ -512,27 +512,25 @@ mod tests { use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; - use crate::test::TestRequest; + use crate::test::{block_on, TestRequest}; use crate::{guard, web, App, HttpRequest, HttpResponse}; #[test] fn test_scope() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope.resource("/path1", |r| r.to(|| HttpResponse::Ok())) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[test] fn test_scope_root() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope @@ -540,58 +538,55 @@ mod tests { .resource("/", |r| r.to(|| HttpResponse::Created())) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); } #[test] fn test_scope_root2() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app/", |scope| { scope.resource("", |r| r.to(|| HttpResponse::Ok())) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let req = TestRequest::with_uri("/app/").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[test] fn test_scope_root3() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app/", |scope| { scope.resource("/", |r| r.to(|| HttpResponse::Ok())) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let req = TestRequest::with_uri("/app/").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[test] fn test_scope_route() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("app", |scope| { scope.resource("/path1", |r| { @@ -600,28 +595,27 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/path1") .method(Method::DELETE) .to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/path1") .method(Method::POST) .to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[test] fn test_scope_route_without_leading_slash() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("app", |scope| { scope.resource("path1", |r| { @@ -630,28 +624,27 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/path1") .method(Method::DELETE) .to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/path1") .method(Method::POST) .to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[test] fn test_scope_guard() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope @@ -659,24 +652,23 @@ mod tests { .resource("/path1", |r| r.to(|| HttpResponse::Ok())) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/path1") .method(Method::POST) .to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let req = TestRequest::with_uri("/app/path1") .method(Method::GET) .to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[test] fn test_scope_variable_segment() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/ab-{project}", |scope| { scope.resource("/path1", |r| { @@ -687,10 +679,10 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/ab-project1/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); match resp.body() { @@ -702,13 +694,12 @@ mod tests { } let req = TestRequest::with_uri("/aa-project1/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[test] fn test_nested_scope() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope.nested("/t1", |scope| { @@ -716,16 +707,15 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/t1/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); } #[test] fn test_nested_scope_no_slash() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope.nested("t1", |scope| { @@ -733,16 +723,15 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/t1/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); } #[test] fn test_nested_scope_root() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope.nested("/t1", |scope| { @@ -752,20 +741,19 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/t1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/t1/").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); } #[test] fn test_nested_scope_filter() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope.nested("/t1", |scope| { @@ -775,24 +763,23 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/t1/path1") .method(Method::POST) .to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let req = TestRequest::with_uri("/app/t1/path1") .method(Method::GET) .to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[test] fn test_nested_scope_with_variable_segment() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope.nested("/{project_id}", |scope| { @@ -807,10 +794,10 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/project_1/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); match resp.body() { @@ -824,7 +811,6 @@ mod tests { #[test] fn test_nested2_scope_with_variable_segment() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope.nested("/{project}", |scope| { @@ -842,10 +828,10 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/test/1/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); match resp.body() { @@ -857,13 +843,12 @@ mod tests { } let req = TestRequest::with_uri("/app/test/1/path2").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[test] fn test_default_resource() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope @@ -871,20 +856,19 @@ mod tests { .default_resource(|r| r.to(|| HttpResponse::BadRequest())) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/path2").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/path2").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[test] fn test_default_resource_propagation() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app1", |scope| { scope.default_resource(|r| r.to(|| HttpResponse::BadRequest())) @@ -892,18 +876,18 @@ mod tests { .scope("/app2", |scope| scope) .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/non-exist").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); let req = TestRequest::with_uri("/app1/non-exist").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/app2/non-exist").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } } diff --git a/src/test.rs b/src/test.rs index 684817ec1..7ceedacc4 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,4 +1,5 @@ //! Various helpers for Actix applications to use during testing. +use std::cell::RefCell; use std::rc::Rc; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; @@ -6,16 +7,47 @@ use actix_http::http::{HttpTryFrom, Method, Version}; use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{Extensions, PayloadStream, Request}; use actix_router::{Path, Url}; +use actix_rt::Runtime; use bytes::Bytes; +use futures::Future; use crate::request::HttpRequest; use crate::service::{ServiceFromRequest, ServiceRequest}; -/// Test `Request` builder +thread_local! { + static RT: RefCell = { + RefCell::new(Runtime::new().unwrap()) + }; +} + +/// Runs the provided future, blocking the current thread until the future +/// completes. +/// +/// This function can be used to synchronously block the current thread +/// until the provided `future` has resolved either successfully or with an +/// error. The result of the future is then returned from this function +/// call. +/// +/// Note that this function is intended to be used only for testing purpose. +/// This function panics on nested call. +pub fn block_on(f: F) -> Result +where + F: Future, +{ + RT.with(move |rt| rt.borrow_mut().block_on(f)) +} + +/// Test `Request` builder. +/// +/// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern. +/// You can generate various types of request via TestRequest's methods: +/// * `TestRequest::to_request` creates `actix_http::Request` instance. +/// * `TestRequest::to_service` creates `ServiceRequest` instance, which is used for testing middlewares and chain adapters. +/// * `TestRequest::to_from` creates `ServiceFromRequest` instance, which is used for testing extractors. +/// * `TestRequest::to_http_request` creates `HttpRequest` instance, which is used for testing handlers. /// /// ```rust,ignore -/// # use actix_web::*; -/// use actix_web::test::TestRequest; +/// use actix_web::test; /// /// fn index(req: HttpRequest) -> HttpResponse { /// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { @@ -26,12 +58,14 @@ use crate::service::{ServiceFromRequest, ServiceRequest}; /// } /// /// fn main() { -/// let resp = TestRequest::with_header("content-type", "text/plain") -/// .run(&index) -/// .unwrap(); +/// let req = test::TestRequest::with_header("content-type", "text/plain") +/// .to_http_request(); +/// +/// let resp = test::block_on(index(req)); /// assert_eq!(resp.status(), StatusCode::OK); /// -/// let resp = TestRequest::default().run(&index).unwrap(); +/// let req = test::TestRequest::default().to_http_request(); +/// let resp = test::block_on(index(req)); /// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); /// } /// ``` @@ -125,7 +159,7 @@ impl TestRequest { } /// Complete request creation and generate `ServiceRequest` instance - pub fn finish(mut self) -> ServiceRequest { + pub fn to_service(mut self) -> ServiceRequest { let req = self.req.finish(); ServiceRequest::new( @@ -163,4 +197,21 @@ impl TestRequest { ); ServiceFromRequest::new(req, None) } + + /// Runs the provided future, blocking the current thread until the future + /// completes. + /// + /// This function can be used to synchronously block the current thread + /// until the provided `future` has resolved either successfully or with an + /// error. The result of the future is then returned from this function + /// call. + /// + /// Note that this function is intended to be used only for testing purpose. + /// This function panics on nested call. + pub fn block_on(f: F) -> Result + where + F: Future, + { + block_on(f) + } } From a88b3b090d4bea412b48cc6be6713ebc87cf0b8b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 4 Mar 2019 15:58:39 -0800 Subject: [PATCH 185/427] allow to specify service config for h1 service --- src/config.rs | 4 ++-- src/h1/service.rs | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index 960f13706..a9e705c95 100644 --- a/src/config.rs +++ b/src/config.rs @@ -65,7 +65,7 @@ impl Default for ServiceConfig { impl ServiceConfig { /// Create instance of `ServiceConfig` - pub(crate) fn new( + pub fn new( keep_alive: KeepAlive, client_timeout: u64, client_disconnect: u64, @@ -282,7 +282,7 @@ impl ServiceConfigBuilder { } /// Finish service configuration and create `ServiceConfig` object. - pub fn finish(self) -> ServiceConfig { + pub fn finish(&mut self) -> ServiceConfig { ServiceConfig::new(self.keep_alive, self.client_timeout, self.client_disconnect) } } diff --git a/src/h1/service.rs b/src/h1/service.rs index 4beb4c9e4..4c1fb9a82 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -34,7 +34,7 @@ where S::Service: 'static, B: MessageBody, { - /// Create new `HttpService` instance. + /// Create new `HttpService` instance with default config. pub fn new>(service: F) -> Self { let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0); @@ -45,6 +45,15 @@ where } } + /// Create new `HttpService` instance with config. + pub fn with_config>(cfg: ServiceConfig, service: F) -> Self { + H1Service { + cfg, + srv: service.into_new_service(), + _t: PhantomData, + } + } + /// Create builder for `HttpService` instance. pub fn build() -> H1ServiceBuilder { H1ServiceBuilder::new() From 2e79562c9d2d16a376e321ff42e662cdc905d0c4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 4 Mar 2019 16:29:03 -0800 Subject: [PATCH 186/427] add HttpServer type --- Cargo.toml | 20 +- examples/basic.rs | 54 ++--- src/lib.rs | 2 + src/server.rs | 521 +++++++++++++++++++++++++++++++++++++++++ staticfiles/Cargo.toml | 3 +- 5 files changed, 569 insertions(+), 31 deletions(-) create mode 100644 src/server.rs diff --git a/Cargo.toml b/Cargo.toml index 3b88c3001..1bc3af3b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,9 @@ members = [ "staticfiles", ] +[package.metadata.docs.rs] +features = ["ssl", "tls", "rust-tls"] + [features] default = ["brotli", "flate2-c"] @@ -42,6 +45,15 @@ flate2-c = ["flate2/miniz-sys"] # rust backend for flate2 crate flate2-rust = ["flate2/rust_backend"] +# tls +tls = ["native-tls", "actix-server/ssl"] + +# openssl +ssl = ["openssl", "actix-server/ssl"] + +# rustls +# rust-tls = ["rustls", "actix-server/rustls"] + [dependencies] actix-codec = "0.1.0" actix-service = "0.3.0" @@ -50,6 +62,7 @@ actix-rt = "0.1.0" actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } +actix-server = { git = "https://github.com/actix/actix-net.git" } bytes = "0.4" derive_more = "0.14" @@ -58,6 +71,7 @@ futures = "0.1" log = "0.4" lazy_static = "1.2" mime = "0.3" +net2 = "0.2.33" num_cpus = "1.10" parking_lot = "0.7" serde = "1.0" @@ -69,8 +83,12 @@ threadpool = "1.7" brotli2 = { version="^0.3.2", optional = true } flate2 = { version="^1.0.2", optional = true, default-features = false } +# ssl support +native-tls = { version="0.2", optional = true } +openssl = { version="0.10", optional = true } +# rustls = { version = "^0.15", optional = true } + [dev-dependencies] -actix-server = { version="0.3.0", features=["ssl"] } actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } rand = "0.6" diff --git a/examples/basic.rs b/examples/basic.rs index 886efb7b4..3cb07959b 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,8 +1,9 @@ use futures::IntoFuture; -use actix_http::{h1, http::Method, Response}; -use actix_server::Server; -use actix_web::{middleware, web, App, Error, HttpRequest, Resource, Route}; +use actix_web::{ + http::Method, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer, + Resource, Route, +}; fn index(req: HttpRequest) -> &'static str { println!("REQ: {:?}", req); @@ -18,39 +19,34 @@ fn no_params() -> &'static str { "Hello world!\r\n" } -fn main() { +fn main() -> std::io::Result<()> { ::std::env::set_var("RUST_LOG", "actix_server=info,actix_web2=info"); env_logger::init(); let sys = actix_rt::System::new("hello-world"); - Server::build() - .bind("test", "127.0.0.1:8080", || { - h1::H1Service::new( - App::new() + HttpServer::new(|| { + App::new() + .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) + .middleware(middleware::Compress::default()) + .resource("/resource1/index.html", |r| r.route(web::get().to(index))) + .service( + "/resource2/index.html", + Resource::new() .middleware( - middleware::DefaultHeaders::new().header("X-Version", "0.2"), + middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), ) - .middleware(middleware::Compress::default()) - .resource("/resource1/index.html", |r| r.route(web::get().to(index))) - .service( - "/resource2/index.html", - Resource::new() - .middleware( - middleware::DefaultHeaders::new() - .header("X-Version-R2", "0.3"), - ) - .default_resource(|r| { - r.route(Route::new().to(|| Response::MethodNotAllowed())) - }) - .route(web::method(Method::GET).to_async(index_async)), - ) - .service("/test1.html", Resource::new().to(|| "Test\r\n")) - .service("/", Resource::new().to(no_params)), + .default_resource(|r| { + r.route(Route::new().to(|| HttpResponse::MethodNotAllowed())) + }) + .route(web::method(Method::GET).to_async(index_async)), ) - }) - .unwrap() - .workers(1) - .start(); + .service("/test1.html", Resource::new().to(|| "Test\r\n")) + .service("/", Resource::new().to(no_params)) + }) + .bind("127.0.0.1:8080")? + .workers(1) + .start(); let _ = sys.run(); + Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 18a6de067..f21c5e43c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ mod resource; mod responder; mod route; mod scope; +mod server; mod service; mod state; pub mod test; @@ -27,6 +28,7 @@ pub use crate::resource::Resource; pub use crate::responder::{Either, Responder}; pub use crate::route::Route; pub use crate::scope::Scope; +pub use crate::server::HttpServer; pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; pub use crate::state::State; diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 000000000..95cab2b08 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,521 @@ +use std::marker::PhantomData; +use std::sync::Arc; +use std::{fmt, io, net}; + +use actix_http::{body::MessageBody, h1, KeepAlive, Request, Response, ServiceConfig}; +use actix_rt::System; +use actix_server::{Server, ServerBuilder}; +use actix_service::{IntoNewService, NewService}; +use parking_lot::Mutex; + +use net2::TcpBuilder; + +#[cfg(feature = "tls")] +use native_tls::TlsAcceptor; + +#[cfg(feature = "ssl")] +use openssl::ssl::{SslAcceptor, SslAcceptorBuilder}; + +struct Socket { + scheme: &'static str, + addr: net::SocketAddr, +} + +struct Config { + keep_alive: KeepAlive, + client_timeout: u64, + client_shutdown: u64, +} + +/// An HTTP Server. +/// +/// Create new http server with application factory. +/// +/// ```rust +/// use std::io; +/// use actix_web::{App, HttpResponse, HttpServer}; +/// +/// fn main() -> io::Result<()> { +/// let sys = actix_rt::System::new("example"); // <- create Actix runtime +/// +/// HttpServer::new( +/// || App::new() +/// .resource("/", |r| r.to(|| HttpResponse::Ok()))) +/// .bind("127.0.0.1:59090")? +/// .start(); +/// +/// # actix_rt::System::current().stop(); +/// sys.run(); +/// Ok(()) +/// } +/// ``` +pub struct HttpServer +where + F: Fn() -> I + Send + Clone + 'static, + I: IntoNewService, + S: NewService, + S::Error: fmt::Debug, + S::Response: Into>, + S::Service: 'static, + B: MessageBody, +{ + pub(super) factory: F, + pub(super) host: Option, + config: Arc>, + backlog: i32, + sockets: Vec, + builder: Option, + _t: PhantomData<(S, B)>, +} + +impl HttpServer +where + F: Fn() -> I + Send + Clone + 'static, + I: IntoNewService, + S: NewService, + S::Error: fmt::Debug, + S::Response: Into>, + S::Service: 'static, + B: MessageBody, +{ + /// Create new http server with application factory + pub fn new(factory: F) -> Self { + HttpServer { + factory, + host: None, + backlog: 2048, + config: Arc::new(Mutex::new(Config { + keep_alive: KeepAlive::Timeout(5), + client_timeout: 5000, + client_shutdown: 5000, + })), + sockets: Vec::new(), + builder: Some(ServerBuilder::default()), + _t: PhantomData, + } + } + + /// Set number of workers to start. + /// + /// By default http server uses number of available logical cpu as threads + /// count. + pub fn workers(mut self, num: usize) -> Self { + self.builder = Some(self.builder.take().unwrap().workers(num)); + self + } + + /// Set the maximum number of pending connections. + /// + /// This refers to the number of clients that can be waiting to be served. + /// Exceeding this number results in the client getting an error when + /// attempting to connect. It should only affect servers under significant + /// load. + /// + /// Generally set in the 64-2048 range. Default value is 2048. + /// + /// This method should be called before `bind()` method call. + pub fn backlog(mut self, num: i32) -> Self { + self.backlog = num; + self + } + + /// Sets the maximum per-worker number of concurrent connections. + /// + /// All socket listeners will stop accepting connections when this limit is reached + /// for each worker. + /// + /// By default max connections is set to a 25k. + pub fn maxconn(mut self, num: usize) -> Self { + self.builder = Some(self.builder.take().unwrap().maxconn(num)); + self + } + + /// Sets the maximum per-worker concurrent connection establish process. + /// + /// All listeners will stop accepting connections when this limit is reached. It + /// can be used to limit the global SSL CPU usage. + /// + /// By default max connections is set to a 256. + pub fn maxconnrate(mut self, num: usize) -> Self { + self.builder = Some(self.builder.take().unwrap().maxconnrate(num)); + self + } + + /// Set server keep-alive setting. + /// + /// By default keep alive is set to a 5 seconds. + pub fn keep_alive>(self, val: T) -> Self { + self.config.lock().keep_alive = val.into(); + self + } + + /// Set server client timeout in milliseconds for first request. + /// + /// Defines a timeout for reading client request header. If a client does not transmit + /// the entire set headers within this time, the request is terminated with + /// the 408 (Request Time-out) error. + /// + /// To disable timeout set value to 0. + /// + /// By default client timeout is set to 5000 milliseconds. + pub fn client_timeout(self, val: u64) -> Self { + self.config.lock().client_timeout = val; + self + } + + /// Set server connection shutdown timeout in milliseconds. + /// + /// Defines a timeout for shutdown connection. If a shutdown procedure does not complete + /// within this time, the request is dropped. + /// + /// To disable timeout set value to 0. + /// + /// By default client timeout is set to 5000 milliseconds. + pub fn client_shutdown(self, val: u64) -> Self { + self.config.lock().client_shutdown = val; + self + } + + /// Set server host name. + /// + /// Host name is used by application router aa a hostname for url + /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. + /// html#method.host) documentation for more information. + pub fn server_hostname(mut self, val: String) -> Self { + self.host = Some(val); + self + } + + /// Stop actix system. + pub fn system_exit(mut self) -> Self { + self.builder = Some(self.builder.take().unwrap().system_exit()); + self + } + + /// Disable signal handling + pub fn disable_signals(mut self) -> Self { + self.builder = Some(self.builder.take().unwrap().disable_signals()); + self + } + + /// Timeout for graceful workers shutdown. + /// + /// After receiving a stop signal, workers have this much time to finish + /// serving requests. Workers still alive after the timeout are force + /// dropped. + /// + /// By default shutdown timeout sets to 30 seconds. + pub fn shutdown_timeout(mut self, sec: u16) -> Self { + self.builder = Some(self.builder.take().unwrap().shutdown_timeout(sec)); + self + } + + /// Get addresses of bound sockets. + pub fn addrs(&self) -> Vec { + self.sockets.iter().map(|s| s.addr).collect() + } + + /// Get addresses of bound sockets and the scheme for it. + /// + /// This is useful when the server is bound from different sources + /// with some sockets listening on http and some listening on https + /// and the user should be presented with an enumeration of which + /// socket requires which protocol. + pub fn addrs_with_scheme(&self) -> Vec<(net::SocketAddr, &str)> { + self.sockets.iter().map(|s| (s.addr, s.scheme)).collect() + } + + /// Use listener for accepting incoming connection requests + /// + /// HttpServer does not change any configuration for TcpListener, + /// it needs to be configured before passing it to listen() method. + pub fn listen(mut self, lst: net::TcpListener) -> Self { + let cfg = self.config.clone(); + let factory = self.factory.clone(); + let addr = lst.local_addr().unwrap(); + self.sockets.push(Socket { + addr, + scheme: "http", + }); + + self.builder = Some(self.builder.take().unwrap().listen( + format!("actix-web-service-{}", addr), + lst, + move || { + let c = cfg.lock(); + let service_config = + ServiceConfig::new(c.keep_alive, c.client_timeout, 0); + h1::H1Service::with_config(service_config, factory()) + }, + )); + + self + } + + #[cfg(feature = "tls")] + /// Use listener for accepting incoming tls connection requests + /// + /// HttpServer does not change any configuration for TcpListener, + /// it needs to be configured before passing it to listen() method. + pub fn listen_tls(self, lst: net::TcpListener, acceptor: TlsAcceptor) -> Self { + use actix_net::service::NewServiceExt; + + self.listen_with(lst, move || { + ssl::NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) + }) + } + + #[cfg(feature = "ssl")] + /// Use listener for accepting incoming tls connection requests + /// + /// This method sets alpn protocols to "h2" and "http/1.1" + pub fn listen_ssl( + mut self, + lst: net::TcpListener, + builder: SslAcceptorBuilder, + ) -> io::Result { + self.listen_ssl_inner(lst, openssl_acceptor(builder)?); + Ok(self) + } + + #[cfg(feature = "ssl")] + fn listen_ssl_inner(&mut self, lst: net::TcpListener, acceptor: SslAcceptor) { + use actix_server::ssl::{OpensslAcceptor, SslError}; + + let acceptor = OpensslAcceptor::new(acceptor); + let factory = self.factory.clone(); + let cfg = self.config.clone(); + let addr = lst.local_addr().unwrap(); + self.sockets.push(Socket { + addr, + scheme: "http", + }); + + self.builder = Some(self.builder.take().unwrap().listen( + format!("actix-web-service-{}", addr), + lst, + move || { + let c = cfg.lock(); + let service_config = + ServiceConfig::new(c.keep_alive, c.client_timeout, c.client_timeout); + acceptor.clone().map_err(|e| SslError::Ssl(e)).and_then( + h1::H1Service::with_config(service_config, factory()) + .map_err(|e| SslError::Service(e)) + .map_init_err(|_| ()), + ) + }, + )); + } + + #[cfg(feature = "rust-tls")] + /// Use listener for accepting incoming tls connection requests + /// + /// This method sets alpn protocols to "h2" and "http/1.1" + pub fn listen_rustls(self, lst: net::TcpListener, config: ServerConfig) -> Self { + use super::{RustlsAcceptor, ServerFlags}; + use actix_net::service::NewServiceExt; + + self.listen_with(lst, move || { + RustlsAcceptor::with_flags(config.clone(), flags).map_err(|_| ()) + }) + } + + /// The socket address to bind + /// + /// To bind multiple addresses this method can be called multiple times. + pub fn bind(mut self, addr: A) -> io::Result { + let sockets = self.bind2(addr)?; + + for lst in sockets { + self = self.listen(lst); + } + + Ok(self) + } + + fn bind2( + &self, + addr: A, + ) -> io::Result> { + let mut err = None; + let mut succ = false; + let mut sockets = Vec::new(); + for addr in addr.to_socket_addrs()? { + match create_tcp_listener(addr, self.backlog) { + Ok(lst) => { + succ = true; + sockets.push(lst); + } + Err(e) => err = Some(e), + } + } + + if !succ { + if let Some(e) = err.take() { + Err(e) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + "Can not bind to address.", + )) + } + } else { + Ok(sockets) + } + } + + #[cfg(feature = "tls")] + /// The ssl socket address to bind + /// + /// To bind multiple addresses this method can be called multiple times. + pub fn bind_tls( + self, + addr: A, + acceptor: TlsAcceptor, + ) -> io::Result { + use actix_net::service::NewServiceExt; + use actix_net::ssl::NativeTlsAcceptor; + + self.bind_with(addr, move || { + NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) + }) + } + + #[cfg(feature = "ssl")] + /// Start listening for incoming tls connections. + /// + /// This method sets alpn protocols to "h2" and "http/1.1" + pub fn bind_ssl( + mut self, + addr: A, + builder: SslAcceptorBuilder, + ) -> io::Result + where + A: net::ToSocketAddrs, + { + let sockets = self.bind2(addr)?; + let acceptor = openssl_acceptor(builder)?; + + for lst in sockets { + self.listen_ssl_inner(lst, acceptor.clone()); + } + + Ok(self) + } + + #[cfg(feature = "rust-tls")] + /// Start listening for incoming tls connections. + /// + /// This method sets alpn protocols to "h2" and "http/1.1" + pub fn bind_rustls( + self, + addr: A, + builder: ServerConfig, + ) -> io::Result { + use super::{RustlsAcceptor, ServerFlags}; + use actix_net::service::NewServiceExt; + + // alpn support + let flags = if self.no_http2 { + ServerFlags::HTTP1 + } else { + ServerFlags::HTTP1 | ServerFlags::HTTP2 + }; + + self.bind_with(addr, move || { + RustlsAcceptor::with_flags(builder.clone(), flags).map_err(|_| ()) + }) + } +} + +impl HttpServer +where + F: Fn() -> I + Send + Clone + 'static, + I: IntoNewService, + S: NewService, + S::Error: fmt::Debug, + S::Response: Into>, + S::Service: 'static, + B: MessageBody, +{ + /// Start listening for incoming connections. + /// + /// This method starts number of http workers in separate threads. + /// For each address this method starts separate thread which does + /// `accept()` in a loop. + /// + /// This methods panics if no socket address can be bound or an `Actix` system is not yet + /// configured. + /// + /// ```rust + /// use actix_web::{App, HttpResponse, HttpServer}; + /// + /// fn main() { + /// let sys = actix_rt::System::new("example"); // <- create Actix system + /// + /// HttpServer::new(|| App::new().resource("/", |r| r.to(|| HttpResponse::Ok()))) + /// .bind("127.0.0.1:0") + /// .expect("Can not bind to 127.0.0.1:0") + /// .start(); + /// # actix_rt::System::current().stop(); + /// sys.run(); // <- Run actix system, this method starts all async processes + /// } + /// ``` + pub fn start(mut self) -> Server { + self.builder.take().unwrap().start() + } + + /// Spawn new thread and start listening for incoming connections. + /// + /// This method spawns new thread and starts new actix system. Other than + /// that it is similar to `start()` method. This method blocks. + /// + /// This methods panics if no socket addresses get bound. + /// + /// ```rust,ignore + /// use actix_web::{App, HttpResponse, HttpServer}; + /// + /// fn main() { + /// HttpServer::new(|| App::new().resource("/", |r| r.to(|| HttpResponse::Ok()))) + /// .bind("127.0.0.1:0") + /// .expect("Can not bind to 127.0.0.1:0") + /// .run(); + /// } + /// ``` + pub fn run(self) { + let sys = System::new("http-server"); + self.start(); + sys.run(); + } +} + +fn create_tcp_listener( + addr: net::SocketAddr, + backlog: i32, +) -> io::Result { + let builder = match addr { + net::SocketAddr::V4(_) => TcpBuilder::new_v4()?, + net::SocketAddr::V6(_) => TcpBuilder::new_v6()?, + }; + builder.reuse_address(true)?; + builder.bind(addr)?; + Ok(builder.listen(backlog)?) +} + +#[cfg(feature = "ssl")] +/// Configure `SslAcceptorBuilder` with custom server flags. +fn openssl_acceptor(mut builder: SslAcceptorBuilder) -> io::Result { + use openssl::ssl::AlpnError; + + builder.set_alpn_select_callback(|_, protos| { + const H2: &[u8] = b"\x02h2"; + if protos.windows(3).any(|window| window == H2) { + Ok(b"h2") + } else { + Err(AlpnError::NOACK) + } + }); + builder.set_alpn_protos(b"\x08http/1.1\x02h2")?; + + Ok(builder.build()) +} diff --git a/staticfiles/Cargo.toml b/staticfiles/Cargo.toml index c25163025..0aa589701 100644 --- a/staticfiles/Cargo.toml +++ b/staticfiles/Cargo.toml @@ -20,7 +20,7 @@ path = "src/lib.rs" [dependencies] actix-web = { path=".." } actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-service = { git = "https://github.com/actix/actix-net.git" } +actix-service = "0.3.0" bytes = "0.4" futures = "0.1" @@ -34,6 +34,7 @@ v_htmlescape = "0.4" [dev-dependencies] actix-rt = "0.1.0" #actix-server = { version="0.2", features=["ssl"] } +actix-web = { path="..", features=["ssl"] } actix-server = { git = "https://github.com/actix/actix-net.git", features=["ssl"] } actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } From 65a313c78b16272bf7918459111fdb1117ad2ca2 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 4 Mar 2019 19:51:09 -0800 Subject: [PATCH 187/427] update utils dep --- Cargo.toml | 4 ++-- src/client/connector.rs | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 403f3303c..35342b8e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,10 +37,10 @@ session = ["cookie/secure"] ssl = ["openssl", "actix-connector/ssl"] [dependencies] -actix-service = "0.3.0" +actix-service = "0.3.1" actix-codec = "0.1.0" actix-connector = "0.3.0" -actix-utils = "0.3.0" +actix-utils = "0.3.1" base64 = "0.10" backtrace = "0.3" diff --git a/src/client/connector.rs b/src/client/connector.rs index 32ba50121..ccb5dbce5 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -2,7 +2,7 @@ use std::time::Duration; use actix_codec::{AsyncRead, AsyncWrite}; use actix_connector::{Resolver, TcpConnector}; -use actix_service::{Apply, Service, ServiceExt}; +use actix_service::{Service, ServiceExt}; use actix_utils::timeout::{TimeoutError, TimeoutService}; use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; @@ -140,8 +140,8 @@ impl Connector { > + Clone { #[cfg(not(feature = "ssl"))] { - let connector = Apply::new( - TimeoutService::new(self.timeout), + let connector = TimeoutService::new( + self.timeout, self.resolver.map_err(ConnectorError::from).and_then( TcpConnector::default() .from_err() @@ -168,8 +168,8 @@ impl Connector { const H2: &[u8] = b"h2"; use actix_connector::ssl::OpensslConnector; - let ssl_service = Apply::new( - TimeoutService::new(self.timeout), + let ssl_service = TimeoutService::new( + self.timeout, self.resolver .clone() .map_err(ConnectorError::from) @@ -197,8 +197,8 @@ impl Connector { TimeoutError::Timeout => ConnectorError::Timeout, }); - let tcp_service = Apply::new( - TimeoutService::new(self.timeout), + let tcp_service = TimeoutService::new( + self.timeout, self.resolver.map_err(ConnectorError::from).and_then( TcpConnector::default() .from_err() From 3a456ec1489cc83a5a5bee74071d7139be44b8f6 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 4 Mar 2019 20:46:33 -0800 Subject: [PATCH 188/427] update actix-service dependency --- Cargo.toml | 5 +++-- src/h1/service.rs | 6 +++--- src/h2/service.rs | 6 +++--- test-server/Cargo.toml | 7 ++++--- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 35342b8e2..86a9c29cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ session = ["cookie/secure"] ssl = ["openssl", "actix-connector/ssl"] [dependencies] -actix-service = "0.3.1" +actix-service = "0.3.2" actix-codec = "0.1.0" actix-connector = "0.3.0" actix-utils = "0.3.1" @@ -78,7 +78,8 @@ openssl = { version="0.10", optional = true } [dev-dependencies] actix-rt = "0.1.0" -actix-server = { version = "0.3.0", features=["ssl"] } +#actix-server = { version = "0.3.0", features=["ssl"] } +actix-server = { git="https://github.com/actix/actix-net.git", features=["ssl"] } actix-connector = { version = "0.3.0", features=["ssl"] } actix-http-test = { path="test-server", features=["ssl"] } env_logger = "0.6" diff --git a/src/h1/service.rs b/src/h1/service.rs index 4c1fb9a82..acc7217b0 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -6,7 +6,7 @@ use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_service::{IntoNewService, NewService, Service}; use actix_utils::cloneable::CloneableService; use futures::future::{ok, FutureResult}; -use futures::{try_ready, Async, Future, Poll, Stream}; +use futures::{try_ready, Async, Future, IntoFuture, Poll, Stream}; use log::error; use crate::body::MessageBody; @@ -78,7 +78,7 @@ where fn new_service(&self, _: &()) -> Self::Future { H1ServiceResponse { - fut: self.srv.new_service(&()), + fut: self.srv.new_service(&()).into_future(), cfg: Some(self.cfg.clone()), _t: PhantomData, } @@ -216,7 +216,7 @@ where #[doc(hidden)] pub struct H1ServiceResponse { - fut: S::Future, + fut: ::Future, cfg: Option, _t: PhantomData<(T, B)>, } diff --git a/src/h2/service.rs b/src/h2/service.rs index fcfc0be22..583f5edda 100644 --- a/src/h2/service.rs +++ b/src/h2/service.rs @@ -7,7 +7,7 @@ use actix_service::{IntoNewService, NewService, Service}; use actix_utils::cloneable::CloneableService; use bytes::Bytes; use futures::future::{ok, FutureResult}; -use futures::{try_ready, Async, Future, Poll, Stream}; +use futures::{try_ready, Async, Future, IntoFuture, Poll, Stream}; use h2::server::{self, Connection, Handshake}; use h2::RecvStream; use log::error; @@ -72,7 +72,7 @@ where fn new_service(&self, _: &()) -> Self::Future { H2ServiceResponse { - fut: self.srv.new_service(&()), + fut: self.srv.new_service(&()).into_future(), cfg: Some(self.cfg.clone()), _t: PhantomData, } @@ -230,7 +230,7 @@ where #[doc(hidden)] pub struct H2ServiceResponse { - fut: S::Future, + fut: ::Future, cfg: Option, _t: PhantomData<(T, B)>, } diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index cf4364c49..df56b8f3d 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -35,9 +35,10 @@ ssl = ["openssl", "actix-http/ssl", "actix-server/ssl"] actix-codec = "0.1" actix-rt = "0.1.0" actix-http = { path=".." } -actix-service = "0.3.0" -actix-server = "0.3.0" -actix-utils = "0.3.0" +actix-service = "0.3.2" +#actix-server = "0.3.0" +actix-server = { git="https://github.com/actix/actix-net.git" } +actix-utils = "0.3.2" base64 = "0.10" bytes = "0.4" From 42f030d3f49555a90e0d0be83e1e949a17d6f6ac Mon Sep 17 00:00:00 2001 From: Douman Date: Sat, 2 Mar 2019 12:55:31 +0300 Subject: [PATCH 189/427] Ensure that Content-Length zero is specified in empty request --- CHANGES.md | 2 + src/client/writer.rs | 91 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d1d838f43..1a3260286 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,8 @@ * Ignored the `If-Modified-Since` if `If-None-Match` is specified. #680 +* Do not remove `Content-Length` on `Body::Empty` and insert zero value if it is missing for `POST` and `PUT` methods. + ## [0.7.18] - 2019-01-10 ### Added diff --git a/src/client/writer.rs b/src/client/writer.rs index 321753bbf..4091ed212 100644 --- a/src/client/writer.rs +++ b/src/client/writer.rs @@ -16,9 +16,9 @@ use flate2::write::{GzEncoder, ZlibEncoder}; use flate2::Compression; use futures::{Async, Poll}; use http::header::{ - HeaderValue, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, + self, HeaderValue, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, }; -use http::{HttpTryFrom, Version}; +use http::{Method, HttpTryFrom, Version}; use time::{self, Duration}; use tokio_io::AsyncWrite; @@ -223,7 +223,19 @@ fn content_encoder(buf: BytesMut, req: &mut ClientRequest) -> Output { let transfer = match body { Body::Empty => { - req.headers_mut().remove(CONTENT_LENGTH); + match req.method() { + //Insert zero content-length only if user hasn't added it. + //We don't really need it for other methods as they are not supposed to carry payload + &Method::POST | &Method::PUT | &Method::PATCH => { + req.headers_mut() + .entry(CONTENT_LENGTH) + .expect("CONTENT_LENGTH to be valid header name") + .or_insert(header::HeaderValue::from_static("0")); + }, + _ => { + req.headers_mut().remove(CONTENT_LENGTH); + } + } return Output::Empty(buf); } Body::Binary(ref mut bytes) => { @@ -410,3 +422,76 @@ impl CachedDate { self.next_update.nsec = 0; } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_content_encoder_empty_body() { + let mut req = ClientRequest::post("http://google.com").finish().expect("Create request"); + + let result = content_encoder(BytesMut::new(), &mut req); + + match result { + Output::Empty(buf) => { + assert_eq!(buf.len(), 0); + let content_len = req.headers().get(CONTENT_LENGTH).expect("To set Content-Length for empty POST"); + assert_eq!(content_len, "0"); + }, + _ => panic!("Unexpected result, should be Output::Empty"), + } + + req.set_method(Method::GET); + + let result = content_encoder(BytesMut::new(), &mut req); + + match result { + Output::Empty(buf) => { + assert_eq!(buf.len(), 0); + assert!(!req.headers().contains_key(CONTENT_LENGTH)); + }, + _ => panic!("Unexpected result, should be Output::Empty"), + } + + req.set_method(Method::PUT); + + let result = content_encoder(BytesMut::new(), &mut req); + + match result { + Output::Empty(buf) => { + assert_eq!(buf.len(), 0); + let content_len = req.headers().get(CONTENT_LENGTH).expect("To set Content-Length for empty PUT"); + assert_eq!(content_len, "0"); + }, + _ => panic!("Unexpected result, should be Output::Empty"), + } + + req.set_method(Method::DELETE); + + let result = content_encoder(BytesMut::new(), &mut req); + + match result { + Output::Empty(buf) => { + assert_eq!(buf.len(), 0); + assert!(!req.headers().contains_key(CONTENT_LENGTH)); + }, + _ => panic!("Unexpected result, should be Output::Empty"), + } + + req.set_method(Method::PATCH); + + let result = content_encoder(BytesMut::new(), &mut req); + + match result { + Output::Empty(buf) => { + assert_eq!(buf.len(), 0); + let content_len = req.headers().get(CONTENT_LENGTH).expect("To set Content-Length for empty PATCH"); + assert_eq!(content_len, "0"); + }, + _ => panic!("Unexpected result, should be Output::Empty"), + } + + + } +} From b6fe1dacf2c8f547138cbef91c3b8fc4330429c6 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 4 Mar 2019 21:37:57 -0800 Subject: [PATCH 190/427] update middleware impl --- Cargo.toml | 7 +++- src/app.rs | 16 ++++---- src/middleware/compress.rs | 51 +++++++++++++++-------- src/middleware/defaultheaders.rs | 70 ++++++++++++++++++++++---------- src/middleware/mod.rs | 45 -------------------- src/resource.rs | 8 ++-- src/scope.rs | 8 ++-- 7 files changed, 102 insertions(+), 103 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1bc3af3b2..0b4ad38a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,8 +56,8 @@ ssl = ["openssl", "actix-server/ssl"] [dependencies] actix-codec = "0.1.0" -actix-service = "0.3.0" -actix-utils = "0.3.0" +actix-service = "0.3.2" +actix-utils = "0.3.1" actix-rt = "0.1.0" actix-http = { git = "https://github.com/actix/actix-http.git" } @@ -99,3 +99,6 @@ serde_derive = "1.0" lto = true opt-level = 3 codegen-units = 1 + +[patch.crates-io] +actix-service = { git = "https://github.com/actix/actix-net.git" } diff --git a/src/app.rs b/src/app.rs index e1479080a..8336fcca3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,8 +7,8 @@ use actix_http::{Extensions, PayloadStream, Request, Response}; use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ - AndThenNewService, ApplyNewService, IntoNewService, IntoNewTransform, NewService, - NewTransform, Service, + AndThenNewService, ApplyTransform, IntoNewService, IntoTransform, NewService, + Service, Transform, }; use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; @@ -237,17 +237,17 @@ where >, > where - M: NewTransform< + M: Transform< AppRouting

    , Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoNewTransform>, + F: IntoTransform>, { let fref = Rc::new(RefCell::new(None)); - let endpoint = ApplyNewService::new(mw, AppEntry::new(fref.clone())); + let endpoint = ApplyTransform::new(mw, AppEntry::new(fref.clone())); AppRouter { endpoint, chain: self.chain, @@ -480,7 +480,7 @@ where >, > where - M: NewTransform< + M: Transform< T::Service, Request = ServiceRequest

    , Response = ServiceResponse, @@ -488,9 +488,9 @@ where InitError = (), >, B1: MessageBody, - F: IntoNewTransform, + F: IntoTransform, { - let endpoint = ApplyNewService::new(mw, self.endpoint); + let endpoint = ApplyTransform::new(mw, self.endpoint); AppRouter { endpoint, chain: self.chain, diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 5d5586cf7..b95553cb5 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -8,8 +8,9 @@ use actix_http::http::header::{ }; use actix_http::http::{HttpTryFrom, StatusCode}; use actix_http::{Error, Head, ResponseHead}; -use actix_service::{IntoNewTransform, Service, Transform}; +use actix_service::{Service, Transform}; use bytes::{Bytes, BytesMut}; +use futures::future::{ok, FutureResult}; use futures::{Async, Future, Poll}; use log::trace; @@ -18,7 +19,6 @@ use brotli2::write::BrotliEncoder; #[cfg(feature = "flate2")] use flate2::write::{GzEncoder, ZlibEncoder}; -use crate::middleware::MiddlewareFactory; use crate::service::{ServiceRequest, ServiceResponse}; #[derive(Debug, Clone)] @@ -37,6 +37,33 @@ impl Default for Compress { } impl Transform for Compress +where + P: 'static, + B: MessageBody, + S: Service, Response = ServiceResponse>, + S::Future: 'static, +{ + type Request = ServiceRequest

    ; + type Response = ServiceResponse>; + type Error = S::Error; + type InitError = (); + type Transform = CompressMiddleware; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(CompressMiddleware { + service, + encoding: self.0, + }) + } +} + +pub struct CompressMiddleware { + service: S, + encoding: ContentEncoding, +} + +impl Service for CompressMiddleware where P: 'static, B: MessageBody, @@ -49,14 +76,14 @@ where type Future = CompressResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) + self.service.poll_ready() } - fn call(&mut self, req: ServiceRequest

    , srv: &mut S) -> Self::Future { + fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { // negotiate content-encoding let encoding = if let Some(val) = req.headers.get(ACCEPT_ENCODING) { if let Ok(enc) = val.to_str() { - AcceptEncoding::parse(enc, self.0) + AcceptEncoding::parse(enc, self.encoding) } else { ContentEncoding::Identity } @@ -66,7 +93,7 @@ where CompressResponse { encoding, - fut: srv.call(req), + fut: self.service.call(req), } } } @@ -102,18 +129,6 @@ where } } -impl IntoNewTransform, S> for Compress -where - P: 'static, - B: MessageBody, - S: Service, Response = ServiceResponse>, - S::Future: 'static, -{ - fn into_new_transform(self) -> MiddlewareFactory { - MiddlewareFactory::new(self) - } -} - enum EncoderBody { Body(B), Other(Box), diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index 40bf9f1cc..2bd1d5d46 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -3,10 +3,10 @@ use std::rc::Rc; use actix_http::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; use actix_http::http::{HeaderMap, HttpTryFrom}; -use actix_service::{IntoNewTransform, Service, Transform}; -use futures::{Async, Future, Poll}; +use actix_service::{Service, Transform}; +use futures::future::{ok, FutureResult}; +use futures::{Future, Poll}; -use crate::middleware::MiddlewareFactory; use crate::service::{ServiceRequest, ServiceResponse}; /// `Middleware` for setting default response headers. @@ -84,35 +84,49 @@ impl DefaultHeaders { } } -impl IntoNewTransform, S> - for DefaultHeaders +impl Transform for DefaultHeaders where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { - fn into_new_transform(self) -> MiddlewareFactory { - MiddlewareFactory::new(self) + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = S::Error; + type InitError = (); + type Transform = DefaultHeadersMiddleware; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(DefaultHeadersMiddleware { + service, + inner: self.inner.clone(), + }) } } -impl Transform for DefaultHeaders +pub struct DefaultHeadersMiddleware { + service: S, + inner: Rc, +} + +impl Service for DefaultHeadersMiddleware where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { - type Request = ServiceRequest; + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = S::Error; type Future = Box>; fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) + self.service.poll_ready() } - fn call(&mut self, req: ServiceRequest, srv: &mut S) -> Self::Future { + fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { let inner = self.inner.clone(); - Box::new(srv.call(req).map(move |mut res| { + Box::new(self.service.call(req).map(move |mut res| { // set response headers for (key, value) in inner.headers.iter() { if !res.headers().contains_key(key) { @@ -143,32 +157,44 @@ mod tests { #[test] fn test_default_headers() { - let mut mw = DefaultHeaders::new().header(CONTENT_TYPE, "0001"); - let mut srv = FnService::new(|req: ServiceRequest<_>| { + let srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().finish()) }); + let mut mw = block_on( + DefaultHeaders::new() + .header(CONTENT_TYPE, "0001") + .new_transform(srv), + ) + .unwrap(); let req = TestRequest::default().to_service(); - let resp = block_on(mw.call(req, &mut srv)).unwrap(); + let resp = block_on(mw.call(req)).unwrap(); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); let req = TestRequest::default().to_service(); - let mut srv = FnService::new(|req: ServiceRequest<_>| { + let srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish()) }); - let resp = block_on(mw.call(req, &mut srv)).unwrap(); + let mut mw = block_on( + DefaultHeaders::new() + .header(CONTENT_TYPE, "0001") + .new_transform(srv), + ) + .unwrap(); + let resp = block_on(mw.call(req)).unwrap(); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); } #[test] fn test_content_type() { - let mut mw = DefaultHeaders::new().content_type(); - let mut srv = FnService::new(|req: ServiceRequest<_>| { + let srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().finish()) }); + let mut mw = + block_on(DefaultHeaders::new().content_type().new_transform(srv)).unwrap(); let req = TestRequest::default().to_service(); - let resp = block_on(mw.call(req, &mut srv)).unwrap(); + let resp = block_on(mw.call(req)).unwrap(); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), "application/octet-stream" diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 85127ee28..fc9923029 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1,8 +1,3 @@ -use std::marker::PhantomData; - -use actix_service::{NewTransform, Service, Transform}; -use futures::future::{ok, FutureResult}; - #[cfg(any(feature = "brotli", feature = "flate2"))] mod compress; #[cfg(any(feature = "brotli", feature = "flate2"))] @@ -10,43 +5,3 @@ pub use self::compress::Compress; mod defaultheaders; pub use self::defaultheaders::DefaultHeaders; - -/// Helper for middleware service factory -pub struct MiddlewareFactory -where - T: Transform + Clone, - S: Service, -{ - tr: T, - _t: PhantomData, -} - -impl MiddlewareFactory -where - T: Transform + Clone, - S: Service, -{ - pub fn new(tr: T) -> Self { - MiddlewareFactory { - tr, - _t: PhantomData, - } - } -} - -impl NewTransform for MiddlewareFactory -where - T: Transform + Clone, - S: Service, -{ - type Request = T::Request; - type Response = T::Response; - type Error = T::Error; - type Transform = T; - type InitError = (); - type Future = FutureResult; - - fn new_transform(&self, _: &C) -> Self::Future { - ok(self.tr.clone()) - } -} diff --git a/src/resource.rs b/src/resource.rs index 342d801d4..755b8d075 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use actix_http::{Error, Response}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ - ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, + ApplyTransform, IntoNewService, IntoTransform, NewService, Service, Transform, }; use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; @@ -194,16 +194,16 @@ where >, > where - M: NewTransform< + M: Transform< T::Service, Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoNewTransform, + F: IntoTransform, { - let endpoint = ApplyNewService::new(mw, self.endpoint); + let endpoint = ApplyTransform::new(mw, self.endpoint); Resource { endpoint, routes: self.routes, diff --git a/src/scope.rs b/src/scope.rs index 7aeb50412..b255ac14b 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -5,7 +5,7 @@ use actix_http::Response; use actix_router::{ResourceDef, ResourceInfo, Router}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ - ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, + ApplyTransform, IntoNewService, IntoTransform, NewService, Service, Transform, }; use futures::future::{ok, Either, Future, FutureResult}; use futures::{Async, Poll}; @@ -251,16 +251,16 @@ where >, > where - M: NewTransform< + M: Transform< T::Service, Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoNewTransform, + F: IntoTransform, { - let endpoint = ApplyNewService::new(mw, self.endpoint); + let endpoint = ApplyTransform::new(mw, self.endpoint); Scope { endpoint, rdef: self.rdef, From ce0b17259858a88ef7cec46f29053c617701c896 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 09:30:11 -0800 Subject: [PATCH 191/427] update actix-service --- Cargo.toml | 13 +++++++---- src/client/connector.rs | 50 ++++++++++++---------------------------- src/client/pool.rs | 15 +++--------- src/client/request.rs | 2 +- src/h1/dispatcher.rs | 14 +++++------ src/h1/service.rs | 35 ++++++++++++++-------------- src/h2/dispatcher.rs | 18 +++++++++------ src/h2/service.rs | 34 ++++++++++++++------------- src/service/senderror.rs | 12 ++++------ src/ws/client/service.rs | 11 ++++----- src/ws/service.rs | 6 ++--- src/ws/transport.rs | 10 ++++---- test-server/Cargo.toml | 6 +++-- test-server/src/lib.rs | 12 ++++------ 14 files changed, 105 insertions(+), 133 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 86a9c29cd..1705479bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,10 +37,14 @@ session = ["cookie/secure"] ssl = ["openssl", "actix-connector/ssl"] [dependencies] -actix-service = "0.3.2" +#actix-service = "0.3.2" actix-codec = "0.1.0" -actix-connector = "0.3.0" -actix-utils = "0.3.1" +#actix-connector = "0.3.0" +#actix-utils = "0.3.1" + +actix-connector = { git="https://github.com/actix/actix-net.git" } +actix-service = { git="https://github.com/actix/actix-net.git" } +actix-utils = { git="https://github.com/actix/actix-net.git" } base64 = "0.10" backtrace = "0.3" @@ -80,7 +84,8 @@ openssl = { version="0.10", optional = true } actix-rt = "0.1.0" #actix-server = { version = "0.3.0", features=["ssl"] } actix-server = { git="https://github.com/actix/actix-net.git", features=["ssl"] } -actix-connector = { version = "0.3.0", features=["ssl"] } +#actix-connector = { version = "0.3.0", features=["ssl"] } +actix-connector = { git="https://github.com/actix/actix-net.git", features=["ssl"] } actix-http-test = { path="test-server", features=["ssl"] } env_logger = "0.6" serde_derive = "1.0" diff --git a/src/client/connector.rs b/src/client/connector.rs index ccb5dbce5..b35e6af91 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -133,11 +133,8 @@ impl Connector { /// Finish configuration process and create connector service. pub fn service( self, - ) -> impl Service< - Request = Connect, - Response = impl Connection, - Error = ConnectorError, - > + Clone { + ) -> impl Service + Clone + { #[cfg(not(feature = "ssl"))] { let connector = TimeoutService::new( @@ -314,12 +311,12 @@ mod connect_impl { Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, T1: Service< - Request = Connect, + Connect, Response = (Connect, Io1, Protocol), Error = ConnectorError, >, T2: Service< - Request = Connect, + Connect, Response = (Connect, Io2, Protocol), Error = ConnectorError, >, @@ -333,12 +330,12 @@ mod connect_impl { Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, T1: Service< - Request = Connect, + Connect, Response = (Connect, Io1, Protocol), Error = ConnectorError, > + Clone, T2: Service< - Request = Connect, + Connect, Response = (Connect, Io2, Protocol), Error = ConnectorError, > + Clone, @@ -351,22 +348,21 @@ mod connect_impl { } } - impl Service for InnerConnector + impl Service for InnerConnector where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, T1: Service< - Request = Connect, + Connect, Response = (Connect, Io1, Protocol), Error = ConnectorError, >, T2: Service< - Request = Connect, + Connect, Response = (Connect, Io2, Protocol), Error = ConnectorError, >, { - type Request = Connect; type Response = EitherConnection; type Error = ConnectorError; type Future = Either< @@ -401,23 +397,15 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseA where Io1: AsyncRead + AsyncWrite + 'static, - T: Service< - Request = Connect, - Response = (Connect, Io1, Protocol), - Error = ConnectorError, - >, + T: Service, { - fut: as Service>::Future, + fut: as Service>::Future, _t: PhantomData, } impl Future for InnerConnectorResponseA where - T: Service< - Request = Connect, - Response = (Connect, Io1, Protocol), - Error = ConnectorError, - >, + T: Service, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { @@ -435,23 +423,15 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseB where Io2: AsyncRead + AsyncWrite + 'static, - T: Service< - Request = Connect, - Response = (Connect, Io2, Protocol), - Error = ConnectorError, - >, + T: Service, { - fut: as Service>::Future, + fut: as Service>::Future, _t: PhantomData, } impl Future for InnerConnectorResponseB where - T: Service< - Request = Connect, - Response = (Connect, Io2, Protocol), - Error = ConnectorError, - >, + T: Service, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { diff --git a/src/client/pool.rs b/src/client/pool.rs index 188980cb3..425e89395 100644 --- a/src/client/pool.rs +++ b/src/client/pool.rs @@ -48,11 +48,7 @@ pub(crate) struct ConnectionPool( impl ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service< - Request = Connect, - Response = (Connect, Io, Protocol), - Error = ConnectorError, - >, + T: Service, { pub(crate) fn new( connector: T, @@ -88,16 +84,11 @@ where } } -impl Service for ConnectionPool +impl Service for ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service< - Request = Connect, - Response = (Connect, Io, Protocol), - Error = ConnectorError, - >, + T: Service, { - type Request = Connect; type Response = IoConnection; type Error = ConnectorError; type Future = Either< diff --git a/src/client/request.rs b/src/client/request.rs index 7e971756d..637635d0f 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -176,7 +176,7 @@ where ) -> impl Future where B: 'static, - T: Service, + T: Service, I: Connection, { let Self { head, body } = self; diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 9ae8cd2a1..7024ce3af 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -36,14 +36,14 @@ bitflags! { } /// Dispatcher for HTTP/1.1 protocol -pub struct Dispatcher +pub struct Dispatcher + 'static, B: MessageBody> where S::Error: Debug, { inner: Option>, } -struct InnerDispatcher +struct InnerDispatcher + 'static, B: MessageBody> where S::Error: Debug, { @@ -67,13 +67,13 @@ enum DispatcherMessage { Error(Response<()>), } -enum State { +enum State, B: MessageBody> { None, ServiceCall(S::Future), SendPayload(ResponseBody), } -impl State { +impl, B: MessageBody> State { fn is_empty(&self) -> bool { if let State::None = self { true @@ -86,7 +86,7 @@ impl State { impl Dispatcher where T: AsyncRead + AsyncWrite, - S: Service + 'static, + S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody, @@ -141,7 +141,7 @@ where impl InnerDispatcher where T: AsyncRead + AsyncWrite, - S: Service + 'static, + S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody, @@ -464,7 +464,7 @@ where impl Future for Dispatcher where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Error: Debug, S::Response: Into>, B: MessageBody, diff --git a/src/h1/service.rs b/src/h1/service.rs index acc7217b0..1c4f1ae3e 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -28,14 +28,14 @@ pub struct H1Service { impl H1Service where - S: NewService, + S: NewService, S::Error: Debug, S::Response: Into>, S::Service: 'static, B: MessageBody, { /// Create new `HttpService` instance with default config. - pub fn new>(service: F) -> Self { + pub fn new>(service: F) -> Self { let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0); H1Service { @@ -46,7 +46,10 @@ where } /// Create new `HttpService` instance with config. - pub fn with_config>(cfg: ServiceConfig, service: F) -> Self { + pub fn with_config>( + cfg: ServiceConfig, + service: F, + ) -> Self { H1Service { cfg, srv: service.into_new_service(), @@ -60,16 +63,15 @@ where } } -impl NewService for H1Service +impl NewService for H1Service where T: AsyncRead + AsyncWrite, - S: NewService, + S: NewService, S::Error: Debug, S::Response: Into>, S::Service: 'static, B: MessageBody, { - type Request = T; type Response = H1ServiceResult; type Error = DispatchError; type InitError = S::InitError; @@ -101,7 +103,7 @@ pub struct H1ServiceBuilder { impl H1ServiceBuilder where - S: NewService, + S: NewService, S::Error: Debug, { /// Create instance of `ServiceConfigBuilder` @@ -199,7 +201,7 @@ where pub fn finish(self, service: F) -> H1Service where B: MessageBody, - F: IntoNewService, + F: IntoNewService, { let cfg = ServiceConfig::new( self.keep_alive, @@ -215,7 +217,7 @@ where } #[doc(hidden)] -pub struct H1ServiceResponse { +pub struct H1ServiceResponse, B> { fut: ::Future, cfg: Option, _t: PhantomData<(T, B)>, @@ -224,7 +226,7 @@ pub struct H1ServiceResponse { impl Future for H1ServiceResponse where T: AsyncRead + AsyncWrite, - S: NewService, + S: NewService, S::Service: 'static, S::Error: Debug, S::Response: Into>, @@ -251,7 +253,7 @@ pub struct H1ServiceHandler { impl H1ServiceHandler where - S: Service, + S: Service, S::Error: Debug, S::Response: Into>, B: MessageBody, @@ -265,15 +267,14 @@ where } } -impl Service for H1ServiceHandler +impl Service for H1ServiceHandler where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Error: Debug, S::Response: Into>, B: MessageBody, { - type Request = T; type Response = H1ServiceResult; type Error = DispatchError; type Future = Dispatcher; @@ -307,11 +308,10 @@ where } } -impl NewService for OneRequest +impl NewService for OneRequest where T: AsyncRead + AsyncWrite, { - type Request = T; type Response = (Request, Framed); type Error = ParseError; type InitError = (); @@ -333,11 +333,10 @@ pub struct OneRequestService { _t: PhantomData, } -impl Service for OneRequestService +impl Service for OneRequestService where T: AsyncRead + AsyncWrite, { - type Request = T; type Response = (Request, Framed); type Error = ParseError; type Future = OneRequestServiceResponse; diff --git a/src/h2/dispatcher.rs b/src/h2/dispatcher.rs index ea8756d27..7f5409c68 100644 --- a/src/h2/dispatcher.rs +++ b/src/h2/dispatcher.rs @@ -38,7 +38,11 @@ bitflags! { } /// Dispatcher for HTTP/2 protocol -pub struct Dispatcher { +pub struct Dispatcher< + T: AsyncRead + AsyncWrite, + S: Service> + 'static, + B: MessageBody, +> { flags: Flags, service: CloneableService, connection: Connection, @@ -51,7 +55,7 @@ pub struct Dispatcher Dispatcher where T: AsyncRead + AsyncWrite, - S: Service> + 'static, + S: Service> + 'static, S::Error: Into + fmt::Debug, S::Response: Into>, B: MessageBody + 'static, @@ -93,7 +97,7 @@ where impl Future for Dispatcher where T: AsyncRead + AsyncWrite, - S: Service> + 'static, + S: Service> + 'static, S::Error: Into + fmt::Debug, S::Response: Into>, B: MessageBody + 'static, @@ -139,20 +143,20 @@ where } } -struct ServiceResponse { +struct ServiceResponse>, B> { state: ServiceResponseState, config: ServiceConfig, buffer: Option, } -enum ServiceResponseState { +enum ServiceResponseState>, B> { ServiceCall(S::Future, Option>), SendPayload(SendStream, ResponseBody), } impl ServiceResponse where - S: Service> + 'static, + S: Service> + 'static, S::Error: Into + fmt::Debug, S::Response: Into>, B: MessageBody + 'static, @@ -220,7 +224,7 @@ where impl Future for ServiceResponse where - S: Service> + 'static, + S: Service> + 'static, S::Error: Into + fmt::Debug, S::Response: Into>, B: MessageBody + 'static, diff --git a/src/h2/service.rs b/src/h2/service.rs index 583f5edda..e225e9fcb 100644 --- a/src/h2/service.rs +++ b/src/h2/service.rs @@ -31,14 +31,14 @@ pub struct H2Service { impl H2Service where - S: NewService>, + S: NewService>, S::Service: 'static, S::Error: Into + Debug + 'static, S::Response: Into>, B: MessageBody + 'static, { /// Create new `HttpService` instance. - pub fn new>(service: F) -> Self { + pub fn new>>(service: F) -> Self { let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0); H2Service { @@ -54,16 +54,15 @@ where } } -impl NewService for H2Service +impl NewService for H2Service where T: AsyncRead + AsyncWrite, - S: NewService>, + S: NewService>, S::Service: 'static, S::Error: Into + Debug, S::Response: Into>, B: MessageBody + 'static, { - type Request = T; type Response = (); type Error = DispatchError<()>; type InitError = S::InitError; @@ -95,7 +94,7 @@ pub struct H2ServiceBuilder { impl H2ServiceBuilder where - S: NewService>, + S: NewService>, S::Service: 'static, S::Error: Into + Debug + 'static, { @@ -213,7 +212,7 @@ where pub fn finish(self, service: F) -> H2Service where B: MessageBody, - F: IntoNewService, + F: IntoNewService>, { let cfg = ServiceConfig::new( self.keep_alive, @@ -229,7 +228,7 @@ where } #[doc(hidden)] -pub struct H2ServiceResponse { +pub struct H2ServiceResponse>, B> { fut: ::Future, cfg: Option, _t: PhantomData<(T, B)>, @@ -238,7 +237,7 @@ pub struct H2ServiceResponse { impl Future for H2ServiceResponse where T: AsyncRead + AsyncWrite, - S: NewService>, + S: NewService>, S::Service: 'static, S::Response: Into>, S::Error: Into + Debug, @@ -265,7 +264,7 @@ pub struct H2ServiceHandler { impl H2ServiceHandler where - S: Service> + 'static, + S: Service> + 'static, S::Error: Into + Debug, S::Response: Into>, B: MessageBody + 'static, @@ -279,15 +278,14 @@ where } } -impl Service for H2ServiceHandler +impl Service for H2ServiceHandler where T: AsyncRead + AsyncWrite, - S: Service> + 'static, + S: Service> + 'static, S::Error: Into + Debug, S::Response: Into>, B: MessageBody + 'static, { - type Request = T; type Response = (); type Error = DispatchError<()>; type Future = H2ServiceHandlerResponse; @@ -310,7 +308,11 @@ where } } -enum State { +enum State< + T: AsyncRead + AsyncWrite, + S: Service> + 'static, + B: MessageBody, +> { Incoming(Dispatcher), Handshake( Option>, @@ -322,7 +324,7 @@ enum State { pub struct H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite, - S: Service> + 'static, + S: Service> + 'static, S::Error: Into + Debug, S::Response: Into>, B: MessageBody + 'static, @@ -333,7 +335,7 @@ where impl Future for H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite, - S: Service> + 'static, + S: Service> + 'static, S::Error: Into + Debug, S::Response: Into>, B: MessageBody, diff --git a/src/service/senderror.rs b/src/service/senderror.rs index 44d362593..8268c6660 100644 --- a/src/service/senderror.rs +++ b/src/service/senderror.rs @@ -22,12 +22,11 @@ where } } -impl NewService for SendError +impl NewService)>> for SendError where T: AsyncRead + AsyncWrite, E: ResponseError, { - type Request = Result)>; type Response = R; type Error = (E, Framed); type InitError = (); @@ -39,12 +38,11 @@ where } } -impl Service for SendError +impl Service)>> for SendError where T: AsyncRead + AsyncWrite, E: ResponseError, { - type Request = Result)>; type Response = R; type Error = (E, Framed); type Future = Either)>, SendErrorFut>; @@ -130,12 +128,11 @@ where } } -impl NewService for SendResponse +impl NewService<(Response, Framed)> for SendResponse where T: AsyncRead + AsyncWrite, B: MessageBody, { - type Request = (Response, Framed); type Response = Framed; type Error = Error; type InitError = (); @@ -147,12 +144,11 @@ where } } -impl Service for SendResponse +impl Service<(Response, Framed)> for SendResponse where T: AsyncRead + AsyncWrite, B: MessageBody, { - type Request = (Response, Framed); type Response = Framed; type Error = Error; type Future = SendResponseFut; diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index 586873d19..c48b6e0c1 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -26,7 +26,7 @@ pub type DefaultClient = Client; /// WebSocket's client pub struct Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite, { connector: T, @@ -34,7 +34,7 @@ where impl Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite, { /// Create new websocket's client factory @@ -51,7 +51,7 @@ impl Default for Client { impl Clone for Client where - T: Service + Clone, + T: Service + Clone, T::Response: AsyncRead + AsyncWrite, { fn clone(&self) -> Self { @@ -61,13 +61,12 @@ where } } -impl Service for Client +impl Service for Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite + 'static, T::Future: 'static, { - type Request = Connect; type Response = Framed; type Error = ClientError; type Future = Either< diff --git a/src/ws/service.rs b/src/ws/service.rs index f3b066053..bbd9f7826 100644 --- a/src/ws/service.rs +++ b/src/ws/service.rs @@ -20,8 +20,7 @@ impl Default for VerifyWebSockets { } } -impl NewService for VerifyWebSockets { - type Request = (Request, Framed); +impl NewService<(Request, Framed)> for VerifyWebSockets { type Response = (Request, Framed); type Error = (HandshakeError, Framed); type InitError = (); @@ -33,8 +32,7 @@ impl NewService for VerifyWebSockets { } } -impl Service for VerifyWebSockets { - type Request = (Request, Framed); +impl Service<(Request, Framed)> for VerifyWebSockets { type Response = (Request, Framed); type Error = (HandshakeError, Framed); type Future = FutureResult; diff --git a/src/ws/transport.rs b/src/ws/transport.rs index da7782be5..6a4f4d227 100644 --- a/src/ws/transport.rs +++ b/src/ws/transport.rs @@ -7,7 +7,7 @@ use super::{Codec, Frame, Message}; pub struct Transport where - S: Service + 'static, + S: Service + 'static, T: AsyncRead + AsyncWrite, { inner: FramedTransport, @@ -16,17 +16,17 @@ where impl Transport where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Future: 'static, S::Error: 'static, { - pub fn new>(io: T, service: F) -> Self { + pub fn new>(io: T, service: F) -> Self { Transport { inner: FramedTransport::new(Framed::new(io, Codec::new()), service), } } - pub fn with>(framed: Framed, service: F) -> Self { + pub fn with>(framed: Framed, service: F) -> Self { Transport { inner: FramedTransport::new(framed, service), } @@ -36,7 +36,7 @@ where impl Future for Transport where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Future: 'static, S::Error: 'static, { diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index df56b8f3d..6a401cc58 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -35,10 +35,12 @@ ssl = ["openssl", "actix-http/ssl", "actix-server/ssl"] actix-codec = "0.1" actix-rt = "0.1.0" actix-http = { path=".." } -actix-service = "0.3.2" +#actix-service = "0.3.2" +actix-service = { git="https://github.com/actix/actix-net.git" } #actix-server = "0.3.0" actix-server = { git="https://github.com/actix/actix-net.git" } -actix-utils = "0.3.2" +#actix-utils = "0.3.2" +actix-utils = { git="https://github.com/actix/actix-net.git" } base64 = "0.10" bytes = "0.4" diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index a13e86cf8..3afee682f 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -56,8 +56,7 @@ impl TestServer { pub fn new( factory: F, ) -> TestServerRuntime< - impl Service - + Clone, + impl Service + Clone, > { let (tx, rx) = mpsc::channel(); @@ -89,11 +88,8 @@ impl TestServer { } fn new_connector( - ) -> impl Service< - Request = Connect, - Response = impl Connection, - Error = ConnectorError, - > + Clone { + ) -> impl Service + Clone + { #[cfg(feature = "ssl")] { use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; @@ -206,7 +202,7 @@ impl TestServerRuntime { impl TestServerRuntime where - T: Service + Clone, + T: Service + Clone, T::Response: Connection, { /// Connect to websocket server at a given path From 03248028a9a7f140a168fdc9a2797ee81210cec7 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 10:08:08 -0800 Subject: [PATCH 192/427] update actix-service --- Cargo.toml | 15 ++++- src/app.rs | 111 ++++++++++++------------------- src/handler.rs | 18 ++--- src/middleware/compress.rs | 14 ++-- src/middleware/defaultheaders.rs | 10 ++- src/middleware/mod.rs | 3 + src/resource.rs | 30 ++++----- src/route.rs | 47 +++++++------ src/scope.rs | 25 +++---- src/server.rs | 12 ++-- src/service.rs | 6 +- 11 files changed, 132 insertions(+), 159 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0b4ad38a6..f473ac554 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ members = [ ] [package.metadata.docs.rs] -features = ["ssl", "tls", "rust-tls"] +features = ["ssl", "tls", "rust-tls"] #, "session"] [features] default = ["brotli", "flate2-c"] @@ -45,6 +45,9 @@ flate2-c = ["flate2/miniz-sys"] # rust backend for flate2 crate flate2-rust = ["flate2/rust_backend"] +# sessions feature, session require "ring" crate and c compiler +# session = ["actix-session"] + # tls tls = ["native-tls", "actix-server/ssl"] @@ -56,10 +59,13 @@ ssl = ["openssl", "actix-server/ssl"] [dependencies] actix-codec = "0.1.0" -actix-service = "0.3.2" -actix-utils = "0.3.1" +#actix-service = "0.3.2" +#actix-utils = "0.3.1" actix-rt = "0.1.0" +actix-service = { git = "https://github.com/actix/actix-net.git" } +actix-utils = { git = "https://github.com/actix/actix-net.git" } + actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } @@ -79,6 +85,9 @@ serde_json = "1.0" serde_urlencoded = "^0.5.3" threadpool = "1.7" +# middlewares +# actix-session = { path="session", optional = true } + # compression brotli2 = { version="^0.3.2", optional = true } flate2 = { version="^1.0.2", optional = true, default-features = false } diff --git a/src/app.rs b/src/app.rs index 8336fcca3..b21645016 100644 --- a/src/app.rs +++ b/src/app.rs @@ -25,7 +25,7 @@ type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, type BoxedResponse = Box>; pub trait HttpServiceFactory { - type Factory: NewService; + type Factory: NewService; fn rdef(&self) -> &ResourceDef; @@ -36,7 +36,7 @@ pub trait HttpServiceFactory { /// for building application instances. pub struct App where - T: NewService, Response = ServiceRequest

    >, + T: NewService>, { chain: T, extensions: Extensions, @@ -61,7 +61,7 @@ impl App where P: 'static, T: NewService< - Request = ServiceRequest, + ServiceRequest, Response = ServiceRequest

    , Error = (), InitError = (), @@ -197,7 +197,7 @@ where where F: FnOnce(Resource

    ) -> Resource, U: NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -230,7 +230,7 @@ where P, B, impl NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -239,12 +239,12 @@ where where M: Transform< AppRouting

    , - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoTransform>, + F: IntoTransform, ServiceRequest

    >, { let fref = Rc::new(RefCell::new(None)); let endpoint = ApplyTransform::new(mw, AppEntry::new(fref.clone())); @@ -269,7 +269,7 @@ where ) -> App< P1, impl NewService< - Request = ServiceRequest, + ServiceRequest, Response = ServiceRequest, Error = (), InitError = (), @@ -277,13 +277,12 @@ where > where C: NewService< - (), - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceRequest, Error = (), InitError = (), >, - F: IntoNewService, + F: IntoNewService>, { let chain = self.chain.and_then(chain.into_new_service()); App { @@ -326,7 +325,7 @@ where P: 'static, B: MessageBody, T: NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -406,7 +405,7 @@ where where F: FnOnce(Resource

    ) -> Resource, U: NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -430,12 +429,9 @@ where pub fn default_resource(mut self, f: F) -> Self where F: FnOnce(Resource

    ) -> R, - R: IntoNewService, - U: NewService< - Request = ServiceRequest

    , - Response = ServiceResponse, - Error = (), - > + 'static, + R: IntoNewService>, + U: NewService, Response = ServiceResponse, Error = ()> + + 'static, { // create and configure default resource self.default = Some(Rc::new(boxed::new_service( @@ -449,12 +445,9 @@ where pub fn service(mut self, rdef: R, factory: F) -> Self where R: Into, - F: IntoNewService, - U: NewService< - Request = ServiceRequest

    , - Response = ServiceResponse, - Error = (), - > + 'static, + F: IntoNewService>, + U: NewService, Response = ServiceResponse, Error = ()> + + 'static, { self.services.push(( rdef.into(), @@ -473,7 +466,7 @@ where P, B1, impl NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -482,13 +475,13 @@ where where M: Transform< T::Service, - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, B1: MessageBody, - F: IntoTransform, + F: IntoTransform>, { let endpoint = ApplyTransform::new(mw, self.endpoint); AppRouter { @@ -542,22 +535,23 @@ where } impl - IntoNewService, T, ()>> for AppRouter + IntoNewService, T>, Request> + for AppRouter where T: NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, C: NewService< - Request = ServiceRequest, + ServiceRequest, Response = ServiceRequest

    , Error = (), InitError = (), >, { - fn into_new_service(self) -> AndThenNewService, T, ()> { + fn into_new_service(self) -> AndThenNewService, T> { // update resource default service if self.default.is_some() { for default in &self.defaults { @@ -592,8 +586,7 @@ pub struct AppRoutingFactory

    { default: Option>>, } -impl NewService for AppRoutingFactory

    { - type Request = ServiceRequest

    ; +impl NewService> for AppRoutingFactory

    { type Response = ServiceResponse; type Error = (); type InitError = (); @@ -709,8 +702,7 @@ pub struct AppRouting

    { default: Option>, } -impl

    Service for AppRouting

    { - type Request = ServiceRequest

    ; +impl

    Service> for AppRouting

    { type Response = ServiceResponse; type Error = (); type Future = Either>; @@ -758,8 +750,7 @@ impl

    AppEntry

    { } } -impl NewService for AppEntry

    { - type Request = ServiceRequest

    ; +impl NewService> for AppEntry

    { type Response = ServiceResponse; type Error = (); type InitError = (); @@ -774,9 +765,8 @@ impl NewService for AppEntry

    { #[doc(hidden)] pub struct AppChain; -impl NewService<()> for AppChain { - type Request = ServiceRequest; - type Response = ServiceRequest; +impl NewService for AppChain { + type Response = ServiceRequest; type Error = (); type InitError = (); type Service = AppChain; @@ -787,9 +777,8 @@ impl NewService<()> for AppChain { } } -impl Service for AppChain { - type Request = ServiceRequest; - type Response = ServiceRequest; +impl Service for AppChain { + type Response = ServiceRequest; type Error = (); type Future = FutureResult; @@ -799,7 +788,7 @@ impl Service for AppChain { } #[inline] - fn call(&mut self, req: Self::Request) -> Self::Future { + fn call(&mut self, req: ServiceRequest) -> Self::Future { ok(req) } } @@ -808,22 +797,17 @@ impl Service for AppChain { /// It also executes state factories. pub struct AppInit where - C: NewService, Response = ServiceRequest

    >, + C: NewService>, { chain: C, state: Vec>, extensions: Rc>>, } -impl NewService for AppInit +impl NewService for AppInit where - C: NewService< - Request = ServiceRequest, - Response = ServiceRequest

    , - InitError = (), - >, + C: NewService, InitError = ()>, { - type Request = Request; type Response = ServiceRequest

    ; type Error = C::Error; type InitError = C::InitError; @@ -842,11 +826,7 @@ where #[doc(hidden)] pub struct AppInitResult where - C: NewService< - Request = ServiceRequest, - Response = ServiceRequest

    , - InitError = (), - >, + C: NewService, InitError = ()>, { chain: C::Future, state: Vec>, @@ -855,11 +835,7 @@ where impl Future for AppInitResult where - C: NewService< - Request = ServiceRequest, - Response = ServiceRequest

    , - InitError = (), - >, + C: NewService, InitError = ()>, { type Item = AppInitService; type Error = C::InitError; @@ -893,17 +869,16 @@ where /// Service to convert `Request` to a `ServiceRequest` pub struct AppInitService where - C: Service, Response = ServiceRequest

    >, + C: Service>, { chain: C, extensions: Rc, } -impl Service for AppInitService +impl Service for AppInitService where - C: Service, Response = ServiceRequest

    >, + C: Service>, { - type Request = Request; type Response = ServiceRequest

    ; type Error = C::Error; type Future = C::Future; @@ -912,7 +887,7 @@ where self.chain.poll_ready() } - fn call(&mut self, req: Request) -> Self::Future { + fn call(&mut self, req: Request) -> Self::Future { let req = ServiceRequest::new( Path::new(Url::new(req.uri().clone())), req, diff --git a/src/handler.rs b/src/handler.rs index 98a36c562..442dc60d8 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -52,12 +52,11 @@ where } } } -impl NewService for Handle +impl NewService<(T, HttpRequest)> for Handle where F: Factory, R: Responder + 'static, { - type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = Void; type InitError = (); @@ -82,12 +81,11 @@ where _t: PhantomData<(T, R)>, } -impl Service for HandleService +impl Service<(T, HttpRequest)> for HandleService where F: Factory, R: Responder + 'static, { - type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = Void; type Future = HandleServiceResponse<::Future>; @@ -184,14 +182,13 @@ where } } } -impl NewService for AsyncHandle +impl NewService<(T, HttpRequest)> for AsyncHandle where F: AsyncFactory, R: IntoFuture, R::Item: Into, R::Error: Into, { - type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = (); type InitError = (); @@ -218,14 +215,13 @@ where _t: PhantomData<(T, R)>, } -impl Service for AsyncHandleService +impl Service<(T, HttpRequest)> for AsyncHandleService where F: AsyncFactory, R: IntoFuture, R::Item: Into, R::Error: Into, { - type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = (); type Future = AsyncHandleServiceResponse; @@ -290,8 +286,7 @@ impl> Extract { } } -impl> NewService for Extract { - type Request = ServiceRequest

    ; +impl> NewService> for Extract { type Response = (T, HttpRequest); type Error = (Error, ServiceFromRequest

    ); type InitError = (); @@ -311,8 +306,7 @@ pub struct ExtractService> { _t: PhantomData<(P, T)>, } -impl> Service for ExtractService { - type Request = ServiceRequest

    ; +impl> Service> for ExtractService { type Response = (T, HttpRequest); type Error = (Error, ServiceFromRequest

    ); type Future = ExtractResponse; diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index b95553cb5..c6f090a68 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -36,14 +36,13 @@ impl Default for Compress { } } -impl Transform for Compress +impl Transform> for Compress where P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { - type Request = ServiceRequest

    ; type Response = ServiceResponse>; type Error = S::Error; type InitError = (); @@ -63,14 +62,13 @@ pub struct CompressMiddleware { encoding: ContentEncoding, } -impl Service for CompressMiddleware +impl Service> for CompressMiddleware where P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { - type Request = ServiceRequest

    ; type Response = ServiceResponse>; type Error = S::Error; type Future = CompressResponse; @@ -103,7 +101,7 @@ pub struct CompressResponse where P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { fut: S::Future, @@ -114,7 +112,7 @@ impl Future for CompressResponse where P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { type Item = ServiceResponse>; diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index 2bd1d5d46..5fd519195 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -84,12 +84,11 @@ impl DefaultHeaders { } } -impl Transform for DefaultHeaders +impl Transform> for DefaultHeaders where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { - type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = S::Error; type InitError = (); @@ -109,12 +108,11 @@ pub struct DefaultHeadersMiddleware { inner: Rc, } -impl Service for DefaultHeadersMiddleware +impl Service> for DefaultHeadersMiddleware where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { - type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = S::Error; type Future = Box>; diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index fc9923029..8c4cd7543 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -5,3 +5,6 @@ pub use self::compress::Compress; mod defaultheaders; pub use self::defaultheaders::DefaultHeaders; + +#[cfg(feature = "session")] +pub use actix_session as session; diff --git a/src/resource.rs b/src/resource.rs index 755b8d075..8d81ead06 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -65,7 +65,7 @@ impl

    Default for Resource

    { impl Resource where T: NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -187,7 +187,7 @@ where ) -> Resource< P, impl NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -196,12 +196,12 @@ where where M: Transform< T::Service, - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoTransform, + F: IntoTransform>, { let endpoint = ApplyTransform::new(mw, self.endpoint); Resource { @@ -216,12 +216,9 @@ where pub fn default_resource(mut self, f: F) -> Self where F: FnOnce(Resource

    ) -> R, - R: IntoNewService, - U: NewService< - Request = ServiceRequest

    , - Response = ServiceResponse, - Error = (), - > + 'static, + R: IntoNewService>, + U: NewService, Response = ServiceResponse, Error = ()> + + 'static, { // create and configure default resource self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::new_service( @@ -236,10 +233,10 @@ where } } -impl IntoNewService for Resource +impl IntoNewService> for Resource where T: NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -260,8 +257,7 @@ pub struct ResourceFactory

    { default: Rc>>>>, } -impl NewService for ResourceFactory

    { - type Request = ServiceRequest

    ; +impl NewService> for ResourceFactory

    { type Response = ServiceResponse; type Error = (); type InitError = (); @@ -351,8 +347,7 @@ pub struct ResourceService

    { default: Option>, } -impl

    Service for ResourceService

    { - type Request = ServiceRequest

    ; +impl

    Service> for ResourceService

    { type Response = ServiceResponse; type Error = (); type Future = Either< @@ -396,8 +391,7 @@ impl

    ResourceEndpoint

    { } } -impl NewService for ResourceEndpoint

    { - type Request = ServiceRequest

    ; +impl NewService> for ResourceEndpoint

    { type Response = ServiceResponse; type Error = (); type InitError = (); diff --git a/src/route.rs b/src/route.rs index 72abeb324..42e784881 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,4 +1,5 @@ use std::cell::RefCell; +use std::marker::PhantomData; use std::rc::Rc; use actix_http::{http::Method, Error, Extensions, Response}; @@ -14,7 +15,7 @@ use crate::HttpResponse; type BoxedRouteService = Box< Service< - Request = Req, + Req, Response = Res, Error = (), Future = Box>, @@ -23,7 +24,7 @@ type BoxedRouteService = Box< type BoxedRouteNewService = Box< NewService< - Request = Req, + Req, Response = Res, Error = (), InitError = (), @@ -85,8 +86,7 @@ impl Route

    { } } -impl

    NewService for Route

    { - type Request = ServiceRequest

    ; +impl

    NewService> for Route

    { type Response = ServiceResponse; type Error = (); type InitError = (); @@ -141,8 +141,7 @@ impl

    RouteService

    { } } -impl

    Service for RouteService

    { - type Request = ServiceRequest

    ; +impl

    Service> for RouteService

    { type Response = ServiceResponse; type Error = (); type Future = Box>; @@ -151,7 +150,7 @@ impl

    Service for RouteService

    { self.service.poll_ready() } - fn call(&mut self, req: Self::Request) -> Self::Future { + fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { self.service.call(req) } } @@ -348,43 +347,46 @@ impl Route

    { struct RouteNewService where - T: NewService, Error = (Error, ServiceFromRequest

    )>, + T: NewService, Error = (Error, ServiceFromRequest

    )>, { service: T, + _t: PhantomData

    , } impl RouteNewService where T: NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (Error, ServiceFromRequest

    ), >, T::Future: 'static, T::Service: 'static, - ::Future: 'static, + >>::Future: 'static, { pub fn new(service: T) -> Self { - RouteNewService { service } + RouteNewService { + service, + _t: PhantomData, + } } } -impl NewService for RouteNewService +impl NewService> for RouteNewService where T: NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (Error, ServiceFromRequest

    ), >, T::Future: 'static, T::Service: 'static, - ::Future: 'static, + >>::Future: 'static, { - type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type InitError = (); - type Service = BoxedRouteService; + type Service = BoxedRouteService, Self::Response>; type Future = Box>; fn new_service(&self, _: &()) -> Self::Future { @@ -394,27 +396,30 @@ where .map_err(|_| ()) .and_then(|service| { let service: BoxedRouteService<_, _> = - Box::new(RouteServiceWrapper { service }); + Box::new(RouteServiceWrapper { + service, + _t: PhantomData, + }); Ok(service) }), ) } } -struct RouteServiceWrapper>> { +struct RouteServiceWrapper>> { service: T, + _t: PhantomData

    , } -impl Service for RouteServiceWrapper +impl Service> for RouteServiceWrapper where T::Future: 'static, T: Service< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (Error, ServiceFromRequest

    ), >, { - type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type Future = Box>; diff --git a/src/scope.rs b/src/scope.rs index b255ac14b..fa7392b41 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -79,7 +79,7 @@ impl Scope

    { impl Scope where T: NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -196,7 +196,7 @@ where where F: FnOnce(Resource

    ) -> Resource, U: NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -219,7 +219,7 @@ where where F: FnOnce(Resource

    ) -> Resource, U: NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -244,7 +244,7 @@ where ) -> Scope< P, impl NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -253,12 +253,12 @@ where where M: Transform< T::Service, - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoTransform, + F: IntoTransform>, { let endpoint = ApplyTransform::new(mw, self.endpoint); Scope { @@ -293,10 +293,10 @@ pub(crate) fn insert_slash(path: &str) -> String { path } -impl IntoNewService for Scope +impl IntoNewService> for Scope where T: NewService< - Request = ServiceRequest

    , + ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -331,8 +331,7 @@ pub struct ScopeFactory

    { default: Rc>>>>, } -impl NewService for ScopeFactory

    { - type Request = ServiceRequest

    ; +impl NewService> for ScopeFactory

    { type Response = ServiceResponse; type Error = (); type InitError = (); @@ -448,8 +447,7 @@ pub struct ScopeService

    { _ready: Option<(ServiceRequest

    , ResourceInfo)>, } -impl

    Service for ScopeService

    { - type Request = ServiceRequest

    ; +impl

    Service> for ScopeService

    { type Response = ServiceResponse; type Error = (); type Future = Either>; @@ -492,8 +490,7 @@ impl

    ScopeEndpoint

    { } } -impl NewService for ScopeEndpoint

    { - type Request = ServiceRequest

    ; +impl NewService> for ScopeEndpoint

    { type Response = ServiceResponse; type Error = (); type InitError = (); diff --git a/src/server.rs b/src/server.rs index 95cab2b08..78ba692e4 100644 --- a/src/server.rs +++ b/src/server.rs @@ -52,8 +52,8 @@ struct Config { pub struct HttpServer where F: Fn() -> I + Send + Clone + 'static, - I: IntoNewService, - S: NewService, + I: IntoNewService, + S: NewService, S::Error: fmt::Debug, S::Response: Into>, S::Service: 'static, @@ -71,8 +71,8 @@ where impl HttpServer where F: Fn() -> I + Send + Clone + 'static, - I: IntoNewService, - S: NewService, + I: IntoNewService, + S: NewService, S::Error: fmt::Debug, S::Response: Into>, S::Service: 'static, @@ -431,8 +431,8 @@ where impl HttpServer where F: Fn() -> I + Send + Clone + 'static, - I: IntoNewService, - S: NewService, + I: IntoNewService, + S: NewService, S::Error: fmt::Debug, S::Response: Into>, S::Service: 'static, diff --git a/src/service.rs b/src/service.rs index 637a8668a..50b2924ad 100644 --- a/src/service.rs +++ b/src/service.rs @@ -5,15 +5,15 @@ use std::rc::Rc; use actix_http::body::{Body, MessageBody, ResponseBody}; use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{ - Error, Extensions, HttpMessage, Payload, Request, RequestHead, Response, - ResponseHead, + Error, Extensions, HttpMessage, Payload, PayloadStream, Request, RequestHead, + Response, ResponseHead, }; use actix_router::{Path, Resource, Url}; use futures::future::{ok, FutureResult, IntoFuture}; use crate::request::HttpRequest; -pub struct ServiceRequest

    { +pub struct ServiceRequest

    { req: HttpRequest, payload: Payload

    , } From 6457996cf1b404245e23cfff0c0836a8051ce37d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 10:12:49 -0800 Subject: [PATCH 193/427] move session to separate crate --- session/src/lib.rs | 618 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 618 insertions(+) create mode 100644 session/src/lib.rs diff --git a/session/src/lib.rs b/session/src/lib.rs new file mode 100644 index 000000000..0271a13f8 --- /dev/null +++ b/session/src/lib.rs @@ -0,0 +1,618 @@ +//! User sessions. +//! +//! Actix provides a general solution for session management. The +//! [**SessionStorage**](struct.SessionStorage.html) +//! middleware can be used with different backend types to store session +//! data in different backends. +//! +//! By default, only cookie session backend is implemented. Other +//! backend implementations can be added. +//! +//! [**CookieSessionBackend**](struct.CookieSessionBackend.html) +//! uses cookies as session storage. `CookieSessionBackend` creates sessions +//! which are limited to storing fewer than 4000 bytes of data, as the payload +//! must fit into a single cookie. An internal server error is generated if a +//! session contains more than 4000 bytes. +//! +//! A cookie may have a security policy of *signed* or *private*. Each has +//! a respective `CookieSessionBackend` constructor. +//! +//! A *signed* cookie may be viewed but not modified by the client. A *private* +//! cookie may neither be viewed nor modified by the client. +//! +//! The constructors take a key as an argument. This is the private key +//! for cookie session - when this value is changed, all session data is lost. +//! +//! In general, you create a `SessionStorage` middleware and initialize it +//! with specific backend implementation, such as a `CookieSessionBackend`. +//! To access session data, +//! [*HttpRequest::session()*](trait.RequestSession.html#tymethod.session) +//! must be used. This method returns a +//! [*Session*](struct.Session.html) object, which allows us to get or set +//! session data. +//! +//! ```rust +//! # extern crate actix_web; +//! # extern crate actix; +//! use actix_web::{server, App, HttpRequest, Result}; +//! use actix_web::middleware::session::{RequestSession, SessionStorage, CookieSessionBackend}; +//! +//! fn index(req: HttpRequest) -> Result<&'static str> { +//! // access session data +//! if let Some(count) = req.session().get::("counter")? { +//! println!("SESSION value: {}", count); +//! req.session().set("counter", count+1)?; +//! } else { +//! req.session().set("counter", 1)?; +//! } +//! +//! Ok("Welcome!") +//! } +//! +//! fn main() { +//! actix::System::run(|| { +//! server::new( +//! || App::new().middleware( +//! SessionStorage::new( // <- create session middleware +//! CookieSessionBackend::signed(&[0; 32]) // <- create signed cookie session backend +//! .secure(false) +//! ))) +//! .bind("127.0.0.1:59880").unwrap() +//! .start(); +//! # actix::System::current().stop(); +//! }); +//! } +//! ``` +use std::cell::RefCell; +use std::collections::HashMap; +use std::marker::PhantomData; +use std::rc::Rc; +use std::sync::Arc; + +use cookie::{Cookie, CookieJar, Key, SameSite}; +use futures::future::{err as FutErr, ok as FutOk, FutureResult}; +use futures::Future; +use http::header::{self, HeaderValue}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use serde_json; +use serde_json::error::Error as JsonError; +use time::Duration; + +use error::{Error, ResponseError, Result}; +use handler::FromRequest; +use httprequest::HttpRequest; +use httpresponse::HttpResponse; +use middleware::{Middleware, Response, Started}; + +/// The helper trait to obtain your session data from a request. +/// +/// ```rust +/// use actix_web::middleware::session::RequestSession; +/// use actix_web::*; +/// +/// fn index(mut req: HttpRequest) -> Result<&'static str> { +/// // access session data +/// if let Some(count) = req.session().get::("counter")? { +/// req.session().set("counter", count + 1)?; +/// } else { +/// req.session().set("counter", 1)?; +/// } +/// +/// Ok("Welcome!") +/// } +/// # fn main() {} +/// ``` +pub trait RequestSession { + /// Get the session from the request + fn session(&self) -> Session; +} + +impl RequestSession for HttpRequest { + fn session(&self) -> Session { + if let Some(s_impl) = self.extensions().get::>() { + return Session(SessionInner::Session(Arc::clone(&s_impl))); + } + Session(SessionInner::None) + } +} + +/// The high-level interface you use to modify session data. +/// +/// Session object could be obtained with +/// [`RequestSession::session`](trait.RequestSession.html#tymethod.session) +/// method. `RequestSession` trait is implemented for `HttpRequest`. +/// +/// ```rust +/// use actix_web::middleware::session::RequestSession; +/// use actix_web::*; +/// +/// fn index(mut req: HttpRequest) -> Result<&'static str> { +/// // access session data +/// if let Some(count) = req.session().get::("counter")? { +/// req.session().set("counter", count + 1)?; +/// } else { +/// req.session().set("counter", 1)?; +/// } +/// +/// Ok("Welcome!") +/// } +/// # fn main() {} +/// ``` +pub struct Session(SessionInner); + +enum SessionInner { + Session(Arc), + None, +} + +impl Session { + /// Get a `value` from the session. + pub fn get(&self, key: &str) -> Result> { + match self.0 { + SessionInner::Session(ref sess) => { + if let Some(s) = sess.as_ref().0.borrow().get(key) { + Ok(Some(serde_json::from_str(s)?)) + } else { + Ok(None) + } + } + SessionInner::None => Ok(None), + } + } + + /// Set a `value` from the session. + pub fn set(&self, key: &str, value: T) -> Result<()> { + match self.0 { + SessionInner::Session(ref sess) => { + sess.as_ref() + .0 + .borrow_mut() + .set(key, serde_json::to_string(&value)?); + Ok(()) + } + SessionInner::None => Ok(()), + } + } + + /// Remove value from the session. + pub fn remove(&self, key: &str) { + match self.0 { + SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().remove(key), + SessionInner::None => (), + } + } + + /// Clear the session. + pub fn clear(&self) { + match self.0 { + SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().clear(), + SessionInner::None => (), + } + } +} + +/// Extractor implementation for Session type. +/// +/// ```rust +/// # use actix_web::*; +/// use actix_web::middleware::session::Session; +/// +/// fn index(session: Session) -> Result<&'static str> { +/// // access session data +/// if let Some(count) = session.get::("counter")? { +/// session.set("counter", count + 1)?; +/// } else { +/// session.set("counter", 1)?; +/// } +/// +/// Ok("Welcome!") +/// } +/// # fn main() {} +/// ``` +impl FromRequest for Session { + type Config = (); + type Result = Session; + + #[inline] + fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { + req.session() + } +} + +struct SessionImplCell(RefCell>); + +/// Session storage middleware +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::middleware::session::{CookieSessionBackend, SessionStorage}; +/// use actix_web::App; +/// +/// fn main() { +/// let app = App::new().middleware(SessionStorage::new( +/// // <- create session middleware +/// CookieSessionBackend::signed(&[0; 32]) // <- create cookie session backend +/// .secure(false), +/// )); +/// } +/// ``` +pub struct SessionStorage(T, PhantomData); + +impl> SessionStorage { + /// Create session storage + pub fn new(backend: T) -> SessionStorage { + SessionStorage(backend, PhantomData) + } +} + +impl> Middleware for SessionStorage { + fn start(&self, req: &HttpRequest) -> Result { + let mut req = req.clone(); + + let fut = self.0.from_request(&mut req).then(move |res| match res { + Ok(sess) => { + req.extensions_mut() + .insert(Arc::new(SessionImplCell(RefCell::new(Box::new(sess))))); + FutOk(None) + } + Err(err) => FutErr(err), + }); + Ok(Started::Future(Box::new(fut))) + } + + fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { + if let Some(s_box) = req.extensions().get::>() { + s_box.0.borrow_mut().write(resp) + } else { + Ok(Response::Done(resp)) + } + } +} + +/// A simple key-value storage interface that is internally used by `Session`. +pub trait SessionImpl: 'static { + /// Get session value by key + fn get(&self, key: &str) -> Option<&str>; + + /// Set session value + fn set(&mut self, key: &str, value: String); + + /// Remove specific key from session + fn remove(&mut self, key: &str); + + /// Remove all values from session + fn clear(&mut self); + + /// Write session to storage backend. + fn write(&self, resp: HttpResponse) -> Result; +} + +/// Session's storage backend trait definition. +pub trait SessionBackend: Sized + 'static { + /// Session item + type Session: SessionImpl; + /// Future that reads session + type ReadFuture: Future; + + /// Parse the session from request and load data from a storage backend. + fn from_request(&self, request: &mut HttpRequest) -> Self::ReadFuture; +} + +/// Session that uses signed cookies as session storage +pub struct CookieSession { + changed: bool, + state: HashMap, + inner: Rc, +} + +/// Errors that can occur during handling cookie session +#[derive(Fail, Debug)] +pub enum CookieSessionError { + /// Size of the serialized session is greater than 4000 bytes. + #[fail(display = "Size of the serialized session is greater than 4000 bytes.")] + Overflow, + /// Fail to serialize session. + #[fail(display = "Fail to serialize session")] + Serialize(JsonError), +} + +impl ResponseError for CookieSessionError {} + +impl SessionImpl for CookieSession { + fn get(&self, key: &str) -> Option<&str> { + if let Some(s) = self.state.get(key) { + Some(s) + } else { + None + } + } + + fn set(&mut self, key: &str, value: String) { + self.changed = true; + self.state.insert(key.to_owned(), value); + } + + fn remove(&mut self, key: &str) { + self.changed = true; + self.state.remove(key); + } + + fn clear(&mut self) { + self.changed = true; + self.state.clear() + } + + fn write(&self, mut resp: HttpResponse) -> Result { + if self.changed { + let _ = self.inner.set_cookie(&mut resp, &self.state); + } + Ok(Response::Done(resp)) + } +} + +enum CookieSecurity { + Signed, + Private, +} + +struct CookieSessionInner { + key: Key, + security: CookieSecurity, + name: String, + path: String, + domain: Option, + secure: bool, + http_only: bool, + max_age: Option, + same_site: Option, +} + +impl CookieSessionInner { + fn new(key: &[u8], security: CookieSecurity) -> CookieSessionInner { + CookieSessionInner { + security, + key: Key::from_master(key), + name: "actix-session".to_owned(), + path: "/".to_owned(), + domain: None, + secure: true, + http_only: true, + max_age: None, + same_site: None, + } + } + + fn set_cookie( + &self, resp: &mut HttpResponse, state: &HashMap, + ) -> Result<()> { + let value = + serde_json::to_string(&state).map_err(CookieSessionError::Serialize)?; + if value.len() > 4064 { + return Err(CookieSessionError::Overflow.into()); + } + + let mut cookie = Cookie::new(self.name.clone(), value); + cookie.set_path(self.path.clone()); + cookie.set_secure(self.secure); + cookie.set_http_only(self.http_only); + + if let Some(ref domain) = self.domain { + cookie.set_domain(domain.clone()); + } + + if let Some(max_age) = self.max_age { + cookie.set_max_age(max_age); + } + + if let Some(same_site) = self.same_site { + cookie.set_same_site(same_site); + } + + let mut jar = CookieJar::new(); + + match self.security { + CookieSecurity::Signed => jar.signed(&self.key).add(cookie), + CookieSecurity::Private => jar.private(&self.key).add(cookie), + } + + for cookie in jar.delta() { + let val = HeaderValue::from_str(&cookie.encoded().to_string())?; + resp.headers_mut().append(header::SET_COOKIE, val); + } + + Ok(()) + } + + fn load(&self, req: &mut HttpRequest) -> HashMap { + if let Ok(cookies) = req.cookies() { + for cookie in cookies.iter() { + if cookie.name() == self.name { + let mut jar = CookieJar::new(); + jar.add_original(cookie.clone()); + + let cookie_opt = match self.security { + CookieSecurity::Signed => jar.signed(&self.key).get(&self.name), + CookieSecurity::Private => { + jar.private(&self.key).get(&self.name) + } + }; + if let Some(cookie) = cookie_opt { + if let Ok(val) = serde_json::from_str(cookie.value()) { + return val; + } + } + } + } + } + HashMap::new() + } +} + +/// Use cookies for session storage. +/// +/// `CookieSessionBackend` creates sessions which are limited to storing +/// fewer than 4000 bytes of data (as the payload must fit into a single +/// cookie). An Internal Server Error is generated if the session contains more +/// than 4000 bytes. +/// +/// A cookie may have a security policy of *signed* or *private*. Each has a +/// respective `CookieSessionBackend` constructor. +/// +/// A *signed* cookie is stored on the client as plaintext alongside +/// a signature such that the cookie may be viewed but not modified by the +/// client. +/// +/// A *private* cookie is stored on the client as encrypted text +/// such that it may neither be viewed nor modified by the client. +/// +/// The constructors take a key as an argument. +/// This is the private key for cookie session - when this value is changed, +/// all session data is lost. The constructors will panic if the key is less +/// than 32 bytes in length. +/// +/// The backend relies on `cookie` crate to create and read cookies. +/// By default all cookies are percent encoded, but certain symbols may +/// cause troubles when reading cookie, if they are not properly percent encoded. +/// +/// # Example +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::middleware::session::CookieSessionBackend; +/// +/// # fn main() { +/// let backend: CookieSessionBackend = CookieSessionBackend::signed(&[0; 32]) +/// .domain("www.rust-lang.org") +/// .name("actix_session") +/// .path("/") +/// .secure(true); +/// # } +/// ``` +pub struct CookieSessionBackend(Rc); + +impl CookieSessionBackend { + /// Construct new *signed* `CookieSessionBackend` instance. + /// + /// Panics if key length is less than 32 bytes. + pub fn signed(key: &[u8]) -> CookieSessionBackend { + CookieSessionBackend(Rc::new(CookieSessionInner::new( + key, + CookieSecurity::Signed, + ))) + } + + /// Construct new *private* `CookieSessionBackend` instance. + /// + /// Panics if key length is less than 32 bytes. + pub fn private(key: &[u8]) -> CookieSessionBackend { + CookieSessionBackend(Rc::new(CookieSessionInner::new( + key, + CookieSecurity::Private, + ))) + } + + /// Sets the `path` field in the session cookie being built. + pub fn path>(mut self, value: S) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().path = value.into(); + self + } + + /// Sets the `name` field in the session cookie being built. + pub fn name>(mut self, value: S) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().name = value.into(); + self + } + + /// Sets the `domain` field in the session cookie being built. + pub fn domain>(mut self, value: S) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); + self + } + + /// Sets the `secure` field in the session cookie being built. + /// + /// If the `secure` field is set, a cookie will only be transmitted when the + /// connection is secure - i.e. `https` + pub fn secure(mut self, value: bool) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().secure = value; + self + } + + /// Sets the `http_only` field in the session cookie being built. + pub fn http_only(mut self, value: bool) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().http_only = value; + self + } + + /// Sets the `same_site` field in the session cookie being built. + pub fn same_site(mut self, value: SameSite) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().same_site = Some(value); + self + } + + /// Sets the `max-age` field in the session cookie being built. + pub fn max_age(mut self, value: Duration) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); + self + } +} + +impl SessionBackend for CookieSessionBackend { + type Session = CookieSession; + type ReadFuture = FutureResult; + + fn from_request(&self, req: &mut HttpRequest) -> Self::ReadFuture { + let state = self.0.load(req); + FutOk(CookieSession { + changed: false, + inner: Rc::clone(&self.0), + state, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use application::App; + use test; + + #[test] + fn cookie_session() { + let mut srv = test::TestServer::with_factory(|| { + App::new() + .middleware(SessionStorage::new( + CookieSessionBackend::signed(&[0; 32]).secure(false), + )).resource("/", |r| { + r.f(|req| { + let _ = req.session().set("counter", 100); + "test" + }) + }) + }); + + let request = srv.get().uri(srv.url("/")).finish().unwrap(); + let response = srv.execute(request.send()).unwrap(); + assert!(response.cookie("actix-session").is_some()); + } + + #[test] + fn cookie_session_extractor() { + let mut srv = test::TestServer::with_factory(|| { + App::new() + .middleware(SessionStorage::new( + CookieSessionBackend::signed(&[0; 32]).secure(false), + )).resource("/", |r| { + r.with(|ses: Session| { + let _ = ses.set("counter", 100); + "test" + }) + }) + }); + + let request = srv.get().uri(srv.url("/")).finish().unwrap(); + let response = srv.execute(request.send()).unwrap(); + assert!(response.cookie("actix-session").is_some()); + } +} From 01329af1c2c6f5cbeff5631598f772e68c51b65f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 10:18:46 -0800 Subject: [PATCH 194/427] fix non ssl code --- src/client/connector.rs | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/client/connector.rs b/src/client/connector.rs index b35e6af91..d1fd802e9 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -238,11 +238,7 @@ mod connect_impl { pub(crate) struct InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service< - Request = Connect, - Response = (Connect, Io, Protocol), - Error = ConnectorError, - >, + T: Service, { pub(crate) tcp_pool: ConnectionPool, } @@ -250,11 +246,8 @@ mod connect_impl { impl Clone for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service< - Request = Connect, - Response = (Connect, Io, Protocol), - Error = ConnectorError, - > + Clone, + T: Service + + Clone, { fn clone(&self) -> Self { InnerConnector { @@ -263,20 +256,15 @@ mod connect_impl { } } - impl Service for InnerConnector + impl Service for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service< - Request = Connect, - Response = (Connect, Io, Protocol), - Error = ConnectorError, - >, + T: Service, { - type Request = Connect; type Response = IoConnection; type Error = ConnectorError; type Future = Either< - as Service>::Future, + as Service>::Future, FutureResult, ConnectorError>, >; From 96477d42cb0c97660a89a7b9eea1076936712048 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 13:16:26 -0800 Subject: [PATCH 195/427] extend HttpMessage trait, add api to work with requests cookies --- Cargo.toml | 5 +-- src/client/response.rs | 19 +++++++++- src/h1/decoder.rs | 3 +- src/httpmessage.rs | 50 +++++++++++++++++++++++- src/message.rs | 16 ++++++++ src/request.rs | 42 +++++++++------------ src/response.rs | 86 +++++++++++++++++------------------------- src/ws/mod.rs | 1 + 8 files changed, 140 insertions(+), 82 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1705479bd..6ab1b0903 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,10 +28,7 @@ name = "actix_http" path = "src/lib.rs" [features] -default = ["session"] - -# sessions feature, session require "ring" crate and c compiler -session = ["cookie/secure"] +default = [] # openssl ssl = ["openssl", "actix-connector/ssl"] diff --git a/src/client/response.rs b/src/client/response.rs index 104d28ed7..236a63382 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -1,3 +1,4 @@ +use std::cell::{Ref, RefMut}; use std::fmt; use bytes::Bytes; @@ -5,6 +6,7 @@ use futures::{Poll, Stream}; use http::{HeaderMap, StatusCode, Version}; use crate::error::PayloadError; +use crate::extensions::Extensions; use crate::httpmessage::HttpMessage; use crate::message::{Head, Message, ResponseHead}; use crate::payload::{Payload, PayloadStream}; @@ -22,6 +24,18 @@ impl HttpMessage for ClientResponse { &self.head.headers } + fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.head.headers + } + + fn extensions(&self) -> Ref { + self.head.extensions() + } + + fn extensions_mut(&self) -> RefMut { + self.head.extensions_mut() + } + fn take_payload(&mut self) -> Payload { std::mem::replace(&mut self.payload, Payload::None) } @@ -30,8 +44,11 @@ impl HttpMessage for ClientResponse { impl ClientResponse { /// Create new Request instance pub fn new() -> ClientResponse { + let head: Message = Message::new(); + head.extensions_mut().clear(); + ClientResponse { - head: Message::new(), + head, payload: Payload::None, } } diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 80bca94c5..77b76c242 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -163,7 +163,7 @@ impl MessageType for Request { } fn headers_mut(&mut self) -> &mut HeaderMap { - self.headers_mut() + &mut self.head_mut().headers } fn decode(src: &mut BytesMut) -> Result, ParseError> { @@ -832,6 +832,7 @@ mod tests { "GET /test HTTP/1.0\r\n\ connection: close\r\n\r\n", ); + let req = parse_ready!(&mut buf); assert_eq!(req.head().ctype, Some(ConnectionType::Close)); diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 79f29a723..17447af63 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -1,6 +1,8 @@ +use std::cell::{Ref, RefMut}; use std::str; use bytes::{Bytes, BytesMut}; +use cookie::Cookie; use encoding::all::UTF_8; use encoding::label::encoding_from_whatwg_label; use encoding::types::{DecoderTrap, Encoding}; @@ -12,12 +14,16 @@ use serde::de::DeserializeOwned; use serde_urlencoded; use crate::error::{ - ContentTypeError, ParseError, PayloadError, ReadlinesError, UrlencodedError, + ContentTypeError, CookieParseError, ParseError, PayloadError, ReadlinesError, + UrlencodedError, }; +use crate::extensions::Extensions; use crate::header::Header; use crate::json::JsonBody; use crate::payload::Payload; +struct Cookies(Vec>); + /// Trait that implements general purpose operations on http messages pub trait HttpMessage: Sized { /// Type of message payload stream @@ -26,9 +32,18 @@ pub trait HttpMessage: Sized { /// Read the message headers. fn headers(&self) -> &HeaderMap; + /// Mutable reference to the message's headers. + fn headers_mut(&mut self) -> &mut HeaderMap; + /// Message payload stream fn take_payload(&mut self) -> Payload; + /// Request's extensions container + fn extensions(&self) -> Ref; + + /// Mutable reference to a the request's extensions container + fn extensions_mut(&self) -> RefMut; + #[doc(hidden)] /// Get a header fn get_header(&self) -> Option @@ -100,6 +115,39 @@ pub trait HttpMessage: Sized { } } + /// Load request cookies. + #[inline] + fn cookies(&self) -> Result>>, CookieParseError> { + if self.extensions().get::().is_none() { + let mut cookies = Vec::new(); + for hdr in self.headers().get_all(header::COOKIE) { + let s = + str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; + for cookie_str in s.split(';').map(|s| s.trim()) { + if !cookie_str.is_empty() { + cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned()); + } + } + } + self.extensions_mut().insert(Cookies(cookies)); + } + Ok(Ref::map(self.extensions(), |ext| { + &ext.get::().unwrap().0 + })) + } + + /// Return request cookie. + fn cookie(&self, name: &str) -> Option> { + if let Ok(cookies) = self.cookies() { + for cookie in cookies.iter() { + if cookie.name() == name { + return Some(cookie.to_owned()); + } + } + } + None + } + /// Load http message body. /// /// By default only 256Kb payload reads to a memory, then diff --git a/src/message.rs b/src/message.rs index 3a1ac1306..f9dfe9736 100644 --- a/src/message.rs +++ b/src/message.rs @@ -125,6 +125,7 @@ pub struct ResponseHead { pub reason: Option<&'static str>, pub no_chunking: bool, pub(crate) ctype: Option, + pub(crate) extensions: RefCell, } impl Default for ResponseHead { @@ -136,10 +137,25 @@ impl Default for ResponseHead { reason: None, no_chunking: false, ctype: None, + extensions: RefCell::new(Extensions::new()), } } } +impl ResponseHead { + /// Message extensions + #[inline] + pub fn extensions(&self) -> Ref { + self.extensions.borrow() + } + + /// Mutable reference to a the message's extensions + #[inline] + pub fn extensions_mut(&self) -> RefMut { + self.extensions.borrow_mut() + } +} + impl Head for ResponseHead { fn clear(&mut self) { self.ctype = None; diff --git a/src/request.rs b/src/request.rs index e1b893f95..761a159d8 100644 --- a/src/request.rs +++ b/src/request.rs @@ -17,10 +17,28 @@ pub struct Request

    { impl

    HttpMessage for Request

    { type Stream = P; + #[inline] fn headers(&self) -> &HeaderMap { &self.head().headers } + #[inline] + fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.head_mut().headers + } + + /// Request extensions + #[inline] + fn extensions(&self) -> Ref { + self.head.extensions() + } + + /// Mutable reference to a the request's extensions + #[inline] + fn extensions_mut(&self) -> RefMut { + self.head.extensions_mut() + } + fn take_payload(&mut self) -> Payload

    { std::mem::replace(&mut self.payload, Payload::None) } @@ -119,30 +137,6 @@ impl

    Request

    { self.head().uri.path() } - #[inline] - /// Returns Request's headers. - pub fn headers(&self) -> &HeaderMap { - &self.head().headers - } - - #[inline] - /// Returns mutable Request's headers. - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.head_mut().headers - } - - /// Request extensions - #[inline] - pub fn extensions(&self) -> Ref { - self.head.extensions() - } - - /// Mutable reference to a the request's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut { - self.head.extensions_mut() - } - /// Check if request requires connection upgrade pub fn upgrade(&self) -> bool { if let Some(conn) = self.head().headers.get(header::CONNECTION) { diff --git a/src/response.rs b/src/response.rs index 9b503de1b..649e7fd88 100644 --- a/src/response.rs +++ b/src/response.rs @@ -766,12 +766,14 @@ impl From for Response { #[cfg(test)] mod tests { + use time::Duration; + use super::*; use crate::body::Body; use crate::http; use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; - - // use test::TestRequest; + use crate::httpmessage::HttpMessage; + use crate::test::TestRequest; #[test] fn test_debug() { @@ -783,38 +785,39 @@ mod tests { assert!(dbg.contains("Response")); } - // #[test] - // fn test_response_cookies() { - // let req = TestRequest::default() - // .header(COOKIE, "cookie1=value1") - // .header(COOKIE, "cookie2=value2") - // .finish(); - // let cookies = req.cookies().unwrap(); + #[test] + fn test_response_cookies() { + let req = TestRequest::default() + .header(COOKIE, "cookie1=value1") + .header(COOKIE, "cookie2=value2") + .finish(); + let cookies = req.cookies().unwrap(); - // let resp = Response::Ok() - // .cookie( - // http::Cookie::build("name", "value") - // .domain("www.rust-lang.org") - // .path("/test") - // .http_only(true) - // .max_age(Duration::days(1)) - // .finish(), - // ).del_cookie(&cookies[0]) - // .finish(); + let resp = Response::Ok() + .cookie( + http::Cookie::build("name", "value") + .domain("www.rust-lang.org") + .path("/test") + .http_only(true) + .max_age(Duration::days(1)) + .finish(), + ) + .del_cookie(&cookies[0]) + .finish(); - // let mut val: Vec<_> = resp - // .headers() - // .get_all("Set-Cookie") - // .iter() - // .map(|v| v.to_str().unwrap().to_owned()) - // .collect(); - // val.sort(); - // assert!(val[0].starts_with("cookie1=; Max-Age=0;")); - // assert_eq!( - // val[1], - // "name=value; HttpOnly; Path=/test; Domain=www.rust-lang.org; Max-Age=86400" - // ); - // } + let mut val: Vec<_> = resp + .headers() + .get_all("Set-Cookie") + .iter() + .map(|v| v.to_str().unwrap().to_owned()) + .collect(); + val.sort(); + assert!(val[0].starts_with("cookie1=; Max-Age=0;")); + assert_eq!( + val[1], + "name=value; HttpOnly; Path=/test; Domain=www.rust-lang.org; Max-Age=86400" + ); + } #[test] fn test_update_response_cookies() { @@ -871,25 +874,6 @@ mod tests { assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") } - // #[test] - // fn test_content_encoding() { - // let resp = Response::build(StatusCode::OK).finish(); - // assert_eq!(resp.content_encoding(), None); - - // #[cfg(feature = "brotli")] - // { - // let resp = Response::build(StatusCode::OK) - // .content_encoding(ContentEncoding::Br) - // .finish(); - // assert_eq!(resp.content_encoding(), Some(ContentEncoding::Br)); - // } - - // let resp = Response::build(StatusCode::OK) - // .content_encoding(ContentEncoding::Gzip) - // .finish(); - // assert_eq!(resp.content_encoding(), Some(ContentEncoding::Gzip)); - // } - #[test] fn test_json() { let resp = Response::build(StatusCode::OK).json(vec!["v1", "v2", "v3"]); diff --git a/src/ws/mod.rs b/src/ws/mod.rs index 2d629c73b..a8de59dd4 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -9,6 +9,7 @@ use derive_more::{Display, From}; use http::{header, Method, StatusCode}; use crate::error::ResponseError; +use crate::httpmessage::HttpMessage; use crate::request::Request; use crate::response::{Response, ResponseBuilder}; From 200cae19a9e21e2d24e57bd37f434cfce6c36a7d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 14:39:06 -0800 Subject: [PATCH 196/427] add HttpMessage impl &mut T --- src/httpmessage.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 17447af63..d95f82f5d 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -270,6 +270,37 @@ pub trait HttpMessage: Sized { } } +impl<'a, T> HttpMessage for &'a mut T +where + T: HttpMessage, +{ + type Stream = T::Stream; + + fn headers(&self) -> &HeaderMap { + (**self).headers() + } + + /// Mutable reference to the message's headers. + fn headers_mut(&mut self) -> &mut HeaderMap { + (**self).headers_mut() + } + + /// Message payload stream + fn take_payload(&mut self) -> Payload { + (**self).take_payload() + } + + /// Request's extensions container + fn extensions(&self) -> Ref { + (**self).extensions() + } + + /// Mutable reference to a the request's extensions container + fn extensions_mut(&self) -> RefMut { + (**self).extensions_mut() + } +} + /// Stream to read request line by line. pub struct Readlines { stream: Payload, From 0d2116156a0fa90590e0be2e496e9406db8aa3f3 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 17:24:24 -0800 Subject: [PATCH 197/427] Messagebody constraint is not required from Response::into_body --- src/response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/response.rs b/src/response.rs index 649e7fd88..8d32570a5 100644 --- a/src/response.rs +++ b/src/response.rs @@ -80,7 +80,7 @@ impl Response { } /// Convert response to response with body - pub fn into_body(self) -> Response { + pub fn into_body(self) -> Response { let b = match self.body { ResponseBody::Body(b) => b, ResponseBody::Other(b) => b, From 496ee8d0395d8ac7714b6c60d012e6e6283e8422 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 18:14:30 -0800 Subject: [PATCH 198/427] remove more MessageBody constraints from Response --- src/response.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/response.rs b/src/response.rs index 8d32570a5..277890e40 100644 --- a/src/response.rs +++ b/src/response.rs @@ -212,7 +212,7 @@ impl Response { } /// Set a body - pub(crate) fn set_body(self, body: B2) -> Response { + pub(crate) fn set_body(self, body: B2) -> Response { Response { head: self.head, body: ResponseBody::Body(body), @@ -230,10 +230,7 @@ impl Response { } /// Set a body and return previous body value - pub(crate) fn replace_body( - self, - body: B2, - ) -> (Response, ResponseBody) { + pub(crate) fn replace_body(self, body: B2) -> (Response, ResponseBody) { ( Response { head: self.head, @@ -245,7 +242,7 @@ impl Response { } /// Set a body and return previous body value - pub fn map_body(mut self, f: F) -> Response + pub fn map_body(mut self, f: F) -> Response where F: FnOnce(&mut ResponseHead, ResponseBody) -> ResponseBody, { @@ -597,7 +594,7 @@ impl ResponseBuilder { /// Set a body and generate `Response`. /// /// `ResponseBuilder` can not be used after this call. - pub fn message_body(&mut self, body: B) -> Response { + pub fn message_body(&mut self, body: B) -> Response { if let Some(e) = self.err.take() { return Response::from(Error::from(e)).into_body(); } From 143ef87b666559dd6c25ddce04db494040eef809 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 18:47:18 -0800 Subject: [PATCH 199/427] add session and cookie session backend --- Cargo.toml | 1 + session/Cargo.toml | 51 ++++ session/src/cookie.rs | 360 +++++++++++++++++++++++ session/src/lib.rs | 652 +++++++----------------------------------- src/request.rs | 38 +-- src/service.rs | 85 ++++-- src/test.rs | 47 ++- 7 files changed, 646 insertions(+), 588 deletions(-) create mode 100644 session/Cargo.toml create mode 100644 session/src/cookie.rs diff --git a/Cargo.toml b/Cargo.toml index f473ac554..2f69c7ef1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ path = "src/lib.rs" [workspace] members = [ ".", + "session", "staticfiles", ] diff --git a/session/Cargo.toml b/session/Cargo.toml new file mode 100644 index 000000000..3bbeb4f8c --- /dev/null +++ b/session/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "actix-session" +version = "0.1.0" +authors = ["Nikolay Kim "] +description = "Session for actix web framework." +readme = "README.md" +keywords = ["http", "web", "framework", "async", "futures"] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web.git" +documentation = "https://docs.rs/actix-web/" +license = "MIT/Apache-2.0" +exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] +workspace = ".." +edition = "2018" + +[lib] +name = "actix_session" +path = "src/lib.rs" + +[features] +default = ["cookie-session"] + +# sessions feature, session require "ring" crate and c compiler +cookie-session = ["cookie/secure"] + +[dependencies] +actix-web = { path=".." } +actix-codec = "0.1.0" + +#actix-service = "0.3.2" +#actix-utils = "0.3.1" +actix-service = { git = "https://github.com/actix/actix-net.git" } +actix-utils = { git = "https://github.com/actix/actix-net.git" } + +actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-router = { git = "https://github.com/actix/actix-net.git" } +actix-server = { git = "https://github.com/actix/actix-net.git" } + +bytes = "0.4" +cookie = { version="0.11", features=["percent-encode"], optional=true } +derive_more = "0.14" +encoding = "0.2" +futures = "0.1" +hashbrown = "0.1.8" +log = "0.4" +serde = "1.0" +serde_json = "1.0" +time = "0.1" + +[dev-dependencies] +actix-rt = "0.1.0" diff --git a/session/src/cookie.rs b/session/src/cookie.rs new file mode 100644 index 000000000..9cde02e0c --- /dev/null +++ b/session/src/cookie.rs @@ -0,0 +1,360 @@ +//! Cookie session. +//! +//! [**CookieSession**](struct.CookieSession.html) +//! uses cookies as session storage. `CookieSession` creates sessions +//! which are limited to storing fewer than 4000 bytes of data, as the payload +//! must fit into a single cookie. An internal server error is generated if a +//! session contains more than 4000 bytes. +//! +//! A cookie may have a security policy of *signed* or *private*. Each has +//! a respective `CookieSession` constructor. +//! +//! A *signed* cookie may be viewed but not modified by the client. A *private* +//! cookie may neither be viewed nor modified by the client. +//! +//! The constructors take a key as an argument. This is the private key +//! for cookie session - when this value is changed, all session data is lost. + +use std::collections::HashMap; +use std::rc::Rc; + +use actix_service::{Service, Transform}; +use actix_web::http::{header::SET_COOKIE, HeaderValue}; +use actix_web::{Error, HttpMessage, ResponseError, ServiceRequest, ServiceResponse}; +use cookie::{Cookie, CookieJar, Key, SameSite}; +use derive_more::{Display, From}; +use futures::future::{ok, Future, FutureResult}; +use futures::Poll; +use serde_json::error::Error as JsonError; +use time::Duration; + +use crate::Session; + +/// Errors that can occur during handling cookie session +#[derive(Debug, From, Display)] +pub enum CookieSessionError { + /// Size of the serialized session is greater than 4000 bytes. + #[display(fmt = "Size of the serialized session is greater than 4000 bytes.")] + Overflow, + /// Fail to serialize session. + #[display(fmt = "Fail to serialize session")] + Serialize(JsonError), +} + +impl ResponseError for CookieSessionError {} + +enum CookieSecurity { + Signed, + Private, +} + +struct CookieSessionInner { + key: Key, + security: CookieSecurity, + name: String, + path: String, + domain: Option, + secure: bool, + http_only: bool, + max_age: Option, + same_site: Option, +} + +impl CookieSessionInner { + fn new(key: &[u8], security: CookieSecurity) -> CookieSessionInner { + CookieSessionInner { + security, + key: Key::from_master(key), + name: "actix-session".to_owned(), + path: "/".to_owned(), + domain: None, + secure: true, + http_only: true, + max_age: None, + same_site: None, + } + } + + fn set_cookie( + &self, + res: &mut ServiceResponse, + state: impl Iterator, + ) -> Result<(), Error> { + let state: HashMap = state.collect(); + let value = + serde_json::to_string(&state).map_err(CookieSessionError::Serialize)?; + if value.len() > 4064 { + return Err(CookieSessionError::Overflow.into()); + } + + let mut cookie = Cookie::new(self.name.clone(), value); + cookie.set_path(self.path.clone()); + cookie.set_secure(self.secure); + cookie.set_http_only(self.http_only); + + if let Some(ref domain) = self.domain { + cookie.set_domain(domain.clone()); + } + + if let Some(max_age) = self.max_age { + cookie.set_max_age(max_age); + } + + if let Some(same_site) = self.same_site { + cookie.set_same_site(same_site); + } + + let mut jar = CookieJar::new(); + + match self.security { + CookieSecurity::Signed => jar.signed(&self.key).add(cookie), + CookieSecurity::Private => jar.private(&self.key).add(cookie), + } + + for cookie in jar.delta() { + let val = HeaderValue::from_str(&cookie.encoded().to_string())?; + res.headers_mut().append(SET_COOKIE, val); + } + + Ok(()) + } + + fn load

    (&self, req: &ServiceRequest

    ) -> HashMap { + if let Ok(cookies) = req.cookies() { + for cookie in cookies.iter() { + if cookie.name() == self.name { + let mut jar = CookieJar::new(); + jar.add_original(cookie.clone()); + + let cookie_opt = match self.security { + CookieSecurity::Signed => jar.signed(&self.key).get(&self.name), + CookieSecurity::Private => { + jar.private(&self.key).get(&self.name) + } + }; + if let Some(cookie) = cookie_opt { + if let Ok(val) = serde_json::from_str(cookie.value()) { + return val; + } + } + } + } + } + HashMap::new() + } +} + +/// Use cookies for session storage. +/// +/// `CookieSession` creates sessions which are limited to storing +/// fewer than 4000 bytes of data (as the payload must fit into a single +/// cookie). An Internal Server Error is generated if the session contains more +/// than 4000 bytes. +/// +/// A cookie may have a security policy of *signed* or *private*. Each has a +/// respective `CookieSessionBackend` constructor. +/// +/// A *signed* cookie is stored on the client as plaintext alongside +/// a signature such that the cookie may be viewed but not modified by the +/// client. +/// +/// A *private* cookie is stored on the client as encrypted text +/// such that it may neither be viewed nor modified by the client. +/// +/// The constructors take a key as an argument. +/// This is the private key for cookie session - when this value is changed, +/// all session data is lost. The constructors will panic if the key is less +/// than 32 bytes in length. +/// +/// The backend relies on `cookie` crate to create and read cookies. +/// By default all cookies are percent encoded, but certain symbols may +/// cause troubles when reading cookie, if they are not properly percent encoded. +/// +/// # Example +/// +/// ```rust +/// use actix_session::CookieSession; +/// use actix_web::{App, HttpResponse, HttpServer}; +/// +/// fn main() { +/// let app = App::new().middleware( +/// CookieSession::signed(&[0; 32]) +/// .domain("www.rust-lang.org") +/// .name("actix_session") +/// .path("/") +/// .secure(true)) +/// .resource("/", |r| r.to(|| HttpResponse::Ok())); +/// } +/// ``` +pub struct CookieSession(Rc); + +impl CookieSession { + /// Construct new *signed* `CookieSessionBackend` instance. + /// + /// Panics if key length is less than 32 bytes. + pub fn signed(key: &[u8]) -> CookieSession { + CookieSession(Rc::new(CookieSessionInner::new( + key, + CookieSecurity::Signed, + ))) + } + + /// Construct new *private* `CookieSessionBackend` instance. + /// + /// Panics if key length is less than 32 bytes. + pub fn private(key: &[u8]) -> CookieSession { + CookieSession(Rc::new(CookieSessionInner::new( + key, + CookieSecurity::Private, + ))) + } + + /// Sets the `path` field in the session cookie being built. + pub fn path>(mut self, value: S) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().path = value.into(); + self + } + + /// Sets the `name` field in the session cookie being built. + pub fn name>(mut self, value: S) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().name = value.into(); + self + } + + /// Sets the `domain` field in the session cookie being built. + pub fn domain>(mut self, value: S) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); + self + } + + /// Sets the `secure` field in the session cookie being built. + /// + /// If the `secure` field is set, a cookie will only be transmitted when the + /// connection is secure - i.e. `https` + pub fn secure(mut self, value: bool) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().secure = value; + self + } + + /// Sets the `http_only` field in the session cookie being built. + pub fn http_only(mut self, value: bool) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().http_only = value; + self + } + + /// Sets the `same_site` field in the session cookie being built. + pub fn same_site(mut self, value: SameSite) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().same_site = Some(value); + self + } + + /// Sets the `max-age` field in the session cookie being built. + pub fn max_age(mut self, value: Duration) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); + self + } +} + +impl Transform> for CookieSession +where + S: Service, Response = ServiceResponse>, + S::Future: 'static, + S::Error: 'static, +{ + type Response = ServiceResponse; + type Error = S::Error; + type InitError = (); + type Transform = CookieSessionMiddleware; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(CookieSessionMiddleware { + service, + inner: self.0.clone(), + }) + } +} + +/// Cookie session middleware +pub struct CookieSessionMiddleware { + service: S, + inner: Rc, +} + +impl Service> for CookieSessionMiddleware +where + S: Service, Response = ServiceResponse>, + S::Future: 'static, + S::Error: 'static, +{ + type Response = ServiceResponse; + type Error = S::Error; + type Future = Box>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + //self.service.poll_ready().map_err(|e| e.into()) + self.service.poll_ready() + } + + fn call(&mut self, mut req: ServiceRequest

    ) -> Self::Future { + let inner = self.inner.clone(); + let state = self.inner.load(&req); + Session::set_session(state.into_iter(), &mut req); + + Box::new(self.service.call(req).map(move |mut res| { + if let Some(state) = Session::get_changes(&mut res) { + res.checked_expr(|res| inner.set_cookie(res, state)) + } else { + res + } + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use actix_web::{test, App}; + + #[test] + fn cookie_session() { + let mut app = test::init_service( + App::new() + .middleware(CookieSession::signed(&[0; 32]).secure(false)) + .resource("/", |r| { + r.to(|ses: Session| { + let _ = ses.set("counter", 100); + "test" + }) + }), + ); + + let request = test::TestRequest::get().to_request(); + let response = test::block_on(app.call(request)).unwrap(); + assert!(response + .cookies() + .find(|c| c.name() == "actix-session") + .is_some()); + } + + #[test] + fn cookie_session_extractor() { + let mut app = test::init_service( + App::new() + .middleware(CookieSession::signed(&[0; 32]).secure(false)) + .resource("/", |r| { + r.to(|ses: Session| { + let _ = ses.set("counter", 100); + "test" + }) + }), + ); + + let request = test::TestRequest::get().to_request(); + let response = test::block_on(app.call(request)).unwrap(); + assert!(response + .cookies() + .find(|c| c.name() == "actix-session") + .is_some()); + } +} diff --git a/session/src/lib.rs b/session/src/lib.rs index 0271a13f8..f57e11f2f 100644 --- a/session/src/lib.rs +++ b/session/src/lib.rs @@ -1,121 +1,61 @@ //! User sessions. //! -//! Actix provides a general solution for session management. The -//! [**SessionStorage**](struct.SessionStorage.html) -//! middleware can be used with different backend types to store session -//! data in different backends. +//! Actix provides a general solution for session management. Session +//! middlewares could provide different implementations which could +//! be accessed via general session api. //! //! By default, only cookie session backend is implemented. Other //! backend implementations can be added. //! -//! [**CookieSessionBackend**](struct.CookieSessionBackend.html) -//! uses cookies as session storage. `CookieSessionBackend` creates sessions -//! which are limited to storing fewer than 4000 bytes of data, as the payload -//! must fit into a single cookie. An internal server error is generated if a -//! session contains more than 4000 bytes. -//! -//! A cookie may have a security policy of *signed* or *private*. Each has -//! a respective `CookieSessionBackend` constructor. -//! -//! A *signed* cookie may be viewed but not modified by the client. A *private* -//! cookie may neither be viewed nor modified by the client. -//! -//! The constructors take a key as an argument. This is the private key -//! for cookie session - when this value is changed, all session data is lost. -//! -//! In general, you create a `SessionStorage` middleware and initialize it -//! with specific backend implementation, such as a `CookieSessionBackend`. -//! To access session data, -//! [*HttpRequest::session()*](trait.RequestSession.html#tymethod.session) -//! must be used. This method returns a -//! [*Session*](struct.Session.html) object, which allows us to get or set -//! session data. +//! In general, you insert a *session* middleware and initialize it +//! , such as a `CookieSessionBackend`. To access session data, +//! [*Session*](struct.Session.html) extractor must be used. Session +//! extractor allows us to get or set session data. //! //! ```rust -//! # extern crate actix_web; -//! # extern crate actix; -//! use actix_web::{server, App, HttpRequest, Result}; -//! use actix_web::middleware::session::{RequestSession, SessionStorage, CookieSessionBackend}; +//! use actix_web::{App, HttpServer, HttpResponse, Error}; +//! use actix_session::{Session, CookieSession}; //! -//! fn index(req: HttpRequest) -> Result<&'static str> { +//! fn index(session: Session) -> Result<&'static str, Error> { //! // access session data -//! if let Some(count) = req.session().get::("counter")? { +//! if let Some(count) = session.get::("counter")? { //! println!("SESSION value: {}", count); -//! req.session().set("counter", count+1)?; +//! session.set("counter", count+1)?; //! } else { -//! req.session().set("counter", 1)?; +//! session.set("counter", 1)?; //! } //! //! Ok("Welcome!") //! } //! -//! fn main() { -//! actix::System::run(|| { -//! server::new( -//! || App::new().middleware( -//! SessionStorage::new( // <- create session middleware -//! CookieSessionBackend::signed(&[0; 32]) // <- create signed cookie session backend +//! fn main() -> std::io::Result<()> { +//! let sys = actix_rt::System::new("example"); // <- create Actix runtime +//! +//! HttpServer::new( +//! || App::new().middleware( +//! CookieSession::signed(&[0; 32]) // <- create cookie based session middleware //! .secure(false) -//! ))) -//! .bind("127.0.0.1:59880").unwrap() -//! .start(); -//! # actix::System::current().stop(); -//! }); +//! ) +//! .resource("/", |r| r.to(|| HttpResponse::Ok()))) +//! .bind("127.0.0.1:59880")? +//! .start(); +//! # actix_rt::System::current().stop(); +//! sys.run(); +//! Ok(()) //! } //! ``` use std::cell::RefCell; -use std::collections::HashMap; -use std::marker::PhantomData; use std::rc::Rc; -use std::sync::Arc; -use cookie::{Cookie, CookieJar, Key, SameSite}; -use futures::future::{err as FutErr, ok as FutOk, FutureResult}; -use futures::Future; -use http::header::{self, HeaderValue}; +use actix_web::{Error, FromRequest, HttpMessage}; +use actix_web::{ServiceFromRequest, ServiceRequest, ServiceResponse}; +use hashbrown::HashMap; use serde::de::DeserializeOwned; use serde::Serialize; use serde_json; -use serde_json::error::Error as JsonError; -use time::Duration; -use error::{Error, ResponseError, Result}; -use handler::FromRequest; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; - -/// The helper trait to obtain your session data from a request. -/// -/// ```rust -/// use actix_web::middleware::session::RequestSession; -/// use actix_web::*; -/// -/// fn index(mut req: HttpRequest) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = req.session().get::("counter")? { -/// req.session().set("counter", count + 1)?; -/// } else { -/// req.session().set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -pub trait RequestSession { - /// Get the session from the request - fn session(&self) -> Session; -} - -impl RequestSession for HttpRequest { - fn session(&self) -> Session { - if let Some(s_impl) = self.extensions().get::>() { - return Session(SessionInner::Session(Arc::clone(&s_impl))); - } - Session(SessionInner::None) - } -} +mod cookie; +pub use crate::cookie::CookieSession; /// The high-level interface you use to modify session data. /// @@ -124,80 +64,9 @@ impl RequestSession for HttpRequest { /// method. `RequestSession` trait is implemented for `HttpRequest`. /// /// ```rust -/// use actix_web::middleware::session::RequestSession; +/// use actix_session::Session; /// use actix_web::*; /// -/// fn index(mut req: HttpRequest) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = req.session().get::("counter")? { -/// req.session().set("counter", count + 1)?; -/// } else { -/// req.session().set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -pub struct Session(SessionInner); - -enum SessionInner { - Session(Arc), - None, -} - -impl Session { - /// Get a `value` from the session. - pub fn get(&self, key: &str) -> Result> { - match self.0 { - SessionInner::Session(ref sess) => { - if let Some(s) = sess.as_ref().0.borrow().get(key) { - Ok(Some(serde_json::from_str(s)?)) - } else { - Ok(None) - } - } - SessionInner::None => Ok(None), - } - } - - /// Set a `value` from the session. - pub fn set(&self, key: &str, value: T) -> Result<()> { - match self.0 { - SessionInner::Session(ref sess) => { - sess.as_ref() - .0 - .borrow_mut() - .set(key, serde_json::to_string(&value)?); - Ok(()) - } - SessionInner::None => Ok(()), - } - } - - /// Remove value from the session. - pub fn remove(&self, key: &str) { - match self.0 { - SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().remove(key), - SessionInner::None => (), - } - } - - /// Clear the session. - pub fn clear(&self) { - match self.0 { - SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().clear(), - SessionInner::None => (), - } - } -} - -/// Extractor implementation for Session type. -/// -/// ```rust -/// # use actix_web::*; -/// use actix_web::middleware::session::Session; -/// /// fn index(session: Session) -> Result<&'static str> { /// // access session data /// if let Some(count) = session.get::("counter")? { @@ -210,409 +79,108 @@ impl Session { /// } /// # fn main() {} /// ``` -impl FromRequest for Session { - type Config = (); - type Result = Session; +pub struct Session(Rc>); - #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { - req.session() - } +#[derive(Default)] +struct SessionInner { + state: HashMap, + changed: bool, } -struct SessionImplCell(RefCell>); - -/// Session storage middleware -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::session::{CookieSessionBackend, SessionStorage}; -/// use actix_web::App; -/// -/// fn main() { -/// let app = App::new().middleware(SessionStorage::new( -/// // <- create session middleware -/// CookieSessionBackend::signed(&[0; 32]) // <- create cookie session backend -/// .secure(false), -/// )); -/// } -/// ``` -pub struct SessionStorage(T, PhantomData); - -impl> SessionStorage { - /// Create session storage - pub fn new(backend: T) -> SessionStorage { - SessionStorage(backend, PhantomData) - } -} - -impl> Middleware for SessionStorage { - fn start(&self, req: &HttpRequest) -> Result { - let mut req = req.clone(); - - let fut = self.0.from_request(&mut req).then(move |res| match res { - Ok(sess) => { - req.extensions_mut() - .insert(Arc::new(SessionImplCell(RefCell::new(Box::new(sess))))); - FutOk(None) - } - Err(err) => FutErr(err), - }); - Ok(Started::Future(Box::new(fut))) - } - - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(s_box) = req.extensions().get::>() { - s_box.0.borrow_mut().write(resp) +impl Session { + /// Get a `value` from the session. + pub fn get(&self, key: &str) -> Result, Error> { + if let Some(s) = self.0.borrow().state.get(key) { + Ok(Some(serde_json::from_str(s)?)) } else { - Ok(Response::Done(resp)) + Ok(None) } } -} -/// A simple key-value storage interface that is internally used by `Session`. -pub trait SessionImpl: 'static { - /// Get session value by key - fn get(&self, key: &str) -> Option<&str>; + /// Set a `value` from the session. + pub fn set(&self, key: &str, value: T) -> Result<(), Error> { + let mut inner = self.0.borrow_mut(); + inner.changed = true; + inner + .state + .insert(key.to_owned(), serde_json::to_string(&value)?); + Ok(()) + } - /// Set session value - fn set(&mut self, key: &str, value: String); + /// Remove value from the session. + pub fn remove(&self, key: &str) { + let mut inner = self.0.borrow_mut(); + inner.changed = true; + inner.state.remove(key); + } - /// Remove specific key from session - fn remove(&mut self, key: &str); + /// Clear the session. + pub fn clear(&self) { + let mut inner = self.0.borrow_mut(); + inner.changed = true; + inner.state.clear() + } - /// Remove all values from session - fn clear(&mut self); + pub fn set_session

    ( + data: impl Iterator, + req: &mut ServiceRequest

    , + ) { + let session = Session::get_session(req); + let mut inner = session.0.borrow_mut(); + inner.state.extend(data); + } - /// Write session to storage backend. - fn write(&self, resp: HttpResponse) -> Result; -} - -/// Session's storage backend trait definition. -pub trait SessionBackend: Sized + 'static { - /// Session item - type Session: SessionImpl; - /// Future that reads session - type ReadFuture: Future; - - /// Parse the session from request and load data from a storage backend. - fn from_request(&self, request: &mut HttpRequest) -> Self::ReadFuture; -} - -/// Session that uses signed cookies as session storage -pub struct CookieSession { - changed: bool, - state: HashMap, - inner: Rc, -} - -/// Errors that can occur during handling cookie session -#[derive(Fail, Debug)] -pub enum CookieSessionError { - /// Size of the serialized session is greater than 4000 bytes. - #[fail(display = "Size of the serialized session is greater than 4000 bytes.")] - Overflow, - /// Fail to serialize session. - #[fail(display = "Fail to serialize session")] - Serialize(JsonError), -} - -impl ResponseError for CookieSessionError {} - -impl SessionImpl for CookieSession { - fn get(&self, key: &str) -> Option<&str> { - if let Some(s) = self.state.get(key) { - Some(s) + pub fn get_changes( + res: &mut ServiceResponse, + ) -> Option> { + if let Some(s_impl) = res + .request() + .extensions() + .get::>>() + { + let state = + std::mem::replace(&mut s_impl.borrow_mut().state, HashMap::new()); + Some(state.into_iter()) } else { None } } - fn set(&mut self, key: &str, value: String) { - self.changed = true; - self.state.insert(key.to_owned(), value); - } - - fn remove(&mut self, key: &str) { - self.changed = true; - self.state.remove(key); - } - - fn clear(&mut self) { - self.changed = true; - self.state.clear() - } - - fn write(&self, mut resp: HttpResponse) -> Result { - if self.changed { - let _ = self.inner.set_cookie(&mut resp, &self.state); + fn get_session(req: R) -> Session { + if let Some(s_impl) = req.extensions().get::>>() { + return Session(Rc::clone(&s_impl)); } - Ok(Response::Done(resp)) + let inner = Rc::new(RefCell::new(SessionInner::default())); + req.extensions_mut().insert(inner.clone()); + Session(inner) } } -enum CookieSecurity { - Signed, - Private, -} - -struct CookieSessionInner { - key: Key, - security: CookieSecurity, - name: String, - path: String, - domain: Option, - secure: bool, - http_only: bool, - max_age: Option, - same_site: Option, -} - -impl CookieSessionInner { - fn new(key: &[u8], security: CookieSecurity) -> CookieSessionInner { - CookieSessionInner { - security, - key: Key::from_master(key), - name: "actix-session".to_owned(), - path: "/".to_owned(), - domain: None, - secure: true, - http_only: true, - max_age: None, - same_site: None, - } - } - - fn set_cookie( - &self, resp: &mut HttpResponse, state: &HashMap, - ) -> Result<()> { - let value = - serde_json::to_string(&state).map_err(CookieSessionError::Serialize)?; - if value.len() > 4064 { - return Err(CookieSessionError::Overflow.into()); - } - - let mut cookie = Cookie::new(self.name.clone(), value); - cookie.set_path(self.path.clone()); - cookie.set_secure(self.secure); - cookie.set_http_only(self.http_only); - - if let Some(ref domain) = self.domain { - cookie.set_domain(domain.clone()); - } - - if let Some(max_age) = self.max_age { - cookie.set_max_age(max_age); - } - - if let Some(same_site) = self.same_site { - cookie.set_same_site(same_site); - } - - let mut jar = CookieJar::new(); - - match self.security { - CookieSecurity::Signed => jar.signed(&self.key).add(cookie), - CookieSecurity::Private => jar.private(&self.key).add(cookie), - } - - for cookie in jar.delta() { - let val = HeaderValue::from_str(&cookie.encoded().to_string())?; - resp.headers_mut().append(header::SET_COOKIE, val); - } - - Ok(()) - } - - fn load(&self, req: &mut HttpRequest) -> HashMap { - if let Ok(cookies) = req.cookies() { - for cookie in cookies.iter() { - if cookie.name() == self.name { - let mut jar = CookieJar::new(); - jar.add_original(cookie.clone()); - - let cookie_opt = match self.security { - CookieSecurity::Signed => jar.signed(&self.key).get(&self.name), - CookieSecurity::Private => { - jar.private(&self.key).get(&self.name) - } - }; - if let Some(cookie) = cookie_opt { - if let Ok(val) = serde_json::from_str(cookie.value()) { - return val; - } - } - } - } - } - HashMap::new() - } -} - -/// Use cookies for session storage. -/// -/// `CookieSessionBackend` creates sessions which are limited to storing -/// fewer than 4000 bytes of data (as the payload must fit into a single -/// cookie). An Internal Server Error is generated if the session contains more -/// than 4000 bytes. -/// -/// A cookie may have a security policy of *signed* or *private*. Each has a -/// respective `CookieSessionBackend` constructor. -/// -/// A *signed* cookie is stored on the client as plaintext alongside -/// a signature such that the cookie may be viewed but not modified by the -/// client. -/// -/// A *private* cookie is stored on the client as encrypted text -/// such that it may neither be viewed nor modified by the client. -/// -/// The constructors take a key as an argument. -/// This is the private key for cookie session - when this value is changed, -/// all session data is lost. The constructors will panic if the key is less -/// than 32 bytes in length. -/// -/// The backend relies on `cookie` crate to create and read cookies. -/// By default all cookies are percent encoded, but certain symbols may -/// cause troubles when reading cookie, if they are not properly percent encoded. -/// -/// # Example +/// Extractor implementation for Session type. /// /// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::session::CookieSessionBackend; +/// # use actix_web::*; +/// use actix_session::Session; /// -/// # fn main() { -/// let backend: CookieSessionBackend = CookieSessionBackend::signed(&[0; 32]) -/// .domain("www.rust-lang.org") -/// .name("actix_session") -/// .path("/") -/// .secure(true); -/// # } +/// fn index(session: Session) -> Result<&'static str> { +/// // access session data +/// if let Some(count) = session.get::("counter")? { +/// session.set("counter", count + 1)?; +/// } else { +/// session.set("counter", 1)?; +/// } +/// +/// Ok("Welcome!") +/// } +/// # fn main() {} /// ``` -pub struct CookieSessionBackend(Rc); +impl

    FromRequest

    for Session { + type Error = Error; + type Future = Result; + type Config = (); -impl CookieSessionBackend { - /// Construct new *signed* `CookieSessionBackend` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn signed(key: &[u8]) -> CookieSessionBackend { - CookieSessionBackend(Rc::new(CookieSessionInner::new( - key, - CookieSecurity::Signed, - ))) - } - - /// Construct new *private* `CookieSessionBackend` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn private(key: &[u8]) -> CookieSessionBackend { - CookieSessionBackend(Rc::new(CookieSessionInner::new( - key, - CookieSecurity::Private, - ))) - } - - /// Sets the `path` field in the session cookie being built. - pub fn path>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().path = value.into(); - self - } - - /// Sets the `name` field in the session cookie being built. - pub fn name>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().name = value.into(); - self - } - - /// Sets the `domain` field in the session cookie being built. - pub fn domain>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); - self - } - - /// Sets the `secure` field in the session cookie being built. - /// - /// If the `secure` field is set, a cookie will only be transmitted when the - /// connection is secure - i.e. `https` - pub fn secure(mut self, value: bool) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().secure = value; - self - } - - /// Sets the `http_only` field in the session cookie being built. - pub fn http_only(mut self, value: bool) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().http_only = value; - self - } - - /// Sets the `same_site` field in the session cookie being built. - pub fn same_site(mut self, value: SameSite) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().same_site = Some(value); - self - } - - /// Sets the `max-age` field in the session cookie being built. - pub fn max_age(mut self, value: Duration) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); - self - } -} - -impl SessionBackend for CookieSessionBackend { - type Session = CookieSession; - type ReadFuture = FutureResult; - - fn from_request(&self, req: &mut HttpRequest) -> Self::ReadFuture { - let state = self.0.load(req); - FutOk(CookieSession { - changed: false, - inner: Rc::clone(&self.0), - state, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use application::App; - use test; - - #[test] - fn cookie_session() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/", |r| { - r.f(|req| { - let _ = req.session().set("counter", 100); - "test" - }) - }) - }); - - let request = srv.get().uri(srv.url("/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.cookie("actix-session").is_some()); - } - - #[test] - fn cookie_session_extractor() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/", |r| { - r.with(|ses: Session| { - let _ = ses.set("counter", 100); - "test" - }) - }) - }); - - let request = srv.get().uri(srv.url("/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.cookie("actix-session").is_some()); + #[inline] + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + Ok(Session::get_session(req)) } } diff --git a/src/request.rs b/src/request.rs index d90627f52..75daf59d8 100644 --- a/src/request.rs +++ b/src/request.rs @@ -64,12 +64,6 @@ impl HttpRequest { self.head().uri.path() } - #[inline] - /// Returns Request's headers. - pub fn headers(&self) -> &HeaderMap { - &self.head().headers - } - /// The query string in the URL. /// /// E.g., id=10 @@ -93,18 +87,6 @@ impl HttpRequest { &self.path } - /// Request extensions - #[inline] - pub fn extensions(&self) -> Ref { - self.head.extensions() - } - - /// Mutable reference to a the request's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut { - self.head.extensions_mut() - } - /// Application extensions #[inline] pub fn app_extensions(&self) -> &Extensions { @@ -130,8 +112,26 @@ impl HttpMessage for HttpRequest { type Stream = (); #[inline] + /// Returns Request's headers. fn headers(&self) -> &HeaderMap { - self.headers() + &self.head().headers + } + + #[inline] + fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.head.headers + } + + /// Request extensions + #[inline] + fn extensions(&self) -> Ref { + self.head.extensions() + } + + /// Mutable reference to a the request's extensions + #[inline] + fn extensions_mut(&self) -> RefMut { + self.head.extensions_mut() } #[inline] diff --git a/src/service.rs b/src/service.rs index 50b2924ad..0da664396 100644 --- a/src/service.rs +++ b/src/service.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use std::cell::{Ref, RefMut}; use std::rc::Rc; -use actix_http::body::{Body, MessageBody, ResponseBody}; +use actix_http::body::{Body, ResponseBody}; use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{ Error, Extensions, HttpMessage, Payload, PayloadStream, Request, RequestHead, @@ -84,12 +84,6 @@ impl

    ServiceRequest

    { self.head().uri.path() } - #[inline] - /// Returns Request's headers. - pub fn headers(&self) -> &HeaderMap { - &self.head().headers - } - /// The query string in the URL. /// /// E.g., id=10 @@ -118,18 +112,6 @@ impl

    ServiceRequest

    { &mut self.req.path } - /// Request extensions - #[inline] - pub fn extensions(&self) -> Ref { - self.req.head.extensions() - } - - /// Mutable reference to a the request's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut { - self.req.head.extensions_mut() - } - /// Application extensions #[inline] pub fn app_extensions(&self) -> &Extensions { @@ -147,8 +129,27 @@ impl

    HttpMessage for ServiceRequest

    { type Stream = P; #[inline] + /// Returns Request's headers. fn headers(&self) -> &HeaderMap { - self.req.headers() + &self.head().headers + } + + #[inline] + /// Mutable reference to the request's headers. + fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.head_mut().headers + } + + /// Request extensions + #[inline] + fn extensions(&self) -> Ref { + self.req.head.extensions() + } + + /// Mutable reference to a the request's extensions + #[inline] + fn extensions_mut(&self) -> RefMut { + self.req.head.extensions_mut() } #[inline] @@ -229,6 +230,23 @@ impl

    HttpMessage for ServiceFromRequest

    { self.req.headers() } + #[inline] + fn headers_mut(&mut self) -> &mut HeaderMap { + self.req.headers_mut() + } + + /// Request extensions + #[inline] + fn extensions(&self) -> Ref { + self.req.head.extensions() + } + + /// Mutable reference to a the request's extensions + #[inline] + fn extensions_mut(&self) -> RefMut { + self.req.head.extensions_mut() + } + #[inline] fn take_payload(&mut self) -> Payload { std::mem::replace(&mut self.payload, Payload::None) @@ -275,11 +293,26 @@ impl ServiceResponse { pub fn headers_mut(&mut self) -> &mut HeaderMap { self.response.headers_mut() } + + /// Execute closure and in case of error convert it to response. + pub fn checked_expr(mut self, f: F) -> Self + where + F: FnOnce(&mut Self) -> Result<(), E>, + E: Into, + { + match f(&mut self) { + Ok(_) => self, + Err(err) => { + let res: Response = err.into().into(); + ServiceResponse::new(self.request, res.into_body()) + } + } + } } -impl ServiceResponse { +impl ServiceResponse { /// Set a new body - pub fn map_body(self, f: F) -> ServiceResponse + pub fn map_body(self, f: F) -> ServiceResponse where F: FnOnce(&mut ResponseHead, ResponseBody) -> ResponseBody, { @@ -292,7 +325,7 @@ impl ServiceResponse { } } -impl std::ops::Deref for ServiceResponse { +impl std::ops::Deref for ServiceResponse { type Target = Response; fn deref(&self) -> &Response { @@ -300,19 +333,19 @@ impl std::ops::Deref for ServiceResponse { } } -impl std::ops::DerefMut for ServiceResponse { +impl std::ops::DerefMut for ServiceResponse { fn deref_mut(&mut self) -> &mut Response { self.response_mut() } } -impl Into> for ServiceResponse { +impl Into> for ServiceResponse { fn into(self) -> Response { self.response } } -impl IntoFuture for ServiceResponse { +impl IntoFuture for ServiceResponse { type Item = ServiceResponse; type Error = Error; type Future = FutureResult, Error>; diff --git a/src/test.rs b/src/test.rs index 7ceedacc4..8b6667a4d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -8,11 +8,12 @@ use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{Extensions, PayloadStream, Request}; use actix_router::{Path, Url}; use actix_rt::Runtime; +use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; use futures::Future; use crate::request::HttpRequest; -use crate::service::{ServiceFromRequest, ServiceRequest}; +use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; thread_local! { static RT: RefCell = { @@ -37,6 +38,34 @@ where RT.with(move |rt| rt.borrow_mut().block_on(f)) } +/// This method accepts application builder instance, and constructs +/// service. +/// +/// ```rust +/// use actix_http::http::{test, App, HttpResponse}; +/// +/// fn main() { +/// let app = test::init_service( +/// App::new() +/// .resource("/test", |r| r.to(|| HttpResponse::Ok())) +/// ) +/// +/// let req = TestRequest::with_uri("/test").to_request(); +/// let resp = block_on(srv.call(req)).unwrap(); +/// assert_eq!(resp.status(), StatusCode::OK); +/// } +/// ``` +pub fn init_service( + app: R, +) -> impl Service, Error = E> +where + R: IntoNewService, + S: NewService, Error = E>, + S::InitError: std::fmt::Debug, +{ + block_on(app.into_new_service().new_service(&())).unwrap() +} + /// Test `Request` builder. /// /// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern. @@ -112,6 +141,22 @@ impl TestRequest { } } + /// Create TestRequest and set method to `Method::GET` + pub fn get() -> TestRequest { + TestRequest { + req: HttpTestRequest::default().method(Method::GET).take(), + extensions: Extensions::new(), + } + } + + /// Create TestRequest and set method to `Method::POST` + pub fn post() -> TestRequest { + TestRequest { + req: HttpTestRequest::default().method(Method::POST).take(), + extensions: Extensions::new(), + } + } + /// Set HTTP version of this request pub fn version(mut self, ver: Version) -> Self { self.req.version(ver); From 0cf73f1a04d23995bffd8db21f00107713baf209 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 18:52:29 -0800 Subject: [PATCH 200/427] move session to different folder --- Cargo.toml | 2 +- {session => actix-session}/Cargo.toml | 0 {session => actix-session}/src/cookie.rs | 0 {session => actix-session}/src/lib.rs | 0 src/test.rs | 14 +++++++++----- 5 files changed, 10 insertions(+), 6 deletions(-) rename {session => actix-session}/Cargo.toml (100%) rename {session => actix-session}/src/cookie.rs (100%) rename {session => actix-session}/src/lib.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 2f69c7ef1..2f50b210a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ path = "src/lib.rs" [workspace] members = [ ".", - "session", + "actix-session", "staticfiles", ] diff --git a/session/Cargo.toml b/actix-session/Cargo.toml similarity index 100% rename from session/Cargo.toml rename to actix-session/Cargo.toml diff --git a/session/src/cookie.rs b/actix-session/src/cookie.rs similarity index 100% rename from session/src/cookie.rs rename to actix-session/src/cookie.rs diff --git a/session/src/lib.rs b/actix-session/src/lib.rs similarity index 100% rename from session/src/lib.rs rename to actix-session/src/lib.rs diff --git a/src/test.rs b/src/test.rs index 8b6667a4d..8495ea89c 100644 --- a/src/test.rs +++ b/src/test.rs @@ -42,16 +42,20 @@ where /// service. /// /// ```rust -/// use actix_http::http::{test, App, HttpResponse}; +/// use actix_web::{test, App, HttpResponse, http::StatusCode}; +/// use actix_service::Service; /// /// fn main() { -/// let app = test::init_service( +/// let mut app = test::init_service( /// App::new() /// .resource("/test", |r| r.to(|| HttpResponse::Ok())) -/// ) +/// ); /// -/// let req = TestRequest::with_uri("/test").to_request(); -/// let resp = block_on(srv.call(req)).unwrap(); +/// // Create request object +/// let req = test::TestRequest::with_uri("/test").to_request(); +/// +/// // Execute application +/// let resp = test::block_on(app.call(req)).unwrap(); /// assert_eq!(resp.status(), StatusCode::OK); /// } /// ``` From 81273f71ef521fe9ec131d13ab4a57e19bb13c99 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 19:03:59 -0800 Subject: [PATCH 201/427] update tests --- src/app.rs | 68 +++++++++++++++++++++++++--------------------------- src/scope.rs | 33 ++++++++++--------------- src/test.rs | 10 ++++---- 3 files changed, 51 insertions(+), 60 deletions(-) diff --git a/src/app.rs b/src/app.rs index b21645016..e38180c4f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -902,16 +902,14 @@ mod tests { use actix_http::http::{Method, StatusCode}; use super::*; - use crate::test::{block_on, TestRequest}; + use crate::test::{self, block_on, TestRequest}; use crate::{web, HttpResponse, State}; #[test] fn test_default_resource() { - let app = App::new() - .resource("/test", |r| r.to(|| HttpResponse::Ok())) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); - + let mut srv = test::init_service( + App::new().resource("/test", |r| r.to(|| HttpResponse::Ok())), + ); let req = TestRequest::with_uri("/test").to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); @@ -920,15 +918,15 @@ mod tests { let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); - let app = App::new() - .resource("/test", |r| r.to(|| HttpResponse::Ok())) - .resource("/test2", |r| { - r.default_resource(|r| r.to(|| HttpResponse::Created())) - .route(web::get().to(|| HttpResponse::Ok())) - }) - .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service( + App::new() + .resource("/test", |r| r.to(|| HttpResponse::Ok())) + .resource("/test2", |r| { + r.default_resource(|r| r.to(|| HttpResponse::Created())) + .route(web::get().to(|| HttpResponse::Ok())) + }) + .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())), + ); let req = TestRequest::with_uri("/blah").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -947,21 +945,21 @@ mod tests { #[test] fn test_state() { - let app = App::new() - .state(10usize) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service( + App::new() + .state(10usize) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + ); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let app = App::new() - .state(10u32) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service( + App::new() + .state(10u32) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + ); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -970,21 +968,21 @@ mod tests { #[test] fn test_state_factory() { - let app = App::new() - .state_factory(|| Ok::<_, ()>(10usize)) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service( + App::new() + .state_factory(|| Ok::<_, ()>(10usize)) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + ); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let app = App::new() - .state_factory(|| Ok::<_, ()>(10u32)) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service( + App::new() + .state_factory(|| Ok::<_, ()>(10u32)) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + ); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); diff --git a/src/scope.rs b/src/scope.rs index fa7392b41..dc88388f8 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -509,17 +509,14 @@ mod tests { use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; - use crate::test::{block_on, TestRequest}; + use crate::test::{self, block_on, TestRequest}; use crate::{guard, web, App, HttpRequest, HttpResponse}; #[test] fn test_scope() { - let app = App::new() - .scope("/app", |scope| { - scope.resource("/path1", |r| r.to(|| HttpResponse::Ok())) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service(App::new().scope("/app", |scope| { + scope.resource("/path1", |r| r.to(|| HttpResponse::Ok())) + })); let req = TestRequest::with_uri("/app/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -528,14 +525,11 @@ mod tests { #[test] fn test_scope_root() { - let app = App::new() - .scope("/app", |scope| { - scope - .resource("", |r| r.to(|| HttpResponse::Ok())) - .resource("/", |r| r.to(|| HttpResponse::Created())) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service(App::new().scope("/app", |scope| { + scope + .resource("", |r| r.to(|| HttpResponse::Ok())) + .resource("/", |r| r.to(|| HttpResponse::Created())) + })); let req = TestRequest::with_uri("/app").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -548,12 +542,9 @@ mod tests { #[test] fn test_scope_root2() { - let app = App::new() - .scope("/app/", |scope| { - scope.resource("", |r| r.to(|| HttpResponse::Ok())) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service(App::new().scope("/app/", |scope| { + scope.resource("", |r| r.to(|| HttpResponse::Ok())) + })); let req = TestRequest::with_uri("/app").to_request(); let resp = block_on(srv.call(req)).unwrap(); diff --git a/src/test.rs b/src/test.rs index 8495ea89c..22bfe0c39 100644 --- a/src/test.rs +++ b/src/test.rs @@ -41,7 +41,7 @@ where /// This method accepts application builder instance, and constructs /// service. /// -/// ```rust +/// ```rust,ignore /// use actix_web::{test, App, HttpResponse, http::StatusCode}; /// use actix_service::Service; /// @@ -80,7 +80,9 @@ where /// * `TestRequest::to_http_request` creates `HttpRequest` instance, which is used for testing handlers. /// /// ```rust,ignore -/// use actix_web::test; +/// # use futures::IntoFuture; +/// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage}; +/// use actix_web::http::{header, StatusCode}; /// /// fn index(req: HttpRequest) -> HttpResponse { /// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { @@ -94,11 +96,11 @@ where /// let req = test::TestRequest::with_header("content-type", "text/plain") /// .to_http_request(); /// -/// let resp = test::block_on(index(req)); +/// let resp = test::block_on(index(req).into_future()).unwrap(); /// assert_eq!(resp.status(), StatusCode::OK); /// /// let req = test::TestRequest::default().to_http_request(); -/// let resp = test::block_on(index(req)); +/// let resp = test::block_on(index(req).into_future()).unwrap(); /// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); /// } /// ``` From d85468f7e107ef5116e3fad40dc9b9ed57bfdb04 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 19:07:07 -0800 Subject: [PATCH 202/427] do not expose headers_mut via HttpMessage --- src/client/response.rs | 4 ---- src/httpmessage.rs | 8 -------- src/request.rs | 10 +++++----- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/client/response.rs b/src/client/response.rs index 236a63382..7c6cdf643 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -24,10 +24,6 @@ impl HttpMessage for ClientResponse { &self.head.headers } - fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.head.headers - } - fn extensions(&self) -> Ref { self.head.extensions() } diff --git a/src/httpmessage.rs b/src/httpmessage.rs index d95f82f5d..3c049c09b 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -32,9 +32,6 @@ pub trait HttpMessage: Sized { /// Read the message headers. fn headers(&self) -> &HeaderMap; - /// Mutable reference to the message's headers. - fn headers_mut(&mut self) -> &mut HeaderMap; - /// Message payload stream fn take_payload(&mut self) -> Payload; @@ -280,11 +277,6 @@ where (**self).headers() } - /// Mutable reference to the message's headers. - fn headers_mut(&mut self) -> &mut HeaderMap { - (**self).headers_mut() - } - /// Message payload stream fn take_payload(&mut self) -> Payload { (**self).take_payload() diff --git a/src/request.rs b/src/request.rs index 761a159d8..0ea251ea0 100644 --- a/src/request.rs +++ b/src/request.rs @@ -22,11 +22,6 @@ impl

    HttpMessage for Request

    { &self.head().headers } - #[inline] - fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.head_mut().headers - } - /// Request extensions #[inline] fn extensions(&self) -> Ref { @@ -107,6 +102,11 @@ impl

    Request

    { &mut *self.head } + /// Mutable reference to the message's headers. + fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.head_mut().headers + } + /// Request's uri. #[inline] pub fn uri(&self) -> &Uri { From f71354783e3d7d75fe9210fa612745be0a1588b3 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 19:10:45 -0800 Subject: [PATCH 203/427] update HttpMessage impls --- src/request.rs | 5 ----- src/service.rs | 17 ++++++----------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/request.rs b/src/request.rs index 75daf59d8..211f60b85 100644 --- a/src/request.rs +++ b/src/request.rs @@ -117,11 +117,6 @@ impl HttpMessage for HttpRequest { &self.head().headers } - #[inline] - fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.head.headers - } - /// Request extensions #[inline] fn extensions(&self) -> Ref { diff --git a/src/service.rs b/src/service.rs index 0da664396..7d17527a4 100644 --- a/src/service.rs +++ b/src/service.rs @@ -78,6 +78,12 @@ impl

    ServiceRequest

    { self.head().version } + #[inline] + /// Returns mutable Request's headers. + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.head_mut().headers + } + /// The target path of this Request. #[inline] pub fn path(&self) -> &str { @@ -134,12 +140,6 @@ impl

    HttpMessage for ServiceRequest

    { &self.head().headers } - #[inline] - /// Mutable reference to the request's headers. - fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.head_mut().headers - } - /// Request extensions #[inline] fn extensions(&self) -> Ref { @@ -230,11 +230,6 @@ impl

    HttpMessage for ServiceFromRequest

    { self.req.headers() } - #[inline] - fn headers_mut(&mut self) -> &mut HeaderMap { - self.req.headers_mut() - } - /// Request extensions #[inline] fn extensions(&self) -> Ref { From 0de47211b2065d48a6ac7d341870b05ab5db8f79 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 19:30:44 -0800 Subject: [PATCH 204/427] tune App::default_resource signature --- src/app.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/app.rs b/src/app.rs index e38180c4f..27ca5c956 100644 --- a/src/app.rs +++ b/src/app.rs @@ -426,12 +426,15 @@ where /// /// Default resource works with resources only and does not work with /// custom services. - pub fn default_resource(mut self, f: F) -> Self + pub fn default_resource(mut self, f: F) -> Self where - F: FnOnce(Resource

    ) -> R, - R: IntoNewService>, - U: NewService, Response = ServiceResponse, Error = ()> - + 'static, + F: FnOnce(Resource

    ) -> Resource, + U: NewService< + ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, { // create and configure default resource self.default = Some(Rc::new(boxed::new_service( From 1a80b70868a79cc5fc03f04b105adac7bae36769 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 19:41:50 -0800 Subject: [PATCH 205/427] add Responder impl for InternalError --- src/responder.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/responder.rs b/src/responder.rs index b2fd848f0..dedfa1b44 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,3 +1,4 @@ +use actix_http::error::InternalError; use actix_http::{dev::ResponseBuilder, http::StatusCode, Error, Response}; use bytes::{Bytes, BytesMut}; use futures::future::{err, ok, Either as EitherFuture, FutureResult}; @@ -252,6 +253,18 @@ where } } +impl Responder for InternalError +where + T: std::fmt::Debug + std::fmt::Display + 'static, +{ + type Error = Error; + type Future = Result; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + Err(self.into()) + } +} + pub struct ResponseFuture(T); impl ResponseFuture { From 34c8b95a35a8830e70a888cbb063678ee8b16b32 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 21:15:18 -0800 Subject: [PATCH 206/427] allow to extract body from response --- src/body.rs | 6 ++++++ src/response.rs | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/body.rs b/src/body.rs index 1f218c4ba..d72f6c378 100644 --- a/src/body.rs +++ b/src/body.rs @@ -59,6 +59,12 @@ impl ResponseBody { } } +impl ResponseBody { + pub fn take_body(&mut self) -> ResponseBody { + std::mem::replace(self, ResponseBody::Other(Body::None)) + } +} + impl ResponseBody { pub fn as_ref(&self) -> Option<&B> { if let ResponseBody::Body(ref b) = self { diff --git a/src/response.rs b/src/response.rs index 277890e40..4e1fe2142 100644 --- a/src/response.rs +++ b/src/response.rs @@ -254,6 +254,11 @@ impl Response { error: self.error, } } + + /// Extract response body + pub fn take_body(&mut self) -> ResponseBody { + self.body.take_body() + } } impl fmt::Debug for Response { From 889d67a356a163fb444a4e7921394c765695427a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 21:19:12 -0800 Subject: [PATCH 207/427] add Stream impl for ResponseBody --- src/body.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/body.rs b/src/body.rs index d72f6c378..b7e8ec98a 100644 --- a/src/body.rs +++ b/src/body.rs @@ -91,6 +91,15 @@ impl MessageBody for ResponseBody { } } +impl Stream for ResponseBody { + type Item = Bytes; + type Error = Error; + + fn poll(&mut self) -> Poll, Self::Error> { + self.poll_next() + } +} + /// Represents various types of http message body. pub enum Body { /// Empty response. `Content-Length` header is not set. From 6efc3438b83497577b8d791b77c6600e95660f35 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 22:10:08 -0800 Subject: [PATCH 208/427] refactor and enable some tests for staticfiles --- .travis.yml | 4 +- Cargo.toml | 2 +- {staticfiles => actix-staticfiles}/CHANGES.md | 0 {staticfiles => actix-staticfiles}/Cargo.toml | 3 +- {staticfiles => actix-staticfiles}/README.md | 0 actix-staticfiles/src/config.rs | 70 + actix-staticfiles/src/error.rs | 41 + actix-staticfiles/src/lib.rs | 1482 ++++++++++++ actix-staticfiles/src/named.rs | 438 ++++ actix-staticfiles/tests/test space.binary | 1 + actix-staticfiles/tests/test.binary | 1 + actix-staticfiles/tests/test.png | Bin 0 -> 168 bytes examples/basic.rs | 25 +- src/app.rs | 45 +- src/lib.rs | 24 +- src/service.rs | 64 +- src/test.rs | 28 + staticfiles/src/lib.rs | 2033 ----------------- 18 files changed, 2198 insertions(+), 2063 deletions(-) rename {staticfiles => actix-staticfiles}/CHANGES.md (100%) rename {staticfiles => actix-staticfiles}/Cargo.toml (93%) rename {staticfiles => actix-staticfiles}/README.md (100%) create mode 100644 actix-staticfiles/src/config.rs create mode 100644 actix-staticfiles/src/error.rs create mode 100644 actix-staticfiles/src/lib.rs create mode 100644 actix-staticfiles/src/named.rs create mode 100644 actix-staticfiles/tests/test space.binary create mode 100644 actix-staticfiles/tests/test.binary create mode 100644 actix-staticfiles/tests/test.png delete mode 100644 staticfiles/src/lib.rs diff --git a/.travis.yml b/.travis.yml index 1d3c227a9..55a03ec8c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,7 @@ before_script: script: - cargo clean - - cargo test -- --nocapture + - cargo test --all -- --nocapture # Upload docs after_success: @@ -49,7 +49,7 @@ after_success: fi - | if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then - cargo tarpaulin --out Xml + cargo tarpaulin --out Xml --all bash <(curl -s https://codecov.io/bash) echo "Uploaded code coverage" fi diff --git a/Cargo.toml b/Cargo.toml index 2f50b210a..acacb2f21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ path = "src/lib.rs" members = [ ".", "actix-session", - "staticfiles", + "actix-staticfiles", ] [package.metadata.docs.rs] diff --git a/staticfiles/CHANGES.md b/actix-staticfiles/CHANGES.md similarity index 100% rename from staticfiles/CHANGES.md rename to actix-staticfiles/CHANGES.md diff --git a/staticfiles/Cargo.toml b/actix-staticfiles/Cargo.toml similarity index 93% rename from staticfiles/Cargo.toml rename to actix-staticfiles/Cargo.toml index 0aa589701..0a5517920 100644 --- a/staticfiles/Cargo.toml +++ b/actix-staticfiles/Cargo.toml @@ -20,7 +20,8 @@ path = "src/lib.rs" [dependencies] actix-web = { path=".." } actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-service = "0.3.0" +actix-service = { git = "https://github.com/actix/actix-net.git" } +#actix-service = "0.3.0" bytes = "0.4" futures = "0.1" diff --git a/staticfiles/README.md b/actix-staticfiles/README.md similarity index 100% rename from staticfiles/README.md rename to actix-staticfiles/README.md diff --git a/actix-staticfiles/src/config.rs b/actix-staticfiles/src/config.rs new file mode 100644 index 000000000..da72da201 --- /dev/null +++ b/actix-staticfiles/src/config.rs @@ -0,0 +1,70 @@ +use actix_http::http::header::DispositionType; +use actix_web::http::Method; +use mime; + +/// Describes `StaticFiles` configiration +/// +/// To configure actix's static resources you need +/// to define own configiration type and implement any method +/// you wish to customize. +/// As trait implements reasonable defaults for Actix. +/// +/// ## Example +/// +/// ```rust,ignore +/// extern crate mime; +/// extern crate actix_web; +/// use actix_web::http::header::DispositionType; +/// use actix_web::fs::{StaticFileConfig, NamedFile}; +/// +/// #[derive(Default)] +/// struct MyConfig; +/// +/// impl StaticFileConfig for MyConfig { +/// fn content_disposition_map(typ: mime::Name) -> DispositionType { +/// DispositionType::Attachment +/// } +/// } +/// +/// let file = NamedFile::open_with_config("foo.txt", MyConfig); +/// ``` +pub trait StaticFileConfig: Default { + ///Describes mapping for mime type to content disposition header + /// + ///By default `IMAGE`, `TEXT` and `VIDEO` are mapped to Inline. + ///Others are mapped to Attachment + fn content_disposition_map(typ: mime::Name) -> DispositionType { + match typ { + mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, + _ => DispositionType::Attachment, + } + } + + ///Describes whether Actix should attempt to calculate `ETag` + /// + ///Defaults to `true` + fn is_use_etag() -> bool { + true + } + + ///Describes whether Actix should use last modified date of file. + /// + ///Defaults to `true` + fn is_use_last_modifier() -> bool { + true + } + + ///Describes allowed methods to access static resources. + /// + ///By default all methods are allowed + fn is_method_allowed(_method: &Method) -> bool { + true + } +} + +///Default content disposition as described in +///[StaticFileConfig](trait.StaticFileConfig.html) +#[derive(Default)] +pub struct DefaultConfig; + +impl StaticFileConfig for DefaultConfig {} diff --git a/actix-staticfiles/src/error.rs b/actix-staticfiles/src/error.rs new file mode 100644 index 000000000..f165a618a --- /dev/null +++ b/actix-staticfiles/src/error.rs @@ -0,0 +1,41 @@ +use actix_web::{http::StatusCode, HttpResponse, ResponseError}; +use derive_more::Display; + +/// Errors which can occur when serving static files. +#[derive(Display, Debug, PartialEq)] +pub enum StaticFilesError { + /// Path is not a directory + #[display(fmt = "Path is not a directory. Unable to serve static files")] + IsNotDirectory, + + /// Cannot render directory + #[display(fmt = "Unable to render directory without index file")] + IsDirectory, +} + +/// Return `NotFound` for `StaticFilesError` +impl ResponseError for StaticFilesError { + fn error_response(&self) -> HttpResponse { + HttpResponse::new(StatusCode::NOT_FOUND) + } +} + +#[derive(Display, Debug, PartialEq)] +pub enum UriSegmentError { + /// The segment started with the wrapped invalid character. + #[display(fmt = "The segment started with the wrapped invalid character")] + BadStart(char), + /// The segment contained the wrapped invalid character. + #[display(fmt = "The segment contained the wrapped invalid character")] + BadChar(char), + /// The segment ended with the wrapped invalid character. + #[display(fmt = "The segment ended with the wrapped invalid character")] + BadEnd(char), +} + +/// Return `BadRequest` for `UriSegmentError` +impl ResponseError for UriSegmentError { + fn error_response(&self) -> HttpResponse { + HttpResponse::new(StatusCode::BAD_REQUEST) + } +} diff --git a/actix-staticfiles/src/lib.rs b/actix-staticfiles/src/lib.rs new file mode 100644 index 000000000..01306d4b3 --- /dev/null +++ b/actix-staticfiles/src/lib.rs @@ -0,0 +1,1482 @@ +//! Static files support +use std::cell::RefCell; +use std::fmt::Write; +use std::fs::{DirEntry, File}; +use std::io::{Read, Seek}; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::{cmp, io}; + +use bytes::Bytes; +use futures::{Async, Future, Poll, Stream}; +use mime; +use mime_guess::get_mime_type; +use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; +use v_htmlescape::escape as escape_html_entity; + +use actix_http::error::{Error, ErrorInternalServerError}; +use actix_service::{boxed::BoxedNewService, NewService, Service}; +use actix_web::dev::{self, HttpServiceFactory, ResourceDef, Url}; +use actix_web::{ + blocking, FromRequest, HttpRequest, HttpResponse, Responder, ServiceFromRequest, + ServiceRequest, ServiceResponse, +}; +use futures::future::{ok, FutureResult}; + +mod config; +mod error; +mod named; + +use self::error::{StaticFilesError, UriSegmentError}; +pub use crate::config::{DefaultConfig, StaticFileConfig}; +pub use crate::named::NamedFile; + +type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; + +/// Return the MIME type associated with a filename extension (case-insensitive). +/// If `ext` is empty or no associated type for the extension was found, returns +/// the type `application/octet-stream`. +#[inline] +pub fn file_extension_to_mime(ext: &str) -> mime::Mime { + get_mime_type(ext) +} + +#[doc(hidden)] +/// A helper created from a `std::fs::File` which reads the file +/// chunk-by-chunk on a `ThreadPool`. +pub struct ChunkedReadFile { + size: u64, + offset: u64, + file: Option, + fut: Option>, + counter: u64, +} + +fn handle_error(err: blocking::BlockingError) -> Error { + match err { + blocking::BlockingError::Error(err) => err.into(), + blocking::BlockingError::Canceled => { + ErrorInternalServerError("Unexpected error").into() + } + } +} + +impl Stream for ChunkedReadFile { + type Item = Bytes; + type Error = Error; + + fn poll(&mut self) -> Poll, Error> { + if self.fut.is_some() { + return match self.fut.as_mut().unwrap().poll().map_err(handle_error)? { + Async::Ready((file, bytes)) => { + self.fut.take(); + self.file = Some(file); + self.offset += bytes.len() as u64; + self.counter += bytes.len() as u64; + Ok(Async::Ready(Some(bytes))) + } + Async::NotReady => Ok(Async::NotReady), + }; + } + + let size = self.size; + let offset = self.offset; + let counter = self.counter; + + if size == counter { + Ok(Async::Ready(None)) + } else { + let mut file = self.file.take().expect("Use after completion"); + self.fut = Some(blocking::run(move || { + let max_bytes: usize; + max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; + let mut buf = Vec::with_capacity(max_bytes); + file.seek(io::SeekFrom::Start(offset))?; + let nbytes = + file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; + if nbytes == 0 { + return Err(io::ErrorKind::UnexpectedEof.into()); + } + Ok((file, Bytes::from(buf))) + })); + self.poll() + } + } +} + +type DirectoryRenderer = + Fn(&Directory, &HttpRequest) -> Result; + +/// A directory; responds with the generated directory listing. +#[derive(Debug)] +pub struct Directory { + /// Base directory + pub base: PathBuf, + /// Path of subdirectory to generate listing for + pub path: PathBuf, +} + +impl Directory { + /// Create a new directory + pub fn new(base: PathBuf, path: PathBuf) -> Directory { + Directory { base, path } + } + + /// Is this entry visible from this directory? + pub fn is_visible(&self, entry: &io::Result) -> bool { + if let Ok(ref entry) = *entry { + if let Some(name) = entry.file_name().to_str() { + if name.starts_with('.') { + return false; + } + } + if let Ok(ref md) = entry.metadata() { + let ft = md.file_type(); + return ft.is_dir() || ft.is_file() || ft.is_symlink(); + } + } + false + } +} + +// show file url as relative to static path +macro_rules! encode_file_url { + ($path:ident) => { + utf8_percent_encode(&$path.to_string_lossy(), DEFAULT_ENCODE_SET) + }; +} + +// " -- " & -- & ' -- ' < -- < > -- > / -- / +macro_rules! encode_file_name { + ($entry:ident) => { + escape_html_entity(&$entry.file_name().to_string_lossy()) + }; +} + +fn directory_listing( + dir: &Directory, + req: &HttpRequest, +) -> Result { + let index_of = format!("Index of {}", req.path()); + let mut body = String::new(); + let base = Path::new(req.path()); + + for entry in dir.path.read_dir()? { + if dir.is_visible(&entry) { + let entry = entry.unwrap(); + let p = match entry.path().strip_prefix(&dir.path) { + Ok(p) => base.join(p), + Err(_) => continue, + }; + + // if file is a directory, add '/' to the end of the name + if let Ok(metadata) = entry.metadata() { + if metadata.is_dir() { + let _ = write!( + body, + "

  • {}/
  • ", + encode_file_url!(p), + encode_file_name!(entry), + ); + } else { + let _ = write!( + body, + "
  • {}
  • ", + encode_file_url!(p), + encode_file_name!(entry), + ); + } + } else { + continue; + } + } + } + + let html = format!( + "\ + {}\ +

    {}

    \ +
      \ + {}\ +
    \n", + index_of, index_of, body + ); + Ok(ServiceResponse::new( + req.clone(), + HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(html), + )) +} + +/// Static files handling +/// +/// `StaticFile` handler must be registered with `App::handler()` method, +/// because `StaticFile` handler requires access sub-path information. +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// use actix_web::{fs, App}; +/// +/// fn main() { +/// let app = App::new() +/// .handler("/static", fs::StaticFiles::new(".").unwrap()) +/// .finish(); +/// } +/// ``` +pub struct StaticFiles { + path: ResourceDef, + directory: PathBuf, + index: Option, + show_index: bool, + default: Rc>>>>, + renderer: Rc, + _chunk_size: usize, + _follow_symlinks: bool, + _cd_map: PhantomData, +} + +impl StaticFiles { + /// Create new `StaticFiles` instance for specified base directory. + /// + /// `StaticFile` uses `ThreadPool` for blocking filesystem operations. + /// By default pool with 5x threads of available cpus is used. + /// Pool size can be changed by setting ACTIX_CPU_POOL environment variable. + pub fn new>(path: &str, dir: T) -> Result, Error> { + Self::with_config(path, dir, DefaultConfig) + } +} + +impl StaticFiles { + /// Create new `StaticFiles` instance for specified base directory. + /// + /// Identical with `new` but allows to specify configiration to use. + pub fn with_config>( + path: &str, + dir: T, + _: C, + ) -> Result, Error> { + let dir = dir.into().canonicalize()?; + + if !dir.is_dir() { + return Err(StaticFilesError::IsNotDirectory.into()); + } + + Ok(StaticFiles { + path: ResourceDef::root_prefix(path), + directory: dir, + index: None, + show_index: false, + default: Rc::new(RefCell::new(None)), + renderer: Rc::new(directory_listing), + _chunk_size: 0, + _follow_symlinks: false, + _cd_map: PhantomData, + }) + } + + /// Show files listing for directories. + /// + /// By default show files listing is disabled. + pub fn show_files_listing(mut self) -> Self { + self.show_index = true; + self + } + + /// Set custom directory renderer + pub fn files_listing_renderer(mut self, f: F) -> Self + where + for<'r, 's> F: + Fn(&'r Directory, &'s HttpRequest) -> Result + + 'static, + { + self.renderer = Rc::new(f); + self + } + + /// Set index file + /// + /// Shows specific index file for directory "/" instead of + /// showing files listing. + pub fn index_file>(mut self, index: T) -> StaticFiles { + self.index = Some(index.into()); + self + } +} + +impl HttpServiceFactory

    for StaticFiles { + type Factory = Self; + + fn rdef(&self) -> &ResourceDef { + &self.path + } + + fn create(self) -> Self { + self + } +} + +impl NewService> + for StaticFiles +{ + type Response = ServiceResponse; + type Error = (); + type Service = StaticFilesService; + type InitError = (); + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(StaticFilesService { + directory: self.directory.clone(), + index: self.index.clone(), + show_index: self.show_index, + default: self.default.clone(), + renderer: self.renderer.clone(), + _chunk_size: self._chunk_size, + _follow_symlinks: self._follow_symlinks, + _cd_map: self._cd_map, + }) + } +} + +pub struct StaticFilesService { + directory: PathBuf, + index: Option, + show_index: bool, + default: Rc>>>>, + renderer: Rc, + _chunk_size: usize, + _follow_symlinks: bool, + _cd_map: PhantomData, +} + +impl Service> for StaticFilesService { + type Response = ServiceResponse; + type Error = (); + type Future = FutureResult; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { + let (req, _) = req.into_parts(); + + let real_path = match PathBufWrp::get_pathbuf(req.match_info()) { + Ok(item) => item, + Err(e) => return ok(ServiceResponse::from_err(e, req.clone())), + }; + + // full filepath + let path = match self.directory.join(&real_path.0).canonicalize() { + Ok(path) => path, + Err(e) => return ok(ServiceResponse::from_err(e, req.clone())), + }; + + if path.is_dir() { + if let Some(ref redir_index) = self.index { + let path = path.join(redir_index); + + match NamedFile::open_with_config(path, C::default()) { + Ok(named_file) => match named_file.respond_to(&req) { + Ok(item) => ok(ServiceResponse::new(req.clone(), item)), + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + }, + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + } + } else if self.show_index { + let dir = Directory::new(self.directory.clone(), path); + let x = (self.renderer)(&dir, &req); + match x { + Ok(resp) => ok(resp), + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + } + } else { + ok(ServiceResponse::from_err( + StaticFilesError::IsDirectory, + req.clone(), + )) + } + } else { + match NamedFile::open_with_config(path, C::default()) { + Ok(named_file) => match named_file.respond_to(&req) { + Ok(item) => ok(ServiceResponse::new(req.clone(), item)), + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + }, + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + } + } + } +} + +struct PathBufWrp(PathBuf); + +impl PathBufWrp { + fn get_pathbuf(path: &dev::Path) -> Result { + let path_str = path.path(); + let mut buf = PathBuf::new(); + for segment in path_str.split('/') { + if segment == ".." { + buf.pop(); + } else if segment.starts_with('.') { + return Err(UriSegmentError::BadStart('.')); + } else if segment.starts_with('*') { + return Err(UriSegmentError::BadStart('*')); + } else if segment.ends_with(':') { + return Err(UriSegmentError::BadEnd(':')); + } else if segment.ends_with('>') { + return Err(UriSegmentError::BadEnd('>')); + } else if segment.ends_with('<') { + return Err(UriSegmentError::BadEnd('<')); + } else if segment.is_empty() { + continue; + } else if cfg!(windows) && segment.contains('\\') { + return Err(UriSegmentError::BadChar('\\')); + } else { + buf.push(segment) + } + } + + Ok(PathBufWrp(buf)) + } +} + +impl

    FromRequest

    for PathBufWrp { + type Error = UriSegmentError; + type Future = Result; + type Config = (); + + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + PathBufWrp::get_pathbuf(req.match_info()) + } +} + +/// HTTP Range header representation. +#[derive(Debug, Clone, Copy)] +struct HttpRange { + pub start: u64, + pub length: u64, +} + +static PREFIX: &'static str = "bytes="; +const PREFIX_LEN: usize = 6; + +impl HttpRange { + /// Parses Range HTTP header string as per RFC 2616. + /// + /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). + /// `size` is full size of response (file). + fn parse(header: &str, size: u64) -> Result, ()> { + if header.is_empty() { + return Ok(Vec::new()); + } + if !header.starts_with(PREFIX) { + return Err(()); + } + + let size_sig = size as i64; + let mut no_overlap = false; + + let all_ranges: Vec> = header[PREFIX_LEN..] + .split(',') + .map(|x| x.trim()) + .filter(|x| !x.is_empty()) + .map(|ra| { + let mut start_end_iter = ra.split('-'); + + let start_str = start_end_iter.next().ok_or(())?.trim(); + let end_str = start_end_iter.next().ok_or(())?.trim(); + + if start_str.is_empty() { + // If no start is specified, end specifies the + // range start relative to the end of the file. + let mut length: i64 = end_str.parse().map_err(|_| ())?; + + if length > size_sig { + length = size_sig; + } + + Ok(Some(HttpRange { + start: (size_sig - length) as u64, + length: length as u64, + })) + } else { + let start: i64 = start_str.parse().map_err(|_| ())?; + + if start < 0 { + return Err(()); + } + if start >= size_sig { + no_overlap = true; + return Ok(None); + } + + let length = if end_str.is_empty() { + // If no end is specified, range extends to end of the file. + size_sig - start + } else { + let mut end: i64 = end_str.parse().map_err(|_| ())?; + + if start > end { + return Err(()); + } + + if end >= size_sig { + end = size_sig - 1; + } + + end - start + 1 + }; + + Ok(Some(HttpRange { + start: start as u64, + length: length as u64, + })) + } + }) + .collect::>()?; + + let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); + + if no_overlap && ranges.is_empty() { + return Err(()); + } + + Ok(ranges) + } +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::ops::Add; + use std::time::{Duration, SystemTime}; + + use bytes::BytesMut; + + use super::*; + use actix_web::http::{header, header::DispositionType, Method, StatusCode}; + use actix_web::test::{self, TestRequest}; + use actix_web::App; + + #[test] + fn test_file_extension_to_mime() { + let m = file_extension_to_mime("jpg"); + assert_eq!(m, mime::IMAGE_JPEG); + + let m = file_extension_to_mime("invalid extension!!"); + assert_eq!(m, mime::APPLICATION_OCTET_STREAM); + + let m = file_extension_to_mime(""); + assert_eq!(m, mime::APPLICATION_OCTET_STREAM); + } + + #[test] + fn test_if_modified_since_without_if_none_match() { + let file = NamedFile::open("Cargo.toml").unwrap(); + let since = + header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + + let req = TestRequest::default() + .header(header::IF_MODIFIED_SINCE, since) + .to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); + } + + #[test] + fn test_if_modified_since_with_if_none_match() { + let file = NamedFile::open("Cargo.toml").unwrap(); + let since = + header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + + let req = TestRequest::default() + .header(header::IF_NONE_MATCH, "miss_etag") + .header(header::IF_MODIFIED_SINCE, since) + .to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); + } + + #[test] + fn test_named_file_text() { + assert!(NamedFile::open("test--").is_err()); + let mut file = NamedFile::open("Cargo.toml").unwrap(); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/x-toml" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"Cargo.toml\"" + ); + } + + #[test] + fn test_named_file_set_content_type() { + let mut file = NamedFile::open("Cargo.toml") + .unwrap() + .set_content_type(mime::TEXT_XML); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/xml" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"Cargo.toml\"" + ); + } + + #[test] + fn test_named_file_image() { + let mut file = NamedFile::open("tests/test.png").unwrap(); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "image/png" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"test.png\"" + ); + } + + #[test] + fn test_named_file_image_attachment() { + use header::{ContentDisposition, DispositionParam, DispositionType}; + let cd = ContentDisposition { + disposition: DispositionType::Attachment, + parameters: vec![DispositionParam::Filename(String::from("test.png"))], + }; + let mut file = NamedFile::open("tests/test.png") + .unwrap() + .set_content_disposition(cd); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "image/png" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "attachment; filename=\"test.png\"" + ); + } + + #[derive(Default)] + pub struct AllAttachmentConfig; + impl StaticFileConfig for AllAttachmentConfig { + fn content_disposition_map(_typ: mime::Name) -> DispositionType { + DispositionType::Attachment + } + } + + #[derive(Default)] + pub struct AllInlineConfig; + impl StaticFileConfig for AllInlineConfig { + fn content_disposition_map(_typ: mime::Name) -> DispositionType { + DispositionType::Inline + } + } + + #[test] + fn test_named_file_image_attachment_and_custom_config() { + let file = + NamedFile::open_with_config("tests/test.png", AllAttachmentConfig).unwrap(); + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "image/png" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "attachment; filename=\"test.png\"" + ); + + let file = + NamedFile::open_with_config("tests/test.png", AllInlineConfig).unwrap(); + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "image/png" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"test.png\"" + ); + } + + #[test] + fn test_named_file_binary() { + let mut file = NamedFile::open("tests/test.binary").unwrap(); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "application/octet-stream" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "attachment; filename=\"test.binary\"" + ); + } + + #[test] + fn test_named_file_status_code_text() { + let mut file = NamedFile::open("Cargo.toml") + .unwrap() + .set_status_code(StatusCode::NOT_FOUND); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/x-toml" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"Cargo.toml\"" + ); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_named_file_ranges_status_code() { + let mut srv = test::init_service( + App::new().service( + StaticFiles::new("/test", ".") + .unwrap() + .index_file("Cargo.toml"), + ), + ); + + // Valid range header + let request = TestRequest::get() + .uri("/t%65st/Cargo.toml") + .header(header::RANGE, "bytes=10-20") + .to_request(); + let response = test::call_success(&mut srv, request); + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + + // Invalid range header + let request = TestRequest::get() + .uri("/t%65st/Cargo.toml") + .header(header::RANGE, "bytes=1-0") + .to_request(); + let response = test::call_success(&mut srv, request); + + assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); + } + + #[test] + fn test_named_file_content_range_headers() { + let mut srv = test::init_service( + App::new().service( + StaticFiles::new("/test", ".") + .unwrap() + .index_file("tests/test.binary"), + ), + ); + + // Valid range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .header(header::RANGE, "bytes=10-20") + .to_request(); + + let response = test::call_success(&mut srv, request); + let contentrange = response + .headers() + .get(header::CONTENT_RANGE) + .unwrap() + .to_str() + .unwrap(); + + assert_eq!(contentrange, "bytes 10-20/100"); + + // Invalid range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .header(header::RANGE, "bytes=10-5") + .to_request(); + let response = test::call_success(&mut srv, request); + + let contentrange = response + .headers() + .get(header::CONTENT_RANGE) + .unwrap() + .to_str() + .unwrap(); + + assert_eq!(contentrange, "bytes */100"); + } + + #[test] + fn test_named_file_content_length_headers() { + let mut srv = test::init_service( + App::new().service( + StaticFiles::new("test", ".") + .unwrap() + .index_file("tests/test.binary"), + ), + ); + + // Valid range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .header(header::RANGE, "bytes=10-20") + .to_request(); + let response = test::call_success(&mut srv, request); + + let contentlength = response + .headers() + .get(header::CONTENT_LENGTH) + .unwrap() + .to_str() + .unwrap(); + + assert_eq!(contentlength, "11"); + + // Invalid range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .header(header::RANGE, "bytes=10-8") + .to_request(); + let response = test::call_success(&mut srv, request); + assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); + + // Without range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + // .no_default_headers() + .to_request(); + let response = test::call_success(&mut srv, request); + + let contentlength = response + .headers() + .get(header::CONTENT_LENGTH) + .unwrap() + .to_str() + .unwrap(); + + assert_eq!(contentlength, "100"); + + // chunked + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .to_request(); + let mut response = test::call_success(&mut srv, request); + + // with enabled compression + // { + // let te = response + // .headers() + // .get(header::TRANSFER_ENCODING) + // .unwrap() + // .to_str() + // .unwrap(); + // assert_eq!(te, "chunked"); + // } + + let bytes = + test::block_on(response.take_body().fold(BytesMut::new(), |mut b, c| { + b.extend(c); + Ok::<_, Error>(b) + })) + .unwrap(); + let data = Bytes::from(fs::read("tests/test.binary").unwrap()); + assert_eq!(bytes.freeze(), data); + } + + #[test] + fn test_static_files_with_spaces() { + let mut srv = test::init_service( + App::new() + .service(StaticFiles::new("/", ".").unwrap().index_file("Cargo.toml")), + ); + let request = TestRequest::get() + .uri("/tests/test%20space.binary") + .to_request(); + let mut response = test::call_success(&mut srv, request); + assert_eq!(response.status(), StatusCode::OK); + + let bytes = + test::block_on(response.take_body().fold(BytesMut::new(), |mut b, c| { + b.extend(c); + Ok::<_, Error>(b) + })) + .unwrap(); + + let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); + assert_eq!(bytes.freeze(), data); + } + + #[derive(Default)] + pub struct OnlyMethodHeadConfig; + impl StaticFileConfig for OnlyMethodHeadConfig { + fn is_method_allowed(method: &Method) -> bool { + match *method { + Method::HEAD => true, + _ => false, + } + } + } + + #[test] + fn test_named_file_not_allowed() { + let file = + NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); + let req = TestRequest::default() + .method(Method::POST) + .to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + + let file = + NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); + let req = TestRequest::default().method(Method::PUT).to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + + let file = + NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); + let req = TestRequest::default().method(Method::GET).to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + } + + // #[test] + // fn test_named_file_content_encoding() { + // let req = TestRequest::default().method(Method::GET).finish(); + // let file = NamedFile::open("Cargo.toml").unwrap(); + + // assert!(file.encoding.is_none()); + // let resp = file + // .set_content_encoding(ContentEncoding::Identity) + // .respond_to(&req) + // .unwrap(); + + // assert!(resp.content_encoding().is_some()); + // assert_eq!(resp.content_encoding().unwrap().as_str(), "identity"); + // } + + #[test] + fn test_named_file_any_method() { + let req = TestRequest::default() + .method(Method::POST) + .to_http_request(); + let file = NamedFile::open("Cargo.toml").unwrap(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn test_static_files() { + let mut srv = test::init_service( + App::new().service(StaticFiles::new("/", ".").unwrap().show_files_listing()), + ); + let req = TestRequest::with_uri("/missing").to_request(); + + let resp = test::call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let mut srv = + test::init_service(App::new().service(StaticFiles::new("/", ".").unwrap())); + + let req = TestRequest::default().to_request(); + let resp = test::call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let mut srv = test::init_service( + App::new().service(StaticFiles::new("/", ".").unwrap().show_files_listing()), + ); + let req = TestRequest::with_uri("/tests").to_request(); + let mut resp = test::call_success(&mut srv, req); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/html; charset=utf-8" + ); + + let bytes = + test::block_on(resp.take_body().fold(BytesMut::new(), |mut b, c| { + b.extend(c); + Ok::<_, Error>(b) + })) + .unwrap(); + assert!(format!("{:?}", bytes).contains("/tests/test.png")); + } + + #[test] + fn test_static_files_bad_directory() { + let st: Result, Error> = StaticFiles::new("/", "missing"); + assert!(st.is_err()); + + let st: Result, Error> = StaticFiles::new("/", "Cargo.toml"); + assert!(st.is_err()); + } + + // #[test] + // fn test_default_handler_file_missing() { + // let st = StaticFiles::new(".") + // .unwrap() + // .default_handler(|_: &_| "default content"); + // let req = TestRequest::with_uri("/missing") + // .param("tail", "missing") + // .finish(); + + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::OK); + // assert_eq!( + // resp.body(), + // &Body::Binary(Binary::Slice(b"default content")) + // ); + // } + + // #[test] + // fn test_serve_index() { + // let st = StaticFiles::new(".").unwrap().index_file("test.binary"); + // let req = TestRequest::default().uri("/tests").finish(); + + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::OK); + // assert_eq!( + // resp.headers() + // .get(header::CONTENT_TYPE) + // .expect("content type"), + // "application/octet-stream" + // ); + // assert_eq!( + // resp.headers() + // .get(header::CONTENT_DISPOSITION) + // .expect("content disposition"), + // "attachment; filename=\"test.binary\"" + // ); + + // let req = TestRequest::default().uri("/tests/").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::OK); + // assert_eq!( + // resp.headers().get(header::CONTENT_TYPE).unwrap(), + // "application/octet-stream" + // ); + // assert_eq!( + // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + // "attachment; filename=\"test.binary\"" + // ); + + // // nonexistent index file + // let req = TestRequest::default().uri("/tests/unknown").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + // let req = TestRequest::default().uri("/tests/unknown/").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::NOT_FOUND); + // } + + // #[test] + // fn test_serve_index_nested() { + // let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); + // let req = TestRequest::default().uri("/src/client").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::OK); + // assert_eq!( + // resp.headers().get(header::CONTENT_TYPE).unwrap(), + // "text/x-rust" + // ); + // assert_eq!( + // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + // "inline; filename=\"mod.rs\"" + // ); + // } + + // #[test] + // fn integration_serve_index() { + // let mut srv = test::TestServer::with_factory(|| { + // App::new().handler( + // "test", + // StaticFiles::new(".").unwrap().index_file("Cargo.toml"), + // ) + // }); + + // let request = srv.get().uri(srv.url("/test")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::OK); + // let bytes = srv.execute(response.body()).unwrap(); + // let data = Bytes::from(fs::read("Cargo.toml").unwrap()); + // assert_eq!(bytes, data); + + // let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::OK); + // let bytes = srv.execute(response.body()).unwrap(); + // let data = Bytes::from(fs::read("Cargo.toml").unwrap()); + // assert_eq!(bytes, data); + + // // nonexistent index file + // let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::NOT_FOUND); + + // let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::NOT_FOUND); + // } + + // #[test] + // fn integration_percent_encoded() { + // let mut srv = test::TestServer::with_factory(|| { + // App::new().handler( + // "test", + // StaticFiles::new(".").unwrap().index_file("Cargo.toml"), + // ) + // }); + + // let request = srv + // .get() + // .uri(srv.url("/test/%43argo.toml")) + // .finish() + // .unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::OK); + // } + + struct T(&'static str, u64, Vec); + + #[test] + fn test_parse() { + let tests = vec![ + T("", 0, vec![]), + T("", 1000, vec![]), + T("foo", 0, vec![]), + T("bytes=", 0, vec![]), + T("bytes=7", 10, vec![]), + T("bytes= 7 ", 10, vec![]), + T("bytes=1-", 0, vec![]), + T("bytes=5-4", 10, vec![]), + T("bytes=0-2,5-4", 10, vec![]), + T("bytes=2-5,4-3", 10, vec![]), + T("bytes=--5,4--3", 10, vec![]), + T("bytes=A-", 10, vec![]), + T("bytes=A- ", 10, vec![]), + T("bytes=A-Z", 10, vec![]), + T("bytes= -Z", 10, vec![]), + T("bytes=5-Z", 10, vec![]), + T("bytes=Ran-dom, garbage", 10, vec![]), + T("bytes=0x01-0x02", 10, vec![]), + T("bytes= ", 10, vec![]), + T("bytes= , , , ", 10, vec![]), + T( + "bytes=0-9", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=0-", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=5-", + 10, + vec![HttpRange { + start: 5, + length: 5, + }], + ), + T( + "bytes=0-20", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=15-,0-5", + 10, + vec![HttpRange { + start: 0, + length: 6, + }], + ), + T( + "bytes=1-2,5-", + 10, + vec![ + HttpRange { + start: 1, + length: 2, + }, + HttpRange { + start: 5, + length: 5, + }, + ], + ), + T( + "bytes=-2 , 7-", + 11, + vec![ + HttpRange { + start: 9, + length: 2, + }, + HttpRange { + start: 7, + length: 4, + }, + ], + ), + T( + "bytes=0-0 ,2-2, 7-", + 11, + vec![ + HttpRange { + start: 0, + length: 1, + }, + HttpRange { + start: 2, + length: 1, + }, + HttpRange { + start: 7, + length: 4, + }, + ], + ), + T( + "bytes=-5", + 10, + vec![HttpRange { + start: 5, + length: 5, + }], + ), + T( + "bytes=-15", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=0-499", + 10000, + vec![HttpRange { + start: 0, + length: 500, + }], + ), + T( + "bytes=500-999", + 10000, + vec![HttpRange { + start: 500, + length: 500, + }], + ), + T( + "bytes=-500", + 10000, + vec![HttpRange { + start: 9500, + length: 500, + }], + ), + T( + "bytes=9500-", + 10000, + vec![HttpRange { + start: 9500, + length: 500, + }], + ), + T( + "bytes=0-0,-1", + 10000, + vec![ + HttpRange { + start: 0, + length: 1, + }, + HttpRange { + start: 9999, + length: 1, + }, + ], + ), + T( + "bytes=500-600,601-999", + 10000, + vec![ + HttpRange { + start: 500, + length: 101, + }, + HttpRange { + start: 601, + length: 399, + }, + ], + ), + T( + "bytes=500-700,601-999", + 10000, + vec![ + HttpRange { + start: 500, + length: 201, + }, + HttpRange { + start: 601, + length: 399, + }, + ], + ), + // Match Apache laxity: + T( + "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", + 11, + vec![ + HttpRange { + start: 1, + length: 2, + }, + HttpRange { + start: 4, + length: 2, + }, + HttpRange { + start: 7, + length: 2, + }, + ], + ), + ]; + + for t in tests { + let header = t.0; + let size = t.1; + let expected = t.2; + + let res = HttpRange::parse(header, size); + + if res.is_err() { + if expected.is_empty() { + continue; + } else { + assert!( + false, + "parse({}, {}) returned error {:?}", + header, + size, + res.unwrap_err() + ); + } + } + + let got = res.unwrap(); + + if got.len() != expected.len() { + assert!( + false, + "len(parseRange({}, {})) = {}, want {}", + header, + size, + got.len(), + expected.len() + ); + continue; + } + + for i in 0..expected.len() { + if got[i].start != expected[i].start { + assert!( + false, + "parseRange({}, {})[{}].start = {}, want {}", + header, size, i, got[i].start, expected[i].start + ) + } + if got[i].length != expected[i].length { + assert!( + false, + "parseRange({}, {})[{}].length = {}, want {}", + header, size, i, got[i].length, expected[i].length + ) + } + } + } + } +} diff --git a/actix-staticfiles/src/named.rs b/actix-staticfiles/src/named.rs new file mode 100644 index 000000000..5fba0483a --- /dev/null +++ b/actix-staticfiles/src/named.rs @@ -0,0 +1,438 @@ +use std::fs::{File, Metadata}; +use std::io; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[cfg(unix)] +use std::os::unix::fs::MetadataExt; + +use mime; +use mime_guess::guess_mime_type; + +use actix_http::error::Error; +use actix_http::http::header::{self, ContentDisposition, DispositionParam}; +use actix_web::http::{ContentEncoding, Method, StatusCode}; +use actix_web::{HttpMessage, HttpRequest, HttpResponse, Responder}; + +use crate::config::{DefaultConfig, StaticFileConfig}; +use crate::{ChunkedReadFile, HttpRange}; + +/// A file with an associated name. +#[derive(Debug)] +pub struct NamedFile { + path: PathBuf, + file: File, + pub(crate) content_type: mime::Mime, + pub(crate) content_disposition: header::ContentDisposition, + pub(crate) md: Metadata, + modified: Option, + encoding: Option, + pub(crate) status_code: StatusCode, + _cd_map: PhantomData, +} + +impl NamedFile { + /// Creates an instance from a previously opened file. + /// + /// The given `path` need not exist and is only used to determine the `ContentType` and + /// `ContentDisposition` headers. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate actix_web; + /// + /// use actix_web::fs::NamedFile; + /// use std::io::{self, Write}; + /// use std::env; + /// use std::fs::File; + /// + /// fn main() -> io::Result<()> { + /// let mut file = File::create("foo.txt")?; + /// file.write_all(b"Hello, world!")?; + /// let named_file = NamedFile::from_file(file, "bar.txt")?; + /// Ok(()) + /// } + /// ``` + pub fn from_file>(file: File, path: P) -> io::Result { + Self::from_file_with_config(file, path, DefaultConfig) + } + + /// Attempts to open a file in read-only mode. + /// + /// # Examples + /// + /// ```rust,ignore + /// use actix_web::fs::NamedFile; + /// + /// let file = NamedFile::open("foo.txt"); + /// ``` + pub fn open>(path: P) -> io::Result { + Self::open_with_config(path, DefaultConfig) + } +} + +impl NamedFile { + /// Creates an instance from a previously opened file using the provided configuration. + /// + /// The given `path` need not exist and is only used to determine the `ContentType` and + /// `ContentDisposition` headers. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate actix_web; + /// + /// use actix_web::fs::{DefaultConfig, NamedFile}; + /// use std::io::{self, Write}; + /// use std::env; + /// use std::fs::File; + /// + /// fn main() -> io::Result<()> { + /// let mut file = File::create("foo.txt")?; + /// file.write_all(b"Hello, world!")?; + /// let named_file = NamedFile::from_file_with_config(file, "bar.txt", DefaultConfig)?; + /// Ok(()) + /// } + /// ``` + pub fn from_file_with_config>( + file: File, + path: P, + _: C, + ) -> io::Result> { + let path = path.as_ref().to_path_buf(); + + // Get the name of the file and use it to construct default Content-Type + // and Content-Disposition values + let (content_type, content_disposition) = { + let filename = match path.file_name() { + Some(name) => name.to_string_lossy(), + None => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Provided path has no filename", + )); + } + }; + + let ct = guess_mime_type(&path); + let disposition_type = C::content_disposition_map(ct.type_()); + let cd = ContentDisposition { + disposition: disposition_type, + parameters: vec![DispositionParam::Filename(filename.into_owned())], + }; + (ct, cd) + }; + + let md = file.metadata()?; + let modified = md.modified().ok(); + let encoding = None; + Ok(NamedFile { + path, + file, + content_type, + content_disposition, + md, + modified, + encoding, + status_code: StatusCode::OK, + _cd_map: PhantomData, + }) + } + + /// Attempts to open a file in read-only mode using provided configuration. + /// + /// # Examples + /// + /// ```rust,ignore + /// use actix_web::fs::{DefaultConfig, NamedFile}; + /// + /// let file = NamedFile::open_with_config("foo.txt", DefaultConfig); + /// ``` + pub fn open_with_config>( + path: P, + config: C, + ) -> io::Result> { + Self::from_file_with_config(File::open(&path)?, path, config) + } + + /// Returns reference to the underlying `File` object. + #[inline] + pub fn file(&self) -> &File { + &self.file + } + + /// Retrieve the path of this file. + /// + /// # Examples + /// + /// ```rust,ignore + /// # use std::io; + /// use actix_web::fs::NamedFile; + /// + /// # fn path() -> io::Result<()> { + /// let file = NamedFile::open("test.txt")?; + /// assert_eq!(file.path().as_os_str(), "foo.txt"); + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn path(&self) -> &Path { + self.path.as_path() + } + + /// Set response **Status Code** + pub fn set_status_code(mut self, status: StatusCode) -> Self { + self.status_code = status; + self + } + + /// Set the MIME Content-Type for serving this file. By default + /// the Content-Type is inferred from the filename extension. + #[inline] + pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self { + self.content_type = mime_type; + self + } + + /// Set the Content-Disposition for serving this file. This allows + /// changing the inline/attachment disposition as well as the filename + /// sent to the peer. By default the disposition is `inline` for text, + /// image, and video content types, and `attachment` otherwise, and + /// the filename is taken from the path provided in the `open` method + /// after converting it to UTF-8 using + /// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy). + #[inline] + pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self { + self.content_disposition = cd; + self + } + + /// Set content encoding for serving this file + #[inline] + pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { + self.encoding = Some(enc); + self + } + + pub(crate) fn etag(&self) -> Option { + // This etag format is similar to Apache's. + self.modified.as_ref().map(|mtime| { + let ino = { + #[cfg(unix)] + { + self.md.ino() + } + #[cfg(not(unix))] + { + 0 + } + }; + + let dur = mtime + .duration_since(UNIX_EPOCH) + .expect("modification time must be after epoch"); + header::EntityTag::strong(format!( + "{:x}:{:x}:{:x}:{:x}", + ino, + self.md.len(), + dur.as_secs(), + dur.subsec_nanos() + )) + }) + } + + pub(crate) fn last_modified(&self) -> Option { + self.modified.map(|mtime| mtime.into()) + } +} + +impl Deref for NamedFile { + type Target = File; + + fn deref(&self) -> &File { + &self.file + } +} + +impl DerefMut for NamedFile { + fn deref_mut(&mut self) -> &mut File { + &mut self.file + } +} + +/// Returns true if `req` has no `If-Match` header or one which matches `etag`. +fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { + match req.get_header::() { + None | Some(header::IfMatch::Any) => true, + Some(header::IfMatch::Items(ref items)) => { + if let Some(some_etag) = etag { + for item in items { + if item.strong_eq(some_etag) { + return true; + } + } + } + false + } + } +} + +/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`. +fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { + match req.get_header::() { + Some(header::IfNoneMatch::Any) => false, + Some(header::IfNoneMatch::Items(ref items)) => { + if let Some(some_etag) = etag { + for item in items { + if item.weak_eq(some_etag) { + return false; + } + } + } + true + } + None => true, + } +} + +impl Responder for NamedFile { + type Error = Error; + type Future = Result; + + fn respond_to(self, req: &HttpRequest) -> Self::Future { + if self.status_code != StatusCode::OK { + let mut resp = HttpResponse::build(self.status_code); + resp.set(header::ContentType(self.content_type.clone())) + .header( + header::CONTENT_DISPOSITION, + self.content_disposition.to_string(), + ); + // TODO blocking by compressing + // if let Some(current_encoding) = self.encoding { + // resp.content_encoding(current_encoding); + // } + let reader = ChunkedReadFile { + size: self.md.len(), + offset: 0, + file: Some(self.file), + fut: None, + counter: 0, + }; + return Ok(resp.streaming(reader)); + } + + if !C::is_method_allowed(req.method()) { + return Ok(HttpResponse::MethodNotAllowed() + .header(header::CONTENT_TYPE, "text/plain") + .header(header::ALLOW, "GET, HEAD") + .body("This resource only supports GET and HEAD.")); + } + + let etag = if C::is_use_etag() { self.etag() } else { None }; + let last_modified = if C::is_use_last_modifier() { + self.last_modified() + } else { + None + }; + + // check preconditions + let precondition_failed = if !any_match(etag.as_ref(), req) { + true + } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = + (last_modified, req.get_header()) + { + m > since + } else { + false + }; + + // check last modified + let not_modified = if !none_match(etag.as_ref(), req) { + true + } else if req.headers().contains_key(header::IF_NONE_MATCH) { + false + } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = + (last_modified, req.get_header()) + { + m <= since + } else { + false + }; + + let mut resp = HttpResponse::build(self.status_code); + resp.set(header::ContentType(self.content_type.clone())) + .header( + header::CONTENT_DISPOSITION, + self.content_disposition.to_string(), + ); + // TODO blocking by compressing + // if let Some(current_encoding) = self.encoding { + // resp.content_encoding(current_encoding); + // } + + resp.if_some(last_modified, |lm, resp| { + resp.set(header::LastModified(lm)); + }) + .if_some(etag, |etag, resp| { + resp.set(header::ETag(etag)); + }); + + resp.header(header::ACCEPT_RANGES, "bytes"); + + let mut length = self.md.len(); + let mut offset = 0; + + // check for range header + if let Some(ranges) = req.headers().get(header::RANGE) { + if let Ok(rangesheader) = ranges.to_str() { + if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) { + length = rangesvec[0].length; + offset = rangesvec[0].start; + // TODO blocking by compressing + // resp.content_encoding(ContentEncoding::Identity); + resp.header( + header::CONTENT_RANGE, + format!( + "bytes {}-{}/{}", + offset, + offset + length - 1, + self.md.len() + ), + ); + } else { + resp.header(header::CONTENT_RANGE, format!("bytes */{}", length)); + return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish()); + }; + } else { + return Ok(resp.status(StatusCode::BAD_REQUEST).finish()); + }; + }; + + resp.header(header::CONTENT_LENGTH, format!("{}", length)); + + if precondition_failed { + return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish()); + } else if not_modified { + return Ok(resp.status(StatusCode::NOT_MODIFIED).finish()); + } + + if *req.method() == Method::HEAD { + Ok(resp.finish()) + } else { + let reader = ChunkedReadFile { + offset, + size: length, + file: Some(self.file), + fut: None, + counter: 0, + }; + if offset != 0 || length != self.md.len() { + return Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader)); + }; + Ok(resp.streaming(reader)) + } + } +} diff --git a/actix-staticfiles/tests/test space.binary b/actix-staticfiles/tests/test space.binary new file mode 100644 index 000000000..ef8ff0245 --- /dev/null +++ b/actix-staticfiles/tests/test space.binary @@ -0,0 +1 @@ +ÂTǑɂVù2þvI ª–\ÇRË™–ˆæeÞvDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ.– û®õpA !ûߦÙx j+Uc÷±©X”c%Û;ï"yì­AI \ No newline at end of file diff --git a/actix-staticfiles/tests/test.binary b/actix-staticfiles/tests/test.binary new file mode 100644 index 000000000..ef8ff0245 --- /dev/null +++ b/actix-staticfiles/tests/test.binary @@ -0,0 +1 @@ +ÂTǑɂVù2þvI ª–\ÇRË™–ˆæeÞvDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ.– û®õpA !ûߦÙx j+Uc÷±©X”c%Û;ï"yì­AI \ No newline at end of file diff --git a/actix-staticfiles/tests/test.png b/actix-staticfiles/tests/test.png new file mode 100644 index 0000000000000000000000000000000000000000..6b7cdc0b8fb5a439d3bd19f210e89a37a2354e61 GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1SFYWcSQjy&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=h((rL&%EBHDibIqn;8;O;+&tGo0?Yw &'static str { @@ -29,19 +28,17 @@ fn main() -> std::io::Result<()> { .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) .middleware(middleware::Compress::default()) .resource("/resource1/index.html", |r| r.route(web::get().to(index))) - .service( - "/resource2/index.html", - Resource::new() - .middleware( - middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), - ) - .default_resource(|r| { - r.route(Route::new().to(|| HttpResponse::MethodNotAllowed())) - }) - .route(web::method(Method::GET).to_async(index_async)), - ) - .service("/test1.html", Resource::new().to(|| "Test\r\n")) - .service("/", Resource::new().to(no_params)) + .resource("/resource2/index.html", |r| { + r.middleware( + middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), + ) + .default_resource(|r| { + r.route(web::route().to(|| HttpResponse::MethodNotAllowed())) + }) + .route(web::method(Method::GET).to_async(index_async)) + }) + .resource("/test1.html", |r| r.to(|| "Test\r\n")) + .resource("/", |r| r.to(no_params)) }) .bind("127.0.0.1:8080")? .workers(1) diff --git a/src/app.rs b/src/app.rs index 27ca5c956..d503d8ddb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -24,8 +24,13 @@ type HttpService

    = BoxedService, ServiceResponse, ()>; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; type BoxedResponse = Box>; -pub trait HttpServiceFactory { - type Factory: NewService; +pub trait HttpServiceFactory

    { + type Factory: NewService< + ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >; fn rdef(&self) -> &ResourceDef; @@ -293,6 +298,29 @@ where } } + /// Register resource handler service. + pub fn service(self, service: F) -> AppRouter> + where + F: HttpServiceFactory

    + 'static, + { + let fref = Rc::new(RefCell::new(None)); + AppRouter { + chain: self.chain, + services: vec![( + service.rdef().clone(), + boxed::new_service(service.create().map_init_err(|_| ())), + None, + )], + default: None, + defaults: vec![], + endpoint: AppEntry::new(fref.clone()), + factory_ref: fref, + extensions: self.extensions, + state: self.state, + _t: PhantomData, + } + } + /// Set server host name. /// /// Host name is used by application router aa a hostname for url @@ -445,16 +473,15 @@ where } /// Register resource handler service. - pub fn service(mut self, rdef: R, factory: F) -> Self + pub fn service(mut self, factory: F) -> Self where - R: Into, - F: IntoNewService>, - U: NewService, Response = ServiceResponse, Error = ()> - + 'static, + F: HttpServiceFactory

    + 'static, { + let rdef = factory.rdef().clone(); + self.services.push(( - rdef.into(), - boxed::new_service(factory.into_new_service().map_init_err(|_| ())), + rdef, + boxed::new_service(factory.create().map_init_err(|_| ())), None, )); self diff --git a/src/lib.rs b/src/lib.rs index f21c5e43c..44dcde35b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,9 +19,9 @@ pub mod test; // re-export for convenience pub use actix_http::Response as HttpResponse; -pub use actix_http::{error, http, Error, HttpMessage, ResponseError, Result}; +pub use actix_http::{body, error, http, Error, HttpMessage, ResponseError, Result}; -pub use crate::app::{App, AppRouter}; +pub use crate::app::App; pub use crate::extract::{FromRequest, Json}; pub use crate::request::HttpRequest; pub use crate::resource::Resource; @@ -32,6 +32,26 @@ pub use crate::server::HttpServer; pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; pub use crate::state::State; +pub mod dev { + //! The `actix-web` prelude for library developers + //! + //! The purpose of this module is to alleviate imports of many common actix + //! traits by adding a glob import to the top of actix heavy modules: + //! + //! ``` + //! # #![allow(unused_imports)] + //! use actix_web::dev::*; + //! ``` + + pub use crate::app::{AppRouter, HttpServiceFactory}; + pub use actix_http::body::{Body, MessageBody, ResponseBody}; + pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder; + pub use actix_http::{ + Extensions, Payload, PayloadStream, RequestHead, ResponseHead, + }; + pub use actix_router::{Path, ResourceDef, Url}; +} + pub mod web { use actix_http::{http::Method, Error, Response}; use futures::IntoFuture; diff --git a/src/service.rs b/src/service.rs index 7d17527a4..c9666e31e 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,8 +1,9 @@ use std::borrow::Cow; use std::cell::{Ref, RefMut}; +use std::fmt; use std::rc::Rc; -use actix_http::body::{Body, ResponseBody}; +use actix_http::body::{Body, MessageBody, ResponseBody}; use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{ Error, Extensions, HttpMessage, Payload, PayloadStream, Request, RequestHead, @@ -123,6 +124,11 @@ impl

    ServiceRequest

    { pub fn app_extensions(&self) -> &Extensions { self.req.app_extensions() } + + /// Deconstruct request into parts + pub fn into_parts(self) -> (HttpRequest, Payload

    ) { + (self.req, self.payload) + } } impl

    Resource for ServiceRequest

    { @@ -172,6 +178,29 @@ impl

    std::ops::DerefMut for ServiceRequest

    { } } +impl

    fmt::Debug for ServiceRequest

    { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!( + f, + "\nServiceRequest {:?} {}:{}", + self.head().version, + self.head().method, + self.path() + )?; + if !self.query_string().is_empty() { + writeln!(f, " query: ?{:?}", self.query_string())?; + } + if !self.match_info().is_empty() { + writeln!(f, " params: {:?}", self.match_info())?; + } + writeln!(f, " headers:")?; + for (key, val) in self.headers().iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } +} + pub struct ServiceFromRequest

    { req: HttpRequest, payload: Payload

    , @@ -259,6 +288,16 @@ impl ServiceResponse { ServiceResponse { request, response } } + /// Create service response from the error + pub fn from_err>(err: E, request: HttpRequest) -> Self { + let e: Error = err.into(); + let res: Response = e.into(); + ServiceResponse { + request, + response: res.into_body(), + } + } + /// Get reference to original request #[inline] pub fn request(&self) -> &HttpRequest { @@ -303,6 +342,11 @@ impl ServiceResponse { } } } + + /// Extract response body + pub fn take_body(&mut self) -> ResponseBody { + self.response.take_body() + } } impl ServiceResponse { @@ -349,3 +393,21 @@ impl IntoFuture for ServiceResponse { ok(self) } } + +impl fmt::Debug for ServiceResponse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let res = writeln!( + f, + "\nServiceResponse {:?} {}{}", + self.response.head().version, + self.response.head().status, + self.response.head().reason.unwrap_or(""), + ); + let _ = writeln!(f, " headers:"); + for (key, val) in self.response.head().headers.iter() { + let _ = writeln!(f, " {:?}: {:?}", key, val); + } + let _ = writeln!(f, " body: {:?}", self.response.body().length()); + res + } +} diff --git a/src/test.rs b/src/test.rs index 22bfe0c39..ccc4b38ec 100644 --- a/src/test.rs +++ b/src/test.rs @@ -70,6 +70,34 @@ where block_on(app.into_new_service().new_service(&())).unwrap() } +/// Calls service and waits for response future completion. +/// +/// ```rust,ignore +/// use actix_web::{test, App, HttpResponse, http::StatusCode}; +/// use actix_service::Service; +/// +/// fn main() { +/// let mut app = test::init_service( +/// App::new() +/// .resource("/test", |r| r.to(|| HttpResponse::Ok())) +/// ); +/// +/// // Create request object +/// let req = test::TestRequest::with_uri("/test").to_request(); +/// +/// // Call application +/// let resp = test::call_succ_service(&mut app, req); +/// assert_eq!(resp.status(), StatusCode::OK); +/// } +/// ``` +pub fn call_success(app: &mut S, req: R) -> S::Response +where + S: Service, Error = E>, + E: std::fmt::Debug, +{ + block_on(app.call(req)).unwrap() +} + /// Test `Request` builder. /// /// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern. diff --git a/staticfiles/src/lib.rs b/staticfiles/src/lib.rs deleted file mode 100644 index c2ac5f3af..000000000 --- a/staticfiles/src/lib.rs +++ /dev/null @@ -1,2033 +0,0 @@ -//! Static files support -use std::cell::RefCell; -use std::fmt::Write; -use std::fs::{DirEntry, File, Metadata}; -use std::io::{Read, Seek}; -use std::marker::PhantomData; -use std::ops::{Deref, DerefMut}; -use std::path::{Path, PathBuf}; -use std::rc::Rc; -use std::time::{SystemTime, UNIX_EPOCH}; -use std::{cmp, io}; - -#[cfg(unix)] -use std::os::unix::fs::MetadataExt; - -use bytes::Bytes; -use derive_more::Display; -use futures::{Async, Future, Poll, Stream}; -use mime; -use mime_guess::{get_mime_type, guess_mime_type}; -use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; -use v_htmlescape::escape as escape_html_entity; - -use actix_http::error::{Error, ErrorInternalServerError, ResponseError}; -use actix_http::http::header::{ - self, ContentDisposition, DispositionParam, DispositionType, -}; -use actix_http::http::{ContentEncoding, Method, StatusCode}; -use actix_http::{HttpMessage, Response}; -use actix_service::boxed::BoxedNewService; -use actix_service::{NewService, Service}; -use actix_web::{ - blocking, FromRequest, HttpRequest, Responder, ServiceRequest, ServiceResponse, -}; -use futures::future::{err, ok, FutureResult}; - -type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; - -///Describes `StaticFiles` configiration -/// -///To configure actix's static resources you need -///to define own configiration type and implement any method -///you wish to customize. -///As trait implements reasonable defaults for Actix. -/// -///## Example -/// -///```rust,ignore -/// extern crate mime; -/// extern crate actix_web; -/// use actix_web::http::header::DispositionType; -/// use actix_web::fs::{StaticFileConfig, NamedFile}; -/// -/// #[derive(Default)] -/// struct MyConfig; -/// -/// impl StaticFileConfig for MyConfig { -/// fn content_disposition_map(typ: mime::Name) -> DispositionType { -/// DispositionType::Attachment -/// } -/// } -/// -/// let file = NamedFile::open_with_config("foo.txt", MyConfig); -///``` -pub trait StaticFileConfig: Default { - ///Describes mapping for mime type to content disposition header - /// - ///By default `IMAGE`, `TEXT` and `VIDEO` are mapped to Inline. - ///Others are mapped to Attachment - fn content_disposition_map(typ: mime::Name) -> DispositionType { - match typ { - mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, - _ => DispositionType::Attachment, - } - } - - ///Describes whether Actix should attempt to calculate `ETag` - /// - ///Defaults to `true` - fn is_use_etag() -> bool { - true - } - - ///Describes whether Actix should use last modified date of file. - /// - ///Defaults to `true` - fn is_use_last_modifier() -> bool { - true - } - - ///Describes allowed methods to access static resources. - /// - ///By default all methods are allowed - fn is_method_allowed(_method: &Method) -> bool { - true - } -} - -///Default content disposition as described in -///[StaticFileConfig](trait.StaticFileConfig.html) -#[derive(Default)] -pub struct DefaultConfig; - -impl StaticFileConfig for DefaultConfig {} - -/// Return the MIME type associated with a filename extension (case-insensitive). -/// If `ext` is empty or no associated type for the extension was found, returns -/// the type `application/octet-stream`. -#[inline] -pub fn file_extension_to_mime(ext: &str) -> mime::Mime { - get_mime_type(ext) -} - -/// A file with an associated name. -#[derive(Debug)] -pub struct NamedFile { - path: PathBuf, - file: File, - content_type: mime::Mime, - content_disposition: header::ContentDisposition, - md: Metadata, - modified: Option, - encoding: Option, - status_code: StatusCode, - _cd_map: PhantomData, -} - -impl NamedFile { - /// Creates an instance from a previously opened file. - /// - /// The given `path` need not exist and is only used to determine the `ContentType` and - /// `ContentDisposition` headers. - /// - /// # Examples - /// - /// ```rust,ignore - /// extern crate actix_web; - /// - /// use actix_web::fs::NamedFile; - /// use std::io::{self, Write}; - /// use std::env; - /// use std::fs::File; - /// - /// fn main() -> io::Result<()> { - /// let mut file = File::create("foo.txt")?; - /// file.write_all(b"Hello, world!")?; - /// let named_file = NamedFile::from_file(file, "bar.txt")?; - /// Ok(()) - /// } - /// ``` - pub fn from_file>(file: File, path: P) -> io::Result { - Self::from_file_with_config(file, path, DefaultConfig) - } - - /// Attempts to open a file in read-only mode. - /// - /// # Examples - /// - /// ```rust,ignore - /// use actix_web::fs::NamedFile; - /// - /// let file = NamedFile::open("foo.txt"); - /// ``` - pub fn open>(path: P) -> io::Result { - Self::open_with_config(path, DefaultConfig) - } -} - -impl NamedFile { - /// Creates an instance from a previously opened file using the provided configuration. - /// - /// The given `path` need not exist and is only used to determine the `ContentType` and - /// `ContentDisposition` headers. - /// - /// # Examples - /// - /// ```rust,ignore - /// extern crate actix_web; - /// - /// use actix_web::fs::{DefaultConfig, NamedFile}; - /// use std::io::{self, Write}; - /// use std::env; - /// use std::fs::File; - /// - /// fn main() -> io::Result<()> { - /// let mut file = File::create("foo.txt")?; - /// file.write_all(b"Hello, world!")?; - /// let named_file = NamedFile::from_file_with_config(file, "bar.txt", DefaultConfig)?; - /// Ok(()) - /// } - /// ``` - pub fn from_file_with_config>( - file: File, - path: P, - _: C, - ) -> io::Result> { - let path = path.as_ref().to_path_buf(); - - // Get the name of the file and use it to construct default Content-Type - // and Content-Disposition values - let (content_type, content_disposition) = { - let filename = match path.file_name() { - Some(name) => name.to_string_lossy(), - None => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Provided path has no filename", - )); - } - }; - - let ct = guess_mime_type(&path); - let disposition_type = C::content_disposition_map(ct.type_()); - let cd = ContentDisposition { - disposition: disposition_type, - parameters: vec![DispositionParam::Filename(filename.into_owned())], - }; - (ct, cd) - }; - - let md = file.metadata()?; - let modified = md.modified().ok(); - let encoding = None; - Ok(NamedFile { - path, - file, - content_type, - content_disposition, - md, - modified, - encoding, - status_code: StatusCode::OK, - _cd_map: PhantomData, - }) - } - - /// Attempts to open a file in read-only mode using provided configuration. - /// - /// # Examples - /// - /// ```rust,ignore - /// use actix_web::fs::{DefaultConfig, NamedFile}; - /// - /// let file = NamedFile::open_with_config("foo.txt", DefaultConfig); - /// ``` - pub fn open_with_config>( - path: P, - config: C, - ) -> io::Result> { - Self::from_file_with_config(File::open(&path)?, path, config) - } - - /// Returns reference to the underlying `File` object. - #[inline] - pub fn file(&self) -> &File { - &self.file - } - - /// Retrieve the path of this file. - /// - /// # Examples - /// - /// ```rust,ignore - /// # use std::io; - /// use actix_web::fs::NamedFile; - /// - /// # fn path() -> io::Result<()> { - /// let file = NamedFile::open("test.txt")?; - /// assert_eq!(file.path().as_os_str(), "foo.txt"); - /// # Ok(()) - /// # } - /// ``` - #[inline] - pub fn path(&self) -> &Path { - self.path.as_path() - } - - /// Set response **Status Code** - pub fn set_status_code(mut self, status: StatusCode) -> Self { - self.status_code = status; - self - } - - /// Set the MIME Content-Type for serving this file. By default - /// the Content-Type is inferred from the filename extension. - #[inline] - pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self { - self.content_type = mime_type; - self - } - - /// Set the Content-Disposition for serving this file. This allows - /// changing the inline/attachment disposition as well as the filename - /// sent to the peer. By default the disposition is `inline` for text, - /// image, and video content types, and `attachment` otherwise, and - /// the filename is taken from the path provided in the `open` method - /// after converting it to UTF-8 using - /// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy). - #[inline] - pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self { - self.content_disposition = cd; - self - } - - /// Set content encoding for serving this file - #[inline] - pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { - self.encoding = Some(enc); - self - } - - fn etag(&self) -> Option { - // This etag format is similar to Apache's. - self.modified.as_ref().map(|mtime| { - let ino = { - #[cfg(unix)] - { - self.md.ino() - } - #[cfg(not(unix))] - { - 0 - } - }; - - let dur = mtime - .duration_since(UNIX_EPOCH) - .expect("modification time must be after epoch"); - header::EntityTag::strong(format!( - "{:x}:{:x}:{:x}:{:x}", - ino, - self.md.len(), - dur.as_secs(), - dur.subsec_nanos() - )) - }) - } - - fn last_modified(&self) -> Option { - self.modified.map(|mtime| mtime.into()) - } -} - -impl Deref for NamedFile { - type Target = File; - - fn deref(&self) -> &File { - &self.file - } -} - -impl DerefMut for NamedFile { - fn deref_mut(&mut self) -> &mut File { - &mut self.file - } -} - -/// Returns true if `req` has no `If-Match` header or one which matches `etag`. -fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { - match req.get_header::() { - None | Some(header::IfMatch::Any) => true, - Some(header::IfMatch::Items(ref items)) => { - if let Some(some_etag) = etag { - for item in items { - if item.strong_eq(some_etag) { - return true; - } - } - } - false - } - } -} - -/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`. -fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { - match req.get_header::() { - Some(header::IfNoneMatch::Any) => false, - Some(header::IfNoneMatch::Items(ref items)) => { - if let Some(some_etag) = etag { - for item in items { - if item.weak_eq(some_etag) { - return false; - } - } - } - true - } - None => true, - } -} - -impl Responder for NamedFile { - type Error = Error; - type Future = FutureResult; - - fn respond_to(self, req: &HttpRequest) -> Self::Future { - if self.status_code != StatusCode::OK { - let mut resp = Response::build(self.status_code); - resp.set(header::ContentType(self.content_type.clone())) - .header( - header::CONTENT_DISPOSITION, - self.content_disposition.to_string(), - ); - // TODO blocking by compressing - // if let Some(current_encoding) = self.encoding { - // resp.content_encoding(current_encoding); - // } - let reader = ChunkedReadFile { - size: self.md.len(), - offset: 0, - file: Some(self.file), - fut: None, - counter: 0, - }; - return ok(resp.streaming(reader)); - } - - if !C::is_method_allowed(req.method()) { - return ok(Response::MethodNotAllowed() - .header(header::CONTENT_TYPE, "text/plain") - .header(header::ALLOW, "GET, HEAD") - .body("This resource only supports GET and HEAD.")); - } - - let etag = if C::is_use_etag() { self.etag() } else { None }; - let last_modified = if C::is_use_last_modifier() { - self.last_modified() - } else { - None - }; - - // check preconditions - let precondition_failed = if !any_match(etag.as_ref(), req) { - true - } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = - (last_modified, req.get_header()) - { - m > since - } else { - false - }; - - // check last modified - let not_modified = if !none_match(etag.as_ref(), req) { - true - } else if req.headers().contains_key(header::IF_NONE_MATCH) { - false - } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = - (last_modified, req.get_header()) - { - m <= since - } else { - false - }; - - let mut resp = Response::build(self.status_code); - resp.set(header::ContentType(self.content_type.clone())) - .header( - header::CONTENT_DISPOSITION, - self.content_disposition.to_string(), - ); - // TODO blocking by compressing - // if let Some(current_encoding) = self.encoding { - // resp.content_encoding(current_encoding); - // } - - resp.if_some(last_modified, |lm, resp| { - resp.set(header::LastModified(lm)); - }) - .if_some(etag, |etag, resp| { - resp.set(header::ETag(etag)); - }); - - resp.header(header::ACCEPT_RANGES, "bytes"); - - let mut length = self.md.len(); - let mut offset = 0; - - // check for range header - if let Some(ranges) = req.headers().get(header::RANGE) { - if let Ok(rangesheader) = ranges.to_str() { - if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) { - length = rangesvec[0].length; - offset = rangesvec[0].start; - // TODO blocking by compressing - // resp.content_encoding(ContentEncoding::Identity); - resp.header( - header::CONTENT_RANGE, - format!( - "bytes {}-{}/{}", - offset, - offset + length - 1, - self.md.len() - ), - ); - } else { - resp.header(header::CONTENT_RANGE, format!("bytes */{}", length)); - return ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish()); - }; - } else { - return ok(resp.status(StatusCode::BAD_REQUEST).finish()); - }; - }; - - resp.header(header::CONTENT_LENGTH, format!("{}", length)); - - if precondition_failed { - return ok(resp.status(StatusCode::PRECONDITION_FAILED).finish()); - } else if not_modified { - return ok(resp.status(StatusCode::NOT_MODIFIED).finish()); - } - - if *req.method() == Method::HEAD { - ok(resp.finish()) - } else { - let reader = ChunkedReadFile { - offset, - size: length, - file: Some(self.file), - fut: None, - counter: 0, - }; - if offset != 0 || length != self.md.len() { - return ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader)); - }; - ok(resp.streaming(reader)) - } - } -} - -#[doc(hidden)] -/// A helper created from a `std::fs::File` which reads the file -/// chunk-by-chunk on a `ThreadPool`. -pub struct ChunkedReadFile { - size: u64, - offset: u64, - file: Option, - fut: Option>, - counter: u64, -} - -fn handle_error(err: blocking::BlockingError) -> Error { - match err { - blocking::BlockingError::Error(err) => err.into(), - blocking::BlockingError::Canceled => { - ErrorInternalServerError("Unexpected error").into() - } - } -} - -impl Stream for ChunkedReadFile { - type Item = Bytes; - type Error = Error; - - fn poll(&mut self) -> Poll, Error> { - if self.fut.is_some() { - return match self.fut.as_mut().unwrap().poll().map_err(handle_error)? { - Async::Ready((file, bytes)) => { - self.fut.take(); - self.file = Some(file); - self.offset += bytes.len() as u64; - self.counter += bytes.len() as u64; - Ok(Async::Ready(Some(bytes))) - } - Async::NotReady => Ok(Async::NotReady), - }; - } - - let size = self.size; - let offset = self.offset; - let counter = self.counter; - - if size == counter { - Ok(Async::Ready(None)) - } else { - let mut file = self.file.take().expect("Use after completion"); - self.fut = Some(blocking::run(move || { - let max_bytes: usize; - max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; - let mut buf = Vec::with_capacity(max_bytes); - file.seek(io::SeekFrom::Start(offset))?; - let nbytes = - file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; - if nbytes == 0 { - return Err(io::ErrorKind::UnexpectedEof.into()); - } - Ok((file, Bytes::from(buf))) - })); - self.poll() - } - } -} - -type DirectoryRenderer = - Fn(&Directory, &HttpRequest) -> Result; - -/// A directory; responds with the generated directory listing. -#[derive(Debug)] -pub struct Directory { - /// Base directory - pub base: PathBuf, - /// Path of subdirectory to generate listing for - pub path: PathBuf, -} - -impl Directory { - /// Create a new directory - pub fn new(base: PathBuf, path: PathBuf) -> Directory { - Directory { base, path } - } - - /// Is this entry visible from this directory? - pub fn is_visible(&self, entry: &io::Result) -> bool { - if let Ok(ref entry) = *entry { - if let Some(name) = entry.file_name().to_str() { - if name.starts_with('.') { - return false; - } - } - if let Ok(ref md) = entry.metadata() { - let ft = md.file_type(); - return ft.is_dir() || ft.is_file() || ft.is_symlink(); - } - } - false - } -} - -// show file url as relative to static path -macro_rules! encode_file_url { - ($path:ident) => { - utf8_percent_encode(&$path.to_string_lossy(), DEFAULT_ENCODE_SET) - }; -} - -// " -- " & -- & ' -- ' < -- < > -- > / -- / -macro_rules! encode_file_name { - ($entry:ident) => { - escape_html_entity(&$entry.file_name().to_string_lossy()) - }; -} - -fn directory_listing( - dir: &Directory, - req: &HttpRequest, -) -> Result { - let index_of = format!("Index of {}", req.path()); - let mut body = String::new(); - let base = Path::new(req.path()); - - for entry in dir.path.read_dir()? { - if dir.is_visible(&entry) { - let entry = entry.unwrap(); - let p = match entry.path().strip_prefix(&dir.path) { - Ok(p) => base.join(p), - Err(_) => continue, - }; - - // if file is a directory, add '/' to the end of the name - if let Ok(metadata) = entry.metadata() { - if metadata.is_dir() { - let _ = write!( - body, - "

  • {}/
  • ", - encode_file_url!(p), - encode_file_name!(entry), - ); - } else { - let _ = write!( - body, - "
  • {}
  • ", - encode_file_url!(p), - encode_file_name!(entry), - ); - } - } else { - continue; - } - } - } - - let html = format!( - "\ - {}\ -

    {}

    \ -
      \ - {}\ -
    \n", - index_of, index_of, body - ); - Ok(ServiceResponse::new( - req.clone(), - Response::Ok() - .content_type("text/html; charset=utf-8") - .body(html), - )) -} - -/// Static files handling -/// -/// `StaticFile` handler must be registered with `App::handler()` method, -/// because `StaticFile` handler requires access sub-path information. -/// -/// ```rust,ignore -/// # extern crate actix_web; -/// use actix_web::{fs, App}; -/// -/// fn main() { -/// let app = App::new() -/// .handler("/static", fs::StaticFiles::new(".").unwrap()) -/// .finish(); -/// } -/// ``` -pub struct StaticFiles { - directory: PathBuf, - index: Option, - show_index: bool, - default: Rc>>>>, - renderer: Rc, - _chunk_size: usize, - _follow_symlinks: bool, - _cd_map: PhantomData, -} - -impl StaticFiles { - /// Create new `StaticFiles` instance for specified base directory. - /// - /// `StaticFile` uses `ThreadPool` for blocking filesystem operations. - /// By default pool with 5x threads of available cpus is used. - /// Pool size can be changed by setting ACTIX_CPU_POOL environment variable. - pub fn new>(dir: T) -> Result, Error> { - Self::with_config(dir, DefaultConfig) - } -} - -impl StaticFiles { - /// Create new `StaticFiles` instance for specified base directory. - /// - /// Identical with `new` but allows to specify configiration to use. - pub fn with_config>( - dir: T, - _: C, - ) -> Result, Error> { - let dir = dir.into().canonicalize()?; - - if !dir.is_dir() { - return Err(StaticFilesError::IsNotDirectory.into()); - } - - Ok(StaticFiles { - directory: dir, - index: None, - show_index: false, - default: Rc::new(RefCell::new(None)), - renderer: Rc::new(directory_listing), - _chunk_size: 0, - _follow_symlinks: false, - _cd_map: PhantomData, - }) - } - - /// Show files listing for directories. - /// - /// By default show files listing is disabled. - pub fn show_files_listing(mut self) -> Self { - self.show_index = true; - self - } - - /// Set custom directory renderer - pub fn files_listing_renderer(mut self, f: F) -> Self - where - for<'r, 's> F: - Fn(&'r Directory, &'s HttpRequest) -> Result - + 'static, - { - self.renderer = Rc::new(f); - self - } - - /// Set index file - /// - /// Shows specific index file for directory "/" instead of - /// showing files listing. - pub fn index_file>(mut self, index: T) -> StaticFiles { - self.index = Some(index.into()); - self - } -} - -impl NewService for StaticFiles { - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; - type Service = StaticFilesService; - type InitError = Error; - type Future = FutureResult; - - fn new_service(&self, _: &()) -> Self::Future { - ok(StaticFilesService { - directory: self.directory.clone(), - index: self.index.clone(), - show_index: self.show_index, - default: self.default.clone(), - renderer: self.renderer.clone(), - _chunk_size: self._chunk_size, - _follow_symlinks: self._follow_symlinks, - _cd_map: self._cd_map, - }) - } -} - -pub struct StaticFilesService { - directory: PathBuf, - index: Option, - show_index: bool, - default: Rc>>>>, - renderer: Rc, - _chunk_size: usize, - _follow_symlinks: bool, - _cd_map: PhantomData, -} - -impl Service for StaticFilesService { - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; - type Future = FutureResult; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - let mut req = req; - let real_path = match PathBufWrp::from_request(&mut req).poll() { - Ok(Async::Ready(item)) => item.0, - Ok(Async::NotReady) => unreachable!(), - Err(e) => return err(Error::from(e)), - }; - // full filepath - let path = match self.directory.join(&real_path).canonicalize() { - Ok(path) => path, - Err(e) => return err(Error::from(e)), - }; - - if path.is_dir() { - if let Some(ref redir_index) = self.index { - let path = path.join(redir_index); - - match NamedFile::open_with_config(path, C::default()) { - Ok(named_file) => match named_file.respond_to(&req).poll() { - Ok(Async::Ready(item)) => { - ok(ServiceResponse::new(req.clone(), item)) - } - Ok(Async::NotReady) => unreachable!(), - Err(e) => err(Error::from(e)), - }, - Err(e) => err(Error::from(e)), - } - } else if self.show_index { - let dir = Directory::new(self.directory.clone(), path); - let x = (self.renderer)(&dir, &req); - match x { - Ok(resp) => ok(resp), - Err(e) => err(Error::from(e)), - } - } else { - err(StaticFilesError::IsDirectory.into()) - } - } else { - match NamedFile::open_with_config(path, C::default()) { - Ok(named_file) => match named_file.respond_to(&req).poll() { - Ok(Async::Ready(item)) => { - ok(ServiceResponse::new(req.clone(), item)) - } - Ok(Async::NotReady) => unreachable!(), - Err(e) => err(Error::from(e)), - }, - Err(e) => err(Error::from(e)), - } - } - } -} - -struct PathBufWrp(PathBuf); - -impl

    FromRequest

    for PathBufWrp { - type Error = UriSegmentError; - type Future = FutureResult; - - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { - let path_str = req.match_info().path(); - let mut buf = PathBuf::new(); - for segment in path_str.split('/') { - if segment == ".." { - buf.pop(); - } else if segment.starts_with('.') { - return err(UriSegmentError::BadStart('.')); - } else if segment.starts_with('*') { - return err(UriSegmentError::BadStart('*')); - } else if segment.ends_with(':') { - return err(UriSegmentError::BadEnd(':')); - } else if segment.ends_with('>') { - return err(UriSegmentError::BadEnd('>')); - } else if segment.ends_with('<') { - return err(UriSegmentError::BadEnd('<')); - } else if segment.is_empty() { - continue; - } else if cfg!(windows) && segment.contains('\\') { - return err(UriSegmentError::BadChar('\\')); - } else { - buf.push(segment) - } - } - - ok(PathBufWrp(buf)) - } -} - -/// Errors which can occur when serving static files. -#[derive(Display, Debug, PartialEq)] -enum StaticFilesError { - /// Path is not a directory - #[display(fmt = "Path is not a directory. Unable to serve static files")] - IsNotDirectory, - - /// Cannot render directory - #[display(fmt = "Unable to render directory without index file")] - IsDirectory, -} - -/// Return `NotFound` for `StaticFilesError` -impl ResponseError for StaticFilesError { - fn error_response(&self) -> Response { - Response::new(StatusCode::NOT_FOUND) - } -} - -#[derive(Display, Debug, PartialEq)] -pub enum UriSegmentError { - /// The segment started with the wrapped invalid character. - #[display(fmt = "The segment started with the wrapped invalid character")] - BadStart(char), - /// The segment contained the wrapped invalid character. - #[display(fmt = "The segment contained the wrapped invalid character")] - BadChar(char), - /// The segment ended with the wrapped invalid character. - #[display(fmt = "The segment ended with the wrapped invalid character")] - BadEnd(char), -} - -/// Return `BadRequest` for `UriSegmentError` -impl ResponseError for UriSegmentError { - fn error_response(&self) -> Response { - Response::new(StatusCode::BAD_REQUEST) - } -} - -/// HTTP Range header representation. -#[derive(Debug, Clone, Copy)] -struct HttpRange { - pub start: u64, - pub length: u64, -} - -static PREFIX: &'static str = "bytes="; -const PREFIX_LEN: usize = 6; - -impl HttpRange { - /// Parses Range HTTP header string as per RFC 2616. - /// - /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). - /// `size` is full size of response (file). - fn parse(header: &str, size: u64) -> Result, ()> { - if header.is_empty() { - return Ok(Vec::new()); - } - if !header.starts_with(PREFIX) { - return Err(()); - } - - let size_sig = size as i64; - let mut no_overlap = false; - - let all_ranges: Vec> = header[PREFIX_LEN..] - .split(',') - .map(|x| x.trim()) - .filter(|x| !x.is_empty()) - .map(|ra| { - let mut start_end_iter = ra.split('-'); - - let start_str = start_end_iter.next().ok_or(())?.trim(); - let end_str = start_end_iter.next().ok_or(())?.trim(); - - if start_str.is_empty() { - // If no start is specified, end specifies the - // range start relative to the end of the file. - let mut length: i64 = end_str.parse().map_err(|_| ())?; - - if length > size_sig { - length = size_sig; - } - - Ok(Some(HttpRange { - start: (size_sig - length) as u64, - length: length as u64, - })) - } else { - let start: i64 = start_str.parse().map_err(|_| ())?; - - if start < 0 { - return Err(()); - } - if start >= size_sig { - no_overlap = true; - return Ok(None); - } - - let length = if end_str.is_empty() { - // If no end is specified, range extends to end of the file. - size_sig - start - } else { - let mut end: i64 = end_str.parse().map_err(|_| ())?; - - if start > end { - return Err(()); - } - - if end >= size_sig { - end = size_sig - 1; - } - - end - start + 1 - }; - - Ok(Some(HttpRange { - start: start as u64, - length: length as u64, - })) - } - }) - .collect::>()?; - - let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); - - if no_overlap && ranges.is_empty() { - return Err(()); - } - - Ok(ranges) - } -} - -// #[cfg(test)] -// mod tests { -// use std::fs; -// use std::ops::Add; -// use std::time::Duration; - -// use super::*; -// use application::App; -// use body::{Binary, Body}; -// use http::{header, Method, StatusCode}; -// use test::{self, TestRequest}; - -// #[test] -// fn test_file_extension_to_mime() { -// let m = file_extension_to_mime("jpg"); -// assert_eq!(m, mime::IMAGE_JPEG); - -// let m = file_extension_to_mime("invalid extension!!"); -// assert_eq!(m, mime::APPLICATION_OCTET_STREAM); - -// let m = file_extension_to_mime(""); -// assert_eq!(m, mime::APPLICATION_OCTET_STREAM); -// } - -// #[test] -// fn test_if_modified_since_without_if_none_match() { -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// let since = -// header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); - -// let req = TestRequest::default() -// .header(header::IF_MODIFIED_SINCE, since) -// .finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); -// } - -// #[test] -// fn test_if_modified_since_with_if_none_match() { -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// let since = -// header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); - -// let req = TestRequest::default() -// .header(header::IF_NONE_MATCH, "miss_etag") -// .header(header::IF_MODIFIED_SINCE, since) -// .finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); -// } - -// #[test] -// fn test_named_file_text() { -// assert!(NamedFile::open("test--").is_err()); -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/x-toml" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"Cargo.toml\"" -// ); -// } - -// #[test] -// fn test_named_file_set_content_type() { -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_content_type(mime::TEXT_XML) -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/xml" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"Cargo.toml\"" -// ); -// } - -// #[test] -// fn test_named_file_image() { -// let mut file = NamedFile::open("tests/test.png") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "image/png" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"test.png\"" -// ); -// } - -// #[test] -// fn test_named_file_image_attachment() { -// use header::{ContentDisposition, DispositionParam, DispositionType}; -// let cd = ContentDisposition { -// disposition: DispositionType::Attachment, -// parameters: vec![DispositionParam::Filename(String::from("test.png"))], -// }; -// let mut file = NamedFile::open("tests/test.png") -// .unwrap() -// .set_content_disposition(cd) -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "image/png" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "attachment; filename=\"test.png\"" -// ); -// } - -// #[derive(Default)] -// pub struct AllAttachmentConfig; -// impl StaticFileConfig for AllAttachmentConfig { -// fn content_disposition_map(_typ: mime::Name) -> DispositionType { -// DispositionType::Attachment -// } -// } - -// #[derive(Default)] -// pub struct AllInlineConfig; -// impl StaticFileConfig for AllInlineConfig { -// fn content_disposition_map(_typ: mime::Name) -> DispositionType { -// DispositionType::Inline -// } -// } - -// #[test] -// fn test_named_file_image_attachment_and_custom_config() { -// let file = NamedFile::open_with_config("tests/test.png", AllAttachmentConfig) -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "image/png" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "attachment; filename=\"test.png\"" -// ); - -// let file = NamedFile::open_with_config("tests/test.png", AllInlineConfig) -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "image/png" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"test.png\"" -// ); -// } - -// #[test] -// fn test_named_file_binary() { -// let mut file = NamedFile::open("tests/test.binary") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "application/octet-stream" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "attachment; filename=\"test.binary\"" -// ); -// } - -// #[test] -// fn test_named_file_status_code_text() { -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_status_code(StatusCode::NOT_FOUND) -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/x-toml" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"Cargo.toml\"" -// ); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); -// } - -// #[test] -// fn test_named_file_ranges_status_code() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), -// ) -// }); - -// // Valid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/Cargo.toml")) -// .header(header::RANGE, "bytes=10-20") -// .finish() -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); - -// // Invalid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/Cargo.toml")) -// .header(header::RANGE, "bytes=1-0") -// .finish() -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); - -// assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); -// } - -// #[test] -// fn test_named_file_content_range_headers() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".") -// .unwrap() -// .index_file("tests/test.binary"), -// ) -// }); - -// // Valid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .header(header::RANGE, "bytes=10-20") -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentrange = response -// .headers() -// .get(header::CONTENT_RANGE) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentrange, "bytes 10-20/100"); - -// // Invalid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .header(header::RANGE, "bytes=10-5") -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentrange = response -// .headers() -// .get(header::CONTENT_RANGE) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentrange, "bytes */100"); -// } - -// #[test] -// fn test_named_file_content_length_headers() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".") -// .unwrap() -// .index_file("tests/test.binary"), -// ) -// }); - -// // Valid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .header(header::RANGE, "bytes=10-20") -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentlength = response -// .headers() -// .get(header::CONTENT_LENGTH) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentlength, "11"); - -// // Invalid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .header(header::RANGE, "bytes=10-8") -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentlength = response -// .headers() -// .get(header::CONTENT_LENGTH) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentlength, "0"); - -// // Without range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .no_default_headers() -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentlength = response -// .headers() -// .get(header::CONTENT_LENGTH) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentlength, "100"); - -// // chunked -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); -// { -// let te = response -// .headers() -// .get(header::TRANSFER_ENCODING) -// .unwrap() -// .to_str() -// .unwrap(); -// assert_eq!(te, "chunked"); -// } -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("tests/test.binary").unwrap()); -// assert_eq!(bytes, data); -// } - -// #[test] -// fn test_static_files_with_spaces() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new() -// .handler("/", StaticFiles::new(".").unwrap().index_file("Cargo.toml")) -// }); -// let request = srv -// .get() -// .uri(srv.url("/tests/test%20space.binary")) -// .finish() -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); - -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); -// assert_eq!(bytes, data); -// } - -// #[derive(Default)] -// pub struct OnlyMethodHeadConfig; -// impl StaticFileConfig for OnlyMethodHeadConfig { -// fn is_method_allowed(method: &Method) -> bool { -// match *method { -// Method::HEAD => true, -// _ => false, -// } -// } -// } - -// #[test] -// fn test_named_file_not_allowed() { -// let file = -// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); -// let req = TestRequest::default().method(Method::POST).finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - -// let file = -// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); -// let req = TestRequest::default().method(Method::PUT).finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - -// let file = -// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); -// let req = TestRequest::default().method(Method::GET).finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); -// } - -// #[test] -// fn test_named_file_content_encoding() { -// let req = TestRequest::default().method(Method::GET).finish(); -// let file = NamedFile::open("Cargo.toml").unwrap(); - -// assert!(file.encoding.is_none()); -// let resp = file -// .set_content_encoding(ContentEncoding::Identity) -// .respond_to(&req) -// .unwrap(); - -// assert!(resp.content_encoding().is_some()); -// assert_eq!(resp.content_encoding().unwrap().as_str(), "identity"); -// } - -// #[test] -// fn test_named_file_any_method() { -// let req = TestRequest::default().method(Method::POST).finish(); -// let file = NamedFile::open("Cargo.toml").unwrap(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::OK); -// } - -// #[test] -// fn test_static_files() { -// let mut st = StaticFiles::new(".").unwrap().show_files_listing(); -// let req = TestRequest::with_uri("/missing") -// .param("tail", "missing") -// .finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); - -// st.show_index = false; -// let req = TestRequest::default().finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); - -// let req = TestRequest::default().param("tail", "").finish(); - -// st.show_index = true; -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/html; charset=utf-8" -// ); -// assert!(resp.body().is_binary()); -// assert!(format!("{:?}", resp.body()).contains("README.md")); -// } - -// #[test] -// fn test_static_files_bad_directory() { -// let st: Result, Error> = StaticFiles::new("missing"); -// assert!(st.is_err()); - -// let st: Result, Error> = StaticFiles::new("Cargo.toml"); -// assert!(st.is_err()); -// } - -// #[test] -// fn test_default_handler_file_missing() { -// let st = StaticFiles::new(".") -// .unwrap() -// .default_handler(|_: &_| "default content"); -// let req = TestRequest::with_uri("/missing") -// .param("tail", "missing") -// .finish(); - -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::OK); -// assert_eq!( -// resp.body(), -// &Body::Binary(Binary::Slice(b"default content")) -// ); -// } - -// #[test] -// fn test_serve_index() { -// let st = StaticFiles::new(".").unwrap().index_file("test.binary"); -// let req = TestRequest::default().uri("/tests").finish(); - -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::OK); -// assert_eq!( -// resp.headers() -// .get(header::CONTENT_TYPE) -// .expect("content type"), -// "application/octet-stream" -// ); -// assert_eq!( -// resp.headers() -// .get(header::CONTENT_DISPOSITION) -// .expect("content disposition"), -// "attachment; filename=\"test.binary\"" -// ); - -// let req = TestRequest::default().uri("/tests/").finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::OK); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "application/octet-stream" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "attachment; filename=\"test.binary\"" -// ); - -// // nonexistent index file -// let req = TestRequest::default().uri("/tests/unknown").finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); - -// let req = TestRequest::default().uri("/tests/unknown/").finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); -// } - -// #[test] -// fn test_serve_index_nested() { -// let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); -// let req = TestRequest::default().uri("/src/client").finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::OK); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/x-rust" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"mod.rs\"" -// ); -// } - -// #[test] -// fn integration_serve_index_with_prefix() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new() -// .prefix("public") -// .handler("/", StaticFiles::new(".").unwrap().index_file("Cargo.toml")) -// }); - -// let request = srv.get().uri(srv.url("/public")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); -// assert_eq!(bytes, data); - -// let request = srv.get().uri(srv.url("/public/")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); -// assert_eq!(bytes, data); -// } - -// #[test] -// fn integration_serve_index() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), -// ) -// }); - -// let request = srv.get().uri(srv.url("/test")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); -// assert_eq!(bytes, data); - -// let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); -// assert_eq!(bytes, data); - -// // nonexistent index file -// let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::NOT_FOUND); - -// let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::NOT_FOUND); -// } - -// #[test] -// fn integration_percent_encoded() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), -// ) -// }); - -// let request = srv -// .get() -// .uri(srv.url("/test/%43argo.toml")) -// .finish() -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// } - -// struct T(&'static str, u64, Vec); - -// #[test] -// fn test_parse() { -// let tests = vec![ -// T("", 0, vec![]), -// T("", 1000, vec![]), -// T("foo", 0, vec![]), -// T("bytes=", 0, vec![]), -// T("bytes=7", 10, vec![]), -// T("bytes= 7 ", 10, vec![]), -// T("bytes=1-", 0, vec![]), -// T("bytes=5-4", 10, vec![]), -// T("bytes=0-2,5-4", 10, vec![]), -// T("bytes=2-5,4-3", 10, vec![]), -// T("bytes=--5,4--3", 10, vec![]), -// T("bytes=A-", 10, vec![]), -// T("bytes=A- ", 10, vec![]), -// T("bytes=A-Z", 10, vec![]), -// T("bytes= -Z", 10, vec![]), -// T("bytes=5-Z", 10, vec![]), -// T("bytes=Ran-dom, garbage", 10, vec![]), -// T("bytes=0x01-0x02", 10, vec![]), -// T("bytes= ", 10, vec![]), -// T("bytes= , , , ", 10, vec![]), -// T( -// "bytes=0-9", -// 10, -// vec![HttpRange { -// start: 0, -// length: 10, -// }], -// ), -// T( -// "bytes=0-", -// 10, -// vec![HttpRange { -// start: 0, -// length: 10, -// }], -// ), -// T( -// "bytes=5-", -// 10, -// vec![HttpRange { -// start: 5, -// length: 5, -// }], -// ), -// T( -// "bytes=0-20", -// 10, -// vec![HttpRange { -// start: 0, -// length: 10, -// }], -// ), -// T( -// "bytes=15-,0-5", -// 10, -// vec![HttpRange { -// start: 0, -// length: 6, -// }], -// ), -// T( -// "bytes=1-2,5-", -// 10, -// vec![ -// HttpRange { -// start: 1, -// length: 2, -// }, -// HttpRange { -// start: 5, -// length: 5, -// }, -// ], -// ), -// T( -// "bytes=-2 , 7-", -// 11, -// vec![ -// HttpRange { -// start: 9, -// length: 2, -// }, -// HttpRange { -// start: 7, -// length: 4, -// }, -// ], -// ), -// T( -// "bytes=0-0 ,2-2, 7-", -// 11, -// vec![ -// HttpRange { -// start: 0, -// length: 1, -// }, -// HttpRange { -// start: 2, -// length: 1, -// }, -// HttpRange { -// start: 7, -// length: 4, -// }, -// ], -// ), -// T( -// "bytes=-5", -// 10, -// vec![HttpRange { -// start: 5, -// length: 5, -// }], -// ), -// T( -// "bytes=-15", -// 10, -// vec![HttpRange { -// start: 0, -// length: 10, -// }], -// ), -// T( -// "bytes=0-499", -// 10000, -// vec![HttpRange { -// start: 0, -// length: 500, -// }], -// ), -// T( -// "bytes=500-999", -// 10000, -// vec![HttpRange { -// start: 500, -// length: 500, -// }], -// ), -// T( -// "bytes=-500", -// 10000, -// vec![HttpRange { -// start: 9500, -// length: 500, -// }], -// ), -// T( -// "bytes=9500-", -// 10000, -// vec![HttpRange { -// start: 9500, -// length: 500, -// }], -// ), -// T( -// "bytes=0-0,-1", -// 10000, -// vec![ -// HttpRange { -// start: 0, -// length: 1, -// }, -// HttpRange { -// start: 9999, -// length: 1, -// }, -// ], -// ), -// T( -// "bytes=500-600,601-999", -// 10000, -// vec![ -// HttpRange { -// start: 500, -// length: 101, -// }, -// HttpRange { -// start: 601, -// length: 399, -// }, -// ], -// ), -// T( -// "bytes=500-700,601-999", -// 10000, -// vec![ -// HttpRange { -// start: 500, -// length: 201, -// }, -// HttpRange { -// start: 601, -// length: 399, -// }, -// ], -// ), -// // Match Apache laxity: -// T( -// "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", -// 11, -// vec![ -// HttpRange { -// start: 1, -// length: 2, -// }, -// HttpRange { -// start: 4, -// length: 2, -// }, -// HttpRange { -// start: 7, -// length: 2, -// }, -// ], -// ), -// ]; - -// for t in tests { -// let header = t.0; -// let size = t.1; -// let expected = t.2; - -// let res = HttpRange::parse(header, size); - -// if res.is_err() { -// if expected.is_empty() { -// continue; -// } else { -// assert!( -// false, -// "parse({}, {}) returned error {:?}", -// header, -// size, -// res.unwrap_err() -// ); -// } -// } - -// let got = res.unwrap(); - -// if got.len() != expected.len() { -// assert!( -// false, -// "len(parseRange({}, {})) = {}, want {}", -// header, -// size, -// got.len(), -// expected.len() -// ); -// continue; -// } - -// for i in 0..expected.len() { -// if got[i].start != expected[i].start { -// assert!( -// false, -// "parseRange({}, {})[{}].start = {}, want {}", -// header, size, i, got[i].start, expected[i].start -// ) -// } -// if got[i].length != expected[i].length { -// assert!( -// false, -// "parseRange({}, {})[{}].length = {}, want {}", -// header, size, i, got[i].length, expected[i].length -// ) -// } -// } -// } -// } -// } From ceca96da281ec0b7d3bdce202f1eb84d2447ce15 Mon Sep 17 00:00:00 2001 From: Stephen Ellis Date: Wed, 6 Mar 2019 01:56:12 -0800 Subject: [PATCH 209/427] Added HTTP Authentication for Client (#540) --- CHANGES.md | 2 ++ src/client/request.rs | 24 ++++++++++++++++++++++++ tests/test_client.rs | 28 ++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 1a3260286..8cce71597 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,8 @@ * Add `insert` and `remove` methods to `HttpResponseBuilder` +* Add client HTTP Authentication methods `.basic_auth()` and `.bearer_auth()`. #540 + ### Fixed * Ignored the `If-Modified-Since` if `If-None-Match` is specified. #680 diff --git a/src/client/request.rs b/src/client/request.rs index ad08ad135..89789933c 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -12,6 +12,7 @@ use serde::Serialize; use serde_json; use serde_urlencoded; use url::Url; +use base64::encode; use super::connector::{ClientConnector, Connection}; use super::pipeline::SendRequest; @@ -485,6 +486,29 @@ impl ClientRequestBuilder { self } + /// Set HTTP basic authorization + pub fn basic_auth(&mut self, username: U, password: Option

    ) -> &mut Self + where + U: fmt::Display, + P: fmt::Display, + { + let auth = match password { + Some(password) => format!("{}:{}", username, password), + None => format!("{}", username) + }; + let header_value = format!("Basic {}", encode(&auth)); + self.header(header::AUTHORIZATION, &*header_value) + } + + /// Set HTTP bearer authentication + pub fn bearer_auth( &mut self, token: T) -> &mut Self + where + T: fmt::Display, + { + let header_value = format!("Bearer {}", token); + self.header(header::AUTHORIZATION, &*header_value) + } + /// Set content length #[inline] pub fn content_length(&mut self, len: u64) -> &mut Self { diff --git a/tests/test_client.rs b/tests/test_client.rs index 9808f3e6f..f3151e3ab 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -506,3 +506,31 @@ fn client_read_until_eof() { let bytes = sys.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(b"welcome!")); } + +#[test] +fn client_basic_auth() { + let mut srv = + test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); + /// set authorization header to Basic + let request = srv + .get() + .basic_auth("username", Some("password")) + .finish() + .unwrap(); + let repr = format!("{:?}", request); + assert!(repr.contains("Basic dXNlcm5hbWU6cGFzc3dvcmQ=")); +} + +#[test] +fn client_bearer_auth() { + let mut srv = + test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); + /// set authorization header to Bearer + let request = srv + .get() + .bearer_auth("someS3cr3tAutht0k3n") + .finish() + .unwrap(); + let repr = format!("{:?}", request); + assert!(repr.contains("Bearer someS3cr3tAutht0k3n")); +} \ No newline at end of file From 3fc28c5d073abd131f3988019553409e0a14616c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 09:27:02 -0800 Subject: [PATCH 210/427] simplify StaticFile constructor, move HttpRange to separate module --- actix-staticfiles/src/lib.rs | 447 +++------------------------------ actix-staticfiles/src/named.rs | 5 +- actix-staticfiles/src/range.rs | 375 +++++++++++++++++++++++++++ 3 files changed, 407 insertions(+), 420 deletions(-) create mode 100644 actix-staticfiles/src/range.rs diff --git a/actix-staticfiles/src/lib.rs b/actix-staticfiles/src/lib.rs index 01306d4b3..81d8269c9 100644 --- a/actix-staticfiles/src/lib.rs +++ b/actix-staticfiles/src/lib.rs @@ -27,10 +27,12 @@ use futures::future::{ok, FutureResult}; mod config; mod error; mod named; +mod range; use self::error::{StaticFilesError, UriSegmentError}; pub use crate::config::{DefaultConfig, StaticFileConfig}; pub use crate::named::NamedFile; +pub use crate::range::HttpRange; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; @@ -212,17 +214,15 @@ fn directory_listing( /// Static files handling /// -/// `StaticFile` handler must be registered with `App::handler()` method, -/// because `StaticFile` handler requires access sub-path information. +/// `StaticFile` handler must be registered with `App::service()` method. /// -/// ```rust,ignore -/// # extern crate actix_web; -/// use actix_web::{fs, App}; +/// ```rust +/// use actix_web::App; +/// use actix_staticfiles as fs; /// /// fn main() { /// let app = App::new() -/// .handler("/static", fs::StaticFiles::new(".").unwrap()) -/// .finish(); +/// .service(fs::StaticFiles::new("/static", ".")); /// } /// ``` pub struct StaticFiles { @@ -243,7 +243,7 @@ impl StaticFiles { /// `StaticFile` uses `ThreadPool` for blocking filesystem operations. /// By default pool with 5x threads of available cpus is used. /// Pool size can be changed by setting ACTIX_CPU_POOL environment variable. - pub fn new>(path: &str, dir: T) -> Result, Error> { + pub fn new>(path: &str, dir: T) -> StaticFiles { Self::with_config(path, dir, DefaultConfig) } } @@ -252,18 +252,13 @@ impl StaticFiles { /// Create new `StaticFiles` instance for specified base directory. /// /// Identical with `new` but allows to specify configiration to use. - pub fn with_config>( - path: &str, - dir: T, - _: C, - ) -> Result, Error> { - let dir = dir.into().canonicalize()?; - + pub fn with_config>(path: &str, dir: T, _: C) -> StaticFiles { + let dir = dir.into().canonicalize().unwrap_or_else(|_| PathBuf::new()); if !dir.is_dir() { - return Err(StaticFilesError::IsNotDirectory.into()); + log::error!("Specified path is not a directory"); } - Ok(StaticFiles { + StaticFiles { path: ResourceDef::root_prefix(path), directory: dir, index: None, @@ -273,7 +268,7 @@ impl StaticFiles { _chunk_size: 0, _follow_symlinks: false, _cd_map: PhantomData, - }) + } } /// Show files listing for directories. @@ -452,101 +447,6 @@ impl

    FromRequest

    for PathBufWrp { } } -/// HTTP Range header representation. -#[derive(Debug, Clone, Copy)] -struct HttpRange { - pub start: u64, - pub length: u64, -} - -static PREFIX: &'static str = "bytes="; -const PREFIX_LEN: usize = 6; - -impl HttpRange { - /// Parses Range HTTP header string as per RFC 2616. - /// - /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). - /// `size` is full size of response (file). - fn parse(header: &str, size: u64) -> Result, ()> { - if header.is_empty() { - return Ok(Vec::new()); - } - if !header.starts_with(PREFIX) { - return Err(()); - } - - let size_sig = size as i64; - let mut no_overlap = false; - - let all_ranges: Vec> = header[PREFIX_LEN..] - .split(',') - .map(|x| x.trim()) - .filter(|x| !x.is_empty()) - .map(|ra| { - let mut start_end_iter = ra.split('-'); - - let start_str = start_end_iter.next().ok_or(())?.trim(); - let end_str = start_end_iter.next().ok_or(())?.trim(); - - if start_str.is_empty() { - // If no start is specified, end specifies the - // range start relative to the end of the file. - let mut length: i64 = end_str.parse().map_err(|_| ())?; - - if length > size_sig { - length = size_sig; - } - - Ok(Some(HttpRange { - start: (size_sig - length) as u64, - length: length as u64, - })) - } else { - let start: i64 = start_str.parse().map_err(|_| ())?; - - if start < 0 { - return Err(()); - } - if start >= size_sig { - no_overlap = true; - return Ok(None); - } - - let length = if end_str.is_empty() { - // If no end is specified, range extends to end of the file. - size_sig - start - } else { - let mut end: i64 = end_str.parse().map_err(|_| ())?; - - if start > end { - return Err(()); - } - - if end >= size_sig { - end = size_sig - 1; - } - - end - start + 1 - }; - - Ok(Some(HttpRange { - start: start as u64, - length: length as u64, - })) - } - }) - .collect::>()?; - - let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); - - if no_overlap && ranges.is_empty() { - return Err(()); - } - - Ok(ranges) - } -} - #[cfg(test)] mod tests { use std::fs; @@ -800,11 +700,7 @@ mod tests { #[test] fn test_named_file_ranges_status_code() { let mut srv = test::init_service( - App::new().service( - StaticFiles::new("/test", ".") - .unwrap() - .index_file("Cargo.toml"), - ), + App::new().service(StaticFiles::new("/test", ".").index_file("Cargo.toml")), ); // Valid range header @@ -828,11 +724,8 @@ mod tests { #[test] fn test_named_file_content_range_headers() { let mut srv = test::init_service( - App::new().service( - StaticFiles::new("/test", ".") - .unwrap() - .index_file("tests/test.binary"), - ), + App::new() + .service(StaticFiles::new("/test", ".").index_file("tests/test.binary")), ); // Valid range header @@ -871,11 +764,8 @@ mod tests { #[test] fn test_named_file_content_length_headers() { let mut srv = test::init_service( - App::new().service( - StaticFiles::new("test", ".") - .unwrap() - .index_file("tests/test.binary"), - ), + App::new() + .service(StaticFiles::new("test", ".").index_file("tests/test.binary")), ); // Valid range header @@ -948,8 +838,7 @@ mod tests { #[test] fn test_static_files_with_spaces() { let mut srv = test::init_service( - App::new() - .service(StaticFiles::new("/", ".").unwrap().index_file("Cargo.toml")), + App::new().service(StaticFiles::new("/", ".").index_file("Cargo.toml")), ); let request = TestRequest::get() .uri("/tests/test%20space.binary") @@ -1030,22 +919,21 @@ mod tests { #[test] fn test_static_files() { let mut srv = test::init_service( - App::new().service(StaticFiles::new("/", ".").unwrap().show_files_listing()), + App::new().service(StaticFiles::new("/", ".").show_files_listing()), ); let req = TestRequest::with_uri("/missing").to_request(); let resp = test::call_success(&mut srv, req); assert_eq!(resp.status(), StatusCode::NOT_FOUND); - let mut srv = - test::init_service(App::new().service(StaticFiles::new("/", ".").unwrap())); + let mut srv = test::init_service(App::new().service(StaticFiles::new("/", "."))); let req = TestRequest::default().to_request(); let resp = test::call_success(&mut srv, req); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let mut srv = test::init_service( - App::new().service(StaticFiles::new("/", ".").unwrap().show_files_listing()), + App::new().service(StaticFiles::new("/", ".").show_files_listing()), ); let req = TestRequest::with_uri("/tests").to_request(); let mut resp = test::call_success(&mut srv, req); @@ -1065,17 +953,13 @@ mod tests { #[test] fn test_static_files_bad_directory() { - let st: Result, Error> = StaticFiles::new("/", "missing"); - assert!(st.is_err()); - - let st: Result, Error> = StaticFiles::new("/", "Cargo.toml"); - assert!(st.is_err()); + let _st: StaticFiles<()> = StaticFiles::new("/", "missing"); + let _st: StaticFiles<()> = StaticFiles::new("/", "Cargo.toml"); } // #[test] // fn test_default_handler_file_missing() { // let st = StaticFiles::new(".") - // .unwrap() // .default_handler(|_: &_| "default content"); // let req = TestRequest::with_uri("/missing") // .param("tail", "missing") @@ -1092,7 +976,7 @@ mod tests { // #[test] // fn test_serve_index() { - // let st = StaticFiles::new(".").unwrap().index_file("test.binary"); + // let st = StaticFiles::new(".").index_file("test.binary"); // let req = TestRequest::default().uri("/tests").finish(); // let resp = st.handle(&req).respond_to(&req).unwrap(); @@ -1138,7 +1022,7 @@ mod tests { // #[test] // fn test_serve_index_nested() { - // let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); + // let st = StaticFiles::new(".").index_file("mod.rs"); // let req = TestRequest::default().uri("/src/client").finish(); // let resp = st.handle(&req).respond_to(&req).unwrap(); // let resp = resp.as_msg(); @@ -1158,7 +1042,7 @@ mod tests { // let mut srv = test::TestServer::with_factory(|| { // App::new().handler( // "test", - // StaticFiles::new(".").unwrap().index_file("Cargo.toml"), + // StaticFiles::new(".").index_file("Cargo.toml"), // ) // }); @@ -1191,7 +1075,7 @@ mod tests { // let mut srv = test::TestServer::with_factory(|| { // App::new().handler( // "test", - // StaticFiles::new(".").unwrap().index_file("Cargo.toml"), + // StaticFiles::new(".").index_file("Cargo.toml"), // ) // }); @@ -1204,279 +1088,4 @@ mod tests { // assert_eq!(response.status(), StatusCode::OK); // } - struct T(&'static str, u64, Vec); - - #[test] - fn test_parse() { - let tests = vec![ - T("", 0, vec![]), - T("", 1000, vec![]), - T("foo", 0, vec![]), - T("bytes=", 0, vec![]), - T("bytes=7", 10, vec![]), - T("bytes= 7 ", 10, vec![]), - T("bytes=1-", 0, vec![]), - T("bytes=5-4", 10, vec![]), - T("bytes=0-2,5-4", 10, vec![]), - T("bytes=2-5,4-3", 10, vec![]), - T("bytes=--5,4--3", 10, vec![]), - T("bytes=A-", 10, vec![]), - T("bytes=A- ", 10, vec![]), - T("bytes=A-Z", 10, vec![]), - T("bytes= -Z", 10, vec![]), - T("bytes=5-Z", 10, vec![]), - T("bytes=Ran-dom, garbage", 10, vec![]), - T("bytes=0x01-0x02", 10, vec![]), - T("bytes= ", 10, vec![]), - T("bytes= , , , ", 10, vec![]), - T( - "bytes=0-9", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=0-", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=5-", - 10, - vec![HttpRange { - start: 5, - length: 5, - }], - ), - T( - "bytes=0-20", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=15-,0-5", - 10, - vec![HttpRange { - start: 0, - length: 6, - }], - ), - T( - "bytes=1-2,5-", - 10, - vec![ - HttpRange { - start: 1, - length: 2, - }, - HttpRange { - start: 5, - length: 5, - }, - ], - ), - T( - "bytes=-2 , 7-", - 11, - vec![ - HttpRange { - start: 9, - length: 2, - }, - HttpRange { - start: 7, - length: 4, - }, - ], - ), - T( - "bytes=0-0 ,2-2, 7-", - 11, - vec![ - HttpRange { - start: 0, - length: 1, - }, - HttpRange { - start: 2, - length: 1, - }, - HttpRange { - start: 7, - length: 4, - }, - ], - ), - T( - "bytes=-5", - 10, - vec![HttpRange { - start: 5, - length: 5, - }], - ), - T( - "bytes=-15", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=0-499", - 10000, - vec![HttpRange { - start: 0, - length: 500, - }], - ), - T( - "bytes=500-999", - 10000, - vec![HttpRange { - start: 500, - length: 500, - }], - ), - T( - "bytes=-500", - 10000, - vec![HttpRange { - start: 9500, - length: 500, - }], - ), - T( - "bytes=9500-", - 10000, - vec![HttpRange { - start: 9500, - length: 500, - }], - ), - T( - "bytes=0-0,-1", - 10000, - vec![ - HttpRange { - start: 0, - length: 1, - }, - HttpRange { - start: 9999, - length: 1, - }, - ], - ), - T( - "bytes=500-600,601-999", - 10000, - vec![ - HttpRange { - start: 500, - length: 101, - }, - HttpRange { - start: 601, - length: 399, - }, - ], - ), - T( - "bytes=500-700,601-999", - 10000, - vec![ - HttpRange { - start: 500, - length: 201, - }, - HttpRange { - start: 601, - length: 399, - }, - ], - ), - // Match Apache laxity: - T( - "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", - 11, - vec![ - HttpRange { - start: 1, - length: 2, - }, - HttpRange { - start: 4, - length: 2, - }, - HttpRange { - start: 7, - length: 2, - }, - ], - ), - ]; - - for t in tests { - let header = t.0; - let size = t.1; - let expected = t.2; - - let res = HttpRange::parse(header, size); - - if res.is_err() { - if expected.is_empty() { - continue; - } else { - assert!( - false, - "parse({}, {}) returned error {:?}", - header, - size, - res.unwrap_err() - ); - } - } - - let got = res.unwrap(); - - if got.len() != expected.len() { - assert!( - false, - "len(parseRange({}, {})) = {}, want {}", - header, - size, - got.len(), - expected.len() - ); - continue; - } - - for i in 0..expected.len() { - if got[i].start != expected[i].start { - assert!( - false, - "parseRange({}, {})[{}].start = {}, want {}", - header, size, i, got[i].start, expected[i].start - ) - } - if got[i].length != expected[i].length { - assert!( - false, - "parseRange({}, {})[{}].length = {}, want {}", - header, size, i, got[i].length, expected[i].length - ) - } - } - } - } } diff --git a/actix-staticfiles/src/named.rs b/actix-staticfiles/src/named.rs index 5fba0483a..2fc1c454d 100644 --- a/actix-staticfiles/src/named.rs +++ b/actix-staticfiles/src/named.rs @@ -17,7 +17,8 @@ use actix_web::http::{ContentEncoding, Method, StatusCode}; use actix_web::{HttpMessage, HttpRequest, HttpResponse, Responder}; use crate::config::{DefaultConfig, StaticFileConfig}; -use crate::{ChunkedReadFile, HttpRange}; +use crate::range::HttpRange; +use crate::ChunkedReadFile; /// A file with an associated name. #[derive(Debug)] @@ -303,6 +304,8 @@ impl Responder for NamedFile { type Future = Result; fn respond_to(self, req: &HttpRequest) -> Self::Future { + println!("RESP: {:?}", req); + if self.status_code != StatusCode::OK { let mut resp = HttpResponse::build(self.status_code); resp.set(header::ContentType(self.content_type.clone())) diff --git a/actix-staticfiles/src/range.rs b/actix-staticfiles/src/range.rs new file mode 100644 index 000000000..d97a35e7e --- /dev/null +++ b/actix-staticfiles/src/range.rs @@ -0,0 +1,375 @@ +/// HTTP Range header representation. +#[derive(Debug, Clone, Copy)] +pub struct HttpRange { + pub start: u64, + pub length: u64, +} + +static PREFIX: &'static str = "bytes="; +const PREFIX_LEN: usize = 6; + +impl HttpRange { + /// Parses Range HTTP header string as per RFC 2616. + /// + /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). + /// `size` is full size of response (file). + pub fn parse(header: &str, size: u64) -> Result, ()> { + if header.is_empty() { + return Ok(Vec::new()); + } + if !header.starts_with(PREFIX) { + return Err(()); + } + + let size_sig = size as i64; + let mut no_overlap = false; + + let all_ranges: Vec> = header[PREFIX_LEN..] + .split(',') + .map(|x| x.trim()) + .filter(|x| !x.is_empty()) + .map(|ra| { + let mut start_end_iter = ra.split('-'); + + let start_str = start_end_iter.next().ok_or(())?.trim(); + let end_str = start_end_iter.next().ok_or(())?.trim(); + + if start_str.is_empty() { + // If no start is specified, end specifies the + // range start relative to the end of the file. + let mut length: i64 = end_str.parse().map_err(|_| ())?; + + if length > size_sig { + length = size_sig; + } + + Ok(Some(HttpRange { + start: (size_sig - length) as u64, + length: length as u64, + })) + } else { + let start: i64 = start_str.parse().map_err(|_| ())?; + + if start < 0 { + return Err(()); + } + if start >= size_sig { + no_overlap = true; + return Ok(None); + } + + let length = if end_str.is_empty() { + // If no end is specified, range extends to end of the file. + size_sig - start + } else { + let mut end: i64 = end_str.parse().map_err(|_| ())?; + + if start > end { + return Err(()); + } + + if end >= size_sig { + end = size_sig - 1; + } + + end - start + 1 + }; + + Ok(Some(HttpRange { + start: start as u64, + length: length as u64, + })) + } + }) + .collect::>()?; + + let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); + + if no_overlap && ranges.is_empty() { + return Err(()); + } + + Ok(ranges) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct T(&'static str, u64, Vec); + + #[test] + fn test_parse() { + let tests = vec![ + T("", 0, vec![]), + T("", 1000, vec![]), + T("foo", 0, vec![]), + T("bytes=", 0, vec![]), + T("bytes=7", 10, vec![]), + T("bytes= 7 ", 10, vec![]), + T("bytes=1-", 0, vec![]), + T("bytes=5-4", 10, vec![]), + T("bytes=0-2,5-4", 10, vec![]), + T("bytes=2-5,4-3", 10, vec![]), + T("bytes=--5,4--3", 10, vec![]), + T("bytes=A-", 10, vec![]), + T("bytes=A- ", 10, vec![]), + T("bytes=A-Z", 10, vec![]), + T("bytes= -Z", 10, vec![]), + T("bytes=5-Z", 10, vec![]), + T("bytes=Ran-dom, garbage", 10, vec![]), + T("bytes=0x01-0x02", 10, vec![]), + T("bytes= ", 10, vec![]), + T("bytes= , , , ", 10, vec![]), + T( + "bytes=0-9", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=0-", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=5-", + 10, + vec![HttpRange { + start: 5, + length: 5, + }], + ), + T( + "bytes=0-20", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=15-,0-5", + 10, + vec![HttpRange { + start: 0, + length: 6, + }], + ), + T( + "bytes=1-2,5-", + 10, + vec![ + HttpRange { + start: 1, + length: 2, + }, + HttpRange { + start: 5, + length: 5, + }, + ], + ), + T( + "bytes=-2 , 7-", + 11, + vec![ + HttpRange { + start: 9, + length: 2, + }, + HttpRange { + start: 7, + length: 4, + }, + ], + ), + T( + "bytes=0-0 ,2-2, 7-", + 11, + vec![ + HttpRange { + start: 0, + length: 1, + }, + HttpRange { + start: 2, + length: 1, + }, + HttpRange { + start: 7, + length: 4, + }, + ], + ), + T( + "bytes=-5", + 10, + vec![HttpRange { + start: 5, + length: 5, + }], + ), + T( + "bytes=-15", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=0-499", + 10000, + vec![HttpRange { + start: 0, + length: 500, + }], + ), + T( + "bytes=500-999", + 10000, + vec![HttpRange { + start: 500, + length: 500, + }], + ), + T( + "bytes=-500", + 10000, + vec![HttpRange { + start: 9500, + length: 500, + }], + ), + T( + "bytes=9500-", + 10000, + vec![HttpRange { + start: 9500, + length: 500, + }], + ), + T( + "bytes=0-0,-1", + 10000, + vec![ + HttpRange { + start: 0, + length: 1, + }, + HttpRange { + start: 9999, + length: 1, + }, + ], + ), + T( + "bytes=500-600,601-999", + 10000, + vec![ + HttpRange { + start: 500, + length: 101, + }, + HttpRange { + start: 601, + length: 399, + }, + ], + ), + T( + "bytes=500-700,601-999", + 10000, + vec![ + HttpRange { + start: 500, + length: 201, + }, + HttpRange { + start: 601, + length: 399, + }, + ], + ), + // Match Apache laxity: + T( + "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", + 11, + vec![ + HttpRange { + start: 1, + length: 2, + }, + HttpRange { + start: 4, + length: 2, + }, + HttpRange { + start: 7, + length: 2, + }, + ], + ), + ]; + + for t in tests { + let header = t.0; + let size = t.1; + let expected = t.2; + + let res = HttpRange::parse(header, size); + + if res.is_err() { + if expected.is_empty() { + continue; + } else { + assert!( + false, + "parse({}, {}) returned error {:?}", + header, + size, + res.unwrap_err() + ); + } + } + + let got = res.unwrap(); + + if got.len() != expected.len() { + assert!( + false, + "len(parseRange({}, {})) = {}, want {}", + header, + size, + got.len(), + expected.len() + ); + continue; + } + + for i in 0..expected.len() { + if got[i].start != expected[i].start { + assert!( + false, + "parseRange({}, {})[{}].start = {}, want {}", + header, size, i, got[i].start, expected[i].start + ) + } + if got[i].length != expected[i].length { + assert!( + false, + "parseRange({}, {})[{}].length = {}, want {}", + header, size, i, got[i].length, expected[i].length + ) + } + } + } + } +} From db566a634cf7562f1d9ea69965aa8ecb8f92725e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 10:03:18 -0800 Subject: [PATCH 211/427] make State type Send compatible --- src/state.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/state.rs b/src/state.rs index d4e4c894e..265c6f017 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,5 +1,5 @@ use std::ops::Deref; -use std::rc::Rc; +use std::sync::Arc; use actix_http::error::{Error, ErrorInternalServerError}; use actix_http::Extensions; @@ -18,11 +18,11 @@ pub(crate) trait StateFactoryResult { } /// Application state -pub struct State(Rc); +pub struct State(Arc); impl State { pub(crate) fn new(state: T) -> State { - State(Rc::new(state)) + State(Arc::new(state)) } /// Get referecnce to inner state type. From db39a604ae0859e50f47f79cfc4a05e1fc2711ca Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 10:03:37 -0800 Subject: [PATCH 212/427] implement ResponseError trait for BlockingError --- src/blocking.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/blocking.rs b/src/blocking.rs index abf4282cf..01be30dd2 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -1,10 +1,15 @@ //! Thread pool for blocking operations +use std::fmt; + +use derive_more::Display; use futures::sync::oneshot; use futures::{Async, Future, Poll}; use parking_lot::Mutex; use threadpool::ThreadPool; +use crate::ResponseError; + /// Env variable for default cpu pool size const ENV_CPU_POOL_VAR: &str = "ACTIX_CPU_POOL"; @@ -36,18 +41,23 @@ thread_local! { }; } -pub enum BlockingError { +#[derive(Debug, Display)] +pub enum BlockingError { + #[display(fmt = "{:?}", _0)] Error(E), + #[display(fmt = "Thread pool is gone")] Canceled, } +impl ResponseError for BlockingError {} + /// Execute blocking function on a thread pool, returns future that resolves /// to result of the function execution. pub fn run(f: F) -> CpuFuture where F: FnOnce() -> Result + Send + 'static, I: Send + 'static, - E: Send + 'static, + E: Send + fmt::Debug + 'static, { let (tx, rx) = oneshot::channel(); POOL.with(|pool| { @@ -63,7 +73,7 @@ pub struct CpuFuture { rx: oneshot::Receiver>, } -impl Future for CpuFuture { +impl Future for CpuFuture { type Item = I; type Error = BlockingError; From ad08e856d736897c6a591f87ad315806af44fac3 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 10:30:17 -0800 Subject: [PATCH 213/427] update actix-rt --- Cargo.toml | 2 +- test-server/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6ab1b0903..6493404dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,7 @@ trust-dns-resolver = { version="0.11.0-alpha.2", default-features = false } openssl = { version="0.10", optional = true } [dev-dependencies] -actix-rt = "0.1.0" +actix-rt = "0.2.0" #actix-server = { version = "0.3.0", features=["ssl"] } actix-server = { git="https://github.com/actix/actix-net.git", features=["ssl"] } #actix-connector = { version = "0.3.0", features=["ssl"] } diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 6a401cc58..554ab20e3 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -33,7 +33,7 @@ ssl = ["openssl", "actix-http/ssl", "actix-server/ssl"] [dependencies] actix-codec = "0.1" -actix-rt = "0.1.0" +actix-rt = "0.2.0" actix-http = { path=".." } #actix-service = "0.3.2" actix-service = { git="https://github.com/actix/actix-net.git" } From 5cde4dc479ce5c08dc7a2db9bf47afa2d2e5a9e7 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 10:41:07 -0800 Subject: [PATCH 214/427] update actix-rt --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index acacb2f21..a17669d18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ ssl = ["openssl", "actix-server/ssl"] actix-codec = "0.1.0" #actix-service = "0.3.2" #actix-utils = "0.3.1" -actix-rt = "0.1.0" +actix-rt = "0.2.0" actix-service = { git = "https://github.com/actix/actix-net.git" } actix-utils = { git = "https://github.com/actix/actix-net.git" } From b689bb92608a51c708edd764111702134e1cf788 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 11:45:33 -0800 Subject: [PATCH 215/427] add failure support --- Cargo.toml | 9 +++++- src/error.rs | 78 +++++++++++++++------------------------------------- 2 files changed, 30 insertions(+), 57 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6493404dd..a0eb17312 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,11 +28,15 @@ name = "actix_http" path = "src/lib.rs" [features] -default = [] +default = ["fail"] # openssl ssl = ["openssl", "actix-connector/ssl"] +# failure integration. it is on by default, it will be off in future versions +# actix itself does not use failure anymore +fail = ["failure"] + [dependencies] #actix-service = "0.3.2" actix-codec = "0.1.0" @@ -77,6 +81,9 @@ trust-dns-resolver = { version="0.11.0-alpha.2", default-features = false } # openssl openssl = { version="0.10", optional = true } +# failure is optional +failure = { version = "0.1.5", optional = true } + [dev-dependencies] actix-rt = "0.2.0" #actix-server = { version = "0.3.0", features=["ssl"] } diff --git a/src/error.rs b/src/error.rs index cd5cabaa6..4762f27ff 100644 --- a/src/error.rs +++ b/src/error.rs @@ -66,28 +66,6 @@ impl Error { } } - // /// Attempts to downcast this `Error` to a particular `Fail` type by - // /// reference. - // /// - // /// If the underlying error is not of type `T`, this will return `None`. - // pub fn downcast_ref(&self) -> Option<&T> { - // // in the most trivial way the cause is directly of the requested type. - // if let Some(rv) = Fail::downcast_ref(self.cause.as_fail()) { - // return Some(rv); - // } - - // // in the more complex case the error has been constructed from a failure - // // error. This happens because we implement From by - // // calling compat() and then storing it here. In failure this is - // // represented by a failure::Error being wrapped in a failure::Compat. - // // - // // So we first downcast into that compat, to then further downcast through - // // the failure's Error downcasting system into the original failure. - // let compat: Option<&failure::Compat> = - // Fail::downcast_ref(self.cause.as_fail()); - // compat.and_then(|e| e.get_ref().downcast_ref()) - // } - /// Converts error to a response instance and set error message as response body pub fn response_with_message(self) -> Response { let message = format!("{}", self); @@ -96,28 +74,6 @@ impl Error { } } -// /// Helper trait to downcast a response error into a fail. -// /// -// /// This is currently not exposed because it's unclear if this is the best way -// /// to achieve the downcasting on `Error` for which this is needed. -// #[doc(hidden)] -// pub trait InternalResponseErrorAsFail { -// #[doc(hidden)] -// fn as_fail(&self) -> &Fail; -// #[doc(hidden)] -// fn as_mut_fail(&mut self) -> &mut Fail; -// } - -// #[doc(hidden)] -// impl InternalResponseErrorAsFail for T { -// fn as_fail(&self) -> &Fail { -// self -// } -// fn as_mut_fail(&mut self) -> &mut Fail { -// self -// } -// } - /// Error that can be converted to `Response` pub trait ResponseError: fmt::Debug + fmt::Display { /// Create response for error @@ -176,18 +132,6 @@ impl From for Error { } } -// /// Compatibility for `failure::Error` -// impl ResponseError for failure::Compat where -// T: fmt::Display + fmt::Debug + Sync + Send + 'static -// { -// } - -// impl From for Error { -// fn from(err: failure::Error) -> Error { -// err.compat().into() -// } -// } - /// Return `GATEWAY_TIMEOUT` for `TimeoutError` impl ResponseError for TimeoutError { fn error_response(&self) -> Response { @@ -1023,6 +967,28 @@ where InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into() } +#[cfg(feature = "fail")] +mod failure_integration { + use super::*; + use failure::{self, Fail}; + + /// Compatibility for `failure::Error` + impl ResponseError for failure::Compat + where + T: fmt::Display + fmt::Debug + Sync + Send + 'static, + { + fn error_response(&self) -> Response { + Response::new(StatusCode::INTERNAL_SERVER_ERROR) + } + } + + impl From for Error { + fn from(err: failure::Error) -> Error { + err.compat().into() + } + } +} + #[cfg(test)] mod tests { use super::*; From fe22e831447551a661f1e3d75185a16b259eba88 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 15:47:15 -0800 Subject: [PATCH 216/427] refactor service registration process; unify services and resources --- actix-session/src/cookie.rs | 26 +- actix-session/src/lib.rs | 14 +- actix-staticfiles/src/lib.rs | 30 +- examples/basic.rs | 28 +- src/app.rs | 420 +++++++++------------------- src/config.rs | 103 +++++++ src/extract.rs | 104 +++---- src/guard.rs | 15 +- src/lib.rs | 91 +++++- src/middleware/defaultheaders.rs | 9 +- src/request.rs | 7 +- src/resource.rs | 117 ++++++-- src/responder.rs | 21 +- src/route.rs | 43 +-- src/scope.rs | 456 +++++++++++++------------------ src/server.rs | 40 +-- src/service.rs | 35 +++ tests/test_server.rs | 65 ++--- 18 files changed, 845 insertions(+), 779 deletions(-) create mode 100644 src/config.rs diff --git a/actix-session/src/cookie.rs b/actix-session/src/cookie.rs index 9cde02e0c..7fd5ec643 100644 --- a/actix-session/src/cookie.rs +++ b/actix-session/src/cookie.rs @@ -174,7 +174,7 @@ impl CookieSessionInner { /// /// ```rust /// use actix_session::CookieSession; -/// use actix_web::{App, HttpResponse, HttpServer}; +/// use actix_web::{web, App, HttpResponse, HttpServer}; /// /// fn main() { /// let app = App::new().middleware( @@ -183,7 +183,7 @@ impl CookieSessionInner { /// .name("actix_session") /// .path("/") /// .secure(true)) -/// .resource("/", |r| r.to(|| HttpResponse::Ok())); +/// .service(web::resource("/").to(|| HttpResponse::Ok())); /// } /// ``` pub struct CookieSession(Rc); @@ -314,19 +314,17 @@ where #[cfg(test)] mod tests { use super::*; - use actix_web::{test, App}; + use actix_web::{test, web, App}; #[test] fn cookie_session() { let mut app = test::init_service( App::new() .middleware(CookieSession::signed(&[0; 32]).secure(false)) - .resource("/", |r| { - r.to(|ses: Session| { - let _ = ses.set("counter", 100); - "test" - }) - }), + .service(web::resource("/").to(|ses: Session| { + let _ = ses.set("counter", 100); + "test" + })), ); let request = test::TestRequest::get().to_request(); @@ -342,12 +340,10 @@ mod tests { let mut app = test::init_service( App::new() .middleware(CookieSession::signed(&[0; 32]).secure(false)) - .resource("/", |r| { - r.to(|ses: Session| { - let _ = ses.set("counter", 100); - "test" - }) - }), + .service(web::resource("/").to(|ses: Session| { + let _ = ses.set("counter", 100); + "test" + })), ); let request = test::TestRequest::get().to_request(); diff --git a/actix-session/src/lib.rs b/actix-session/src/lib.rs index f57e11f2f..62bc5c8fb 100644 --- a/actix-session/src/lib.rs +++ b/actix-session/src/lib.rs @@ -13,7 +13,7 @@ //! extractor allows us to get or set session data. //! //! ```rust -//! use actix_web::{App, HttpServer, HttpResponse, Error}; +//! use actix_web::{web, App, HttpServer, HttpResponse, Error}; //! use actix_session::{Session, CookieSession}; //! //! fn index(session: Session) -> Result<&'static str, Error> { @@ -29,19 +29,17 @@ //! } //! //! fn main() -> std::io::Result<()> { -//! let sys = actix_rt::System::new("example"); // <- create Actix runtime -//! +//! # std::thread::spawn(|| //! HttpServer::new( //! || App::new().middleware( //! CookieSession::signed(&[0; 32]) // <- create cookie based session middleware //! .secure(false) //! ) -//! .resource("/", |r| r.to(|| HttpResponse::Ok()))) +//! .service(web::resource("/").to(|| HttpResponse::Ok()))) //! .bind("127.0.0.1:59880")? -//! .start(); -//! # actix_rt::System::current().stop(); -//! sys.run(); -//! Ok(()) +//! .run() +//! # ); +//! # Ok(()) //! } //! ``` use std::cell::RefCell; diff --git a/actix-staticfiles/src/lib.rs b/actix-staticfiles/src/lib.rs index 81d8269c9..7c3f68493 100644 --- a/actix-staticfiles/src/lib.rs +++ b/actix-staticfiles/src/lib.rs @@ -17,7 +17,7 @@ use v_htmlescape::escape as escape_html_entity; use actix_http::error::{Error, ErrorInternalServerError}; use actix_service::{boxed::BoxedNewService, NewService, Service}; -use actix_web::dev::{self, HttpServiceFactory, ResourceDef, Url}; +use actix_web::dev::{self, AppConfig, HttpServiceFactory, ResourceDef, Url}; use actix_web::{ blocking, FromRequest, HttpRequest, HttpResponse, Responder, ServiceFromRequest, ServiceRequest, ServiceResponse, @@ -226,7 +226,7 @@ fn directory_listing( /// } /// ``` pub struct StaticFiles { - path: ResourceDef, + path: String, directory: PathBuf, index: Option, show_index: bool, @@ -259,7 +259,7 @@ impl StaticFiles { } StaticFiles { - path: ResourceDef::root_prefix(path), + path: path.to_string(), directory: dir, index: None, show_index: false, @@ -300,15 +300,21 @@ impl StaticFiles { } } -impl HttpServiceFactory

    for StaticFiles { - type Factory = Self; - - fn rdef(&self) -> &ResourceDef { - &self.path - } - - fn create(self) -> Self { - self +impl HttpServiceFactory

    for StaticFiles +where + P: 'static, + C: StaticFileConfig + 'static, +{ + fn register(self, config: &mut AppConfig

    ) { + if self.default.borrow().is_none() { + *self.default.borrow_mut() = Some(config.default_service()); + } + let rdef = if config.is_root() { + ResourceDef::root_prefix(&self.path) + } else { + ResourceDef::prefix(&self.path) + }; + config.register_service(rdef, None, self) } } diff --git a/examples/basic.rs b/examples/basic.rs index 7c72439e7..5fd862d49 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -27,23 +27,23 @@ fn main() -> std::io::Result<()> { App::new() .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) .middleware(middleware::Compress::default()) - .resource("/resource1/index.html", |r| r.route(web::get().to(index))) - .resource("/resource2/index.html", |r| { - r.middleware( - middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), - ) - .default_resource(|r| { - r.route(web::route().to(|| HttpResponse::MethodNotAllowed())) - }) - .route(web::method(Method::GET).to_async(index_async)) - }) - .resource("/test1.html", |r| r.to(|| "Test\r\n")) - .resource("/", |r| r.to(no_params)) + .service(web::resource("/resource1/index.html").route(web::get().to(index))) + .service( + web::resource("/resource2/index.html") + .middleware( + middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), + ) + .default_resource(|r| { + r.route(web::route().to(|| HttpResponse::MethodNotAllowed())) + }) + .route(web::method(Method::GET).to_async(index_async)), + ) + .service(web::resource("/test1.html").to(|| "Test\r\n")) + .service(web::resource("/").to(no_params)) }) .bind("127.0.0.1:8080")? .workers(1) .start(); - let _ = sys.run(); - Ok(()) + sys.run() } diff --git a/src/app.rs b/src/app.rs index d503d8ddb..7c194d273 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,16 +7,20 @@ use actix_http::{Extensions, PayloadStream, Request, Response}; use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ - AndThenNewService, ApplyTransform, IntoNewService, IntoTransform, NewService, - Service, Transform, + fn_service, AndThenNewService, ApplyTransform, IntoNewService, IntoTransform, + NewService, Service, Transform, }; use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; +use crate::config::AppConfig; use crate::guard::Guard; use crate::resource::Resource; -use crate::scope::{insert_slash, Scope}; -use crate::service::{ServiceRequest, ServiceResponse}; +use crate::route::Route; +use crate::service::{ + HttpServiceFactory, ServiceFactory, ServiceFactoryWrapper, ServiceRequest, + ServiceResponse, +}; use crate::state::{State, StateFactory, StateFactoryResult}; type Guards = Vec>; @@ -24,19 +28,6 @@ type HttpService

    = BoxedService, ServiceResponse, ()>; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; type BoxedResponse = Box>; -pub trait HttpServiceFactory

    { - type Factory: NewService< - ServiceRequest

    , - Response = ServiceResponse, - Error = (), - InitError = (), - >; - - fn rdef(&self) -> &ResourceDef; - - fn create(self) -> Self::Factory; -} - /// Application builder - structure that follows the builder pattern /// for building application instances. pub struct App @@ -97,9 +88,9 @@ where /// fn main() { /// let app = App::new() /// .state(MyState{ counter: Cell::new(0) }) - /// .resource( - /// "/index.html", - /// |r| r.route(web::get().to(index))); + /// .service( + /// web::resource("/index.html").route( + /// web::get().to(index))); /// } /// ``` pub fn state(mut self, state: S) -> Self { @@ -120,112 +111,6 @@ where self } - /// Configure scope for common root path. - /// - /// Scopes collect multiple paths under a common path prefix. - /// Scope path can contain variable path segments as resources. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().scope("/{project_id}", |scope| { - /// scope - /// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) - /// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) - /// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) - /// }); - /// } - /// ``` - /// - /// In the above example, three routes get added: - /// * /{project_id}/path1 - /// * /{project_id}/path2 - /// * /{project_id}/path3 - /// - pub fn scope(self, path: &str, f: F) -> AppRouter> - where - F: FnOnce(Scope

    ) -> Scope

    , - { - let mut scope = f(Scope::new(path)); - let rdef = scope.rdef().clone(); - let default = scope.get_default(); - let guards = scope.take_guards(); - - let fref = Rc::new(RefCell::new(None)); - AppRouter { - chain: self.chain, - services: vec![(rdef, boxed::new_service(scope.into_new_service()), guards)], - default: None, - defaults: vec![default], - endpoint: AppEntry::new(fref.clone()), - factory_ref: fref, - extensions: self.extensions, - state: self.state, - _t: PhantomData, - } - } - - /// Configure resource for a specific path. - /// - /// Resources may have variable path segments. For example, a - /// resource with the path `/a/{name}/c` would match all incoming - /// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. - /// - /// A variable segment is specified in the form `{identifier}`, - /// where the identifier can be used later in a request handler to - /// access the matched value for that segment. This is done by - /// looking up the identifier in the `Params` object returned by - /// `HttpRequest.match_info()` method. - /// - /// By default, each segment matches the regular expression `[^{}/]+`. - /// - /// You can also specify a custom regex in the form `{identifier:regex}`: - /// - /// For instance, to route `GET`-requests on any route matching - /// `/users/{userid}/{friend}` and store `userid` and `friend` in - /// the exposed `Params` object: - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{web, http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().resource("/users/{userid}/{friend}", |r| { - /// r.route(web::get().to(|| HttpResponse::Ok())) - /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) - /// }); - /// } - /// ``` - pub fn resource(self, path: &str, f: F) -> AppRouter> - where - F: FnOnce(Resource

    ) -> Resource, - U: NewService< - ServiceRequest

    , - Response = ServiceResponse, - Error = (), - InitError = (), - > + 'static, - { - let rdef = ResourceDef::new(&insert_slash(path)); - let res = f(Resource::new()); - let default = res.get_default(); - - let fref = Rc::new(RefCell::new(None)); - AppRouter { - chain: self.chain, - services: vec![(rdef, boxed::new_service(res.into_new_service()), None)], - default: None, - defaults: vec![default], - endpoint: AppEntry::new(fref.clone()), - factory_ref: fref, - extensions: self.extensions, - state: self.state, - _t: PhantomData, - } - } - /// Register a middleware. pub fn middleware( self, @@ -259,7 +144,6 @@ where state: self.state, services: Vec::new(), default: None, - defaults: Vec::new(), factory_ref: fref, extensions: self.extensions, _t: PhantomData, @@ -298,25 +182,52 @@ where } } - /// Register resource handler service. + /// Configure route for a specific path. + /// + /// This is a simplified version of the `App::service()` method. + /// This method can not be could multiple times, in that case + /// multiple resources with one route would be registered for same resource path. + /// + /// ```rust + /// use actix_web::{web, App, HttpResponse, extract::Path}; + /// + /// fn index(data: Path<(String, String)>) -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new() + /// .route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())); + /// } + /// ``` + pub fn route( + self, + path: &str, + mut route: Route

    , + ) -> AppRouter> { + self.service( + Resource::new(path) + .add_guards(route.take_guards()) + .route(route), + ) + } + + /// Register http service. pub fn service(self, service: F) -> AppRouter> where F: HttpServiceFactory

    + 'static, { let fref = Rc::new(RefCell::new(None)); + AppRouter { chain: self.chain, - services: vec![( - service.rdef().clone(), - boxed::new_service(service.create().map_init_err(|_| ())), - None, - )], default: None, - defaults: vec![], endpoint: AppEntry::new(fref.clone()), factory_ref: fref, extensions: self.extensions, state: self.state, + services: vec![Box::new(ServiceFactoryWrapper::new(service))], _t: PhantomData, } } @@ -338,10 +249,9 @@ where /// for building application instances. pub struct AppRouter { chain: C, - services: Vec<(ResourceDef, HttpNewService

    , Option)>, - default: Option>>, - defaults: Vec>>>>>, endpoint: T, + services: Vec>>, + default: Option>>, factory_ref: Rc>>>, extensions: Extensions, state: Vec>, @@ -359,131 +269,48 @@ where InitError = (), >, { - /// Configure scope for common root path. + /// Configure route for a specific path. /// - /// Scopes collect multiple paths under a common path prefix. - /// Scope path can contain variable path segments as resources. + /// This is a simplified version of the `App::service()` method. + /// This method can not be could multiple times, in that case + /// multiple resources with one route would be registered for same resource path. /// /// ```rust - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest, HttpResponse}; + /// use actix_web::{web, App, HttpResponse, extract::Path}; /// - /// fn main() { - /// let app = App::new().scope("/{project_id}", |scope| { - /// scope - /// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) - /// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) - /// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) - /// }); + /// fn index(data: Path<(String, String)>) -> &'static str { + /// "Welcome!" /// } - /// ``` - /// - /// In the above example, three routes get added: - /// * /{project_id}/path1 - /// * /{project_id}/path2 - /// * /{project_id}/path3 - /// - pub fn scope(mut self, path: &str, f: F) -> Self - where - F: FnOnce(Scope

    ) -> Scope

    , - { - let mut scope = f(Scope::new(path)); - let rdef = scope.rdef().clone(); - let guards = scope.take_guards(); - self.defaults.push(scope.get_default()); - self.services - .push((rdef, boxed::new_service(scope.into_new_service()), guards)); - self - } - - /// Configure resource for a specific path. - /// - /// Resources may have variable path segments. For example, a - /// resource with the path `/a/{name}/c` would match all incoming - /// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. - /// - /// A variable segment is specified in the form `{identifier}`, - /// where the identifier can be used later in a request handler to - /// access the matched value for that segment. This is done by - /// looking up the identifier in the `Params` object returned by - /// `HttpRequest.match_info()` method. - /// - /// By default, each segment matches the regular expression `[^{}/]+`. - /// - /// You can also specify a custom regex in the form `{identifier:regex}`: - /// - /// For instance, to route `GET`-requests on any route matching - /// `/users/{userid}/{friend}` and store `userid` and `friend` in - /// the exposed `Params` object: - /// - /// ```rust - /// use actix_web::{web, http, App, HttpResponse}; /// /// fn main() { /// let app = App::new() - /// .resource("/users/{userid}/{friend}", |r| { - /// r.route(web::to(|| HttpResponse::Ok())) - /// }) - /// .resource("/index.html", |r| { - /// r.route(web::head().to(|| HttpResponse::MethodNotAllowed())) - /// }); + /// .route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())); /// } /// ``` - pub fn resource(mut self, path: &str, f: F) -> Self - where - F: FnOnce(Resource

    ) -> Resource, - U: NewService< - ServiceRequest

    , - Response = ServiceResponse, - Error = (), - InitError = (), - > + 'static, - { - let rdef = ResourceDef::new(&insert_slash(path)); - let resource = f(Resource::new()); - self.defaults.push(resource.get_default()); - self.services.push(( - rdef, - boxed::new_service(resource.into_new_service()), - None, - )); - self + pub fn route(self, path: &str, mut route: Route

    ) -> Self { + self.service( + Resource::new(path) + .add_guards(route.take_guards()) + .route(route), + ) } - /// Default resource to be used if no matching route could be found. + /// Register http service. /// - /// Default resource works with resources only and does not work with - /// custom services. - pub fn default_resource(mut self, f: F) -> Self - where - F: FnOnce(Resource

    ) -> Resource, - U: NewService< - ServiceRequest

    , - Response = ServiceResponse, - Error = (), - InitError = (), - > + 'static, - { - // create and configure default resource - self.default = Some(Rc::new(boxed::new_service( - f(Resource::new()).into_new_service().map_init_err(|_| ()), - ))); - - self - } - - /// Register resource handler service. + /// Http service is any type that implements `HttpServiceFactory` trait. + /// + /// Actix web provides several services implementations: + /// + /// * *Resource* is an entry in route table which corresponds to requested URL. + /// * *Scope* is a set of resources with common root path. + /// * "StaticFiles" is a service for static files support pub fn service(mut self, factory: F) -> Self where F: HttpServiceFactory

    + 'static, { - let rdef = factory.rdef().clone(); - - self.services.push(( - rdef, - boxed::new_service(factory.create().map_init_err(|_| ())), - None, - )); + self.services + .push(Box::new(ServiceFactoryWrapper::new(factory))); self } @@ -520,13 +347,34 @@ where state: self.state, services: self.services, default: self.default, - defaults: self.defaults, factory_ref: self.factory_ref, extensions: self.extensions, _t: PhantomData, } } + /// Default resource to be used if no matching route could be found. + /// + /// Default resource works with resources only and does not work with + /// custom services. + pub fn default_resource(mut self, f: F) -> Self + where + F: FnOnce(Resource

    ) -> Resource, + U: NewService< + ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, + { + // create and configure default resource + self.default = Some(Rc::new(boxed::new_service( + f(Resource::new("")).into_new_service().map_init_err(|_| ()), + ))); + + self + } + /// Register an external resource. /// /// External resources are useful for URL generation purposes only @@ -583,19 +431,30 @@ where { fn into_new_service(self) -> AndThenNewService, T> { // update resource default service - if self.default.is_some() { - for default in &self.defaults { - if default.borrow_mut().is_none() { - *default.borrow_mut() = self.default.clone(); - } - } - } + let default = self.default.unwrap_or_else(|| { + Rc::new(boxed::new_service(fn_service(|req: ServiceRequest

    | { + Ok(req.into_response(Response::NotFound().finish())) + }))) + }); + + let mut config = AppConfig::new( + "127.0.0.1:8080".parse().unwrap(), + "localhost:8080".to_owned(), + false, + default.clone(), + ); + + // register services + self.services + .into_iter() + .for_each(|mut srv| srv.register(&mut config)); // set factory *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { - default: self.default.clone(), + default: default, services: Rc::new( - self.services + config + .into_services() .into_iter() .map(|(rdef, srv, guards)| (rdef, srv, RefCell::new(guards))) .collect(), @@ -613,7 +472,7 @@ where pub struct AppRoutingFactory

    { services: Rc, RefCell>)>>, - default: Option>>, + default: Rc>, } impl NewService> for AppRoutingFactory

    { @@ -624,12 +483,6 @@ impl NewService> for AppRoutingFactory

    { type Future = AppRoutingFactoryResponse

    ; fn new_service(&self, _: &()) -> Self::Future { - let default_fut = if let Some(ref default) = self.default { - Some(default.new_service(&())) - } else { - None - }; - AppRoutingFactoryResponse { fut: self .services @@ -643,7 +496,7 @@ impl NewService> for AppRoutingFactory

    { }) .collect(), default: None, - default_fut, + default_fut: Some(self.default.new_service(&())), } } } @@ -929,16 +782,15 @@ where #[cfg(test)] mod tests { - use actix_http::http::{Method, StatusCode}; - use super::*; - use crate::test::{self, block_on, TestRequest}; + use crate::http::{Method, StatusCode}; + use crate::test::{self, block_on, init_service, TestRequest}; use crate::{web, HttpResponse, State}; #[test] fn test_default_resource() { - let mut srv = test::init_service( - App::new().resource("/test", |r| r.to(|| HttpResponse::Ok())), + let mut srv = init_service( + App::new().service(web::resource("/test").to(|| HttpResponse::Ok())), ); let req = TestRequest::with_uri("/test").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -948,13 +800,14 @@ mod tests { let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); - let mut srv = test::init_service( + let mut srv = init_service( App::new() - .resource("/test", |r| r.to(|| HttpResponse::Ok())) - .resource("/test2", |r| { - r.default_resource(|r| r.to(|| HttpResponse::Created())) - .route(web::get().to(|| HttpResponse::Ok())) - }) + .service(web::resource("/test").to(|| HttpResponse::Ok())) + .service( + web::resource("/test2") + .default_resource(|r| r.to(|| HttpResponse::Created())) + .route(web::get().to(|| HttpResponse::Ok())), + ) .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())), ); @@ -975,22 +828,21 @@ mod tests { #[test] fn test_state() { - let mut srv = test::init_service( + let mut srv = init_service( App::new() .state(10usize) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + .service(web::resource("/").to(|_: State| HttpResponse::Ok())), ); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let mut srv = test::init_service( + let mut srv = init_service( App::new() .state(10u32) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + .service(web::resource("/").to(|_: State| HttpResponse::Ok())), ); - let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); @@ -998,22 +850,20 @@ mod tests { #[test] fn test_state_factory() { - let mut srv = test::init_service( + let mut srv = init_service( App::new() .state_factory(|| Ok::<_, ()>(10usize)) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + .service(web::resource("/").to(|_: State| HttpResponse::Ok())), ); - let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let mut srv = test::init_service( + let mut srv = init_service( App::new() .state_factory(|| Ok::<_, ()>(10u32)) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + .service(web::resource("/").to(|_: State| HttpResponse::Ok())), ); - let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 000000000..483b0a508 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,103 @@ +use std::net::SocketAddr; +use std::rc::Rc; + +use actix_router::ResourceDef; +use actix_service::{boxed, IntoNewService, NewService}; + +use crate::guard::Guard; +use crate::service::{ServiceRequest, ServiceResponse}; + +type Guards = Vec>; +type HttpNewService

    = + boxed::BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; + +/// Application configuration +pub struct AppConfig

    { + addr: SocketAddr, + secure: bool, + host: String, + root: bool, + default: Rc>, + services: Vec<(ResourceDef, HttpNewService

    , Option)>, +} + +impl AppConfig

    { + /// Crate server settings instance + pub(crate) fn new( + addr: SocketAddr, + host: String, + secure: bool, + default: Rc>, + ) -> Self { + AppConfig { + addr, + secure, + host, + default, + root: true, + services: Vec::new(), + } + } + + /// Check if root is beeing configured + pub fn is_root(&self) -> bool { + self.root + } + + pub(crate) fn into_services( + self, + ) -> Vec<(ResourceDef, HttpNewService

    , Option)> { + self.services + } + + pub(crate) fn clone_config(&self) -> Self { + AppConfig { + addr: self.addr, + secure: self.secure, + host: self.host.clone(), + default: self.default.clone(), + services: Vec::new(), + root: false, + } + } + + /// Returns the socket address of the local half of this TCP connection + pub fn local_addr(&self) -> SocketAddr { + self.addr + } + + /// Returns true if connection is secure(https) + pub fn secure(&self) -> bool { + self.secure + } + + /// Returns host header value + pub fn host(&self) -> &str { + &self.host + } + + pub fn default_service(&self) -> Rc> { + self.default.clone() + } + + pub fn register_service( + &mut self, + rdef: ResourceDef, + guards: Option>>, + service: F, + ) where + F: IntoNewService>, + S: NewService< + ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, + { + self.services.push(( + rdef, + boxed::new_service(service.into_new_service()), + guards, + )); + } +} diff --git a/src/extract.rs b/src/extract.rs index 7350d7d95..0b212aba5 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -88,9 +88,9 @@ impl ExtractorConfig for () { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/{username}/{count}/index.html", // <- define path parameters -/// |r| r.route(web::get().to(index)) // <- register handler with `Path` extractor +/// let app = App::new().service( +/// web::resource("/{username}/{count}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- register handler with `Path` extractor /// ); /// } /// ``` @@ -113,9 +113,9 @@ impl ExtractorConfig for () { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/{username}/index.html", // <- define path parameters -/// |r| r.route(web::get().to(index)) // <- use handler with Path` extractor +/// let app = App::new().service( +/// web::resource("/{username}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- use handler with Path` extractor /// ); /// } /// ``` @@ -180,9 +180,9 @@ impl From for Path { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/{username}/{count}/index.html", // <- define path parameters -/// |r| r.route(web::get().to(index)) // <- register handler with `Path` extractor +/// let app = App::new().service( +/// web::resource("/{username}/{count}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- register handler with `Path` extractor /// ); /// } /// ``` @@ -205,9 +205,9 @@ impl From for Path { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/{username}/index.html", // <- define path parameters -/// |r| r.route(web::get().to(index)) // <- use handler with Path` extractor +/// let app = App::new().service( +/// web::resource("/{username}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- use handler with Path` extractor /// ); /// } /// ``` @@ -266,9 +266,8 @@ impl fmt::Display for Path { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| r.route(web::get().to(index))); // <- use `Query` extractor +/// let app = App::new().service( +/// web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor /// } /// ``` pub struct Query(T); @@ -322,9 +321,9 @@ impl Query { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| r.route(web::get().to(index))); // <- use `Query` extractor +/// let app = App::new().service( +/// web::resource("/index.html") +/// .route(web::get().to(index))); // <- use `Query` extractor /// } /// ``` impl FromRequest

    for Query @@ -462,14 +461,13 @@ impl fmt::Display for Form { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| { -/// r.route(web::get() +/// let app = App::new().service( +/// web::resource("/index.html") +/// .route(web::get() /// // change `Form` extractor configuration /// .config(extract::FormConfig::default().limit(4097)) /// .to(index)) -/// }); +/// ); /// } /// ``` #[derive(Clone)] @@ -535,9 +533,10 @@ impl Default for FormConfig { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| r.route(web::post().to(index))); +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::post().to(index)) +/// ); /// } /// ``` /// @@ -645,9 +644,10 @@ impl Responder for Json { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| r.route(web::post().to(index))); +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::post().to(index)) +/// ); /// } /// ``` impl FromRequest

    for Json @@ -692,16 +692,17 @@ where /// } /// /// fn main() { -/// let app = App::new().resource("/index.html", |r| { -/// r.route(web::post().config( -/// // change json extractor configuration -/// extract::JsonConfig::default().limit(4096) -/// .error_handler(|err, req| { // <- create custom error response -/// error::InternalError::from_response( -/// err, HttpResponse::Conflict().finish()).into() -/// })) -/// .to(index)) -/// }); +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::post().config( +/// // change json extractor configuration +/// extract::JsonConfig::default().limit(4096) +/// .error_handler(|err, req| { // <- create custom error response +/// error::InternalError::from_response( +/// err, HttpResponse::Conflict().finish()).into() +/// })) +/// .to(index)) +/// ); /// } /// ``` #[derive(Clone)] @@ -757,8 +758,10 @@ impl Default for JsonConfig { /// } /// /// fn main() { -/// let app = App::new() -/// .resource("/index.html", |r| r.route(web::get().to(index))); +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get().to(index)) +/// ); /// } /// ``` impl

    FromRequest

    for Bytes @@ -801,12 +804,12 @@ where /// } /// /// fn main() { -/// let app = App::new().resource("/index.html", |r| { -/// r.route( +/// let app = App::new().service( +/// web::resource("/index.html").route( /// web::get() /// .config(extract::PayloadConfig::new(4096)) // <- limit size of the payload /// .to(index)) // <- register handler with extractor params -/// }); +/// ); /// } /// ``` impl

    FromRequest

    for String @@ -896,9 +899,10 @@ where /// } /// /// fn main() { -/// let app = App::new().resource("/users/:first", |r| { -/// r.route(web::post().to(index)) -/// }); +/// let app = App::new().service( +/// web::resource("/users/:first").route( +/// web::post().to(index)) +/// ); /// } /// ``` impl FromRequest

    for Option @@ -959,9 +963,9 @@ where /// } /// /// fn main() { -/// let app = App::new().resource("/users/:first", |r| { -/// r.route(web::post().to(index)) -/// }); +/// let app = App::new().service( +/// web::resource("/users/:first").route(web::post().to(index)) +/// ); /// } /// ``` impl FromRequest

    for Result diff --git a/src/guard.rs b/src/guard.rs index 93b6e1325..1632b9975 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -19,11 +19,10 @@ pub trait Guard { /// use actix_web::{web, guard, App, HttpResponse}; /// /// fn main() { -/// App::new().resource("/index.html", |r| -/// r.route( -/// web::route() -/// .guard(guard::Any(guard::Get()).or(guard::Post())) -/// .to(|| HttpResponse::MethodNotAllowed())) +/// App::new().service(web::resource("/index.html").route( +/// web::route() +/// .guard(guard::Any(guard::Get()).or(guard::Post())) +/// .to(|| HttpResponse::MethodNotAllowed())) /// ); /// } /// ``` @@ -60,12 +59,12 @@ impl Guard for AnyGuard { /// use actix_web::{guard, web, App, HttpResponse}; /// /// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route(web::route() +/// App::new().service(web::resource("/index.html").route( +/// web::route() /// .guard( /// guard::All(guard::Get()).and(guard::Header("content-type", "text/plain"))) /// .to(|| HttpResponse::MethodNotAllowed())) -/// }); +/// ); /// } /// ``` pub fn All(guard: F) -> AllGuard { diff --git a/src/lib.rs b/src/lib.rs index 44dcde35b..3400fe290 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod extract; mod handler; // mod info; pub mod blocking; +mod config; pub mod guard; pub mod middleware; mod request; @@ -43,13 +44,24 @@ pub mod dev { //! use actix_web::dev::*; //! ``` - pub use crate::app::{AppRouter, HttpServiceFactory}; + pub use crate::app::AppRouter; + pub use crate::config::AppConfig; + pub use crate::service::HttpServiceFactory; + pub use actix_http::body::{Body, MessageBody, ResponseBody}; pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder; pub use actix_http::{ Extensions, Payload, PayloadStream, RequestHead, ResponseHead, }; pub use actix_router::{Path, ResourceDef, Url}; + + pub(crate) fn insert_slash(path: &str) -> String { + let mut path = path.to_owned(); + if !path.is_empty() && !path.starts_with('/') { + path.insert(0, '/'); + }; + path + } } pub mod web { @@ -58,8 +70,74 @@ pub mod web { use crate::extract::FromRequest; use crate::handler::{AsyncFactory, Factory}; + use crate::resource::Resource; use crate::responder::Responder; - use crate::Route; + use crate::route::Route; + use crate::scope::Scope; + + /// Create resource for a specific path. + /// + /// Resources may have variable path segments. For example, a + /// resource with the path `/a/{name}/c` would match all incoming + /// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. + /// + /// A variable segment is specified in the form `{identifier}`, + /// where the identifier can be used later in a request handler to + /// access the matched value for that segment. This is done by + /// looking up the identifier in the `Params` object returned by + /// `HttpRequest.match_info()` method. + /// + /// By default, each segment matches the regular expression `[^{}/]+`. + /// + /// You can also specify a custom regex in the form `{identifier:regex}`: + /// + /// For instance, to route `GET`-requests on any route matching + /// `/users/{userid}/{friend}` and store `userid` and `friend` in + /// the exposed `Params` object: + /// + /// ```rust + /// # extern crate actix_web; + /// use actix_web::{web, http, App, HttpResponse}; + /// + /// fn main() { + /// let app = App::new().service( + /// web::resource("/users/{userid}/{friend}") + /// .route(web::get().to(|| HttpResponse::Ok())) + /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) + /// ); + /// } + /// ``` + pub fn resource(path: &str) -> Resource

    { + Resource::new(path) + } + + /// Configure scope for common root path. + /// + /// Scopes collect multiple paths under a common path prefix. + /// Scope path can contain variable path segments as resources. + /// + /// ```rust + /// # extern crate actix_web; + /// use actix_web::{web, App, HttpRequest, HttpResponse}; + /// + /// fn main() { + /// let app = App::new().service( + /// web::scope("/{project_id}") + /// .service(web::resource("/path1").to(|| HttpResponse::Ok())) + /// .service(web::resource("/path2").to(|| HttpResponse::Ok())) + /// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) + /// ); + /// } + /// ``` + /// + /// In the above example, three routes get added: + /// * /{project_id}/path1 + /// * /{project_id}/path2 + /// * /{project_id}/path3 + /// + pub fn scope(path: &str) -> Scope

    { + Scope::new(path) + } /// Create **route** without configuration. pub fn route() -> Route

    { @@ -105,7 +183,10 @@ pub mod web { /// unimplemented!() /// } /// - /// App::new().resource("/", |r| r.route(web::to(index))); + /// App::new().service( + /// web::resource("/").route( + /// web::to(index)) + /// ); /// ``` pub fn to(handler: F) -> Route

    where @@ -125,7 +206,9 @@ pub mod web { /// futures::future::ok(HttpResponse::Ok().finish()) /// } /// - /// App::new().resource("/", |r| r.route(web::to_async(index))); + /// App::new().service(web::resource("/").route( + /// web::to_async(index)) + /// ); /// ``` pub fn to_async(handler: F) -> Route

    where diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index 5fd519195..137913d21 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -19,10 +19,11 @@ use crate::service::{ServiceRequest, ServiceResponse}; /// fn main() { /// let app = App::new() /// .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) -/// .resource("/test", |r| { -/// r.route(web::get().to(|| HttpResponse::Ok())) -/// .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed())) -/// }); +/// .service( +/// web::resource("/test") +/// .route(web::get().to(|| HttpResponse::Ok())) +/// .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed())) +/// ); /// } /// ``` #[derive(Clone)] diff --git a/src/request.rs b/src/request.rs index 211f60b85..1c86cac35 100644 --- a/src/request.rs +++ b/src/request.rs @@ -149,9 +149,10 @@ impl HttpMessage for HttpRequest { /// } /// /// fn main() { -/// let app = App::new().resource("/users/:first", |r| { -/// r.route(web::get().to(index)) -/// }); +/// let app = App::new().service( +/// web::resource("/users/{first}").route( +/// web::get().to(index)) +/// ); /// } /// ``` impl

    FromRequest

    for HttpRequest { diff --git a/src/resource.rs b/src/resource.rs index 8d81ead06..b5cf640c4 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -9,7 +9,9 @@ use actix_service::{ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; +use crate::dev::{insert_slash, AppConfig, HttpServiceFactory, ResourceDef}; use crate::extract::FromRequest; +use crate::guard::Guard; use crate::handler::{AsyncFactory, Factory}; use crate::responder::Responder; use crate::route::{CreateRouteService, Route, RouteService}; @@ -32,38 +34,37 @@ type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, /// use actix_web::{web, App, HttpResponse}; /// /// fn main() { -/// let app = App::new() -/// .resource( -/// "/", |r| r.route(web::get().to(|| HttpResponse::Ok()))); +/// let app = App::new().service( +/// web::resource("/") +/// .route(web::get().to(|| HttpResponse::Ok()))); /// } pub struct Resource> { - routes: Vec>, endpoint: T, + rdef: String, + routes: Vec>, + guards: Vec>, default: Rc>>>>, factory_ref: Rc>>>, } impl

    Resource

    { - pub fn new() -> Resource

    { + pub fn new(path: &str) -> Resource

    { let fref = Rc::new(RefCell::new(None)); Resource { routes: Vec::new(), + rdef: path.to_string(), endpoint: ResourceEndpoint::new(fref.clone()), factory_ref: fref, + guards: Vec::new(), default: Rc::new(RefCell::new(None)), } } } -impl

    Default for Resource

    { - fn default() -> Self { - Self::new() - } -} - -impl Resource +impl Resource where + P: 'static, T: NewService< ServiceRequest

    , Response = ServiceResponse, @@ -71,19 +72,52 @@ where InitError = (), >, { + /// Add match guard to a resource. + /// + /// ```rust + /// use actix_web::{web, guard, App, HttpResponse, extract::Path}; + /// + /// fn index(data: Path<(String, String)>) -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new() + /// .service( + /// web::resource("/app") + /// .guard(guard::Header("content-type", "text/plain")) + /// .route(web::get().to(index)) + /// ) + /// .service( + /// web::resource("/app") + /// .guard(guard::Header("content-type", "text/json")) + /// .route(web::get().to(|| HttpResponse::MethodNotAllowed())) + /// ); + /// } + /// ``` + pub fn guard(mut self, guard: G) -> Self { + self.guards.push(Box::new(guard)); + self + } + + pub(crate) fn add_guards(mut self, guards: Vec>) -> Self { + self.guards.extend(guards); + self + } + /// Register a new route. /// /// ```rust /// use actix_web::{web, guard, App, HttpResponse}; /// /// fn main() { - /// let app = App::new() - /// .resource("/", |r| { - /// r.route(web::route() + /// let app = App::new().service( + /// web::resource("/").route( + /// web::route() /// .guard(guard::Any(guard::Get()).or(guard::Put())) /// .guard(guard::Header("Content-Type", "text/plain")) /// .to(|| HttpResponse::Ok())) - /// }); + /// ); /// } /// ``` /// @@ -93,12 +127,12 @@ where /// use actix_web::{web, guard, App, HttpResponse}; /// /// fn main() { - /// let app = App::new() - /// .resource("/container/", |r| { - /// r.route(web::get().to(get_handler)) + /// let app = App::new().service( + /// web::resource("/container/") + /// .route(web::get().to(get_handler)) /// .route(web::post().to(post_handler)) /// .route(web::delete().to(delete_handler)) - /// }); + /// ); /// } /// # fn get_handler() {} /// # fn post_handler() {} @@ -109,8 +143,7 @@ where self } - /// Register a new route and add handler. This route get called for all - /// requests. + /// Register a new route and add handler. This route matches all requests. /// /// ```rust /// use actix_web::*; @@ -119,7 +152,7 @@ where /// unimplemented!() /// } /// - /// App::new().resource("/", |r| r.to(index)); + /// App::new().service(web::resource("/").to(index)); /// ``` /// /// This is shortcut for: @@ -128,7 +161,7 @@ where /// # extern crate actix_web; /// # use actix_web::*; /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } - /// App::new().resource("/", |r| r.route(web::route().to(index))); + /// App::new().service(web::resource("/").route(web::route().to(index))); /// ``` pub fn to(mut self, handler: F) -> Self where @@ -150,7 +183,7 @@ where /// ok(HttpResponse::Ok().finish()) /// } /// - /// App::new().resource("/", |r| r.to_async(index)); + /// App::new().service(web::resource("/").to_async(index)); /// ``` /// /// This is shortcut for: @@ -161,7 +194,7 @@ where /// # fn index(req: HttpRequest) -> Box> { /// # unimplemented!() /// # } - /// App::new().resource("/", |r| r.route(web::route().to_async(index))); + /// App::new().service(web::resource("/").route(web::route().to_async(index))); /// ``` #[allow(clippy::wrong_self_convention)] pub fn to_async(mut self, handler: F) -> Self @@ -206,6 +239,8 @@ where let endpoint = ApplyTransform::new(mw, self.endpoint); Resource { endpoint, + rdef: self.rdef, + guards: self.guards, routes: self.routes, default: self.default, factory_ref: self.factory_ref, @@ -222,14 +257,38 @@ where { // create and configure default resource self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::new_service( - f(Resource::new()).into_new_service().map_init_err(|_| ()), + f(Resource::new("")).into_new_service().map_init_err(|_| ()), ))))); self } +} - pub(crate) fn get_default(&self) -> Rc>>>> { - self.default.clone() +impl HttpServiceFactory

    for Resource +where + P: 'static, + T: NewService< + ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, +{ + fn register(mut self, config: &mut AppConfig

    ) { + if self.default.borrow().is_none() { + *self.default.borrow_mut() = Some(config.default_service()); + } + let guards = if self.guards.is_empty() { + None + } else { + Some(std::mem::replace(&mut self.guards, Vec::new())) + }; + let rdef = if config.is_root() { + ResourceDef::new(&insert_slash(&self.rdef)) + } else { + ResourceDef::new(&insert_slash(&self.rdef)) + }; + config.register_service(rdef, guards, self) } } diff --git a/src/responder.rs b/src/responder.rs index dedfa1b44..9e9e0f109 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -288,22 +288,21 @@ where #[cfg(test)] mod tests { - // use actix_http::body::Body; - use actix_http::body::{Body, ResponseBody}; - use actix_http::http::StatusCode; - use actix_service::{IntoNewService, NewService, Service}; + use actix_service::Service; use bytes::Bytes; - use crate::test::TestRequest; - use crate::App; + use crate::body::{Body, ResponseBody}; + use crate::http::StatusCode; + use crate::test::{init_service, TestRequest}; + use crate::{web, App}; #[test] fn test_option_responder() { - let app = App::new() - .resource("/none", |r| r.to(|| -> Option<&'static str> { None })) - .resource("/some", |r| r.to(|| Some("some"))) - .into_new_service(); - let mut srv = TestRequest::block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new() + .service(web::resource("/none").to(|| -> Option<&'static str> { None })) + .service(web::resource("/some").to(|| Some("some"))), + ); let req = TestRequest::with_uri("/none").to_request(); let resp = TestRequest::block_on(srv.call(req)).unwrap(); diff --git a/src/route.rs b/src/route.rs index 42e784881..bac897ab1 100644 --- a/src/route.rs +++ b/src/route.rs @@ -84,6 +84,10 @@ impl Route

    { *self.config_ref.borrow_mut() = self.config.storage.clone(); self } + + pub(crate) fn take_guards(&mut self) -> Vec> { + std::mem::replace(Rc::get_mut(&mut self.guards).unwrap(), Vec::new()) + } } impl

    NewService> for Route

    { @@ -161,12 +165,12 @@ impl Route

    { /// ```rust /// # use actix_web::*; /// # fn main() { - /// App::new().resource("/path", |r| { - /// r.route(web::get() - /// .guard(guard::Get()) + /// App::new().service(web::resource("/path").route( + /// web::get() + /// .method(http::Method::CONNECT) /// .guard(guard::Header("content-type", "text/plain")) /// .to(|req: HttpRequest| HttpResponse::Ok())) - /// }); + /// ); /// # } /// ``` pub fn method(mut self, method: Method) -> Self { @@ -181,12 +185,12 @@ impl Route

    { /// ```rust /// # use actix_web::*; /// # fn main() { - /// App::new().resource("/path", |r| { - /// r.route(web::route() + /// App::new().service(web::resource("/path").route( + /// web::route() /// .guard(guard::Get()) /// .guard(guard::Header("content-type", "text/plain")) /// .to(|req: HttpRequest| HttpResponse::Ok())) - /// }); + /// ); /// # } /// ``` pub fn guard(mut self, f: F) -> Self { @@ -229,9 +233,9 @@ impl Route

    { /// } /// /// fn main() { - /// let app = App::new().resource( - /// "/{username}/index.html", // <- define path parameters - /// |r| r.route(web::get().to(index)), // <- register handler + /// let app = App::new().service( + /// web::resource("/{username}/index.html") // <- define path parameters + /// .route(web::get().to(index)) // <- register handler /// ); /// } /// ``` @@ -254,9 +258,9 @@ impl Route

    { /// } /// /// fn main() { - /// let app = App::new().resource( - /// "/{username}/index.html", // <- define path parameters - /// |r| r.route(web::get().to(index)), + /// let app = App::new().service( + /// web::resource("/{username}/index.html") // <- define path parameters + /// .route(web::get().to(index)) /// ); /// } /// ``` @@ -294,9 +298,9 @@ impl Route

    { /// } /// /// fn main() { - /// let app = App::new().resource( - /// "/{username}/index.html", // <- define path parameters - /// |r| r.route(web::get().to_async(index)), // <- register async handler + /// let app = App::new().service( + /// web::resource("/{username}/index.html") // <- define path parameters + /// .route(web::get().to_async(index)) // <- register async handler /// ); /// } /// ``` @@ -328,15 +332,14 @@ impl Route

    { /// } /// /// fn main() { - /// let app = App::new().resource("/index.html", |r| { - /// r.route( + /// let app = App::new().service( + /// web::resource("/index.html").route( /// web::get() /// // limit size of the payload /// .config(extract::PayloadConfig::new(4096)) /// // register handler /// .to(index) - /// ) - /// }); + /// )); /// } /// ``` pub fn config(mut self, config: C) -> Self { diff --git a/src/scope.rs b/src/scope.rs index dc88388f8..9bc0b50d7 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -10,10 +10,13 @@ use actix_service::{ use futures::future::{ok, Either, Future, FutureResult}; use futures::{Async, Poll}; +use crate::dev::{insert_slash, AppConfig, HttpServiceFactory}; use crate::guard::Guard; use crate::resource::Resource; use crate::route::Route; -use crate::service::{ServiceRequest, ServiceResponse}; +use crate::service::{ + ServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, +}; type Guards = Vec>; type HttpService

    = BoxedService, ServiceResponse, ()>; @@ -35,12 +38,12 @@ type BoxedResponse = Box>; /// use actix_web::{web, App, HttpResponse}; /// /// fn main() { -/// let app = App::new().scope("/{project_id}/", |scope| { -/// scope -/// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) -/// .resource("/path2", |r| r.route(web::get().to(|| HttpResponse::Ok()))) -/// .resource("/path3", |r| r.route(web::head().to(|| HttpResponse::MethodNotAllowed()))) -/// }); +/// let app = App::new().service( +/// web::scope("/{project_id}/") +/// .service(web::resource("/path1").to(|| HttpResponse::Ok())) +/// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) +/// .service(web::resource("/path3").route(web::head().to(|| HttpResponse::MethodNotAllowed()))) +/// ); /// } /// ``` /// @@ -51,11 +54,10 @@ type BoxedResponse = Box>; /// pub struct Scope> { endpoint: T, - rdef: ResourceDef, - services: Vec<(ResourceDef, HttpNewService

    , Option)>, + rdef: String, + services: Vec>>, guards: Vec>, default: Rc>>>>, - defaults: Vec>>>>>, factory_ref: Rc>>>, } @@ -63,21 +65,20 @@ impl Scope

    { /// Create a new scope pub fn new(path: &str) -> Scope

    { let fref = Rc::new(RefCell::new(None)); - let rdef = ResourceDef::prefix(&insert_slash(path)); Scope { endpoint: ScopeEndpoint::new(fref.clone()), - rdef: rdef.clone(), + rdef: path.to_string(), guards: Vec::new(), services: Vec::new(), default: Rc::new(RefCell::new(None)), - defaults: Vec::new(), factory_ref: fref, } } } -impl Scope +impl Scope where + P: 'static, T: NewService< ServiceRequest

    , Response = ServiceResponse, @@ -85,12 +86,7 @@ where InitError = (), >, { - #[inline] - pub(crate) fn rdef(&self) -> &ResourceDef { - &self.rdef - } - - /// Add guard to a scope. + /// Add match guard to a scope. /// /// ```rust /// use actix_web::{web, guard, App, HttpRequest, HttpResponse, extract::Path}; @@ -100,14 +96,14 @@ where /// } /// /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope + /// let app = App::new().service( + /// web::scope("/app") /// .guard(guard::Header("content-type", "text/plain")) - /// .route("/test1",web::get().to(index)) + /// .route("/test1", web::get().to(index)) /// .route("/test2", web::post().to(|r: HttpRequest| { /// HttpResponse::MethodNotAllowed() /// })) - /// }); + /// ); /// } /// ``` pub fn guard(mut self, guard: G) -> Self { @@ -115,10 +111,10 @@ where self } - /// Create nested scope. + /// Create nested service. /// /// ```rust - /// use actix_web::{App, HttpRequest}; + /// use actix_web::{web, App, HttpRequest}; /// /// struct AppState; /// @@ -127,28 +123,25 @@ where /// } /// /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope.nested("/v1", |scope| scope.resource("/test1", |r| r.to(index))) - /// }); + /// let app = App::new().service( + /// web::scope("/app").service( + /// web::scope("/v1") + /// .service(web::resource("/test1").to(index))) + /// ); /// } /// ``` - pub fn nested(mut self, path: &str, f: F) -> Self + pub fn service(mut self, factory: F) -> Self where - F: FnOnce(Scope

    ) -> Scope

    , + F: HttpServiceFactory

    + 'static, { - let mut scope = f(Scope::new(path)); - let rdef = scope.rdef().clone(); - let guards = scope.take_guards(); - self.defaults.push(scope.get_default()); self.services - .push((rdef, boxed::new_service(scope.into_new_service()), guards)); - + .push(Box::new(ServiceFactoryWrapper::new(factory))); self } /// Configure route for a specific path. /// - /// This is a simplified version of the `Scope::resource()` method. + /// This is a simplified version of the `Scope::service()` method. /// This method can not be could multiple times, in that case /// multiple resources with one route would be registered for same resource path. /// @@ -160,58 +153,19 @@ where /// } /// /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope.route("/test1", web::get().to(index)) + /// let app = App::new().service( + /// web::scope("/app") + /// .route("/test1", web::get().to(index)) /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())) - /// }); + /// ); /// } /// ``` - pub fn route(self, path: &str, route: Route

    ) -> Self { - self.resource(path, move |r| r.route(route)) - } - - /// configure resource for a specific path. - /// - /// This method is similar to an `App::resource()` method. - /// Resources may have variable path segments. Resource path uses scope - /// path as a path prefix. - /// - /// ```rust - /// use actix_web::*; - /// - /// fn main() { - /// let app = App::new().scope("/api", |scope| { - /// scope.resource("/users/{userid}/{friend}", |r| { - /// r.route(web::get().to(|| HttpResponse::Ok())) - /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) - /// .route(web::route() - /// .guard(guard::Any(guard::Get()).or(guard::Put())) - /// .guard(guard::Header("Content-Type", "text/plain")) - /// .to(|| HttpResponse::Ok())) - /// }) - /// }); - /// } - /// ``` - pub fn resource(mut self, path: &str, f: F) -> Self - where - F: FnOnce(Resource

    ) -> Resource, - U: NewService< - ServiceRequest

    , - Response = ServiceResponse, - Error = (), - InitError = (), - > + 'static, - { - // add resource - let rdef = ResourceDef::new(&insert_slash(path)); - let resource = f(Resource::new()); - self.defaults.push(resource.get_default()); - self.services.push(( - rdef, - boxed::new_service(resource.into_new_service()), - None, - )); - self + pub fn route(self, path: &str, mut route: Route

    ) -> Self { + self.service( + Resource::new(path) + .add_guards(route.take_guards()) + .route(route), + ) } /// Default resource to be used if no matching route could be found. @@ -227,7 +181,7 @@ where { // create and configure default resource self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::new_service( - f(Resource::new()).into_new_service().map_init_err(|_| ()), + f(Resource::new("")).into_new_service().map_init_err(|_| ()), ))))); self @@ -267,62 +221,53 @@ where guards: self.guards, services: self.services, default: self.default, - defaults: self.defaults, factory_ref: self.factory_ref, } } - - pub(crate) fn get_default(&self) -> Rc>>>> { - self.default.clone() - } - - pub(crate) fn take_guards(&mut self) -> Option>> { - if self.guards.is_empty() { - None - } else { - Some(std::mem::replace(&mut self.guards, Vec::new())) - } - } } -pub(crate) fn insert_slash(path: &str) -> String { - let mut path = path.to_owned(); - if !path.is_empty() && !path.starts_with('/') { - path.insert(0, '/'); - }; - path -} - -impl IntoNewService> for Scope +impl HttpServiceFactory

    for Scope where + P: 'static, T: NewService< - ServiceRequest

    , - Response = ServiceResponse, - Error = (), - InitError = (), - >, + ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, { - fn into_new_service(self) -> T { - // update resource default service - if let Some(ref d) = *self.default.as_ref().borrow() { - for default in &self.defaults { - if default.borrow_mut().is_none() { - *default.borrow_mut() = Some(d.clone()); - } - } + fn register(self, config: &mut AppConfig

    ) { + if self.default.borrow().is_none() { + *self.default.borrow_mut() = Some(config.default_service()); } + // register services + let mut cfg = config.clone_config(); + self.services + .into_iter() + .for_each(|mut srv| srv.register(&mut cfg)); + *self.factory_ref.borrow_mut() = Some(ScopeFactory { default: self.default.clone(), services: Rc::new( - self.services + cfg.into_services() .into_iter() .map(|(rdef, srv, guards)| (rdef, srv, RefCell::new(guards))) .collect(), ), }); - self.endpoint + let guards = if self.guards.is_empty() { + None + } else { + Some(self.guards) + }; + let rdef = if config.is_root() { + ResourceDef::prefix(&insert_slash(&self.rdef)) + } else { + ResourceDef::prefix(&insert_slash(&self.rdef)) + }; + config.register_service(rdef, guards, self.endpoint) } } @@ -504,19 +449,22 @@ impl NewService> for ScopeEndpoint

    { #[cfg(test)] mod tests { - use actix_http::body::{Body, ResponseBody}; - use actix_http::http::{Method, StatusCode}; - use actix_service::{IntoNewService, NewService, Service}; + use actix_service::Service; use bytes::Bytes; - use crate::test::{self, block_on, TestRequest}; + use crate::body::{Body, ResponseBody}; + use crate::http::{Method, StatusCode}; + use crate::test::{block_on, init_service, TestRequest}; use crate::{guard, web, App, HttpRequest, HttpResponse}; #[test] fn test_scope() { - let mut srv = test::init_service(App::new().scope("/app", |scope| { - scope.resource("/path1", |r| r.to(|| HttpResponse::Ok())) - })); + let mut srv = init_service( + App::new().service( + web::scope("/app") + .service(web::resource("/path1").to(|| HttpResponse::Ok())), + ), + ); let req = TestRequest::with_uri("/app/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -525,11 +473,13 @@ mod tests { #[test] fn test_scope_root() { - let mut srv = test::init_service(App::new().scope("/app", |scope| { - scope - .resource("", |r| r.to(|| HttpResponse::Ok())) - .resource("/", |r| r.to(|| HttpResponse::Created())) - })); + let mut srv = init_service( + App::new().service( + web::scope("/app") + .service(web::resource("").to(|| HttpResponse::Ok())) + .service(web::resource("/").to(|| HttpResponse::Created())), + ), + ); let req = TestRequest::with_uri("/app").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -542,9 +492,9 @@ mod tests { #[test] fn test_scope_root2() { - let mut srv = test::init_service(App::new().scope("/app/", |scope| { - scope.resource("", |r| r.to(|| HttpResponse::Ok())) - })); + let mut srv = init_service(App::new().service( + web::scope("/app/").service(web::resource("").to(|| HttpResponse::Ok())), + )); let req = TestRequest::with_uri("/app").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -557,12 +507,9 @@ mod tests { #[test] fn test_scope_root3() { - let app = App::new() - .scope("/app/", |scope| { - scope.resource("/", |r| r.to(|| HttpResponse::Ok())) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service(App::new().service( + web::scope("/app/").service(web::resource("/").to(|| HttpResponse::Ok())), + )); let req = TestRequest::with_uri("/app").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -575,15 +522,13 @@ mod tests { #[test] fn test_scope_route() { - let app = App::new() - .scope("app", |scope| { - scope.resource("/path1", |r| { - r.route(web::get().to(|| HttpResponse::Ok())) - .route(web::delete().to(|| HttpResponse::Ok())) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new().service( + web::scope("app") + .route("/path1", web::get().to(|| HttpResponse::Ok())) + .route("/path1", web::delete().to(|| HttpResponse::Ok())), + ), + ); let req = TestRequest::with_uri("/app/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -604,15 +549,15 @@ mod tests { #[test] fn test_scope_route_without_leading_slash() { - let app = App::new() - .scope("app", |scope| { - scope.resource("path1", |r| { - r.route(web::get().to(|| HttpResponse::Ok())) - .route(web::delete().to(|| HttpResponse::Ok())) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new().service( + web::scope("app").service( + web::resource("path1") + .route(web::get().to(|| HttpResponse::Ok())) + .route(web::delete().to(|| HttpResponse::Ok())), + ), + ), + ); let req = TestRequest::with_uri("/app/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -633,14 +578,13 @@ mod tests { #[test] fn test_scope_guard() { - let app = App::new() - .scope("/app", |scope| { - scope + let mut srv = init_service( + App::new().service( + web::scope("/app") .guard(guard::Get()) - .resource("/path1", |r| r.to(|| HttpResponse::Ok())) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + .service(web::resource("/path1").to(|| HttpResponse::Ok())), + ), + ); let req = TestRequest::with_uri("/app/path1") .method(Method::POST) @@ -657,17 +601,13 @@ mod tests { #[test] fn test_scope_variable_segment() { - let app = App::new() - .scope("/ab-{project}", |scope| { - scope.resource("/path1", |r| { - r.to(|r: HttpRequest| { - HttpResponse::Ok() - .body(format!("project: {}", &r.match_info()["project"])) - }) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = + init_service(App::new().service(web::scope("/ab-{project}").service( + web::resource("/path1").to(|r: HttpRequest| { + HttpResponse::Ok() + .body(format!("project: {}", &r.match_info()["project"])) + }), + ))); let req = TestRequest::with_uri("/ab-project1/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -688,14 +628,14 @@ mod tests { #[test] fn test_nested_scope() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope.resource("/path1", |r| r.to(|| HttpResponse::Created())) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new().service( + web::scope("/app") + .service(web::scope("/t1").service( + web::resource("/path1").to(|| HttpResponse::Created()), + )), + ), + ); let req = TestRequest::with_uri("/app/t1/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -704,14 +644,14 @@ mod tests { #[test] fn test_nested_scope_no_slash() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("t1", |scope| { - scope.resource("/path1", |r| r.to(|| HttpResponse::Created())) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new().service( + web::scope("/app") + .service(web::scope("t1").service( + web::resource("/path1").to(|| HttpResponse::Created()), + )), + ), + ); let req = TestRequest::with_uri("/app/t1/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -720,16 +660,15 @@ mod tests { #[test] fn test_nested_scope_root() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope - .resource("", |r| r.to(|| HttpResponse::Ok())) - .resource("/", |r| r.to(|| HttpResponse::Created())) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new().service( + web::scope("/app").service( + web::scope("/t1") + .service(web::resource("").to(|| HttpResponse::Ok())) + .service(web::resource("/").to(|| HttpResponse::Created())), + ), + ), + ); let req = TestRequest::with_uri("/app/t1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -742,16 +681,15 @@ mod tests { #[test] fn test_nested_scope_filter() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope + let mut srv = init_service( + App::new().service( + web::scope("/app").service( + web::scope("/t1") .guard(guard::Get()) - .resource("/path1", |r| r.to(|| HttpResponse::Ok())) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + .service(web::resource("/path1").to(|| HttpResponse::Ok())), + ), + ), + ); let req = TestRequest::with_uri("/app/t1/path1") .method(Method::POST) @@ -768,21 +706,14 @@ mod tests { #[test] fn test_nested_scope_with_variable_segment() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/{project_id}", |scope| { - scope.resource("/path1", |r| { - r.to(|r: HttpRequest| { - HttpResponse::Created().body(format!( - "project: {}", - &r.match_info()["project_id"] - )) - }) - }) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service(App::new().service(web::scope("/app").service( + web::scope("/{project_id}").service(web::resource("/path1").to( + |r: HttpRequest| { + HttpResponse::Created() + .body(format!("project: {}", &r.match_info()["project_id"])) + }, + )), + ))); let req = TestRequest::with_uri("/app/project_1/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -799,24 +730,17 @@ mod tests { #[test] fn test_nested2_scope_with_variable_segment() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/{project}", |scope| { - scope.nested("/{id}", |scope| { - scope.resource("/path1", |r| { - r.to(|r: HttpRequest| { - HttpResponse::Created().body(format!( - "project: {} - {}", - &r.match_info()["project"], - &r.match_info()["id"], - )) - }) - }) - }) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service(App::new().service(web::scope("/app").service( + web::scope("/{project}").service(web::scope("/{id}").service( + web::resource("/path1").to(|r: HttpRequest| { + HttpResponse::Created().body(format!( + "project: {} - {}", + &r.match_info()["project"], + &r.match_info()["id"], + )) + }), + )), + ))); let req = TestRequest::with_uri("/app/test/1/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -837,14 +761,13 @@ mod tests { #[test] fn test_default_resource() { - let app = App::new() - .scope("/app", |scope| { - scope - .resource("/path1", |r| r.to(|| HttpResponse::Ok())) - .default_resource(|r| r.to(|| HttpResponse::BadRequest())) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new().service( + web::scope("/app") + .service(web::resource("/path1").to(|| HttpResponse::Ok())) + .default_resource(|r| r.to(|| HttpResponse::BadRequest())), + ), + ); let req = TestRequest::with_uri("/app/path2").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -857,14 +780,15 @@ mod tests { #[test] fn test_default_resource_propagation() { - let app = App::new() - .scope("/app1", |scope| { - scope.default_resource(|r| r.to(|| HttpResponse::BadRequest())) - }) - .scope("/app2", |scope| scope) - .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new() + .service( + web::scope("/app1") + .default_resource(|r| r.to(|| HttpResponse::BadRequest())), + ) + .service(web::scope("/app2")) + .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())), + ); let req = TestRequest::with_uri("/non-exist").to_request(); let resp = block_on(srv.call(req)).unwrap(); diff --git a/src/server.rs b/src/server.rs index 78ba692e4..c28cb280c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -33,20 +33,19 @@ struct Config { /// /// ```rust /// use std::io; -/// use actix_web::{App, HttpResponse, HttpServer}; +/// use actix_web::{web, App, HttpResponse, HttpServer}; /// /// fn main() -> io::Result<()> { /// let sys = actix_rt::System::new("example"); // <- create Actix runtime /// /// HttpServer::new( /// || App::new() -/// .resource("/", |r| r.to(|| HttpResponse::Ok()))) +/// .service(web::resource("/").to(|| HttpResponse::Ok()))) /// .bind("127.0.0.1:59090")? /// .start(); /// /// # actix_rt::System::current().stop(); -/// sys.run(); -/// Ok(()) +/// sys.run() /// } /// ``` pub struct HttpServer @@ -448,17 +447,17 @@ where /// configured. /// /// ```rust - /// use actix_web::{App, HttpResponse, HttpServer}; + /// use std::io; + /// use actix_web::{web, App, HttpResponse, HttpServer}; /// - /// fn main() { + /// fn main() -> io::Result<()> { /// let sys = actix_rt::System::new("example"); // <- create Actix system /// - /// HttpServer::new(|| App::new().resource("/", |r| r.to(|| HttpResponse::Ok()))) - /// .bind("127.0.0.1:0") - /// .expect("Can not bind to 127.0.0.1:0") + /// HttpServer::new(|| App::new().service(web::resource("/").to(|| HttpResponse::Ok()))) + /// .bind("127.0.0.1:0")? /// .start(); /// # actix_rt::System::current().stop(); - /// sys.run(); // <- Run actix system, this method starts all async processes + /// sys.run() // <- Run actix system, this method starts all async processes /// } /// ``` pub fn start(mut self) -> Server { @@ -472,20 +471,23 @@ where /// /// This methods panics if no socket addresses get bound. /// - /// ```rust,ignore - /// use actix_web::{App, HttpResponse, HttpServer}; + /// ```rust + /// use std::io; + /// use actix_web::{web, App, HttpResponse, HttpServer}; /// - /// fn main() { - /// HttpServer::new(|| App::new().resource("/", |r| r.to(|| HttpResponse::Ok()))) - /// .bind("127.0.0.1:0") - /// .expect("Can not bind to 127.0.0.1:0") - /// .run(); + /// fn main() -> io::Result<()> { + /// # std::thread::spawn(|| { + /// HttpServer::new(|| App::new().service(web::resource("/").to(|| HttpResponse::Ok()))) + /// .bind("127.0.0.1:0")? + /// .run() + /// # }); + /// # Ok(()) /// } /// ``` - pub fn run(self) { + pub fn run(self) -> io::Result<()> { let sys = System::new("http-server"); self.start(); - sys.run(); + sys.run() } } diff --git a/src/service.rs b/src/service.rs index c9666e31e..e2213060c 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::cell::{Ref, RefMut}; use std::fmt; +use std::marker::PhantomData; use std::rc::Rc; use actix_http::body::{Body, MessageBody, ResponseBody}; @@ -12,8 +13,42 @@ use actix_http::{ use actix_router::{Path, Resource, Url}; use futures::future::{ok, FutureResult, IntoFuture}; +use crate::config::AppConfig; use crate::request::HttpRequest; +pub trait HttpServiceFactory

    { + fn register(self, config: &mut AppConfig

    ); +} + +pub(crate) trait ServiceFactory

    { + fn register(&mut self, config: &mut AppConfig

    ); +} + +pub(crate) struct ServiceFactoryWrapper { + factory: Option, + _t: PhantomData

    , +} + +impl ServiceFactoryWrapper { + pub fn new(factory: T) -> Self { + Self { + factory: Some(factory), + _t: PhantomData, + } + } +} + +impl ServiceFactory

    for ServiceFactoryWrapper +where + T: HttpServiceFactory

    , +{ + fn register(&mut self, config: &mut AppConfig

    ) { + if let Some(item) = self.factory.take() { + item.register(config) + } + } +} + pub struct ServiceRequest

    { req: HttpRequest, payload: Payload

    , diff --git a/tests/test_server.rs b/tests/test_server.rs index d28f207c1..ebe968fa5 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -40,7 +40,8 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ fn test_body() { let mut srv = TestServer::new(|| { h1::H1Service::new( - App::new().resource("/", |r| r.route(web::to(|| Response::Ok().body(STR)))), + App::new() + .service(web::resource("/").route(web::to(|| Response::Ok().body(STR)))), ) }); @@ -59,7 +60,7 @@ fn test_body_gzip() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) - .resource("/", |r| r.route(web::to(|| Response::Ok().body(STR)))), + .service(web::resource("/").route(web::to(|| Response::Ok().body(STR)))), ) }); @@ -87,9 +88,10 @@ fn test_body_gzip_large() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) - .resource("/", |r| { - r.route(web::to(move || Response::Ok().body(data.clone()))) - }), + .service( + web::resource("/") + .route(web::to(move || Response::Ok().body(data.clone()))), + ), ) }); @@ -120,9 +122,10 @@ fn test_body_gzip_large_random() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) - .resource("/", |r| { - r.route(web::to(move || Response::Ok().body(data.clone()))) - }), + .service( + web::resource("/") + .route(web::to(move || Response::Ok().body(data.clone()))), + ), ) }); @@ -147,13 +150,11 @@ fn test_body_chunked_implicit() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) - .resource("/", |r| { - r.route(web::get().to(move || { - Response::Ok().streaming(once(Ok::<_, Error>( - Bytes::from_static(STR.as_ref()), - ))) - })) - }), + .service(web::resource("/").route(web::get().to(move || { + Response::Ok().streaming(once(Ok::<_, Error>(Bytes::from_static( + STR.as_ref(), + )))) + }))), ) }); @@ -181,13 +182,11 @@ fn test_body_br_streaming() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Br)) - .resource("/", |r| { - r.route(web::to(move || { - Response::Ok().streaming(once(Ok::<_, Error>( - Bytes::from_static(STR.as_ref()), - ))) - })) - }), + .service(web::resource("/").route(web::to(move || { + Response::Ok().streaming(once(Ok::<_, Error>(Bytes::from_static( + STR.as_ref(), + )))) + }))), ) }); @@ -208,9 +207,9 @@ fn test_body_br_streaming() { #[test] fn test_head_binary() { let mut srv = TestServer::new(move || { - h1::H1Service::new(App::new().resource("/", |r| { - r.route(web::head().to(move || Response::Ok().content_length(100).body(STR))) - })) + h1::H1Service::new(App::new().service(web::resource("/").route( + web::head().to(move || Response::Ok().content_length(100).body(STR)), + ))) }); let request = srv.head().finish().unwrap(); @@ -230,14 +229,14 @@ fn test_head_binary() { #[test] fn test_no_chunking() { let mut srv = TestServer::new(move || { - h1::H1Service::new(App::new().resource("/", |r| { - r.route(web::to(move || { + h1::H1Service::new(App::new().service(web::resource("/").route(web::to( + move || { Response::Ok() .no_chunking() .content_length(STR.len() as u64) .streaming(once(Ok::<_, Error>(Bytes::from_static(STR.as_ref())))) - })) - })) + }, + )))) }); let request = srv.get().finish().unwrap(); @@ -256,7 +255,9 @@ fn test_body_deflate() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Deflate)) - .resource("/", |r| r.route(web::to(move || Response::Ok().body(STR)))), + .service( + web::resource("/").route(web::to(move || Response::Ok().body(STR))), + ), ) }); @@ -281,7 +282,9 @@ fn test_body_brotli() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Br)) - .resource("/", |r| r.route(web::to(move || Response::Ok().body(STR)))), + .service( + web::resource("/").route(web::to(move || Response::Ok().body(STR))), + ), ) }); From 561a89b8b3aa84fb1993c9f7380d8dc077972cef Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 17:32:41 -0800 Subject: [PATCH 217/427] copy logger --- logger.rs | 384 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 logger.rs diff --git a/logger.rs b/logger.rs new file mode 100644 index 000000000..b7bb1bb80 --- /dev/null +++ b/logger.rs @@ -0,0 +1,384 @@ +//! Request logging middleware +use std::collections::HashSet; +use std::env; +use std::fmt::{self, Display, Formatter}; + +use regex::Regex; +use time; + +use error::Result; +use httpmessage::HttpMessage; +use httprequest::HttpRequest; +use httpresponse::HttpResponse; +use middleware::{Finished, Middleware, Started}; + +/// `Middleware` for logging request and response info to the terminal. +/// +/// `Logger` middleware uses standard log crate to log information. You should +/// enable logger for `actix_web` package to see access log. +/// ([`env_logger`](https://docs.rs/env_logger/*/env_logger/) or similar) +/// +/// ## Usage +/// +/// Create `Logger` middleware with the specified `format`. +/// Default `Logger` could be created with `default` method, it uses the +/// default format: +/// +/// ```ignore +/// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T +/// ``` +/// ```rust +/// # extern crate actix_web; +/// extern crate env_logger; +/// use actix_web::middleware::Logger; +/// use actix_web::App; +/// +/// fn main() { +/// std::env::set_var("RUST_LOG", "actix_web=info"); +/// env_logger::init(); +/// +/// let app = App::new() +/// .middleware(Logger::default()) +/// .middleware(Logger::new("%a %{User-Agent}i")) +/// .finish(); +/// } +/// ``` +/// +/// ## Format +/// +/// `%%` The percent sign +/// +/// `%a` Remote IP-address (IP-address of proxy if using reverse proxy) +/// +/// `%t` Time when the request was started to process +/// +/// `%r` First line of request +/// +/// `%s` Response status code +/// +/// `%b` Size of response in bytes, including HTTP headers +/// +/// `%T` Time taken to serve the request, in seconds with floating fraction in +/// .06f format +/// +/// `%D` Time taken to serve the request, in milliseconds +/// +/// `%{FOO}i` request.headers['FOO'] +/// +/// `%{FOO}o` response.headers['FOO'] +/// +/// `%{FOO}e` os.environ['FOO'] +/// +pub struct Logger { + format: Format, + exclude: HashSet, +} + +impl Logger { + /// Create `Logger` middleware with the specified `format`. + pub fn new(format: &str) -> Logger { + Logger { + format: Format::new(format), + exclude: HashSet::new(), + } + } + + /// Ignore and do not log access info for specified path. + pub fn exclude>(mut self, path: T) -> Self { + self.exclude.insert(path.into()); + self + } +} + +impl Default for Logger { + /// Create `Logger` middleware with format: + /// + /// ```ignore + /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T + /// ``` + fn default() -> Logger { + Logger { + format: Format::default(), + exclude: HashSet::new(), + } + } +} + +struct StartTime(time::Tm); + +impl Logger { + fn log(&self, req: &HttpRequest, resp: &HttpResponse) { + if let Some(entry_time) = req.extensions().get::() { + let render = |fmt: &mut Formatter| { + for unit in &self.format.0 { + unit.render(fmt, req, resp, entry_time.0)?; + } + Ok(()) + }; + info!("{}", FormatDisplay(&render)); + } + } +} + +impl Middleware for Logger { + fn start(&self, req: &HttpRequest) -> Result { + if !self.exclude.contains(req.path()) { + req.extensions_mut().insert(StartTime(time::now())); + } + Ok(Started::Done) + } + + fn finish(&self, req: &HttpRequest, resp: &HttpResponse) -> Finished { + self.log(req, resp); + Finished::Done + } +} + +/// A formatting style for the `Logger`, consisting of multiple +/// `FormatText`s concatenated into one line. +#[derive(Clone)] +#[doc(hidden)] +struct Format(Vec); + +impl Default for Format { + /// Return the default formatting style for the `Logger`: + fn default() -> Format { + Format::new(r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#) + } +} + +impl Format { + /// Create a `Format` from a format string. + /// + /// Returns `None` if the format string syntax is incorrect. + pub fn new(s: &str) -> Format { + trace!("Access log format: {}", s); + let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbTD]?)").unwrap(); + + let mut idx = 0; + let mut results = Vec::new(); + for cap in fmt.captures_iter(s) { + let m = cap.get(0).unwrap(); + let pos = m.start(); + if idx != pos { + results.push(FormatText::Str(s[idx..pos].to_owned())); + } + idx = m.end(); + + if let Some(key) = cap.get(2) { + results.push(match cap.get(3).unwrap().as_str() { + "i" => FormatText::RequestHeader(key.as_str().to_owned()), + "o" => FormatText::ResponseHeader(key.as_str().to_owned()), + "e" => FormatText::EnvironHeader(key.as_str().to_owned()), + _ => unreachable!(), + }) + } else { + let m = cap.get(1).unwrap(); + results.push(match m.as_str() { + "%" => FormatText::Percent, + "a" => FormatText::RemoteAddr, + "t" => FormatText::RequestTime, + "r" => FormatText::RequestLine, + "s" => FormatText::ResponseStatus, + "b" => FormatText::ResponseSize, + "T" => FormatText::Time, + "D" => FormatText::TimeMillis, + _ => FormatText::Str(m.as_str().to_owned()), + }); + } + } + if idx != s.len() { + results.push(FormatText::Str(s[idx..].to_owned())); + } + + Format(results) + } +} + +/// A string of text to be logged. This is either one of the data +/// fields supported by the `Logger`, or a custom `String`. +#[doc(hidden)] +#[derive(Debug, Clone)] +pub enum FormatText { + Str(String), + Percent, + RequestLine, + RequestTime, + ResponseStatus, + ResponseSize, + Time, + TimeMillis, + RemoteAddr, + RequestHeader(String), + ResponseHeader(String), + EnvironHeader(String), +} + +impl FormatText { + fn render( + &self, fmt: &mut Formatter, req: &HttpRequest, resp: &HttpResponse, + entry_time: time::Tm, + ) -> Result<(), fmt::Error> { + match *self { + FormatText::Str(ref string) => fmt.write_str(string), + FormatText::Percent => "%".fmt(fmt), + FormatText::RequestLine => { + if req.query_string().is_empty() { + fmt.write_fmt(format_args!( + "{} {} {:?}", + req.method(), + req.path(), + req.version() + )) + } else { + fmt.write_fmt(format_args!( + "{} {}?{} {:?}", + req.method(), + req.path(), + req.query_string(), + req.version() + )) + } + } + FormatText::ResponseStatus => resp.status().as_u16().fmt(fmt), + FormatText::ResponseSize => resp.response_size().fmt(fmt), + FormatText::Time => { + let rt = time::now() - entry_time; + let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000_000.0; + fmt.write_fmt(format_args!("{:.6}", rt)) + } + FormatText::TimeMillis => { + let rt = time::now() - entry_time; + let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000.0; + fmt.write_fmt(format_args!("{:.6}", rt)) + } + FormatText::RemoteAddr => { + if let Some(remote) = req.connection_info().remote() { + return remote.fmt(fmt); + } else { + "-".fmt(fmt) + } + } + FormatText::RequestTime => entry_time + .strftime("[%d/%b/%Y:%H:%M:%S %z]") + .unwrap() + .fmt(fmt), + FormatText::RequestHeader(ref name) => { + let s = if let Some(val) = req.headers().get(name) { + if let Ok(s) = val.to_str() { + s + } else { + "-" + } + } else { + "-" + }; + fmt.write_fmt(format_args!("{}", s)) + } + FormatText::ResponseHeader(ref name) => { + let s = if let Some(val) = resp.headers().get(name) { + if let Ok(s) = val.to_str() { + s + } else { + "-" + } + } else { + "-" + }; + fmt.write_fmt(format_args!("{}", s)) + } + FormatText::EnvironHeader(ref name) => { + if let Ok(val) = env::var(name) { + fmt.write_fmt(format_args!("{}", val)) + } else { + "-".fmt(fmt) + } + } + } + } +} + +pub(crate) struct FormatDisplay<'a>(&'a Fn(&mut Formatter) -> Result<(), fmt::Error>); + +impl<'a> fmt::Display for FormatDisplay<'a> { + fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { + (self.0)(fmt) + } +} + +#[cfg(test)] +mod tests { + use time; + + use super::*; + use http::{header, StatusCode}; + use test::TestRequest; + + #[test] + fn test_logger() { + let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test"); + + let req = TestRequest::with_header( + header::USER_AGENT, + header::HeaderValue::from_static("ACTIX-WEB"), + ).finish(); + let resp = HttpResponse::build(StatusCode::OK) + .header("X-Test", "ttt") + .force_close() + .finish(); + + match logger.start(&req) { + Ok(Started::Done) => (), + _ => panic!(), + }; + match logger.finish(&req, &resp) { + Finished::Done => (), + _ => panic!(), + } + let entry_time = time::now(); + let render = |fmt: &mut Formatter| { + for unit in &logger.format.0 { + unit.render(fmt, &req, &resp, entry_time)?; + } + Ok(()) + }; + let s = format!("{}", FormatDisplay(&render)); + assert!(s.contains("ACTIX-WEB ttt")); + } + + #[test] + fn test_default_format() { + let format = Format::default(); + + let req = TestRequest::with_header( + header::USER_AGENT, + header::HeaderValue::from_static("ACTIX-WEB"), + ).finish(); + let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); + let entry_time = time::now(); + + let render = |fmt: &mut Formatter| { + for unit in &format.0 { + unit.render(fmt, &req, &resp, entry_time)?; + } + Ok(()) + }; + let s = format!("{}", FormatDisplay(&render)); + assert!(s.contains("GET / HTTP/1.1")); + assert!(s.contains("200 0")); + assert!(s.contains("ACTIX-WEB")); + + let req = TestRequest::with_uri("/?test").finish(); + let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); + let entry_time = time::now(); + + let render = |fmt: &mut Formatter| { + for unit in &format.0 { + unit.render(fmt, &req, &resp, entry_time)?; + } + Ok(()) + }; + let s = format!("{}", FormatDisplay(&render)); + assert!(s.contains("GET /?test HTTP/1.1")); + } +} From 244fff9e0af6284e92cc38417f1d295c8e20d5aa Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 19:19:27 -0800 Subject: [PATCH 218/427] added Logger middleware --- Cargo.toml | 2 + src/app.rs | 4 +- src/lib.rs | 2 +- src/middleware/defaultheaders.rs | 6 +- logger.rs => src/middleware/logger.rs | 399 +++++++++++++++++--------- src/middleware/mod.rs | 3 + src/resource.rs | 2 +- src/route.rs | 5 +- src/scope.rs | 6 +- 9 files changed, 279 insertions(+), 150 deletions(-) rename logger.rs => src/middleware/logger.rs (52%) diff --git a/Cargo.toml b/Cargo.toml index a17669d18..2abb4c72e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,10 +81,12 @@ mime = "0.3" net2 = "0.2.33" num_cpus = "1.10" parking_lot = "0.7" +regex = "1.0" serde = "1.0" serde_json = "1.0" serde_urlencoded = "^0.5.3" threadpool = "1.7" +time = "0.1" # middlewares # actix-session = { path="session", optional = true } diff --git a/src/app.rs b/src/app.rs index 7c194d273..f62c064a7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -451,7 +451,7 @@ where // set factory *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { - default: default, + default, services: Rc::new( config .into_services() @@ -784,7 +784,7 @@ where mod tests { use super::*; use crate::http::{Method, StatusCode}; - use crate::test::{self, block_on, init_service, TestRequest}; + use crate::test::{block_on, init_service, TestRequest}; use crate::{web, HttpResponse, State}; #[test] diff --git a/src/lib.rs b/src/lib.rs index 3400fe290..fd1b21f35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,7 +48,7 @@ pub mod dev { pub use crate::config::AppConfig; pub use crate::service::HttpServiceFactory; - pub use actix_http::body::{Body, MessageBody, ResponseBody}; + pub use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder; pub use actix_http::{ Extensions, Payload, PayloadStream, RequestHead, ResponseHead, diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index 137913d21..f4def58d1 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -1,12 +1,12 @@ //! Middleware for setting default response headers use std::rc::Rc; -use actix_http::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; -use actix_http::http::{HeaderMap, HttpTryFrom}; use actix_service::{Service, Transform}; use futures::future::{ok, FutureResult}; use futures::{Future, Poll}; +use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; +use crate::http::{HeaderMap, HttpTryFrom}; use crate::service::{ServiceRequest, ServiceResponse}; /// `Middleware` for setting default response headers. @@ -147,10 +147,10 @@ where #[cfg(test)] mod tests { - use actix_http::http::header::CONTENT_TYPE; use actix_service::FnService; use super::*; + use crate::http::header::CONTENT_TYPE; use crate::test::{block_on, TestRequest}; use crate::{HttpResponse, ServiceRequest}; diff --git a/logger.rs b/src/middleware/logger.rs similarity index 52% rename from logger.rs rename to src/middleware/logger.rs index b7bb1bb80..769d84280 100644 --- a/logger.rs +++ b/src/middleware/logger.rs @@ -2,15 +2,19 @@ use std::collections::HashSet; use std::env; use std::fmt::{self, Display, Formatter}; +use std::rc::Rc; +use actix_service::{Service, Transform}; +use bytes::Bytes; +use futures::future::{ok, FutureResult}; +use futures::{Async, Future, Poll}; use regex::Regex; use time; -use error::Result; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Finished, Middleware, Started}; +use crate::dev::{BodyLength, MessageBody, ResponseBody}; +use crate::error::{Error, Result}; +use crate::service::{ServiceRequest, ServiceResponse}; +use crate::{HttpMessage, HttpResponse}; /// `Middleware` for logging request and response info to the terminal. /// @@ -69,7 +73,9 @@ use middleware::{Finished, Middleware, Started}; /// /// `%{FOO}e` os.environ['FOO'] /// -pub struct Logger { +pub struct Logger(Rc); + +struct Inner { format: Format, exclude: HashSet, } @@ -77,15 +83,18 @@ pub struct Logger { impl Logger { /// Create `Logger` middleware with the specified `format`. pub fn new(format: &str) -> Logger { - Logger { + Logger(Rc::new(Inner { format: Format::new(format), exclude: HashSet::new(), - } + })) } /// Ignore and do not log access info for specified path. pub fn exclude>(mut self, path: T) -> Self { - self.exclude.insert(path.into()); + Rc::get_mut(&mut self.0) + .unwrap() + .exclude + .insert(path.into()); self } } @@ -97,40 +106,147 @@ impl Default for Logger { /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T /// ``` fn default() -> Logger { - Logger { + Logger(Rc::new(Inner { format: Format::default(), exclude: HashSet::new(), + })) + } +} + +impl Transform> for Logger +where + S: Service, Response = ServiceResponse>, + B: MessageBody, +{ + type Response = ServiceResponse>; + type Error = S::Error; + type InitError = (); + type Transform = LoggerMiddleware; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(LoggerMiddleware { + service, + inner: self.0.clone(), + }) + } +} + +/// Logger middleware +pub struct LoggerMiddleware { + inner: Rc, + service: S, +} + +impl Service> for LoggerMiddleware +where + S: Service, Response = ServiceResponse>, + B: MessageBody, +{ + type Response = ServiceResponse>; + type Error = S::Error; + type Future = LoggerResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready() + } + + fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { + if self.inner.exclude.contains(req.path()) { + LoggerResponse { + fut: self.service.call(req), + format: None, + time: time::now(), + } + } else { + let now = time::now(); + let mut format = self.inner.format.clone(); + + for unit in &mut format.0 { + unit.render_request(now, &req); + } + LoggerResponse { + fut: self.service.call(req), + format: Some(format), + time: now, + } } } } -struct StartTime(time::Tm); +#[doc(hidden)] +pub struct LoggerResponse +where + B: MessageBody, + S: Service, Response = ServiceResponse>, +{ + fut: S::Future, + time: time::Tm, + format: Option, +} -impl Logger { - fn log(&self, req: &HttpRequest, resp: &HttpResponse) { - if let Some(entry_time) = req.extensions().get::() { +impl Future for LoggerResponse +where + B: MessageBody, + S: Service, Response = ServiceResponse>, +{ + type Item = ServiceResponse>; + type Error = S::Error; + + fn poll(&mut self) -> Poll { + let res = futures::try_ready!(self.fut.poll()); + + if let Some(ref mut format) = self.format { + for unit in &mut format.0 { + unit.render_response(&res); + } + } + + Ok(Async::Ready(res.map_body(move |_, body| { + ResponseBody::Body(StreamLog { + body, + size: 0, + time: self.time, + format: self.format.take(), + }) + }))) + } +} + +pub struct StreamLog { + body: ResponseBody, + format: Option, + size: usize, + time: time::Tm, +} + +impl Drop for StreamLog { + fn drop(&mut self) { + if let Some(ref format) = self.format { let render = |fmt: &mut Formatter| { - for unit in &self.format.0 { - unit.render(fmt, req, resp, entry_time.0)?; + for unit in &format.0 { + unit.render(fmt, self.size, self.time)?; } Ok(()) }; - info!("{}", FormatDisplay(&render)); + log::info!("{}", FormatDisplay(&render)); } } } -impl Middleware for Logger { - fn start(&self, req: &HttpRequest) -> Result { - if !self.exclude.contains(req.path()) { - req.extensions_mut().insert(StartTime(time::now())); - } - Ok(Started::Done) +impl MessageBody for StreamLog { + fn length(&self) -> BodyLength { + self.body.length() } - fn finish(&self, req: &HttpRequest, resp: &HttpResponse) -> Finished { - self.log(req, resp); - Finished::Done + fn poll_next(&mut self) -> Poll, Error> { + match self.body.poll_next()? { + Async::Ready(Some(chunk)) => { + self.size += chunk.len(); + Ok(Async::Ready(Some(chunk))) + } + val => Ok(val), + } } } @@ -152,7 +268,7 @@ impl Format { /// /// Returns `None` if the format string syntax is incorrect. pub fn new(s: &str) -> Format { - trace!("Access log format: {}", s); + log::trace!("Access log format: {}", s); let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbTD]?)").unwrap(); let mut idx = 0; @@ -215,33 +331,16 @@ pub enum FormatText { } impl FormatText { - fn render( - &self, fmt: &mut Formatter, req: &HttpRequest, resp: &HttpResponse, + fn render( + &self, + fmt: &mut Formatter, + size: usize, entry_time: time::Tm, ) -> Result<(), fmt::Error> { match *self { FormatText::Str(ref string) => fmt.write_str(string), FormatText::Percent => "%".fmt(fmt), - FormatText::RequestLine => { - if req.query_string().is_empty() { - fmt.write_fmt(format_args!( - "{} {} {:?}", - req.method(), - req.path(), - req.version() - )) - } else { - fmt.write_fmt(format_args!( - "{} {}?{} {:?}", - req.method(), - req.path(), - req.query_string(), - req.version() - )) - } - } - FormatText::ResponseStatus => resp.status().as_u16().fmt(fmt), - FormatText::ResponseSize => resp.response_size().fmt(fmt), + FormatText::ResponseSize => size.fmt(fmt), FormatText::Time => { let rt = time::now() - entry_time; let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000_000.0; @@ -252,17 +351,71 @@ impl FormatText { let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000.0; fmt.write_fmt(format_args!("{:.6}", rt)) } - FormatText::RemoteAddr => { - if let Some(remote) = req.connection_info().remote() { - return remote.fmt(fmt); + // FormatText::RemoteAddr => { + // if let Some(remote) = req.connection_info().remote() { + // return remote.fmt(fmt); + // } else { + // "-".fmt(fmt) + // } + // } + FormatText::EnvironHeader(ref name) => { + if let Ok(val) = env::var(name) { + fmt.write_fmt(format_args!("{}", val)) } else { "-".fmt(fmt) } } - FormatText::RequestTime => entry_time - .strftime("[%d/%b/%Y:%H:%M:%S %z]") - .unwrap() - .fmt(fmt), + _ => Ok(()), + } + } + + fn render_response(&mut self, res: &HttpResponse) { + match *self { + FormatText::ResponseStatus => { + *self = FormatText::Str(format!("{}", res.status().as_u16())) + } + FormatText::ResponseHeader(ref name) => { + let s = if let Some(val) = res.headers().get(name) { + if let Ok(s) = val.to_str() { + s + } else { + "-" + } + } else { + "-" + }; + *self = FormatText::Str(s.to_string()) + } + _ => (), + } + } + + fn render_request

    (&mut self, now: time::Tm, req: &ServiceRequest

    ) { + match *self { + FormatText::RequestLine => { + *self = if req.query_string().is_empty() { + FormatText::Str(format!( + "{} {} {:?}", + req.method(), + req.path(), + req.version() + )) + } else { + FormatText::Str(format!( + "{} {}?{} {:?}", + req.method(), + req.path(), + req.query_string(), + req.version() + )) + }; + } + FormatText::RequestTime => { + *self = FormatText::Str(format!( + "{:?}", + now.strftime("[%d/%b/%Y:%H:%M:%S %z]").unwrap() + )) + } FormatText::RequestHeader(ref name) => { let s = if let Some(val) = req.headers().get(name) { if let Ok(s) = val.to_str() { @@ -273,27 +426,9 @@ impl FormatText { } else { "-" }; - fmt.write_fmt(format_args!("{}", s)) - } - FormatText::ResponseHeader(ref name) => { - let s = if let Some(val) = resp.headers().get(name) { - if let Ok(s) = val.to_str() { - s - } else { - "-" - } - } else { - "-" - }; - fmt.write_fmt(format_args!("{}", s)) - } - FormatText::EnvironHeader(ref name) => { - if let Ok(val) = env::var(name) { - fmt.write_fmt(format_args!("{}", val)) - } else { - "-".fmt(fmt) - } + *self = FormatText::Str(s.to_string()); } + _ => (), } } } @@ -308,77 +443,67 @@ impl<'a> fmt::Display for FormatDisplay<'a> { #[cfg(test)] mod tests { - use time; + use actix_service::{FnService, Service, Transform}; use super::*; - use http::{header, StatusCode}; - use test::TestRequest; + use crate::http::{header, StatusCode}; + use crate::test::{block_on, TestRequest}; #[test] fn test_logger() { + let srv = FnService::new(|req: ServiceRequest<_>| { + req.into_response( + HttpResponse::build(StatusCode::OK) + .header("X-Test", "ttt") + .finish(), + ) + }); let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test"); - let req = TestRequest::with_header( - header::USER_AGENT, - header::HeaderValue::from_static("ACTIX-WEB"), - ).finish(); - let resp = HttpResponse::build(StatusCode::OK) - .header("X-Test", "ttt") - .force_close() - .finish(); - - match logger.start(&req) { - Ok(Started::Done) => (), - _ => panic!(), - }; - match logger.finish(&req, &resp) { - Finished::Done => (), - _ => panic!(), - } - let entry_time = time::now(); - let render = |fmt: &mut Formatter| { - for unit in &logger.format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("ACTIX-WEB ttt")); - } - - #[test] - fn test_default_format() { - let format = Format::default(); + let mut srv = block_on(logger.new_transform(srv)).unwrap(); let req = TestRequest::with_header( header::USER_AGENT, header::HeaderValue::from_static("ACTIX-WEB"), - ).finish(); - let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); - let entry_time = time::now(); - - let render = |fmt: &mut Formatter| { - for unit in &format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("GET / HTTP/1.1")); - assert!(s.contains("200 0")); - assert!(s.contains("ACTIX-WEB")); - - let req = TestRequest::with_uri("/?test").finish(); - let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); - let entry_time = time::now(); - - let render = |fmt: &mut Formatter| { - for unit in &format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("GET /?test HTTP/1.1")); + ) + .to_service(); + let _res = block_on(srv.call(req)); } + + // #[test] + // fn test_default_format() { + // let format = Format::default(); + + // let req = TestRequest::with_header( + // header::USER_AGENT, + // header::HeaderValue::from_static("ACTIX-WEB"), + // ) + // .finish(); + // let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); + // let entry_time = time::now(); + + // let render = |fmt: &mut Formatter| { + // for unit in &format.0 { + // unit.render(fmt, &req, &resp, entry_time)?; + // } + // Ok(()) + // }; + // let s = format!("{}", FormatDisplay(&render)); + // assert!(s.contains("GET / HTTP/1.1")); + // assert!(s.contains("200 0")); + // assert!(s.contains("ACTIX-WEB")); + + // let req = TestRequest::with_uri("/?test").finish(); + // let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); + // let entry_time = time::now(); + + // let render = |fmt: &mut Formatter| { + // for unit in &format.0 { + // unit.render(fmt, &req, &resp, entry_time)?; + // } + // Ok(()) + // }; + // let s = format!("{}", FormatDisplay(&render)); + // assert!(s.contains("GET /?test HTTP/1.1")); + // } } diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 8c4cd7543..d63ca8930 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -8,3 +8,6 @@ pub use self::defaultheaders::DefaultHeaders; #[cfg(feature = "session")] pub use actix_session as session; + +mod logger; +pub use self::logger::Logger; diff --git a/src/resource.rs b/src/resource.rs index b5cf640c4..ddcbbcd38 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -286,7 +286,7 @@ where let rdef = if config.is_root() { ResourceDef::new(&insert_slash(&self.rdef)) } else { - ResourceDef::new(&insert_slash(&self.rdef)) + ResourceDef::new(&self.rdef) }; config.register_service(rdef, guards, self) } diff --git a/src/route.rs b/src/route.rs index bac897ab1..b611164e9 100644 --- a/src/route.rs +++ b/src/route.rs @@ -50,9 +50,8 @@ impl Route

    { let config_ref = Rc::new(RefCell::new(None)); Route { service: Box::new(RouteNewService::new( - Extract::new(config_ref.clone()).and_then( - Handle::new(|| HttpResponse::NotFound()).map_err(|_| panic!()), - ), + Extract::new(config_ref.clone()) + .and_then(Handle::new(HttpResponse::NotFound).map_err(|_| panic!())), )), guards: Rc::new(Vec::new()), config: ConfigStorage::default(), diff --git a/src/scope.rs b/src/scope.rs index 9bc0b50d7..29f44fc4a 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -10,7 +10,7 @@ use actix_service::{ use futures::future::{ok, Either, Future, FutureResult}; use futures::{Async, Poll}; -use crate::dev::{insert_slash, AppConfig, HttpServiceFactory}; +use crate::dev::{AppConfig, HttpServiceFactory}; use crate::guard::Guard; use crate::resource::Resource; use crate::route::Route; @@ -263,9 +263,9 @@ where Some(self.guards) }; let rdef = if config.is_root() { - ResourceDef::prefix(&insert_slash(&self.rdef)) + ResourceDef::root_prefix(&self.rdef) } else { - ResourceDef::prefix(&insert_slash(&self.rdef)) + ResourceDef::prefix(&self.rdef) }; config.register_service(rdef, guards, self.endpoint) } From 60c048c8cd1e47e25aeb8973a941a58e9d8ee0e2 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 19:27:18 -0800 Subject: [PATCH 219/427] fix nested resources --- src/middleware/logger.rs | 5 +---- src/resource.rs | 2 +- src/scope.rs | 11 +++++------ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 769d84280..d8b4e643f 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -32,8 +32,6 @@ use crate::{HttpMessage, HttpResponse}; /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T /// ``` /// ```rust -/// # extern crate actix_web; -/// extern crate env_logger; /// use actix_web::middleware::Logger; /// use actix_web::App; /// @@ -43,8 +41,7 @@ use crate::{HttpMessage, HttpResponse}; /// /// let app = App::new() /// .middleware(Logger::default()) -/// .middleware(Logger::new("%a %{User-Agent}i")) -/// .finish(); +/// .middleware(Logger::new("%a %{User-Agent}i")); /// } /// ``` /// diff --git a/src/resource.rs b/src/resource.rs index ddcbbcd38..157b181ef 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -283,7 +283,7 @@ where } else { Some(std::mem::replace(&mut self.guards, Vec::new())) }; - let rdef = if config.is_root() { + let rdef = if config.is_root() || !self.rdef.is_empty() { ResourceDef::new(&insert_slash(&self.rdef)) } else { ResourceDef::new(&self.rdef) diff --git a/src/scope.rs b/src/scope.rs index 29f44fc4a..5580b15e4 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -262,12 +262,11 @@ where } else { Some(self.guards) }; - let rdef = if config.is_root() { - ResourceDef::root_prefix(&self.rdef) - } else { - ResourceDef::prefix(&self.rdef) - }; - config.register_service(rdef, guards, self.endpoint) + config.register_service( + ResourceDef::root_prefix(&self.rdef), + guards, + self.endpoint, + ) } } From e25483a0d58ebf6b688fb195d94471223a082cc1 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 21:12:35 -0800 Subject: [PATCH 220/427] fix warnings --- src/error.rs | 12 +----------- src/header/common/content_disposition.rs | 4 ++-- src/header/shared/quality_item.rs | 2 +- src/request.rs | 2 +- src/test.rs | 2 -- src/ws/client/mod.rs | 14 -------------- 6 files changed, 5 insertions(+), 31 deletions(-) diff --git a/src/error.rs b/src/error.rs index 4762f27ff..a3037b985 100644 --- a/src/error.rs +++ b/src/error.rs @@ -970,23 +970,13 @@ where #[cfg(feature = "fail")] mod failure_integration { use super::*; - use failure::{self, Fail}; /// Compatibility for `failure::Error` - impl ResponseError for failure::Compat - where - T: fmt::Display + fmt::Debug + Sync + Send + 'static, - { + impl ResponseError for failure::Error { fn error_response(&self) -> Response { Response::new(StatusCode::INTERNAL_SERVER_ERROR) } } - - impl From for Error { - fn from(err: failure::Error) -> Error { - err.compat().into() - } - } } #[cfg(test)] diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs index e04f9c89f..700400dad 100644 --- a/src/header/common/content_disposition.rs +++ b/src/header/common/content_disposition.rs @@ -27,7 +27,7 @@ fn split_once(haystack: &str, needle: char) -> (&str, &str) { /// first part and the left of the last part. fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) { let (first, last) = split_once(haystack, needle); - (first.trim_right(), last.trim_left()) + (first.trim_end(), last.trim_start()) } /// The implied disposition of the content of the HTTP body. @@ -324,7 +324,7 @@ impl ContentDisposition { } } left = &left[end.ok_or(crate::error::ParseError::Header)? + 1..]; - left = split_once(left, ';').1.trim_left(); + left = split_once(left, ';').1.trim_start(); // In fact, it should not be Err if the above code is correct. String::from_utf8(quoted_string) .map_err(|_| crate::error::ParseError::Header)? diff --git a/src/header/shared/quality_item.rs b/src/header/shared/quality_item.rs index 07c206581..fc3930c5e 100644 --- a/src/header/shared/quality_item.rs +++ b/src/header/shared/quality_item.rs @@ -58,7 +58,7 @@ impl fmt::Display for QualityItem { match self.quality.0 { 1000 => Ok(()), 0 => f.write_str("; q=0"), - x => write!(f, "; q=0.{}", format!("{:03}", x).trim_right_matches('0')), + x => write!(f, "; q=0.{}", format!("{:03}", x).trim_end_matches('0')), } } } diff --git a/src/request.rs b/src/request.rs index 0ea251ea0..a645c7aeb 100644 --- a/src/request.rs +++ b/src/request.rs @@ -103,7 +103,7 @@ impl

    Request

    { } /// Mutable reference to the message's headers. - fn headers_mut(&mut self) -> &mut HeaderMap { + pub fn headers_mut(&mut self) -> &mut HeaderMap { &mut self.head_mut().headers } diff --git a/src/test.rs b/src/test.rs index 4b925d020..e2e0fd76e 100644 --- a/src/test.rs +++ b/src/test.rs @@ -46,7 +46,6 @@ struct Inner { headers: HeaderMap, _cookies: Option>>, payload: Option, - prefix: u16, } impl Default for TestRequest { @@ -58,7 +57,6 @@ impl Default for TestRequest { headers: HeaderMap::new(), _cookies: None, payload: None, - prefix: 0, })) } } diff --git a/src/ws/client/mod.rs b/src/ws/client/mod.rs index 12c7229b9..8e59e846d 100644 --- a/src/ws/client/mod.rs +++ b/src/ws/client/mod.rs @@ -25,20 +25,6 @@ impl Protocol { } } - fn is_http(self) -> bool { - match self { - Protocol::Https | Protocol::Http => true, - _ => false, - } - } - - fn is_secure(self) -> bool { - match self { - Protocol::Https | Protocol::Wss => true, - _ => false, - } - } - fn port(self) -> u16 { match self { Protocol::Http | Protocol::Ws => 80, From 3b069e0568cbd2ecc129afd0ace04a968cc4cb0c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 22:56:34 -0800 Subject: [PATCH 221/427] added combined http1/2 service --- Cargo.toml | 3 +- src/error.rs | 25 +-- src/h1/dispatcher.rs | 53 +++-- src/h1/mod.rs | 27 +-- src/h1/service.rs | 15 +- src/h2/dispatcher.rs | 28 ++- src/h2/mod.rs | 18 +- src/h2/service.rs | 72 +++---- src/lib.rs | 1 + src/service/mod.rs | 2 + src/service/service.rs | 446 +++++++++++++++++++++++++++++++++++++++++ test-server/src/lib.rs | 2 +- tests/test_server.rs | 52 ++++- 13 files changed, 575 insertions(+), 169 deletions(-) create mode 100644 src/service/service.rs diff --git a/Cargo.toml b/Cargo.toml index a0eb17312..9d55b85c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,8 @@ fail = ["failure"] [dependencies] #actix-service = "0.3.2" -actix-codec = "0.1.0" +actix-codec = "0.1.1" + #actix-connector = "0.3.0" #actix-utils = "0.3.1" diff --git a/src/error.rs b/src/error.rs index a3037b985..696162f86 100644 --- a/src/error.rs +++ b/src/error.rs @@ -328,12 +328,11 @@ impl ResponseError for cookie::ParseError { } } -#[derive(Debug, Display)] +#[derive(Debug, Display, From)] /// A set of errors that can occur during dispatching http requests -pub enum DispatchError { +pub enum DispatchError { /// Service error - #[display(fmt = "Service specific error: {:?}", _0)] - Service(E), + Service, /// An `io::Error` that occurred while trying to read or write to a network /// stream. @@ -373,24 +372,6 @@ pub enum DispatchError { Unknown, } -impl From for DispatchError { - fn from(err: ParseError) -> Self { - DispatchError::Parse(err) - } -} - -impl From for DispatchError { - fn from(err: io::Error) -> Self { - DispatchError::Io(err) - } -} - -impl From for DispatchError { - fn from(err: h2::Error) -> Self { - DispatchError::H2(err) - } -} - /// A set of error that can occure during parsing content type #[derive(PartialEq, Debug, Display)] pub enum ContentTypeError { diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 7024ce3af..42ab33e79 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -20,7 +20,7 @@ use crate::response::Response; use super::codec::Codec; use super::payload::{Payload, PayloadSender, PayloadStatus, PayloadWriter}; -use super::{H1ServiceResult, Message, MessageType}; +use super::{Message, MessageType}; const MAX_PIPELINED_MESSAGES: usize = 16; @@ -50,7 +50,7 @@ where service: CloneableService, flags: Flags, framed: Framed, - error: Option>, + error: Option, config: ServiceConfig, state: State, @@ -93,12 +93,17 @@ where { /// Create http/1 dispatcher. pub fn new(stream: T, config: ServiceConfig, service: CloneableService) -> Self { - Dispatcher::with_timeout(stream, config, None, service) + Dispatcher::with_timeout( + Framed::new(stream, Codec::new(config.clone())), + config, + None, + service, + ) } /// Create http/1 dispatcher with slow request timeout. pub fn with_timeout( - stream: T, + framed: Framed, config: ServiceConfig, timeout: Option, service: CloneableService, @@ -109,7 +114,6 @@ where } else { Flags::empty() }; - let framed = Framed::new(stream, Codec::new(config.clone())); // keep-alive timer let (ka_expire, ka_timer) = if let Some(delay) = timeout { @@ -167,7 +171,7 @@ where } /// Flush stream - fn poll_flush(&mut self) -> Poll> { + fn poll_flush(&mut self) -> Poll { if !self.framed.is_write_buf_empty() { match self.framed.poll_complete() { Ok(Async::NotReady) => Ok(Async::NotReady), @@ -192,7 +196,7 @@ where &mut self, message: Response<()>, body: ResponseBody, - ) -> Result, DispatchError> { + ) -> Result, DispatchError> { self.framed .force_send(Message::Item((message, body.length()))) .map_err(|err| { @@ -210,7 +214,7 @@ where } } - fn poll_response(&mut self) -> Result<(), DispatchError> { + fn poll_response(&mut self) -> Result<(), DispatchError> { let mut retry = self.can_read(); loop { let state = match mem::replace(&mut self.state, State::None) { @@ -225,7 +229,7 @@ where None => None, }, State::ServiceCall(mut fut) => { - match fut.poll().map_err(DispatchError::Service)? { + match fut.poll().map_err(|_| DispatchError::Service)? { Async::Ready(res) => { let (res, body) = res.into().replace_body(()); Some(self.send_response(res, body)?) @@ -283,12 +287,9 @@ where Ok(()) } - fn handle_request( - &mut self, - req: Request, - ) -> Result, DispatchError> { + fn handle_request(&mut self, req: Request) -> Result, DispatchError> { let mut task = self.service.call(req); - match task.poll().map_err(DispatchError::Service)? { + match task.poll().map_err(|_| DispatchError::Service)? { Async::Ready(res) => { let (res, body) = res.into().replace_body(()); self.send_response(res, body) @@ -298,7 +299,7 @@ where } /// Process one incoming requests - pub(self) fn poll_request(&mut self) -> Result> { + pub(self) fn poll_request(&mut self) -> Result { // limit a mount of non processed requests if self.messages.len() >= MAX_PIPELINED_MESSAGES { return Ok(false); @@ -400,7 +401,7 @@ where } /// keep-alive timer - fn poll_keepalive(&mut self) -> Result<(), DispatchError> { + fn poll_keepalive(&mut self) -> Result<(), DispatchError> { if self.ka_timer.is_none() { return Ok(()); } @@ -469,8 +470,8 @@ where S::Response: Into>, B: MessageBody, { - type Item = H1ServiceResult; - type Error = DispatchError; + type Item = (); + type Error = DispatchError; #[inline] fn poll(&mut self) -> Poll { @@ -490,7 +491,7 @@ where } if inner.flags.contains(Flags::DISCONNECTED) { - return Ok(Async::Ready(H1ServiceResult::Disconnected)); + return Ok(Async::Ready(())); } // keep-alive and stream errors @@ -523,14 +524,12 @@ where }; let mut inner = self.inner.take().unwrap(); - if shutdown { - Ok(Async::Ready(H1ServiceResult::Shutdown( - inner.framed.into_inner(), - ))) - } else { - let req = inner.unhandled.take().unwrap(); - Ok(Async::Ready(H1ServiceResult::Unhandled(req, inner.framed))) - } + + // TODO: shutdown + Ok(Async::Ready(())) + //Ok(Async::Ready(HttpServiceResult::Shutdown( + // inner.framed.into_inner(), + //))) } } diff --git a/src/h1/mod.rs b/src/h1/mod.rs index 9054c2665..e3d63c521 100644 --- a/src/h1/mod.rs +++ b/src/h1/mod.rs @@ -1,7 +1,4 @@ //! HTTP/1 implementation -use std::fmt; - -use actix_codec::Framed; use bytes::Bytes; mod client; @@ -18,29 +15,6 @@ pub use self::dispatcher::Dispatcher; pub use self::payload::{Payload, PayloadBuffer}; pub use self::service::{H1Service, H1ServiceHandler, OneRequest}; -use crate::request::Request; - -/// H1 service response type -pub enum H1ServiceResult { - Disconnected, - Shutdown(T), - Unhandled(Request, Framed), -} - -impl fmt::Debug for H1ServiceResult { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - H1ServiceResult::Disconnected => write!(f, "H1ServiceResult::Disconnected"), - H1ServiceResult::Shutdown(ref v) => { - write!(f, "H1ServiceResult::Shutdown({:?})", v) - } - H1ServiceResult::Unhandled(ref req, _) => { - write!(f, "H1ServiceResult::Unhandled({:?})", req) - } - } - } -} - #[derive(Debug)] /// Codec message pub enum Message { @@ -67,6 +41,7 @@ pub enum MessageType { #[cfg(test)] mod tests { use super::*; + use crate::request::Request; impl Message { pub fn message(self) -> Request { diff --git a/src/h1/service.rs b/src/h1/service.rs index 1c4f1ae3e..229229e69 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -17,7 +17,7 @@ use crate::response::Response; use super::codec::Codec; use super::dispatcher::Dispatcher; -use super::{H1ServiceResult, Message}; +use super::Message; /// `NewService` implementation for HTTP1 transport pub struct H1Service { @@ -72,8 +72,8 @@ where S::Service: 'static, B: MessageBody, { - type Response = H1ServiceResult; - type Error = DispatchError; + type Response = (); + type Error = DispatchError; type InitError = S::InitError; type Service = H1ServiceHandler; type Future = H1ServiceResponse; @@ -275,12 +275,15 @@ where S::Response: Into>, B: MessageBody, { - type Response = H1ServiceResult; - type Error = DispatchError; + type Response = (); + type Error = DispatchError; type Future = Dispatcher; fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.srv.poll_ready().map_err(DispatchError::Service) + self.srv.poll_ready().map_err(|e| { + log::error!("Http service readiness error: {:?}", e); + DispatchError::Service + }) } fn call(&mut self, req: T) -> Self::Future { diff --git a/src/h2/dispatcher.rs b/src/h2/dispatcher.rs index 7f5409c68..be813bd57 100644 --- a/src/h2/dispatcher.rs +++ b/src/h2/dispatcher.rs @@ -26,8 +26,6 @@ use crate::payload::Payload; use crate::request::Request; use crate::response::Response; -use super::H2ServiceResult; - const CHUNK_SIZE: usize = 16_384; bitflags! { @@ -40,7 +38,7 @@ bitflags! { /// Dispatcher for HTTP/2 protocol pub struct Dispatcher< T: AsyncRead + AsyncWrite, - S: Service> + 'static, + S: Service + 'static, B: MessageBody, > { flags: Flags, @@ -55,8 +53,8 @@ pub struct Dispatcher< impl Dispatcher where T: AsyncRead + AsyncWrite, - S: Service> + 'static, - S::Error: Into + fmt::Debug, + S: Service + 'static, + S::Error: fmt::Debug, S::Response: Into>, B: MessageBody + 'static, { @@ -97,13 +95,13 @@ where impl Future for Dispatcher where T: AsyncRead + AsyncWrite, - S: Service> + 'static, - S::Error: Into + fmt::Debug, + S: Service + 'static, + S::Error: fmt::Debug, S::Response: Into>, B: MessageBody + 'static, { type Item = (); - type Error = DispatchError<()>; + type Error = DispatchError; #[inline] fn poll(&mut self) -> Poll { @@ -143,21 +141,21 @@ where } } -struct ServiceResponse>, B> { +struct ServiceResponse, B> { state: ServiceResponseState, config: ServiceConfig, buffer: Option, } -enum ServiceResponseState>, B> { +enum ServiceResponseState, B> { ServiceCall(S::Future, Option>), SendPayload(SendStream, ResponseBody), } impl ServiceResponse where - S: Service> + 'static, - S::Error: Into + fmt::Debug, + S: Service + 'static, + S::Error: fmt::Debug, S::Response: Into>, B: MessageBody + 'static, { @@ -224,8 +222,8 @@ where impl Future for ServiceResponse where - S: Service> + 'static, - S::Error: Into + fmt::Debug, + S: Service + 'static, + S::Error: fmt::Debug, S::Response: Into>, B: MessageBody + 'static, { @@ -258,7 +256,7 @@ where } Ok(Async::NotReady) => Ok(Async::NotReady), Err(e) => { - let res: Response = e.into().into(); + let res: Response = Response::InternalServerError().finish(); let (res, body) = res.replace_body(()); let mut send = send.take().unwrap(); diff --git a/src/h2/mod.rs b/src/h2/mod.rs index 55e057607..c5972123f 100644 --- a/src/h2/mod.rs +++ b/src/h2/mod.rs @@ -9,26 +9,10 @@ use h2::RecvStream; mod dispatcher; mod service; +pub use self::dispatcher::Dispatcher; pub use self::service::H2Service; use crate::error::PayloadError; -/// H1 service response type -pub enum H2ServiceResult { - Disconnected, - Shutdown(T), -} - -impl fmt::Debug for H2ServiceResult { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - H2ServiceResult::Disconnected => write!(f, "H2ServiceResult::Disconnected"), - H2ServiceResult::Shutdown(ref v) => { - write!(f, "H2ServiceResult::Shutdown({:?})", v) - } - } - } -} - /// H2 receive stream pub struct Payload { pl: RecvStream, diff --git a/src/h2/service.rs b/src/h2/service.rs index e225e9fcb..a47f507b5 100644 --- a/src/h2/service.rs +++ b/src/h2/service.rs @@ -20,7 +20,6 @@ use crate::request::Request; use crate::response::Response; use super::dispatcher::Dispatcher; -use super::H2ServiceResult; /// `NewService` implementation for HTTP2 transport pub struct H2Service { @@ -31,14 +30,14 @@ pub struct H2Service { impl H2Service where - S: NewService>, + S: NewService, S::Service: 'static, - S::Error: Into + Debug + 'static, + S::Error: Debug + 'static, S::Response: Into>, B: MessageBody + 'static, { /// Create new `HttpService` instance. - pub fn new>>(service: F) -> Self { + pub fn new>(service: F) -> Self { let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0); H2Service { @@ -57,14 +56,14 @@ where impl NewService for H2Service where T: AsyncRead + AsyncWrite, - S: NewService>, + S: NewService, S::Service: 'static, - S::Error: Into + Debug, + S::Error: Debug, S::Response: Into>, B: MessageBody + 'static, { type Response = (); - type Error = DispatchError<()>; + type Error = DispatchError; type InitError = S::InitError; type Service = H2ServiceHandler; type Future = H2ServiceResponse; @@ -94,9 +93,9 @@ pub struct H2ServiceBuilder { impl H2ServiceBuilder where - S: NewService>, + S: NewService, S::Service: 'static, - S::Error: Into + Debug + 'static, + S::Error: Debug + 'static, { /// Create instance of `H2ServiceBuilder` pub fn new() -> H2ServiceBuilder { @@ -189,30 +188,11 @@ where self } - // #[cfg(feature = "ssl")] - // /// Configure alpn protocols for SslAcceptorBuilder. - // pub fn configure_openssl( - // builder: &mut openssl::ssl::SslAcceptorBuilder, - // ) -> io::Result<()> { - // let protos: &[u8] = b"\x02h2"; - // builder.set_alpn_select_callback(|_, protos| { - // const H2: &[u8] = b"\x02h2"; - // if protos.windows(3).any(|window| window == H2) { - // Ok(b"h2") - // } else { - // Err(openssl::ssl::AlpnError::NOACK) - // } - // }); - // builder.set_alpn_protos(&protos)?; - - // Ok(()) - // } - /// Finish service configuration and create `H1Service` instance. pub fn finish(self, service: F) -> H2Service where B: MessageBody, - F: IntoNewService>, + F: IntoNewService, { let cfg = ServiceConfig::new( self.keep_alive, @@ -228,7 +208,7 @@ where } #[doc(hidden)] -pub struct H2ServiceResponse>, B> { +pub struct H2ServiceResponse, B> { fut: ::Future, cfg: Option, _t: PhantomData<(T, B)>, @@ -237,10 +217,10 @@ pub struct H2ServiceResponse>, B> { impl Future for H2ServiceResponse where T: AsyncRead + AsyncWrite, - S: NewService>, + S: NewService, S::Service: 'static, S::Response: Into>, - S::Error: Into + Debug, + S::Error: Debug, B: MessageBody + 'static, { type Item = H2ServiceHandler; @@ -264,8 +244,8 @@ pub struct H2ServiceHandler { impl H2ServiceHandler where - S: Service> + 'static, - S::Error: Into + Debug, + S: Service + 'static, + S::Error: Debug, S::Response: Into>, B: MessageBody + 'static, { @@ -281,19 +261,19 @@ where impl Service for H2ServiceHandler where T: AsyncRead + AsyncWrite, - S: Service> + 'static, - S::Error: Into + Debug, + S: Service + 'static, + S::Error: Debug, S::Response: Into>, B: MessageBody + 'static, { type Response = (); - type Error = DispatchError<()>; + type Error = DispatchError; type Future = H2ServiceHandlerResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { self.srv.poll_ready().map_err(|e| { error!("Service readiness error: {:?}", e); - DispatchError::Service(()) + DispatchError::Service }) } @@ -308,11 +288,7 @@ where } } -enum State< - T: AsyncRead + AsyncWrite, - S: Service> + 'static, - B: MessageBody, -> { +enum State + 'static, B: MessageBody> { Incoming(Dispatcher), Handshake( Option>, @@ -324,8 +300,8 @@ enum State< pub struct H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite, - S: Service> + 'static, - S::Error: Into + Debug, + S: Service + 'static, + S::Error: Debug, S::Response: Into>, B: MessageBody + 'static, { @@ -335,13 +311,13 @@ where impl Future for H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite, - S: Service> + 'static, - S::Error: Into + Debug, + S: Service + 'static, + S::Error: Debug, S::Response: Into>, B: MessageBody, { type Item = (); - type Error = DispatchError<()>; + type Error = DispatchError; fn poll(&mut self) -> Poll { match self.state { diff --git a/src/lib.rs b/src/lib.rs index 8750b24ca..74a46fd17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,6 +97,7 @@ pub use self::message::{Head, Message, RequestHead, ResponseHead}; pub use self::payload::{Payload, PayloadStream}; pub use self::request::Request; pub use self::response::Response; +pub use self::service::HttpService; pub use self::service::{SendError, SendResponse}; pub mod dev { diff --git a/src/service/mod.rs b/src/service/mod.rs index 83a40bd12..25e95bf60 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -1,3 +1,5 @@ mod senderror; +mod service; pub use self::senderror::{SendError, SendResponse}; +pub use self::service::HttpService; diff --git a/src/service/service.rs b/src/service/service.rs new file mode 100644 index 000000000..5f6ee8190 --- /dev/null +++ b/src/service/service.rs @@ -0,0 +1,446 @@ +use std::fmt::Debug; +use std::marker::PhantomData; +use std::{fmt, io, net}; + +use actix_codec::{AsyncRead, AsyncWrite, Framed, FramedParts}; +use actix_service::{IntoNewService, NewService, Service}; +use actix_utils::cloneable::CloneableService; +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use futures::{try_ready, Async, Future, IntoFuture, Poll}; +use h2::server::{self, Handshake}; +use log::error; + +use crate::body::MessageBody; +use crate::config::{KeepAlive, ServiceConfig}; +use crate::error::DispatchError; +use crate::request::Request; +use crate::response::Response; + +use crate::{h1, h2::Dispatcher}; + +/// `NewService` HTTP1.1/HTTP2 transport implementation +pub struct HttpService { + srv: S, + cfg: ServiceConfig, + _t: PhantomData<(T, B)>, +} + +impl HttpService +where + S: NewService, + S::Service: 'static, + S::Error: Debug + 'static, + S::Response: Into>, + B: MessageBody + 'static, +{ + /// Create new `HttpService` instance. + pub fn new>(service: F) -> Self { + let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0); + + HttpService { + cfg, + srv: service.into_new_service(), + _t: PhantomData, + } + } + + /// Create builder for `HttpService` instance. + pub fn build() -> HttpServiceBuilder { + HttpServiceBuilder::new() + } +} + +impl NewService for HttpService +where + T: AsyncRead + AsyncWrite + 'static, + S: NewService, + S::Service: 'static, + S::Error: Debug, + S::Response: Into>, + B: MessageBody + 'static, +{ + type Response = (); + type Error = DispatchError; + type InitError = S::InitError; + type Service = HttpServiceHandler; + type Future = HttpServiceResponse; + + fn new_service(&self, _: &()) -> Self::Future { + HttpServiceResponse { + fut: self.srv.new_service(&()).into_future(), + cfg: Some(self.cfg.clone()), + _t: PhantomData, + } + } +} + +/// A http service factory builder +/// +/// This type can be used to construct an instance of `ServiceConfig` through a +/// builder-like pattern. +pub struct HttpServiceBuilder { + keep_alive: KeepAlive, + client_timeout: u64, + client_disconnect: u64, + host: String, + addr: net::SocketAddr, + secure: bool, + _t: PhantomData<(T, S)>, +} + +impl HttpServiceBuilder +where + S: NewService, + S::Service: 'static, + S::Error: Debug + 'static, +{ + /// Create instance of `HttpServiceBuilder` type + pub fn new() -> HttpServiceBuilder { + HttpServiceBuilder { + keep_alive: KeepAlive::Timeout(5), + client_timeout: 5000, + client_disconnect: 0, + secure: false, + host: "localhost".to_owned(), + addr: "127.0.0.1:8080".parse().unwrap(), + _t: PhantomData, + } + } + + /// Enable secure flag for current server. + /// This flags also enables `client disconnect timeout`. + /// + /// By default this flag is set to false. + pub fn secure(mut self) -> Self { + self.secure = true; + if self.client_disconnect == 0 { + self.client_disconnect = 3000; + } + self + } + + /// Set server keep-alive setting. + /// + /// By default keep alive is set to a 5 seconds. + pub fn keep_alive>(mut self, val: U) -> Self { + self.keep_alive = val.into(); + self + } + + /// Set server client timeout in milliseconds for first request. + /// + /// Defines a timeout for reading client request header. If a client does not transmit + /// the entire set headers within this time, the request is terminated with + /// the 408 (Request Time-out) error. + /// + /// To disable timeout set value to 0. + /// + /// By default client timeout is set to 5000 milliseconds. + pub fn client_timeout(mut self, val: u64) -> Self { + self.client_timeout = val; + self + } + + /// Set server connection disconnect timeout in milliseconds. + /// + /// Defines a timeout for disconnect connection. If a disconnect procedure does not complete + /// within this time, the request get dropped. This timeout affects secure connections. + /// + /// To disable timeout set value to 0. + /// + /// By default disconnect timeout is set to 3000 milliseconds. + pub fn client_disconnect(mut self, val: u64) -> Self { + self.client_disconnect = val; + self + } + + /// Set server host name. + /// + /// Host name is used by application router aa a hostname for url + /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. + /// html#method.host) documentation for more information. + /// + /// By default host name is set to a "localhost" value. + pub fn server_hostname(mut self, val: &str) -> Self { + self.host = val.to_owned(); + self + } + + /// Set server ip address. + /// + /// Host name is used by application router aa a hostname for url + /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. + /// html#method.host) documentation for more information. + /// + /// By default server address is set to a "127.0.0.1:8080" + pub fn server_address(mut self, addr: U) -> Self { + match addr.to_socket_addrs() { + Err(err) => error!("Can not convert to SocketAddr: {}", err), + Ok(mut addrs) => { + if let Some(addr) = addrs.next() { + self.addr = addr; + } + } + } + self + } + + // #[cfg(feature = "ssl")] + // /// Configure alpn protocols for SslAcceptorBuilder. + // pub fn configure_openssl( + // builder: &mut openssl::ssl::SslAcceptorBuilder, + // ) -> io::Result<()> { + // let protos: &[u8] = b"\x02h2"; + // builder.set_alpn_select_callback(|_, protos| { + // const H2: &[u8] = b"\x02h2"; + // if protos.windows(3).any(|window| window == H2) { + // Ok(b"h2") + // } else { + // Err(openssl::ssl::AlpnError::NOACK) + // } + // }); + // builder.set_alpn_protos(&protos)?; + + // Ok(()) + // } + + /// Finish service configuration and create `HttpService` instance. + pub fn finish(self, service: F) -> HttpService + where + B: MessageBody, + F: IntoNewService, + { + let cfg = ServiceConfig::new( + self.keep_alive, + self.client_timeout, + self.client_disconnect, + ); + HttpService { + cfg, + srv: service.into_new_service(), + _t: PhantomData, + } + } +} + +#[doc(hidden)] +pub struct HttpServiceResponse, B> { + fut: ::Future, + cfg: Option, + _t: PhantomData<(T, B)>, +} + +impl Future for HttpServiceResponse +where + T: AsyncRead + AsyncWrite, + S: NewService, + S::Service: 'static, + S::Response: Into>, + S::Error: Debug, + B: MessageBody + 'static, +{ + type Item = HttpServiceHandler; + type Error = S::InitError; + + fn poll(&mut self) -> Poll { + let service = try_ready!(self.fut.poll()); + Ok(Async::Ready(HttpServiceHandler::new( + self.cfg.take().unwrap(), + service, + ))) + } +} + +/// `Service` implementation for http transport +pub struct HttpServiceHandler { + srv: CloneableService, + cfg: ServiceConfig, + _t: PhantomData<(T, B)>, +} + +impl HttpServiceHandler +where + S: Service + 'static, + S::Error: Debug, + S::Response: Into>, + B: MessageBody + 'static, +{ + fn new(cfg: ServiceConfig, srv: S) -> HttpServiceHandler { + HttpServiceHandler { + cfg, + srv: CloneableService::new(srv), + _t: PhantomData, + } + } +} + +impl Service for HttpServiceHandler +where + T: AsyncRead + AsyncWrite + 'static, + S: Service + 'static, + S::Error: Debug, + S::Response: Into>, + B: MessageBody + 'static, +{ + type Response = (); + type Error = DispatchError; + type Future = HttpServiceHandlerResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.srv.poll_ready().map_err(|e| { + error!("Service readiness error: {:?}", e); + DispatchError::Service + }) + } + + fn call(&mut self, req: T) -> Self::Future { + HttpServiceHandlerResponse { + state: State::Unknown(Some(( + req, + BytesMut::with_capacity(14), + self.cfg.clone(), + self.srv.clone(), + ))), + } + } +} + +enum State + 'static, B: MessageBody> +where + S::Error: fmt::Debug, + T: AsyncRead + AsyncWrite + 'static, +{ + H1(h1::Dispatcher), + H2(Dispatcher, S, B>), + Unknown(Option<(T, BytesMut, ServiceConfig, CloneableService)>), + Handshake(Option<(Handshake, Bytes>, ServiceConfig, CloneableService)>), +} + +pub struct HttpServiceHandlerResponse +where + T: AsyncRead + AsyncWrite + 'static, + S: Service + 'static, + S::Error: Debug, + S::Response: Into>, + B: MessageBody + 'static, +{ + state: State, +} + +const HTTP2_PREFACE: [u8; 14] = *b"PRI * HTTP/2.0"; + +impl Future for HttpServiceHandlerResponse +where + T: AsyncRead + AsyncWrite, + S: Service + 'static, + S::Error: Debug, + S::Response: Into>, + B: MessageBody, +{ + type Item = (); + type Error = DispatchError; + + fn poll(&mut self) -> Poll { + match self.state { + State::H1(ref mut disp) => disp.poll(), + State::H2(ref mut disp) => disp.poll(), + State::Unknown(ref mut data) => { + if let Some(ref mut item) = data { + loop { + unsafe { + let b = item.1.bytes_mut(); + let n = { try_ready!(item.0.poll_read(b)) }; + item.1.advance_mut(n); + if item.1.len() >= HTTP2_PREFACE.len() { + break; + } + } + } + } else { + panic!() + } + let (io, buf, cfg, srv) = data.take().unwrap(); + if buf[..14] == HTTP2_PREFACE[..] { + let io = Io { + inner: io, + unread: Some(buf), + }; + self.state = + State::Handshake(Some((server::handshake(io), cfg, srv))); + } else { + let framed = Framed::from_parts(FramedParts::with_read_buf( + io, + h1::Codec::new(cfg.clone()), + buf, + )); + self.state = + State::H1(h1::Dispatcher::with_timeout(framed, cfg, None, srv)) + } + self.poll() + } + State::Handshake(ref mut data) => { + let conn = if let Some(ref mut item) = data { + match item.0.poll() { + Ok(Async::Ready(conn)) => conn, + Ok(Async::NotReady) => return Ok(Async::NotReady), + Err(err) => { + trace!("H2 handshake error: {}", err); + return Err(err.into()); + } + } + } else { + panic!() + }; + let (_, cfg, srv) = data.take().unwrap(); + self.state = State::H2(Dispatcher::new(srv, conn, cfg, None)); + self.poll() + } + } + } +} + +/// Wrapper for `AsyncRead + AsyncWrite` types +struct Io { + unread: Option, + inner: T, +} + +impl io::Read for Io { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if let Some(mut bytes) = self.unread.take() { + let size = std::cmp::min(buf.len(), bytes.len()); + buf[..size].copy_from_slice(&bytes[..size]); + if bytes.len() > size { + bytes.split_to(size); + self.unread = Some(bytes); + } + Ok(size) + } else { + self.inner.read(buf) + } + } +} + +impl io::Write for Io { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.inner.write(buf) + } + fn flush(&mut self) -> io::Result<()> { + self.inner.flush() + } +} + +impl AsyncRead for Io { + unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { + self.inner.prepare_uninitialized_buffer(buf) + } +} + +impl AsyncWrite for Io { + fn shutdown(&mut self) -> Poll<(), io::Error> { + self.inner.shutdown() + } + fn write_buf(&mut self, buf: &mut B) -> Poll { + self.inner.write_buf(buf) + } +} diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 3afee682f..695a477d2 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -73,7 +73,7 @@ impl TestServer { .start(); tx.send((System::current(), local_addr)).unwrap(); - sys.run(); + sys.run() }); let (system, addr) = rx.recv().unwrap(); diff --git a/tests/test_server.rs b/tests/test_server.rs index fd848b82b..4cffdd096 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -10,8 +10,8 @@ use futures::stream::once; use actix_http::body::Body; use actix_http::{ - body, client, h1, h2, http, Error, HttpMessage as HttpMessage2, KeepAlive, Request, - Response, + body, client, h1, h2, http, Error, HttpMessage as HttpMessage2, HttpService, + KeepAlive, Request, Response, }; #[test] @@ -31,6 +31,26 @@ fn test_h1() { assert!(response.status().is_success()); } +#[test] +fn test_h1_2() { + let mut srv = TestServer::new(|| { + HttpService::build() + .keep_alive(KeepAlive::Disabled) + .client_timeout(1000) + .client_disconnect(1000) + .server_hostname("localhost") + .finish(|req: Request| { + assert_eq!(req.version(), http::Version::HTTP_11); + future::ok::<_, ()>(Response::Ok().finish()) + }) + .map(|_| ()) + }); + + let req = client::ClientRequest::get(srv.url("/")).finish().unwrap(); + let response = srv.send_request(req).unwrap(); + assert!(response.status().is_success()); +} + #[cfg(feature = "ssl")] fn ssl_acceptor() -> std::io::Result> { use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; @@ -71,7 +91,30 @@ fn test_h2() -> std::io::Result<()> { let req = client::ClientRequest::get(srv.surl("/")).finish().unwrap(); let response = srv.send_request(req).unwrap(); - println!("RES: {:?}", response); + assert!(response.status().is_success()); + Ok(()) +} + +#[cfg(feature = "ssl")] +#[test] +fn test_h2_1() -> std::io::Result<()> { + let openssl = ssl_acceptor()?; + let mut srv = TestServer::new(move || { + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)) + .and_then( + HttpService::build() + .finish(|req: Request| { + assert_eq!(req.version(), http::Version::HTTP_2); + future::ok::<_, Error>(Response::Ok().finish()) + }) + .map_err(|_| ()), + ) + }); + + let req = client::ClientRequest::get(srv.surl("/")).finish().unwrap(); + let response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); Ok(()) } @@ -79,9 +122,6 @@ fn test_h2() -> std::io::Result<()> { #[cfg(feature = "ssl")] #[test] fn test_h2_body() -> std::io::Result<()> { - // std::env::set_var("RUST_LOG", "actix_http=trace"); - // env_logger::init(); - let data = "HELLOWORLD".to_owned().repeat(64 * 1024); let openssl = ssl_acceptor()?; let mut srv = TestServer::new(move || { From 6d639ae3df10be9e10449774ab96c46c7ca11483 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 22:59:56 -0800 Subject: [PATCH 222/427] allow to create http services with config --- src/h2/service.rs | 12 ++++++++++++ src/service/service.rs | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/h2/service.rs b/src/h2/service.rs index a47f507b5..530629947 100644 --- a/src/h2/service.rs +++ b/src/h2/service.rs @@ -47,6 +47,18 @@ where } } + /// Create new `HttpService` instance with config. + pub fn with_config>( + cfg: ServiceConfig, + service: F, + ) -> Self { + H2Service { + cfg, + srv: service.into_new_service(), + _t: PhantomData, + } + } + /// Create builder for `HttpService` instance. pub fn build() -> H2ServiceBuilder { H2ServiceBuilder::new() diff --git a/src/service/service.rs b/src/service/service.rs index 5f6ee8190..3d0009ed0 100644 --- a/src/service/service.rs +++ b/src/service/service.rs @@ -44,6 +44,18 @@ where } } + /// Create new `HttpService` instance with config. + pub fn with_config>( + cfg: ServiceConfig, + service: F, + ) -> Self { + HttpService { + cfg, + srv: service.into_new_service(), + _t: PhantomData, + } + } + /// Create builder for `HttpService` instance. pub fn build() -> HttpServiceBuilder { HttpServiceBuilder::new() From 6e638129c54a558cde24c34974de8443206f7517 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 23:06:14 -0800 Subject: [PATCH 223/427] use generic HttpService --- src/server.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/server.rs b/src/server.rs index c28cb280c..d6d88d000 100644 --- a/src/server.rs +++ b/src/server.rs @@ -2,7 +2,9 @@ use std::marker::PhantomData; use std::sync::Arc; use std::{fmt, io, net}; -use actix_http::{body::MessageBody, h1, KeepAlive, Request, Response, ServiceConfig}; +use actix_http::{ + body::MessageBody, HttpService, KeepAlive, Request, Response, ServiceConfig, +}; use actix_rt::System; use actix_server::{Server, ServerBuilder}; use actix_service::{IntoNewService, NewService}; @@ -72,10 +74,10 @@ where F: Fn() -> I + Send + Clone + 'static, I: IntoNewService, S: NewService, - S::Error: fmt::Debug, + S::Error: fmt::Debug + 'static, S::Response: Into>, S::Service: 'static, - B: MessageBody, + B: MessageBody + 'static, { /// Create new http server with application factory pub fn new(factory: F) -> Self { @@ -244,7 +246,7 @@ where let c = cfg.lock(); let service_config = ServiceConfig::new(c.keep_alive, c.client_timeout, 0); - h1::H1Service::with_config(service_config, factory()) + HttpService::with_config(service_config, factory()) }, )); @@ -298,7 +300,7 @@ where let service_config = ServiceConfig::new(c.keep_alive, c.client_timeout, c.client_timeout); acceptor.clone().map_err(|e| SslError::Ssl(e)).and_then( - h1::H1Service::with_config(service_config, factory()) + HttpService::with_config(service_config, factory()) .map_err(|e| SslError::Service(e)) .map_init_err(|_| ()), ) From e56691bcf23b4a9b37dc42823562fef9e8f4c514 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 23:39:08 -0800 Subject: [PATCH 224/427] rename to Files --- Cargo.toml | 2 +- {actix-staticfiles => actix-files}/CHANGES.md | 0 {actix-staticfiles => actix-files}/Cargo.toml | 2 +- {actix-staticfiles => actix-files}/README.md | 0 .../src/config.rs | 0 .../src/error.rs | 6 +- {actix-staticfiles => actix-files}/src/lib.rs | 64 +++++++++--------- .../src/named.rs | 0 .../src/range.rs | 0 .../tests/test space.binary | 0 .../tests/test.binary | 0 .../tests/test.png | Bin 12 files changed, 37 insertions(+), 37 deletions(-) rename {actix-staticfiles => actix-files}/CHANGES.md (100%) rename {actix-staticfiles => actix-files}/Cargo.toml (97%) rename {actix-staticfiles => actix-files}/README.md (100%) rename {actix-staticfiles => actix-files}/src/config.rs (100%) rename {actix-staticfiles => actix-files}/src/error.rs (91%) rename {actix-staticfiles => actix-files}/src/lib.rs (94%) rename {actix-staticfiles => actix-files}/src/named.rs (100%) rename {actix-staticfiles => actix-files}/src/range.rs (100%) rename {actix-staticfiles => actix-files}/tests/test space.binary (100%) rename {actix-staticfiles => actix-files}/tests/test.binary (100%) rename {actix-staticfiles => actix-files}/tests/test.png (100%) diff --git a/Cargo.toml b/Cargo.toml index 2abb4c72e..7b2e6c99a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ path = "src/lib.rs" [workspace] members = [ ".", + "actix-files", "actix-session", - "actix-staticfiles", ] [package.metadata.docs.rs] diff --git a/actix-staticfiles/CHANGES.md b/actix-files/CHANGES.md similarity index 100% rename from actix-staticfiles/CHANGES.md rename to actix-files/CHANGES.md diff --git a/actix-staticfiles/Cargo.toml b/actix-files/Cargo.toml similarity index 97% rename from actix-staticfiles/Cargo.toml rename to actix-files/Cargo.toml index 0a5517920..7082d1678 100644 --- a/actix-staticfiles/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "actix-staticfiles" +name = "actix-files" version = "0.1.0" authors = ["Nikolay Kim "] description = "Static files support for Actix web." diff --git a/actix-staticfiles/README.md b/actix-files/README.md similarity index 100% rename from actix-staticfiles/README.md rename to actix-files/README.md diff --git a/actix-staticfiles/src/config.rs b/actix-files/src/config.rs similarity index 100% rename from actix-staticfiles/src/config.rs rename to actix-files/src/config.rs diff --git a/actix-staticfiles/src/error.rs b/actix-files/src/error.rs similarity index 91% rename from actix-staticfiles/src/error.rs rename to actix-files/src/error.rs index f165a618a..ca99fa813 100644 --- a/actix-staticfiles/src/error.rs +++ b/actix-files/src/error.rs @@ -3,7 +3,7 @@ use derive_more::Display; /// Errors which can occur when serving static files. #[derive(Display, Debug, PartialEq)] -pub enum StaticFilesError { +pub enum FilesError { /// Path is not a directory #[display(fmt = "Path is not a directory. Unable to serve static files")] IsNotDirectory, @@ -13,8 +13,8 @@ pub enum StaticFilesError { IsDirectory, } -/// Return `NotFound` for `StaticFilesError` -impl ResponseError for StaticFilesError { +/// Return `NotFound` for `FilesError` +impl ResponseError for FilesError { fn error_response(&self) -> HttpResponse { HttpResponse::new(StatusCode::NOT_FOUND) } diff --git a/actix-staticfiles/src/lib.rs b/actix-files/src/lib.rs similarity index 94% rename from actix-staticfiles/src/lib.rs rename to actix-files/src/lib.rs index 7c3f68493..c6b52f049 100644 --- a/actix-staticfiles/src/lib.rs +++ b/actix-files/src/lib.rs @@ -29,7 +29,7 @@ mod error; mod named; mod range; -use self::error::{StaticFilesError, UriSegmentError}; +use self::error::{FilesError, UriSegmentError}; pub use crate::config::{DefaultConfig, StaticFileConfig}; pub use crate::named::NamedFile; pub use crate::range::HttpRange; @@ -222,10 +222,10 @@ fn directory_listing( /// /// fn main() { /// let app = App::new() -/// .service(fs::StaticFiles::new("/static", ".")); +/// .service(fs::Files::new("/static", ".")); /// } /// ``` -pub struct StaticFiles { +pub struct Files { path: String, directory: PathBuf, index: Option, @@ -237,28 +237,28 @@ pub struct StaticFiles { _cd_map: PhantomData, } -impl StaticFiles { - /// Create new `StaticFiles` instance for specified base directory. +impl Files { + /// Create new `Files` instance for specified base directory. /// /// `StaticFile` uses `ThreadPool` for blocking filesystem operations. /// By default pool with 5x threads of available cpus is used. /// Pool size can be changed by setting ACTIX_CPU_POOL environment variable. - pub fn new>(path: &str, dir: T) -> StaticFiles { + pub fn new>(path: &str, dir: T) -> Files { Self::with_config(path, dir, DefaultConfig) } } -impl StaticFiles { - /// Create new `StaticFiles` instance for specified base directory. +impl Files { + /// Create new `Files` instance for specified base directory. /// /// Identical with `new` but allows to specify configiration to use. - pub fn with_config>(path: &str, dir: T, _: C) -> StaticFiles { + pub fn with_config>(path: &str, dir: T, _: C) -> Files { let dir = dir.into().canonicalize().unwrap_or_else(|_| PathBuf::new()); if !dir.is_dir() { log::error!("Specified path is not a directory"); } - StaticFiles { + Files { path: path.to_string(), directory: dir, index: None, @@ -294,13 +294,13 @@ impl StaticFiles { /// /// Shows specific index file for directory "/" instead of /// showing files listing. - pub fn index_file>(mut self, index: T) -> StaticFiles { + pub fn index_file>(mut self, index: T) -> Files { self.index = Some(index.into()); self } } -impl HttpServiceFactory

    for StaticFiles +impl HttpServiceFactory

    for Files where P: 'static, C: StaticFileConfig + 'static, @@ -319,16 +319,16 @@ where } impl NewService> - for StaticFiles + for Files { type Response = ServiceResponse; type Error = (); - type Service = StaticFilesService; + type Service = FilesService; type InitError = (); type Future = FutureResult; fn new_service(&self, _: &()) -> Self::Future { - ok(StaticFilesService { + ok(FilesService { directory: self.directory.clone(), index: self.index.clone(), show_index: self.show_index, @@ -341,7 +341,7 @@ impl NewService> } } -pub struct StaticFilesService { +pub struct FilesService { directory: PathBuf, index: Option, show_index: bool, @@ -352,7 +352,7 @@ pub struct StaticFilesService { _cd_map: PhantomData, } -impl Service> for StaticFilesService { +impl Service> for FilesService { type Response = ServiceResponse; type Error = (); type Future = FutureResult; @@ -395,7 +395,7 @@ impl Service> for StaticFilesService

    = StaticFiles::new("/", "missing"); - let _st: StaticFiles<()> = StaticFiles::new("/", "Cargo.toml"); + let _st: Files<()> = Files::new("/", "missing"); + let _st: Files<()> = Files::new("/", "Cargo.toml"); } // #[test] // fn test_default_handler_file_missing() { - // let st = StaticFiles::new(".") + // let st = Files::new(".") // .default_handler(|_: &_| "default content"); // let req = TestRequest::with_uri("/missing") // .param("tail", "missing") @@ -982,7 +982,7 @@ mod tests { // #[test] // fn test_serve_index() { - // let st = StaticFiles::new(".").index_file("test.binary"); + // let st = Files::new(".").index_file("test.binary"); // let req = TestRequest::default().uri("/tests").finish(); // let resp = st.handle(&req).respond_to(&req).unwrap(); @@ -1028,7 +1028,7 @@ mod tests { // #[test] // fn test_serve_index_nested() { - // let st = StaticFiles::new(".").index_file("mod.rs"); + // let st = Files::new(".").index_file("mod.rs"); // let req = TestRequest::default().uri("/src/client").finish(); // let resp = st.handle(&req).respond_to(&req).unwrap(); // let resp = resp.as_msg(); @@ -1048,7 +1048,7 @@ mod tests { // let mut srv = test::TestServer::with_factory(|| { // App::new().handler( // "test", - // StaticFiles::new(".").index_file("Cargo.toml"), + // Files::new(".").index_file("Cargo.toml"), // ) // }); @@ -1081,7 +1081,7 @@ mod tests { // let mut srv = test::TestServer::with_factory(|| { // App::new().handler( // "test", - // StaticFiles::new(".").index_file("Cargo.toml"), + // Files::new(".").index_file("Cargo.toml"), // ) // }); diff --git a/actix-staticfiles/src/named.rs b/actix-files/src/named.rs similarity index 100% rename from actix-staticfiles/src/named.rs rename to actix-files/src/named.rs diff --git a/actix-staticfiles/src/range.rs b/actix-files/src/range.rs similarity index 100% rename from actix-staticfiles/src/range.rs rename to actix-files/src/range.rs diff --git a/actix-staticfiles/tests/test space.binary b/actix-files/tests/test space.binary similarity index 100% rename from actix-staticfiles/tests/test space.binary rename to actix-files/tests/test space.binary diff --git a/actix-staticfiles/tests/test.binary b/actix-files/tests/test.binary similarity index 100% rename from actix-staticfiles/tests/test.binary rename to actix-files/tests/test.binary diff --git a/actix-staticfiles/tests/test.png b/actix-files/tests/test.png similarity index 100% rename from actix-staticfiles/tests/test.png rename to actix-files/tests/test.png From 1151b5bf7c39f6beec1fdce83701df0c4c4b018e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 23:43:03 -0800 Subject: [PATCH 225/427] fix crate name --- actix-files/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 7082d1678..bd61c880d 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -14,7 +14,7 @@ edition = "2018" workspace = ".." [lib] -name = "actix_staticfiles" +name = "actix_files" path = "src/lib.rs" [dependencies] From 22708e78a9043c16038d24d5edb7941b4241891e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 11:09:42 -0800 Subject: [PATCH 226/427] added proc-macros for route registration --- Cargo.toml | 2 + actix-files/src/lib.rs | 10 +-- actix-web-codegen/Cargo.toml | 15 +++++ actix-web-codegen/src/lib.rs | 115 ++++++++++++++++++++++++++++++++ actix-web-codegen/src/server.rs | 31 +++++++++ examples/basic.rs | 13 ++-- src/handler.rs | 48 ++++++------- src/lib.rs | 18 +++++ src/route.rs | 11 +-- 9 files changed, 221 insertions(+), 42 deletions(-) create mode 100644 actix-web-codegen/Cargo.toml create mode 100644 actix-web-codegen/src/lib.rs create mode 100644 actix-web-codegen/src/server.rs diff --git a/Cargo.toml b/Cargo.toml index 7b2e6c99a..f9e2266ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ ".", "actix-files", "actix-session", + "actix-web-codegen", ] [package.metadata.docs.rs] @@ -63,6 +64,7 @@ actix-codec = "0.1.0" #actix-service = "0.3.2" #actix-utils = "0.3.1" actix-rt = "0.2.0" +actix-web-codegen = { path="actix-web-codegen" } actix-service = { git = "https://github.com/actix/actix-net.git" } actix-utils = { git = "https://github.com/actix/actix-net.git" } diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index c6b52f049..c08cae9c2 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -318,9 +318,7 @@ where } } -impl NewService> - for Files -{ +impl NewService> for Files { type Response = ServiceResponse; type Error = (); type Service = FilesService; @@ -730,8 +728,7 @@ mod tests { #[test] fn test_named_file_content_range_headers() { let mut srv = test::init_service( - App::new() - .service(Files::new("/test", ".").index_file("tests/test.binary")), + App::new().service(Files::new("/test", ".").index_file("tests/test.binary")), ); // Valid range header @@ -770,8 +767,7 @@ mod tests { #[test] fn test_named_file_content_length_headers() { let mut srv = test::init_service( - App::new() - .service(Files::new("test", ".").index_file("tests/test.binary")), + App::new().service(Files::new("test", ".").index_file("tests/test.binary")), ); // Valid range header diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml new file mode 100644 index 000000000..24ed36b77 --- /dev/null +++ b/actix-web-codegen/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "actix-web-codegen" +description = "Actix web codegen macros" +version = "0.1.0" +authors = ["Nikolay Kim "] +license = "MIT/Apache-2.0" +edition = "2018" +workspace = ".." + +[lib] +proc-macro = true + +[dependencies] +quote = "0.6" +syn = { version = "0.15", features = ["full", "parsing"] } diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs new file mode 100644 index 000000000..1052c82a7 --- /dev/null +++ b/actix-web-codegen/src/lib.rs @@ -0,0 +1,115 @@ +#![recursion_limit = "512"] + +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::parse_macro_input; + +/// #[get("path")] attribute +#[proc_macro_attribute] +pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as syn::AttributeArgs); + if args.is_empty() { + panic!("invalid server definition, expected: #[get(\"some path\")]"); + } + + // path + let path = match args[0] { + syn::NestedMeta::Literal(syn::Lit::Str(ref fname)) => { + let fname = quote!(#fname).to_string(); + fname.as_str()[1..fname.len() - 1].to_owned() + } + _ => panic!("resource path"), + }; + + let ast: syn::ItemFn = syn::parse(input).unwrap(); + let name = ast.ident.clone(); + + (quote! { + #[allow(non_camel_case_types)] + struct #name; + + impl actix_web::dev::HttpServiceFactory

    for #name { + fn register(self, config: &mut actix_web::dev::AppConfig

    ) { + #ast + actix_web::dev::HttpServiceFactory::register( + actix_web::Resource::new(#path) + .route(actix_web::web::get().to(#name)), config); + } + } + }) + .into() +} + +/// #[post("path")] attribute +#[proc_macro_attribute] +pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as syn::AttributeArgs); + if args.is_empty() { + panic!("invalid server definition, expected: #[get(\"some path\")]"); + } + + // path + let path = match args[0] { + syn::NestedMeta::Literal(syn::Lit::Str(ref fname)) => { + let fname = quote!(#fname).to_string(); + fname.as_str()[1..fname.len() - 1].to_owned() + } + _ => panic!("resource path"), + }; + + let ast: syn::ItemFn = syn::parse(input).unwrap(); + let name = ast.ident.clone(); + + (quote! { + #[allow(non_camel_case_types)] + struct #name; + + impl actix_web::dev::HttpServiceFactory

    for #name { + fn register(self, config: &mut actix_web::dev::AppConfig

    ) { + #ast + actix_web::dev::HttpServiceFactory::register( + actix_web::Resource::new(#path) + .route(actix_web::web::post().to(#name)), config); + } + } + }) + .into() +} + +/// #[put("path")] attribute +#[proc_macro_attribute] +pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as syn::AttributeArgs); + if args.is_empty() { + panic!("invalid server definition, expected: #[get(\"some path\")]"); + } + + // path + let path = match args[0] { + syn::NestedMeta::Literal(syn::Lit::Str(ref fname)) => { + let fname = quote!(#fname).to_string(); + fname.as_str()[1..fname.len() - 1].to_owned() + } + _ => panic!("resource path"), + }; + + let ast: syn::ItemFn = syn::parse(input).unwrap(); + let name = ast.ident.clone(); + + (quote! { + #[allow(non_camel_case_types)] + struct #name; + + impl actix_web::dev::HttpServiceFactory

    for #name { + fn register(self, config: &mut actix_web::dev::AppConfig

    ) { + #ast + actix_web::dev::HttpServiceFactory::register( + actix_web::Resource::new(#path) + .route(actix_web::web::put().to(#name)), config); + } + } + }) + .into() +} diff --git a/actix-web-codegen/src/server.rs b/actix-web-codegen/src/server.rs new file mode 100644 index 000000000..43e663f3a --- /dev/null +++ b/actix-web-codegen/src/server.rs @@ -0,0 +1,31 @@ +use std::collections::HashSet; +use std::env; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn; + +/// Thrift mux server impl +pub struct Server {} + +impl Server { + fn new() -> Server { + Server {} + } + + /// generate servers + pub fn generate(input: TokenStream) { + let mut srv = Server::new(); + let ast: syn::ItemFn = syn::parse2(input).unwrap(); + println!("T: {:?}", ast.ident); + + // quote! { + // #ast + + // #(#servers)* + // } + } +} diff --git a/examples/basic.rs b/examples/basic.rs index 5fd862d49..39633f523 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,9 +1,9 @@ use futures::IntoFuture; -use actix_web::{ - http::Method, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer, -}; +use actix_web::macros::get; +use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; +#[get("/resource1/index.html")] fn index(req: HttpRequest) -> &'static str { println!("REQ: {:?}", req); "Hello world!\r\n" @@ -14,6 +14,7 @@ fn index_async(req: HttpRequest) -> impl IntoFuture &'static str { "Hello world!\r\n" } @@ -27,7 +28,8 @@ fn main() -> std::io::Result<()> { App::new() .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) .middleware(middleware::Compress::default()) - .service(web::resource("/resource1/index.html").route(web::get().to(index))) + .service(index) + .service(no_params) .service( web::resource("/resource2/index.html") .middleware( @@ -36,10 +38,9 @@ fn main() -> std::io::Result<()> { .default_resource(|r| { r.route(web::route().to(|| HttpResponse::MethodNotAllowed())) }) - .route(web::method(Method::GET).to_async(index_async)), + .route(web::get().to_async(index_async)), ) .service(web::resource("/test1.html").to(|| "Test\r\n")) - .service(web::resource("/").to(no_params)) }) .bind("127.0.0.1:8080")? .workers(1) diff --git a/src/handler.rs b/src/handler.rs index 442dc60d8..435d9a8bb 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -31,7 +31,7 @@ where } #[doc(hidden)] -pub struct Handle +pub struct Handler where F: Factory, R: Responder, @@ -40,19 +40,19 @@ where _t: PhantomData<(T, R)>, } -impl Handle +impl Handler where F: Factory, R: Responder, { pub fn new(hnd: F) -> Self { - Handle { + Handler { hnd, _t: PhantomData, } } } -impl NewService<(T, HttpRequest)> for Handle +impl NewService<(T, HttpRequest)> for Handler where F: Factory, R: Responder + 'static, @@ -60,11 +60,11 @@ where type Response = ServiceResponse; type Error = Void; type InitError = (); - type Service = HandleService; + type Service = HandlerService; type Future = FutureResult; fn new_service(&self, _: &()) -> Self::Future { - ok(HandleService { + ok(HandlerService { hnd: self.hnd.clone(), _t: PhantomData, }) @@ -72,7 +72,7 @@ where } #[doc(hidden)] -pub struct HandleService +pub struct HandlerService where F: Factory, R: Responder + 'static, @@ -81,14 +81,14 @@ where _t: PhantomData<(T, R)>, } -impl Service<(T, HttpRequest)> for HandleService +impl Service<(T, HttpRequest)> for HandlerService where F: Factory, R: Responder + 'static, { type Response = ServiceResponse; type Error = Void; - type Future = HandleServiceResponse<::Future>; + type Future = HandlerServiceResponse<::Future>; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) @@ -96,19 +96,19 @@ where fn call(&mut self, (param, req): (T, HttpRequest)) -> Self::Future { let fut = self.hnd.call(param).respond_to(&req).into_future(); - HandleServiceResponse { + HandlerServiceResponse { fut, req: Some(req), } } } -pub struct HandleServiceResponse { +pub struct HandlerServiceResponse { fut: T, req: Option, } -impl Future for HandleServiceResponse +impl Future for HandlerServiceResponse where T: Future, T::Error: Into, @@ -157,7 +157,7 @@ where } #[doc(hidden)] -pub struct AsyncHandle +pub struct AsyncHandler where F: AsyncFactory, R: IntoFuture, @@ -168,7 +168,7 @@ where _t: PhantomData<(T, R)>, } -impl AsyncHandle +impl AsyncHandler where F: AsyncFactory, R: IntoFuture, @@ -176,13 +176,13 @@ where R::Error: Into, { pub fn new(hnd: F) -> Self { - AsyncHandle { + AsyncHandler { hnd, _t: PhantomData, } } } -impl NewService<(T, HttpRequest)> for AsyncHandle +impl NewService<(T, HttpRequest)> for AsyncHandler where F: AsyncFactory, R: IntoFuture, @@ -192,11 +192,11 @@ where type Response = ServiceResponse; type Error = (); type InitError = (); - type Service = AsyncHandleService; + type Service = AsyncHandlerService; type Future = FutureResult; fn new_service(&self, _: &()) -> Self::Future { - ok(AsyncHandleService { + ok(AsyncHandlerService { hnd: self.hnd.clone(), _t: PhantomData, }) @@ -204,7 +204,7 @@ where } #[doc(hidden)] -pub struct AsyncHandleService +pub struct AsyncHandlerService where F: AsyncFactory, R: IntoFuture, @@ -215,7 +215,7 @@ where _t: PhantomData<(T, R)>, } -impl Service<(T, HttpRequest)> for AsyncHandleService +impl Service<(T, HttpRequest)> for AsyncHandlerService where F: AsyncFactory, R: IntoFuture, @@ -224,14 +224,14 @@ where { type Response = ServiceResponse; type Error = (); - type Future = AsyncHandleServiceResponse; + type Future = AsyncHandlerServiceResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } fn call(&mut self, (param, req): (T, HttpRequest)) -> Self::Future { - AsyncHandleServiceResponse { + AsyncHandlerServiceResponse { fut: self.hnd.call(param).into_future(), req: Some(req), } @@ -239,12 +239,12 @@ where } #[doc(hidden)] -pub struct AsyncHandleServiceResponse { +pub struct AsyncHandlerServiceResponse { fut: T, req: Option, } -impl Future for AsyncHandleServiceResponse +impl Future for AsyncHandlerServiceResponse where T: Future, T::Item: Into, diff --git a/src/lib.rs b/src/lib.rs index fd1b21f35..dd60c7b84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,24 @@ mod service; mod state; pub mod test; +/// Attribute macros for route registration +/// +/// ```rust +/// use actix_web::{macros, App, HttpResponse}; +/// +/// #[macros::get("/index.html")] +/// fn index() -> HttpResponse { +/// HttpResponse::Ok().finish() +/// } +/// +/// fn main() { +/// let app = App::new().service(index); +/// } +/// ``` +pub mod macros { + pub use actix_web_codegen::{get, post, put}; +} + // re-export for convenience pub use actix_http::Response as HttpResponse; pub use actix_http::{body, error, http, Error, HttpMessage, ResponseError, Result}; diff --git a/src/route.rs b/src/route.rs index b611164e9..9538dfd2c 100644 --- a/src/route.rs +++ b/src/route.rs @@ -8,7 +8,7 @@ use futures::{Async, Future, IntoFuture, Poll}; use crate::extract::{ConfigStorage, ExtractorConfig, FromRequest}; use crate::guard::{self, Guard}; -use crate::handler::{AsyncFactory, AsyncHandle, Extract, Factory, Handle}; +use crate::handler::{AsyncFactory, AsyncHandler, Extract, Factory, Handler}; use crate::responder::Responder; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; use crate::HttpResponse; @@ -50,8 +50,9 @@ impl Route

    { let config_ref = Rc::new(RefCell::new(None)); Route { service: Box::new(RouteNewService::new( - Extract::new(config_ref.clone()) - .and_then(Handle::new(HttpResponse::NotFound).map_err(|_| panic!())), + Extract::new(config_ref.clone()).and_then( + Handler::new(HttpResponse::NotFound).map_err(|_| panic!()), + ), )), guards: Rc::new(Vec::new()), config: ConfigStorage::default(), @@ -272,7 +273,7 @@ impl Route

    { T::Config::store_default(&mut self.config); self.service = Box::new(RouteNewService::new( Extract::new(self.config_ref.clone()) - .and_then(Handle::new(handler).map_err(|_| panic!())), + .and_then(Handler::new(handler).map_err(|_| panic!())), )); self } @@ -314,7 +315,7 @@ impl Route

    { { self.service = Box::new(RouteNewService::new( Extract::new(self.config_ref.clone()) - .and_then(AsyncHandle::new(handler).map_err(|_| panic!())), + .and_then(AsyncHandler::new(handler).map_err(|_| panic!())), )); self } From ceb6d45bf240ae228e4ce7f4bc17f5b747e4e6ae Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 11:43:46 -0800 Subject: [PATCH 227/427] reexpost extractors in web module --- actix-web-codegen/src/lib.rs | 9 +++++--- examples/basic.rs | 14 ++++++------ src/app.rs | 42 ++++++++++++++++-------------------- src/extract.rs | 8 +++---- src/lib.rs | 6 ++++-- src/route.rs | 4 ++-- 6 files changed, 41 insertions(+), 42 deletions(-) diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 1052c82a7..26b422d77 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -35,7 +35,8 @@ pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { #ast actix_web::dev::HttpServiceFactory::register( actix_web::Resource::new(#path) - .route(actix_web::web::get().to(#name)), config); + .guard(actix_web::guard::Get()) + .to(#name), config); } } }) @@ -71,7 +72,8 @@ pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { #ast actix_web::dev::HttpServiceFactory::register( actix_web::Resource::new(#path) - .route(actix_web::web::post().to(#name)), config); + .guard(actix_web::guard::Post()) + .to(#name), config); } } }) @@ -107,7 +109,8 @@ pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { #ast actix_web::dev::HttpServiceFactory::register( actix_web::Resource::new(#path) - .route(actix_web::web::put().to(#name)), config); + .guard(actix_web::guard::Put()) + .to(#name), config); } } }) diff --git a/examples/basic.rs b/examples/basic.rs index 39633f523..3f832780a 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -3,10 +3,10 @@ use futures::IntoFuture; use actix_web::macros::get; use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; -#[get("/resource1/index.html")] -fn index(req: HttpRequest) -> &'static str { +#[get("/resource1/{name}/index.html")] +fn index(req: HttpRequest, name: web::Path) -> String { println!("REQ: {:?}", req); - "Hello world!\r\n" + format!("Hello: {}!\r\n", name) } fn index_async(req: HttpRequest) -> impl IntoFuture { @@ -20,14 +20,14 @@ fn no_params() -> &'static str { } fn main() -> std::io::Result<()> { - ::std::env::set_var("RUST_LOG", "actix_server=info,actix_web2=info"); + std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info"); env_logger::init(); - let sys = actix_rt::System::new("hello-world"); HttpServer::new(|| { App::new() .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) .middleware(middleware::Compress::default()) + .middleware(middleware::Logger::default()) .service(index) .service(no_params) .service( @@ -44,7 +44,5 @@ fn main() -> std::io::Result<()> { }) .bind("127.0.0.1:8080")? .workers(1) - .start(); - - sys.run() + .run() } diff --git a/src/app.rs b/src/app.rs index f62c064a7..1cff1788e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -75,13 +75,13 @@ where /// /// ```rust /// use std::cell::Cell; - /// use actix_web::{web, State, App}; + /// use actix_web::{web, App}; /// /// struct MyState { /// counter: Cell, /// } /// - /// fn index(state: State) { + /// fn index(state: web::State) { /// state.counter.set(state.counter.get() + 1); /// } /// @@ -785,7 +785,7 @@ mod tests { use super::*; use crate::http::{Method, StatusCode}; use crate::test::{block_on, init_service, TestRequest}; - use crate::{web, HttpResponse, State}; + use crate::{web, HttpResponse}; #[test] fn test_default_resource() { @@ -828,21 +828,19 @@ mod tests { #[test] fn test_state() { - let mut srv = init_service( - App::new() - .state(10usize) - .service(web::resource("/").to(|_: State| HttpResponse::Ok())), - ); + let mut srv = + init_service(App::new().state(10usize).service( + web::resource("/").to(|_: web::State| HttpResponse::Ok()), + )); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let mut srv = init_service( - App::new() - .state(10u32) - .service(web::resource("/").to(|_: State| HttpResponse::Ok())), - ); + let mut srv = + init_service(App::new().state(10u32).service( + web::resource("/").to(|_: web::State| HttpResponse::Ok()), + )); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); @@ -850,20 +848,18 @@ mod tests { #[test] fn test_state_factory() { - let mut srv = init_service( - App::new() - .state_factory(|| Ok::<_, ()>(10usize)) - .service(web::resource("/").to(|_: State| HttpResponse::Ok())), - ); + let mut srv = + init_service(App::new().state_factory(|| Ok::<_, ()>(10usize)).service( + web::resource("/").to(|_: web::State| HttpResponse::Ok()), + )); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let mut srv = init_service( - App::new() - .state_factory(|| Ok::<_, ()>(10u32)) - .service(web::resource("/").to(|_: State| HttpResponse::Ok())), - ); + let mut srv = + init_service(App::new().state_factory(|| Ok::<_, ()>(10u32)).service( + web::resource("/").to(|_: web::State| HttpResponse::Ok()), + )); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); diff --git a/src/extract.rs b/src/extract.rs index 0b212aba5..6c838901f 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -554,8 +554,8 @@ impl Default for FormConfig { /// name: String, /// } /// -/// fn index(req: HttpRequest) -> Result> { -/// Ok(Json(MyObj { +/// fn index(req: HttpRequest) -> Result> { +/// Ok(web::Json(MyObj { /// name: req.match_info().get("name").unwrap().to_string(), /// })) /// } @@ -679,7 +679,7 @@ where /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{error, extract, web, App, HttpResponse, Json}; +/// use actix_web::{error, extract, web, App, HttpResponse}; /// /// #[derive(Deserialize)] /// struct Info { @@ -687,7 +687,7 @@ where /// } /// /// /// deserialize `Info` from request's body, max payload size is 4kb -/// fn index(info: Json) -> String { +/// fn index(info: web::Json) -> String { /// format!("Welcome {}!", info.username) /// } /// diff --git a/src/lib.rs b/src/lib.rs index dd60c7b84..35a88b981 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,7 +41,7 @@ pub use actix_http::Response as HttpResponse; pub use actix_http::{body, error, http, Error, HttpMessage, ResponseError, Result}; pub use crate::app::App; -pub use crate::extract::{FromRequest, Json}; +pub use crate::extract::FromRequest; pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::{Either, Responder}; @@ -49,7 +49,6 @@ pub use crate::route::Route; pub use crate::scope::Scope; pub use crate::server::HttpServer; pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; -pub use crate::state::State; pub mod dev { //! The `actix-web` prelude for library developers @@ -93,6 +92,9 @@ pub mod web { use crate::route::Route; use crate::scope::Scope; + pub use crate::extract::{Json, Path, Query}; + pub use crate::state::State; + /// Create resource for a specific path. /// /// Resources may have variable path segments. For example, a diff --git a/src/route.rs b/src/route.rs index 9538dfd2c..45bc65344 100644 --- a/src/route.rs +++ b/src/route.rs @@ -245,7 +245,7 @@ impl Route

    { /// ```rust /// # use std::collections::HashMap; /// # use serde_derive::Deserialize; - /// use actix_web::{web, App, Json, extract::Path, extract::Query}; + /// use actix_web::{web, App}; /// /// #[derive(Deserialize)] /// struct Info { @@ -253,7 +253,7 @@ impl Route

    { /// } /// /// /// extract path info using serde - /// fn index(path: Path, query: Query>, body: Json) -> String { + /// fn index(path: web::Path, query: web::Query>, body: web::Json) -> String { /// format!("Welcome {}!", path.username) /// } /// From d77954d19e99aabaa8749ead28f5a03435b0b656 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 12:32:40 -0800 Subject: [PATCH 228/427] fix files test --- actix-files/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index c08cae9c2..5dd98dcc6 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -214,11 +214,11 @@ fn directory_listing( /// Static files handling /// -/// `StaticFile` handler must be registered with `App::service()` method. +/// `Files` service must be registered with `App::service()` method. /// /// ```rust /// use actix_web::App; -/// use actix_staticfiles as fs; +/// use actix_files as fs; /// /// fn main() { /// let app = App::new() @@ -240,7 +240,7 @@ pub struct Files { impl Files { /// Create new `Files` instance for specified base directory. /// - /// `StaticFile` uses `ThreadPool` for blocking filesystem operations. + /// `File` uses `ThreadPool` for blocking filesystem operations. /// By default pool with 5x threads of available cpus is used. /// Pool size can be changed by setting ACTIX_CPU_POOL environment variable. pub fn new>(path: &str, dir: T) -> Files { From b211966c28112f808005ee19a92c94ad650ce86c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 13:33:40 -0800 Subject: [PATCH 229/427] Payload extractor --- actix-web-codegen/src/server.rs | 31 ----------- examples/basic.rs | 4 +- src/extract.rs | 95 ++++++++++++++++++++++++++++++++- src/lib.rs | 29 ++++------ 4 files changed, 107 insertions(+), 52 deletions(-) delete mode 100644 actix-web-codegen/src/server.rs diff --git a/actix-web-codegen/src/server.rs b/actix-web-codegen/src/server.rs deleted file mode 100644 index 43e663f3a..000000000 --- a/actix-web-codegen/src/server.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::collections::HashSet; -use std::env; -use std::fs::File; -use std::io::Read; -use std::path::PathBuf; - -use proc_macro2::TokenStream; -use quote::{quote, ToTokens}; -use syn; - -/// Thrift mux server impl -pub struct Server {} - -impl Server { - fn new() -> Server { - Server {} - } - - /// generate servers - pub fn generate(input: TokenStream) { - let mut srv = Server::new(); - let ast: syn::ItemFn = syn::parse2(input).unwrap(); - println!("T: {:?}", ast.ident); - - // quote! { - // #ast - - // #(#servers)* - // } - } -} diff --git a/examples/basic.rs b/examples/basic.rs index 3f832780a..f8b816480 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,6 +1,8 @@ use futures::IntoFuture; -use actix_web::macros::get; +#[macro_use] +extern crate actix_web; + use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; #[get("/resource1/{name}/index.html")] diff --git a/src/extract.rs b/src/extract.rs index 6c838901f..ac04f1c4e 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -409,7 +409,7 @@ impl DerefMut for Form { impl FromRequest

    for Form where T: DeserializeOwned + 'static, - P: Stream + 'static, + P: Stream + 'static, { type Error = Error; type Future = Box>; @@ -653,7 +653,7 @@ impl Responder for Json { impl FromRequest

    for Json where T: DeserializeOwned + 'static, - P: Stream + 'static, + P: Stream + 'static, { type Error = Error; type Future = Box>; @@ -739,6 +739,97 @@ impl Default for JsonConfig { } } +/// Payload extractor returns request 's payload stream. +/// +/// ## Example +/// +/// ```rust +/// use futures::{Future, Stream}; +/// use actix_web::{web, error, App, Error, HttpResponse}; +/// +/// /// extract binary data from request +/// fn index

    (body: web::Payload

    ) -> impl Future +/// where +/// P: Stream +/// { +/// body.map_err(Error::from) +/// .fold(web::BytesMut::new(), move |mut body, chunk| { +/// body.extend_from_slice(&chunk); +/// Ok::<_, Error>(body) +/// }) +/// .and_then(|body| { +/// format!("Body {:?}!", body); +/// Ok(HttpResponse::Ok().finish()) +/// }) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get().to_async(index)) +/// ); +/// } +/// ``` +pub struct Payload(crate::dev::Payload); + +impl Stream for Payload +where + T: Stream, +{ + type Item = Bytes; + type Error = PayloadError; + + #[inline] + fn poll(&mut self) -> Poll, PayloadError> { + self.0.poll() + } +} + +/// Get request's payload stream +/// +/// ## Example +/// +/// ```rust +/// use futures::{Future, Stream}; +/// use actix_web::{web, error, App, Error, HttpResponse}; +/// +/// /// extract binary data from request +/// fn index

    (body: web::Payload

    ) -> impl Future +/// where +/// P: Stream +/// { +/// body.map_err(Error::from) +/// .fold(web::BytesMut::new(), move |mut body, chunk| { +/// body.extend_from_slice(&chunk); +/// Ok::<_, Error>(body) +/// }) +/// .and_then(|body| { +/// format!("Body {:?}!", body); +/// Ok(HttpResponse::Ok().finish()) +/// }) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get().to_async(index)) +/// ); +/// } +/// ``` +impl

    FromRequest

    for Payload

    +where + P: Stream, +{ + type Error = Error; + type Future = Result, Error>; + type Config = (); + + #[inline] + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + Ok(Payload(req.take_payload())) + } +} + /// Request binary data from a request's payload. /// /// Loads request's payload and construct Bytes instance. diff --git a/src/lib.rs b/src/lib.rs index 35a88b981..ad4a2a866 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,23 +18,12 @@ mod service; mod state; pub mod test; -/// Attribute macros for route registration -/// -/// ```rust -/// use actix_web::{macros, App, HttpResponse}; -/// -/// #[macros::get("/index.html")] -/// fn index() -> HttpResponse { -/// HttpResponse::Ok().finish() -/// } -/// -/// fn main() { -/// let app = App::new().service(index); -/// } -/// ``` -pub mod macros { - pub use actix_web_codegen::{get, post, put}; -} +#[allow(unused_imports)] +#[macro_use] +extern crate actix_web_codegen; + +#[doc(hidden)] +pub use actix_web_codegen::*; // re-export for convenience pub use actix_http::Response as HttpResponse; @@ -85,6 +74,9 @@ pub mod web { use actix_http::{http::Method, Error, Response}; use futures::IntoFuture; + pub use actix_http::Response as HttpResponse; + pub use bytes::{Bytes, BytesMut}; + use crate::extract::FromRequest; use crate::handler::{AsyncFactory, Factory}; use crate::resource::Resource; @@ -92,7 +84,8 @@ pub mod web { use crate::route::Route; use crate::scope::Scope; - pub use crate::extract::{Json, Path, Query}; + pub use crate::extract::{Json, Path, Payload, Query}; + pub use crate::request::HttpRequest; pub use crate::state::State; /// Create resource for a specific path. From 0e57b4ad618bcf0d4c4140acd0bb268c4060211e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 14:01:52 -0800 Subject: [PATCH 230/427] export extractor configs via web module --- src/app.rs | 8 ++++---- src/extract.rs | 50 ++++++++++++++++++++++++------------------------- src/lib.rs | 5 +++-- src/resource.rs | 4 ++-- src/route.rs | 12 ++++++------ src/scope.rs | 8 ++++---- 6 files changed, 44 insertions(+), 43 deletions(-) diff --git a/src/app.rs b/src/app.rs index 1cff1788e..c1c019a3c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -189,9 +189,9 @@ where /// multiple resources with one route would be registered for same resource path. /// /// ```rust - /// use actix_web::{web, App, HttpResponse, extract::Path}; + /// use actix_web::{web, App, HttpResponse}; /// - /// fn index(data: Path<(String, String)>) -> &'static str { + /// fn index(data: web::Path<(String, String)>) -> &'static str { /// "Welcome!" /// } /// @@ -276,9 +276,9 @@ where /// multiple resources with one route would be registered for same resource path. /// /// ```rust - /// use actix_web::{web, App, HttpResponse, extract::Path}; + /// use actix_web::{web, App, HttpResponse}; /// - /// fn index(data: Path<(String, String)>) -> &'static str { + /// fn index(data: web::Path<(String, String)>) -> &'static str { /// "Welcome!" /// } /// diff --git a/src/extract.rs b/src/extract.rs index ac04f1c4e..c34d9df70 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -78,12 +78,12 @@ impl ExtractorConfig for () { /// ## Example /// /// ```rust -/// use actix_web::{web, http, App, extract::Path}; +/// use actix_web::{web, App}; /// /// /// extract path info from "/{username}/{count}/index.html" url /// /// {username} - deserializes to a String /// /// {count} - - deserializes to a u32 -/// fn index(info: Path<(String, u32)>) -> String { +/// fn index(info: web::Path<(String, u32)>) -> String { /// format!("Welcome {}! {}", info.0, info.1) /// } /// @@ -100,7 +100,7 @@ impl ExtractorConfig for () { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, extract::Path, Error}; +/// use actix_web::{web, App, Error}; /// /// #[derive(Deserialize)] /// struct Info { @@ -108,7 +108,7 @@ impl ExtractorConfig for () { /// } /// /// /// extract `Info` from a path using serde -/// fn index(info: Path) -> Result { +/// fn index(info: web::Path) -> Result { /// Ok(format!("Welcome {}!", info.username)) /// } /// @@ -170,12 +170,12 @@ impl From for Path { /// ## Example /// /// ```rust -/// use actix_web::{web, http, App, extract::Path}; +/// use actix_web::{web, App}; /// /// /// extract path info from "/{username}/{count}/index.html" url /// /// {username} - deserializes to a String /// /// {count} - - deserializes to a u32 -/// fn index(info: Path<(String, u32)>) -> String { +/// fn index(info: web::Path<(String, u32)>) -> String { /// format!("Welcome {}! {}", info.0, info.1) /// } /// @@ -192,7 +192,7 @@ impl From for Path { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, extract::Path, Error}; +/// use actix_web::{web, App, Error}; /// /// #[derive(Deserialize)] /// struct Info { @@ -200,7 +200,7 @@ impl From for Path { /// } /// /// /// extract `Info` from a path using serde -/// fn index(info: Path) -> Result { +/// fn index(info: web::Path) -> Result { /// Ok(format!("Welcome {}!", info.username)) /// } /// @@ -244,7 +244,7 @@ impl fmt::Display for Path { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, extract, App}; +/// use actix_web::{web, App}; /// /// #[derive(Debug, Deserialize)] /// pub enum ResponseType { @@ -261,7 +261,7 @@ impl fmt::Display for Path { /// // Use `Query` extractor for query information. /// // This handler get called only if request's query contains `username` field /// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` -/// fn index(info: extract::Query) -> String { +/// fn index(info: web::Query) -> String { /// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) /// } /// @@ -299,7 +299,7 @@ impl Query { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, extract, App}; +/// use actix_web::{web, App}; /// /// #[derive(Debug, Deserialize)] /// pub enum ResponseType { @@ -316,7 +316,7 @@ impl Query { /// // Use `Query` extractor for query information. /// // This handler get called only if request's query contains `username` field /// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` -/// fn index(info: extract::Query) -> String { +/// fn index(info: web::Query) -> String { /// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) /// } /// @@ -368,7 +368,7 @@ impl fmt::Display for Query { /// ```rust /// # extern crate actix_web; /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, extract::Form}; +/// use actix_web::{web, App}; /// /// #[derive(Deserialize)] /// struct FormData { @@ -378,7 +378,7 @@ impl fmt::Display for Query { /// /// Extract form data using serde. /// /// This handler get called only if content type is *x-www-form-urlencoded* /// /// and content of the request could be deserialized to a `FormData` struct -/// fn index(form: Form) -> String { +/// fn index(form: web::Form) -> String { /// format!("Welcome {}!", form.username) /// } /// # fn main() {} @@ -447,7 +447,7 @@ impl fmt::Display for Form { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, extract, App, Result}; +/// use actix_web::{web, App, Result}; /// /// #[derive(Deserialize)] /// struct FormData { @@ -456,7 +456,7 @@ impl fmt::Display for Form { /// /// /// Extract form data using serde. /// /// Custom configuration is used for this handler, max payload size is 4k -/// fn index(form: extract::Form) -> Result { +/// fn index(form: web::Form) -> Result { /// Ok(format!("Welcome {}!", form.username)) /// } /// @@ -465,7 +465,7 @@ impl fmt::Display for Form { /// web::resource("/index.html") /// .route(web::get() /// // change `Form` extractor configuration -/// .config(extract::FormConfig::default().limit(4097)) +/// .config(web::FormConfig::default().limit(4097)) /// .to(index)) /// ); /// } @@ -520,7 +520,7 @@ impl Default for FormConfig { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, extract, App}; +/// use actix_web::{web, App}; /// /// #[derive(Deserialize)] /// struct Info { @@ -528,7 +528,7 @@ impl Default for FormConfig { /// } /// /// /// deserialize `Info` from request's body -/// fn index(info: extract::Json) -> String { +/// fn index(info: web::Json) -> String { /// format!("Welcome {}!", info.username) /// } /// @@ -631,7 +631,7 @@ impl Responder for Json { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, extract, App}; +/// use actix_web::{web, App}; /// /// #[derive(Deserialize)] /// struct Info { @@ -639,7 +639,7 @@ impl Responder for Json { /// } /// /// /// deserialize `Info` from request's body -/// fn index(info: extract::Json) -> String { +/// fn index(info: web::Json) -> String { /// format!("Welcome {}!", info.username) /// } /// @@ -679,7 +679,7 @@ where /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{error, extract, web, App, HttpResponse}; +/// use actix_web::{error, web, App, HttpResponse}; /// /// #[derive(Deserialize)] /// struct Info { @@ -696,7 +696,7 @@ where /// web::resource("/index.html").route( /// web::post().config( /// // change json extractor configuration -/// extract::JsonConfig::default().limit(4096) +/// web::JsonConfig::default().limit(4096) /// .error_handler(|err, req| { // <- create custom error response /// error::InternalError::from_response( /// err, HttpResponse::Conflict().finish()).into() @@ -887,7 +887,7 @@ where /// ## Example /// /// ```rust -/// use actix_web::{web, extract, App}; +/// use actix_web::{web, App}; /// /// /// extract text data from request /// fn index(text: String) -> String { @@ -898,7 +898,7 @@ where /// let app = App::new().service( /// web::resource("/index.html").route( /// web::get() -/// .config(extract::PayloadConfig::new(4096)) // <- limit size of the payload +/// .config(web::PayloadConfig::new(4096)) // <- limit size of the payload /// .to(index)) // <- register handler with extractor params /// ); /// } diff --git a/src/lib.rs b/src/lib.rs index ad4a2a866..def2abcf2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #![allow(clippy::type_complexity)] mod app; -pub mod extract; +mod extract; mod handler; // mod info; pub mod blocking; @@ -84,7 +84,8 @@ pub mod web { use crate::route::Route; use crate::scope::Scope; - pub use crate::extract::{Json, Path, Payload, Query}; + pub use crate::extract::{Form, Json, Path, Payload, Query}; + pub use crate::extract::{FormConfig, JsonConfig, PayloadConfig}; pub use crate::request::HttpRequest; pub use crate::state::State; diff --git a/src/resource.rs b/src/resource.rs index 157b181ef..13afff70c 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -75,9 +75,9 @@ where /// Add match guard to a resource. /// /// ```rust - /// use actix_web::{web, guard, App, HttpResponse, extract::Path}; + /// use actix_web::{web, guard, App, HttpResponse}; /// - /// fn index(data: Path<(String, String)>) -> &'static str { + /// fn index(data: web::Path<(String, String)>) -> &'static str { /// "Welcome!" /// } /// diff --git a/src/route.rs b/src/route.rs index 45bc65344..d1a8320d9 100644 --- a/src/route.rs +++ b/src/route.rs @@ -220,7 +220,7 @@ impl Route

    { /// /// ```rust /// #[macro_use] extern crate serde_derive; - /// use actix_web::{web, http, App, extract::Path}; + /// use actix_web::{web, http, App}; /// /// #[derive(Deserialize)] /// struct Info { @@ -228,7 +228,7 @@ impl Route

    { /// } /// /// /// extract path info using serde - /// fn index(info: Path) -> String { + /// fn index(info: web::Path) -> String { /// format!("Welcome {}!", info.username) /// } /// @@ -284,7 +284,7 @@ impl Route

    { /// ```rust /// # use futures::future::ok; /// #[macro_use] extern crate serde_derive; - /// use actix_web::{web, App, Error, extract::Path}; + /// use actix_web::{web, App, Error}; /// use futures::Future; /// /// #[derive(Deserialize)] @@ -293,7 +293,7 @@ impl Route

    { /// } /// /// /// extract path info using serde - /// fn index(info: Path) -> impl Future { + /// fn index(info: web::Path) -> impl Future { /// ok("Hello World!") /// } /// @@ -324,7 +324,7 @@ impl Route

    { /// for specific route. /// /// ```rust - /// use actix_web::{web, extract, App}; + /// use actix_web::{web, App}; /// /// /// extract text data from request /// fn index(body: String) -> String { @@ -336,7 +336,7 @@ impl Route

    { /// web::resource("/index.html").route( /// web::get() /// // limit size of the payload - /// .config(extract::PayloadConfig::new(4096)) + /// .config(web::PayloadConfig::new(4096)) /// // register handler /// .to(index) /// )); diff --git a/src/scope.rs b/src/scope.rs index 5580b15e4..a6358869a 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -89,9 +89,9 @@ where /// Add match guard to a scope. /// /// ```rust - /// use actix_web::{web, guard, App, HttpRequest, HttpResponse, extract::Path}; + /// use actix_web::{web, guard, App, HttpRequest, HttpResponse}; /// - /// fn index(data: Path<(String, String)>) -> &'static str { + /// fn index(data: web::Path<(String, String)>) -> &'static str { /// "Welcome!" /// } /// @@ -146,9 +146,9 @@ where /// multiple resources with one route would be registered for same resource path. /// /// ```rust - /// use actix_web::{web, App, HttpResponse, extract::Path}; + /// use actix_web::{web, App, HttpResponse}; /// - /// fn index(data: Path<(String, String)>) -> &'static str { + /// fn index(data: web::Path<(String, String)>) -> &'static str { /// "Welcome!" /// } /// From c2a350b33fba39ed7e175dfaa5b5ddd1b3da88aa Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 14:40:20 -0800 Subject: [PATCH 231/427] export blocking via web module --- src/lib.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index def2abcf2..6cf18a2ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,6 +77,7 @@ pub mod web { pub use actix_http::Response as HttpResponse; pub use bytes::{Bytes, BytesMut}; + use crate::blocking::CpuFuture; use crate::extract::FromRequest; use crate::handler::{AsyncFactory, Factory}; use crate::resource::Resource; @@ -234,4 +235,15 @@ pub mod web { { Route::new().to_async(handler) } + + /// Execute blocking function on a thread pool, returns future that resolves + /// to result of the function execution. + pub fn blocking(f: F) -> CpuFuture + where + F: FnOnce() -> Result + Send + 'static, + I: Send + 'static, + E: Send + std::fmt::Debug + 'static, + { + crate::blocking::run(f) + } } From b6b2eadb3ac0230c0b2c688b6f3e586a5821884d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 14:41:43 -0800 Subject: [PATCH 232/427] rename blocking fn --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 6cf18a2ab..6e809fb65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -238,7 +238,7 @@ pub mod web { /// Execute blocking function on a thread pool, returns future that resolves /// to result of the function execution. - pub fn blocking(f: F) -> CpuFuture + pub fn block(f: F) -> CpuFuture where F: FnOnce() -> Result + Send + 'static, I: Send + 'static, From 88e5059910bfc54be07b52043c2af97a1fa9790d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 15:37:39 -0800 Subject: [PATCH 233/427] add doc string to guards --- src/guard.rs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/guard.rs b/src/guard.rs index 1632b9975..f9565d0fb 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -1,4 +1,30 @@ //! Route match guards. +//! +//! Guards are one of the way how actix-web router chooses +//! handler service. In essence it just function that accepts +//! reference to a `RequestHead` instance and returns boolean. +//! It is possible to add guards to *scopes*, *resources* +//! and *routes*. Actix provide several guards by default, like various +//! http methods, header, etc. To become a guard, type must implement `Guard` +//! trait. Simple functions coulds guards as well. +//! +//! Guard can not modify request object. But it is possible to +//! to store extra attributes on a request by using `Extensions` container. +//! Extensions container available via `RequestHead::extensions()` method. +//! +//! ```rust +//! use actix_web::{web, http, dev, guard, App, HttpResponse}; +//! +//! fn main() { +//! App::new().service(web::resource("/index.html").route( +//! web::route() +//! .guard(guard::Post()) +//! .guard(|head: &dev::RequestHead| head.method == http::Method::GET) +//! .to(|| HttpResponse::MethodNotAllowed())) +//! ); +//! } +//! ``` + #![allow(non_snake_case)] use actix_http::http::{self, header, HttpTryFrom}; use actix_http::RequestHead; @@ -13,6 +39,18 @@ pub trait Guard { fn check(&self, request: &RequestHead) -> bool; } +#[doc(hidden)] +pub struct FnGuard bool + 'static>(F); + +impl Guard for F +where + F: Fn(&RequestHead) -> bool + 'static, +{ + fn check(&self, head: &RequestHead) -> bool { + (*self)(head) + } +} + /// Return guard that matches if any of supplied guards. /// /// ```rust From eef687ec80897b476498a70f037472bef22a3994 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 15:51:24 -0800 Subject: [PATCH 234/427] remove unneeded methods --- src/lib.rs | 29 +++++++++++++++++------------ src/responder.rs | 2 +- src/route.rs | 20 -------------------- src/scope.rs | 2 +- 4 files changed, 19 insertions(+), 34 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6e809fb65..a61387e81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ pub use actix_web_codegen::*; // re-export for convenience pub use actix_http::Response as HttpResponse; -pub use actix_http::{body, error, http, Error, HttpMessage, ResponseError, Result}; +pub use actix_http::{error, http, Error, HttpMessage, ResponseError, Result}; pub use crate::app::App; pub use crate::extract::FromRequest; @@ -154,37 +154,42 @@ pub mod web { Scope::new(path) } - /// Create **route** without configuration. + /// Create *route* without configuration. pub fn route() -> Route

    { Route::new() } - /// Create **route** with `GET` method guard. + /// Create *route* with `GET` method guard. pub fn get() -> Route

    { - Route::get() + Route::new().method(Method::GET) } - /// Create **route** with `POST` method guard. + /// Create *route* with `POST` method guard. pub fn post() -> Route

    { - Route::post() + Route::new().method(Method::POST) } - /// Create **route** with `PUT` method guard. + /// Create *route* with `PUT` method guard. pub fn put() -> Route

    { - Route::put() + Route::new().method(Method::PUT) } - /// Create **route** with `DELETE` method guard. + /// Create *route* with `PATCH` method guard. + pub fn patch() -> Route

    { + Route::new().method(Method::PATCH) + } + + /// Create *route* with `DELETE` method guard. pub fn delete() -> Route

    { - Route::delete() + Route::new().method(Method::DELETE) } - /// Create **route** with `HEAD` method guard. + /// Create *route* with `HEAD` method guard. pub fn head() -> Route

    { Route::new().method(Method::HEAD) } - /// Create **route** and add method guard. + /// Create *route* and add method guard. pub fn method(method: Method) -> Route

    { Route::new().method(method) } diff --git a/src/responder.rs b/src/responder.rs index 9e9e0f109..6dce300a6 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -291,7 +291,7 @@ mod tests { use actix_service::Service; use bytes::Bytes; - use crate::body::{Body, ResponseBody}; + use crate::dev::{Body, ResponseBody}; use crate::http::StatusCode; use crate::test::{init_service, TestRequest}; use crate::{web, App}; diff --git a/src/route.rs b/src/route.rs index d1a8320d9..f7b99050e 100644 --- a/src/route.rs +++ b/src/route.rs @@ -60,26 +60,6 @@ impl Route

    { } } - /// Create new `GET` route. - pub fn get() -> Route

    { - Route::new().method(Method::GET) - } - - /// Create new `POST` route. - pub fn post() -> Route

    { - Route::new().method(Method::POST) - } - - /// Create new `PUT` route. - pub fn put() -> Route

    { - Route::new().method(Method::PUT) - } - - /// Create new `DELETE` route. - pub fn delete() -> Route

    { - Route::new().method(Method::DELETE) - } - pub(crate) fn finish(self) -> Self { *self.config_ref.borrow_mut() = self.config.storage.clone(); self diff --git a/src/scope.rs b/src/scope.rs index a6358869a..2c2e3c2ba 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -451,7 +451,7 @@ mod tests { use actix_service::Service; use bytes::Bytes; - use crate::body::{Body, ResponseBody}; + use crate::dev::{Body, ResponseBody}; use crate::http::{Method, StatusCode}; use crate::test::{block_on, init_service, TestRequest}; use crate::{guard, web, App, HttpRequest, HttpResponse}; From 2f6df111832112ad012687557f66b112ef0d1ed1 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 19:31:17 -0800 Subject: [PATCH 235/427] do not execute blocking fn if result is not required --- src/blocking.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/blocking.rs b/src/blocking.rs index 01be30dd2..fc9cec299 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -41,6 +41,7 @@ thread_local! { }; } +/// Blocking operation execution error #[derive(Debug, Display)] pub enum BlockingError { #[display(fmt = "{:?}", _0)] @@ -62,13 +63,17 @@ where let (tx, rx) = oneshot::channel(); POOL.with(|pool| { pool.execute(move || { - let _ = tx.send(f()); + if !tx.is_canceled() { + let _ = tx.send(f()); + } }) }); CpuFuture { rx } } +/// Blocking operation completion future. It resolves with results +/// of blocking function execution. pub struct CpuFuture { rx: oneshot::Receiver>, } From e3245223893a648db8c4cd5ccc426a4aef9e16e6 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 8 Mar 2019 22:47:49 -0800 Subject: [PATCH 236/427] listen method has different signature --- test-server/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 695a477d2..dd7bc362a 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -67,7 +67,7 @@ impl TestServer { let local_addr = tcp.local_addr().unwrap(); Server::build() - .listen("test", tcp, factory) + .listen("test", tcp, factory)? .workers(1) .disable_signals() .start(); From ca73f178c927d9e3d786952826005e652f00fa35 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 07:37:23 -0800 Subject: [PATCH 237/427] revert generic service request; add ServerConfig to service factories --- Cargo.toml | 2 ++ examples/echo.rs | 10 +++--- examples/echo2.rs | 10 +++--- examples/framed_hello.rs | 10 +++--- examples/hello-world.rs | 10 +++--- src/client/connector.rs | 68 ++++++++++++++++++++++++++++------------ src/client/pool.rs | 15 +++++++-- src/client/request.rs | 2 +- src/h1/dispatcher.rs | 14 ++++----- src/h1/service.rs | 39 +++++++++++++---------- src/h2/dispatcher.rs | 14 ++++----- src/h2/service.rs | 41 ++++++++++++++---------- src/lib.rs | 3 +- src/service/senderror.rs | 12 ++++--- src/service/service.rs | 37 ++++++++++++---------- src/ws/client/mod.rs | 14 +++++++++ src/ws/client/service.rs | 11 ++++--- src/ws/service.rs | 6 ++-- src/ws/transport.rs | 10 +++--- test-server/src/lib.rs | 12 ++++--- tests/test_server.rs | 21 ++++++++----- 21 files changed, 222 insertions(+), 139 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9d55b85c5..594ab88db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ actix-codec = "0.1.1" actix-connector = { git="https://github.com/actix/actix-net.git" } actix-service = { git="https://github.com/actix/actix-net.git" } actix-utils = { git="https://github.com/actix/actix-net.git" } +actix-server-config = { git="https://github.com/actix/actix-net.git" } base64 = "0.10" backtrace = "0.3" @@ -92,6 +93,7 @@ actix-server = { git="https://github.com/actix/actix-net.git", features=["ssl"] #actix-connector = { version = "0.3.0", features=["ssl"] } actix-connector = { git="https://github.com/actix/actix-net.git", features=["ssl"] } actix-http-test = { path="test-server", features=["ssl"] } + env_logger = "0.6" serde_derive = "1.0" openssl = { version="0.10" } diff --git a/examples/echo.rs b/examples/echo.rs index 03d5b4706..eb4aaa657 100644 --- a/examples/echo.rs +++ b/examples/echo.rs @@ -1,3 +1,5 @@ +use std::{env, io}; + use actix_http::HttpMessage; use actix_http::{h1, Request, Response}; use actix_server::Server; @@ -6,9 +8,8 @@ use bytes::Bytes; use futures::Future; use http::header::HeaderValue; use log::info; -use std::env; -fn main() { +fn main() -> io::Result<()> { env::set_var("RUST_LOG", "echo=info"); env_logger::init(); @@ -27,7 +28,6 @@ fn main() { }) }) .map(|_| ()) - }) - .unwrap() - .run(); + })? + .run() } diff --git a/examples/echo2.rs b/examples/echo2.rs index 2fd9cbcff..3bb8d83d5 100644 --- a/examples/echo2.rs +++ b/examples/echo2.rs @@ -1,3 +1,5 @@ +use std::{env, io}; + use actix_http::http::HeaderValue; use actix_http::HttpMessage; use actix_http::{h1, Error, Request, Response}; @@ -6,7 +8,6 @@ use actix_service::NewService; use bytes::Bytes; use futures::Future; use log::info; -use std::env; fn handle_request(mut req: Request) -> impl Future { req.body().limit(512).from_err().and_then(|bytes: Bytes| { @@ -17,7 +18,7 @@ fn handle_request(mut req: Request) -> impl Future io::Result<()> { env::set_var("RUST_LOG", "echo=info"); env_logger::init(); @@ -29,7 +30,6 @@ fn main() { .server_hostname("localhost") .finish(|_req: Request| handle_request(_req)) .map(|_| ()) - }) - .unwrap() - .run(); + })? + .run() } diff --git a/examples/framed_hello.rs b/examples/framed_hello.rs index 5bbc3be9b..ff3977854 100644 --- a/examples/framed_hello.rs +++ b/examples/framed_hello.rs @@ -1,3 +1,5 @@ +use std::{env, io}; + use actix_codec::Framed; use actix_http::{h1, Response, SendResponse, ServiceConfig}; use actix_server::Server; @@ -5,9 +7,8 @@ use actix_service::NewService; use actix_utils::framed::IntoFramed; use actix_utils::stream::TakeItem; use futures::Future; -use std::env; -fn main() { +fn main() -> io::Result<()> { env::set_var("RUST_LOG", "framed_hello=info"); env_logger::init(); @@ -20,7 +21,6 @@ fn main() { .map_err(|_| ()) .map(|_| ()) }) - }) - .unwrap() - .run(); + })? + .run() } diff --git a/examples/hello-world.rs b/examples/hello-world.rs index e0c322a22..05d69fed8 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -1,12 +1,13 @@ +use std::{env, io}; + use actix_http::{h1, Response}; use actix_server::Server; use actix_service::NewService; use futures::future; use http::header::HeaderValue; use log::info; -use std::env; -fn main() { +fn main() -> io::Result<()> { env::set_var("RUST_LOG", "hello_world=info"); env_logger::init(); @@ -23,7 +24,6 @@ fn main() { future::ok::<_, ()>(res.body("Hello world!")) }) .map(|_| ()) - }) - .unwrap() - .run(); + })? + .run() } diff --git a/src/client/connector.rs b/src/client/connector.rs index d1fd802e9..5eb85ba61 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -133,8 +133,11 @@ impl Connector { /// Finish configuration process and create connector service. pub fn service( self, - ) -> impl Service + Clone - { + ) -> impl Service< + Request = Connect, + Response = impl Connection, + Error = ConnectorError, + > + Clone { #[cfg(not(feature = "ssl"))] { let connector = TimeoutService::new( @@ -238,7 +241,11 @@ mod connect_impl { pub(crate) struct InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io, Protocol), + Error = ConnectorError, + >, { pub(crate) tcp_pool: ConnectionPool, } @@ -246,8 +253,11 @@ mod connect_impl { impl Clone for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service - + Clone, + T: Service< + Request = Connect, + Response = (Connect, Io, Protocol), + Error = ConnectorError, + > + Clone, { fn clone(&self) -> Self { InnerConnector { @@ -256,15 +266,16 @@ mod connect_impl { } } - impl Service for InnerConnector + impl Service for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, T: Service, { + type Request = Connect; type Response = IoConnection; type Error = ConnectorError; type Future = Either< - as Service>::Future, + as Service>::Future, FutureResult, ConnectorError>, >; @@ -299,12 +310,12 @@ mod connect_impl { Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, T1: Service< - Connect, + Request = Connect, Response = (Connect, Io1, Protocol), Error = ConnectorError, >, T2: Service< - Connect, + Request = Connect, Response = (Connect, Io2, Protocol), Error = ConnectorError, >, @@ -318,12 +329,12 @@ mod connect_impl { Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, T1: Service< - Connect, + Request = Connect, Response = (Connect, Io1, Protocol), Error = ConnectorError, > + Clone, T2: Service< - Connect, + Request = Connect, Response = (Connect, Io2, Protocol), Error = ConnectorError, > + Clone, @@ -336,21 +347,22 @@ mod connect_impl { } } - impl Service for InnerConnector + impl Service for InnerConnector where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, T1: Service< - Connect, + Request = Connect, Response = (Connect, Io1, Protocol), Error = ConnectorError, >, T2: Service< - Connect, + Request = Connect, Response = (Connect, Io2, Protocol), Error = ConnectorError, >, { + type Request = Connect; type Response = EitherConnection; type Error = ConnectorError; type Future = Either< @@ -385,15 +397,23 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseA where Io1: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io1, Protocol), + Error = ConnectorError, + >, { - fut: as Service>::Future, + fut: as Service>::Future, _t: PhantomData, } impl Future for InnerConnectorResponseA where - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io1, Protocol), + Error = ConnectorError, + >, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { @@ -411,15 +431,23 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseB where Io2: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io2, Protocol), + Error = ConnectorError, + >, { - fut: as Service>::Future, + fut: as Service>::Future, _t: PhantomData, } impl Future for InnerConnectorResponseB where - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io2, Protocol), + Error = ConnectorError, + >, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { diff --git a/src/client/pool.rs b/src/client/pool.rs index 425e89395..188980cb3 100644 --- a/src/client/pool.rs +++ b/src/client/pool.rs @@ -48,7 +48,11 @@ pub(crate) struct ConnectionPool( impl ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io, Protocol), + Error = ConnectorError, + >, { pub(crate) fn new( connector: T, @@ -84,11 +88,16 @@ where } } -impl Service for ConnectionPool +impl Service for ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io, Protocol), + Error = ConnectorError, + >, { + type Request = Connect; type Response = IoConnection; type Error = ConnectorError; type Future = Either< diff --git a/src/client/request.rs b/src/client/request.rs index 637635d0f..7e971756d 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -176,7 +176,7 @@ where ) -> impl Future where B: 'static, - T: Service, + T: Service, I: Connection, { let Self { head, body } = self; diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 42ab33e79..a7eb96c3f 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -36,14 +36,14 @@ bitflags! { } /// Dispatcher for HTTP/1.1 protocol -pub struct Dispatcher + 'static, B: MessageBody> +pub struct Dispatcher + 'static, B: MessageBody> where S::Error: Debug, { inner: Option>, } -struct InnerDispatcher + 'static, B: MessageBody> +struct InnerDispatcher + 'static, B: MessageBody> where S::Error: Debug, { @@ -67,13 +67,13 @@ enum DispatcherMessage { Error(Response<()>), } -enum State, B: MessageBody> { +enum State, B: MessageBody> { None, ServiceCall(S::Future), SendPayload(ResponseBody), } -impl, B: MessageBody> State { +impl, B: MessageBody> State { fn is_empty(&self) -> bool { if let State::None = self { true @@ -86,7 +86,7 @@ impl, B: MessageBody> State { impl Dispatcher where T: AsyncRead + AsyncWrite, - S: Service + 'static, + S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody, @@ -145,7 +145,7 @@ where impl InnerDispatcher where T: AsyncRead + AsyncWrite, - S: Service + 'static, + S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody, @@ -465,7 +465,7 @@ where impl Future for Dispatcher where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Error: Debug, S::Response: Into>, B: MessageBody, diff --git a/src/h1/service.rs b/src/h1/service.rs index 229229e69..6a36678b3 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -3,6 +3,7 @@ use std::marker::PhantomData; use std::net; use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_server_config::ServerConfig as SrvConfig; use actix_service::{IntoNewService, NewService, Service}; use actix_utils::cloneable::CloneableService; use futures::future::{ok, FutureResult}; @@ -28,14 +29,14 @@ pub struct H1Service { impl H1Service where - S: NewService, + S: NewService, S::Error: Debug, S::Response: Into>, S::Service: 'static, B: MessageBody, { /// Create new `HttpService` instance with default config. - pub fn new>(service: F) -> Self { + pub fn new>(service: F) -> Self { let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0); H1Service { @@ -46,7 +47,7 @@ where } /// Create new `HttpService` instance with config. - pub fn with_config>( + pub fn with_config>( cfg: ServiceConfig, service: F, ) -> Self { @@ -63,24 +64,25 @@ where } } -impl NewService for H1Service +impl NewService for H1Service where T: AsyncRead + AsyncWrite, - S: NewService, + S: NewService, S::Error: Debug, S::Response: Into>, S::Service: 'static, B: MessageBody, { + type Request = T; type Response = (); type Error = DispatchError; type InitError = S::InitError; type Service = H1ServiceHandler; type Future = H1ServiceResponse; - fn new_service(&self, _: &()) -> Self::Future { + fn new_service(&self, cfg: &SrvConfig) -> Self::Future { H1ServiceResponse { - fut: self.srv.new_service(&()).into_future(), + fut: self.srv.new_service(cfg).into_future(), cfg: Some(self.cfg.clone()), _t: PhantomData, } @@ -103,7 +105,7 @@ pub struct H1ServiceBuilder { impl H1ServiceBuilder where - S: NewService, + S: NewService, S::Error: Debug, { /// Create instance of `ServiceConfigBuilder` @@ -201,7 +203,7 @@ where pub fn finish(self, service: F) -> H1Service where B: MessageBody, - F: IntoNewService, + F: IntoNewService, { let cfg = ServiceConfig::new( self.keep_alive, @@ -217,7 +219,7 @@ where } #[doc(hidden)] -pub struct H1ServiceResponse, B> { +pub struct H1ServiceResponse, B> { fut: ::Future, cfg: Option, _t: PhantomData<(T, B)>, @@ -226,7 +228,7 @@ pub struct H1ServiceResponse, B> { impl Future for H1ServiceResponse where T: AsyncRead + AsyncWrite, - S: NewService, + S: NewService, S::Service: 'static, S::Error: Debug, S::Response: Into>, @@ -253,7 +255,7 @@ pub struct H1ServiceHandler { impl H1ServiceHandler where - S: Service, + S: Service, S::Error: Debug, S::Response: Into>, B: MessageBody, @@ -267,14 +269,15 @@ where } } -impl Service for H1ServiceHandler +impl Service for H1ServiceHandler where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Error: Debug, S::Response: Into>, B: MessageBody, { + type Request = T; type Response = (); type Error = DispatchError; type Future = Dispatcher; @@ -311,17 +314,18 @@ where } } -impl NewService for OneRequest +impl NewService for OneRequest where T: AsyncRead + AsyncWrite, { + type Request = T; type Response = (Request, Framed); type Error = ParseError; type InitError = (); type Service = OneRequestService; type Future = FutureResult; - fn new_service(&self, _: &()) -> Self::Future { + fn new_service(&self, _: &SrvConfig) -> Self::Future { ok(OneRequestService { config: self.config.clone(), _t: PhantomData, @@ -336,10 +340,11 @@ pub struct OneRequestService { _t: PhantomData, } -impl Service for OneRequestService +impl Service for OneRequestService where T: AsyncRead + AsyncWrite, { + type Request = T; type Response = (Request, Framed); type Error = ParseError; type Future = OneRequestServiceResponse; diff --git a/src/h2/dispatcher.rs b/src/h2/dispatcher.rs index be813bd57..a3c731ebb 100644 --- a/src/h2/dispatcher.rs +++ b/src/h2/dispatcher.rs @@ -38,7 +38,7 @@ bitflags! { /// Dispatcher for HTTP/2 protocol pub struct Dispatcher< T: AsyncRead + AsyncWrite, - S: Service + 'static, + S: Service + 'static, B: MessageBody, > { flags: Flags, @@ -53,7 +53,7 @@ pub struct Dispatcher< impl Dispatcher where T: AsyncRead + AsyncWrite, - S: Service + 'static, + S: Service + 'static, S::Error: fmt::Debug, S::Response: Into>, B: MessageBody + 'static, @@ -95,7 +95,7 @@ where impl Future for Dispatcher where T: AsyncRead + AsyncWrite, - S: Service + 'static, + S: Service + 'static, S::Error: fmt::Debug, S::Response: Into>, B: MessageBody + 'static, @@ -141,20 +141,20 @@ where } } -struct ServiceResponse, B> { +struct ServiceResponse { state: ServiceResponseState, config: ServiceConfig, buffer: Option, } -enum ServiceResponseState, B> { +enum ServiceResponseState { ServiceCall(S::Future, Option>), SendPayload(SendStream, ResponseBody), } impl ServiceResponse where - S: Service + 'static, + S: Service + 'static, S::Error: fmt::Debug, S::Response: Into>, B: MessageBody + 'static, @@ -222,7 +222,7 @@ where impl Future for ServiceResponse where - S: Service + 'static, + S: Service + 'static, S::Error: fmt::Debug, S::Response: Into>, B: MessageBody + 'static, diff --git a/src/h2/service.rs b/src/h2/service.rs index 530629947..57515d4e8 100644 --- a/src/h2/service.rs +++ b/src/h2/service.rs @@ -3,6 +3,7 @@ use std::marker::PhantomData; use std::{io, net}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_server_config::ServerConfig as SrvConfig; use actix_service::{IntoNewService, NewService, Service}; use actix_utils::cloneable::CloneableService; use bytes::Bytes; @@ -30,14 +31,14 @@ pub struct H2Service { impl H2Service where - S: NewService, + S: NewService, S::Service: 'static, S::Error: Debug + 'static, S::Response: Into>, B: MessageBody + 'static, { /// Create new `HttpService` instance. - pub fn new>(service: F) -> Self { + pub fn new>(service: F) -> Self { let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0); H2Service { @@ -48,7 +49,7 @@ where } /// Create new `HttpService` instance with config. - pub fn with_config>( + pub fn with_config>( cfg: ServiceConfig, service: F, ) -> Self { @@ -65,24 +66,25 @@ where } } -impl NewService for H2Service +impl NewService for H2Service where T: AsyncRead + AsyncWrite, - S: NewService, + S: NewService, S::Service: 'static, S::Error: Debug, S::Response: Into>, B: MessageBody + 'static, { + type Request = T; type Response = (); type Error = DispatchError; type InitError = S::InitError; type Service = H2ServiceHandler; type Future = H2ServiceResponse; - fn new_service(&self, _: &()) -> Self::Future { + fn new_service(&self, cfg: &SrvConfig) -> Self::Future { H2ServiceResponse { - fut: self.srv.new_service(&()).into_future(), + fut: self.srv.new_service(cfg).into_future(), cfg: Some(self.cfg.clone()), _t: PhantomData, } @@ -105,7 +107,7 @@ pub struct H2ServiceBuilder { impl H2ServiceBuilder where - S: NewService, + S: NewService, S::Service: 'static, S::Error: Debug + 'static, { @@ -204,7 +206,7 @@ where pub fn finish(self, service: F) -> H2Service where B: MessageBody, - F: IntoNewService, + F: IntoNewService, { let cfg = ServiceConfig::new( self.keep_alive, @@ -220,7 +222,7 @@ where } #[doc(hidden)] -pub struct H2ServiceResponse, B> { +pub struct H2ServiceResponse, B> { fut: ::Future, cfg: Option, _t: PhantomData<(T, B)>, @@ -229,7 +231,7 @@ pub struct H2ServiceResponse, B> { impl Future for H2ServiceResponse where T: AsyncRead + AsyncWrite, - S: NewService, + S: NewService, S::Service: 'static, S::Response: Into>, S::Error: Debug, @@ -256,7 +258,7 @@ pub struct H2ServiceHandler { impl H2ServiceHandler where - S: Service + 'static, + S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody + 'static, @@ -270,14 +272,15 @@ where } } -impl Service for H2ServiceHandler +impl Service for H2ServiceHandler where T: AsyncRead + AsyncWrite, - S: Service + 'static, + S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody + 'static, { + type Request = T; type Response = (); type Error = DispatchError; type Future = H2ServiceHandlerResponse; @@ -300,7 +303,11 @@ where } } -enum State + 'static, B: MessageBody> { +enum State< + T: AsyncRead + AsyncWrite, + S: Service + 'static, + B: MessageBody, +> { Incoming(Dispatcher), Handshake( Option>, @@ -312,7 +319,7 @@ enum State + 'static, B: MessageB pub struct H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite, - S: Service + 'static, + S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody + 'static, @@ -323,7 +330,7 @@ where impl Future for H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite, - S: Service + 'static, + S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody, diff --git a/src/lib.rs b/src/lib.rs index 74a46fd17..9d8bc50f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,8 +97,7 @@ pub use self::message::{Head, Message, RequestHead, ResponseHead}; pub use self::payload::{Payload, PayloadStream}; pub use self::request::Request; pub use self::response::Response; -pub use self::service::HttpService; -pub use self::service::{SendError, SendResponse}; +pub use self::service::{HttpService, SendError, SendResponse}; pub mod dev { //! The `actix-web` prelude for library developers diff --git a/src/service/senderror.rs b/src/service/senderror.rs index 8268c6660..44d362593 100644 --- a/src/service/senderror.rs +++ b/src/service/senderror.rs @@ -22,11 +22,12 @@ where } } -impl NewService)>> for SendError +impl NewService for SendError where T: AsyncRead + AsyncWrite, E: ResponseError, { + type Request = Result)>; type Response = R; type Error = (E, Framed); type InitError = (); @@ -38,11 +39,12 @@ where } } -impl Service)>> for SendError +impl Service for SendError where T: AsyncRead + AsyncWrite, E: ResponseError, { + type Request = Result)>; type Response = R; type Error = (E, Framed); type Future = Either)>, SendErrorFut>; @@ -128,11 +130,12 @@ where } } -impl NewService<(Response, Framed)> for SendResponse +impl NewService for SendResponse where T: AsyncRead + AsyncWrite, B: MessageBody, { + type Request = (Response, Framed); type Response = Framed; type Error = Error; type InitError = (); @@ -144,11 +147,12 @@ where } } -impl Service<(Response, Framed)> for SendResponse +impl Service for SendResponse where T: AsyncRead + AsyncWrite, B: MessageBody, { + type Request = (Response, Framed); type Response = Framed; type Error = Error; type Future = SendResponseFut; diff --git a/src/service/service.rs b/src/service/service.rs index 3d0009ed0..0fa5d6653 100644 --- a/src/service/service.rs +++ b/src/service/service.rs @@ -3,6 +3,7 @@ use std::marker::PhantomData; use std::{fmt, io, net}; use actix_codec::{AsyncRead, AsyncWrite, Framed, FramedParts}; +use actix_server_config::ServerConfig as SrvConfig; use actix_service::{IntoNewService, NewService, Service}; use actix_utils::cloneable::CloneableService; use bytes::{Buf, BufMut, Bytes, BytesMut}; @@ -27,14 +28,14 @@ pub struct HttpService { impl HttpService where - S: NewService, + S: NewService, S::Service: 'static, S::Error: Debug + 'static, S::Response: Into>, B: MessageBody + 'static, { /// Create new `HttpService` instance. - pub fn new>(service: F) -> Self { + pub fn new>(service: F) -> Self { let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0); HttpService { @@ -45,7 +46,7 @@ where } /// Create new `HttpService` instance with config. - pub fn with_config>( + pub fn with_config>( cfg: ServiceConfig, service: F, ) -> Self { @@ -62,24 +63,25 @@ where } } -impl NewService for HttpService +impl NewService for HttpService where T: AsyncRead + AsyncWrite + 'static, - S: NewService, + S: NewService, S::Service: 'static, S::Error: Debug, S::Response: Into>, B: MessageBody + 'static, { + type Request = T; type Response = (); type Error = DispatchError; type InitError = S::InitError; type Service = HttpServiceHandler; type Future = HttpServiceResponse; - fn new_service(&self, _: &()) -> Self::Future { + fn new_service(&self, cfg: &SrvConfig) -> Self::Future { HttpServiceResponse { - fut: self.srv.new_service(&()).into_future(), + fut: self.srv.new_service(cfg).into_future(), cfg: Some(self.cfg.clone()), _t: PhantomData, } @@ -102,7 +104,7 @@ pub struct HttpServiceBuilder { impl HttpServiceBuilder where - S: NewService, + S: NewService, S::Service: 'static, S::Error: Debug + 'static, { @@ -220,7 +222,7 @@ where pub fn finish(self, service: F) -> HttpService where B: MessageBody, - F: IntoNewService, + F: IntoNewService, { let cfg = ServiceConfig::new( self.keep_alive, @@ -236,7 +238,7 @@ where } #[doc(hidden)] -pub struct HttpServiceResponse, B> { +pub struct HttpServiceResponse, B> { fut: ::Future, cfg: Option, _t: PhantomData<(T, B)>, @@ -245,7 +247,7 @@ pub struct HttpServiceResponse, B> { impl Future for HttpServiceResponse where T: AsyncRead + AsyncWrite, - S: NewService, + S: NewService, S::Service: 'static, S::Response: Into>, S::Error: Debug, @@ -272,7 +274,7 @@ pub struct HttpServiceHandler { impl HttpServiceHandler where - S: Service + 'static, + S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody + 'static, @@ -286,14 +288,15 @@ where } } -impl Service for HttpServiceHandler +impl Service for HttpServiceHandler where T: AsyncRead + AsyncWrite + 'static, - S: Service + 'static, + S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody + 'static, { + type Request = T; type Response = (); type Error = DispatchError; type Future = HttpServiceHandlerResponse; @@ -317,7 +320,7 @@ where } } -enum State + 'static, B: MessageBody> +enum State + 'static, B: MessageBody> where S::Error: fmt::Debug, T: AsyncRead + AsyncWrite + 'static, @@ -331,7 +334,7 @@ where pub struct HttpServiceHandlerResponse where T: AsyncRead + AsyncWrite + 'static, - S: Service + 'static, + S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody + 'static, @@ -344,7 +347,7 @@ const HTTP2_PREFACE: [u8; 14] = *b"PRI * HTTP/2.0"; impl Future for HttpServiceHandlerResponse where T: AsyncRead + AsyncWrite, - S: Service + 'static, + S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody, diff --git a/src/ws/client/mod.rs b/src/ws/client/mod.rs index 8e59e846d..12c7229b9 100644 --- a/src/ws/client/mod.rs +++ b/src/ws/client/mod.rs @@ -25,6 +25,20 @@ impl Protocol { } } + fn is_http(self) -> bool { + match self { + Protocol::Https | Protocol::Http => true, + _ => false, + } + } + + fn is_secure(self) -> bool { + match self { + Protocol::Https | Protocol::Wss => true, + _ => false, + } + } + fn port(self) -> u16 { match self { Protocol::Http | Protocol::Ws => 80, diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index c48b6e0c1..586873d19 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -26,7 +26,7 @@ pub type DefaultClient = Client; /// WebSocket's client pub struct Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite, { connector: T, @@ -34,7 +34,7 @@ where impl Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite, { /// Create new websocket's client factory @@ -51,7 +51,7 @@ impl Default for Client { impl Clone for Client where - T: Service + Clone, + T: Service + Clone, T::Response: AsyncRead + AsyncWrite, { fn clone(&self) -> Self { @@ -61,12 +61,13 @@ where } } -impl Service for Client +impl Service for Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite + 'static, T::Future: 'static, { + type Request = Connect; type Response = Framed; type Error = ClientError; type Future = Either< diff --git a/src/ws/service.rs b/src/ws/service.rs index bbd9f7826..f3b066053 100644 --- a/src/ws/service.rs +++ b/src/ws/service.rs @@ -20,7 +20,8 @@ impl Default for VerifyWebSockets { } } -impl NewService<(Request, Framed)> for VerifyWebSockets { +impl NewService for VerifyWebSockets { + type Request = (Request, Framed); type Response = (Request, Framed); type Error = (HandshakeError, Framed); type InitError = (); @@ -32,7 +33,8 @@ impl NewService<(Request, Framed)> for VerifyWebSockets { } } -impl Service<(Request, Framed)> for VerifyWebSockets { +impl Service for VerifyWebSockets { + type Request = (Request, Framed); type Response = (Request, Framed); type Error = (HandshakeError, Framed); type Future = FutureResult; diff --git a/src/ws/transport.rs b/src/ws/transport.rs index 6a4f4d227..da7782be5 100644 --- a/src/ws/transport.rs +++ b/src/ws/transport.rs @@ -7,7 +7,7 @@ use super::{Codec, Frame, Message}; pub struct Transport where - S: Service + 'static, + S: Service + 'static, T: AsyncRead + AsyncWrite, { inner: FramedTransport, @@ -16,17 +16,17 @@ where impl Transport where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Future: 'static, S::Error: 'static, { - pub fn new>(io: T, service: F) -> Self { + pub fn new>(io: T, service: F) -> Self { Transport { inner: FramedTransport::new(Framed::new(io, Codec::new()), service), } } - pub fn with>(framed: Framed, service: F) -> Self { + pub fn with>(framed: Framed, service: F) -> Self { Transport { inner: FramedTransport::new(framed, service), } @@ -36,7 +36,7 @@ where impl Future for Transport where T: AsyncRead + AsyncWrite, - S: Service, + S: Service, S::Future: 'static, S::Error: 'static, { diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index dd7bc362a..6b70fbc10 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -56,7 +56,8 @@ impl TestServer { pub fn new( factory: F, ) -> TestServerRuntime< - impl Service + Clone, + impl Service + + Clone, > { let (tx, rx) = mpsc::channel(); @@ -88,8 +89,11 @@ impl TestServer { } fn new_connector( - ) -> impl Service + Clone - { + ) -> impl Service< + Request = Connect, + Response = impl Connection, + Error = ConnectorError, + > + Clone { #[cfg(feature = "ssl")] { use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; @@ -202,7 +206,7 @@ impl TestServerRuntime { impl TestServerRuntime where - T: Service + Clone, + T: Service + Clone, T::Response: Connection, { /// Connect to websocket server at a given path diff --git a/tests/test_server.rs b/tests/test_server.rs index 4cffdd096..b7cd5557b 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -565,17 +565,22 @@ fn test_body_chunked_implicit() { assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } +use actix_server_config::ServerConfig; +use actix_service::fn_cfg_factory; + #[test] fn test_response_http_error_handling() { let mut srv = TestServer::new(|| { - h1::H1Service::new(|_| { - let broken_header = Bytes::from_static(b"\0\0\0"); - ok::<_, ()>( - Response::Ok() - .header(http::header::CONTENT_TYPE, broken_header) - .body(STR), - ) - }) + h1::H1Service::new(fn_cfg_factory(|_: &ServerConfig| { + Ok::<_, ()>(|_| { + let broken_header = Bytes::from_static(b"\0\0\0"); + ok::<_, ()>( + Response::Ok() + .header(http::header::CONTENT_TYPE, broken_header) + .body(STR), + ) + }) + })) }); let req = srv.get().finish().unwrap(); From aadcdaa3d6b109bc169e2d083bad8817594b789f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 07:39:34 -0800 Subject: [PATCH 238/427] add resource map, it allow to check if router has resource and it allows to generate urls for named resources --- Cargo.toml | 3 + actix-files/src/lib.rs | 31 +++++++ src/app.rs | 24 +++++- src/config.rs | 17 +++- src/error.rs | 20 +++++ src/lib.rs | 4 +- src/request.rs | 46 ++++++++++ src/resource.rs | 2 +- src/rmap.rs | 188 +++++++++++++++++++++++++++++++++++++++++ src/scope.rs | 19 ++++- src/server.rs | 8 +- src/service.rs | 4 +- src/test.rs | 13 ++- 13 files changed, 361 insertions(+), 18 deletions(-) create mode 100644 src/error.rs create mode 100644 src/rmap.rs diff --git a/Cargo.toml b/Cargo.toml index f9e2266ea..5e2027c59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,11 +72,13 @@ actix-utils = { git = "https://github.com/actix/actix-net.git" } actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } +#actix-router = { path="../actix-net/router" } bytes = "0.4" derive_more = "0.14" encoding = "0.2" futures = "0.1" +hashbrown = "0.1.8" log = "0.4" lazy_static = "1.2" mime = "0.3" @@ -89,6 +91,7 @@ serde_json = "1.0" serde_urlencoded = "^0.5.3" threadpool = "1.7" time = "0.1" +url = { version="1.7", features=["query_encoding"] } # middlewares # actix-session = { path="session", optional = true } diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 5dd98dcc6..17efdd813 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -1090,4 +1090,35 @@ mod tests { // assert_eq!(response.status(), StatusCode::OK); // } + #[test] + fn test_path_buf() { + assert_eq!( + PathBuf::from_param("/test/.tt"), + Err(UriSegmentError::BadStart('.')) + ); + assert_eq!( + PathBuf::from_param("/test/*tt"), + Err(UriSegmentError::BadStart('*')) + ); + assert_eq!( + PathBuf::from_param("/test/tt:"), + Err(UriSegmentError::BadEnd(':')) + ); + assert_eq!( + PathBuf::from_param("/test/tt<"), + Err(UriSegmentError::BadEnd('<')) + ); + assert_eq!( + PathBuf::from_param("/test/tt>"), + Err(UriSegmentError::BadEnd('>')) + ); + assert_eq!( + PathBuf::from_param("/seg1/seg2/"), + Ok(PathBuf::from_iter(vec!["seg1", "seg2"])) + ); + assert_eq!( + PathBuf::from_param("/seg1/../seg2/"), + Ok(PathBuf::from_iter(vec!["seg2"])) + ); + } } diff --git a/src/app.rs b/src/app.rs index c1c019a3c..b4f6e5352 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,6 +16,7 @@ use futures::{Async, Future, IntoFuture, Poll}; use crate::config::AppConfig; use crate::guard::Guard; use crate::resource::Resource; +use crate::rmap::ResourceMap; use crate::route::Route; use crate::service::{ HttpServiceFactory, ServiceFactory, ServiceFactoryWrapper, ServiceRequest, @@ -449,19 +450,29 @@ where .into_iter() .for_each(|mut srv| srv.register(&mut config)); - // set factory + let mut rmap = ResourceMap::new(ResourceDef::new("")); + + // complete pipeline creation *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { default, services: Rc::new( config .into_services() .into_iter() - .map(|(rdef, srv, guards)| (rdef, srv, RefCell::new(guards))) + .map(|(mut rdef, srv, guards, nested)| { + rmap.add(&mut rdef, nested); + (rdef, srv, RefCell::new(guards)) + }) .collect(), ), }); + // complete ResourceMap tree creation + let rmap = Rc::new(rmap); + rmap.finish(rmap.clone()); + AppInit { + rmap, chain: self.chain, state: self.state, extensions: Rc::new(RefCell::new(Rc::new(self.extensions))), @@ -561,8 +572,7 @@ impl

    Future for AppRoutingFactoryResponse

    { .fold(Router::build(), |mut router, item| { match item { CreateAppRoutingItem::Service(path, guards, service) => { - router.rdef(path, service); - router.set_user_data(guards); + router.rdef(path, service).2 = guards; } CreateAppRoutingItem::Future(_, _, _) => unreachable!(), } @@ -683,6 +693,7 @@ where C: NewService>, { chain: C, + rmap: Rc, state: Vec>, extensions: Rc>>, } @@ -702,6 +713,7 @@ where chain: self.chain.new_service(&()), state: self.state.iter().map(|s| s.construct()).collect(), extensions: self.extensions.clone(), + rmap: self.rmap.clone(), } } } @@ -712,6 +724,7 @@ where C: NewService, InitError = ()>, { chain: C::Future, + rmap: Rc, state: Vec>, extensions: Rc>>, } @@ -744,6 +757,7 @@ where Ok(Async::Ready(AppInitService { chain, + rmap: self.rmap.clone(), extensions: self.extensions.borrow().clone(), })) } @@ -755,6 +769,7 @@ where C: Service>, { chain: C, + rmap: Rc, extensions: Rc, } @@ -774,6 +789,7 @@ where let req = ServiceRequest::new( Path::new(Url::new(req.uri().clone())), req, + self.rmap.clone(), self.extensions.clone(), ); self.chain.call(req) diff --git a/src/config.rs b/src/config.rs index 483b0a508..4afd213cc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,7 @@ use actix_router::ResourceDef; use actix_service::{boxed, IntoNewService, NewService}; use crate::guard::Guard; +use crate::rmap::ResourceMap; use crate::service::{ServiceRequest, ServiceResponse}; type Guards = Vec>; @@ -18,7 +19,12 @@ pub struct AppConfig

    { host: String, root: bool, default: Rc>, - services: Vec<(ResourceDef, HttpNewService

    , Option)>, + services: Vec<( + ResourceDef, + HttpNewService

    , + Option, + Option>, + )>, } impl AppConfig

    { @@ -46,7 +52,12 @@ impl AppConfig

    { pub(crate) fn into_services( self, - ) -> Vec<(ResourceDef, HttpNewService

    , Option)> { + ) -> Vec<( + ResourceDef, + HttpNewService

    , + Option, + Option>, + )> { self.services } @@ -85,6 +96,7 @@ impl AppConfig

    { rdef: ResourceDef, guards: Option>>, service: F, + nested: Option>, ) where F: IntoNewService>, S: NewService< @@ -98,6 +110,7 @@ impl AppConfig

    { rdef, boxed::new_service(service.into_new_service()), guards, + nested, )); } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 000000000..d1c0d3ca9 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,20 @@ +pub use actix_http::error::*; +use derive_more::{Display, From}; +use url::ParseError as UrlParseError; + +/// Errors which can occur when attempting to generate resource uri. +#[derive(Debug, PartialEq, Display, From)] +pub enum UrlGenerationError { + /// Resource not found + #[display(fmt = "Resource not found")] + ResourceNotFound, + /// Not all path pattern covered + #[display(fmt = "Not all path pattern covered")] + NotEnoughElements, + /// URL parse error + #[display(fmt = "{}", _0)] + ParseError(UrlParseError), +} + +/// `InternalServerError` for `UrlGeneratorError` +impl ResponseError for UrlGenerationError {} diff --git a/src/lib.rs b/src/lib.rs index a61387e81..94bf1ba76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,11 +6,13 @@ mod handler; // mod info; pub mod blocking; mod config; +pub mod error; pub mod guard; pub mod middleware; mod request; mod resource; mod responder; +mod rmap; mod route; mod scope; mod server; @@ -27,7 +29,7 @@ pub use actix_web_codegen::*; // re-export for convenience pub use actix_http::Response as HttpResponse; -pub use actix_http::{error, http, Error, HttpMessage, ResponseError, Result}; +pub use actix_http::{http, Error, HttpMessage, ResponseError, Result}; pub use crate::app::App; pub use crate::extract::FromRequest; diff --git a/src/request.rs b/src/request.rs index 1c86cac35..6655f1baa 100644 --- a/src/request.rs +++ b/src/request.rs @@ -7,7 +7,9 @@ use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; use actix_router::{Path, Url}; +use crate::error::UrlGenerationError; use crate::extract::FromRequest; +use crate::rmap::ResourceMap; use crate::service::ServiceFromRequest; #[derive(Clone)] @@ -15,6 +17,7 @@ use crate::service::ServiceFromRequest; pub struct HttpRequest { pub(crate) head: Message, pub(crate) path: Path, + rmap: Rc, extensions: Rc, } @@ -23,11 +26,13 @@ impl HttpRequest { pub(crate) fn new( head: Message, path: Path, + rmap: Rc, extensions: Rc, ) -> HttpRequest { HttpRequest { head, path, + rmap, extensions, } } @@ -93,6 +98,47 @@ impl HttpRequest { &self.extensions } + /// Generate url for named resource + /// + /// ```rust + /// # extern crate actix_web; + /// # use actix_web::{App, HttpRequest, HttpResponse, http}; + /// # + /// fn index(req: HttpRequest) -> HttpResponse { + /// let url = req.url_for("foo", &["1", "2", "3"]); // <- generate url for "foo" resource + /// HttpResponse::Ok().into() + /// } + /// + /// fn main() { + /// let app = App::new() + /// .resource("/test/{one}/{two}/{three}", |r| { + /// r.name("foo"); // <- set resource name, then it could be used in `url_for` + /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); + /// }) + /// .finish(); + /// } + /// ``` + pub fn url_for( + &self, + name: &str, + elements: U, + ) -> Result + where + U: IntoIterator, + I: AsRef, + { + self.rmap.url_for(&self, name, elements) + } + + /// Generate url for named resource + /// + /// This method is similar to `HttpRequest::url_for()` but it can be used + /// for urls that do not contain variable parts. + pub fn url_for_static(&self, name: &str) -> Result { + const NO_PARAMS: [&str; 0] = []; + self.url_for(name, &NO_PARAMS) + } + // /// Get *ConnectionInfo* for the correct request. // #[inline] // pub fn connection_info(&self) -> Ref { diff --git a/src/resource.rs b/src/resource.rs index 13afff70c..a1177ca0a 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -288,7 +288,7 @@ where } else { ResourceDef::new(&self.rdef) }; - config.register_service(rdef, guards, self) + config.register_service(rdef, guards, self, None) } } diff --git a/src/rmap.rs b/src/rmap.rs new file mode 100644 index 000000000..4922084bf --- /dev/null +++ b/src/rmap.rs @@ -0,0 +1,188 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use actix_router::ResourceDef; +use hashbrown::HashMap; +use url::Url; + +use crate::error::UrlGenerationError; +use crate::request::HttpRequest; + +#[derive(Clone, Debug)] +pub struct ResourceMap { + root: ResourceDef, + parent: RefCell>>, + named: HashMap, + patterns: Vec<(ResourceDef, Option>)>, +} + +impl ResourceMap { + pub fn new(root: ResourceDef) -> Self { + ResourceMap { + root, + parent: RefCell::new(None), + named: HashMap::new(), + patterns: Vec::new(), + } + } + + pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option>) { + pattern.set_id(self.patterns.len() as u16); + self.patterns.push((pattern.clone(), nested)); + if !pattern.name().is_empty() { + self.named + .insert(pattern.name().to_string(), pattern.clone()); + } + } + + pub(crate) fn finish(&self, current: Rc) { + for (_, nested) in &self.patterns { + if let Some(ref nested) = nested { + *nested.parent.borrow_mut() = Some(current.clone()) + } + } + } +} + +impl ResourceMap { + /// Generate url for named resource + /// + /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. + /// url_for) for detailed information. + pub fn url_for( + &self, + req: &HttpRequest, + name: &str, + elements: U, + ) -> Result + where + U: IntoIterator, + I: AsRef, + { + let mut path = String::new(); + let mut elements = elements.into_iter(); + + if self.patterns_for(name, &mut path, &mut elements)?.is_some() { + if path.starts_with('/') { + // let conn = req.connection_info(); + // Ok(Url::parse(&format!( + // "{}://{}{}", + // conn.scheme(), + // conn.host(), + // path + // ))?) + unimplemented!() + } else { + Ok(Url::parse(&path)?) + } + } else { + Err(UrlGenerationError::ResourceNotFound) + } + } + + pub fn has_resource(&self, path: &str) -> bool { + let path = if path.is_empty() { "/" } else { path }; + + for (pattern, rmap) in &self.patterns { + if let Some(ref rmap) = rmap { + if let Some(plen) = pattern.is_prefix_match(path) { + return rmap.has_resource(&path[plen..]); + } + } else if pattern.is_match(path) { + return true; + } + } + false + } + + fn patterns_for( + &self, + name: &str, + path: &mut String, + elements: &mut U, + ) -> Result, UrlGenerationError> + where + U: Iterator, + I: AsRef, + { + if self.pattern_for(name, path, elements)?.is_some() { + Ok(Some(())) + } else { + self.parent_pattern_for(name, path, elements) + } + } + + fn pattern_for( + &self, + name: &str, + path: &mut String, + elements: &mut U, + ) -> Result, UrlGenerationError> + where + U: Iterator, + I: AsRef, + { + if let Some(pattern) = self.named.get(name) { + self.fill_root(path, elements)?; + if pattern.resource_path(path, elements) { + Ok(Some(())) + } else { + Err(UrlGenerationError::NotEnoughElements) + } + } else { + for (_, rmap) in &self.patterns { + if let Some(ref rmap) = rmap { + if rmap.pattern_for(name, path, elements)?.is_some() { + return Ok(Some(())); + } + } + } + Ok(None) + } + } + + fn fill_root( + &self, + path: &mut String, + elements: &mut U, + ) -> Result<(), UrlGenerationError> + where + U: Iterator, + I: AsRef, + { + if let Some(ref parent) = *self.parent.borrow() { + parent.fill_root(path, elements)?; + } + if self.root.resource_path(path, elements) { + Ok(()) + } else { + Err(UrlGenerationError::NotEnoughElements) + } + } + + fn parent_pattern_for( + &self, + name: &str, + path: &mut String, + elements: &mut U, + ) -> Result, UrlGenerationError> + where + U: Iterator, + I: AsRef, + { + if let Some(ref parent) = *self.parent.borrow() { + if let Some(pattern) = parent.named.get(name) { + self.fill_root(path, elements)?; + if pattern.resource_path(path, elements) { + Ok(Some(())) + } else { + Err(UrlGenerationError::NotEnoughElements) + } + } else { + parent.parent_pattern_for(name, path, elements) + } + } else { + Ok(None) + } + } +} diff --git a/src/scope.rs b/src/scope.rs index 2c2e3c2ba..15c652c8f 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -13,6 +13,7 @@ use futures::{Async, Poll}; use crate::dev::{AppConfig, HttpServiceFactory}; use crate::guard::Guard; use crate::resource::Resource; +use crate::rmap::ResourceMap; use crate::route::Route; use crate::service::{ ServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, @@ -237,35 +238,46 @@ where > + 'static, { fn register(self, config: &mut AppConfig

    ) { + // update default resource if needed if self.default.borrow().is_none() { *self.default.borrow_mut() = Some(config.default_service()); } - // register services + // register nested services let mut cfg = config.clone_config(); self.services .into_iter() .for_each(|mut srv| srv.register(&mut cfg)); + let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef)); + + // complete scope pipeline creation *self.factory_ref.borrow_mut() = Some(ScopeFactory { default: self.default.clone(), services: Rc::new( cfg.into_services() .into_iter() - .map(|(rdef, srv, guards)| (rdef, srv, RefCell::new(guards))) + .map(|(mut rdef, srv, guards, nested)| { + rmap.add(&mut rdef, nested); + (rdef, srv, RefCell::new(guards)) + }) .collect(), ), }); + // get guards let guards = if self.guards.is_empty() { None } else { Some(self.guards) }; + + // register final service config.register_service( ResourceDef::root_prefix(&self.rdef), guards, self.endpoint, + Some(Rc::new(rmap)), ) } } @@ -367,8 +379,7 @@ impl

    Future for ScopeFactoryResponse

    { .fold(Router::build(), |mut router, item| { match item { CreateScopeServiceItem::Service(path, guards, service) => { - router.rdef(path, service); - router.set_user_data(guards); + router.rdef(path, service).2 = guards; } CreateScopeServiceItem::Future(_, _, _) => unreachable!(), } diff --git a/src/server.rs b/src/server.rs index d6d88d000..d3ae5b2b5 100644 --- a/src/server.rs +++ b/src/server.rs @@ -230,7 +230,7 @@ where /// /// HttpServer does not change any configuration for TcpListener, /// it needs to be configured before passing it to listen() method. - pub fn listen(mut self, lst: net::TcpListener) -> Self { + pub fn listen(mut self, lst: net::TcpListener) -> io::Result { let cfg = self.config.clone(); let factory = self.factory.clone(); let addr = lst.local_addr().unwrap(); @@ -248,9 +248,9 @@ where ServiceConfig::new(c.keep_alive, c.client_timeout, 0); HttpService::with_config(service_config, factory()) }, - )); + )?); - self + Ok(self) } #[cfg(feature = "tls")] @@ -328,7 +328,7 @@ where let sockets = self.bind2(addr)?; for lst in sockets { - self = self.listen(lst); + self = self.listen(lst)?; } Ok(self) diff --git a/src/service.rs b/src/service.rs index e2213060c..ba8114588 100644 --- a/src/service.rs +++ b/src/service.rs @@ -15,6 +15,7 @@ use futures::future::{ok, FutureResult, IntoFuture}; use crate::config::AppConfig; use crate::request::HttpRequest; +use crate::rmap::ResourceMap; pub trait HttpServiceFactory

    { fn register(self, config: &mut AppConfig

    ); @@ -58,12 +59,13 @@ impl

    ServiceRequest

    { pub(crate) fn new( path: Path, request: Request

    , + rmap: Rc, extensions: Rc, ) -> Self { let (head, payload) = request.into_parts(); ServiceRequest { payload, - req: HttpRequest::new(head, path, extensions), + req: HttpRequest::new(head, path, rmap, extensions), } } diff --git a/src/test.rs b/src/test.rs index ccc4b38ec..e4cdefbe8 100644 --- a/src/test.rs +++ b/src/test.rs @@ -6,13 +6,14 @@ use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, Version}; use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{Extensions, PayloadStream, Request}; -use actix_router::{Path, Url}; +use actix_router::{Path, ResourceDef, Url}; use actix_rt::Runtime; use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; use futures::Future; use crate::request::HttpRequest; +use crate::rmap::ResourceMap; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; thread_local! { @@ -135,6 +136,7 @@ where pub struct TestRequest { req: HttpTestRequest, extensions: Extensions, + rmap: ResourceMap, } impl Default for TestRequest { @@ -142,6 +144,7 @@ impl Default for TestRequest { TestRequest { req: HttpTestRequest::default(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } } @@ -152,6 +155,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().uri(path).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -160,6 +164,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().set(hdr).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -172,6 +177,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().header(key, value).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -180,6 +186,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().method(Method::GET).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -188,6 +195,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().method(Method::POST).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -244,6 +252,7 @@ impl TestRequest { ServiceRequest::new( Path::new(Url::new(req.uri().clone())), req, + Rc::new(self.rmap), Rc::new(self.extensions), ) } @@ -260,6 +269,7 @@ impl TestRequest { ServiceRequest::new( Path::new(Url::new(req.uri().clone())), req, + Rc::new(self.rmap), Rc::new(self.extensions), ) .into_request() @@ -272,6 +282,7 @@ impl TestRequest { let req = ServiceRequest::new( Path::new(Url::new(req.uri().clone())), req, + Rc::new(self.rmap), Rc::new(self.extensions), ); ServiceFromRequest::new(req, None) From fde55ffa14884e045590efb7135171f722cbde1d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 09:49:11 -0800 Subject: [PATCH 239/427] revert generic request parameter for service; support ServerConfig as new factory config --- Cargo.toml | 2 +- src/app.rs | 419 +++-------------------------- src/app_service.rs | 439 +++++++++++++++++++++++++++++++ src/config.rs | 4 +- src/handler.rs | 18 +- src/lib.rs | 1 + src/middleware/compress.rs | 17 +- src/middleware/defaultheaders.rs | 10 +- src/middleware/logger.rs | 18 +- src/resource.rs | 32 ++- src/route.rs | 30 ++- src/scope.rs | 21 +- src/server.rs | 13 +- src/test.rs | 17 +- 14 files changed, 581 insertions(+), 460 deletions(-) create mode 100644 src/app_service.rs diff --git a/Cargo.toml b/Cargo.toml index 5e2027c59..dbc0a65d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ actix-utils = { git = "https://github.com/actix/actix-net.git" } actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } -#actix-router = { path="../actix-net/router" } +actix-server-config = { git = "https://github.com/actix/actix-net.git" } bytes = "0.4" derive_more = "0.14" diff --git a/src/app.rs b/src/app.rs index b4f6e5352..76a3a1ce4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,37 +3,30 @@ use std::marker::PhantomData; use std::rc::Rc; use actix_http::body::{Body, MessageBody}; -use actix_http::{Extensions, PayloadStream, Request, Response}; -use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; -use actix_service::boxed::{self, BoxedNewService, BoxedService}; +use actix_http::{Extensions, PayloadStream}; +use actix_server_config::ServerConfig; +use actix_service::boxed::{self, BoxedNewService}; use actix_service::{ - fn_service, AndThenNewService, ApplyTransform, IntoNewService, IntoTransform, - NewService, Service, Transform, + ApplyTransform, IntoNewService, IntoTransform, NewService, Transform, }; -use futures::future::{ok, Either, FutureResult}; -use futures::{Async, Future, IntoFuture, Poll}; +use futures::IntoFuture; -use crate::config::AppConfig; -use crate::guard::Guard; +use crate::app_service::{AppChain, AppEntry, AppInit, AppRouting, AppRoutingFactory}; use crate::resource::Resource; -use crate::rmap::ResourceMap; use crate::route::Route; use crate::service::{ HttpServiceFactory, ServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, }; -use crate::state::{State, StateFactory, StateFactoryResult}; +use crate::state::{State, StateFactory}; -type Guards = Vec>; -type HttpService

    = BoxedService, ServiceResponse, ()>; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; -type BoxedResponse = Box>; /// Application builder - structure that follows the builder pattern /// for building application instances. pub struct App where - T: NewService>, + T: NewService>, { chain: T, extensions: Extensions, @@ -58,7 +51,7 @@ impl App where P: 'static, T: NewService< - ServiceRequest, + Request = ServiceRequest, Response = ServiceRequest

    , Error = (), InitError = (), @@ -121,7 +114,7 @@ where P, B, impl NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -130,12 +123,12 @@ where where M: Transform< AppRouting

    , - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoTransform, ServiceRequest

    >, + F: IntoTransform>, { let fref = Rc::new(RefCell::new(None)); let endpoint = ApplyTransform::new(mw, AppEntry::new(fref.clone())); @@ -159,7 +152,7 @@ where ) -> App< P1, impl NewService< - ServiceRequest, + Request = ServiceRequest, Response = ServiceRequest, Error = (), InitError = (), @@ -167,12 +160,12 @@ where > where C: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceRequest, Error = (), InitError = (), >, - F: IntoNewService>, + F: IntoNewService, { let chain = self.chain.and_then(chain.into_new_service()); App { @@ -264,7 +257,7 @@ where P: 'static, B: MessageBody, T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -324,7 +317,7 @@ where P, B1, impl NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -333,13 +326,13 @@ where where M: Transform< T::Service, - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, B1: MessageBody, - F: IntoTransform>, + F: IntoTransform, { let endpoint = ApplyTransform::new(mw, self.endpoint); AppRouter { @@ -362,7 +355,7 @@ where where F: FnOnce(Resource

    ) -> Resource, U: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -413,391 +406,39 @@ where } } -impl - IntoNewService, T>, Request> +impl IntoNewService, ServerConfig> for AppRouter where T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, C: NewService< - ServiceRequest, + Request = ServiceRequest, Response = ServiceRequest

    , Error = (), InitError = (), >, { - fn into_new_service(self) -> AndThenNewService, T> { - // update resource default service - let default = self.default.unwrap_or_else(|| { - Rc::new(boxed::new_service(fn_service(|req: ServiceRequest

    | { - Ok(req.into_response(Response::NotFound().finish())) - }))) - }); - - let mut config = AppConfig::new( - "127.0.0.1:8080".parse().unwrap(), - "localhost:8080".to_owned(), - false, - default.clone(), - ); - - // register services - self.services - .into_iter() - .for_each(|mut srv| srv.register(&mut config)); - - let mut rmap = ResourceMap::new(ResourceDef::new("")); - - // complete pipeline creation - *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { - default, - services: Rc::new( - config - .into_services() - .into_iter() - .map(|(mut rdef, srv, guards, nested)| { - rmap.add(&mut rdef, nested); - (rdef, srv, RefCell::new(guards)) - }) - .collect(), - ), - }); - - // complete ResourceMap tree creation - let rmap = Rc::new(rmap); - rmap.finish(rmap.clone()); - + fn into_new_service(self) -> AppInit { AppInit { - rmap, chain: self.chain, state: self.state, + endpoint: self.endpoint, + services: RefCell::new(self.services), + default: self.default, + factory_ref: self.factory_ref, extensions: Rc::new(RefCell::new(Rc::new(self.extensions))), } - .and_then(self.endpoint) - } -} - -pub struct AppRoutingFactory

    { - services: Rc, RefCell>)>>, - default: Rc>, -} - -impl NewService> for AppRoutingFactory

    { - type Response = ServiceResponse; - type Error = (); - type InitError = (); - type Service = AppRouting

    ; - type Future = AppRoutingFactoryResponse

    ; - - fn new_service(&self, _: &()) -> Self::Future { - AppRoutingFactoryResponse { - fut: self - .services - .iter() - .map(|(path, service, guards)| { - CreateAppRoutingItem::Future( - Some(path.clone()), - guards.borrow_mut().take(), - service.new_service(&()), - ) - }) - .collect(), - default: None, - default_fut: Some(self.default.new_service(&())), - } - } -} - -type HttpServiceFut

    = Box, Error = ()>>; - -/// Create app service -#[doc(hidden)] -pub struct AppRoutingFactoryResponse

    { - fut: Vec>, - default: Option>, - default_fut: Option, Error = ()>>>, -} - -enum CreateAppRoutingItem

    { - Future(Option, Option, HttpServiceFut

    ), - Service(ResourceDef, Option, HttpService

    ), -} - -impl

    Future for AppRoutingFactoryResponse

    { - type Item = AppRouting

    ; - type Error = (); - - fn poll(&mut self) -> Poll { - let mut done = true; - - if let Some(ref mut fut) = self.default_fut { - match fut.poll()? { - Async::Ready(default) => self.default = Some(default), - Async::NotReady => done = false, - } - } - - // poll http services - for item in &mut self.fut { - let res = match item { - CreateAppRoutingItem::Future( - ref mut path, - ref mut guards, - ref mut fut, - ) => match fut.poll()? { - Async::Ready(service) => { - Some((path.take().unwrap(), guards.take(), service)) - } - Async::NotReady => { - done = false; - None - } - }, - CreateAppRoutingItem::Service(_, _, _) => continue, - }; - - if let Some((path, guards, service)) = res { - *item = CreateAppRoutingItem::Service(path, guards, service); - } - } - - if done { - let router = self - .fut - .drain(..) - .fold(Router::build(), |mut router, item| { - match item { - CreateAppRoutingItem::Service(path, guards, service) => { - router.rdef(path, service).2 = guards; - } - CreateAppRoutingItem::Future(_, _, _) => unreachable!(), - } - router - }); - Ok(Async::Ready(AppRouting { - ready: None, - router: router.finish(), - default: self.default.take(), - })) - } else { - Ok(Async::NotReady) - } - } -} - -pub struct AppRouting

    { - router: Router, Guards>, - ready: Option<(ServiceRequest

    , ResourceInfo)>, - default: Option>, -} - -impl

    Service> for AppRouting

    { - type Response = ServiceResponse; - type Error = (); - type Future = Either>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - if self.ready.is_none() { - Ok(Async::Ready(())) - } else { - Ok(Async::NotReady) - } - } - - fn call(&mut self, mut req: ServiceRequest

    ) -> Self::Future { - let res = self.router.recognize_mut_checked(&mut req, |req, guards| { - if let Some(ref guards) = guards { - for f in guards { - if !f.check(req.head()) { - return false; - } - } - } - true - }); - - if let Some((srv, _info)) = res { - Either::A(srv.call(req)) - } else if let Some(ref mut default) = self.default { - Either::A(default.call(req)) - } else { - let req = req.into_request(); - Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) - } - } -} - -#[doc(hidden)] -/// Wrapper service for routing -pub struct AppEntry

    { - factory: Rc>>>, -} - -impl

    AppEntry

    { - fn new(factory: Rc>>>) -> Self { - AppEntry { factory } - } -} - -impl NewService> for AppEntry

    { - type Response = ServiceResponse; - type Error = (); - type InitError = (); - type Service = AppRouting

    ; - type Future = AppRoutingFactoryResponse

    ; - - fn new_service(&self, _: &()) -> Self::Future { - self.factory.borrow_mut().as_mut().unwrap().new_service(&()) - } -} - -#[doc(hidden)] -pub struct AppChain; - -impl NewService for AppChain { - type Response = ServiceRequest; - type Error = (); - type InitError = (); - type Service = AppChain; - type Future = FutureResult; - - fn new_service(&self, _: &()) -> Self::Future { - ok(AppChain) - } -} - -impl Service for AppChain { - type Response = ServiceRequest; - type Error = (); - type Future = FutureResult; - - #[inline] - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - #[inline] - fn call(&mut self, req: ServiceRequest) -> Self::Future { - ok(req) - } -} - -/// Service factory to convert `Request` to a `ServiceRequest`. -/// It also executes state factories. -pub struct AppInit -where - C: NewService>, -{ - chain: C, - rmap: Rc, - state: Vec>, - extensions: Rc>>, -} - -impl NewService for AppInit -where - C: NewService, InitError = ()>, -{ - type Response = ServiceRequest

    ; - type Error = C::Error; - type InitError = C::InitError; - type Service = AppInitService; - type Future = AppInitResult; - - fn new_service(&self, _: &()) -> Self::Future { - AppInitResult { - chain: self.chain.new_service(&()), - state: self.state.iter().map(|s| s.construct()).collect(), - extensions: self.extensions.clone(), - rmap: self.rmap.clone(), - } - } -} - -#[doc(hidden)] -pub struct AppInitResult -where - C: NewService, InitError = ()>, -{ - chain: C::Future, - rmap: Rc, - state: Vec>, - extensions: Rc>>, -} - -impl Future for AppInitResult -where - C: NewService, InitError = ()>, -{ - type Item = AppInitService; - type Error = C::InitError; - - fn poll(&mut self) -> Poll { - if let Some(extensions) = Rc::get_mut(&mut *self.extensions.borrow_mut()) { - let mut idx = 0; - while idx < self.state.len() { - if let Async::Ready(_) = self.state[idx].poll_result(extensions)? { - self.state.remove(idx); - } else { - idx += 1; - } - } - if !self.state.is_empty() { - return Ok(Async::NotReady); - } - } else { - log::warn!("Multiple copies of app extensions exists"); - } - - let chain = futures::try_ready!(self.chain.poll()); - - Ok(Async::Ready(AppInitService { - chain, - rmap: self.rmap.clone(), - extensions: self.extensions.borrow().clone(), - })) - } -} - -/// Service to convert `Request` to a `ServiceRequest` -pub struct AppInitService -where - C: Service>, -{ - chain: C, - rmap: Rc, - extensions: Rc, -} - -impl Service for AppInitService -where - C: Service>, -{ - type Response = ServiceRequest

    ; - type Error = C::Error; - type Future = C::Future; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.chain.poll_ready() - } - - fn call(&mut self, req: Request) -> Self::Future { - let req = ServiceRequest::new( - Path::new(Url::new(req.uri().clone())), - req, - self.rmap.clone(), - self.extensions.clone(), - ); - self.chain.call(req) } } #[cfg(test)] mod tests { + use actix_service::Service; + use super::*; use crate::http::{Method, StatusCode}; use crate::test::{block_on, init_service, TestRequest}; diff --git a/src/app_service.rs b/src/app_service.rs new file mode 100644 index 000000000..094486d9b --- /dev/null +++ b/src/app_service.rs @@ -0,0 +1,439 @@ +use std::cell::RefCell; +use std::marker::PhantomData; +use std::rc::Rc; + +use actix_http::{Extensions, Request, Response}; +use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; +use actix_server_config::ServerConfig; +use actix_service::boxed::{self, BoxedNewService, BoxedService}; +use actix_service::{fn_service, AndThen, NewService, Service, ServiceExt}; +use futures::future::{ok, Either, FutureResult}; +use futures::{Async, Future, Poll}; + +use crate::config::AppConfig; +use crate::guard::Guard; +use crate::rmap::ResourceMap; +use crate::service::{ServiceFactory, ServiceRequest, ServiceResponse}; +use crate::state::{StateFactory, StateFactoryResult}; + +type Guards = Vec>; +type HttpService

    = BoxedService, ServiceResponse, ()>; +type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; +type BoxedResponse = Box>; + +/// Service factory to convert `Request` to a `ServiceRequest`. +/// It also executes state factories. +pub struct AppInit +where + C: NewService>, + T: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + pub(crate) chain: C, + pub(crate) endpoint: T, + pub(crate) state: Vec>, + pub(crate) extensions: Rc>>, + pub(crate) services: RefCell>>>, + pub(crate) default: Option>>, + pub(crate) factory_ref: Rc>>>, +} + +impl NewService for AppInit +where + C: NewService< + Request = ServiceRequest, + Response = ServiceRequest

    , + Error = (), + InitError = (), + >, + T: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + type Request = Request; + type Response = ServiceResponse; + type Error = C::Error; + type InitError = C::InitError; + type Service = AndThen, T::Service>; + type Future = AppInitResult; + + fn new_service(&self, _: &ServerConfig) -> Self::Future { + // update resource default service + let default = self.default.clone().unwrap_or_else(|| { + Rc::new(boxed::new_service(fn_service(|req: ServiceRequest

    | { + Ok(req.into_response(Response::NotFound().finish())) + }))) + }); + + let mut config = AppConfig::new( + "127.0.0.1:8080".parse().unwrap(), + "localhost:8080".to_owned(), + false, + default.clone(), + ); + + // register services + std::mem::replace(&mut *self.services.borrow_mut(), Vec::new()) + .into_iter() + .for_each(|mut srv| srv.register(&mut config)); + + let mut rmap = ResourceMap::new(ResourceDef::new("")); + + // complete pipeline creation + *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { + default, + services: Rc::new( + config + .into_services() + .into_iter() + .map(|(mut rdef, srv, guards, nested)| { + rmap.add(&mut rdef, nested); + (rdef, srv, RefCell::new(guards)) + }) + .collect(), + ), + }); + + // complete ResourceMap tree creation + let rmap = Rc::new(rmap); + rmap.finish(rmap.clone()); + + AppInitResult { + chain: None, + chain_fut: self.chain.new_service(&()), + endpoint: None, + endpoint_fut: self.endpoint.new_service(&()), + state: self.state.iter().map(|s| s.construct()).collect(), + extensions: self.extensions.clone(), + rmap, + _t: PhantomData, + } + } +} + +pub struct AppInitResult +where + C: NewService, + T: NewService, +{ + chain: Option, + endpoint: Option, + chain_fut: C::Future, + endpoint_fut: T::Future, + rmap: Rc, + state: Vec>, + extensions: Rc>>, + _t: PhantomData<(P, B)>, +} + +impl Future for AppInitResult +where + C: NewService< + Request = ServiceRequest, + Response = ServiceRequest

    , + Error = (), + InitError = (), + >, + T: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + type Item = AndThen, T::Service>; + type Error = C::InitError; + + fn poll(&mut self) -> Poll { + if let Some(extensions) = Rc::get_mut(&mut *self.extensions.borrow_mut()) { + let mut idx = 0; + while idx < self.state.len() { + if let Async::Ready(_) = self.state[idx].poll_result(extensions)? { + self.state.remove(idx); + } else { + idx += 1; + } + } + if !self.state.is_empty() { + return Ok(Async::NotReady); + } + } else { + log::warn!("Multiple copies of app extensions exists"); + } + + if self.chain.is_none() { + if let Async::Ready(srv) = self.chain_fut.poll()? { + self.chain = Some(srv); + } + } + + if self.endpoint.is_none() { + if let Async::Ready(srv) = self.endpoint_fut.poll()? { + self.endpoint = Some(srv); + } + } + + if self.chain.is_some() && self.endpoint.is_some() { + Ok(Async::Ready( + AppInitService { + chain: self.chain.take().unwrap(), + rmap: self.rmap.clone(), + extensions: self.extensions.borrow().clone(), + } + .and_then(self.endpoint.take().unwrap()), + )) + } else { + Ok(Async::NotReady) + } + } +} + +/// Service to convert `Request` to a `ServiceRequest` +pub struct AppInitService +where + C: Service, Error = ()>, +{ + chain: C, + rmap: Rc, + extensions: Rc, +} + +impl Service for AppInitService +where + C: Service, Error = ()>, +{ + type Request = Request; + type Response = ServiceRequest

    ; + type Error = C::Error; + type Future = C::Future; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.chain.poll_ready() + } + + fn call(&mut self, req: Request) -> Self::Future { + let req = ServiceRequest::new( + Path::new(Url::new(req.uri().clone())), + req, + self.rmap.clone(), + self.extensions.clone(), + ); + self.chain.call(req) + } +} + +pub struct AppRoutingFactory

    { + services: Rc, RefCell>)>>, + default: Rc>, +} + +impl NewService for AppRoutingFactory

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = AppRouting

    ; + type Future = AppRoutingFactoryResponse

    ; + + fn new_service(&self, _: &()) -> Self::Future { + AppRoutingFactoryResponse { + fut: self + .services + .iter() + .map(|(path, service, guards)| { + CreateAppRoutingItem::Future( + Some(path.clone()), + guards.borrow_mut().take(), + service.new_service(&()), + ) + }) + .collect(), + default: None, + default_fut: Some(self.default.new_service(&())), + } + } +} + +type HttpServiceFut

    = Box, Error = ()>>; + +/// Create app service +#[doc(hidden)] +pub struct AppRoutingFactoryResponse

    { + fut: Vec>, + default: Option>, + default_fut: Option, Error = ()>>>, +} + +enum CreateAppRoutingItem

    { + Future(Option, Option, HttpServiceFut

    ), + Service(ResourceDef, Option, HttpService

    ), +} + +impl

    Future for AppRoutingFactoryResponse

    { + type Item = AppRouting

    ; + type Error = (); + + fn poll(&mut self) -> Poll { + let mut done = true; + + if let Some(ref mut fut) = self.default_fut { + match fut.poll()? { + Async::Ready(default) => self.default = Some(default), + Async::NotReady => done = false, + } + } + + // poll http services + for item in &mut self.fut { + let res = match item { + CreateAppRoutingItem::Future( + ref mut path, + ref mut guards, + ref mut fut, + ) => match fut.poll()? { + Async::Ready(service) => { + Some((path.take().unwrap(), guards.take(), service)) + } + Async::NotReady => { + done = false; + None + } + }, + CreateAppRoutingItem::Service(_, _, _) => continue, + }; + + if let Some((path, guards, service)) = res { + *item = CreateAppRoutingItem::Service(path, guards, service); + } + } + + if done { + let router = self + .fut + .drain(..) + .fold(Router::build(), |mut router, item| { + match item { + CreateAppRoutingItem::Service(path, guards, service) => { + router.rdef(path, service).2 = guards; + } + CreateAppRoutingItem::Future(_, _, _) => unreachable!(), + } + router + }); + Ok(Async::Ready(AppRouting { + ready: None, + router: router.finish(), + default: self.default.take(), + })) + } else { + Ok(Async::NotReady) + } + } +} + +pub struct AppRouting

    { + router: Router, Guards>, + ready: Option<(ServiceRequest

    , ResourceInfo)>, + default: Option>, +} + +impl

    Service for AppRouting

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type Future = Either>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + if self.ready.is_none() { + Ok(Async::Ready(())) + } else { + Ok(Async::NotReady) + } + } + + fn call(&mut self, mut req: ServiceRequest

    ) -> Self::Future { + let res = self.router.recognize_mut_checked(&mut req, |req, guards| { + if let Some(ref guards) = guards { + for f in guards { + if !f.check(req.head()) { + return false; + } + } + } + true + }); + + if let Some((srv, _info)) = res { + Either::A(srv.call(req)) + } else if let Some(ref mut default) = self.default { + Either::A(default.call(req)) + } else { + let req = req.into_request(); + Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) + } + } +} + +/// Wrapper service for routing +pub struct AppEntry

    { + factory: Rc>>>, +} + +impl

    AppEntry

    { + pub fn new(factory: Rc>>>) -> Self { + AppEntry { factory } + } +} + +impl NewService for AppEntry

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = AppRouting

    ; + type Future = AppRoutingFactoryResponse

    ; + + fn new_service(&self, _: &()) -> Self::Future { + self.factory.borrow_mut().as_mut().unwrap().new_service(&()) + } +} + +#[doc(hidden)] +pub struct AppChain; + +impl NewService for AppChain { + type Request = ServiceRequest; + type Response = ServiceRequest; + type Error = (); + type InitError = (); + type Service = AppChain; + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(AppChain) + } +} + +impl Service for AppChain { + type Request = ServiceRequest; + type Response = ServiceRequest; + type Error = (); + type Future = FutureResult; + + #[inline] + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + #[inline] + fn call(&mut self, req: ServiceRequest) -> Self::Future { + ok(req) + } +} diff --git a/src/config.rs b/src/config.rs index 4afd213cc..47c2f7c45 100644 --- a/src/config.rs +++ b/src/config.rs @@ -98,9 +98,9 @@ impl AppConfig

    { service: F, nested: Option>, ) where - F: IntoNewService>, + F: IntoNewService, S: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), diff --git a/src/handler.rs b/src/handler.rs index 435d9a8bb..876456510 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -52,11 +52,12 @@ where } } } -impl NewService<(T, HttpRequest)> for Handler +impl NewService for Handler where F: Factory, R: Responder + 'static, { + type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = Void; type InitError = (); @@ -81,11 +82,12 @@ where _t: PhantomData<(T, R)>, } -impl Service<(T, HttpRequest)> for HandlerService +impl Service for HandlerService where F: Factory, R: Responder + 'static, { + type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = Void; type Future = HandlerServiceResponse<::Future>; @@ -182,13 +184,14 @@ where } } } -impl NewService<(T, HttpRequest)> for AsyncHandler +impl NewService for AsyncHandler where F: AsyncFactory, R: IntoFuture, R::Item: Into, R::Error: Into, { + type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = (); type InitError = (); @@ -215,13 +218,14 @@ where _t: PhantomData<(T, R)>, } -impl Service<(T, HttpRequest)> for AsyncHandlerService +impl Service for AsyncHandlerService where F: AsyncFactory, R: IntoFuture, R::Item: Into, R::Error: Into, { + type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = (); type Future = AsyncHandlerServiceResponse; @@ -286,7 +290,8 @@ impl> Extract { } } -impl> NewService> for Extract { +impl> NewService for Extract { + type Request = ServiceRequest

    ; type Response = (T, HttpRequest); type Error = (Error, ServiceFromRequest

    ); type InitError = (); @@ -306,7 +311,8 @@ pub struct ExtractService> { _t: PhantomData<(P, T)>, } -impl> Service> for ExtractService { +impl> Service for ExtractService { + type Request = ServiceRequest

    ; type Response = (T, HttpRequest); type Error = (Error, ServiceFromRequest

    ); type Future = ExtractResponse; diff --git a/src/lib.rs b/src/lib.rs index 94bf1ba76..19f466b4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ #![allow(clippy::type_complexity)] mod app; +mod app_service; mod extract; mod handler; // mod info; diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index c6f090a68..b3880a539 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -1,4 +1,5 @@ use std::io::Write; +use std::marker::PhantomData; use std::str::FromStr; use std::{cmp, fmt, io}; @@ -36,13 +37,14 @@ impl Default for Compress { } } -impl Transform> for Compress +impl Transform for Compress where P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { + type Request = ServiceRequest

    ; type Response = ServiceResponse>; type Error = S::Error; type InitError = (); @@ -62,13 +64,14 @@ pub struct CompressMiddleware { encoding: ContentEncoding, } -impl Service> for CompressMiddleware +impl Service for CompressMiddleware where P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { + type Request = ServiceRequest

    ; type Response = ServiceResponse>; type Error = S::Error; type Future = CompressResponse; @@ -92,6 +95,7 @@ where CompressResponse { encoding, fut: self.service.call(req), + _t: PhantomData, } } } @@ -101,18 +105,19 @@ pub struct CompressResponse where P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, S::Future: 'static, { fut: S::Future, encoding: ContentEncoding, + _t: PhantomData<(P, B)>, } impl Future for CompressResponse where P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { type Item = ServiceResponse>; diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index f4def58d1..b4927962f 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -85,11 +85,12 @@ impl DefaultHeaders { } } -impl Transform> for DefaultHeaders +impl Transform for DefaultHeaders where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = S::Error; type InitError = (); @@ -109,11 +110,12 @@ pub struct DefaultHeadersMiddleware { inner: Rc, } -impl Service> for DefaultHeadersMiddleware +impl Service for DefaultHeadersMiddleware where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = S::Error; type Future = Box>; diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index d8b4e643f..4af3e10d8 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::env; use std::fmt::{self, Display, Formatter}; +use std::marker::PhantomData; use std::rc::Rc; use actix_service::{Service, Transform}; @@ -110,11 +111,12 @@ impl Default for Logger { } } -impl Transform> for Logger +impl Transform for Logger where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, B: MessageBody, { + type Request = ServiceRequest

    ; type Response = ServiceResponse>; type Error = S::Error; type InitError = (); @@ -135,11 +137,12 @@ pub struct LoggerMiddleware { service: S, } -impl Service> for LoggerMiddleware +impl Service for LoggerMiddleware where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, B: MessageBody, { + type Request = ServiceRequest

    ; type Response = ServiceResponse>; type Error = S::Error; type Future = LoggerResponse; @@ -154,6 +157,7 @@ where fut: self.service.call(req), format: None, time: time::now(), + _t: PhantomData, } } else { let now = time::now(); @@ -166,6 +170,7 @@ where fut: self.service.call(req), format: Some(format), time: now, + _t: PhantomData, } } } @@ -175,17 +180,18 @@ where pub struct LoggerResponse where B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, { fut: S::Future, time: time::Tm, format: Option, + _t: PhantomData<(P, B)>, } impl Future for LoggerResponse where B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, { type Item = ServiceResponse>; type Error = S::Error; diff --git a/src/resource.rs b/src/resource.rs index a1177ca0a..cc8316653 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -66,7 +66,7 @@ impl Resource where P: 'static, T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -220,7 +220,7 @@ where ) -> Resource< P, impl NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -229,12 +229,12 @@ where where M: Transform< T::Service, - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoTransform>, + F: IntoTransform, { let endpoint = ApplyTransform::new(mw, self.endpoint); Resource { @@ -251,9 +251,12 @@ where pub fn default_resource(mut self, f: F) -> Self where F: FnOnce(Resource

    ) -> R, - R: IntoNewService>, - U: NewService, Response = ServiceResponse, Error = ()> - + 'static, + R: IntoNewService, + U: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + > + 'static, { // create and configure default resource self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::new_service( @@ -268,7 +271,7 @@ impl HttpServiceFactory

    for Resource where P: 'static, T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -292,10 +295,10 @@ where } } -impl IntoNewService> for Resource +impl IntoNewService for Resource where T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -316,7 +319,8 @@ pub struct ResourceFactory

    { default: Rc>>>>, } -impl NewService> for ResourceFactory

    { +impl NewService for ResourceFactory

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type InitError = (); @@ -406,7 +410,8 @@ pub struct ResourceService

    { default: Option>, } -impl

    Service> for ResourceService

    { +impl

    Service for ResourceService

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type Future = Either< @@ -450,7 +455,8 @@ impl

    ResourceEndpoint

    { } } -impl NewService> for ResourceEndpoint

    { +impl NewService for ResourceEndpoint

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type InitError = (); diff --git a/src/route.rs b/src/route.rs index f7b99050e..c189c61b1 100644 --- a/src/route.rs +++ b/src/route.rs @@ -15,7 +15,7 @@ use crate::HttpResponse; type BoxedRouteService = Box< Service< - Req, + Request = Req, Response = Res, Error = (), Future = Box>, @@ -24,7 +24,7 @@ type BoxedRouteService = Box< type BoxedRouteNewService = Box< NewService< - Req, + Request = Req, Response = Res, Error = (), InitError = (), @@ -70,7 +70,8 @@ impl Route

    { } } -impl

    NewService> for Route

    { +impl

    NewService for Route

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type InitError = (); @@ -125,7 +126,8 @@ impl

    RouteService

    { } } -impl

    Service> for RouteService

    { +impl

    Service for RouteService

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type Future = Box>; @@ -330,7 +332,7 @@ impl Route

    { struct RouteNewService where - T: NewService, Error = (Error, ServiceFromRequest

    )>, + T: NewService, Error = (Error, ServiceFromRequest

    )>, { service: T, _t: PhantomData

    , @@ -339,13 +341,13 @@ where impl RouteNewService where T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (Error, ServiceFromRequest

    ), >, T::Future: 'static, T::Service: 'static, - >>::Future: 'static, + ::Future: 'static, { pub fn new(service: T) -> Self { RouteNewService { @@ -355,17 +357,18 @@ where } } -impl NewService> for RouteNewService +impl NewService for RouteNewService where T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (Error, ServiceFromRequest

    ), >, T::Future: 'static, T::Service: 'static, - >>::Future: 'static, + ::Future: 'static, { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type InitError = (); @@ -389,20 +392,21 @@ where } } -struct RouteServiceWrapper>> { +struct RouteServiceWrapper { service: T, _t: PhantomData

    , } -impl Service> for RouteServiceWrapper +impl Service for RouteServiceWrapper where T::Future: 'static, T: Service< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (Error, ServiceFromRequest

    ), >, { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type Future = Box>; diff --git a/src/scope.rs b/src/scope.rs index 15c652c8f..6c511c69c 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -81,7 +81,7 @@ impl Scope where P: 'static, T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -174,7 +174,7 @@ where where F: FnOnce(Resource

    ) -> Resource, U: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -199,7 +199,7 @@ where ) -> Scope< P, impl NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -208,12 +208,12 @@ where where M: Transform< T::Service, - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoTransform>, + F: IntoTransform, { let endpoint = ApplyTransform::new(mw, self.endpoint); Scope { @@ -231,7 +231,7 @@ impl HttpServiceFactory

    for Scope where P: 'static, T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -287,7 +287,8 @@ pub struct ScopeFactory

    { default: Rc>>>>, } -impl NewService> for ScopeFactory

    { +impl NewService for ScopeFactory

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type InitError = (); @@ -402,7 +403,8 @@ pub struct ScopeService

    { _ready: Option<(ServiceRequest

    , ResourceInfo)>, } -impl

    Service> for ScopeService

    { +impl

    Service for ScopeService

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type Future = Either>; @@ -445,7 +447,8 @@ impl

    ScopeEndpoint

    { } } -impl NewService> for ScopeEndpoint

    { +impl NewService for ScopeEndpoint

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type InitError = (); diff --git a/src/server.rs b/src/server.rs index d3ae5b2b5..d95743651 100644 --- a/src/server.rs +++ b/src/server.rs @@ -7,6 +7,7 @@ use actix_http::{ }; use actix_rt::System; use actix_server::{Server, ServerBuilder}; +use actix_server_config::ServerConfig; use actix_service::{IntoNewService, NewService}; use parking_lot::Mutex; @@ -53,8 +54,8 @@ struct Config { pub struct HttpServer where F: Fn() -> I + Send + Clone + 'static, - I: IntoNewService, - S: NewService, + I: IntoNewService, + S: NewService, S::Error: fmt::Debug, S::Response: Into>, S::Service: 'static, @@ -72,8 +73,8 @@ where impl HttpServer where F: Fn() -> I + Send + Clone + 'static, - I: IntoNewService, - S: NewService, + I: IntoNewService, + S: NewService, S::Error: fmt::Debug + 'static, S::Response: Into>, S::Service: 'static, @@ -432,8 +433,8 @@ where impl HttpServer where F: Fn() -> I + Send + Clone + 'static, - I: IntoNewService, - S: NewService, + I: IntoNewService, + S: NewService, S::Error: fmt::Debug, S::Response: Into>, S::Service: 'static, diff --git a/src/test.rs b/src/test.rs index e4cdefbe8..c88835a3f 100644 --- a/src/test.rs +++ b/src/test.rs @@ -8,6 +8,7 @@ use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{Extensions, PayloadStream, Request}; use actix_router::{Path, ResourceDef, Url}; use actix_rt::Runtime; +use actix_server_config::ServerConfig; use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; use futures::Future; @@ -62,13 +63,19 @@ where /// ``` pub fn init_service( app: R, -) -> impl Service, Error = E> +) -> impl Service, Error = E> where - R: IntoNewService, - S: NewService, Error = E>, + R: IntoNewService, + S: NewService< + ServerConfig, + Request = Request, + Response = ServiceResponse, + Error = E, + >, S::InitError: std::fmt::Debug, { - block_on(app.into_new_service().new_service(&())).unwrap() + let cfg = ServerConfig::new("127.0.0.1:8080".parse().unwrap()); + block_on(app.into_new_service().new_service(&cfg)).unwrap() } /// Calls service and waits for response future completion. @@ -93,7 +100,7 @@ where /// ``` pub fn call_success(app: &mut S, req: R) -> S::Response where - S: Service, Error = E>, + S: Service, Error = E>, E: std::fmt::Debug, { block_on(app.call(req)).unwrap() From d026821924cc97482c334952e214f0f86b1ee0e0 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 10:39:06 -0800 Subject: [PATCH 240/427] unify service builders --- examples/echo.rs | 7 +- examples/echo2.rs | 10 +- examples/hello-world.rs | 7 +- src/builder.rs | 141 ++++++++++++++++++++++++++++ src/config.rs | 118 +----------------------- src/h1/service.rs | 136 --------------------------- src/h2/service.rs | 135 --------------------------- src/lib.rs | 4 +- src/service/service.rs | 155 +------------------------------ tests/test_client.rs | 8 +- tests/test_server.rs | 198 ++++++++++++++++++++++++++++------------ 11 files changed, 300 insertions(+), 619 deletions(-) create mode 100644 src/builder.rs diff --git a/examples/echo.rs b/examples/echo.rs index eb4aaa657..8ec0e6a97 100644 --- a/examples/echo.rs +++ b/examples/echo.rs @@ -1,9 +1,8 @@ use std::{env, io}; use actix_http::HttpMessage; -use actix_http::{h1, Request, Response}; +use actix_http::{HttpService, Request, Response}; use actix_server::Server; -use actix_service::NewService; use bytes::Bytes; use futures::Future; use http::header::HeaderValue; @@ -15,10 +14,9 @@ fn main() -> io::Result<()> { Server::build() .bind("echo", "127.0.0.1:8080", || { - h1::H1Service::build() + HttpService::build() .client_timeout(1000) .client_disconnect(1000) - .server_hostname("localhost") .finish(|mut req: Request| { req.body().limit(512).and_then(|bytes: Bytes| { info!("request body: {:?}", bytes); @@ -27,7 +25,6 @@ fn main() -> io::Result<()> { Ok(res.body(bytes)) }) }) - .map(|_| ()) })? .run() } diff --git a/examples/echo2.rs b/examples/echo2.rs index 3bb8d83d5..101adc1cf 100644 --- a/examples/echo2.rs +++ b/examples/echo2.rs @@ -2,9 +2,8 @@ use std::{env, io}; use actix_http::http::HeaderValue; use actix_http::HttpMessage; -use actix_http::{h1, Error, Request, Response}; +use actix_http::{Error, HttpService, Request, Response}; use actix_server::Server; -use actix_service::NewService; use bytes::Bytes; use futures::Future; use log::info; @@ -24,12 +23,7 @@ fn main() -> io::Result<()> { Server::build() .bind("echo", "127.0.0.1:8080", || { - h1::H1Service::build() - .client_timeout(1000) - .client_disconnect(1000) - .server_hostname("localhost") - .finish(|_req: Request| handle_request(_req)) - .map(|_| ()) + HttpService::build().finish(|_req: Request| handle_request(_req)) })? .run() } diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 05d69fed8..6e3820390 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -1,8 +1,7 @@ use std::{env, io}; -use actix_http::{h1, Response}; +use actix_http::{HttpService, Response}; use actix_server::Server; -use actix_service::NewService; use futures::future; use http::header::HeaderValue; use log::info; @@ -13,17 +12,15 @@ fn main() -> io::Result<()> { Server::build() .bind("hello-world", "127.0.0.1:8080", || { - h1::H1Service::build() + HttpService::build() .client_timeout(1000) .client_disconnect(1000) - .server_hostname("localhost") .finish(|_req| { info!("{:?}", _req); let mut res = Response::Ok(); res.header("x-head", HeaderValue::from_static("dummy value!")); future::ok::<_, ()>(res.body("Hello world!")) }) - .map(|_| ()) })? .run() } diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 000000000..c55b8d5fc --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,141 @@ +use std::fmt::Debug; +use std::marker::PhantomData; + +use actix_server_config::ServerConfig as SrvConfig; +use actix_service::{IntoNewService, NewService}; + +use crate::body::MessageBody; +use crate::config::{KeepAlive, ServiceConfig}; +use crate::request::Request; +use crate::response::Response; + +use crate::h1::H1Service; +use crate::h2::H2Service; +use crate::service::HttpService; + +/// A http service builder +/// +/// This type can be used to construct an instance of `http service` through a +/// builder-like pattern. +pub struct HttpServiceBuilder { + keep_alive: KeepAlive, + client_timeout: u64, + client_disconnect: u64, + _t: PhantomData<(T, S)>, +} + +impl HttpServiceBuilder +where + S: NewService, + S::Error: Debug + 'static, + S::Service: 'static, +{ + /// Create instance of `ServiceConfigBuilder` + pub fn new() -> HttpServiceBuilder { + HttpServiceBuilder { + keep_alive: KeepAlive::Timeout(5), + client_timeout: 5000, + client_disconnect: 0, + _t: PhantomData, + } + } + + /// Set server keep-alive setting. + /// + /// By default keep alive is set to a 5 seconds. + pub fn keep_alive>(mut self, val: U) -> Self { + self.keep_alive = val.into(); + self + } + + /// Set server client timeout in milliseconds for first request. + /// + /// Defines a timeout for reading client request header. If a client does not transmit + /// the entire set headers within this time, the request is terminated with + /// the 408 (Request Time-out) error. + /// + /// To disable timeout set value to 0. + /// + /// By default client timeout is set to 5000 milliseconds. + pub fn client_timeout(mut self, val: u64) -> Self { + self.client_timeout = val; + self + } + + /// Set server connection disconnect timeout in milliseconds. + /// + /// Defines a timeout for disconnect connection. If a disconnect procedure does not complete + /// within this time, the request get dropped. This timeout affects secure connections. + /// + /// To disable timeout set value to 0. + /// + /// By default disconnect timeout is set to 3000 milliseconds. + pub fn client_disconnect(mut self, val: u64) -> Self { + self.client_disconnect = val; + self + } + + // #[cfg(feature = "ssl")] + // /// Configure alpn protocols for SslAcceptorBuilder. + // pub fn configure_openssl( + // builder: &mut openssl::ssl::SslAcceptorBuilder, + // ) -> io::Result<()> { + // let protos: &[u8] = b"\x02h2"; + // builder.set_alpn_select_callback(|_, protos| { + // const H2: &[u8] = b"\x02h2"; + // if protos.windows(3).any(|window| window == H2) { + // Ok(b"h2") + // } else { + // Err(openssl::ssl::AlpnError::NOACK) + // } + // }); + // builder.set_alpn_protos(&protos)?; + + // Ok(()) + // } + + /// Finish service configuration and create *http service* for HTTP/1 protocol. + pub fn h1(self, service: F) -> H1Service + where + B: MessageBody + 'static, + F: IntoNewService, + S::Response: Into>, + { + let cfg = ServiceConfig::new( + self.keep_alive, + self.client_timeout, + self.client_disconnect, + ); + H1Service::with_config(cfg, service.into_new_service()) + } + + /// Finish service configuration and create *http service* for HTTP/2 protocol. + pub fn h2(self, service: F) -> H2Service + where + B: MessageBody + 'static, + F: IntoNewService, + S::Response: Into>, + { + let cfg = ServiceConfig::new( + self.keep_alive, + self.client_timeout, + self.client_disconnect, + ); + H2Service::with_config(cfg, service.into_new_service()) + } + + /// Finish service configuration and create `HttpService` instance. + pub fn finish(self, service: F) -> HttpService + where + B: MessageBody + 'static, + F: IntoNewService, + S::Response: Into>, + { + let cfg = ServiceConfig::new( + self.keep_alive, + self.client_timeout, + self.client_disconnect, + ); + HttpService::with_config(cfg, service.into_new_service()) + } +} diff --git a/src/config.rs b/src/config.rs index a9e705c95..3c7df2feb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,12 +1,11 @@ use std::cell::UnsafeCell; +use std::fmt; use std::fmt::Write; use std::rc::Rc; use std::time::{Duration, Instant}; -use std::{fmt, net}; use bytes::BytesMut; use futures::{future, Future}; -use log::error; use time; use tokio_timer::{sleep, Delay}; @@ -90,11 +89,6 @@ impl ServiceConfig { })) } - /// Create worker settings builder. - pub fn build() -> ServiceConfigBuilder { - ServiceConfigBuilder::new() - } - #[inline] /// Keep alive duration if configured. pub fn keep_alive(&self) -> Option { @@ -177,116 +171,6 @@ impl ServiceConfig { } } -/// A service config builder -/// -/// This type can be used to construct an instance of `ServiceConfig` through a -/// builder-like pattern. -pub struct ServiceConfigBuilder { - keep_alive: KeepAlive, - client_timeout: u64, - client_disconnect: u64, - host: String, - addr: net::SocketAddr, - secure: bool, -} - -impl ServiceConfigBuilder { - /// Create instance of `ServiceConfigBuilder` - pub fn new() -> ServiceConfigBuilder { - ServiceConfigBuilder { - keep_alive: KeepAlive::Timeout(5), - client_timeout: 5000, - client_disconnect: 0, - secure: false, - host: "localhost".to_owned(), - addr: "127.0.0.1:8080".parse().unwrap(), - } - } - - /// Enable secure flag for current server. - /// This flags also enables `client disconnect timeout`. - /// - /// By default this flag is set to false. - pub fn secure(mut self) -> Self { - self.secure = true; - if self.client_disconnect == 0 { - self.client_disconnect = 3000; - } - self - } - - /// Set server keep-alive setting. - /// - /// By default keep alive is set to a 5 seconds. - pub fn keep_alive>(mut self, val: T) -> Self { - self.keep_alive = val.into(); - self - } - - /// Set server client timeout in milliseconds for first request. - /// - /// Defines a timeout for reading client request header. If a client does not transmit - /// the entire set headers within this time, the request is terminated with - /// the 408 (Request Time-out) error. - /// - /// To disable timeout set value to 0. - /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_timeout(mut self, val: u64) -> Self { - self.client_timeout = val; - self - } - - /// Set server connection disconnect timeout in milliseconds. - /// - /// Defines a timeout for disconnect connection. If a disconnect procedure does not complete - /// within this time, the request get dropped. This timeout affects secure connections. - /// - /// To disable timeout set value to 0. - /// - /// By default disconnect timeout is set to 3000 milliseconds. - pub fn client_disconnect(mut self, val: u64) -> Self { - self.client_disconnect = val; - self - } - - /// Set server host name. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - /// - /// By default host name is set to a "localhost" value. - pub fn server_hostname(mut self, val: &str) -> Self { - self.host = val.to_owned(); - self - } - - /// Set server ip address. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - /// - /// By default server address is set to a "127.0.0.1:8080" - pub fn server_address(mut self, addr: S) -> Self { - match addr.to_socket_addrs() { - Err(err) => error!("Can not convert to SocketAddr: {}", err), - Ok(mut addrs) => { - if let Some(addr) = addrs.next() { - self.addr = addr; - } - } - } - self - } - - /// Finish service configuration and create `ServiceConfig` object. - pub fn finish(&mut self) -> ServiceConfig { - ServiceConfig::new(self.keep_alive, self.client_timeout, self.client_disconnect) - } -} - struct Date { bytes: [u8; DATE_VALUE_LENGTH], pos: usize, diff --git a/src/h1/service.rs b/src/h1/service.rs index 6a36678b3..e55ff0d99 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -1,6 +1,5 @@ use std::fmt::Debug; use std::marker::PhantomData; -use std::net; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_server_config::ServerConfig as SrvConfig; @@ -8,7 +7,6 @@ use actix_service::{IntoNewService, NewService, Service}; use actix_utils::cloneable::CloneableService; use futures::future::{ok, FutureResult}; use futures::{try_ready, Async, Future, IntoFuture, Poll, Stream}; -use log::error; use crate::body::MessageBody; use crate::config::{KeepAlive, ServiceConfig}; @@ -57,11 +55,6 @@ where _t: PhantomData, } } - - /// Create builder for `HttpService` instance. - pub fn build() -> H1ServiceBuilder { - H1ServiceBuilder::new() - } } impl NewService for H1Service @@ -89,135 +82,6 @@ where } } -/// A http/1 new service builder -/// -/// This type can be used to construct an instance of `ServiceConfig` through a -/// builder-like pattern. -pub struct H1ServiceBuilder { - keep_alive: KeepAlive, - client_timeout: u64, - client_disconnect: u64, - host: String, - addr: net::SocketAddr, - secure: bool, - _t: PhantomData<(T, S)>, -} - -impl H1ServiceBuilder -where - S: NewService, - S::Error: Debug, -{ - /// Create instance of `ServiceConfigBuilder` - pub fn new() -> H1ServiceBuilder { - H1ServiceBuilder { - keep_alive: KeepAlive::Timeout(5), - client_timeout: 5000, - client_disconnect: 0, - secure: false, - host: "localhost".to_owned(), - addr: "127.0.0.1:8080".parse().unwrap(), - _t: PhantomData, - } - } - - /// Enable secure flag for current server. - /// This flags also enables `client disconnect timeout`. - /// - /// By default this flag is set to false. - pub fn secure(mut self) -> Self { - self.secure = true; - if self.client_disconnect == 0 { - self.client_disconnect = 3000; - } - self - } - - /// Set server keep-alive setting. - /// - /// By default keep alive is set to a 5 seconds. - pub fn keep_alive>(mut self, val: U) -> Self { - self.keep_alive = val.into(); - self - } - - /// Set server client timeout in milliseconds for first request. - /// - /// Defines a timeout for reading client request header. If a client does not transmit - /// the entire set headers within this time, the request is terminated with - /// the 408 (Request Time-out) error. - /// - /// To disable timeout set value to 0. - /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_timeout(mut self, val: u64) -> Self { - self.client_timeout = val; - self - } - - /// Set server connection disconnect timeout in milliseconds. - /// - /// Defines a timeout for disconnect connection. If a disconnect procedure does not complete - /// within this time, the request get dropped. This timeout affects secure connections. - /// - /// To disable timeout set value to 0. - /// - /// By default disconnect timeout is set to 3000 milliseconds. - pub fn client_disconnect(mut self, val: u64) -> Self { - self.client_disconnect = val; - self - } - - /// Set server host name. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - /// - /// By default host name is set to a "localhost" value. - pub fn server_hostname(mut self, val: &str) -> Self { - self.host = val.to_owned(); - self - } - - /// Set server ip address. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - /// - /// By default server address is set to a "127.0.0.1:8080" - pub fn server_address(mut self, addr: U) -> Self { - match addr.to_socket_addrs() { - Err(err) => error!("Can not convert to SocketAddr: {}", err), - Ok(mut addrs) => { - if let Some(addr) = addrs.next() { - self.addr = addr; - } - } - } - self - } - - /// Finish service configuration and create `H1Service` instance. - pub fn finish(self, service: F) -> H1Service - where - B: MessageBody, - F: IntoNewService, - { - let cfg = ServiceConfig::new( - self.keep_alive, - self.client_timeout, - self.client_disconnect, - ); - H1Service { - cfg, - srv: service.into_new_service(), - _t: PhantomData, - } - } -} - #[doc(hidden)] pub struct H1ServiceResponse, B> { fut: ::Future, diff --git a/src/h2/service.rs b/src/h2/service.rs index 57515d4e8..ce7c3b5dd 100644 --- a/src/h2/service.rs +++ b/src/h2/service.rs @@ -59,11 +59,6 @@ where _t: PhantomData, } } - - /// Create builder for `HttpService` instance. - pub fn build() -> H2ServiceBuilder { - H2ServiceBuilder::new() - } } impl NewService for H2Service @@ -91,136 +86,6 @@ where } } -/// A http/2 new service builder -/// -/// This type can be used to construct an instance of `ServiceConfig` through a -/// builder-like pattern. -pub struct H2ServiceBuilder { - keep_alive: KeepAlive, - client_timeout: u64, - client_disconnect: u64, - host: String, - addr: net::SocketAddr, - secure: bool, - _t: PhantomData<(T, S)>, -} - -impl H2ServiceBuilder -where - S: NewService, - S::Service: 'static, - S::Error: Debug + 'static, -{ - /// Create instance of `H2ServiceBuilder` - pub fn new() -> H2ServiceBuilder { - H2ServiceBuilder { - keep_alive: KeepAlive::Timeout(5), - client_timeout: 5000, - client_disconnect: 0, - secure: false, - host: "localhost".to_owned(), - addr: "127.0.0.1:8080".parse().unwrap(), - _t: PhantomData, - } - } - - /// Enable secure flag for current server. - /// This flags also enables `client disconnect timeout`. - /// - /// By default this flag is set to false. - pub fn secure(mut self) -> Self { - self.secure = true; - if self.client_disconnect == 0 { - self.client_disconnect = 3000; - } - self - } - - /// Set server keep-alive setting. - /// - /// By default keep alive is set to a 5 seconds. - pub fn keep_alive>(mut self, val: U) -> Self { - self.keep_alive = val.into(); - self - } - - /// Set server client timeout in milliseconds for first request. - /// - /// Defines a timeout for reading client request header. If a client does not transmit - /// the entire set headers within this time, the request is terminated with - /// the 408 (Request Time-out) error. - /// - /// To disable timeout set value to 0. - /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_timeout(mut self, val: u64) -> Self { - self.client_timeout = val; - self - } - - /// Set server connection disconnect timeout in milliseconds. - /// - /// Defines a timeout for disconnect connection. If a disconnect procedure does not complete - /// within this time, the request get dropped. This timeout affects secure connections. - /// - /// To disable timeout set value to 0. - /// - /// By default disconnect timeout is set to 3000 milliseconds. - pub fn client_disconnect(mut self, val: u64) -> Self { - self.client_disconnect = val; - self - } - - /// Set server host name. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - /// - /// By default host name is set to a "localhost" value. - pub fn server_hostname(mut self, val: &str) -> Self { - self.host = val.to_owned(); - self - } - - /// Set server ip address. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - /// - /// By default server address is set to a "127.0.0.1:8080" - pub fn server_address(mut self, addr: U) -> Self { - match addr.to_socket_addrs() { - Err(err) => error!("Can not convert to SocketAddr: {}", err), - Ok(mut addrs) => { - if let Some(addr) = addrs.next() { - self.addr = addr; - } - } - } - self - } - - /// Finish service configuration and create `H1Service` instance. - pub fn finish(self, service: F) -> H2Service - where - B: MessageBody, - F: IntoNewService, - { - let cfg = ServiceConfig::new( - self.keep_alive, - self.client_timeout, - self.client_disconnect, - ); - H2Service { - cfg, - srv: service.into_new_service(), - _t: PhantomData, - } - } -} - #[doc(hidden)] pub struct H2ServiceResponse, B> { fut: ::Future, diff --git a/src/lib.rs b/src/lib.rs index 9d8bc50f1..41ee55fec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,6 +69,7 @@ extern crate log; pub mod body; +mod builder; pub mod client; mod config; mod extensions; @@ -89,7 +90,8 @@ pub mod h2; pub mod test; pub mod ws; -pub use self::config::{KeepAlive, ServiceConfig, ServiceConfigBuilder}; +pub use self::builder::HttpServiceBuilder; +pub use self::config::{KeepAlive, ServiceConfig}; pub use self::error::{Error, ResponseError, Result}; pub use self::extensions::Extensions; pub use self::httpmessage::HttpMessage; diff --git a/src/service/service.rs b/src/service/service.rs index 0fa5d6653..ac28c77a5 100644 --- a/src/service/service.rs +++ b/src/service/service.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; use std::marker::PhantomData; -use std::{fmt, io, net}; +use std::{fmt, io}; use actix_codec::{AsyncRead, AsyncWrite, Framed, FramedParts}; use actix_server_config::ServerConfig as SrvConfig; @@ -12,11 +12,11 @@ use h2::server::{self, Handshake}; use log::error; use crate::body::MessageBody; +use crate::builder::HttpServiceBuilder; use crate::config::{KeepAlive, ServiceConfig}; use crate::error::DispatchError; use crate::request::Request; use crate::response::Response; - use crate::{h1, h2::Dispatcher}; /// `NewService` HTTP1.1/HTTP2 transport implementation @@ -46,7 +46,7 @@ where } /// Create new `HttpService` instance with config. - pub fn with_config>( + pub(crate) fn with_config>( cfg: ServiceConfig, service: F, ) -> Self { @@ -88,155 +88,6 @@ where } } -/// A http service factory builder -/// -/// This type can be used to construct an instance of `ServiceConfig` through a -/// builder-like pattern. -pub struct HttpServiceBuilder { - keep_alive: KeepAlive, - client_timeout: u64, - client_disconnect: u64, - host: String, - addr: net::SocketAddr, - secure: bool, - _t: PhantomData<(T, S)>, -} - -impl HttpServiceBuilder -where - S: NewService, - S::Service: 'static, - S::Error: Debug + 'static, -{ - /// Create instance of `HttpServiceBuilder` type - pub fn new() -> HttpServiceBuilder { - HttpServiceBuilder { - keep_alive: KeepAlive::Timeout(5), - client_timeout: 5000, - client_disconnect: 0, - secure: false, - host: "localhost".to_owned(), - addr: "127.0.0.1:8080".parse().unwrap(), - _t: PhantomData, - } - } - - /// Enable secure flag for current server. - /// This flags also enables `client disconnect timeout`. - /// - /// By default this flag is set to false. - pub fn secure(mut self) -> Self { - self.secure = true; - if self.client_disconnect == 0 { - self.client_disconnect = 3000; - } - self - } - - /// Set server keep-alive setting. - /// - /// By default keep alive is set to a 5 seconds. - pub fn keep_alive>(mut self, val: U) -> Self { - self.keep_alive = val.into(); - self - } - - /// Set server client timeout in milliseconds for first request. - /// - /// Defines a timeout for reading client request header. If a client does not transmit - /// the entire set headers within this time, the request is terminated with - /// the 408 (Request Time-out) error. - /// - /// To disable timeout set value to 0. - /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_timeout(mut self, val: u64) -> Self { - self.client_timeout = val; - self - } - - /// Set server connection disconnect timeout in milliseconds. - /// - /// Defines a timeout for disconnect connection. If a disconnect procedure does not complete - /// within this time, the request get dropped. This timeout affects secure connections. - /// - /// To disable timeout set value to 0. - /// - /// By default disconnect timeout is set to 3000 milliseconds. - pub fn client_disconnect(mut self, val: u64) -> Self { - self.client_disconnect = val; - self - } - - /// Set server host name. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - /// - /// By default host name is set to a "localhost" value. - pub fn server_hostname(mut self, val: &str) -> Self { - self.host = val.to_owned(); - self - } - - /// Set server ip address. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - /// - /// By default server address is set to a "127.0.0.1:8080" - pub fn server_address(mut self, addr: U) -> Self { - match addr.to_socket_addrs() { - Err(err) => error!("Can not convert to SocketAddr: {}", err), - Ok(mut addrs) => { - if let Some(addr) = addrs.next() { - self.addr = addr; - } - } - } - self - } - - // #[cfg(feature = "ssl")] - // /// Configure alpn protocols for SslAcceptorBuilder. - // pub fn configure_openssl( - // builder: &mut openssl::ssl::SslAcceptorBuilder, - // ) -> io::Result<()> { - // let protos: &[u8] = b"\x02h2"; - // builder.set_alpn_select_callback(|_, protos| { - // const H2: &[u8] = b"\x02h2"; - // if protos.windows(3).any(|window| window == H2) { - // Ok(b"h2") - // } else { - // Err(openssl::ssl::AlpnError::NOACK) - // } - // }); - // builder.set_alpn_protos(&protos)?; - - // Ok(()) - // } - - /// Finish service configuration and create `HttpService` instance. - pub fn finish(self, service: F) -> HttpService - where - B: MessageBody, - F: IntoNewService, - { - let cfg = ServiceConfig::new( - self.keep_alive, - self.client_timeout, - self.client_disconnect, - ); - HttpService { - cfg, - srv: service.into_new_service(), - _t: PhantomData, - } - } -} - #[doc(hidden)] pub struct HttpServiceResponse, B> { fut: ::Future, diff --git a/tests/test_client.rs b/tests/test_client.rs index f44c45cbc..782e487ca 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -3,7 +3,7 @@ use bytes::Bytes; use futures::future::{self, ok}; use actix_http::HttpMessage; -use actix_http::{client, h1, Request, Response}; +use actix_http::{client, HttpService, Request, Response}; use actix_http_test::TestServer; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ @@ -32,7 +32,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ fn test_h1_v2() { env_logger::init(); let mut srv = TestServer::new(move || { - h1::H1Service::build() + HttpService::build() .finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) .map(|_| ()) }); @@ -66,7 +66,7 @@ fn test_h1_v2() { #[test] fn test_connection_close() { let mut srv = TestServer::new(move || { - h1::H1Service::build() + HttpService::build() .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .map(|_| ()) }); @@ -80,7 +80,7 @@ fn test_connection_close() { #[test] fn test_with_query_parameter() { let mut srv = TestServer::new(move || { - h1::H1Service::build() + HttpService::build() .finish(|req: Request| { if req.uri().query().unwrap().contains("qp=") { ok::<_, ()>(Response::Ok().finish()) diff --git a/tests/test_server.rs b/tests/test_server.rs index b7cd5557b..7a28bca8a 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -10,20 +10,18 @@ use futures::stream::once; use actix_http::body::Body; use actix_http::{ - body, client, h1, h2, http, Error, HttpMessage as HttpMessage2, HttpService, - KeepAlive, Request, Response, + body, client, http, Error, HttpMessage as HttpMessage2, HttpService, KeepAlive, + Request, Response, }; #[test] fn test_h1() { let mut srv = TestServer::new(|| { - h1::H1Service::build() + HttpService::build() .keep_alive(KeepAlive::Disabled) .client_timeout(1000) .client_disconnect(1000) - .server_hostname("localhost") - .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) - .map(|_| ()) + .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) }); let req = client::ClientRequest::get(srv.url("/")).finish().unwrap(); @@ -38,7 +36,6 @@ fn test_h1_2() { .keep_alive(KeepAlive::Disabled) .client_timeout(1000) .client_disconnect(1000) - .server_hostname("localhost") .finish(|req: Request| { assert_eq!(req.version(), http::Version::HTTP_11); future::ok::<_, ()>(Response::Ok().finish()) @@ -83,8 +80,8 @@ fn test_h2() -> std::io::Result<()> { .clone() .map_err(|e| println!("Openssl error: {}", e)) .and_then( - h2::H2Service::build() - .finish(|_| future::ok::<_, Error>(Response::Ok().finish())) + HttpService::build() + .h2(|_| future::ok::<_, Error>(Response::Ok().finish())) .map_err(|_| ()), ) }); @@ -129,8 +126,8 @@ fn test_h2_body() -> std::io::Result<()> { .clone() .map_err(|e| println!("Openssl error: {}", e)) .and_then( - h2::H2Service::build() - .finish(|mut req: Request<_>| { + HttpService::build() + .h2(|mut req: Request<_>| { req.body() .limit(1024 * 1024) .and_then(|body| Ok(Response::Ok().body(body))) @@ -153,10 +150,9 @@ fn test_h2_body() -> std::io::Result<()> { #[test] fn test_slow_request() { let srv = TestServer::new(|| { - h1::H1Service::build() + HttpService::build() .client_timeout(100) - .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) - .map(|_| ()) + .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) }); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); @@ -167,9 +163,9 @@ fn test_slow_request() { } #[test] -fn test_malformed_request() { +fn test_http1_malformed_request() { let srv = TestServer::new(|| { - h1::H1Service::new(|_| future::ok::<_, ()>(Response::Ok().finish())).map(|_| ()) + HttpService::build().h1(|_| future::ok::<_, ()>(Response::Ok().finish())) }); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); @@ -180,11 +176,9 @@ fn test_malformed_request() { } #[test] -fn test_keepalive() { +fn test_http1_keepalive() { let srv = TestServer::new(|| { - h1::H1Service::build() - .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) - .map(|_| ()) + HttpService::build().h1(|_| future::ok::<_, ()>(Response::Ok().finish())) }); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); @@ -200,12 +194,11 @@ fn test_keepalive() { } #[test] -fn test_keepalive_timeout() { +fn test_http1_keepalive_timeout() { let srv = TestServer::new(|| { - h1::H1Service::build() + HttpService::build() .keep_alive(1) - .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) - .map(|_| ()) + .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) }); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); @@ -221,11 +214,9 @@ fn test_keepalive_timeout() { } #[test] -fn test_keepalive_close() { +fn test_http1_keepalive_close() { let srv = TestServer::new(|| { - h1::H1Service::build() - .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) - .map(|_| ()) + HttpService::build().h1(|_| future::ok::<_, ()>(Response::Ok().finish())) }); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); @@ -241,11 +232,9 @@ fn test_keepalive_close() { } #[test] -fn test_keepalive_http10_default_close() { +fn test_http10_keepalive_default_close() { let srv = TestServer::new(|| { - h1::H1Service::build() - .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) - .map(|_| ()) + HttpService::build().h1(|_| future::ok::<_, ()>(Response::Ok().finish())) }); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); @@ -260,11 +249,9 @@ fn test_keepalive_http10_default_close() { } #[test] -fn test_keepalive_http10() { +fn test_http10_keepalive() { let srv = TestServer::new(|| { - h1::H1Service::build() - .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) - .map(|_| ()) + HttpService::build().h1(|_| future::ok::<_, ()>(Response::Ok().finish())) }); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); @@ -286,12 +273,11 @@ fn test_keepalive_http10() { } #[test] -fn test_keepalive_disabled() { +fn test_http1_keepalive_disabled() { let srv = TestServer::new(|| { - h1::H1Service::build() + HttpService::build() .keep_alive(KeepAlive::Disabled) - .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) - .map(|_| ()) + .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) }); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); @@ -313,7 +299,7 @@ fn test_content_length() { }; let mut srv = TestServer::new(|| { - h1::H1Service::new(|req: Request| { + HttpService::build().h1(|req: Request| { let indx: usize = req.uri().path()[1..].parse().unwrap(); let statuses = [ StatusCode::NO_CONTENT, @@ -325,7 +311,6 @@ fn test_content_length() { ]; future::ok::<_, ()>(Response::new(statuses[indx])) }) - .map(|_| ()) }); let header = HeaderName::from_static("content-length"); @@ -356,6 +341,65 @@ fn test_content_length() { } } +// TODO: fix +// #[test] +// fn test_h2_content_length() { +// use actix_http::http::{ +// header::{HeaderName, HeaderValue}, +// StatusCode, +// }; +// let openssl = ssl_acceptor().unwrap(); + +// let mut srv = TestServer::new(move || { +// openssl +// .clone() +// .map_err(|e| println!("Openssl error: {}", e)) +// .and_then( +// HttpService::build() +// .h2(|req: Request| { +// let indx: usize = req.uri().path()[1..].parse().unwrap(); +// let statuses = [ +// StatusCode::NO_CONTENT, +// StatusCode::CONTINUE, +// StatusCode::SWITCHING_PROTOCOLS, +// StatusCode::PROCESSING, +// StatusCode::OK, +// StatusCode::NOT_FOUND, +// ]; +// future::ok::<_, ()>(Response::new(statuses[indx])) +// }) +// .map_err(|_| ()), +// ) +// }); + +// let header = HeaderName::from_static("content-length"); +// let value = HeaderValue::from_static("0"); + +// { +// for i in 0..4 { +// let req = client::ClientRequest::get(srv.surl(&format!("/{}", i))) +// .finish() +// .unwrap(); +// let response = srv.send_request(req).unwrap(); +// assert_eq!(response.headers().get(&header), None); + +// let req = client::ClientRequest::head(srv.surl(&format!("/{}", i))) +// .finish() +// .unwrap(); +// let response = srv.send_request(req).unwrap(); +// assert_eq!(response.headers().get(&header), None); +// } + +// for i in 4..6 { +// let req = client::ClientRequest::get(srv.surl(&format!("/{}", i))) +// .finish() +// .unwrap(); +// let response = srv.send_request(req).unwrap(); +// assert_eq!(response.headers().get(&header), Some(&value)); +// } +// } +// } + #[test] fn test_headers() { let data = STR.repeat(10); @@ -363,7 +407,7 @@ fn test_headers() { let mut srv = TestServer::new(move || { let data = data.clone(); - h1::H1Service::new(move |_| { + HttpService::build().h1(move |_| { let mut builder = Response::Ok(); for idx in 0..90 { builder.header( @@ -384,9 +428,8 @@ fn test_headers() { ); } future::ok::<_, ()>(builder.body(data.clone())) - }).map(|_| ()) + }) }); - let mut connector = srv.new_connector(); let req = srv.get().finish().unwrap(); @@ -399,6 +442,52 @@ fn test_headers() { assert_eq!(bytes, Bytes::from(data2)); } +#[test] +fn test_h2_headers() { + let data = STR.repeat(10); + let data2 = data.clone(); + let openssl = ssl_acceptor().unwrap(); + + let mut srv = TestServer::new(move || { + let data = data.clone(); + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)) + .and_then( + HttpService::build().h2(move |_| { + let mut builder = Response::Ok(); + for idx in 0..90 { + builder.header( + format!("X-TEST-{}", idx).as_str(), + "TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ + TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", + ); + } + future::ok::<_, ()>(builder.body(data.clone())) + }).map_err(|_| ())) + }); + let mut connector = srv.new_connector(); + + let req = client::ClientRequest::get(srv.surl("/")).finish().unwrap(); + let mut response = srv.block_on(req.send(&mut connector)).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from(data2)); +} + const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -424,7 +513,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ #[test] fn test_body() { let mut srv = TestServer::new(|| { - h1::H1Service::new(|_| future::ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) + HttpService::build().h1(|_| future::ok::<_, ()>(Response::Ok().body(STR))) }); let req = srv.get().finish().unwrap(); @@ -439,7 +528,7 @@ fn test_body() { #[test] fn test_head_empty() { let mut srv = TestServer::new(|| { - h1::H1Service::new(|_| ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) + HttpService::build().h1(|_| ok::<_, ()>(Response::Ok().body(STR))) }); let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); @@ -462,10 +551,9 @@ fn test_head_empty() { #[test] fn test_head_binary() { let mut srv = TestServer::new(|| { - h1::H1Service::new(|_| { + HttpService::build().h1(|_| { ok::<_, ()>(Response::Ok().content_length(STR.len() as u64).body(STR)) }) - .map(|_| ()) }); let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); @@ -488,7 +576,7 @@ fn test_head_binary() { #[test] fn test_head_binary2() { let mut srv = TestServer::new(|| { - h1::H1Service::new(|_| ok::<_, ()>(Response::Ok().body(STR))).map(|_| ()) + HttpService::build().h1(|_| ok::<_, ()>(Response::Ok().body(STR))) }); let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); @@ -507,14 +595,13 @@ fn test_head_binary2() { #[test] fn test_body_length() { let mut srv = TestServer::new(|| { - h1::H1Service::new(|_| { + HttpService::build().h1(|_| { let body = once(Ok(Bytes::from_static(STR.as_ref()))); ok::<_, ()>( Response::Ok() .body(Body::from_message(body::SizedStream::new(STR.len(), body))), ) }) - .map(|_| ()) }); let req = srv.get().finish().unwrap(); @@ -529,11 +616,10 @@ fn test_body_length() { #[test] fn test_body_chunked_explicit() { let mut srv = TestServer::new(|| { - h1::H1Service::new(|_| { + HttpService::build().h1(|_| { let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); ok::<_, ()>(Response::Ok().streaming(body)) }) - .map(|_| ()) }); let req = srv.get().finish().unwrap(); @@ -550,7 +636,7 @@ fn test_body_chunked_explicit() { #[test] fn test_body_chunked_implicit() { let mut srv = TestServer::new(|| { - h1::H1Service::new(|_| { + HttpService::build().h1(|_| { let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); ok::<_, ()>(Response::Ok().streaming(body)) }) @@ -571,7 +657,7 @@ use actix_service::fn_cfg_factory; #[test] fn test_response_http_error_handling() { let mut srv = TestServer::new(|| { - h1::H1Service::new(fn_cfg_factory(|_: &ServerConfig| { + HttpService::build().h1(fn_cfg_factory(|_: &ServerConfig| { Ok::<_, ()>(|_| { let broken_header = Bytes::from_static(b"\0\0\0"); ok::<_, ()>( From c0ce7f0bae77ed61330d80fba2b8ec6fdf838556 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 10:53:00 -0800 Subject: [PATCH 241/427] update http service usage; add app host --- src/app.rs | 11 ++++-- src/server.rs | 97 +++++++++++++++++++++++++-------------------------- 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/src/app.rs b/src/app.rs index 76a3a1ce4..42ce62d89 100644 --- a/src/app.rs +++ b/src/app.rs @@ -31,6 +31,7 @@ where chain: T, extensions: Extensions, state: Vec>, + host: String, _t: PhantomData<(P,)>, } @@ -42,6 +43,7 @@ impl App { chain: AppChain, extensions: Extensions::new(), state: Vec::new(), + host: "localhost:8080".to_string(), _t: PhantomData, } } @@ -140,6 +142,7 @@ where default: None, factory_ref: fref, extensions: self.extensions, + host: self.host, _t: PhantomData, } } @@ -172,6 +175,7 @@ where chain, state: self.state, extensions: self.extensions, + host: self.host, _t: PhantomData, } } @@ -221,6 +225,7 @@ where factory_ref: fref, extensions: self.extensions, state: self.state, + host: self.host, services: vec![Box::new(ServiceFactoryWrapper::new(service))], _t: PhantomData, } @@ -233,8 +238,8 @@ where /// html#method.host) documentation for more information. /// /// By default host name is set to a "localhost" value. - pub fn hostname(self, _val: &str) -> Self { - // self.host = val.to_owned(); + pub fn hostname(mut self, val: &str) -> Self { + self.host = val.to_owned(); self } } @@ -249,6 +254,7 @@ pub struct AppRouter { factory_ref: Rc>>>, extensions: Extensions, state: Vec>, + host: String, _t: PhantomData<(P, B)>, } @@ -343,6 +349,7 @@ where default: self.default, factory_ref: self.factory_ref, extensions: self.extensions, + host: self.host, _t: PhantomData, } } diff --git a/src/server.rs b/src/server.rs index d95743651..5d717817d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -2,9 +2,7 @@ use std::marker::PhantomData; use std::sync::Arc; use std::{fmt, io, net}; -use actix_http::{ - body::MessageBody, HttpService, KeepAlive, Request, Response, ServiceConfig, -}; +use actix_http::{body::MessageBody, HttpService, KeepAlive, Request, Response}; use actix_rt::System; use actix_server::{Server, ServerBuilder}; use actix_server_config::ServerConfig; @@ -13,8 +11,8 @@ use parking_lot::Mutex; use net2::TcpBuilder; -#[cfg(feature = "tls")] -use native_tls::TlsAcceptor; +// #[cfg(feature = "tls")] +// use native_tls::TlsAcceptor; #[cfg(feature = "ssl")] use openssl::ssl::{SslAcceptor, SslAcceptorBuilder}; @@ -245,27 +243,28 @@ where lst, move || { let c = cfg.lock(); - let service_config = - ServiceConfig::new(c.keep_alive, c.client_timeout, 0); - HttpService::with_config(service_config, factory()) + HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .finish(factory()) }, )?); Ok(self) } - #[cfg(feature = "tls")] - /// Use listener for accepting incoming tls connection requests - /// - /// HttpServer does not change any configuration for TcpListener, - /// it needs to be configured before passing it to listen() method. - pub fn listen_tls(self, lst: net::TcpListener, acceptor: TlsAcceptor) -> Self { - use actix_net::service::NewServiceExt; + // #[cfg(feature = "tls")] + // /// Use listener for accepting incoming tls connection requests + // /// + // /// HttpServer does not change any configuration for TcpListener, + // /// it needs to be configured before passing it to listen() method. + // pub fn listen_nativetls(self, lst: net::TcpListener, acceptor: TlsAcceptor) -> Self { + // use actix_server::ssl; - self.listen_with(lst, move || { - ssl::NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) - }) - } + // self.listen_with(lst, move || { + // ssl::NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) + // }) + // } #[cfg(feature = "ssl")] /// Use listener for accepting incoming tls connection requests @@ -276,12 +275,16 @@ where lst: net::TcpListener, builder: SslAcceptorBuilder, ) -> io::Result { - self.listen_ssl_inner(lst, openssl_acceptor(builder)?); + self.listen_ssl_inner(lst, openssl_acceptor(builder)?)?; Ok(self) } #[cfg(feature = "ssl")] - fn listen_ssl_inner(&mut self, lst: net::TcpListener, acceptor: SslAcceptor) { + fn listen_ssl_inner( + &mut self, + lst: net::TcpListener, + acceptor: SslAcceptor, + ) -> io::Result<()> { use actix_server::ssl::{OpensslAcceptor, SslError}; let acceptor = OpensslAcceptor::new(acceptor); @@ -298,15 +301,18 @@ where lst, move || { let c = cfg.lock(); - let service_config = - ServiceConfig::new(c.keep_alive, c.client_timeout, c.client_timeout); acceptor.clone().map_err(|e| SslError::Ssl(e)).and_then( - HttpService::with_config(service_config, factory()) + HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .client_disconnect(c.client_shutdown) + .finish(factory()) .map_err(|e| SslError::Service(e)) .map_init_err(|_| ()), ) }, - )); + )?); + Ok(()) } #[cfg(feature = "rust-tls")] @@ -315,7 +321,6 @@ where /// This method sets alpn protocols to "h2" and "http/1.1" pub fn listen_rustls(self, lst: net::TcpListener, config: ServerConfig) -> Self { use super::{RustlsAcceptor, ServerFlags}; - use actix_net::service::NewServiceExt; self.listen_with(lst, move || { RustlsAcceptor::with_flags(config.clone(), flags).map_err(|_| ()) @@ -366,22 +371,21 @@ where } } - #[cfg(feature = "tls")] - /// The ssl socket address to bind - /// - /// To bind multiple addresses this method can be called multiple times. - pub fn bind_tls( - self, - addr: A, - acceptor: TlsAcceptor, - ) -> io::Result { - use actix_net::service::NewServiceExt; - use actix_net::ssl::NativeTlsAcceptor; + // #[cfg(feature = "tls")] + // /// The ssl socket address to bind + // /// + // /// To bind multiple addresses this method can be called multiple times. + // pub fn bind_nativetls( + // self, + // addr: A, + // acceptor: TlsAcceptor, + // ) -> io::Result { + // use actix_server::ssl::NativeTlsAcceptor; - self.bind_with(addr, move || { - NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) - }) - } + // self.bind_with(addr, move || { + // NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) + // }) + // } #[cfg(feature = "ssl")] /// Start listening for incoming tls connections. @@ -399,7 +403,7 @@ where let acceptor = openssl_acceptor(builder)?; for lst in sockets { - self.listen_ssl_inner(lst, acceptor.clone()); + self.listen_ssl_inner(lst, acceptor.clone())?; } Ok(self) @@ -415,14 +419,7 @@ where builder: ServerConfig, ) -> io::Result { use super::{RustlsAcceptor, ServerFlags}; - use actix_net::service::NewServiceExt; - - // alpn support - let flags = if self.no_http2 { - ServerFlags::HTTP1 - } else { - ServerFlags::HTTP1 | ServerFlags::HTTP2 - }; + use actix_service::NewServiceExt; self.bind_with(addr, move || { RustlsAcceptor::with_flags(builder.clone(), flags).map_err(|_| ()) From 9c7056e9b84f63c78587e4ceab32f8c4b6e526aa Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 13:38:56 -0800 Subject: [PATCH 242/427] fix connector --- src/builder.rs | 2 +- src/client/connector.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index c55b8d5fc..1df96b0e1 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -69,7 +69,7 @@ where /// /// To disable timeout set value to 0. /// - /// By default disconnect timeout is set to 3000 milliseconds. + /// By default disconnect timeout is set to 0. pub fn client_disconnect(mut self, val: u64) -> Self { self.client_disconnect = val; self diff --git a/src/client/connector.rs b/src/client/connector.rs index 5eb85ba61..ccb5dbce5 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -269,7 +269,11 @@ mod connect_impl { impl Service for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service< + Request = Connect, + Response = (Connect, Io, Protocol), + Error = ConnectorError, + >, { type Request = Connect; type Response = IoConnection; From 54678308d0da4e917ab1cb98a62c986044e5d824 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 14:06:24 -0800 Subject: [PATCH 243/427] propogate app config with http request; add tests for url_for --- Cargo.toml | 3 +- actix-files/src/lib.rs | 41 ++++----- actix-files/src/named.rs | 2 - actix-session/src/cookie.rs | 10 ++- actix-web-codegen/src/lib.rs | 6 +- src/app.rs | 61 +++++++------- src/app_service.rs | 59 ++++++------- src/config.rs | 106 +++++++++++++++++------- src/error.rs | 2 + src/info.rs | 61 ++++++-------- src/lib.rs | 12 +-- src/request.rs | 156 +++++++++++++++++++++++++++++++---- src/resource.rs | 20 ++++- src/rmap.rs | 15 ++-- src/scope.rs | 4 +- src/service.rs | 18 ++-- src/state.rs | 2 +- src/test.rs | 34 +++++--- 18 files changed, 397 insertions(+), 215 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dbc0a65d2..9c3408251 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,9 +70,10 @@ actix-service = { git = "https://github.com/actix/actix-net.git" } actix-utils = { git = "https://github.com/actix/actix-net.git" } actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-router = { git = "https://github.com/actix/actix-net.git" } +#actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } actix-server-config = { git = "https://github.com/actix/actix-net.git" } +actix-router = { path = "../actix-net/router" } bytes = "0.4" derive_more = "0.14" diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 17efdd813..14c25be79 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -17,7 +17,7 @@ use v_htmlescape::escape as escape_html_entity; use actix_http::error::{Error, ErrorInternalServerError}; use actix_service::{boxed::BoxedNewService, NewService, Service}; -use actix_web::dev::{self, AppConfig, HttpServiceFactory, ResourceDef, Url}; +use actix_web::dev::{HttpServiceFactory, ResourceDef, ServiceConfig}; use actix_web::{ blocking, FromRequest, HttpRequest, HttpResponse, Responder, ServiceFromRequest, ServiceRequest, ServiceResponse, @@ -305,7 +305,7 @@ where P: 'static, C: StaticFileConfig + 'static, { - fn register(self, config: &mut AppConfig

    ) { + fn register(self, config: &mut ServiceConfig

    ) { if self.default.borrow().is_none() { *self.default.borrow_mut() = Some(config.default_service()); } @@ -314,11 +314,12 @@ where } else { ResourceDef::prefix(&self.path) }; - config.register_service(rdef, None, self) + config.register_service(rdef, None, self, None) } } -impl NewService> for Files { +impl NewService for Files { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type Service = FilesService; @@ -350,7 +351,8 @@ pub struct FilesService { _cd_map: PhantomData, } -impl Service> for FilesService { +impl Service for FilesService { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type Future = FutureResult; @@ -362,7 +364,7 @@ impl Service> for FilesService { fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { let (req, _) = req.into_parts(); - let real_path = match PathBufWrp::get_pathbuf(req.match_info()) { + let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) { Ok(item) => item, Err(e) => return ok(ServiceResponse::from_err(e, req.clone())), }; @@ -409,13 +411,13 @@ impl Service> for FilesService { } } +#[derive(Debug)] struct PathBufWrp(PathBuf); impl PathBufWrp { - fn get_pathbuf(path: &dev::Path) -> Result { - let path_str = path.path(); + fn get_pathbuf(path: &str) -> Result { let mut buf = PathBuf::new(); - for segment in path_str.split('/') { + for segment in path.split('/') { if segment == ".." { buf.pop(); } else if segment.starts_with('.') { @@ -447,13 +449,14 @@ impl

    FromRequest

    for PathBufWrp { type Config = (); fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - PathBufWrp::get_pathbuf(req.match_info()) + PathBufWrp::get_pathbuf(req.match_info().path()) } } #[cfg(test)] mod tests { use std::fs; + use std::iter::FromIterator; use std::ops::Add; use std::time::{Duration, SystemTime}; @@ -1093,32 +1096,32 @@ mod tests { #[test] fn test_path_buf() { assert_eq!( - PathBuf::from_param("/test/.tt"), + PathBufWrp::get_pathbuf("/test/.tt").map(|t| t.0), Err(UriSegmentError::BadStart('.')) ); assert_eq!( - PathBuf::from_param("/test/*tt"), + PathBufWrp::get_pathbuf("/test/*tt").map(|t| t.0), Err(UriSegmentError::BadStart('*')) ); assert_eq!( - PathBuf::from_param("/test/tt:"), + PathBufWrp::get_pathbuf("/test/tt:").map(|t| t.0), Err(UriSegmentError::BadEnd(':')) ); assert_eq!( - PathBuf::from_param("/test/tt<"), + PathBufWrp::get_pathbuf("/test/tt<").map(|t| t.0), Err(UriSegmentError::BadEnd('<')) ); assert_eq!( - PathBuf::from_param("/test/tt>"), + PathBufWrp::get_pathbuf("/test/tt>").map(|t| t.0), Err(UriSegmentError::BadEnd('>')) ); assert_eq!( - PathBuf::from_param("/seg1/seg2/"), - Ok(PathBuf::from_iter(vec!["seg1", "seg2"])) + PathBufWrp::get_pathbuf("/seg1/seg2/").unwrap().0, + PathBuf::from_iter(vec!["seg1", "seg2"]) ); assert_eq!( - PathBuf::from_param("/seg1/../seg2/"), - Ok(PathBuf::from_iter(vec!["seg2"])) + PathBufWrp::get_pathbuf("/seg1/../seg2/").unwrap().0, + PathBuf::from_iter(vec!["seg2"]) ); } } diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 2fc1c454d..6372a183c 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -304,8 +304,6 @@ impl Responder for NamedFile { type Future = Result; fn respond_to(self, req: &HttpRequest) -> Self::Future { - println!("RESP: {:?}", req); - if self.status_code != StatusCode::OK { let mut resp = HttpResponse::build(self.status_code); resp.set(header::ContentType(self.content_type.clone())) diff --git a/actix-session/src/cookie.rs b/actix-session/src/cookie.rs index 7fd5ec643..e25031456 100644 --- a/actix-session/src/cookie.rs +++ b/actix-session/src/cookie.rs @@ -255,12 +255,13 @@ impl CookieSession { } } -impl Transform> for CookieSession +impl Transform for CookieSession where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, S::Error: 'static, { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = S::Error; type InitError = (); @@ -281,12 +282,13 @@ pub struct CookieSessionMiddleware { inner: Rc, } -impl Service> for CookieSessionMiddleware +impl Service for CookieSessionMiddleware where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, S::Error: 'static, { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = S::Error; type Future = Box>; diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 26b422d77..13d1b97f5 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -31,7 +31,7 @@ pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { struct #name; impl actix_web::dev::HttpServiceFactory

    for #name { - fn register(self, config: &mut actix_web::dev::AppConfig

    ) { + fn register(self, config: &mut actix_web::dev::ServiceConfig

    ) { #ast actix_web::dev::HttpServiceFactory::register( actix_web::Resource::new(#path) @@ -68,7 +68,7 @@ pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { struct #name; impl actix_web::dev::HttpServiceFactory

    for #name { - fn register(self, config: &mut actix_web::dev::AppConfig

    ) { + fn register(self, config: &mut actix_web::dev::ServiceConfig

    ) { #ast actix_web::dev::HttpServiceFactory::register( actix_web::Resource::new(#path) @@ -105,7 +105,7 @@ pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { struct #name; impl actix_web::dev::HttpServiceFactory

    for #name { - fn register(self, config: &mut actix_web::dev::AppConfig

    ) { + fn register(self, config: &mut actix_web::dev::ServiceConfig

    ) { #ast actix_web::dev::HttpServiceFactory::register( actix_web::Resource::new(#path) diff --git a/src/app.rs b/src/app.rs index 42ce62d89..29dd1ab60 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,8 @@ use std::marker::PhantomData; use std::rc::Rc; use actix_http::body::{Body, MessageBody}; -use actix_http::{Extensions, PayloadStream}; +use actix_http::PayloadStream; +use actix_router::ResourceDef; use actix_server_config::ServerConfig; use actix_service::boxed::{self, BoxedNewService}; use actix_service::{ @@ -12,6 +13,7 @@ use actix_service::{ use futures::IntoFuture; use crate::app_service::{AppChain, AppEntry, AppInit, AppRouting, AppRoutingFactory}; +use crate::config::{AppConfig, AppConfigInner}; use crate::resource::Resource; use crate::route::Route; use crate::service::{ @@ -29,9 +31,8 @@ where T: NewService>, { chain: T, - extensions: Extensions, state: Vec>, - host: String, + config: AppConfigInner, _t: PhantomData<(P,)>, } @@ -41,9 +42,8 @@ impl App { pub fn new() -> Self { App { chain: AppChain, - extensions: Extensions::new(), state: Vec::new(), - host: "localhost:8080".to_string(), + config: AppConfigInner::default(), _t: PhantomData, } } @@ -141,8 +141,8 @@ where services: Vec::new(), default: None, factory_ref: fref, - extensions: self.extensions, - host: self.host, + config: self.config, + external: Vec::new(), _t: PhantomData, } } @@ -174,8 +174,7 @@ where App { chain, state: self.state, - extensions: self.extensions, - host: self.host, + config: self.config, _t: PhantomData, } } @@ -223,10 +222,10 @@ where default: None, endpoint: AppEntry::new(fref.clone()), factory_ref: fref, - extensions: self.extensions, state: self.state, - host: self.host, + config: self.config, services: vec![Box::new(ServiceFactoryWrapper::new(service))], + external: Vec::new(), _t: PhantomData, } } @@ -239,7 +238,7 @@ where /// /// By default host name is set to a "localhost" value. pub fn hostname(mut self, val: &str) -> Self { - self.host = val.to_owned(); + self.config.host = val.to_owned(); self } } @@ -252,9 +251,9 @@ pub struct AppRouter { services: Vec>>, default: Option>>, factory_ref: Rc>>>, - extensions: Extensions, state: Vec>, - host: String, + config: AppConfigInner, + external: Vec, _t: PhantomData<(P, B)>, } @@ -348,8 +347,8 @@ where services: self.services, default: self.default, factory_ref: self.factory_ref, - extensions: self.extensions, - host: self.host, + config: self.config, + external: self.external, _t: PhantomData, } } @@ -382,33 +381,30 @@ where /// and are never considered for matching at request time. Calls to /// `HttpRequest::url_for()` will work as expected. /// - /// ```rust,ignore - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest, HttpResponse, Result}; + /// ```rust + /// use actix_web::{web, App, HttpRequest, HttpResponse, Result}; /// - /// fn index(req: &HttpRequest) -> Result { - /// let url = req.url_for("youtube", &["oHg5SJYRHA0"])?; - /// assert_eq!(url.as_str(), "https://youtube.com/watch/oHg5SJYRHA0"); + /// fn index(req: HttpRequest) -> Result { + /// let url = req.url_for("youtube", &["asdlkjqme"])?; + /// assert_eq!(url.as_str(), "https://youtube.com/watch/asdlkjqme"); /// Ok(HttpResponse::Ok().into()) /// } /// /// fn main() { /// let app = App::new() - /// .resource("/index.html", |r| r.get().f(index)) - /// .external_resource("youtube", "https://youtube.com/watch/{video_id}") - /// .finish(); + /// .service(web::resource("/index.html").route( + /// web::get().to(index))) + /// .external_resource("youtube", "https://youtube.com/watch/{video_id}"); /// } /// ``` - pub fn external_resource(self, _name: N, _url: U) -> Self + pub fn external_resource(mut self, name: N, url: U) -> Self where N: AsRef, U: AsRef, { - // self.parts - // .as_mut() - // .expect("Use after finish") - // .router - // .register_external(name.as_ref(), ResourceDef::external(url.as_ref())); + let mut rdef = ResourceDef::new(url.as_ref()); + *rdef.name_mut() = name.as_ref().to_string(); + self.external.push(rdef); self } } @@ -435,9 +431,10 @@ where state: self.state, endpoint: self.endpoint, services: RefCell::new(self.services), + external: RefCell::new(self.external), default: self.default, factory_ref: self.factory_ref, - extensions: Rc::new(RefCell::new(Rc::new(self.extensions))), + config: RefCell::new(AppConfig(Rc::new(self.config))), } } } diff --git a/src/app_service.rs b/src/app_service.rs index 094486d9b..75e4b3164 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::marker::PhantomData; use std::rc::Rc; -use actix_http::{Extensions, Request, Response}; +use actix_http::{Request, Response}; use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; use actix_server_config::ServerConfig; use actix_service::boxed::{self, BoxedNewService, BoxedService}; @@ -10,7 +10,7 @@ use actix_service::{fn_service, AndThen, NewService, Service, ServiceExt}; use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, Poll}; -use crate::config::AppConfig; +use crate::config::{AppConfig, ServiceConfig}; use crate::guard::Guard; use crate::rmap::ResourceMap; use crate::service::{ServiceFactory, ServiceRequest, ServiceResponse}; @@ -36,10 +36,11 @@ where pub(crate) chain: C, pub(crate) endpoint: T, pub(crate) state: Vec>, - pub(crate) extensions: Rc>>, + pub(crate) config: RefCell, pub(crate) services: RefCell>>>, pub(crate) default: Option>>, pub(crate) factory_ref: Rc>>>, + pub(crate) external: RefCell>, } impl NewService for AppInit @@ -64,7 +65,7 @@ where type Service = AndThen, T::Service>; type Future = AppInitResult; - fn new_service(&self, _: &ServerConfig) -> Self::Future { + fn new_service(&self, cfg: &ServerConfig) -> Self::Future { // update resource default service let default = self.default.clone().unwrap_or_else(|| { Rc::new(boxed::new_service(fn_service(|req: ServiceRequest

    | { @@ -72,12 +73,15 @@ where }))) }); - let mut config = AppConfig::new( - "127.0.0.1:8080".parse().unwrap(), - "localhost:8080".to_owned(), - false, - default.clone(), - ); + { + let mut c = self.config.borrow_mut(); + let loc_cfg = Rc::get_mut(&mut c.0).unwrap(); + loc_cfg.secure = cfg.secure(); + loc_cfg.addr = cfg.local_addr(); + } + + let mut config = + ServiceConfig::new(self.config.borrow().clone(), default.clone()); // register services std::mem::replace(&mut *self.services.borrow_mut(), Vec::new()) @@ -101,6 +105,11 @@ where ), }); + // external resources + for mut rdef in std::mem::replace(&mut *self.external.borrow_mut(), Vec::new()) { + rmap.add(&mut rdef, None); + } + // complete ResourceMap tree creation let rmap = Rc::new(rmap); rmap.finish(rmap.clone()); @@ -111,7 +120,7 @@ where endpoint: None, endpoint_fut: self.endpoint.new_service(&()), state: self.state.iter().map(|s| s.construct()).collect(), - extensions: self.extensions.clone(), + config: self.config.borrow().clone(), rmap, _t: PhantomData, } @@ -129,7 +138,7 @@ where endpoint_fut: T::Future, rmap: Rc, state: Vec>, - extensions: Rc>>, + config: AppConfig, _t: PhantomData<(P, B)>, } @@ -152,20 +161,14 @@ where type Error = C::InitError; fn poll(&mut self) -> Poll { - if let Some(extensions) = Rc::get_mut(&mut *self.extensions.borrow_mut()) { - let mut idx = 0; - while idx < self.state.len() { - if let Async::Ready(_) = self.state[idx].poll_result(extensions)? { - self.state.remove(idx); - } else { - idx += 1; - } + let mut idx = 0; + let mut extensions = self.config.0.extensions.borrow_mut(); + while idx < self.state.len() { + if let Async::Ready(_) = self.state[idx].poll_result(&mut extensions)? { + self.state.remove(idx); + } else { + idx += 1; } - if !self.state.is_empty() { - return Ok(Async::NotReady); - } - } else { - log::warn!("Multiple copies of app extensions exists"); } if self.chain.is_none() { @@ -185,7 +188,7 @@ where AppInitService { chain: self.chain.take().unwrap(), rmap: self.rmap.clone(), - extensions: self.extensions.borrow().clone(), + config: self.config.clone(), } .and_then(self.endpoint.take().unwrap()), )) @@ -202,7 +205,7 @@ where { chain: C, rmap: Rc, - extensions: Rc, + config: AppConfig, } impl Service for AppInitService @@ -223,7 +226,7 @@ where Path::new(Url::new(req.uri().clone())), req, self.rmap.clone(), - self.extensions.clone(), + self.config.clone(), ); self.chain.call(req) } diff --git a/src/config.rs b/src/config.rs index 47c2f7c45..f84376c76 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,8 @@ +use std::cell::{Ref, RefCell}; use std::net::SocketAddr; use std::rc::Rc; +use actix_http::Extensions; use actix_router::ResourceDef; use actix_service::{boxed, IntoNewService, NewService}; @@ -13,10 +15,8 @@ type HttpNewService

    = boxed::BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; /// Application configuration -pub struct AppConfig

    { - addr: SocketAddr, - secure: bool, - host: String, +pub struct ServiceConfig

    { + config: AppConfig, root: bool, default: Rc>, services: Vec<( @@ -27,18 +27,11 @@ pub struct AppConfig

    { )>, } -impl AppConfig

    { +impl ServiceConfig

    { /// Crate server settings instance - pub(crate) fn new( - addr: SocketAddr, - host: String, - secure: bool, - default: Rc>, - ) -> Self { - AppConfig { - addr, - secure, - host, + pub(crate) fn new(config: AppConfig, default: Rc>) -> Self { + ServiceConfig { + config, default, root: true, services: Vec::new(), @@ -62,31 +55,20 @@ impl AppConfig

    { } pub(crate) fn clone_config(&self) -> Self { - AppConfig { - addr: self.addr, - secure: self.secure, - host: self.host.clone(), + ServiceConfig { + config: self.config.clone(), default: self.default.clone(), services: Vec::new(), root: false, } } - /// Returns the socket address of the local half of this TCP connection - pub fn local_addr(&self) -> SocketAddr { - self.addr - } - - /// Returns true if connection is secure(https) - pub fn secure(&self) -> bool { - self.secure - } - - /// Returns host header value - pub fn host(&self) -> &str { - &self.host + /// Service configuration + pub fn config(&self) -> &AppConfig { + &self.config } + /// Default resource pub fn default_service(&self) -> Rc> { self.default.clone() } @@ -114,3 +96,63 @@ impl AppConfig

    { )); } } + +#[derive(Clone)] +pub struct AppConfig(pub(crate) Rc); + +impl AppConfig { + pub(crate) fn new(inner: AppConfigInner) -> Self { + AppConfig(Rc::new(inner)) + } + + /// Set server host name. + /// + /// Host name is used by application router aa a hostname for url + /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. + /// html#method.host) documentation for more information. + /// + /// By default host name is set to a "localhost" value. + pub fn host(&self) -> &str { + &self.0.host + } + + /// Returns true if connection is secure(https) + pub fn secure(&self) -> bool { + self.0.secure + } + + /// Returns the socket address of the local half of this TCP connection + pub fn local_addr(&self) -> SocketAddr { + self.0.addr + } + + /// Resource map + pub fn rmap(&self) -> &ResourceMap { + &self.0.rmap + } + + /// Application extensions + pub fn extensions(&self) -> Ref { + self.0.extensions.borrow() + } +} + +pub(crate) struct AppConfigInner { + pub(crate) secure: bool, + pub(crate) host: String, + pub(crate) addr: SocketAddr, + pub(crate) rmap: ResourceMap, + pub(crate) extensions: RefCell, +} + +impl Default for AppConfigInner { + fn default() -> AppConfigInner { + AppConfigInner { + secure: false, + addr: "127.0.0.1:8080".parse().unwrap(), + host: "localhost:8080".to_owned(), + rmap: ResourceMap::new(ResourceDef::new("")), + extensions: RefCell::new(Extensions::new()), + } + } +} diff --git a/src/error.rs b/src/error.rs index d1c0d3ca9..54ca74dc2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +//! Error and Result module + pub use actix_http::error::*; use derive_more::{Display, From}; use url::ParseError as UrlParseError; diff --git a/src/info.rs b/src/info.rs index 3b51215fe..c058bd517 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,19 +1,14 @@ use std::cell::Ref; -use actix_http::http::header::{self, HeaderName}; -use actix_http::RequestHead; +use crate::dev::{AppConfig, RequestHead}; +use crate::http::header::{self, HeaderName}; const X_FORWARDED_FOR: &[u8] = b"x-forwarded-for"; const X_FORWARDED_HOST: &[u8] = b"x-forwarded-host"; const X_FORWARDED_PROTO: &[u8] = b"x-forwarded-proto"; -pub enum ConnectionInfoError { - UnknownHost, - UnknownScheme, -} - /// `HttpRequest` connection information -#[derive(Clone, Default)] +#[derive(Debug, Clone, Default)] pub struct ConnectionInfo { scheme: String, host: String, @@ -23,19 +18,19 @@ pub struct ConnectionInfo { impl ConnectionInfo { /// Create *ConnectionInfo* instance for a request. - pub fn get(req: &RequestHead) -> Ref { + pub fn get<'a>(req: &'a RequestHead, cfg: &AppConfig) -> Ref<'a, Self> { if !req.extensions().contains::() { - req.extensions_mut().insert(ConnectionInfo::new(req)); + req.extensions_mut().insert(ConnectionInfo::new(req, cfg)); } Ref::map(req.extensions(), |e| e.get().unwrap()) } #[cfg_attr(feature = "cargo-clippy", allow(cyclomatic_complexity))] - fn new(req: &RequestHead) -> ConnectionInfo { + fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { let mut host = None; let mut scheme = None; let mut remote = None; - let mut peer = None; + let peer = None; // load forwarded header for hdr in req.headers.get_all(header::FORWARDED) { @@ -82,7 +77,7 @@ impl ConnectionInfo { } if scheme.is_none() { scheme = req.uri.scheme_part().map(|a| a.as_str()); - if scheme.is_none() && req.server_settings().secure() { + if scheme.is_none() && cfg.secure() { scheme = Some("https") } } @@ -105,7 +100,7 @@ impl ConnectionInfo { if host.is_none() { host = req.uri.authority_part().map(|a| a.as_str()); if host.is_none() { - host = Some(req.server_settings().host()); + host = Some(cfg.host()); } } } @@ -121,10 +116,10 @@ impl ConnectionInfo { remote = h.split(',').next().map(|v| v.trim()); } } - if remote.is_none() { - // get peeraddr from socketaddr - peer = req.peer_addr().map(|addr| format!("{}", addr)); - } + // if remote.is_none() { + // get peeraddr from socketaddr + // peer = req.peer_addr().map(|addr| format!("{}", addr)); + // } } ConnectionInfo { @@ -186,9 +181,8 @@ mod tests { #[test] fn test_forwarded() { - let req = TestRequest::default().request(); - let mut info = ConnectionInfo::default(); - info.update(&req); + let req = TestRequest::default().to_http_request(); + let info = req.connection_info(); assert_eq!(info.scheme(), "http"); assert_eq!(info.host(), "localhost:8080"); @@ -197,44 +191,39 @@ mod tests { header::FORWARDED, "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org", ) - .request(); + .to_http_request(); - let mut info = ConnectionInfo::default(); - info.update(&req); + let info = req.connection_info(); assert_eq!(info.scheme(), "https"); assert_eq!(info.host(), "rust-lang.org"); assert_eq!(info.remote(), Some("192.0.2.60")); let req = TestRequest::default() .header(header::HOST, "rust-lang.org") - .request(); + .to_http_request(); - let mut info = ConnectionInfo::default(); - info.update(&req); + let info = req.connection_info(); assert_eq!(info.scheme(), "http"); assert_eq!(info.host(), "rust-lang.org"); assert_eq!(info.remote(), None); let req = TestRequest::default() .header(X_FORWARDED_FOR, "192.0.2.60") - .request(); - let mut info = ConnectionInfo::default(); - info.update(&req); + .to_http_request(); + let info = req.connection_info(); assert_eq!(info.remote(), Some("192.0.2.60")); let req = TestRequest::default() .header(X_FORWARDED_HOST, "192.0.2.60") - .request(); - let mut info = ConnectionInfo::default(); - info.update(&req); + .to_http_request(); + let info = req.connection_info(); assert_eq!(info.host(), "192.0.2.60"); assert_eq!(info.remote(), None); let req = TestRequest::default() .header(X_FORWARDED_PROTO, "https") - .request(); - let mut info = ConnectionInfo::default(); - info.update(&req); + .to_http_request(); + let info = req.connection_info(); assert_eq!(info.scheme(), "https"); } } diff --git a/src/lib.rs b/src/lib.rs index 19f466b4c..6329d53ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,13 +2,13 @@ mod app; mod app_service; -mod extract; -mod handler; -// mod info; pub mod blocking; mod config; pub mod error; +mod extract; pub mod guard; +mod handler; +mod info; pub mod middleware; mod request; mod resource; @@ -54,7 +54,9 @@ pub mod dev { //! ``` pub use crate::app::AppRouter; - pub use crate::config::AppConfig; + pub use crate::config::{AppConfig, ServiceConfig}; + pub use crate::info::ConnectionInfo; + pub use crate::rmap::ResourceMap; pub use crate::service::HttpServiceFactory; pub use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; @@ -62,7 +64,7 @@ pub mod dev { pub use actix_http::{ Extensions, Payload, PayloadStream, RequestHead, ResponseHead, }; - pub use actix_router::{Path, ResourceDef, Url}; + pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub(crate) fn insert_slash(path: &str) -> String { let mut path = path.to_owned(); diff --git a/src/request.rs b/src/request.rs index 6655f1baa..717514838 100644 --- a/src/request.rs +++ b/src/request.rs @@ -7,8 +7,10 @@ use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; use actix_router::{Path, Url}; +use crate::config::AppConfig; use crate::error::UrlGenerationError; use crate::extract::FromRequest; +use crate::info::ConnectionInfo; use crate::rmap::ResourceMap; use crate::service::ServiceFromRequest; @@ -18,7 +20,7 @@ pub struct HttpRequest { pub(crate) head: Message, pub(crate) path: Path, rmap: Rc, - extensions: Rc, + config: AppConfig, } impl HttpRequest { @@ -27,13 +29,13 @@ impl HttpRequest { head: Message, path: Path, rmap: Rc, - extensions: Rc, + config: AppConfig, ) -> HttpRequest { HttpRequest { head, path, rmap, - extensions, + config, } } } @@ -92,17 +94,17 @@ impl HttpRequest { &self.path } - /// Application extensions + /// App config #[inline] - pub fn app_extensions(&self) -> &Extensions { - &self.extensions + pub fn config(&self) -> &AppConfig { + &self.config } /// Generate url for named resource /// /// ```rust /// # extern crate actix_web; - /// # use actix_web::{App, HttpRequest, HttpResponse, http}; + /// # use actix_web::{web, App, HttpRequest, HttpResponse}; /// # /// fn index(req: HttpRequest) -> HttpResponse { /// let url = req.url_for("foo", &["1", "2", "3"]); // <- generate url for "foo" resource @@ -111,11 +113,10 @@ impl HttpRequest { /// /// fn main() { /// let app = App::new() - /// .resource("/test/{one}/{two}/{three}", |r| { - /// r.name("foo"); // <- set resource name, then it could be used in `url_for` - /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); - /// }) - /// .finish(); + /// .service(web::resource("/test/{one}/{two}/{three}") + /// .name("foo") // <- set resource name, then it could be used in `url_for` + /// .route(web::get().to(|| HttpResponse::Ok())) + /// ); /// } /// ``` pub fn url_for( @@ -139,11 +140,11 @@ impl HttpRequest { self.url_for(name, &NO_PARAMS) } - // /// Get *ConnectionInfo* for the correct request. - // #[inline] - // pub fn connection_info(&self) -> Ref { - // ConnectionInfo::get(&*self) - // } + /// Get *ConnectionInfo* for the current request. + #[inline] + pub fn connection_info(&self) -> Ref { + ConnectionInfo::get(&*self, &*self.config()) + } } impl Deref for HttpRequest { @@ -234,3 +235,124 @@ impl fmt::Debug for HttpRequest { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::{ResourceDef, ResourceMap}; + use crate::http::header; + use crate::test::TestRequest; + + #[test] + fn test_debug() { + let req = + TestRequest::with_header("content-type", "text/plain").to_http_request(); + let dbg = format!("{:?}", req); + assert!(dbg.contains("HttpRequest")); + } + + #[test] + fn test_no_request_cookies() { + let req = TestRequest::default().to_http_request(); + assert!(req.cookies().unwrap().is_empty()); + } + + #[test] + fn test_request_cookies() { + let req = TestRequest::default() + .header(header::COOKIE, "cookie1=value1") + .header(header::COOKIE, "cookie2=value2") + .to_http_request(); + { + let cookies = req.cookies().unwrap(); + assert_eq!(cookies.len(), 2); + assert_eq!(cookies[0].name(), "cookie1"); + assert_eq!(cookies[0].value(), "value1"); + assert_eq!(cookies[1].name(), "cookie2"); + assert_eq!(cookies[1].value(), "value2"); + } + + let cookie = req.cookie("cookie1"); + assert!(cookie.is_some()); + let cookie = cookie.unwrap(); + assert_eq!(cookie.name(), "cookie1"); + assert_eq!(cookie.value(), "value1"); + + let cookie = req.cookie("cookie-unknown"); + assert!(cookie.is_none()); + } + + #[test] + fn test_request_query() { + let req = TestRequest::with_uri("/?id=test").to_http_request(); + assert_eq!(req.query_string(), "id=test"); + } + + #[test] + fn test_url_for() { + let mut res = ResourceDef::new("/user/{name}.{ext}"); + *res.name_mut() = "index".to_string(); + + let mut rmap = ResourceMap::new(ResourceDef::new("")); + rmap.add(&mut res, None); + assert!(rmap.has_resource("/user/test.html")); + assert!(!rmap.has_resource("/test/unknown")); + + let req = TestRequest::with_header(header::HOST, "www.rust-lang.org") + .rmap(rmap) + .to_http_request(); + + assert_eq!( + req.url_for("unknown", &["test"]), + Err(UrlGenerationError::ResourceNotFound) + ); + assert_eq!( + req.url_for("index", &["test"]), + Err(UrlGenerationError::NotEnoughElements) + ); + let url = req.url_for("index", &["test", "html"]); + assert_eq!( + url.ok().unwrap().as_str(), + "http://www.rust-lang.org/user/test.html" + ); + } + + #[test] + fn test_url_for_static() { + let mut rdef = ResourceDef::new("/index.html"); + *rdef.name_mut() = "index".to_string(); + + let mut rmap = ResourceMap::new(ResourceDef::new("")); + rmap.add(&mut rdef, None); + + assert!(rmap.has_resource("/index.html")); + + let req = TestRequest::with_uri("/test") + .header(header::HOST, "www.rust-lang.org") + .rmap(rmap) + .to_http_request(); + let url = req.url_for_static("index"); + assert_eq!( + url.ok().unwrap().as_str(), + "http://www.rust-lang.org/index.html" + ); + } + + #[test] + fn test_url_for_external() { + let mut rdef = ResourceDef::new("https://youtube.com/watch/{video_id}"); + + *rdef.name_mut() = "youtube".to_string(); + + let mut rmap = ResourceMap::new(ResourceDef::new("")); + rmap.add(&mut rdef, None); + assert!(rmap.has_resource("https://youtube.com/watch/unknown")); + + let req = TestRequest::default().rmap(rmap).to_http_request(); + let url = req.url_for("youtube", &["oHg5SJYRHA0"]); + assert_eq!( + url.ok().unwrap().as_str(), + "https://youtube.com/watch/oHg5SJYRHA0" + ); + } +} diff --git a/src/resource.rs b/src/resource.rs index cc8316653..57f6f710a 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -9,7 +9,7 @@ use actix_service::{ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; -use crate::dev::{insert_slash, AppConfig, HttpServiceFactory, ResourceDef}; +use crate::dev::{insert_slash, HttpServiceFactory, ResourceDef, ServiceConfig}; use crate::extract::FromRequest; use crate::guard::Guard; use crate::handler::{AsyncFactory, Factory}; @@ -41,6 +41,7 @@ type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, pub struct Resource> { endpoint: T, rdef: String, + name: Option, routes: Vec>, guards: Vec>, default: Rc>>>>, @@ -54,6 +55,7 @@ impl

    Resource

    { Resource { routes: Vec::new(), rdef: path.to_string(), + name: None, endpoint: ResourceEndpoint::new(fref.clone()), factory_ref: fref, guards: Vec::new(), @@ -72,6 +74,14 @@ where InitError = (), >, { + /// Set resource name. + /// + /// Name is used for url generation. + pub fn name(mut self, name: &str) -> Self { + self.name = Some(name.to_string()); + self + } + /// Add match guard to a resource. /// /// ```rust @@ -240,6 +250,7 @@ where Resource { endpoint, rdef: self.rdef, + name: self.name, guards: self.guards, routes: self.routes, default: self.default, @@ -277,7 +288,7 @@ where InitError = (), > + 'static, { - fn register(mut self, config: &mut AppConfig

    ) { + fn register(mut self, config: &mut ServiceConfig

    ) { if self.default.borrow().is_none() { *self.default.borrow_mut() = Some(config.default_service()); } @@ -286,11 +297,14 @@ where } else { Some(std::mem::replace(&mut self.guards, Vec::new())) }; - let rdef = if config.is_root() || !self.rdef.is_empty() { + let mut rdef = if config.is_root() || !self.rdef.is_empty() { ResourceDef::new(&insert_slash(&self.rdef)) } else { ResourceDef::new(&self.rdef) }; + if let Some(ref name) = self.name { + *rdef.name_mut() = name.clone(); + } config.register_service(rdef, guards, self, None) } } diff --git a/src/rmap.rs b/src/rmap.rs index 4922084bf..35fe8ee32 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -64,14 +64,13 @@ impl ResourceMap { if self.patterns_for(name, &mut path, &mut elements)?.is_some() { if path.starts_with('/') { - // let conn = req.connection_info(); - // Ok(Url::parse(&format!( - // "{}://{}{}", - // conn.scheme(), - // conn.host(), - // path - // ))?) - unimplemented!() + let conn = req.connection_info(); + Ok(Url::parse(&format!( + "{}://{}{}", + conn.scheme(), + conn.host(), + path + ))?) } else { Ok(Url::parse(&path)?) } diff --git a/src/scope.rs b/src/scope.rs index 6c511c69c..3b5061737 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -10,7 +10,7 @@ use actix_service::{ use futures::future::{ok, Either, Future, FutureResult}; use futures::{Async, Poll}; -use crate::dev::{AppConfig, HttpServiceFactory}; +use crate::dev::{HttpServiceFactory, ServiceConfig}; use crate::guard::Guard; use crate::resource::Resource; use crate::rmap::ResourceMap; @@ -237,7 +237,7 @@ where InitError = (), > + 'static, { - fn register(self, config: &mut AppConfig

    ) { + fn register(self, config: &mut ServiceConfig

    ) { // update default resource if needed if self.default.borrow().is_none() { *self.default.borrow_mut() = Some(config.default_service()); diff --git a/src/service.rs b/src/service.rs index ba8114588..f4b63a460 100644 --- a/src/service.rs +++ b/src/service.rs @@ -13,16 +13,16 @@ use actix_http::{ use actix_router::{Path, Resource, Url}; use futures::future::{ok, FutureResult, IntoFuture}; -use crate::config::AppConfig; +use crate::config::{AppConfig, ServiceConfig}; use crate::request::HttpRequest; use crate::rmap::ResourceMap; pub trait HttpServiceFactory

    { - fn register(self, config: &mut AppConfig

    ); + fn register(self, config: &mut ServiceConfig

    ); } pub(crate) trait ServiceFactory

    { - fn register(&mut self, config: &mut AppConfig

    ); + fn register(&mut self, config: &mut ServiceConfig

    ); } pub(crate) struct ServiceFactoryWrapper { @@ -43,7 +43,7 @@ impl ServiceFactory

    for ServiceFactoryWrapper where T: HttpServiceFactory

    , { - fn register(&mut self, config: &mut AppConfig

    ) { + fn register(&mut self, config: &mut ServiceConfig

    ) { if let Some(item) = self.factory.take() { item.register(config) } @@ -60,12 +60,12 @@ impl

    ServiceRequest

    { path: Path, request: Request

    , rmap: Rc, - extensions: Rc, + config: AppConfig, ) -> Self { let (head, payload) = request.into_parts(); ServiceRequest { payload, - req: HttpRequest::new(head, path, rmap, extensions), + req: HttpRequest::new(head, path, rmap, config), } } @@ -156,10 +156,10 @@ impl

    ServiceRequest

    { &mut self.req.path } - /// Application extensions + /// Service configuration #[inline] - pub fn app_extensions(&self) -> &Extensions { - self.req.app_extensions() + pub fn app_config(&self) -> &AppConfig { + self.req.config() } /// Deconstruct request into parts diff --git a/src/state.rs b/src/state.rs index 265c6f017..2c623c70d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -52,7 +52,7 @@ impl FromRequest

    for State { #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - if let Some(st) = req.app_extensions().get::>() { + if let Some(st) = req.config().extensions().get::>() { Ok(st.clone()) } else { Err(ErrorInternalServerError( diff --git a/src/test.rs b/src/test.rs index c88835a3f..b47daa2c6 100644 --- a/src/test.rs +++ b/src/test.rs @@ -5,7 +5,7 @@ use std::rc::Rc; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, Version}; use actix_http::test::TestRequest as HttpTestRequest; -use actix_http::{Extensions, PayloadStream, Request}; +use actix_http::{PayloadStream, Request}; use actix_router::{Path, ResourceDef, Url}; use actix_rt::Runtime; use actix_server_config::ServerConfig; @@ -13,6 +13,7 @@ use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; use futures::Future; +use crate::config::{AppConfig, AppConfigInner}; use crate::request::HttpRequest; use crate::rmap::ResourceMap; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; @@ -142,16 +143,16 @@ where /// ``` pub struct TestRequest { req: HttpTestRequest, - extensions: Extensions, rmap: ResourceMap, + config: AppConfigInner, } impl Default for TestRequest { fn default() -> TestRequest { TestRequest { req: HttpTestRequest::default(), - extensions: Extensions::new(), rmap: ResourceMap::new(ResourceDef::new("")), + config: AppConfigInner::default(), } } } @@ -161,8 +162,8 @@ impl TestRequest { pub fn with_uri(path: &str) -> TestRequest { TestRequest { req: HttpTestRequest::default().uri(path).take(), - extensions: Extensions::new(), rmap: ResourceMap::new(ResourceDef::new("")), + config: AppConfigInner::default(), } } @@ -170,7 +171,7 @@ impl TestRequest { pub fn with_hdr(hdr: H) -> TestRequest { TestRequest { req: HttpTestRequest::default().set(hdr).take(), - extensions: Extensions::new(), + config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -183,7 +184,7 @@ impl TestRequest { { TestRequest { req: HttpTestRequest::default().header(key, value).take(), - extensions: Extensions::new(), + config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -192,7 +193,7 @@ impl TestRequest { pub fn get() -> TestRequest { TestRequest { req: HttpTestRequest::default().method(Method::GET).take(), - extensions: Extensions::new(), + config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -201,7 +202,7 @@ impl TestRequest { pub fn post() -> TestRequest { TestRequest { req: HttpTestRequest::default().method(Method::POST).take(), - extensions: Extensions::new(), + config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -247,8 +248,15 @@ impl TestRequest { } /// Set request config - pub fn config(mut self, data: T) -> Self { - self.extensions.insert(data); + pub fn config(self, data: T) -> Self { + self.config.extensions.borrow_mut().insert(data); + self + } + + #[cfg(test)] + /// Set request config + pub(crate) fn rmap(mut self, rmap: ResourceMap) -> Self { + self.rmap = rmap; self } @@ -260,7 +268,7 @@ impl TestRequest { Path::new(Url::new(req.uri().clone())), req, Rc::new(self.rmap), - Rc::new(self.extensions), + AppConfig::new(self.config), ) } @@ -277,7 +285,7 @@ impl TestRequest { Path::new(Url::new(req.uri().clone())), req, Rc::new(self.rmap), - Rc::new(self.extensions), + AppConfig::new(self.config), ) .into_request() } @@ -290,7 +298,7 @@ impl TestRequest { Path::new(Url::new(req.uri().clone())), req, Rc::new(self.rmap), - Rc::new(self.extensions), + AppConfig::new(self.config), ); ServiceFromRequest::new(req, None) } From d2dba028f60c1b9d4f75bc96ece6162baa3dfcd4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 14:07:43 -0800 Subject: [PATCH 244/427] fix dependency link --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9c3408251..dbc0a65d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,10 +70,9 @@ actix-service = { git = "https://github.com/actix/actix-net.git" } actix-utils = { git = "https://github.com/actix/actix-net.git" } actix-http = { git = "https://github.com/actix/actix-http.git" } -#actix-router = { git = "https://github.com/actix/actix-net.git" } +actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } actix-server-config = { git = "https://github.com/actix/actix-net.git" } -actix-router = { path = "../actix-net/router" } bytes = "0.4" derive_more = "0.14" From 6c4be45787ed0a3f684f844a67402eeebb395c77 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 14:33:33 -0800 Subject: [PATCH 245/427] update deps --- Cargo.toml | 14 ++++---------- test-server/Cargo.toml | 8 +++----- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 594ab88db..ae81f1520 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,15 +38,10 @@ ssl = ["openssl", "actix-connector/ssl"] fail = ["failure"] [dependencies] -#actix-service = "0.3.2" +actix-service = "0.3.3" actix-codec = "0.1.1" - -#actix-connector = "0.3.0" -#actix-utils = "0.3.1" - -actix-connector = { git="https://github.com/actix/actix-net.git" } -actix-service = { git="https://github.com/actix/actix-net.git" } -actix-utils = { git="https://github.com/actix/actix-net.git" } +actix-connector = "0.3.0" +actix-utils = "0.3.3" actix-server-config = { git="https://github.com/actix/actix-net.git" } base64 = "0.10" @@ -90,8 +85,7 @@ failure = { version = "0.1.5", optional = true } actix-rt = "0.2.0" #actix-server = { version = "0.3.0", features=["ssl"] } actix-server = { git="https://github.com/actix/actix-net.git", features=["ssl"] } -#actix-connector = { version = "0.3.0", features=["ssl"] } -actix-connector = { git="https://github.com/actix/actix-net.git", features=["ssl"] } +actix-connector = { version = "0.3.0", features=["ssl"] } actix-http-test = { path="test-server", features=["ssl"] } env_logger = "0.6" diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 554ab20e3..49f18f04e 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -32,15 +32,13 @@ session = ["cookie/secure"] ssl = ["openssl", "actix-http/ssl", "actix-server/ssl"] [dependencies] -actix-codec = "0.1" +actix-codec = "0.1.1" actix-rt = "0.2.0" actix-http = { path=".." } -#actix-service = "0.3.2" -actix-service = { git="https://github.com/actix/actix-net.git" } +actix-service = "0.3.3" #actix-server = "0.3.0" actix-server = { git="https://github.com/actix/actix-net.git" } -#actix-utils = "0.3.2" -actix-utils = { git="https://github.com/actix/actix-net.git" } +actix-utils = "0.3.2" base64 = "0.10" bytes = "0.4" From 85664cc6f7499b57ca979341780278eabeed1f83 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 14:56:18 -0800 Subject: [PATCH 246/427] update deps --- Cargo.toml | 12 +++--------- actix-files/Cargo.toml | 3 +-- actix-session/Cargo.toml | 12 ++++-------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dbc0a65d2..6be1996e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,16 +61,13 @@ ssl = ["openssl", "actix-server/ssl"] [dependencies] actix-codec = "0.1.0" -#actix-service = "0.3.2" -#actix-utils = "0.3.1" +actix-service = "0.3.3" +actix-utils = "0.3.3" +actix-router = "0.1.0" actix-rt = "0.2.0" actix-web-codegen = { path="actix-web-codegen" } -actix-service = { git = "https://github.com/actix/actix-net.git" } -actix-utils = { git = "https://github.com/actix/actix-net.git" } - actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } actix-server-config = { git = "https://github.com/actix/actix-net.git" } @@ -116,6 +113,3 @@ serde_derive = "1.0" lto = true opt-level = 3 codegen-units = 1 - -[patch.crates-io] -actix-service = { git = "https://github.com/actix/actix-net.git" } diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index bd61c880d..c0f38b9a6 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -20,8 +20,7 @@ path = "src/lib.rs" [dependencies] actix-web = { path=".." } actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-service = { git = "https://github.com/actix/actix-net.git" } -#actix-service = "0.3.0" +actix-service = "0.3.3" bytes = "0.4" futures = "0.1" diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml index 3bbeb4f8c..421c6fc42 100644 --- a/actix-session/Cargo.toml +++ b/actix-session/Cargo.toml @@ -25,13 +25,9 @@ cookie-session = ["cookie/secure"] [dependencies] actix-web = { path=".." } -actix-codec = "0.1.0" - -#actix-service = "0.3.2" -#actix-utils = "0.3.1" -actix-service = { git = "https://github.com/actix/actix-net.git" } -actix-utils = { git = "https://github.com/actix/actix-net.git" } - +actix-codec = "0.1.1" +actix-service = "0.3.3" +actix-utils = "0.3.3" actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } @@ -48,4 +44,4 @@ serde_json = "1.0" time = "0.1" [dev-dependencies] -actix-rt = "0.1.0" +actix-rt = "0.2.0" From 513ce0b08d51b75d73a98d6087cd329ee95ba09c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 17:42:35 -0800 Subject: [PATCH 247/427] add json and form client request's method --- src/client/request.rs | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/client/request.rs b/src/client/request.rs index 7e971756d..efae5b94e 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -7,6 +7,8 @@ use bytes::{BufMut, Bytes, BytesMut}; use cookie::{Cookie, CookieJar}; use futures::{Future, Stream}; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; +use serde::Serialize; +use serde_json; use crate::body::{BodyStream, MessageBody}; use crate::error::Error; @@ -558,6 +560,48 @@ impl ClientRequestBuilder { Ok(ClientRequest { head, body }) } + /// Set a JSON body and generate `ClientRequest` + /// + /// `ClientRequestBuilder` can not be used after this call. + pub fn json( + &mut self, + value: T, + ) -> Result, Error> { + let body = serde_json::to_string(&value)?; + + let contains = if let Some(head) = parts(&mut self.head, &self.err) { + head.headers.contains_key(header::CONTENT_TYPE) + } else { + true + }; + if !contains { + self.header(header::CONTENT_TYPE, "application/json"); + } + + Ok(self.body(body)?) + } + + /// Set a urlencoded body and generate `ClientRequest` + /// + /// `ClientRequestBuilder` can not be used after this call. + pub fn form( + &mut self, + value: T, + ) -> Result, Error> { + let body = serde_urlencoded::to_string(&value)?; + + let contains = if let Some(head) = parts(&mut self.head, &self.err) { + head.headers.contains_key(header::CONTENT_TYPE) + } else { + true + }; + if !contains { + self.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded"); + } + + Ok(self.body(body)?) + } + /// Set an streaming body and generate `ClientRequest`. /// /// `ClientRequestBuilder` can not be used after this call. From 134863d5c828fad574d75fa40347a7cfc379a5b4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 18:04:40 -0800 Subject: [PATCH 248/427] move middlewares --- errhandlers.rs | 141 +++++++++++++++++ identity.rs | 399 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 540 insertions(+) create mode 100644 errhandlers.rs create mode 100644 identity.rs diff --git a/errhandlers.rs b/errhandlers.rs new file mode 100644 index 000000000..c7d19d334 --- /dev/null +++ b/errhandlers.rs @@ -0,0 +1,141 @@ +use std::collections::HashMap; + +use error::Result; +use http::StatusCode; +use httprequest::HttpRequest; +use httpresponse::HttpResponse; +use middleware::{Middleware, Response}; + +type ErrorHandler = Fn(&HttpRequest, HttpResponse) -> Result; + +/// `Middleware` for allowing custom handlers for responses. +/// +/// You can use `ErrorHandlers::handler()` method to register a custom error +/// handler for specific status code. You can modify existing response or +/// create completely new one. +/// +/// ## Example +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::middleware::{ErrorHandlers, Response}; +/// use actix_web::{http, App, HttpRequest, HttpResponse, Result}; +/// +/// fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { +/// let mut builder = resp.into_builder(); +/// builder.header(http::header::CONTENT_TYPE, "application/json"); +/// Ok(Response::Done(builder.into())) +/// } +/// +/// fn main() { +/// let app = App::new() +/// .middleware( +/// ErrorHandlers::new() +/// .handler(http::StatusCode::INTERNAL_SERVER_ERROR, render_500), +/// ) +/// .resource("/test", |r| { +/// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); +/// r.method(http::Method::HEAD) +/// .f(|_| HttpResponse::MethodNotAllowed()); +/// }) +/// .finish(); +/// } +/// ``` +pub struct ErrorHandlers { + handlers: HashMap>>, +} + +impl Default for ErrorHandlers { + fn default() -> Self { + ErrorHandlers { + handlers: HashMap::new(), + } + } +} + +impl ErrorHandlers { + /// Construct new `ErrorHandlers` instance + pub fn new() -> Self { + ErrorHandlers::default() + } + + /// Register error handler for specified status code + pub fn handler(mut self, status: StatusCode, handler: F) -> Self + where + F: Fn(&HttpRequest, HttpResponse) -> Result + 'static, + { + self.handlers.insert(status, Box::new(handler)); + self + } +} + +impl Middleware for ErrorHandlers { + fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { + if let Some(handler) = self.handlers.get(&resp.status()) { + handler(req, resp) + } else { + Ok(Response::Done(resp)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use error::{Error, ErrorInternalServerError}; + use http::header::CONTENT_TYPE; + use http::StatusCode; + use httpmessage::HttpMessage; + use middleware::Started; + use test::{self, TestRequest}; + + fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { + let mut builder = resp.into_builder(); + builder.header(CONTENT_TYPE, "0001"); + Ok(Response::Done(builder.into())) + } + + #[test] + fn test_handler() { + let mw = + ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); + + let mut req = TestRequest::default().finish(); + let resp = HttpResponse::InternalServerError().finish(); + let resp = match mw.response(&mut req, resp) { + Ok(Response::Done(resp)) => resp, + _ => panic!(), + }; + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); + + let resp = HttpResponse::Ok().finish(); + let resp = match mw.response(&mut req, resp) { + Ok(Response::Done(resp)) => resp, + _ => panic!(), + }; + assert!(!resp.headers().contains_key(CONTENT_TYPE)); + } + + struct MiddlewareOne; + + impl Middleware for MiddlewareOne { + fn start(&self, _: &HttpRequest) -> Result { + Err(ErrorInternalServerError("middleware error")) + } + } + + #[test] + fn test_middleware_start_error() { + let mut srv = test::TestServer::new(move |app| { + app.middleware( + ErrorHandlers::new() + .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), + ).middleware(MiddlewareOne) + .handler(|_| HttpResponse::Ok()) + }); + + let request = srv.get().finish().unwrap(); + let response = srv.execute(request.send()).unwrap(); + assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "0001"); + } +} diff --git a/identity.rs b/identity.rs new file mode 100644 index 000000000..a664ba1f0 --- /dev/null +++ b/identity.rs @@ -0,0 +1,399 @@ +//! Request identity service for Actix applications. +//! +//! [**IdentityService**](struct.IdentityService.html) middleware can be +//! used with different policies types to store identity information. +//! +//! By default, only cookie identity policy is implemented. Other backend +//! implementations can be added separately. +//! +//! [**CookieIdentityPolicy**](struct.CookieIdentityPolicy.html) +//! uses cookies as identity storage. +//! +//! To access current request identity +//! [**RequestIdentity**](trait.RequestIdentity.html) should be used. +//! *HttpRequest* implements *RequestIdentity* trait. +//! +//! ```rust +//! use actix_web::middleware::identity::RequestIdentity; +//! use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; +//! use actix_web::*; +//! +//! fn index(req: HttpRequest) -> Result { +//! // access request identity +//! if let Some(id) = req.identity() { +//! Ok(format!("Welcome! {}", id)) +//! } else { +//! Ok("Welcome Anonymous!".to_owned()) +//! } +//! } +//! +//! fn login(mut req: HttpRequest) -> HttpResponse { +//! req.remember("User1".to_owned()); // <- remember identity +//! HttpResponse::Ok().finish() +//! } +//! +//! fn logout(mut req: HttpRequest) -> HttpResponse { +//! req.forget(); // <- remove identity +//! HttpResponse::Ok().finish() +//! } +//! +//! fn main() { +//! let app = App::new().middleware(IdentityService::new( +//! // <- create identity middleware +//! CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend +//! .name("auth-cookie") +//! .secure(false), +//! )); +//! } +//! ``` +use std::rc::Rc; + +use cookie::{Cookie, CookieJar, Key, SameSite}; +use futures::future::{err as FutErr, ok as FutOk, FutureResult}; +use futures::Future; +use time::Duration; + +use error::{Error, Result}; +use http::header::{self, HeaderValue}; +use httprequest::HttpRequest; +use httpresponse::HttpResponse; +use middleware::{Middleware, Response, Started}; + +/// The helper trait to obtain your identity from a request. +/// +/// ```rust +/// use actix_web::middleware::identity::RequestIdentity; +/// use actix_web::*; +/// +/// fn index(req: HttpRequest) -> Result { +/// // access request identity +/// if let Some(id) = req.identity() { +/// Ok(format!("Welcome! {}", id)) +/// } else { +/// Ok("Welcome Anonymous!".to_owned()) +/// } +/// } +/// +/// fn login(mut req: HttpRequest) -> HttpResponse { +/// req.remember("User1".to_owned()); // <- remember identity +/// HttpResponse::Ok().finish() +/// } +/// +/// fn logout(mut req: HttpRequest) -> HttpResponse { +/// req.forget(); // <- remove identity +/// HttpResponse::Ok().finish() +/// } +/// # fn main() {} +/// ``` +pub trait RequestIdentity { + /// Return the claimed identity of the user associated request or + /// ``None`` if no identity can be found associated with the request. + fn identity(&self) -> Option; + + /// Remember identity. + fn remember(&self, identity: String); + + /// This method is used to 'forget' the current identity on subsequent + /// requests. + fn forget(&self); +} + +impl RequestIdentity for HttpRequest { + fn identity(&self) -> Option { + if let Some(id) = self.extensions().get::() { + return id.0.identity().map(|s| s.to_owned()); + } + None + } + + fn remember(&self, identity: String) { + if let Some(id) = self.extensions_mut().get_mut::() { + return id.0.as_mut().remember(identity); + } + } + + fn forget(&self) { + if let Some(id) = self.extensions_mut().get_mut::() { + return id.0.forget(); + } + } +} + +/// An identity +pub trait Identity: 'static { + /// Return the claimed identity of the user associated request or + /// ``None`` if no identity can be found associated with the request. + fn identity(&self) -> Option<&str>; + + /// Remember identity. + fn remember(&mut self, key: String); + + /// This method is used to 'forget' the current identity on subsequent + /// requests. + fn forget(&mut self); + + /// Write session to storage backend. + fn write(&mut self, resp: HttpResponse) -> Result; +} + +/// Identity policy definition. +pub trait IdentityPolicy: Sized + 'static { + /// The associated identity + type Identity: Identity; + + /// The return type of the middleware + type Future: Future; + + /// Parse the session from request and load data from a service identity. + fn from_request(&self, request: &HttpRequest) -> Self::Future; +} + +/// Request identity middleware +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; +/// use actix_web::App; +/// +/// fn main() { +/// let app = App::new().middleware(IdentityService::new( +/// // <- create identity middleware +/// CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend +/// .name("auth-cookie") +/// .secure(false), +/// )); +/// } +/// ``` +pub struct IdentityService { + backend: T, +} + +impl IdentityService { + /// Create new identity service with specified backend. + pub fn new(backend: T) -> Self { + IdentityService { backend } + } +} + +struct IdentityBox(Box); + +impl> Middleware for IdentityService { + fn start(&self, req: &HttpRequest) -> Result { + let req = req.clone(); + let fut = self.backend.from_request(&req).then(move |res| match res { + Ok(id) => { + req.extensions_mut().insert(IdentityBox(Box::new(id))); + FutOk(None) + } + Err(err) => FutErr(err), + }); + Ok(Started::Future(Box::new(fut))) + } + + fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { + if let Some(ref mut id) = req.extensions_mut().get_mut::() { + id.0.as_mut().write(resp) + } else { + Ok(Response::Done(resp)) + } + } +} + +#[doc(hidden)] +/// Identity that uses private cookies as identity storage. +pub struct CookieIdentity { + changed: bool, + identity: Option, + inner: Rc, +} + +impl Identity for CookieIdentity { + fn identity(&self) -> Option<&str> { + self.identity.as_ref().map(|s| s.as_ref()) + } + + fn remember(&mut self, value: String) { + self.changed = true; + self.identity = Some(value); + } + + fn forget(&mut self) { + self.changed = true; + self.identity = None; + } + + fn write(&mut self, mut resp: HttpResponse) -> Result { + if self.changed { + let _ = self.inner.set_cookie(&mut resp, self.identity.take()); + } + Ok(Response::Done(resp)) + } +} + +struct CookieIdentityInner { + key: Key, + name: String, + path: String, + domain: Option, + secure: bool, + max_age: Option, + same_site: Option, +} + +impl CookieIdentityInner { + fn new(key: &[u8]) -> CookieIdentityInner { + CookieIdentityInner { + key: Key::from_master(key), + name: "actix-identity".to_owned(), + path: "/".to_owned(), + domain: None, + secure: true, + max_age: None, + same_site: None, + } + } + + fn set_cookie(&self, resp: &mut HttpResponse, id: Option) -> Result<()> { + let some = id.is_some(); + { + let id = id.unwrap_or_else(String::new); + let mut cookie = Cookie::new(self.name.clone(), id); + cookie.set_path(self.path.clone()); + cookie.set_secure(self.secure); + cookie.set_http_only(true); + + if let Some(ref domain) = self.domain { + cookie.set_domain(domain.clone()); + } + + if let Some(max_age) = self.max_age { + cookie.set_max_age(max_age); + } + + if let Some(same_site) = self.same_site { + cookie.set_same_site(same_site); + } + + let mut jar = CookieJar::new(); + if some { + jar.private(&self.key).add(cookie); + } else { + jar.add_original(cookie.clone()); + jar.private(&self.key).remove(cookie); + } + + for cookie in jar.delta() { + let val = HeaderValue::from_str(&cookie.to_string())?; + resp.headers_mut().append(header::SET_COOKIE, val); + } + } + + Ok(()) + } + + fn load(&self, req: &HttpRequest) -> Option { + if let Ok(cookies) = req.cookies() { + for cookie in cookies.iter() { + if cookie.name() == self.name { + let mut jar = CookieJar::new(); + jar.add_original(cookie.clone()); + + let cookie_opt = jar.private(&self.key).get(&self.name); + if let Some(cookie) = cookie_opt { + return Some(cookie.value().into()); + } + } + } + } + None + } +} + +/// Use cookies for request identity storage. +/// +/// The constructors take a key as an argument. +/// This is the private key for cookie - when this value is changed, +/// all identities are lost. The constructors will panic if the key is less +/// than 32 bytes in length. +/// +/// # Example +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; +/// use actix_web::App; +/// +/// fn main() { +/// let app = App::new().middleware(IdentityService::new( +/// // <- create identity middleware +/// CookieIdentityPolicy::new(&[0; 32]) // <- construct cookie policy +/// .domain("www.rust-lang.org") +/// .name("actix_auth") +/// .path("/") +/// .secure(true), +/// )); +/// } +/// ``` +pub struct CookieIdentityPolicy(Rc); + +impl CookieIdentityPolicy { + /// Construct new `CookieIdentityPolicy` instance. + /// + /// Panics if key length is less than 32 bytes. + pub fn new(key: &[u8]) -> CookieIdentityPolicy { + CookieIdentityPolicy(Rc::new(CookieIdentityInner::new(key))) + } + + /// Sets the `path` field in the session cookie being built. + pub fn path>(mut self, value: S) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().path = value.into(); + self + } + + /// Sets the `name` field in the session cookie being built. + pub fn name>(mut self, value: S) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().name = value.into(); + self + } + + /// Sets the `domain` field in the session cookie being built. + pub fn domain>(mut self, value: S) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); + self + } + + /// Sets the `secure` field in the session cookie being built. + /// + /// If the `secure` field is set, a cookie will only be transmitted when the + /// connection is secure - i.e. `https` + pub fn secure(mut self, value: bool) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().secure = value; + self + } + + /// Sets the `max-age` field in the session cookie being built. + pub fn max_age(mut self, value: Duration) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); + self + } + + /// Sets the `same_site` field in the session cookie being built. + pub fn same_site(mut self, same_site: SameSite) -> Self { + Rc::get_mut(&mut self.0).unwrap().same_site = Some(same_site); + self + } +} + +impl IdentityPolicy for CookieIdentityPolicy { + type Identity = CookieIdentity; + type Future = FutureResult; + + fn from_request(&self, req: &HttpRequest) -> Self::Future { + let identity = self.0.load(req); + FutOk(CookieIdentity { + identity, + changed: false, + inner: Rc::clone(&self.0), + }) + } +} From 12f0c78091a7d2ec668345e231aed8ad3318b88d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 20:40:09 -0800 Subject: [PATCH 249/427] port identity middleware --- Cargo.toml | 7 +- errhandlers.rs | 141 ---------- identity.rs => src/middleware/identity.rs | 320 +++++++++++++--------- src/middleware/mod.rs | 7 +- src/service.rs | 11 +- 5 files changed, 211 insertions(+), 275 deletions(-) delete mode 100644 errhandlers.rs rename identity.rs => src/middleware/identity.rs (53%) diff --git a/Cargo.toml b/Cargo.toml index 6be1996e0..c64f4bc68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,10 +33,10 @@ members = [ ] [package.metadata.docs.rs] -features = ["ssl", "tls", "rust-tls"] #, "session"] +features = ["ssl", "tls", "rust-tls", "session"] [features] -default = ["brotli", "flate2-c"] +default = ["brotli", "flate2-c", "session"] # brotli encoding, requires c compiler brotli = ["brotli2"] @@ -48,7 +48,7 @@ flate2-c = ["flate2/miniz-sys"] flate2-rust = ["flate2/rust_backend"] # sessions feature, session require "ring" crate and c compiler -# session = ["actix-session"] +session = ["cookie/secure"] # tls tls = ["native-tls", "actix-server/ssl"] @@ -72,6 +72,7 @@ actix-server = { git = "https://github.com/actix/actix-net.git" } actix-server-config = { git = "https://github.com/actix/actix-net.git" } bytes = "0.4" +cookie = { version="0.11", features=["percent-encode"] } derive_more = "0.14" encoding = "0.2" futures = "0.1" diff --git a/errhandlers.rs b/errhandlers.rs deleted file mode 100644 index c7d19d334..000000000 --- a/errhandlers.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::collections::HashMap; - -use error::Result; -use http::StatusCode; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response}; - -type ErrorHandler = Fn(&HttpRequest, HttpResponse) -> Result; - -/// `Middleware` for allowing custom handlers for responses. -/// -/// You can use `ErrorHandlers::handler()` method to register a custom error -/// handler for specific status code. You can modify existing response or -/// create completely new one. -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::{ErrorHandlers, Response}; -/// use actix_web::{http, App, HttpRequest, HttpResponse, Result}; -/// -/// fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { -/// let mut builder = resp.into_builder(); -/// builder.header(http::header::CONTENT_TYPE, "application/json"); -/// Ok(Response::Done(builder.into())) -/// } -/// -/// fn main() { -/// let app = App::new() -/// .middleware( -/// ErrorHandlers::new() -/// .handler(http::StatusCode::INTERNAL_SERVER_ERROR, render_500), -/// ) -/// .resource("/test", |r| { -/// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -/// r.method(http::Method::HEAD) -/// .f(|_| HttpResponse::MethodNotAllowed()); -/// }) -/// .finish(); -/// } -/// ``` -pub struct ErrorHandlers { - handlers: HashMap>>, -} - -impl Default for ErrorHandlers { - fn default() -> Self { - ErrorHandlers { - handlers: HashMap::new(), - } - } -} - -impl ErrorHandlers { - /// Construct new `ErrorHandlers` instance - pub fn new() -> Self { - ErrorHandlers::default() - } - - /// Register error handler for specified status code - pub fn handler(mut self, status: StatusCode, handler: F) -> Self - where - F: Fn(&HttpRequest, HttpResponse) -> Result + 'static, - { - self.handlers.insert(status, Box::new(handler)); - self - } -} - -impl Middleware for ErrorHandlers { - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(handler) = self.handlers.get(&resp.status()) { - handler(req, resp) - } else { - Ok(Response::Done(resp)) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use error::{Error, ErrorInternalServerError}; - use http::header::CONTENT_TYPE; - use http::StatusCode; - use httpmessage::HttpMessage; - use middleware::Started; - use test::{self, TestRequest}; - - fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { - let mut builder = resp.into_builder(); - builder.header(CONTENT_TYPE, "0001"); - Ok(Response::Done(builder.into())) - } - - #[test] - fn test_handler() { - let mw = - ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); - - let mut req = TestRequest::default().finish(); - let resp = HttpResponse::InternalServerError().finish(); - let resp = match mw.response(&mut req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - - let resp = HttpResponse::Ok().finish(); - let resp = match mw.response(&mut req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert!(!resp.headers().contains_key(CONTENT_TYPE)); - } - - struct MiddlewareOne; - - impl Middleware for MiddlewareOne { - fn start(&self, _: &HttpRequest) -> Result { - Err(ErrorInternalServerError("middleware error")) - } - } - - #[test] - fn test_middleware_start_error() { - let mut srv = test::TestServer::new(move |app| { - app.middleware( - ErrorHandlers::new() - .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), - ).middleware(MiddlewareOne) - .handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "0001"); - } -} diff --git a/identity.rs b/src/middleware/identity.rs similarity index 53% rename from identity.rs rename to src/middleware/identity.rs index a664ba1f0..d04ed717c 100644 --- a/identity.rs +++ b/src/middleware/identity.rs @@ -14,26 +14,26 @@ //! *HttpRequest* implements *RequestIdentity* trait. //! //! ```rust -//! use actix_web::middleware::identity::RequestIdentity; +//! use actix_web::middleware::identity::Identity; //! use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; //! use actix_web::*; //! -//! fn index(req: HttpRequest) -> Result { +//! fn index(id: Identity) -> String { //! // access request identity -//! if let Some(id) = req.identity() { -//! Ok(format!("Welcome! {}", id)) +//! if let Some(id) = id.identity() { +//! format!("Welcome! {}", id) //! } else { -//! Ok("Welcome Anonymous!".to_owned()) +//! "Welcome Anonymous!".to_owned() //! } //! } //! -//! fn login(mut req: HttpRequest) -> HttpResponse { -//! req.remember("User1".to_owned()); // <- remember identity +//! fn login(id: Idenity) -> HttpResponse { +//! id.remember("User1".to_owned()); // <- remember identity //! HttpResponse::Ok().finish() //! } //! -//! fn logout(mut req: HttpRequest) -> HttpResponse { -//! req.forget(); // <- remove identity +//! fn logout(id: Identity) -> HttpResponse { +//! id.forget(); // <- remove identity //! HttpResponse::Ok().finish() //! } //! @@ -42,118 +42,144 @@ //! // <- create identity middleware //! CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend //! .name("auth-cookie") -//! .secure(false), +//! .secure(false)) +//! .service(web::resource("/index.html").to(index) +//! .service(web::resource("/login.html").to(login) +//! .service(web::resource("/logout.html").to(logout) //! )); //! } //! ``` +use std::cell::RefCell; use std::rc::Rc; +use actix_service::{Service, Transform}; use cookie::{Cookie, CookieJar, Key, SameSite}; -use futures::future::{err as FutErr, ok as FutOk, FutureResult}; -use futures::Future; +use futures::future::{ok, Either, FutureResult}; +use futures::{Future, IntoFuture, Poll}; use time::Duration; -use error::{Error, Result}; -use http::header::{self, HeaderValue}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; +use crate::error::{Error, Result}; +use crate::http::header::{self, HeaderValue}; +use crate::request::HttpRequest; +use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; +use crate::FromRequest; +use crate::HttpMessage; -/// The helper trait to obtain your identity from a request. +/// The extractor type to obtain your identity from a request. /// /// ```rust -/// use actix_web::middleware::identity::RequestIdentity; /// use actix_web::*; +/// use actix_web::middleware::identity::Identity; /// -/// fn index(req: HttpRequest) -> Result { +/// fn index(id: Identity) -> Result { /// // access request identity -/// if let Some(id) = req.identity() { +/// if let Some(id) = id.identity() { /// Ok(format!("Welcome! {}", id)) /// } else { /// Ok("Welcome Anonymous!".to_owned()) /// } /// } /// -/// fn login(mut req: HttpRequest) -> HttpResponse { -/// req.remember("User1".to_owned()); // <- remember identity +/// fn login(id: Identity) -> HttpResponse { +/// id.remember("User1".to_owned()); // <- remember identity /// HttpResponse::Ok().finish() /// } /// -/// fn logout(mut req: HttpRequest) -> HttpResponse { -/// req.forget(); // <- remove identity +/// fn logout(id: Identity) -> HttpResponse { +/// id.forget(); // <- remove identity /// HttpResponse::Ok().finish() /// } /// # fn main() {} /// ``` -pub trait RequestIdentity { +#[derive(Clone)] +pub struct Identity(HttpRequest); + +impl Identity { /// Return the claimed identity of the user associated request or /// ``None`` if no identity can be found associated with the request. - fn identity(&self) -> Option; + pub fn identity(&self) -> Option { + if let Some(id) = self.0.extensions().get::() { + id.id.clone() + } else { + None + } + } /// Remember identity. - fn remember(&self, identity: String); + pub fn remember(&self, identity: String) { + if let Some(id) = self.0.extensions_mut().get_mut::() { + id.id = Some(identity); + id.changed = true; + } + } /// This method is used to 'forget' the current identity on subsequent /// requests. - fn forget(&self); -} - -impl RequestIdentity for HttpRequest { - fn identity(&self) -> Option { - if let Some(id) = self.extensions().get::() { - return id.0.identity().map(|s| s.to_owned()); - } - None - } - - fn remember(&self, identity: String) { - if let Some(id) = self.extensions_mut().get_mut::() { - return id.0.as_mut().remember(identity); - } - } - - fn forget(&self) { - if let Some(id) = self.extensions_mut().get_mut::() { - return id.0.forget(); + pub fn forget(&self) { + if let Some(id) = self.0.extensions_mut().get_mut::() { + id.id = None; + id.changed = true; } } } -/// An identity -pub trait Identity: 'static { - /// Return the claimed identity of the user associated request or - /// ``None`` if no identity can be found associated with the request. - fn identity(&self) -> Option<&str>; +struct IdentityItem { + id: Option, + changed: bool, +} - /// Remember identity. - fn remember(&mut self, key: String); +/// Extractor implementation for Identity type. +/// +/// ```rust +/// # use actix_web::*; +/// use actix_web::middleware::identity::Identity; +/// +/// fn index(id: Identity) -> String { +/// // access request identity +/// if let Some(id) = id.identity() { +/// format!("Welcome! {}", id) +/// } else { +/// "Welcome Anonymous!".to_owned() +/// } +/// } +/// # fn main() {} +/// ``` +impl

    FromRequest

    for Identity { + type Error = Error; + type Future = Result; + type Config = (); - /// This method is used to 'forget' the current identity on subsequent - /// requests. - fn forget(&mut self); - - /// Write session to storage backend. - fn write(&mut self, resp: HttpResponse) -> Result; + #[inline] + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + Ok(Identity(req.clone())) + } } /// Identity policy definition. -pub trait IdentityPolicy: Sized + 'static { - /// The associated identity - type Identity: Identity; +pub trait IdentityPolicy: Sized + 'static { + /// The return type of the middleware + type Future: IntoFuture, Error = Error>; /// The return type of the middleware - type Future: Future; + type ResponseFuture: IntoFuture; /// Parse the session from request and load data from a service identity. - fn from_request(&self, request: &HttpRequest) -> Self::Future; + fn from_request

    (&self, request: &mut ServiceRequest

    ) -> Self::Future; + + /// Write changes to response + fn to_response( + &self, + identity: Option, + changed: bool, + response: &mut ServiceResponse, + ) -> Self::ResponseFuture; } /// Request identity middleware /// /// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; /// use actix_web::App; +/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; /// /// fn main() { /// let app = App::new().middleware(IdentityService::new( @@ -165,68 +191,97 @@ pub trait IdentityPolicy: Sized + 'static { /// } /// ``` pub struct IdentityService { - backend: T, + backend: Rc, } impl IdentityService { /// Create new identity service with specified backend. pub fn new(backend: T) -> Self { - IdentityService { backend } - } -} - -struct IdentityBox(Box); - -impl> Middleware for IdentityService { - fn start(&self, req: &HttpRequest) -> Result { - let req = req.clone(); - let fut = self.backend.from_request(&req).then(move |res| match res { - Ok(id) => { - req.extensions_mut().insert(IdentityBox(Box::new(id))); - FutOk(None) - } - Err(err) => FutErr(err), - }); - Ok(Started::Future(Box::new(fut))) - } - - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(ref mut id) = req.extensions_mut().get_mut::() { - id.0.as_mut().write(resp) - } else { - Ok(Response::Done(resp)) + IdentityService { + backend: Rc::new(backend), } } } -#[doc(hidden)] -/// Identity that uses private cookies as identity storage. -pub struct CookieIdentity { - changed: bool, - identity: Option, - inner: Rc, +impl Transform for IdentityService +where + P: 'static, + S: Service, Response = ServiceResponse> + 'static, + S::Future: 'static, + T: IdentityPolicy, + B: 'static, +{ + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = S::Error; + type InitError = (); + type Transform = IdentityServiceMiddleware; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(IdentityServiceMiddleware { + backend: self.backend.clone(), + service: Rc::new(RefCell::new(service)), + }) + } } -impl Identity for CookieIdentity { - fn identity(&self) -> Option<&str> { - self.identity.as_ref().map(|s| s.as_ref()) +pub struct IdentityServiceMiddleware { + backend: Rc, + service: Rc>, +} + +impl Service for IdentityServiceMiddleware +where + P: 'static, + B: 'static, + S: Service, Response = ServiceResponse> + 'static, + S::Future: 'static, + T: IdentityPolicy, +{ + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = S::Error; + type Future = Box>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.borrow_mut().poll_ready() } - fn remember(&mut self, value: String) { - self.changed = true; - self.identity = Some(value); - } + fn call(&mut self, mut req: ServiceRequest

    ) -> Self::Future { + let srv = self.service.clone(); + let backend = self.backend.clone(); - fn forget(&mut self) { - self.changed = true; - self.identity = None; - } + Box::new( + self.backend.from_request(&mut req).into_future().then( + move |res| match res { + Ok(id) => { + req.extensions_mut() + .insert(IdentityItem { id, changed: false }); - fn write(&mut self, mut resp: HttpResponse) -> Result { - if self.changed { - let _ = self.inner.set_cookie(&mut resp, self.identity.take()); - } - Ok(Response::Done(resp)) + Either::A(srv.borrow_mut().call(req).and_then(move |mut res| { + let id = + res.request().extensions_mut().remove::(); + + if let Some(id) = id { + return Either::A( + backend + .to_response(id.id, id.changed, &mut res) + .into_future() + .then(move |t| match t { + Ok(_) => Ok(res), + Err(e) => Ok(res.error_response(e)), + }), + ); + } else { + Either::B(ok(res)) + } + })) + } + Err(err) => Either::B(ok(req.error_response(err))), + }, + ), + ) } } @@ -253,7 +308,11 @@ impl CookieIdentityInner { } } - fn set_cookie(&self, resp: &mut HttpResponse, id: Option) -> Result<()> { + fn set_cookie( + &self, + resp: &mut ServiceResponse, + id: Option, + ) -> Result<()> { let some = id.is_some(); { let id = id.unwrap_or_else(String::new); @@ -291,7 +350,7 @@ impl CookieIdentityInner { Ok(()) } - fn load(&self, req: &HttpRequest) -> Option { + fn load(&self, req: &ServiceRequest) -> Option { if let Ok(cookies) = req.cookies() { for cookie in cookies.iter() { if cookie.name() == self.name { @@ -384,16 +443,23 @@ impl CookieIdentityPolicy { } } -impl IdentityPolicy for CookieIdentityPolicy { - type Identity = CookieIdentity; - type Future = FutureResult; +impl IdentityPolicy for CookieIdentityPolicy { + type Future = Result, Error>; + type ResponseFuture = Result<(), Error>; - fn from_request(&self, req: &HttpRequest) -> Self::Future { - let identity = self.0.load(req); - FutOk(CookieIdentity { - identity, - changed: false, - inner: Rc::clone(&self.0), - }) + fn from_request

    (&self, req: &mut ServiceRequest

    ) -> Self::Future { + Ok(self.0.load(req)) + } + + fn to_response( + &self, + id: Option, + changed: bool, + res: &mut ServiceResponse, + ) -> Self::ResponseFuture { + if changed { + let _ = self.0.set_cookie(res, id); + } + Ok(()) } } diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index d63ca8930..288c1d63b 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -6,8 +6,11 @@ pub use self::compress::Compress; mod defaultheaders; pub use self::defaultheaders::DefaultHeaders; -#[cfg(feature = "session")] -pub use actix_session as session; +// #[cfg(feature = "session")] +// pub use actix_session as session; mod logger; pub use self::logger::Logger; + +#[cfg(feature = "session")] +pub mod identity; diff --git a/src/service.rs b/src/service.rs index f4b63a460..612fe4e89 100644 --- a/src/service.rs +++ b/src/service.rs @@ -82,8 +82,9 @@ impl

    ServiceRequest

    { /// Create service response for error #[inline] - pub fn error_response>(self, err: E) -> ServiceResponse { - ServiceResponse::new(self.req, err.into().into()) + pub fn error_response>(self, err: E) -> ServiceResponse { + let res: Response = err.into().into(); + ServiceResponse::new(self.req, res.into_body()) } /// This method returns reference to the request head @@ -335,6 +336,12 @@ impl ServiceResponse { } } + /// Create service response for error + #[inline] + pub fn error_response>(self, err: E) -> Self { + Self::from_err(err, self.request) + } + /// Get reference to original request #[inline] pub fn request(&self) -> &HttpRequest { From be9031c55ef38b012476524b7562cfd58318931a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 20:48:05 -0800 Subject: [PATCH 250/427] update doc api --- src/middleware/identity.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs index d04ed717c..74e5f3418 100644 --- a/src/middleware/identity.rs +++ b/src/middleware/identity.rs @@ -10,8 +10,7 @@ //! uses cookies as identity storage. //! //! To access current request identity -//! [**RequestIdentity**](trait.RequestIdentity.html) should be used. -//! *HttpRequest* implements *RequestIdentity* trait. +//! [**Identity**](trait.Identity.html) extractor should be used. //! //! ```rust //! use actix_web::middleware::identity::Identity; @@ -226,6 +225,7 @@ where } } +#[doc(hidden)] pub struct IdentityServiceMiddleware { backend: Rc, service: Rc>, From 3a2035a121048234b8cc63215b3df248a4e60c40 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 21:15:26 -0800 Subject: [PATCH 251/427] fix doc tests --- src/middleware/identity.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs index 74e5f3418..7cf739bc4 100644 --- a/src/middleware/identity.rs +++ b/src/middleware/identity.rs @@ -26,7 +26,7 @@ //! } //! } //! -//! fn login(id: Idenity) -> HttpResponse { +//! fn login(id: Identity) -> HttpResponse { //! id.remember("User1".to_owned()); // <- remember identity //! HttpResponse::Ok().finish() //! } @@ -41,11 +41,10 @@ //! // <- create identity middleware //! CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend //! .name("auth-cookie") -//! .secure(false)) -//! .service(web::resource("/index.html").to(index) -//! .service(web::resource("/login.html").to(login) -//! .service(web::resource("/logout.html").to(logout) -//! )); +//! .secure(false))) +//! .service(web::resource("/index.html").to(index)) +//! .service(web::resource("/login.html").to(login)) +//! .service(web::resource("/logout.html").to(logout)); //! } //! ``` use std::cell::RefCell; From 9b8812423c0efbb8acadf991f4ab883c6f1fee7b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 09:20:58 -0700 Subject: [PATCH 252/427] reexport Server controller form actix-server --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 6329d53ce..322070245 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,6 +65,7 @@ pub mod dev { Extensions, Payload, PayloadStream, RequestHead, ResponseHead, }; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; + pub use actix_server::Server; pub(crate) fn insert_slash(path: &str) -> String { let mut path = path.to_owned(); From 49d65fb07ace03b8f2750ebe3608255eb350f817 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 09:34:25 -0700 Subject: [PATCH 253/427] move extract to submodule --- src/{extract.rs => extract/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{extract.rs => extract/mod.rs} (100%) diff --git a/src/extract.rs b/src/extract/mod.rs similarity index 100% rename from src/extract.rs rename to src/extract/mod.rs From ee8725b58112d756117bf6fd5601fa14622dfb14 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 10:01:24 -0700 Subject: [PATCH 254/427] move extractors to separate submod --- src/extract/mod.rs | 997 +-------------------------------------------- 1 file changed, 20 insertions(+), 977 deletions(-) diff --git a/src/extract/mod.rs b/src/extract/mod.rs index c34d9df70..5d08dc079 100644 --- a/src/extract/mod.rs +++ b/src/extract/mod.rs @@ -1,32 +1,24 @@ -#![allow(dead_code)] -use std::ops::{Deref, DerefMut}; use std::rc::Rc; -use std::{fmt, str}; -use bytes::Bytes; -use encoding::all::UTF_8; -use encoding::types::{DecoderTrap, Encoding}; -use futures::future::{err, ok, Either, FutureResult}; -use futures::{future, Async, Future, IntoFuture, Poll, Stream}; -use mime::Mime; -use serde::de::{self, DeserializeOwned}; -use serde::Serialize; -use serde_json; -use serde_urlencoded; +use actix_http::error::Error; +use actix_http::Extensions; +use futures::future::ok; +use futures::{future, Async, Future, IntoFuture, Poll}; -use actix_http::dev::{JsonBody, MessageBody, UrlEncoded}; -use actix_http::error::{ - Error, ErrorBadRequest, ErrorNotFound, JsonPayloadError, PayloadError, - UrlencodedError, -}; -use actix_http::http::StatusCode; -use actix_http::{Extensions, HttpMessage, Response}; -use actix_router::PathDeserializer; - -use crate::request::HttpRequest; -use crate::responder::Responder; use crate::service::ServiceFromRequest; +mod form; +mod json; +mod path; +mod payload; +mod query; + +pub use self::form::{Form, FormConfig}; +pub use self::json::{Json, JsonConfig}; +pub use self::path::Path; +pub use self::payload::{Payload, PayloadConfig}; +pub use self::query::Query; + /// Trait implemented by types that can be extracted from request. /// /// Types that implement this trait can be used with `Route` handlers. @@ -72,882 +64,6 @@ impl ExtractorConfig for () { fn store_default(_: &mut ConfigStorage) {} } -#[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from the request's path. -/// -/// ## Example -/// -/// ```rust -/// use actix_web::{web, App}; -/// -/// /// extract path info from "/{username}/{count}/index.html" url -/// /// {username} - deserializes to a String -/// /// {count} - - deserializes to a u32 -/// fn index(info: web::Path<(String, u32)>) -> String { -/// format!("Welcome {}! {}", info.0, info.1) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/{username}/{count}/index.html") // <- define path parameters -/// .route(web::get().to(index)) // <- register handler with `Path` extractor -/// ); -/// } -/// ``` -/// -/// It is possible to extract path information to a specific type that -/// implements `Deserialize` trait from *serde*. -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, Error}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// extract `Info` from a path using serde -/// fn index(info: web::Path) -> Result { -/// Ok(format!("Welcome {}!", info.username)) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/{username}/index.html") // <- define path parameters -/// .route(web::get().to(index)) // <- use handler with Path` extractor -/// ); -/// } -/// ``` -pub struct Path { - inner: T, -} - -impl AsRef for Path { - fn as_ref(&self) -> &T { - &self.inner - } -} - -impl Deref for Path { - type Target = T; - - fn deref(&self) -> &T { - &self.inner - } -} - -impl DerefMut for Path { - fn deref_mut(&mut self) -> &mut T { - &mut self.inner - } -} - -impl Path { - /// Deconstruct to an inner value - pub fn into_inner(self) -> T { - self.inner - } - - /// Extract path information from a request - pub fn extract(req: &HttpRequest) -> Result, de::value::Error> - where - T: DeserializeOwned, - { - de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) - .map(|inner| Path { inner }) - } -} - -impl From for Path { - fn from(inner: T) -> Path { - Path { inner } - } -} - -/// Extract typed information from the request's path. -/// -/// ## Example -/// -/// ```rust -/// use actix_web::{web, App}; -/// -/// /// extract path info from "/{username}/{count}/index.html" url -/// /// {username} - deserializes to a String -/// /// {count} - - deserializes to a u32 -/// fn index(info: web::Path<(String, u32)>) -> String { -/// format!("Welcome {}! {}", info.0, info.1) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/{username}/{count}/index.html") // <- define path parameters -/// .route(web::get().to(index)) // <- register handler with `Path` extractor -/// ); -/// } -/// ``` -/// -/// It is possible to extract path information to a specific type that -/// implements `Deserialize` trait from *serde*. -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, Error}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// extract `Info` from a path using serde -/// fn index(info: web::Path) -> Result { -/// Ok(format!("Welcome {}!", info.username)) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/{username}/index.html") // <- define path parameters -/// .route(web::get().to(index)) // <- use handler with Path` extractor -/// ); -/// } -/// ``` -impl FromRequest

    for Path -where - T: DeserializeOwned, -{ - type Error = Error; - type Future = Result; - type Config = (); - - #[inline] - fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - Self::extract(req).map_err(ErrorNotFound) - } -} - -impl fmt::Debug for Path { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.inner.fmt(f) - } -} - -impl fmt::Display for Path { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.inner.fmt(f) - } -} - -#[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from from the request's query. -/// -/// ## Example -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App}; -/// -/// #[derive(Debug, Deserialize)] -/// pub enum ResponseType { -/// Token, -/// Code -/// } -/// -/// #[derive(Deserialize)] -/// pub struct AuthRequest { -/// id: u64, -/// response_type: ResponseType, -/// } -/// -/// // Use `Query` extractor for query information. -/// // This handler get called only if request's query contains `username` field -/// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` -/// fn index(info: web::Query) -> String { -/// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor -/// } -/// ``` -pub struct Query(T); - -impl Deref for Query { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Query { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl Query { - /// Deconstruct to a inner value - pub fn into_inner(self) -> T { - self.0 - } -} - -/// Extract typed information from from the request's query. -/// -/// ## Example -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App}; -/// -/// #[derive(Debug, Deserialize)] -/// pub enum ResponseType { -/// Token, -/// Code -/// } -/// -/// #[derive(Deserialize)] -/// pub struct AuthRequest { -/// id: u64, -/// response_type: ResponseType, -/// } -/// -/// // Use `Query` extractor for query information. -/// // This handler get called only if request's query contains `username` field -/// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` -/// fn index(info: web::Query) -> String { -/// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html") -/// .route(web::get().to(index))); // <- use `Query` extractor -/// } -/// ``` -impl FromRequest

    for Query -where - T: de::DeserializeOwned, -{ - type Error = Error; - type Future = Result; - type Config = (); - - #[inline] - fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - serde_urlencoded::from_str::(req.query_string()) - .map(|val| Ok(Query(val))) - .unwrap_or_else(|e| Err(e.into())) - } -} - -impl fmt::Debug for Query { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::Display for Query { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -#[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from the request's body. -/// -/// To extract typed information from request's body, the type `T` must -/// implement the `Deserialize` trait from *serde*. -/// -/// [**FormConfig**](struct.FormConfig.html) allows to configure extraction -/// process. -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App}; -/// -/// #[derive(Deserialize)] -/// struct FormData { -/// username: String, -/// } -/// -/// /// Extract form data using serde. -/// /// This handler get called only if content type is *x-www-form-urlencoded* -/// /// and content of the request could be deserialized to a `FormData` struct -/// fn index(form: web::Form) -> String { -/// format!("Welcome {}!", form.username) -/// } -/// # fn main() {} -/// ``` -pub struct Form(pub T); - -impl Form { - /// Deconstruct to an inner value - pub fn into_inner(self) -> T { - self.0 - } -} - -impl Deref for Form { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Form { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl FromRequest

    for Form -where - T: DeserializeOwned + 'static, - P: Stream + 'static, -{ - type Error = Error; - type Future = Box>; - type Config = FormConfig; - - #[inline] - fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - let req2 = req.clone(); - let cfg = req.load_config::(); - - let limit = cfg.limit; - let err = Rc::clone(&cfg.ehandler); - Box::new( - UrlEncoded::new(req) - .limit(limit) - .map_err(move |e| (*err)(e, &req2)) - .map(Form), - ) - } -} - -impl fmt::Debug for Form { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::Display for Form { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -/// Form extractor configuration -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, Result}; -/// -/// #[derive(Deserialize)] -/// struct FormData { -/// username: String, -/// } -/// -/// /// Extract form data using serde. -/// /// Custom configuration is used for this handler, max payload size is 4k -/// fn index(form: web::Form) -> Result { -/// Ok(format!("Welcome {}!", form.username)) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html") -/// .route(web::get() -/// // change `Form` extractor configuration -/// .config(web::FormConfig::default().limit(4097)) -/// .to(index)) -/// ); -/// } -/// ``` -#[derive(Clone)] -pub struct FormConfig { - limit: usize, - ehandler: Rc Error>, -} - -impl FormConfig { - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - /// Set custom error handler - pub fn error_handler(mut self, f: F) -> Self - where - F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static, - { - self.ehandler = Rc::new(f); - self - } -} - -impl ExtractorConfig for FormConfig {} - -impl Default for FormConfig { - fn default() -> Self { - FormConfig { - limit: 262_144, - ehandler: Rc::new(|e, _| e.into()), - } - } -} - -/// Json helper -/// -/// Json can be used for two different purpose. First is for json response -/// generation and second is for extracting typed information from request's -/// payload. -/// -/// To extract typed information from request's body, the type `T` must -/// implement the `Deserialize` trait from *serde*. -/// -/// [**JsonConfig**](struct.JsonConfig.html) allows to configure extraction -/// process. -/// -/// ## Example -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// deserialize `Info` from request's body -/// fn index(info: web::Json) -> String { -/// format!("Welcome {}!", info.username) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route( -/// web::post().to(index)) -/// ); -/// } -/// ``` -/// -/// The `Json` type allows you to respond with well-formed JSON data: simply -/// return a value of type Json where T is the type of a structure -/// to serialize into *JSON*. The type `T` must implement the `Serialize` -/// trait from *serde*. -/// -/// ```rust -/// # #[macro_use] extern crate serde_derive; -/// # use actix_web::*; -/// # -/// #[derive(Serialize)] -/// struct MyObj { -/// name: String, -/// } -/// -/// fn index(req: HttpRequest) -> Result> { -/// Ok(web::Json(MyObj { -/// name: req.match_info().get("name").unwrap().to_string(), -/// })) -/// } -/// # fn main() {} -/// ``` -pub struct Json(pub T); - -impl Json { - /// Deconstruct to an inner value - pub fn into_inner(self) -> T { - self.0 - } -} - -impl Deref for Json { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Json { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl fmt::Debug for Json -where - T: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Json: {:?}", self.0) - } -} - -impl fmt::Display for Json -where - T: fmt::Display, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} - -impl Responder for Json { - type Error = Error; - type Future = Result; - - fn respond_to(self, _: &HttpRequest) -> Self::Future { - let body = match serde_json::to_string(&self.0) { - Ok(body) => body, - Err(e) => return Err(e.into()), - }; - - Ok(Response::build(StatusCode::OK) - .content_type("application/json") - .body(body)) - } -} - -/// Json extractor. Allow to extract typed information from request's -/// payload. -/// -/// To extract typed information from request's body, the type `T` must -/// implement the `Deserialize` trait from *serde*. -/// -/// [**JsonConfig**](struct.JsonConfig.html) allows to configure extraction -/// process. -/// -/// ## Example -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// deserialize `Info` from request's body -/// fn index(info: web::Json) -> String { -/// format!("Welcome {}!", info.username) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route( -/// web::post().to(index)) -/// ); -/// } -/// ``` -impl FromRequest

    for Json -where - T: DeserializeOwned + 'static, - P: Stream + 'static, -{ - type Error = Error; - type Future = Box>; - type Config = JsonConfig; - - #[inline] - fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - let req2 = req.clone(); - let cfg = req.load_config::(); - - let limit = cfg.limit; - let err = Rc::clone(&cfg.ehandler); - Box::new( - JsonBody::new(req) - .limit(limit) - .map_err(move |e| (*err)(e, &req2)) - .map(Json), - ) - } -} - -/// Json extractor configuration -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{error, web, App, HttpResponse}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// deserialize `Info` from request's body, max payload size is 4kb -/// fn index(info: web::Json) -> String { -/// format!("Welcome {}!", info.username) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route( -/// web::post().config( -/// // change json extractor configuration -/// web::JsonConfig::default().limit(4096) -/// .error_handler(|err, req| { // <- create custom error response -/// error::InternalError::from_response( -/// err, HttpResponse::Conflict().finish()).into() -/// })) -/// .to(index)) -/// ); -/// } -/// ``` -#[derive(Clone)] -pub struct JsonConfig { - limit: usize, - ehandler: Rc Error>, -} - -impl JsonConfig { - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - /// Set custom error handler - pub fn error_handler(mut self, f: F) -> Self - where - F: Fn(JsonPayloadError, &HttpRequest) -> Error + 'static, - { - self.ehandler = Rc::new(f); - self - } -} - -impl ExtractorConfig for JsonConfig {} - -impl Default for JsonConfig { - fn default() -> Self { - JsonConfig { - limit: 262_144, - ehandler: Rc::new(|e, _| e.into()), - } - } -} - -/// Payload extractor returns request 's payload stream. -/// -/// ## Example -/// -/// ```rust -/// use futures::{Future, Stream}; -/// use actix_web::{web, error, App, Error, HttpResponse}; -/// -/// /// extract binary data from request -/// fn index

    (body: web::Payload

    ) -> impl Future -/// where -/// P: Stream -/// { -/// body.map_err(Error::from) -/// .fold(web::BytesMut::new(), move |mut body, chunk| { -/// body.extend_from_slice(&chunk); -/// Ok::<_, Error>(body) -/// }) -/// .and_then(|body| { -/// format!("Body {:?}!", body); -/// Ok(HttpResponse::Ok().finish()) -/// }) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route( -/// web::get().to_async(index)) -/// ); -/// } -/// ``` -pub struct Payload(crate::dev::Payload); - -impl Stream for Payload -where - T: Stream, -{ - type Item = Bytes; - type Error = PayloadError; - - #[inline] - fn poll(&mut self) -> Poll, PayloadError> { - self.0.poll() - } -} - -/// Get request's payload stream -/// -/// ## Example -/// -/// ```rust -/// use futures::{Future, Stream}; -/// use actix_web::{web, error, App, Error, HttpResponse}; -/// -/// /// extract binary data from request -/// fn index

    (body: web::Payload

    ) -> impl Future -/// where -/// P: Stream -/// { -/// body.map_err(Error::from) -/// .fold(web::BytesMut::new(), move |mut body, chunk| { -/// body.extend_from_slice(&chunk); -/// Ok::<_, Error>(body) -/// }) -/// .and_then(|body| { -/// format!("Body {:?}!", body); -/// Ok(HttpResponse::Ok().finish()) -/// }) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route( -/// web::get().to_async(index)) -/// ); -/// } -/// ``` -impl

    FromRequest

    for Payload

    -where - P: Stream, -{ - type Error = Error; - type Future = Result, Error>; - type Config = (); - - #[inline] - fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - Ok(Payload(req.take_payload())) - } -} - -/// Request binary data from a request's payload. -/// -/// Loads request's payload and construct Bytes instance. -/// -/// [**PayloadConfig**](struct.PayloadConfig.html) allows to configure -/// extraction process. -/// -/// ## Example -/// -/// ```rust -/// use bytes::Bytes; -/// use actix_web::{web, App}; -/// -/// /// extract binary data from request -/// fn index(body: Bytes) -> String { -/// format!("Body {:?}!", body) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route( -/// web::get().to(index)) -/// ); -/// } -/// ``` -impl

    FromRequest

    for Bytes -where - P: Stream + 'static, -{ - type Error = Error; - type Future = - Either>, FutureResult>; - type Config = PayloadConfig; - - #[inline] - fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - let cfg = req.load_config::(); - - if let Err(e) = cfg.check_mimetype(req) { - return Either::B(err(e)); - } - - let limit = cfg.limit; - Either::A(Box::new(MessageBody::new(req).limit(limit).from_err())) - } -} - -/// Extract text information from a request's body. -/// -/// Text extractor automatically decode body according to the request's charset. -/// -/// [**PayloadConfig**](struct.PayloadConfig.html) allows to configure -/// extraction process. -/// -/// ## Example -/// -/// ```rust -/// use actix_web::{web, App}; -/// -/// /// extract text data from request -/// fn index(text: String) -> String { -/// format!("Body {}!", text) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route( -/// web::get() -/// .config(web::PayloadConfig::new(4096)) // <- limit size of the payload -/// .to(index)) // <- register handler with extractor params -/// ); -/// } -/// ``` -impl

    FromRequest

    for String -where - P: Stream + 'static, -{ - type Error = Error; - type Future = - Either>, FutureResult>; - type Config = PayloadConfig; - - #[inline] - fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - let cfg = req.load_config::(); - - // check content-type - if let Err(e) = cfg.check_mimetype(req) { - return Either::B(err(e)); - } - - // check charset - let encoding = match req.encoding() { - Ok(enc) => enc, - Err(e) => return Either::B(err(e.into())), - }; - let limit = cfg.limit; - - Either::A(Box::new( - MessageBody::new(req) - .limit(limit) - .from_err() - .and_then(move |body| { - let enc: *const Encoding = encoding as *const Encoding; - if enc == UTF_8 { - Ok(str::from_utf8(body.as_ref()) - .map_err(|_| ErrorBadRequest("Can not decode body"))? - .to_owned()) - } else { - Ok(encoding - .decode(&body, DecoderTrap::Strict) - .map_err(|_| ErrorBadRequest("Can not decode body"))?) - } - }), - )) - } -} - /// Optionally extract a field from the request /// /// If the FromRequest for T fails, return None rather than returning an error response @@ -1009,7 +125,10 @@ where fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { Box::new(T::from_request(req).into_future().then(|r| match r { Ok(v) => future::ok(Some(v)), - Err(_) => future::ok(None), + Err(e) => { + log::debug!("Error for Option extractor: {}", e.into()); + future::ok(None) + } })) } } @@ -1078,64 +197,6 @@ where } } -/// Payload configuration for request's payload. -#[derive(Clone)] -pub struct PayloadConfig { - limit: usize, - mimetype: Option, -} - -impl PayloadConfig { - /// Create `PayloadConfig` instance and set max size of payload. - pub fn new(limit: usize) -> Self { - Self::default().limit(limit) - } - - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - /// Set required mime-type of the request. By default mime type is not - /// enforced. - pub fn mimetype(mut self, mt: Mime) -> Self { - self.mimetype = Some(mt); - self - } - - fn check_mimetype

    (&self, req: &ServiceFromRequest

    ) -> Result<(), Error> { - // check content-type - if let Some(ref mt) = self.mimetype { - match req.mime_type() { - Ok(Some(ref req_mt)) => { - if mt != req_mt { - return Err(ErrorBadRequest("Unexpected Content-Type")); - } - } - Ok(None) => { - return Err(ErrorBadRequest("Content-Type is expected")); - } - Err(err) => { - return Err(err.into()); - } - } - } - Ok(()) - } -} - -impl ExtractorConfig for PayloadConfig {} - -impl Default for PayloadConfig { - fn default() -> Self { - PayloadConfig { - limit: 262_144, - mimetype: None, - } - } -} - #[doc(hidden)] impl

    FromRequest

    for () { type Error = Error; @@ -1360,24 +421,6 @@ mod tests { assert!(r.is_err()); } - #[test] - fn test_payload_config() { - let req = TestRequest::default().to_from(); - let cfg = PayloadConfig::default().mimetype(mime::APPLICATION_JSON); - assert!(cfg.check_mimetype(&req).is_err()); - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ) - .to_from(); - assert!(cfg.check_mimetype(&req).is_err()); - - let req = - TestRequest::with_header(header::CONTENT_TYPE, "application/json").to_from(); - assert!(cfg.check_mimetype(&req).is_ok()); - } - #[derive(Deserialize)] struct MyStruct { key: String, From 16c42be4a2a753f0ec2f96875a37a0487bfbe4e5 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 10:53:56 -0700 Subject: [PATCH 255/427] simplify extractor configuration, config is optional now --- src/extract/form.rs | 197 +++++++++++++++++++++++ src/extract/json.rs | 259 ++++++++++++++++++++++++++++++ src/extract/mod.rs | 75 +-------- src/extract/path.rs | 176 +++++++++++++++++++++ src/extract/payload.rs | 313 +++++++++++++++++++++++++++++++++++++ src/extract/query.rs | 126 +++++++++++++++ src/middleware/identity.rs | 1 - src/request.rs | 1 - src/route.rs | 18 ++- src/service.rs | 10 +- src/state.rs | 1 - 11 files changed, 1086 insertions(+), 91 deletions(-) create mode 100644 src/extract/form.rs create mode 100644 src/extract/json.rs create mode 100644 src/extract/path.rs create mode 100644 src/extract/payload.rs create mode 100644 src/extract/query.rs diff --git a/src/extract/form.rs b/src/extract/form.rs new file mode 100644 index 000000000..19849ac8b --- /dev/null +++ b/src/extract/form.rs @@ -0,0 +1,197 @@ +//! Form extractor + +use std::rc::Rc; +use std::{fmt, ops}; + +use actix_http::dev::UrlEncoded; +use actix_http::error::{Error, UrlencodedError}; +use bytes::Bytes; +use futures::{Future, Stream}; +use serde::de::DeserializeOwned; + +use crate::extract::FromRequest; +use crate::request::HttpRequest; +use crate::service::ServiceFromRequest; + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +/// Extract typed information from the request's body. +/// +/// To extract typed information from request's body, the type `T` must +/// implement the `Deserialize` trait from *serde*. +/// +/// [**FormConfig**](struct.FormConfig.html) allows to configure extraction +/// process. +/// +/// ## Example +/// +/// ```rust +/// # extern crate actix_web; +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App}; +/// +/// #[derive(Deserialize)] +/// struct FormData { +/// username: String, +/// } +/// +/// /// Extract form data using serde. +/// /// This handler get called only if content type is *x-www-form-urlencoded* +/// /// and content of the request could be deserialized to a `FormData` struct +/// fn index(form: web::Form) -> String { +/// format!("Welcome {}!", form.username) +/// } +/// # fn main() {} +/// ``` +pub struct Form(pub T); + +impl Form { + /// Deconstruct to an inner value + pub fn into_inner(self) -> T { + self.0 + } +} + +impl ops::Deref for Form { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl ops::DerefMut for Form { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl FromRequest

    for Form +where + T: DeserializeOwned + 'static, + P: Stream + 'static, +{ + type Error = Error; + type Future = Box>; + + #[inline] + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + let req2 = req.clone(); + let (limit, err) = req + .load_config::() + .map(|c| (c.limit, c.ehandler.clone())) + .unwrap_or((16384, None)); + + Box::new( + UrlEncoded::new(req) + .limit(limit) + .map_err(move |e| { + if let Some(err) = err { + (*err)(e, &req2) + } else { + e.into() + } + }) + .map(Form), + ) + } +} + +impl fmt::Debug for Form { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Display for Form { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +/// Form extractor configuration +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App, Result}; +/// +/// #[derive(Deserialize)] +/// struct FormData { +/// username: String, +/// } +/// +/// /// Extract form data using serde. +/// /// Custom configuration is used for this handler, max payload size is 4k +/// fn index(form: web::Form) -> Result { +/// Ok(format!("Welcome {}!", form.username)) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html") +/// .route(web::get() +/// // change `Form` extractor configuration +/// .config(web::FormConfig::default().limit(4097)) +/// .to(index)) +/// ); +/// } +/// ``` +#[derive(Clone)] +pub struct FormConfig { + limit: usize, + ehandler: Option Error>>, +} + +impl FormConfig { + /// Change max size of payload. By default max size is 16Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + /// Set custom error handler + pub fn error_handler(mut self, f: F) -> Self + where + F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static, + { + self.ehandler = Some(Rc::new(f)); + self + } +} + +impl Default for FormConfig { + fn default() -> Self { + FormConfig { + limit: 16384, + ehandler: None, + } + } +} + +#[cfg(test)] +mod tests { + use actix_http::http::header; + use bytes::Bytes; + use serde_derive::Deserialize; + + use super::*; + use crate::test::{block_on, TestRequest}; + + #[derive(Deserialize, Debug, PartialEq)] + struct Info { + hello: String, + } + + #[test] + fn test_form() { + let mut req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .header(header::CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .to_from(); + + let s = block_on(Form::::from_request(&mut req)).unwrap(); + assert_eq!(s.hello, "world"); + } +} diff --git a/src/extract/json.rs b/src/extract/json.rs new file mode 100644 index 000000000..f74b082d1 --- /dev/null +++ b/src/extract/json.rs @@ -0,0 +1,259 @@ +//! Json extractor/responder + +use std::rc::Rc; +use std::{fmt, ops}; + +use bytes::Bytes; +use futures::{Future, Stream}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use serde_json; + +use actix_http::dev::JsonBody; +use actix_http::error::{Error, JsonPayloadError}; +use actix_http::http::StatusCode; +use actix_http::Response; + +use crate::extract::FromRequest; +use crate::request::HttpRequest; +use crate::responder::Responder; +use crate::service::ServiceFromRequest; + +/// Json helper +/// +/// Json can be used for two different purpose. First is for json response +/// generation and second is for extracting typed information from request's +/// payload. +/// +/// To extract typed information from request's body, the type `T` must +/// implement the `Deserialize` trait from *serde*. +/// +/// [**JsonConfig**](struct.JsonConfig.html) allows to configure extraction +/// process. +/// +/// ## Example +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// deserialize `Info` from request's body +/// fn index(info: web::Json) -> String { +/// format!("Welcome {}!", info.username) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::post().to(index)) +/// ); +/// } +/// ``` +/// +/// The `Json` type allows you to respond with well-formed JSON data: simply +/// return a value of type Json where T is the type of a structure +/// to serialize into *JSON*. The type `T` must implement the `Serialize` +/// trait from *serde*. +/// +/// ```rust +/// # #[macro_use] extern crate serde_derive; +/// # use actix_web::*; +/// # +/// #[derive(Serialize)] +/// struct MyObj { +/// name: String, +/// } +/// +/// fn index(req: HttpRequest) -> Result> { +/// Ok(web::Json(MyObj { +/// name: req.match_info().get("name").unwrap().to_string(), +/// })) +/// } +/// # fn main() {} +/// ``` +pub struct Json(pub T); + +impl Json { + /// Deconstruct to an inner value + pub fn into_inner(self) -> T { + self.0 + } +} + +impl ops::Deref for Json { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl ops::DerefMut for Json { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl fmt::Debug for Json +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Json: {:?}", self.0) + } +} + +impl fmt::Display for Json +where + T: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl Responder for Json { + type Error = Error; + type Future = Result; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + let body = match serde_json::to_string(&self.0) { + Ok(body) => body, + Err(e) => return Err(e.into()), + }; + + Ok(Response::build(StatusCode::OK) + .content_type("application/json") + .body(body)) + } +} + +/// Json extractor. Allow to extract typed information from request's +/// payload. +/// +/// To extract typed information from request's body, the type `T` must +/// implement the `Deserialize` trait from *serde*. +/// +/// [**JsonConfig**](struct.JsonConfig.html) allows to configure extraction +/// process. +/// +/// ## Example +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// deserialize `Info` from request's body +/// fn index(info: web::Json) -> String { +/// format!("Welcome {}!", info.username) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::post().to(index)) +/// ); +/// } +/// ``` +impl FromRequest

    for Json +where + T: DeserializeOwned + 'static, + P: Stream + 'static, +{ + type Error = Error; + type Future = Box>; + + #[inline] + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + let req2 = req.clone(); + let (limit, err) = req + .load_config::() + .map(|c| (c.limit, c.ehandler.clone())) + .unwrap_or((32768, None)); + + Box::new( + JsonBody::new(req) + .limit(limit) + .map_err(move |e| { + if let Some(err) = err { + (*err)(e, &req2) + } else { + e.into() + } + }) + .map(Json), + ) + } +} + +/// Json extractor configuration +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{error, web, App, HttpResponse}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// deserialize `Info` from request's body, max payload size is 4kb +/// fn index(info: web::Json) -> String { +/// format!("Welcome {}!", info.username) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::post().config( +/// // change json extractor configuration +/// web::JsonConfig::default().limit(4096) +/// .error_handler(|err, req| { // <- create custom error response +/// error::InternalError::from_response( +/// err, HttpResponse::Conflict().finish()).into() +/// })) +/// .to(index)) +/// ); +/// } +/// ``` +#[derive(Clone)] +pub struct JsonConfig { + limit: usize, + ehandler: Option Error>>, +} + +impl JsonConfig { + /// Change max size of payload. By default max size is 32Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + /// Set custom error handler + pub fn error_handler(mut self, f: F) -> Self + where + F: Fn(JsonPayloadError, &HttpRequest) -> Error + 'static, + { + self.ehandler = Some(Rc::new(f)); + self + } +} + +impl Default for JsonConfig { + fn default() -> Self { + JsonConfig { + limit: 32768, + ehandler: None, + } + } +} diff --git a/src/extract/mod.rs b/src/extract/mod.rs index 5d08dc079..d8958b2d9 100644 --- a/src/extract/mod.rs +++ b/src/extract/mod.rs @@ -1,7 +1,6 @@ -use std::rc::Rc; +//! Request extractors use actix_http::error::Error; -use actix_http::Extensions; use futures::future::ok; use futures::{future, Async, Future, IntoFuture, Poll}; @@ -29,41 +28,10 @@ pub trait FromRequest

    : Sized { /// Future that resolves to a Self type Future: IntoFuture; - /// Configuration for the extractor - type Config: ExtractorConfig; - /// Convert request to a Self fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future; } -/// Storage for extractor configs -#[derive(Default)] -pub struct ConfigStorage { - pub(crate) storage: Option>, -} - -impl ConfigStorage { - pub fn store(&mut self, config: C) { - if self.storage.is_none() { - self.storage = Some(Rc::new(Extensions::new())); - } - if let Some(ref mut ext) = self.storage { - Rc::get_mut(ext).unwrap().insert(config); - } - } -} - -pub trait ExtractorConfig: Default + Clone + 'static { - /// Set default configuration to config storage - fn store_default(ext: &mut ConfigStorage) { - ext.store(Self::default()) - } -} - -impl ExtractorConfig for () { - fn store_default(_: &mut ConfigStorage) {} -} - /// Optionally extract a field from the request /// /// If the FromRequest for T fails, return None rather than returning an error response @@ -84,7 +52,6 @@ impl ExtractorConfig for () { /// impl

    FromRequest

    for Thing { /// type Error = Error; /// type Future = Result; -/// type Config = (); /// /// fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { /// if rand::random() { @@ -119,7 +86,6 @@ where { type Error = Error; type Future = Box, Error = Error>>; - type Config = T::Config; #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { @@ -153,7 +119,6 @@ where /// impl

    FromRequest

    for Thing { /// type Error = Error; /// type Future = Result; -/// type Config = (); /// /// fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { /// if rand::random() { @@ -186,7 +151,6 @@ where { type Error = Error; type Future = Box, Error = Error>>; - type Config = T::Config; #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { @@ -201,23 +165,12 @@ where impl

    FromRequest

    for () { type Error = Error; type Future = Result<(), Error>; - type Config = (); fn from_request(_req: &mut ServiceFromRequest

    ) -> Self::Future { Ok(()) } } -macro_rules! tuple_config ({ $($T:ident),+} => { - impl<$($T,)+> ExtractorConfig for ($($T,)+) - where $($T: ExtractorConfig + Clone,)+ - { - fn store_default(ext: &mut ConfigStorage) { - $($T::store_default(ext);)+ - } - } -}); - macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { /// FromRequest implementation for tuple @@ -226,7 +179,6 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { { type Error = Error; type Future = $fut_type; - type Config = ($($T::Config,)+); fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { $fut_type { @@ -277,17 +229,6 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { mod m { use super::*; -tuple_config!(A); -tuple_config!(A, B); -tuple_config!(A, B, C); -tuple_config!(A, B, C, D); -tuple_config!(A, B, C, D, E); -tuple_config!(A, B, C, D, E, F); -tuple_config!(A, B, C, D, E, F, G); -tuple_config!(A, B, C, D, E, F, G, H); -tuple_config!(A, B, C, D, E, F, G, H, I); -tuple_config!(A, B, C, D, E, F, G, H, I, J); - tuple_from_req!(TupleFromRequest1, (0, A)); tuple_from_req!(TupleFromRequest2, (0, A), (1, B)); tuple_from_req!(TupleFromRequest3, (0, A), (1, B), (2, C)); @@ -335,20 +276,6 @@ mod tests { assert_eq!(s, "hello=world"); } - #[test] - fn test_form() { - let mut req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ) - .header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .to_from(); - - let s = block_on(Form::::from_request(&mut req)).unwrap(); - assert_eq!(s.hello, "world"); - } - #[test] fn test_option() { let mut req = TestRequest::with_header( diff --git a/src/extract/path.rs b/src/extract/path.rs new file mode 100644 index 000000000..d9461263a --- /dev/null +++ b/src/extract/path.rs @@ -0,0 +1,176 @@ +//! Path extractor + +use std::{fmt, ops}; + +use actix_http::error::{Error, ErrorNotFound}; +use actix_router::PathDeserializer; +use serde::de; + +use crate::request::HttpRequest; +use crate::service::ServiceFromRequest; + +use super::FromRequest; + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +/// Extract typed information from the request's path. +/// +/// ## Example +/// +/// ```rust +/// use actix_web::{web, App}; +/// +/// /// extract path info from "/{username}/{count}/index.html" url +/// /// {username} - deserializes to a String +/// /// {count} - - deserializes to a u32 +/// fn index(info: web::Path<(String, u32)>) -> String { +/// format!("Welcome {}! {}", info.0, info.1) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{username}/{count}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- register handler with `Path` extractor +/// ); +/// } +/// ``` +/// +/// It is possible to extract path information to a specific type that +/// implements `Deserialize` trait from *serde*. +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App, Error}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// extract `Info` from a path using serde +/// fn index(info: web::Path) -> Result { +/// Ok(format!("Welcome {}!", info.username)) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{username}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- use handler with Path` extractor +/// ); +/// } +/// ``` +pub struct Path { + inner: T, +} + +impl Path { + /// Deconstruct to an inner value + pub fn into_inner(self) -> T { + self.inner + } + + /// Extract path information from a request + pub fn extract(req: &HttpRequest) -> Result, de::value::Error> + where + T: de::DeserializeOwned, + { + de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) + .map(|inner| Path { inner }) + } +} + +impl AsRef for Path { + fn as_ref(&self) -> &T { + &self.inner + } +} + +impl ops::Deref for Path { + type Target = T; + + fn deref(&self) -> &T { + &self.inner + } +} + +impl ops::DerefMut for Path { + fn deref_mut(&mut self) -> &mut T { + &mut self.inner + } +} + +impl From for Path { + fn from(inner: T) -> Path { + Path { inner } + } +} + +impl fmt::Debug for Path { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.inner.fmt(f) + } +} + +impl fmt::Display for Path { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.inner.fmt(f) + } +} + +/// Extract typed information from the request's path. +/// +/// ## Example +/// +/// ```rust +/// use actix_web::{web, App}; +/// +/// /// extract path info from "/{username}/{count}/index.html" url +/// /// {username} - deserializes to a String +/// /// {count} - - deserializes to a u32 +/// fn index(info: web::Path<(String, u32)>) -> String { +/// format!("Welcome {}! {}", info.0, info.1) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{username}/{count}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- register handler with `Path` extractor +/// ); +/// } +/// ``` +/// +/// It is possible to extract path information to a specific type that +/// implements `Deserialize` trait from *serde*. +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App, Error}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// extract `Info` from a path using serde +/// fn index(info: web::Path) -> Result { +/// Ok(format!("Welcome {}!", info.username)) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{username}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- use handler with Path` extractor +/// ); +/// } +/// ``` +impl FromRequest

    for Path +where + T: de::DeserializeOwned, +{ + type Error = Error; + type Future = Result; + + #[inline] + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + Self::extract(req).map_err(ErrorNotFound) + } +} diff --git a/src/extract/payload.rs b/src/extract/payload.rs new file mode 100644 index 000000000..82024c0d8 --- /dev/null +++ b/src/extract/payload.rs @@ -0,0 +1,313 @@ +//! Payload/Bytes/String extractors +use std::str; + +use actix_http::dev::MessageBody; +use actix_http::error::{Error, ErrorBadRequest, PayloadError}; +use actix_http::HttpMessage; +use bytes::Bytes; +use encoding::all::UTF_8; +use encoding::types::{DecoderTrap, Encoding}; +use futures::future::{err, Either, FutureResult}; +use futures::{Future, Poll, Stream}; +use mime::Mime; + +use crate::extract::FromRequest; +use crate::service::ServiceFromRequest; + +/// Payload extractor returns request 's payload stream. +/// +/// ## Example +/// +/// ```rust +/// use futures::{Future, Stream}; +/// use actix_web::{web, error, App, Error, HttpResponse}; +/// +/// /// extract binary data from request +/// fn index

    (body: web::Payload

    ) -> impl Future +/// where +/// P: Stream +/// { +/// body.map_err(Error::from) +/// .fold(web::BytesMut::new(), move |mut body, chunk| { +/// body.extend_from_slice(&chunk); +/// Ok::<_, Error>(body) +/// }) +/// .and_then(|body| { +/// format!("Body {:?}!", body); +/// Ok(HttpResponse::Ok().finish()) +/// }) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get().to_async(index)) +/// ); +/// } +/// ``` +pub struct Payload(crate::dev::Payload); + +impl Stream for Payload +where + T: Stream, +{ + type Item = Bytes; + type Error = PayloadError; + + #[inline] + fn poll(&mut self) -> Poll, PayloadError> { + self.0.poll() + } +} + +/// Get request's payload stream +/// +/// ## Example +/// +/// ```rust +/// use futures::{Future, Stream}; +/// use actix_web::{web, error, App, Error, HttpResponse}; +/// +/// /// extract binary data from request +/// fn index

    (body: web::Payload

    ) -> impl Future +/// where +/// P: Stream +/// { +/// body.map_err(Error::from) +/// .fold(web::BytesMut::new(), move |mut body, chunk| { +/// body.extend_from_slice(&chunk); +/// Ok::<_, Error>(body) +/// }) +/// .and_then(|body| { +/// format!("Body {:?}!", body); +/// Ok(HttpResponse::Ok().finish()) +/// }) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get().to_async(index)) +/// ); +/// } +/// ``` +impl

    FromRequest

    for Payload

    +where + P: Stream, +{ + type Error = Error; + type Future = Result, Error>; + + #[inline] + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + Ok(Payload(req.take_payload())) + } +} + +/// Request binary data from a request's payload. +/// +/// Loads request's payload and construct Bytes instance. +/// +/// [**PayloadConfig**](struct.PayloadConfig.html) allows to configure +/// extraction process. +/// +/// ## Example +/// +/// ```rust +/// use bytes::Bytes; +/// use actix_web::{web, App}; +/// +/// /// extract binary data from request +/// fn index(body: Bytes) -> String { +/// format!("Body {:?}!", body) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get().to(index)) +/// ); +/// } +/// ``` +impl

    FromRequest

    for Bytes +where + P: Stream + 'static, +{ + type Error = Error; + type Future = + Either>, FutureResult>; + + #[inline] + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + let mut tmp; + let cfg = if let Some(cfg) = req.load_config::() { + cfg + } else { + tmp = PayloadConfig::default(); + &tmp + }; + + if let Err(e) = cfg.check_mimetype(req) { + return Either::B(err(e)); + } + + let limit = cfg.limit; + Either::A(Box::new(MessageBody::new(req).limit(limit).from_err())) + } +} + +/// Extract text information from a request's body. +/// +/// Text extractor automatically decode body according to the request's charset. +/// +/// [**PayloadConfig**](struct.PayloadConfig.html) allows to configure +/// extraction process. +/// +/// ## Example +/// +/// ```rust +/// use actix_web::{web, App}; +/// +/// /// extract text data from request +/// fn index(text: String) -> String { +/// format!("Body {}!", text) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get() +/// .config(web::PayloadConfig::new(4096)) // <- limit size of the payload +/// .to(index)) // <- register handler with extractor params +/// ); +/// } +/// ``` +impl

    FromRequest

    for String +where + P: Stream + 'static, +{ + type Error = Error; + type Future = + Either>, FutureResult>; + + #[inline] + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + let mut tmp; + let cfg = if let Some(cfg) = req.load_config::() { + cfg + } else { + tmp = PayloadConfig::default(); + &tmp + }; + + // check content-type + if let Err(e) = cfg.check_mimetype(req) { + return Either::B(err(e)); + } + + // check charset + let encoding = match req.encoding() { + Ok(enc) => enc, + Err(e) => return Either::B(err(e.into())), + }; + let limit = cfg.limit; + + Either::A(Box::new( + MessageBody::new(req) + .limit(limit) + .from_err() + .and_then(move |body| { + let enc: *const Encoding = encoding as *const Encoding; + if enc == UTF_8 { + Ok(str::from_utf8(body.as_ref()) + .map_err(|_| ErrorBadRequest("Can not decode body"))? + .to_owned()) + } else { + Ok(encoding + .decode(&body, DecoderTrap::Strict) + .map_err(|_| ErrorBadRequest("Can not decode body"))?) + } + }), + )) + } +} +/// Payload configuration for request's payload. +#[derive(Clone)] +pub struct PayloadConfig { + limit: usize, + mimetype: Option, +} + +impl PayloadConfig { + /// Create `PayloadConfig` instance and set max size of payload. + pub fn new(limit: usize) -> Self { + Self::default().limit(limit) + } + + /// Change max size of payload. By default max size is 256Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + /// Set required mime-type of the request. By default mime type is not + /// enforced. + pub fn mimetype(mut self, mt: Mime) -> Self { + self.mimetype = Some(mt); + self + } + + fn check_mimetype

    (&self, req: &ServiceFromRequest

    ) -> Result<(), Error> { + // check content-type + if let Some(ref mt) = self.mimetype { + match req.mime_type() { + Ok(Some(ref req_mt)) => { + if mt != req_mt { + return Err(ErrorBadRequest("Unexpected Content-Type")); + } + } + Ok(None) => { + return Err(ErrorBadRequest("Content-Type is expected")); + } + Err(err) => { + return Err(err.into()); + } + } + } + Ok(()) + } +} + +impl Default for PayloadConfig { + fn default() -> Self { + PayloadConfig { + limit: 262_144, + mimetype: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::header; + use crate::test::TestRequest; + + #[test] + fn test_payload_config() { + let req = TestRequest::default().to_from(); + let cfg = PayloadConfig::default().mimetype(mime::APPLICATION_JSON); + assert!(cfg.check_mimetype(&req).is_err()); + + let req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .to_from(); + assert!(cfg.check_mimetype(&req).is_err()); + + let req = + TestRequest::with_header(header::CONTENT_TYPE, "application/json").to_from(); + assert!(cfg.check_mimetype(&req).is_ok()); + } +} diff --git a/src/extract/query.rs b/src/extract/query.rs new file mode 100644 index 000000000..f0eb6a7ae --- /dev/null +++ b/src/extract/query.rs @@ -0,0 +1,126 @@ +//! Query extractor + +use std::{fmt, ops}; + +use actix_http::error::Error; +use serde::de; +use serde_urlencoded; + +use crate::extract::FromRequest; +use crate::service::ServiceFromRequest; + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +/// Extract typed information from from the request's query. +/// +/// ## Example +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App}; +/// +/// #[derive(Debug, Deserialize)] +/// pub enum ResponseType { +/// Token, +/// Code +/// } +/// +/// #[derive(Deserialize)] +/// pub struct AuthRequest { +/// id: u64, +/// response_type: ResponseType, +/// } +/// +/// // Use `Query` extractor for query information. +/// // This handler get called only if request's query contains `username` field +/// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` +/// fn index(info: web::Query) -> String { +/// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor +/// } +/// ``` +pub struct Query(T); + +impl Query { + /// Deconstruct to a inner value + pub fn into_inner(self) -> T { + self.0 + } +} + +impl ops::Deref for Query { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl ops::DerefMut for Query { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl fmt::Debug for Query { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Display for Query { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +/// Extract typed information from from the request's query. +/// +/// ## Example +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App}; +/// +/// #[derive(Debug, Deserialize)] +/// pub enum ResponseType { +/// Token, +/// Code +/// } +/// +/// #[derive(Deserialize)] +/// pub struct AuthRequest { +/// id: u64, +/// response_type: ResponseType, +/// } +/// +/// // Use `Query` extractor for query information. +/// // This handler get called only if request's query contains `username` field +/// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` +/// fn index(info: web::Query) -> String { +/// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html") +/// .route(web::get().to(index))); // <- use `Query` extractor +/// } +/// ``` +impl FromRequest

    for Query +where + T: de::DeserializeOwned, +{ + type Error = Error; + type Future = Result; + + #[inline] + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + serde_urlencoded::from_str::(req.query_string()) + .map(|val| Ok(Query(val))) + .unwrap_or_else(|e| Err(e.into())) + } +} diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs index 7cf739bc4..f3ccca932 100644 --- a/src/middleware/identity.rs +++ b/src/middleware/identity.rs @@ -145,7 +145,6 @@ struct IdentityItem { impl

    FromRequest

    for Identity { type Error = Error; type Future = Result; - type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { diff --git a/src/request.rs b/src/request.rs index 717514838..5517302f0 100644 --- a/src/request.rs +++ b/src/request.rs @@ -205,7 +205,6 @@ impl HttpMessage for HttpRequest { impl

    FromRequest

    for HttpRequest { type Error = Error; type Future = Result; - type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { diff --git a/src/route.rs b/src/route.rs index c189c61b1..1955a81ad 100644 --- a/src/route.rs +++ b/src/route.rs @@ -6,7 +6,7 @@ use actix_http::{http::Method, Error, Extensions, Response}; use actix_service::{NewService, Service}; use futures::{Async, Future, IntoFuture, Poll}; -use crate::extract::{ConfigStorage, ExtractorConfig, FromRequest}; +use crate::extract::FromRequest; use crate::guard::{self, Guard}; use crate::handler::{AsyncFactory, AsyncHandler, Extract, Factory, Handler}; use crate::responder::Responder; @@ -40,7 +40,7 @@ type BoxedRouteNewService = Box< pub struct Route

    { service: BoxedRouteNewService, ServiceResponse>, guards: Rc>>, - config: ConfigStorage, + config: Option, config_ref: Rc>>>, } @@ -55,13 +55,13 @@ impl Route

    { ), )), guards: Rc::new(Vec::new()), - config: ConfigStorage::default(), + config: None, config_ref, } } - pub(crate) fn finish(self) -> Self { - *self.config_ref.borrow_mut() = self.config.storage.clone(); + pub(crate) fn finish(mut self) -> Self { + *self.config_ref.borrow_mut() = self.config.take().map(|e| Rc::new(e)); self } @@ -252,7 +252,6 @@ impl Route

    { T: FromRequest

    + 'static, R: Responder + 'static, { - T::Config::store_default(&mut self.config); self.service = Box::new(RouteNewService::new( Extract::new(self.config_ref.clone()) .and_then(Handler::new(handler).map_err(|_| panic!())), @@ -324,8 +323,11 @@ impl Route

    { /// )); /// } /// ``` - pub fn config(mut self, config: C) -> Self { - self.config.store(config); + pub fn config(mut self, config: C) -> Self { + if self.config.is_none() { + self.config = Some(Extensions::new()); + } + self.config.as_mut().unwrap().insert(config); self } } diff --git a/src/service.rs b/src/service.rs index 612fe4e89..08330282d 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::cell::{Ref, RefMut}; use std::fmt; use std::marker::PhantomData; @@ -271,13 +270,12 @@ impl

    ServiceFromRequest

    { } /// Load extractor configuration - pub fn load_config(&self) -> Cow { + pub fn load_config(&self) -> Option<&T> { if let Some(ref ext) = self.config { - if let Some(cfg) = ext.get::() { - return Cow::Borrowed(cfg); - } + ext.get::() + } else { + None } - Cow::Owned(T::default()) } } diff --git a/src/state.rs b/src/state.rs index 2c623c70d..b70540c07 100644 --- a/src/state.rs +++ b/src/state.rs @@ -48,7 +48,6 @@ impl Clone for State { impl FromRequest

    for State { type Error = Error; type Future = Result; - type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { From b6c11357983a195b277edf76d0302baf5c51f459 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 10:56:53 -0700 Subject: [PATCH 256/427] hide blocking mod --- src/error.rs | 2 ++ src/lib.rs | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index 54ca74dc2..fd0ee998f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,6 +4,8 @@ pub use actix_http::error::*; use derive_more::{Display, From}; use url::ParseError as UrlParseError; +pub use crate::blocking::BlockingError; + /// Errors which can occur when attempting to generate resource uri. #[derive(Debug, PartialEq, Display, From)] pub enum UrlGenerationError { diff --git a/src/lib.rs b/src/lib.rs index 322070245..f6f722be6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ mod app; mod app_service; -pub mod blocking; +mod blocking; mod config; pub mod error; mod extract; @@ -54,6 +54,7 @@ pub mod dev { //! ``` pub use crate::app::AppRouter; + pub use crate::blocking::CpuFuture; pub use crate::config::{AppConfig, ServiceConfig}; pub use crate::info::ConnectionInfo; pub use crate::rmap::ResourceMap; From 039efc570384c87fcb3442cf3b0a2d5f930d92ef Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 11:04:50 -0700 Subject: [PATCH 257/427] move tests to different mods --- src/extract/mod.rs | 52 ------------------------------------------ src/extract/path.rs | 42 ++++++++++++++++++++++++++++++++++ src/extract/payload.rs | 24 ++++++++++++++++++- 3 files changed, 65 insertions(+), 53 deletions(-) diff --git a/src/extract/mod.rs b/src/extract/mod.rs index d8958b2d9..78c6ba790 100644 --- a/src/extract/mod.rs +++ b/src/extract/mod.rs @@ -256,26 +256,6 @@ mod tests { hello: String, } - #[test] - fn test_bytes() { - let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .to_from(); - - let s = block_on(Bytes::from_request(&mut req)).unwrap(); - assert_eq!(s, Bytes::from_static(b"hello=world")); - } - - #[test] - fn test_string() { - let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .to_from(); - - let s = block_on(String::from_request(&mut req)).unwrap(); - assert_eq!(s, "hello=world"); - } - #[test] fn test_option() { let mut req = TestRequest::with_header( @@ -400,36 +380,4 @@ mod tests { assert_eq!(res[1], "32".to_owned()); } - #[test] - fn test_extract_path_single() { - let resource = ResourceDef::new("/{value}/"); - - let mut req = TestRequest::with_uri("/32/").to_from(); - resource.match_path(req.match_info_mut()); - - assert_eq!(*Path::::from_request(&mut req).unwrap(), 32); - } - - #[test] - fn test_tuple_extract() { - let resource = ResourceDef::new("/{key}/{value}/"); - - let mut req = TestRequest::with_uri("/name/user1/?id=test").to_from(); - resource.match_path(req.match_info_mut()); - - let res = block_on(<(Path<(String, String)>,)>::from_request(&mut req)).unwrap(); - assert_eq!((res.0).0, "name"); - assert_eq!((res.0).1, "user1"); - - let res = block_on( - <(Path<(String, String)>, Path<(String, String)>)>::from_request(&mut req), - ) - .unwrap(); - assert_eq!((res.0).0, "name"); - assert_eq!((res.0).1, "user1"); - assert_eq!((res.1).0, "name"); - assert_eq!((res.1).1, "user1"); - - let () = <()>::from_request(&mut req).unwrap(); - } } diff --git a/src/extract/path.rs b/src/extract/path.rs index d9461263a..fc6811c78 100644 --- a/src/extract/path.rs +++ b/src/extract/path.rs @@ -174,3 +174,45 @@ where Self::extract(req).map_err(ErrorNotFound) } } + +#[cfg(test)] +mod tests { + use actix_router::ResourceDef; + + use super::*; + use crate::test::{block_on, TestRequest}; + + #[test] + fn test_extract_path_single() { + let resource = ResourceDef::new("/{value}/"); + + let mut req = TestRequest::with_uri("/32/").to_from(); + resource.match_path(req.match_info_mut()); + + assert_eq!(*Path::::from_request(&mut req).unwrap(), 32); + } + + #[test] + fn test_tuple_extract() { + let resource = ResourceDef::new("/{key}/{value}/"); + + let mut req = TestRequest::with_uri("/name/user1/?id=test").to_from(); + resource.match_path(req.match_info_mut()); + + let res = block_on(<(Path<(String, String)>,)>::from_request(&mut req)).unwrap(); + assert_eq!((res.0).0, "name"); + assert_eq!((res.0).1, "user1"); + + let res = block_on( + <(Path<(String, String)>, Path<(String, String)>)>::from_request(&mut req), + ) + .unwrap(); + assert_eq!((res.0).0, "name"); + assert_eq!((res.0).1, "user1"); + assert_eq!((res.1).0, "name"); + assert_eq!((res.1).1, "user1"); + + let () = <()>::from_request(&mut req).unwrap(); + } + +} diff --git a/src/extract/payload.rs b/src/extract/payload.rs index 82024c0d8..13532eeef 100644 --- a/src/extract/payload.rs +++ b/src/extract/payload.rs @@ -289,9 +289,11 @@ impl Default for PayloadConfig { #[cfg(test)] mod tests { + use bytes::Bytes; + use super::*; use crate::http::header; - use crate::test::TestRequest; + use crate::test::{block_on, TestRequest}; #[test] fn test_payload_config() { @@ -310,4 +312,24 @@ mod tests { TestRequest::with_header(header::CONTENT_TYPE, "application/json").to_from(); assert!(cfg.check_mimetype(&req).is_ok()); } + + #[test] + fn test_bytes() { + let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .to_from(); + + let s = block_on(Bytes::from_request(&mut req)).unwrap(); + assert_eq!(s, Bytes::from_static(b"hello=world")); + } + + #[test] + fn test_string() { + let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .to_from(); + + let s = block_on(String::from_request(&mut req)).unwrap(); + assert_eq!(s, "hello=world"); + } } From 79875ea03955c9ae56b4efd79d54dfe7157d0a3b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 14:22:53 -0700 Subject: [PATCH 258/427] update deps --- actix-files/src/lib.rs | 19 ++++++++----------- actix-session/src/lib.rs | 1 - 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 14c25be79..3ac176193 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -15,11 +15,11 @@ use mime_guess::get_mime_type; use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; use v_htmlescape::escape as escape_html_entity; -use actix_http::error::{Error, ErrorInternalServerError}; use actix_service::{boxed::BoxedNewService, NewService, Service}; -use actix_web::dev::{HttpServiceFactory, ResourceDef, ServiceConfig}; +use actix_web::dev::{CpuFuture, HttpServiceFactory, ResourceDef, ServiceConfig}; +use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; use actix_web::{ - blocking, FromRequest, HttpRequest, HttpResponse, Responder, ServiceFromRequest, + web, FromRequest, HttpRequest, HttpResponse, Responder, ServiceFromRequest, ServiceRequest, ServiceResponse, }; use futures::future::{ok, FutureResult}; @@ -51,16 +51,14 @@ pub struct ChunkedReadFile { size: u64, offset: u64, file: Option, - fut: Option>, + fut: Option>, counter: u64, } -fn handle_error(err: blocking::BlockingError) -> Error { +fn handle_error(err: BlockingError) -> Error { match err { - blocking::BlockingError::Error(err) => err.into(), - blocking::BlockingError::Canceled => { - ErrorInternalServerError("Unexpected error").into() - } + BlockingError::Error(err) => err.into(), + BlockingError::Canceled => ErrorInternalServerError("Unexpected error").into(), } } @@ -90,7 +88,7 @@ impl Stream for ChunkedReadFile { Ok(Async::Ready(None)) } else { let mut file = self.file.take().expect("Use after completion"); - self.fut = Some(blocking::run(move || { + self.fut = Some(web::block(move || { let max_bytes: usize; max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; let mut buf = Vec::with_capacity(max_bytes); @@ -446,7 +444,6 @@ impl PathBufWrp { impl

    FromRequest

    for PathBufWrp { type Error = UriSegmentError; type Future = Result; - type Config = (); fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { PathBufWrp::get_pathbuf(req.match_info().path()) diff --git a/actix-session/src/lib.rs b/actix-session/src/lib.rs index 62bc5c8fb..79b7e1f9c 100644 --- a/actix-session/src/lib.rs +++ b/actix-session/src/lib.rs @@ -175,7 +175,6 @@ impl Session { impl

    FromRequest

    for Session { type Error = Error; type Future = Result; - type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { From d7557720394a4518d720f54c31f7dd17d11ada3c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 15:30:31 -0700 Subject: [PATCH 259/427] add From impls for ResponseBuilder --- src/response.rs | 80 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/src/response.rs b/src/response.rs index 4e1fe2142..34ac54ea7 100644 --- a/src/response.rs +++ b/src/response.rs @@ -57,28 +57,6 @@ impl Response { resp } - /// Convert `Response` to a `ResponseBuilder` - #[inline] - pub fn into_builder(self) -> ResponseBuilder { - // If this response has cookies, load them into a jar - let mut jar: Option = None; - for c in self.cookies() { - if let Some(ref mut j) = jar { - j.add_original(c.into_owned()); - } else { - let mut j = CookieJar::new(); - j.add_original(c.into_owned()); - jar = Some(j); - } - } - - ResponseBuilder { - head: Some(self.head), - err: None, - cookies: jar, - } - } - /// Convert response to response with body pub fn into_body(self) -> Response { let b = match self.body { @@ -692,6 +670,62 @@ fn parts<'a>( parts.as_mut() } +/// Convert `Response` to a `ResponseBuilder`. Body get dropped. +impl From> for ResponseBuilder { + fn from(res: Response) -> ResponseBuilder { + // If this response has cookies, load them into a jar + let mut jar: Option = None; + for c in res.cookies() { + if let Some(ref mut j) = jar { + j.add_original(c.into_owned()); + } else { + let mut j = CookieJar::new(); + j.add_original(c.into_owned()); + jar = Some(j); + } + } + + ResponseBuilder { + head: Some(res.head), + err: None, + cookies: jar, + } + } +} + +/// Convert `ResponseHead` to a `ResponseBuilder` +impl<'a> From<&'a ResponseHead> for ResponseBuilder { + fn from(head: &'a ResponseHead) -> ResponseBuilder { + // If this response has cookies, load them into a jar + let mut jar: Option = None; + let cookies = CookieIter { + iter: head.headers.get_all(header::SET_COOKIE).iter(), + }; + for c in cookies { + if let Some(ref mut j) = jar { + j.add_original(c.into_owned()); + } else { + let mut j = CookieJar::new(); + j.add_original(c.into_owned()); + jar = Some(j); + } + } + + let mut msg: Message = Message::new(); + msg.version = head.version; + msg.status = head.status; + msg.reason = head.reason; + msg.headers = head.headers.clone(); + msg.no_chunking = head.no_chunking; + + ResponseBuilder { + head: Some(msg), + err: None, + cookies: jar, + } + } +} + impl IntoFuture for ResponseBuilder { type Item = Response; type Error = Error; @@ -989,7 +1023,7 @@ mod tests { resp.add_cookie(&http::Cookie::new("cookie1", "val100")) .unwrap(); - let mut builder = resp.into_builder(); + let mut builder: ResponseBuilder = resp.into(); let resp = builder.status(StatusCode::BAD_REQUEST).finish(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); From 4d96abb639eca231ab26ef6bb11cd4ba102c4040 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 16:35:38 -0700 Subject: [PATCH 260/427] use actix_web::Error for middleware errors --- src/app.rs | 29 ++--- src/app_service.rs | 32 ++--- src/config.rs | 5 +- src/handler.rs | 6 +- src/lib.rs | 5 +- src/middleware/defaultheaders.rs | 3 +- src/middleware/errhandlers.rs | 210 +++++++++++++++++++++++++++++++ src/middleware/mod.rs | 8 +- src/resource.rs | 25 ++-- src/route.rs | 18 +-- src/scope.rs | 24 ++-- src/service.rs | 18 +-- src/test.rs | 7 +- 13 files changed, 305 insertions(+), 85 deletions(-) create mode 100644 src/middleware/errhandlers.rs diff --git a/src/app.rs b/src/app.rs index 29dd1ab60..54b5ded25 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,8 +3,6 @@ use std::marker::PhantomData; use std::rc::Rc; use actix_http::body::{Body, MessageBody}; -use actix_http::PayloadStream; -use actix_router::ResourceDef; use actix_server_config::ServerConfig; use actix_service::boxed::{self, BoxedNewService}; use actix_service::{ @@ -14,6 +12,8 @@ use futures::IntoFuture; use crate::app_service::{AppChain, AppEntry, AppInit, AppRouting, AppRoutingFactory}; use crate::config::{AppConfig, AppConfigInner}; +use crate::dev::{PayloadStream, ResourceDef}; +use crate::error::Error; use crate::resource::Resource; use crate::route::Route; use crate::service::{ @@ -22,7 +22,8 @@ use crate::service::{ }; use crate::state::{State, StateFactory}; -type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; +type HttpNewService

    = + BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; /// Application builder - structure that follows the builder pattern /// for building application instances. @@ -55,7 +56,7 @@ where T: NewService< Request = ServiceRequest, Response = ServiceRequest

    , - Error = (), + Error = Error, InitError = (), >, { @@ -118,7 +119,7 @@ where impl NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, > @@ -127,7 +128,7 @@ where AppRouting

    , Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, F: IntoTransform>, @@ -157,7 +158,7 @@ where impl NewService< Request = ServiceRequest, Response = ServiceRequest, - Error = (), + Error = Error, InitError = (), >, > @@ -165,7 +166,7 @@ where C: NewService< Request = ServiceRequest

    , Response = ServiceRequest, - Error = (), + Error = Error, InitError = (), >, F: IntoNewService, @@ -264,7 +265,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, { @@ -324,7 +325,7 @@ where impl NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, > @@ -333,7 +334,7 @@ where T::Service, Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, B1: MessageBody, @@ -363,7 +364,7 @@ where U: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), > + 'static, { @@ -415,13 +416,13 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, C: NewService< Request = ServiceRequest, Response = ServiceRequest

    , - Error = (), + Error = Error, InitError = (), >, { diff --git a/src/app_service.rs b/src/app_service.rs index 75e4b3164..c59b80bcc 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -11,15 +11,17 @@ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, Poll}; use crate::config::{AppConfig, ServiceConfig}; +use crate::error::Error; use crate::guard::Guard; use crate::rmap::ResourceMap; use crate::service::{ServiceFactory, ServiceRequest, ServiceResponse}; use crate::state::{StateFactory, StateFactoryResult}; type Guards = Vec>; -type HttpService

    = BoxedService, ServiceResponse, ()>; -type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; -type BoxedResponse = Box>; +type HttpService

    = BoxedService, ServiceResponse, Error>; +type HttpNewService

    = + BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; +type BoxedResponse = Box>; /// Service factory to convert `Request` to a `ServiceRequest`. /// It also executes state factories. @@ -29,7 +31,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, { @@ -48,13 +50,13 @@ where C: NewService< Request = ServiceRequest, Response = ServiceRequest

    , - Error = (), + Error = Error, InitError = (), >, T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, { @@ -147,13 +149,13 @@ where C: NewService< Request = ServiceRequest, Response = ServiceRequest

    , - Error = (), + Error = Error, InitError = (), >, T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, { @@ -201,7 +203,7 @@ where /// Service to convert `Request` to a `ServiceRequest` pub struct AppInitService where - C: Service, Error = ()>, + C: Service, Error = Error>, { chain: C, rmap: Rc, @@ -210,7 +212,7 @@ where impl Service for AppInitService where - C: Service, Error = ()>, + C: Service, Error = Error>, { type Request = Request; type Response = ServiceRequest

    ; @@ -240,7 +242,7 @@ pub struct AppRoutingFactory

    { impl NewService for AppRoutingFactory

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = AppRouting

    ; type Future = AppRoutingFactoryResponse

    ; @@ -350,7 +352,7 @@ pub struct AppRouting

    { impl

    Service for AppRouting

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type Future = Either>; fn poll_ready(&mut self) -> Poll<(), Self::Error> { @@ -398,7 +400,7 @@ impl

    AppEntry

    { impl NewService for AppEntry

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = AppRouting

    ; type Future = AppRoutingFactoryResponse

    ; @@ -414,7 +416,7 @@ pub struct AppChain; impl NewService for AppChain { type Request = ServiceRequest; type Response = ServiceRequest; - type Error = (); + type Error = Error; type InitError = (); type Service = AppChain; type Future = FutureResult; @@ -427,7 +429,7 @@ impl NewService for AppChain { impl Service for AppChain { type Request = ServiceRequest; type Response = ServiceRequest; - type Error = (); + type Error = Error; type Future = FutureResult; #[inline] diff --git a/src/config.rs b/src/config.rs index f84376c76..ceb58feb7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,13 +6,14 @@ use actix_http::Extensions; use actix_router::ResourceDef; use actix_service::{boxed, IntoNewService, NewService}; +use crate::error::Error; use crate::guard::Guard; use crate::rmap::ResourceMap; use crate::service::{ServiceRequest, ServiceResponse}; type Guards = Vec>; type HttpNewService

    = - boxed::BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; + boxed::BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; /// Application configuration pub struct ServiceConfig

    { @@ -84,7 +85,7 @@ impl ServiceConfig

    { S: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), > + 'static, { diff --git a/src/handler.rs b/src/handler.rs index 876456510..4ff3193c8 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -193,7 +193,7 @@ where { type Request = (T, HttpRequest); type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = AsyncHandlerService; type Future = FutureResult; @@ -227,7 +227,7 @@ where { type Request = (T, HttpRequest); type Response = ServiceResponse; - type Error = (); + type Error = Error; type Future = AsyncHandlerServiceResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { @@ -255,7 +255,7 @@ where T::Error: Into, { type Item = ServiceResponse; - type Error = (); + type Error = Error; fn poll(&mut self) -> Poll { match self.fut.poll() { diff --git a/src/lib.rs b/src/lib.rs index f6f722be6..c04480af5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,7 +40,6 @@ pub use crate::responder::{Either, Responder}; pub use crate::route::Route; pub use crate::scope::Scope; pub use crate::server::HttpServer; -pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; pub mod dev { //! The `actix-web` prelude for library developers @@ -58,7 +57,9 @@ pub mod dev { pub use crate::config::{AppConfig, ServiceConfig}; pub use crate::info::ConnectionInfo; pub use crate::rmap::ResourceMap; - pub use crate::service::HttpServiceFactory; + pub use crate::service::{ + HttpServiceFactory, ServiceFromRequest, ServiceRequest, ServiceResponse, + }; pub use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder; diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index b4927962f..bca2cf6e0 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -152,9 +152,10 @@ mod tests { use actix_service::FnService; use super::*; + use crate::dev::ServiceRequest; use crate::http::header::CONTENT_TYPE; use crate::test::{block_on, TestRequest}; - use crate::{HttpResponse, ServiceRequest}; + use crate::HttpResponse; #[test] fn test_default_headers() { diff --git a/src/middleware/errhandlers.rs b/src/middleware/errhandlers.rs new file mode 100644 index 000000000..7a79aae16 --- /dev/null +++ b/src/middleware/errhandlers.rs @@ -0,0 +1,210 @@ +use std::rc::Rc; + +use actix_service::{Service, Transform}; +use futures::future::{err, ok, Either, Future, FutureResult}; +use futures::Poll; +use hashbrown::HashMap; + +use crate::dev::{ServiceRequest, ServiceResponse}; +use crate::error::{Error, Result}; +use crate::http::StatusCode; + +/// Error handler response +pub enum ErrorHandlerResponse { + /// New http response got generated + Response(ServiceResponse), + /// Result is a future that resolves to a new http response + Future(Box, Error = Error>>), +} + +type ErrorHandler = Fn(ServiceResponse) -> Result>; + +/// `Middleware` for allowing custom handlers for responses. +/// +/// You can use `ErrorHandlers::handler()` method to register a custom error +/// handler for specific status code. You can modify existing response or +/// create completely new one. +/// +/// ## Example +/// +/// ```rust +/// use actix_web::middleware::{ErrorHandlers, ErrorHandlerResponse}; +/// use actix_web::{web, http, dev, App, HttpRequest, HttpResponse, Result}; +/// +/// fn render_500(mut res: dev::ServiceResponse) -> Result> { +/// res.response_mut() +/// .headers_mut() +/// .insert(http::header::CONTENT_TYPE, http::HeaderValue::from_static("Error")); +/// Ok(ErrorHandlerResponse::Response(res)) +/// } +/// +/// fn main() { +/// let app = App::new() +/// .middleware( +/// ErrorHandlers::new() +/// .handler(http::StatusCode::INTERNAL_SERVER_ERROR, render_500), +/// ) +/// .service(web::resource("/test") +/// .route(web::get().to(|| HttpResponse::Ok())) +/// .route(web::head().to(|| HttpResponse::MethodNotAllowed()) +/// )); +/// } +/// ``` +pub struct ErrorHandlers { + handlers: Rc>>>, +} + +impl Default for ErrorHandlers { + fn default() -> Self { + ErrorHandlers { + handlers: Rc::new(HashMap::new()), + } + } +} + +impl ErrorHandlers { + /// Construct new `ErrorHandlers` instance + pub fn new() -> Self { + ErrorHandlers::default() + } + + /// Register error handler for specified status code + pub fn handler(mut self, status: StatusCode, handler: F) -> Self + where + F: Fn(ServiceResponse) -> Result> + 'static, + { + Rc::get_mut(&mut self.handlers) + .unwrap() + .insert(status, Box::new(handler)); + self + } +} + +impl Transform for ErrorHandlers +where + S: Service< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = Error, + >, + S::Future: 'static, + S::Error: 'static, + B: 'static, +{ + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = Error; + type InitError = (); + type Transform = ErrorHandlersMiddleware; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(ErrorHandlersMiddleware { + service, + handlers: self.handlers.clone(), + }) + } +} + +pub struct ErrorHandlersMiddleware { + service: S, + handlers: Rc>>>, +} + +impl Service for ErrorHandlersMiddleware +where + S: Service< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = Error, + >, + S::Future: 'static, + S::Error: 'static, + B: 'static, +{ + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = Error; + type Future = Box>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready() + } + + fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { + let handlers = self.handlers.clone(); + + Box::new(self.service.call(req).and_then(move |res| { + if let Some(handler) = handlers.get(&res.status()) { + match handler(res) { + Ok(ErrorHandlerResponse::Response(res)) => Either::A(ok(res)), + Ok(ErrorHandlerResponse::Future(fut)) => Either::B(fut), + Err(e) => Either::A(err(e)), + } + } else { + Either::A(ok(res)) + } + })) + } +} + +#[cfg(test)] +mod tests { + use actix_service::FnService; + use futures::future::ok; + + use super::*; + use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode}; + use crate::test::{self, TestRequest}; + use crate::HttpResponse; + + fn render_500(mut res: ServiceResponse) -> Result> { + res.response_mut() + .headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); + Ok(ErrorHandlerResponse::Response(res)) + } + + #[test] + fn test_handler() { + let srv = FnService::new(|req: ServiceRequest<_>| { + req.into_response(HttpResponse::InternalServerError().finish()) + }); + + let mut mw = test::block_on( + ErrorHandlers::new() + .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500) + .new_transform(srv), + ) + .unwrap(); + + let resp = test::call_success(&mut mw, TestRequest::default().to_service()); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); + } + + fn render_500_async( + mut res: ServiceResponse, + ) -> Result> { + res.response_mut() + .headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); + Ok(ErrorHandlerResponse::Future(Box::new(ok(res)))) + } + + #[test] + fn test_handler_async() { + let srv = FnService::new(|req: ServiceRequest<_>| { + req.into_response(HttpResponse::InternalServerError().finish()) + }); + + let mut mw = test::block_on( + ErrorHandlers::new() + .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500_async) + .new_transform(srv), + ) + .unwrap(); + + let resp = test::call_success(&mut mw, TestRequest::default().to_service()); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); + } +} diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 288c1d63b..6e55cd67e 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -4,13 +4,15 @@ mod compress; pub use self::compress::Compress; mod defaultheaders; +mod errhandlers; +mod logger; + pub use self::defaultheaders::DefaultHeaders; +pub use self::errhandlers::{ErrorHandlerResponse, ErrorHandlers}; +pub use self::logger::Logger; // #[cfg(feature = "session")] // pub use actix_session as session; -mod logger; -pub use self::logger::Logger; - #[cfg(feature = "session")] pub mod identity; diff --git a/src/resource.rs b/src/resource.rs index 57f6f710a..e4fe65c05 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -17,8 +17,9 @@ use crate::responder::Responder; use crate::route::{CreateRouteService, Route, RouteService}; use crate::service::{ServiceRequest, ServiceResponse}; -type HttpService

    = BoxedService, ServiceResponse, ()>; -type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; +type HttpService

    = BoxedService, ServiceResponse, Error>; +type HttpNewService

    = + BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; /// *Resource* is an entry in route table which corresponds to requested URL. /// @@ -70,7 +71,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, { @@ -232,7 +233,7 @@ where impl NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, > @@ -241,7 +242,7 @@ where T::Service, Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, F: IntoTransform, @@ -266,7 +267,7 @@ where U: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, > + 'static, { // create and configure default resource @@ -284,7 +285,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), > + 'static, { @@ -314,7 +315,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, { @@ -336,7 +337,7 @@ pub struct ResourceFactory

    { impl NewService for ResourceFactory

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = ResourceService

    ; type Future = CreateResourceService

    ; @@ -427,9 +428,9 @@ pub struct ResourceService

    { impl

    Service for ResourceService

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type Future = Either< - Box>, + Box>, Either< Box>, FutureResult, @@ -472,7 +473,7 @@ impl

    ResourceEndpoint

    { impl NewService for ResourceEndpoint

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = ResourceService

    ; type Future = CreateResourceService

    ; diff --git a/src/route.rs b/src/route.rs index 1955a81ad..707da3d85 100644 --- a/src/route.rs +++ b/src/route.rs @@ -17,8 +17,8 @@ type BoxedRouteService = Box< Service< Request = Req, Response = Res, - Error = (), - Future = Box>, + Error = Error, + Future = Box>, >, >; @@ -26,7 +26,7 @@ type BoxedRouteNewService = Box< NewService< Request = Req, Response = Res, - Error = (), + Error = Error, InitError = (), Service = BoxedRouteService, Future = Box, Error = ()>>, @@ -73,7 +73,7 @@ impl Route

    { impl

    NewService for Route

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = RouteService

    ; type Future = CreateRouteService

    ; @@ -129,7 +129,7 @@ impl

    RouteService

    { impl

    Service for RouteService

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type Future = Box>; fn poll_ready(&mut self) -> Poll<(), Self::Error> { @@ -188,7 +188,7 @@ impl Route

    { // T: NewService< // Request = HandlerRequest, // Response = HandlerRequest, - // InitError = (), + // InitError = Error, // >, // { // RouteServiceBuilder { @@ -372,7 +372,7 @@ where { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = BoxedRouteService, Self::Response>; type Future = Box>; @@ -410,11 +410,11 @@ where { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type Future = Box>; fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.service.poll_ready().map_err(|_| ()) + self.service.poll_ready().map_err(|(e, _)| e) } fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { diff --git a/src/scope.rs b/src/scope.rs index 3b5061737..9f5b650cd 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -11,6 +11,7 @@ use futures::future::{ok, Either, Future, FutureResult}; use futures::{Async, Poll}; use crate::dev::{HttpServiceFactory, ServiceConfig}; +use crate::error::Error; use crate::guard::Guard; use crate::resource::Resource; use crate::rmap::ResourceMap; @@ -20,9 +21,10 @@ use crate::service::{ }; type Guards = Vec>; -type HttpService

    = BoxedService, ServiceResponse, ()>; -type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; -type BoxedResponse = Box>; +type HttpService

    = BoxedService, ServiceResponse, Error>; +type HttpNewService

    = + BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; +type BoxedResponse = Box>; /// Resources scope /// @@ -83,7 +85,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, { @@ -176,7 +178,7 @@ where U: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), > + 'static, { @@ -201,7 +203,7 @@ where impl NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, > @@ -210,7 +212,7 @@ where T::Service, Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, F: IntoTransform, @@ -233,7 +235,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), > + 'static, { @@ -290,7 +292,7 @@ pub struct ScopeFactory

    { impl NewService for ScopeFactory

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = ScopeService

    ; type Future = ScopeFactoryResponse

    ; @@ -406,7 +408,7 @@ pub struct ScopeService

    { impl

    Service for ScopeService

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type Future = Either>; fn poll_ready(&mut self) -> Poll<(), Self::Error> { @@ -450,7 +452,7 @@ impl

    ScopeEndpoint

    { impl NewService for ScopeEndpoint

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = ScopeService

    ; type Future = ScopeFactoryResponse

    ; diff --git a/src/service.rs b/src/service.rs index 08330282d..e907a1abc 100644 --- a/src/service.rs +++ b/src/service.rs @@ -340,6 +340,12 @@ impl ServiceResponse { Self::from_err(err, self.request) } + /// Create service response + #[inline] + pub fn into_response(self, response: Response) -> ServiceResponse { + ServiceResponse::new(self.request, response) + } + /// Get reference to original request #[inline] pub fn request(&self) -> &HttpRequest { @@ -358,18 +364,6 @@ impl ServiceResponse { &mut self.response } - /// Get the headers from the response - #[inline] - pub fn headers(&self) -> &HeaderMap { - self.response.headers() - } - - /// Get a mutable reference to the headers - #[inline] - pub fn headers_mut(&mut self) -> &mut HeaderMap { - self.response.headers_mut() - } - /// Execute closure and in case of error convert it to response. pub fn checked_expr(mut self, f: F) -> Self where diff --git a/src/test.rs b/src/test.rs index b47daa2c6..445924006 100644 --- a/src/test.rs +++ b/src/test.rs @@ -14,9 +14,9 @@ use bytes::Bytes; use futures::Future; use crate::config::{AppConfig, AppConfigInner}; -use crate::request::HttpRequest; use crate::rmap::ResourceMap; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; +use crate::{HttpRequest, HttpResponse}; thread_local! { static RT: RefCell = { @@ -277,6 +277,11 @@ impl TestRequest { self.req.finish() } + /// Complete request creation and generate `ServiceResponse` instance + pub fn to_response(self, res: HttpResponse) -> ServiceResponse { + self.to_service().into_response(res) + } + /// Complete request creation and generate `HttpRequest` instance pub fn to_http_request(mut self) -> HttpRequest { let req = self.req.finish(); From 615fbb49bdeb3fd8efa83672a7b71caf4f146465 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 17:00:03 -0700 Subject: [PATCH 261/427] support cookies in TestRequest --- src/test.rs | 117 +++++++++++----------------------------------------- 1 file changed, 24 insertions(+), 93 deletions(-) diff --git a/src/test.rs b/src/test.rs index e2e0fd76e..4ac30be00 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,10 +1,12 @@ //! Test Various helpers for Actix applications to use during testing. +use std::fmt::Write as FmtWrite; use std::str::FromStr; use bytes::Bytes; -use cookie::Cookie; -use http::header::HeaderName; +use cookie::{Cookie, CookieJar}; +use http::header::{self, HeaderName, HeaderValue}; use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; +use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use crate::header::{Header, IntoHeaderValue}; use crate::payload::Payload; @@ -44,7 +46,7 @@ struct Inner { method: Method, uri: Uri, headers: HeaderMap, - _cookies: Option>>, + cookies: CookieJar, payload: Option, } @@ -55,7 +57,7 @@ impl Default for TestRequest { uri: Uri::from_str("/").unwrap(), version: Version::HTTP_11, headers: HeaderMap::new(), - _cookies: None, + cookies: CookieJar::new(), payload: None, })) } @@ -123,6 +125,12 @@ impl TestRequest { panic!("Can not create header"); } + /// Set cookie for this request + pub fn cookie<'a>(mut self, cookie: Cookie<'a>) -> Self { + parts(&mut self.0).cookies.add(cookie.into_owned()); + self + } + /// Set request payload pub fn set_payload>(&mut self, data: B) -> &mut Self { let mut payload = crate::h1::Payload::empty(); @@ -143,7 +151,7 @@ impl TestRequest { version, headers, payload, - .. + cookies, } = self.0.take().expect("cannot reuse test request builder");; let mut req = if let Some(pl) = payload { @@ -158,96 +166,19 @@ impl TestRequest { head.version = version; head.headers = headers; - // req.set_cookies(cookies); + let mut cookie = String::new(); + for c in cookies.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); + } + head.headers.insert( + header::COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); + req } - - // /// This method generates `HttpRequest` instance and runs handler - // /// with generated request. - // pub fn run>(self, h: &H) -> Result { - // let req = self.finish(); - // let resp = h.handle(&req); - - // match resp.respond_to(&req) { - // Ok(resp) => match resp.into().into() { - // AsyncResultItem::Ok(resp) => Ok(resp), - // AsyncResultItem::Err(err) => Err(err), - // AsyncResultItem::Future(fut) => { - // let mut sys = System::new("test"); - // sys.block_on(fut) - // } - // }, - // Err(err) => Err(err.into()), - // } - // } - - // /// This method generates `HttpRequest` instance and runs handler - // /// with generated request. - // /// - // /// This method panics is handler returns actor. - // pub fn run_async(self, h: H) -> Result - // where - // H: Fn(HttpRequest) -> F + 'static, - // F: Future + 'static, - // R: Responder + 'static, - // E: Into + 'static, - // { - // let req = self.finish(); - // let fut = h(req.clone()); - - // let mut sys = System::new("test"); - // match sys.block_on(fut) { - // Ok(r) => match r.respond_to(&req) { - // Ok(reply) => match reply.into().into() { - // AsyncResultItem::Ok(resp) => Ok(resp), - // _ => panic!("Nested async replies are not supported"), - // }, - // Err(e) => Err(e), - // }, - // Err(err) => Err(err), - // } - // } - - // /// This method generates `HttpRequest` instance and executes handler - // pub fn run_async_result(self, f: F) -> Result - // where - // F: FnOnce(&HttpRequest) -> R, - // R: Into>, - // { - // let req = self.finish(); - // let res = f(&req); - - // match res.into().into() { - // AsyncResultItem::Ok(resp) => Ok(resp), - // AsyncResultItem::Err(err) => Err(err), - // AsyncResultItem::Future(fut) => { - // let mut sys = System::new("test"); - // sys.block_on(fut) - // } - // } - // } - - // /// This method generates `HttpRequest` instance and executes handler - // pub fn execute(self, f: F) -> Result - // where - // F: FnOnce(&HttpRequest) -> R, - // R: Responder + 'static, - // { - // let req = self.finish(); - // let resp = f(&req); - - // match resp.respond_to(&req) { - // Ok(resp) => match resp.into().into() { - // AsyncResultItem::Ok(resp) => Ok(resp), - // AsyncResultItem::Err(err) => Err(err), - // AsyncResultItem::Future(fut) => { - // let mut sys = System::new("test"); - // sys.block_on(fut) - // } - // }, - // Err(err) => Err(err.into()), - // } - // } } #[inline] From 50a0cb5653d6d7558caa6a97b0526120486ec8ba Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 17:02:14 -0700 Subject: [PATCH 262/427] do no move self --- src/test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test.rs b/src/test.rs index 4ac30be00..63ad14994 100644 --- a/src/test.rs +++ b/src/test.rs @@ -126,7 +126,7 @@ impl TestRequest { } /// Set cookie for this request - pub fn cookie<'a>(mut self, cookie: Cookie<'a>) -> Self { + pub fn cookie<'a>(&mut self, cookie: Cookie<'a>) -> &mut Self { parts(&mut self.0).cookies.add(cookie.into_owned()); self } From 64360041944638c45f658b9c58b66186127f4344 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 17:06:43 -0700 Subject: [PATCH 263/427] set test cookie if it is not empty --- src/test.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/test.rs b/src/test.rs index 63ad14994..c60d2d01c 100644 --- a/src/test.rs +++ b/src/test.rs @@ -172,10 +172,12 @@ impl TestRequest { let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); let _ = write!(&mut cookie, "; {}={}", name, value); } - head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); + if !cookie.is_empty() { + head.headers.insert( + header::COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); + } req } From 0f0d6b65cab6dfa162c13c4f6c6ba6dbd355357c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 16:39:46 -0700 Subject: [PATCH 264/427] update service request/response location --- actix-files/src/lib.rs | 15 ++++++++------- actix-session/src/cookie.rs | 3 ++- actix-session/src/lib.rs | 2 +- src/extract/mod.rs | 8 ++++---- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 3ac176193..07fc00631 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -16,12 +16,12 @@ use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; use v_htmlescape::escape as escape_html_entity; use actix_service::{boxed::BoxedNewService, NewService, Service}; -use actix_web::dev::{CpuFuture, HttpServiceFactory, ResourceDef, ServiceConfig}; -use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; -use actix_web::{ - web, FromRequest, HttpRequest, HttpResponse, Responder, ServiceFromRequest, +use actix_web::dev::{ + CpuFuture, HttpServiceFactory, ResourceDef, ServiceConfig, ServiceFromRequest, ServiceRequest, ServiceResponse, }; +use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; +use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder}; use futures::future::{ok, FutureResult}; mod config; @@ -34,7 +34,8 @@ pub use crate::config::{DefaultConfig, StaticFileConfig}; pub use crate::named::NamedFile; pub use crate::range::HttpRange; -type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; +type HttpNewService

    = + BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; /// Return the MIME type associated with a filename extension (case-insensitive). /// If `ext` is empty or no associated type for the extension was found, returns @@ -319,7 +320,7 @@ where impl NewService for Files { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type Service = FilesService; type InitError = (); type Future = FutureResult; @@ -352,7 +353,7 @@ pub struct FilesService { impl Service for FilesService { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type Future = FutureResult; fn poll_ready(&mut self) -> Poll<(), Self::Error> { diff --git a/actix-session/src/cookie.rs b/actix-session/src/cookie.rs index e25031456..37c552ea8 100644 --- a/actix-session/src/cookie.rs +++ b/actix-session/src/cookie.rs @@ -19,8 +19,9 @@ use std::collections::HashMap; use std::rc::Rc; use actix_service::{Service, Transform}; +use actix_web::dev::{ServiceRequest, ServiceResponse}; use actix_web::http::{header::SET_COOKIE, HeaderValue}; -use actix_web::{Error, HttpMessage, ResponseError, ServiceRequest, ServiceResponse}; +use actix_web::{Error, HttpMessage, ResponseError}; use cookie::{Cookie, CookieJar, Key, SameSite}; use derive_more::{Display, From}; use futures::future::{ok, Future, FutureResult}; diff --git a/actix-session/src/lib.rs b/actix-session/src/lib.rs index 79b7e1f9c..1dd367ba7 100644 --- a/actix-session/src/lib.rs +++ b/actix-session/src/lib.rs @@ -45,8 +45,8 @@ use std::cell::RefCell; use std::rc::Rc; +use actix_web::dev::{ServiceFromRequest, ServiceRequest, ServiceResponse}; use actix_web::{Error, FromRequest, HttpMessage}; -use actix_web::{ServiceFromRequest, ServiceRequest, ServiceResponse}; use hashbrown::HashMap; use serde::de::DeserializeOwned; use serde::Serialize; diff --git a/src/extract/mod.rs b/src/extract/mod.rs index 78c6ba790..25a046d47 100644 --- a/src/extract/mod.rs +++ b/src/extract/mod.rs @@ -40,7 +40,7 @@ pub trait FromRequest

    : Sized { /// /// ```rust /// # #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, Error, FromRequest, ServiceFromRequest}; +/// use actix_web::{web, dev, App, Error, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use rand; /// @@ -53,7 +53,7 @@ pub trait FromRequest

    : Sized { /// type Error = Error; /// type Future = Result; /// -/// fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { +/// fn from_request(req: &mut dev::ServiceFromRequest

    ) -> Self::Future { /// if rand::random() { /// Ok(Thing { name: "thingy".into() }) /// } else { @@ -107,7 +107,7 @@ where /// /// ```rust /// # #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, Result, Error, FromRequest, ServiceFromRequest}; +/// use actix_web::{web, dev, App, Result, Error, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use rand; /// @@ -120,7 +120,7 @@ where /// type Error = Error; /// type Future = Result; /// -/// fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { +/// fn from_request(req: &mut dev::ServiceFromRequest

    ) -> Self::Future { /// if rand::random() { /// Ok(Thing { name: "thingy".into() }) /// } else { From b8829bbf221dc5611973d524305ca59aaf10b3d6 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 17:10:41 -0700 Subject: [PATCH 265/427] add identity middleware tests --- src/middleware/identity.rs | 66 ++++++++++++++++++++++++++++++++++++++ src/test.rs | 7 ++++ 2 files changed, 73 insertions(+) diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs index f3ccca932..d0a4146ae 100644 --- a/src/middleware/identity.rs +++ b/src/middleware/identity.rs @@ -461,3 +461,69 @@ impl IdentityPolicy for CookieIdentityPolicy { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::StatusCode; + use crate::test::{self, TestRequest}; + use crate::{web, App, HttpResponse}; + + #[test] + fn test_identity() { + let mut srv = test::init_service( + App::new() + .middleware(IdentityService::new( + CookieIdentityPolicy::new(&[0; 32]) + .domain("www.rust-lang.org") + .name("actix_auth") + .path("/") + .secure(true), + )) + .service(web::resource("/index").to(|id: Identity| { + if id.identity().is_some() { + HttpResponse::Created() + } else { + HttpResponse::Ok() + } + })) + .service(web::resource("/login").to(|id: Identity| { + id.remember("test".to_string()); + HttpResponse::Ok() + })) + .service(web::resource("/logout").to(|id: Identity| { + if id.identity().is_some() { + id.forget(); + HttpResponse::Ok() + } else { + HttpResponse::BadRequest() + } + })), + ); + let resp = + test::call_success(&mut srv, TestRequest::with_uri("/index").to_request()); + assert_eq!(resp.status(), StatusCode::OK); + + let resp = + test::call_success(&mut srv, TestRequest::with_uri("/login").to_request()); + assert_eq!(resp.status(), StatusCode::OK); + let c = resp.cookies().next().unwrap().to_owned(); + + let resp = test::call_success( + &mut srv, + TestRequest::with_uri("/index") + .cookie(c.clone()) + .to_request(), + ); + assert_eq!(resp.status(), StatusCode::CREATED); + + let resp = test::call_success( + &mut srv, + TestRequest::with_uri("/logout") + .cookie(c.clone()) + .to_request(), + ); + assert_eq!(resp.status(), StatusCode::OK); + assert!(resp.headers().contains_key(header::SET_COOKIE)) + } +} diff --git a/src/test.rs b/src/test.rs index 445924006..03700b679 100644 --- a/src/test.rs +++ b/src/test.rs @@ -11,6 +11,7 @@ use actix_rt::Runtime; use actix_server_config::ServerConfig; use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; +use cookie::Cookie; use futures::Future; use crate::config::{AppConfig, AppConfigInner}; @@ -241,6 +242,12 @@ impl TestRequest { self } + /// Set cookie for this request + pub fn cookie<'a>(mut self, cookie: Cookie<'a>) -> Self { + self.req.cookie(cookie); + self + } + /// Set request payload pub fn set_payload>(mut self, data: B) -> Self { self.req.set_payload(data); From 9680423025a9ece3e0d3bb4148655b9867120c7c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 18:33:47 -0700 Subject: [PATCH 266/427] Add more tests for route --- src/app.rs | 80 ---------------------------------------------------- src/lib.rs | 3 +- src/route.rs | 38 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 81 deletions(-) diff --git a/src/app.rs b/src/app.rs index 54b5ded25..2e2a8c2dd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -526,84 +526,4 @@ mod tests { let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } - - // #[test] - // fn test_handler() { - // let app = App::new() - // .handler("/test", |_: &_| HttpResponse::Ok()) - // .finish(); - - // let req = TestRequest::with_uri("/test").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - - // let req = TestRequest::with_uri("/test/").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - - // let req = TestRequest::with_uri("/test/app").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - - // let req = TestRequest::with_uri("/testapp").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - // let req = TestRequest::with_uri("/blah").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - // } - - // #[test] - // fn test_handler2() { - // let app = App::new() - // .handler("test", |_: &_| HttpResponse::Ok()) - // .finish(); - - // let req = TestRequest::with_uri("/test").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - - // let req = TestRequest::with_uri("/test/").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - - // let req = TestRequest::with_uri("/test/app").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - - // let req = TestRequest::with_uri("/testapp").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - // let req = TestRequest::with_uri("/blah").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - // } - - // #[test] - // fn test_route() { - // let app = App::new() - // .route("/test", Method::GET, |_: HttpRequest| HttpResponse::Ok()) - // .route("/test", Method::POST, |_: HttpRequest| { - // HttpResponse::Created() - // }) - // .finish(); - - // let req = TestRequest::with_uri("/test").method(Method::GET).request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - - // let req = TestRequest::with_uri("/test") - // .method(Method::POST) - // .request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - - // let req = TestRequest::with_uri("/test") - // .method(Method::HEAD) - // .request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - // } } diff --git a/src/lib.rs b/src/lib.rs index c04480af5..62f6399d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,7 +79,7 @@ pub mod dev { } pub mod web { - use actix_http::{http::Method, Error, Response}; + use actix_http::{http::Method, Response}; use futures::IntoFuture; pub use actix_http::Response as HttpResponse; @@ -93,6 +93,7 @@ pub mod web { use crate::route::Route; use crate::scope::Scope; + pub use crate::error::Error; pub use crate::extract::{Form, Json, Path, Payload, Query}; pub use crate::extract::{FormConfig, JsonConfig, PayloadConfig}; pub use crate::request::HttpRequest; diff --git a/src/route.rs b/src/route.rs index 707da3d85..5d339a3bb 100644 --- a/src/route.rs +++ b/src/route.rs @@ -424,3 +424,41 @@ where })) } } + +#[cfg(test)] +mod tests { + use crate::http::{Method, StatusCode}; + use crate::test::{call_success, init_service, TestRequest}; + use crate::{web, App, Error, HttpResponse}; + + #[test] + fn test_route() { + let mut srv = init_service( + App::new().service( + web::resource("/test") + .route(web::get().to(|| HttpResponse::Ok())) + .route( + web::post().to_async(|| Ok::<_, Error>(HttpResponse::Created())), + ), + ), + ); + + let req = TestRequest::with_uri("/test") + .method(Method::GET) + .to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/test") + .method(Method::POST) + .to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::CREATED); + + let req = TestRequest::with_uri("/test") + .method(Method::HEAD) + .to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } +} From cc7f6b5eef4d9e2f67265f421e8d1e39fdf70168 Mon Sep 17 00:00:00 2001 From: David McGuire Date: Sun, 10 Mar 2019 21:26:54 -0700 Subject: [PATCH 267/427] Fix preflight CORS header compliance; refactor previous patch. (#717) --- CHANGES.md | 2 + src/middleware/cors.rs | 119 +++++++++++++++++++++++++---------------- 2 files changed, 74 insertions(+), 47 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8cce71597..76f3465ef 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,8 @@ * Do not remove `Content-Length` on `Body::Empty` and insert zero value if it is missing for `POST` and `PUT` methods. +* Fix preflight CORS header compliance; refactor previous patch (#603). #717 + ## [0.7.18] - 2019-01-10 ### Added diff --git a/src/middleware/cors.rs b/src/middleware/cors.rs index 386d00078..80ee5b193 100644 --- a/src/middleware/cors.rs +++ b/src/middleware/cors.rs @@ -307,6 +307,32 @@ impl Cors { } } + fn access_control_allow_origin(&self, req: &Request) -> Option { + match self.inner.origins { + AllOrSome::All => { + if self.inner.send_wildcard { + Some(HeaderValue::from_static("*")) + } else if let Some(origin) = req.headers().get(header::ORIGIN) { + Some(origin.clone()) + } else { + None + } + } + AllOrSome::Some(ref origins) => { + if let Some(origin) = req.headers().get(header::ORIGIN).filter(|o| { + match o.to_str() { + Ok(os) => origins.contains(os), + _ => false + } + }) { + Some(origin.clone()) + } else { + Some(self.inner.origins_str.as_ref().unwrap().clone()) + } + } + } + } + fn validate_allowed_method(&self, req: &Request) -> Result<(), CorsError> { if let Some(hdr) = req.headers().get(header::ACCESS_CONTROL_REQUEST_METHOD) { if let Ok(meth) = hdr.to_str() { @@ -390,21 +416,9 @@ impl Middleware for Cors { }).if_some(headers, |headers, resp| { let _ = resp.header(header::ACCESS_CONTROL_ALLOW_HEADERS, headers); - }).if_true(self.inner.origins.is_all(), |resp| { - if self.inner.send_wildcard { - resp.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - } else { - let origin = req.headers().get(header::ORIGIN).unwrap(); - resp.header( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - origin.clone(), - ); - } - }).if_true(self.inner.origins.is_some(), |resp| { - resp.header( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - self.inner.origins_str.as_ref().unwrap().clone(), - ); + }).if_some(self.access_control_allow_origin(&req), |origin, resp| { + let _ = + resp.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin); }).if_true(self.inner.supports_credentials, |resp| { resp.header(header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); }).header( @@ -430,37 +444,11 @@ impl Middleware for Cors { fn response( &self, req: &HttpRequest, mut resp: HttpResponse, ) -> Result { - match self.inner.origins { - AllOrSome::All => { - if self.inner.send_wildcard { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - HeaderValue::from_static("*"), - ); - } else if let Some(origin) = req.headers().get(header::ORIGIN) { - resp.headers_mut() - .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone()); - } - } - AllOrSome::Some(ref origins) => { - if let Some(origin) = req.headers().get(header::ORIGIN).filter(|o| { - match o.to_str() { - Ok(os) => origins.contains(os), - _ => false - } - }) { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - origin.clone(), - ); - } else { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - self.inner.origins_str.as_ref().unwrap().clone() - ); - }; - } - } + + if let Some(origin) = self.access_control_allow_origin(req) { + resp.headers_mut() + .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone()); + }; if let Some(ref expose) = self.inner.expose_hdrs { resp.headers_mut().insert( @@ -1201,7 +1189,6 @@ mod tests { let resp: HttpResponse = HttpResponse::Ok().into(); let resp = cors.response(&req, resp).unwrap().response(); - print!("{:?}", resp); assert_eq!( &b"https://example.com"[..], resp.headers() @@ -1224,4 +1211,42 @@ mod tests { .as_bytes() ); } + + #[test] + fn test_multiple_origins_preflight() { + let cors = Cors::build() + .allowed_origin("https://example.com") + .allowed_origin("https://example.org") + .allowed_methods(vec![Method::GET]) + .finish(); + + + let req = TestRequest::with_header("Origin", "https://example.com") + .header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET") + .method(Method::OPTIONS) + .finish(); + + let resp = cors.start(&req).ok().unwrap().response(); + assert_eq!( + &b"https://example.com"[..], + resp.headers() + .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) + .unwrap() + .as_bytes() + ); + + let req = TestRequest::with_header("Origin", "https://example.org") + .header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET") + .method(Method::OPTIONS) + .finish(); + + let resp = cors.start(&req).ok().unwrap().response(); + assert_eq!( + &b"https://example.org"[..], + resp.headers() + .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) + .unwrap() + .as_bytes() + ); + } } From ad43ca735b748c41bc6ff0da8948544be85e590d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 11 Mar 2019 15:09:42 -0700 Subject: [PATCH 268/427] update server service requirenments --- examples/framed_hello.rs | 7 +-- src/builder.rs | 6 +-- src/h1/service.rs | 67 ++++++++++++++-------------- src/h2/service.rs | 40 ++++++++--------- src/service/service.rs | 94 +++++++++++++++++++++++++++------------- tests/test_server.rs | 2 +- tests/test_ws.rs | 6 ++- 7 files changed, 130 insertions(+), 92 deletions(-) diff --git a/examples/framed_hello.rs b/examples/framed_hello.rs index ff3977854..74b0f7dfd 100644 --- a/examples/framed_hello.rs +++ b/examples/framed_hello.rs @@ -2,8 +2,8 @@ use std::{env, io}; use actix_codec::Framed; use actix_http::{h1, Response, SendResponse, ServiceConfig}; -use actix_server::Server; -use actix_service::NewService; +use actix_server::{Io, Server}; +use actix_service::{fn_service, NewService}; use actix_utils::framed::IntoFramed; use actix_utils::stream::TakeItem; use futures::Future; @@ -14,7 +14,8 @@ fn main() -> io::Result<()> { Server::build() .bind("framed_hello", "127.0.0.1:8080", || { - IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) + fn_service(|io: Io<_>| Ok(io.into_parts().0)) + .and_then(IntoFramed::new(|| h1::Codec::new(ServiceConfig::default()))) .and_then(TakeItem::new().map_err(|_| ())) .and_then(|(_req, _framed): (_, Framed<_, _>)| { SendResponse::send(_framed, Response::Ok().body("Hello world!")) diff --git a/src/builder.rs b/src/builder.rs index 1df96b0e1..2f7466a90 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -95,7 +95,7 @@ where // } /// Finish service configuration and create *http service* for HTTP/1 protocol. - pub fn h1(self, service: F) -> H1Service + pub fn h1(self, service: F) -> H1Service where B: MessageBody + 'static, F: IntoNewService, @@ -110,7 +110,7 @@ where } /// Finish service configuration and create *http service* for HTTP/2 protocol. - pub fn h2(self, service: F) -> H2Service + pub fn h2(self, service: F) -> H2Service where B: MessageBody + 'static, F: IntoNewService, @@ -125,7 +125,7 @@ where } /// Finish service configuration and create `HttpService` instance. - pub fn finish(self, service: F) -> HttpService + pub fn finish(self, service: F) -> HttpService where B: MessageBody + 'static, F: IntoNewService, diff --git a/src/h1/service.rs b/src/h1/service.rs index e55ff0d99..f3301b9b2 100644 --- a/src/h1/service.rs +++ b/src/h1/service.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use std::marker::PhantomData; use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_server_config::ServerConfig as SrvConfig; +use actix_server_config::{Io, ServerConfig as SrvConfig}; use actix_service::{IntoNewService, NewService, Service}; use actix_utils::cloneable::CloneableService; use futures::future::{ok, FutureResult}; @@ -19,13 +19,13 @@ use super::dispatcher::Dispatcher; use super::Message; /// `NewService` implementation for HTTP1 transport -pub struct H1Service { +pub struct H1Service { srv: S, cfg: ServiceConfig, - _t: PhantomData<(T, B)>, + _t: PhantomData<(T, P, B)>, } -impl H1Service +impl H1Service where S: NewService, S::Error: Debug, @@ -57,7 +57,7 @@ where } } -impl NewService for H1Service +impl NewService for H1Service where T: AsyncRead + AsyncWrite, S: NewService, @@ -66,12 +66,12 @@ where S::Service: 'static, B: MessageBody, { - type Request = T; + type Request = Io; type Response = (); type Error = DispatchError; type InitError = S::InitError; - type Service = H1ServiceHandler; - type Future = H1ServiceResponse; + type Service = H1ServiceHandler; + type Future = H1ServiceResponse; fn new_service(&self, cfg: &SrvConfig) -> Self::Future { H1ServiceResponse { @@ -83,13 +83,13 @@ where } #[doc(hidden)] -pub struct H1ServiceResponse, B> { +pub struct H1ServiceResponse, B> { fut: ::Future, cfg: Option, - _t: PhantomData<(T, B)>, + _t: PhantomData<(T, P, B)>, } -impl Future for H1ServiceResponse +impl Future for H1ServiceResponse where T: AsyncRead + AsyncWrite, S: NewService, @@ -98,7 +98,7 @@ where S::Response: Into>, B: MessageBody, { - type Item = H1ServiceHandler; + type Item = H1ServiceHandler; type Error = S::InitError; fn poll(&mut self) -> Poll { @@ -111,20 +111,20 @@ where } /// `Service` implementation for HTTP1 transport -pub struct H1ServiceHandler { +pub struct H1ServiceHandler { srv: CloneableService, cfg: ServiceConfig, - _t: PhantomData<(T, B)>, + _t: PhantomData<(T, P, B)>, } -impl H1ServiceHandler +impl H1ServiceHandler where S: Service, S::Error: Debug, S::Response: Into>, B: MessageBody, { - fn new(cfg: ServiceConfig, srv: S) -> H1ServiceHandler { + fn new(cfg: ServiceConfig, srv: S) -> H1ServiceHandler { H1ServiceHandler { srv: CloneableService::new(srv), cfg, @@ -133,7 +133,7 @@ where } } -impl Service for H1ServiceHandler +impl Service for H1ServiceHandler where T: AsyncRead + AsyncWrite, S: Service, @@ -141,7 +141,7 @@ where S::Response: Into>, B: MessageBody, { - type Request = T; + type Request = Io; type Response = (); type Error = DispatchError; type Future = Dispatcher; @@ -153,19 +153,19 @@ where }) } - fn call(&mut self, req: T) -> Self::Future { - Dispatcher::new(req, self.cfg.clone(), self.srv.clone()) + fn call(&mut self, req: Self::Request) -> Self::Future { + Dispatcher::new(req.into_parts().0, self.cfg.clone(), self.srv.clone()) } } /// `NewService` implementation for `OneRequestService` service #[derive(Default)] -pub struct OneRequest { +pub struct OneRequest { config: ServiceConfig, - _t: PhantomData, + _t: PhantomData<(T, P)>, } -impl OneRequest +impl OneRequest where T: AsyncRead + AsyncWrite, { @@ -178,15 +178,15 @@ where } } -impl NewService for OneRequest +impl NewService for OneRequest where T: AsyncRead + AsyncWrite, { - type Request = T; + type Request = Io; type Response = (Request, Framed); type Error = ParseError; type InitError = (); - type Service = OneRequestService; + type Service = OneRequestService; type Future = FutureResult; fn new_service(&self, _: &SrvConfig) -> Self::Future { @@ -199,16 +199,16 @@ where /// `Service` implementation for HTTP1 transport. Reads one request and returns /// request and framed object. -pub struct OneRequestService { +pub struct OneRequestService { config: ServiceConfig, - _t: PhantomData, + _t: PhantomData<(T, P)>, } -impl Service for OneRequestService +impl Service for OneRequestService where T: AsyncRead + AsyncWrite, { - type Request = T; + type Request = Io; type Response = (Request, Framed); type Error = ParseError; type Future = OneRequestServiceResponse; @@ -217,9 +217,12 @@ where Ok(Async::Ready(())) } - fn call(&mut self, req: T) -> Self::Future { + fn call(&mut self, req: Self::Request) -> Self::Future { OneRequestServiceResponse { - framed: Some(Framed::new(req, Codec::new(self.config.clone()))), + framed: Some(Framed::new( + req.into_parts().0, + Codec::new(self.config.clone()), + )), } } } diff --git a/src/h2/service.rs b/src/h2/service.rs index ce7c3b5dd..6ab37919c 100644 --- a/src/h2/service.rs +++ b/src/h2/service.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use std::{io, net}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_server_config::ServerConfig as SrvConfig; +use actix_server_config::{Io, ServerConfig as SrvConfig}; use actix_service::{IntoNewService, NewService, Service}; use actix_utils::cloneable::CloneableService; use bytes::Bytes; @@ -23,13 +23,13 @@ use crate::response::Response; use super::dispatcher::Dispatcher; /// `NewService` implementation for HTTP2 transport -pub struct H2Service { +pub struct H2Service { srv: S, cfg: ServiceConfig, - _t: PhantomData<(T, B)>, + _t: PhantomData<(T, P, B)>, } -impl H2Service +impl H2Service where S: NewService, S::Service: 'static, @@ -61,7 +61,7 @@ where } } -impl NewService for H2Service +impl NewService for H2Service where T: AsyncRead + AsyncWrite, S: NewService, @@ -70,12 +70,12 @@ where S::Response: Into>, B: MessageBody + 'static, { - type Request = T; + type Request = Io; type Response = (); type Error = DispatchError; type InitError = S::InitError; - type Service = H2ServiceHandler; - type Future = H2ServiceResponse; + type Service = H2ServiceHandler; + type Future = H2ServiceResponse; fn new_service(&self, cfg: &SrvConfig) -> Self::Future { H2ServiceResponse { @@ -87,13 +87,13 @@ where } #[doc(hidden)] -pub struct H2ServiceResponse, B> { +pub struct H2ServiceResponse, B> { fut: ::Future, cfg: Option, - _t: PhantomData<(T, B)>, + _t: PhantomData<(T, P, B)>, } -impl Future for H2ServiceResponse +impl Future for H2ServiceResponse where T: AsyncRead + AsyncWrite, S: NewService, @@ -102,7 +102,7 @@ where S::Error: Debug, B: MessageBody + 'static, { - type Item = H2ServiceHandler; + type Item = H2ServiceHandler; type Error = S::InitError; fn poll(&mut self) -> Poll { @@ -115,20 +115,20 @@ where } /// `Service` implementation for http/2 transport -pub struct H2ServiceHandler { +pub struct H2ServiceHandler { srv: CloneableService, cfg: ServiceConfig, - _t: PhantomData<(T, B)>, + _t: PhantomData<(T, P, B)>, } -impl H2ServiceHandler +impl H2ServiceHandler where S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody + 'static, { - fn new(cfg: ServiceConfig, srv: S) -> H2ServiceHandler { + fn new(cfg: ServiceConfig, srv: S) -> H2ServiceHandler { H2ServiceHandler { cfg, srv: CloneableService::new(srv), @@ -137,7 +137,7 @@ where } } -impl Service for H2ServiceHandler +impl Service for H2ServiceHandler where T: AsyncRead + AsyncWrite, S: Service + 'static, @@ -145,7 +145,7 @@ where S::Response: Into>, B: MessageBody + 'static, { - type Request = T; + type Request = Io; type Response = (); type Error = DispatchError; type Future = H2ServiceHandlerResponse; @@ -157,12 +157,12 @@ where }) } - fn call(&mut self, req: T) -> Self::Future { + fn call(&mut self, req: Self::Request) -> Self::Future { H2ServiceHandlerResponse { state: State::Handshake( Some(self.srv.clone()), Some(self.cfg.clone()), - server::handshake(req), + server::handshake(req.into_parts().0), ), } } diff --git a/src/service/service.rs b/src/service/service.rs index ac28c77a5..3ddf55739 100644 --- a/src/service/service.rs +++ b/src/service/service.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use std::{fmt, io}; use actix_codec::{AsyncRead, AsyncWrite, Framed, FramedParts}; -use actix_server_config::ServerConfig as SrvConfig; +use actix_server_config::{Io as ServerIo, Protocol, ServerConfig as SrvConfig}; use actix_service::{IntoNewService, NewService, Service}; use actix_utils::cloneable::CloneableService; use bytes::{Buf, BufMut, Bytes, BytesMut}; @@ -20,13 +20,27 @@ use crate::response::Response; use crate::{h1, h2::Dispatcher}; /// `NewService` HTTP1.1/HTTP2 transport implementation -pub struct HttpService { +pub struct HttpService { srv: S, cfg: ServiceConfig, - _t: PhantomData<(T, B)>, + _t: PhantomData<(T, P, B)>, } -impl HttpService +impl HttpService +where + S: NewService, + S::Service: 'static, + S::Error: Debug + 'static, + S::Response: Into>, + B: MessageBody + 'static, +{ + /// Create builder for `HttpService` instance. + pub fn build() -> HttpServiceBuilder { + HttpServiceBuilder::new() + } +} + +impl HttpService where S: NewService, S::Service: 'static, @@ -56,14 +70,9 @@ where _t: PhantomData, } } - - /// Create builder for `HttpService` instance. - pub fn build() -> HttpServiceBuilder { - HttpServiceBuilder::new() - } } -impl NewService for HttpService +impl NewService for HttpService where T: AsyncRead + AsyncWrite + 'static, S: NewService, @@ -72,12 +81,12 @@ where S::Response: Into>, B: MessageBody + 'static, { - type Request = T; + type Request = ServerIo; type Response = (); type Error = DispatchError; type InitError = S::InitError; - type Service = HttpServiceHandler; - type Future = HttpServiceResponse; + type Service = HttpServiceHandler; + type Future = HttpServiceResponse; fn new_service(&self, cfg: &SrvConfig) -> Self::Future { HttpServiceResponse { @@ -89,13 +98,13 @@ where } #[doc(hidden)] -pub struct HttpServiceResponse, B> { +pub struct HttpServiceResponse, B> { fut: ::Future, cfg: Option, - _t: PhantomData<(T, B)>, + _t: PhantomData<(T, P, B)>, } -impl Future for HttpServiceResponse +impl Future for HttpServiceResponse where T: AsyncRead + AsyncWrite, S: NewService, @@ -104,7 +113,7 @@ where S::Error: Debug, B: MessageBody + 'static, { - type Item = HttpServiceHandler; + type Item = HttpServiceHandler; type Error = S::InitError; fn poll(&mut self) -> Poll { @@ -117,20 +126,20 @@ where } /// `Service` implementation for http transport -pub struct HttpServiceHandler { +pub struct HttpServiceHandler { srv: CloneableService, cfg: ServiceConfig, - _t: PhantomData<(T, B)>, + _t: PhantomData<(T, P, B)>, } -impl HttpServiceHandler +impl HttpServiceHandler where S: Service + 'static, S::Error: Debug, S::Response: Into>, B: MessageBody + 'static, { - fn new(cfg: ServiceConfig, srv: S) -> HttpServiceHandler { + fn new(cfg: ServiceConfig, srv: S) -> HttpServiceHandler { HttpServiceHandler { cfg, srv: CloneableService::new(srv), @@ -139,7 +148,7 @@ where } } -impl Service for HttpServiceHandler +impl Service for HttpServiceHandler where T: AsyncRead + AsyncWrite + 'static, S: Service + 'static, @@ -147,7 +156,7 @@ where S::Response: Into>, B: MessageBody + 'static, { - type Request = T; + type Request = ServerIo; type Response = (); type Error = DispatchError; type Future = HttpServiceHandlerResponse; @@ -159,14 +168,37 @@ where }) } - fn call(&mut self, req: T) -> Self::Future { - HttpServiceHandlerResponse { - state: State::Unknown(Some(( - req, - BytesMut::with_capacity(14), - self.cfg.clone(), - self.srv.clone(), - ))), + fn call(&mut self, req: Self::Request) -> Self::Future { + let (io, params, proto) = req.into_parts(); + match proto { + Protocol::Http2 => { + let io = Io { + inner: io, + unread: None, + }; + HttpServiceHandlerResponse { + state: State::Handshake(Some(( + server::handshake(io), + self.cfg.clone(), + self.srv.clone(), + ))), + } + } + Protocol::Http10 | Protocol::Http11 => HttpServiceHandlerResponse { + state: State::H1(h1::Dispatcher::new( + io, + self.cfg.clone(), + self.srv.clone(), + )), + }, + _ => HttpServiceHandlerResponse { + state: State::Unknown(Some(( + io, + BytesMut::with_capacity(14), + self.cfg.clone(), + self.srv.clone(), + ))), + }, } } } diff --git a/tests/test_server.rs b/tests/test_server.rs index 7a28bca8a..3771d35c6 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -49,7 +49,7 @@ fn test_h1_2() { } #[cfg(feature = "ssl")] -fn ssl_acceptor() -> std::io::Result> { +fn ssl_acceptor() -> std::io::Result> { use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; // load ssl keys let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); diff --git a/tests/test_ws.rs b/tests/test_ws.rs index 4111ca3db..634b9acdd 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -2,7 +2,8 @@ use std::io; use actix_codec::Framed; use actix_http_test::TestServer; -use actix_service::NewService; +use actix_server::Io; +use actix_service::{fn_service, NewService}; use actix_utils::framed::IntoFramed; use actix_utils::stream::TakeItem; use bytes::{Bytes, BytesMut}; @@ -35,7 +36,8 @@ fn ws_service(req: ws::Frame) -> impl Future| Ok(io.into_parts().0)) + .and_then(IntoFramed::new(|| h1::Codec::new(ServiceConfig::default()))) .and_then(TakeItem::new().map_err(|_| ())) .and_then(|(req, framed): (_, Framed<_, _>)| { // validate request From eae48f9612d2391ead1963a2f49aabfd5b420aac Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 11 Mar 2019 15:26:05 -0700 Subject: [PATCH 269/427] use server backlog --- src/server.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/server.rs b/src/server.rs index 5d717817d..f80e5a0e5 100644 --- a/src/server.rs +++ b/src/server.rs @@ -83,12 +83,12 @@ where HttpServer { factory, host: None, - backlog: 2048, config: Arc::new(Mutex::new(Config { keep_alive: KeepAlive::Timeout(5), client_timeout: 5000, client_shutdown: 5000, })), + backlog: 1024, sockets: Vec::new(), builder: Some(ServerBuilder::default()), _t: PhantomData, @@ -114,8 +114,9 @@ where /// Generally set in the 64-2048 range. Default value is 2048. /// /// This method should be called before `bind()` method call. - pub fn backlog(mut self, num: i32) -> Self { - self.backlog = num; + pub fn backlog(mut self, backlog: i32) -> Self { + self.backlog = backlog; + self.builder = Some(self.builder.take().unwrap().backlog(backlog)); self } From e15e4f18fd8588c64fbf4096e40553de36278af1 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 11 Mar 2019 16:42:33 -0700 Subject: [PATCH 270/427] update tests --- src/client/h2proto.rs | 12 +- src/h1/dispatcher.rs | 36 ++-- src/h2/dispatcher.rs | 10 +- src/h2/mod.rs | 5 +- test-server/src/lib.rs | 5 + tests/test_server.rs | 411 ++++++++++++++++++++++++++++++++++------- 6 files changed, 383 insertions(+), 96 deletions(-) diff --git a/src/client/h2proto.rs b/src/client/h2proto.rs index 617c21b60..c05aeddbe 100644 --- a/src/client/h2proto.rs +++ b/src/client/h2proto.rs @@ -6,10 +6,11 @@ use futures::future::{err, Either}; use futures::{Async, Future, Poll}; use h2::{client::SendRequest, SendStream}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; -use http::{request::Request, HttpTryFrom, Version}; +use http::{request::Request, HttpTryFrom, Method, Version}; use crate::body::{BodyLength, MessageBody}; use crate::message::{Message, RequestHead, ResponseHead}; +use crate::payload::Payload; use super::connection::{ConnectionType, IoConnection}; use super::error::SendRequestError; @@ -28,6 +29,7 @@ where B: MessageBody, { trace!("Sending client request: {:?} {:?}", head, body.length()); + let head_req = head.method == Method::HEAD; let length = body.length(); let eof = match length { BodyLength::None | BodyLength::Empty | BodyLength::Sized(0) => true, @@ -99,18 +101,16 @@ where } } }) - .and_then(|resp| { + .and_then(move |resp| { let (parts, body) = resp.into_parts(); + let payload = if head_req { Payload::None } else { body.into() }; let mut head: Message = Message::new(); head.version = parts.version; head.status = parts.status; head.headers = parts.headers; - Ok(ClientResponse { - head, - payload: body.into(), - }) + Ok(ClientResponse { head, payload }) }) .from_err() } diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index a7eb96c3f..8543aa213 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -228,18 +228,21 @@ where } None => None, }, - State::ServiceCall(mut fut) => { - match fut.poll().map_err(|_| DispatchError::Service)? { - Async::Ready(res) => { - let (res, body) = res.into().replace_body(()); - Some(self.send_response(res, body)?) - } - Async::NotReady => { - self.state = State::ServiceCall(fut); - None - } + State::ServiceCall(mut fut) => match fut.poll() { + Ok(Async::Ready(res)) => { + let (res, body) = res.into().replace_body(()); + Some(self.send_response(res, body)?) } - } + Ok(Async::NotReady) => { + self.state = State::ServiceCall(fut); + None + } + Err(_e) => { + let res: Response = Response::InternalServerError().finish(); + let (res, body) = res.replace_body(()); + Some(self.send_response(res, body.into_body())?) + } + }, State::SendPayload(mut stream) => { loop { if !self.framed.is_write_buf_full() { @@ -289,12 +292,17 @@ where fn handle_request(&mut self, req: Request) -> Result, DispatchError> { let mut task = self.service.call(req); - match task.poll().map_err(|_| DispatchError::Service)? { - Async::Ready(res) => { + match task.poll() { + Ok(Async::Ready(res)) => { let (res, body) = res.into().replace_body(()); self.send_response(res, body) } - Async::NotReady => Ok(State::ServiceCall(task)), + Ok(Async::NotReady) => Ok(State::ServiceCall(task)), + Err(_e) => { + let res: Response = Response::InternalServerError().finish(); + let (res, body) = res.replace_body(()); + self.send_response(res, body.into_body()) + } } } diff --git a/src/h2/dispatcher.rs b/src/h2/dispatcher.rs index a3c731ebb..28465b509 100644 --- a/src/h2/dispatcher.rs +++ b/src/h2/dispatcher.rs @@ -107,9 +107,7 @@ where fn poll(&mut self) -> Poll { loop { match self.connection.poll()? { - Async::Ready(None) => { - self.flags.insert(Flags::DISCONNECTED); - } + Async::Ready(None) => return Ok(Async::Ready(())), Async::Ready(Some((req, res))) => { // update keep-alive expire if self.ka_timer.is_some() { @@ -255,7 +253,7 @@ where } } Ok(Async::NotReady) => Ok(Async::NotReady), - Err(e) => { + Err(_e) => { let res: Response = Response::InternalServerError().finish(); let (res, body) = res.replace_body(()); @@ -304,7 +302,9 @@ where } } else { match body.poll_next() { - Ok(Async::NotReady) => return Ok(Async::NotReady), + Ok(Async::NotReady) => { + return Ok(Async::NotReady); + } Ok(Async::Ready(None)) => { if let Err(e) = stream.send_data(Bytes::new(), true) { warn!("{:?}", e); diff --git a/src/h2/mod.rs b/src/h2/mod.rs index c5972123f..919317e05 100644 --- a/src/h2/mod.rs +++ b/src/h2/mod.rs @@ -40,7 +40,10 @@ impl Stream for Payload { } Ok(Async::Ready(None)) => Ok(Async::Ready(None)), Ok(Async::NotReady) => Ok(Async::NotReady), - Err(err) => Err(err.into()), + Err(err) => { + println!("======== {:?}", err); + Err(err.into()) + } } } } diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 6b70fbc10..d810d8915 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -167,6 +167,11 @@ impl TestServerRuntime { ClientRequest::get(self.url("/").as_str()) } + /// Create https `GET` request + pub fn sget(&self) -> ClientRequestBuilder { + ClientRequest::get(self.surl("/").as_str()) + } + /// Create `POST` request pub fn post(&self) -> ClientRequestBuilder { ClientRequest::post(self.url("/").as_str()) diff --git a/tests/test_server.rs b/tests/test_server.rs index 3771d35c6..49a943dbb 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -3,15 +3,16 @@ use std::time::Duration; use std::{net, thread}; use actix_http_test::TestServer; -use actix_service::NewService; +use actix_server_config::ServerConfig; +use actix_service::{fn_cfg_factory, NewService}; use bytes::Bytes; use futures::future::{self, ok, Future}; use futures::stream::once; use actix_http::body::Body; use actix_http::{ - body, client, http, Error, HttpMessage as HttpMessage2, HttpService, KeepAlive, - Request, Response, + body, client, error, http, http::header, Error, HttpMessage as HttpMessage2, + HttpService, KeepAlive, Request, Response, }; #[test] @@ -152,7 +153,7 @@ fn test_slow_request() { let srv = TestServer::new(|| { HttpService::build() .client_timeout(100) - .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) + .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) }); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); @@ -341,67 +342,66 @@ fn test_content_length() { } } -// TODO: fix -// #[test] -// fn test_h2_content_length() { -// use actix_http::http::{ -// header::{HeaderName, HeaderValue}, -// StatusCode, -// }; -// let openssl = ssl_acceptor().unwrap(); +#[test] +fn test_h2_content_length() { + use actix_http::http::{ + header::{HeaderName, HeaderValue}, + StatusCode, + }; + let openssl = ssl_acceptor().unwrap(); -// let mut srv = TestServer::new(move || { -// openssl -// .clone() -// .map_err(|e| println!("Openssl error: {}", e)) -// .and_then( -// HttpService::build() -// .h2(|req: Request| { -// let indx: usize = req.uri().path()[1..].parse().unwrap(); -// let statuses = [ -// StatusCode::NO_CONTENT, -// StatusCode::CONTINUE, -// StatusCode::SWITCHING_PROTOCOLS, -// StatusCode::PROCESSING, -// StatusCode::OK, -// StatusCode::NOT_FOUND, -// ]; -// future::ok::<_, ()>(Response::new(statuses[indx])) -// }) -// .map_err(|_| ()), -// ) -// }); + let mut srv = TestServer::new(move || { + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)) + .and_then( + HttpService::build() + .h2(|req: Request| { + let indx: usize = req.uri().path()[1..].parse().unwrap(); + let statuses = [ + StatusCode::NO_CONTENT, + StatusCode::CONTINUE, + StatusCode::SWITCHING_PROTOCOLS, + StatusCode::PROCESSING, + StatusCode::OK, + StatusCode::NOT_FOUND, + ]; + future::ok::<_, ()>(Response::new(statuses[indx])) + }) + .map_err(|_| ()), + ) + }); -// let header = HeaderName::from_static("content-length"); -// let value = HeaderValue::from_static("0"); + let header = HeaderName::from_static("content-length"); + let value = HeaderValue::from_static("0"); -// { -// for i in 0..4 { -// let req = client::ClientRequest::get(srv.surl(&format!("/{}", i))) -// .finish() -// .unwrap(); -// let response = srv.send_request(req).unwrap(); -// assert_eq!(response.headers().get(&header), None); + { + for i in 0..4 { + let req = client::ClientRequest::get(srv.surl(&format!("/{}", i))) + .finish() + .unwrap(); + let response = srv.send_request(req).unwrap(); + assert_eq!(response.headers().get(&header), None); -// let req = client::ClientRequest::head(srv.surl(&format!("/{}", i))) -// .finish() -// .unwrap(); -// let response = srv.send_request(req).unwrap(); -// assert_eq!(response.headers().get(&header), None); -// } + let req = client::ClientRequest::head(srv.surl(&format!("/{}", i))) + .finish() + .unwrap(); + let response = srv.send_request(req).unwrap(); + assert_eq!(response.headers().get(&header), None); + } -// for i in 4..6 { -// let req = client::ClientRequest::get(srv.surl(&format!("/{}", i))) -// .finish() -// .unwrap(); -// let response = srv.send_request(req).unwrap(); -// assert_eq!(response.headers().get(&header), Some(&value)); -// } -// } -// } + for i in 4..6 { + let req = client::ClientRequest::get(srv.surl(&format!("/{}", i))) + .finish() + .unwrap(); + let response = srv.send_request(req).unwrap(); + assert_eq!(response.headers().get(&header), Some(&value)); + } + } +} #[test] -fn test_headers() { +fn test_h1_headers() { let data = STR.repeat(10); let data2 = data.clone(); @@ -511,7 +511,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World"; #[test] -fn test_body() { +fn test_h1_body() { let mut srv = TestServer::new(|| { HttpService::build().h1(|_| future::ok::<_, ()>(Response::Ok().body(STR))) }); @@ -526,7 +526,30 @@ fn test_body() { } #[test] -fn test_head_empty() { +fn test_h2_body2() { + let openssl = ssl_acceptor().unwrap(); + let mut srv = TestServer::new(move || { + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)) + .and_then( + HttpService::build() + .h2(|_| future::ok::<_, ()>(Response::Ok().body(STR))) + .map_err(|_| ()), + ) + }); + + let req = srv.sget().finish().unwrap(); + let mut response = srv.send_request(req).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} + +#[test] +fn test_h1_head_empty() { let mut srv = TestServer::new(|| { HttpService::build().h1(|_| ok::<_, ()>(Response::Ok().body(STR))) }); @@ -549,7 +572,39 @@ fn test_head_empty() { } #[test] -fn test_head_binary() { +fn test_h2_head_empty() { + let openssl = ssl_acceptor().unwrap(); + let mut srv = TestServer::new(move || { + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)) + .and_then( + HttpService::build() + .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) + .map_err(|_| ()), + ) + }); + + let req = client::ClientRequest::head(srv.surl("/")).finish().unwrap(); + let mut response = srv.send_request(req).unwrap(); + assert!(response.status().is_success()); + assert_eq!(response.version(), http::Version::HTTP_2); + + { + let len = response + .headers() + .get(http::header::CONTENT_LENGTH) + .unwrap(); + assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); + } + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert!(bytes.is_empty()); +} + +#[test] +fn test_h1_head_binary() { let mut srv = TestServer::new(|| { HttpService::build().h1(|_| { ok::<_, ()>(Response::Ok().content_length(STR.len() as u64).body(STR)) @@ -574,7 +629,42 @@ fn test_head_binary() { } #[test] -fn test_head_binary2() { +fn test_h2_head_binary() { + let openssl = ssl_acceptor().unwrap(); + let mut srv = TestServer::new(move || { + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)) + .and_then( + HttpService::build() + .h2(|_| { + ok::<_, ()>( + Response::Ok().content_length(STR.len() as u64).body(STR), + ) + }) + .map_err(|_| ()), + ) + }); + + let req = client::ClientRequest::head(srv.surl("/")).finish().unwrap(); + let mut response = srv.send_request(req).unwrap(); + assert!(response.status().is_success()); + + { + let len = response + .headers() + .get(http::header::CONTENT_LENGTH) + .unwrap(); + assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); + } + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert!(bytes.is_empty()); +} + +#[test] +fn test_h1_head_binary2() { let mut srv = TestServer::new(|| { HttpService::build().h1(|_| ok::<_, ()>(Response::Ok().body(STR))) }); @@ -593,7 +683,34 @@ fn test_head_binary2() { } #[test] -fn test_body_length() { +fn test_h2_head_binary2() { + let openssl = ssl_acceptor().unwrap(); + let mut srv = TestServer::new(move || { + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)) + .and_then( + HttpService::build() + .h2(|_| ok::<_, ()>(Response::Ok().body(STR))) + .map_err(|_| ()), + ) + }); + + let req = client::ClientRequest::head(srv.surl("/")).finish().unwrap(); + let response = srv.send_request(req).unwrap(); + assert!(response.status().is_success()); + + { + let len = response + .headers() + .get(http::header::CONTENT_LENGTH) + .unwrap(); + assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); + } +} + +#[test] +fn test_h1_body_length() { let mut srv = TestServer::new(|| { HttpService::build().h1(|_| { let body = once(Ok(Bytes::from_static(STR.as_ref()))); @@ -614,17 +731,58 @@ fn test_body_length() { } #[test] -fn test_body_chunked_explicit() { +fn test_h2_body_length() { + let openssl = ssl_acceptor().unwrap(); + let mut srv = TestServer::new(move || { + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)) + .and_then( + HttpService::build() + .h2(|_| { + let body = once(Ok(Bytes::from_static(STR.as_ref()))); + ok::<_, ()>(Response::Ok().body(Body::from_message( + body::SizedStream::new(STR.len(), body), + ))) + }) + .map_err(|_| ()), + ) + }); + + let req = srv.sget().finish().unwrap(); + let mut response = srv.send_request(req).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} + +#[test] +fn test_h1_body_chunked_explicit() { let mut srv = TestServer::new(|| { HttpService::build().h1(|_| { let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>(Response::Ok().streaming(body)) + ok::<_, ()>( + Response::Ok() + .header(header::TRANSFER_ENCODING, "chunked") + .streaming(body), + ) }) }); let req = srv.get().finish().unwrap(); let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); + assert_eq!( + response + .headers() + .get(header::TRANSFER_ENCODING) + .unwrap() + .to_str() + .unwrap(), + "chunked" + ); // read response let bytes = srv.block_on(response.body()).unwrap(); @@ -634,7 +792,41 @@ fn test_body_chunked_explicit() { } #[test] -fn test_body_chunked_implicit() { +fn test_h2_body_chunked_explicit() { + let openssl = ssl_acceptor().unwrap(); + let mut srv = TestServer::new(move || { + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)) + .and_then( + HttpService::build() + .h2(|_| { + let body = + once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); + ok::<_, ()>( + Response::Ok() + .header(header::TRANSFER_ENCODING, "chunked") + .streaming(body), + ) + }) + .map_err(|_| ()), + ) + }); + + let req = srv.sget().finish().unwrap(); + let mut response = srv.send_request(req).unwrap(); + assert!(response.status().is_success()); + assert!(!response.headers().contains_key(header::TRANSFER_ENCODING)); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + + // decode + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} + +#[test] +fn test_h1_body_chunked_implicit() { let mut srv = TestServer::new(|| { HttpService::build().h1(|_| { let body = once::<_, Error>(Ok(Bytes::from_static(STR.as_ref()))); @@ -645,17 +837,23 @@ fn test_body_chunked_implicit() { let req = srv.get().finish().unwrap(); let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); + assert_eq!( + response + .headers() + .get(header::TRANSFER_ENCODING) + .unwrap() + .to_str() + .unwrap(), + "chunked" + ); // read response let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } -use actix_server_config::ServerConfig; -use actix_service::fn_cfg_factory; - #[test] -fn test_response_http_error_handling() { +fn test_h1_response_http_error_handling() { let mut srv = TestServer::new(|| { HttpService::build().h1(fn_cfg_factory(|_: &ServerConfig| { Ok::<_, ()>(|_| { @@ -677,3 +875,76 @@ fn test_response_http_error_handling() { let bytes = srv.block_on(response.body()).unwrap(); assert!(bytes.is_empty()); } + +#[test] +fn test_h2_response_http_error_handling() { + let openssl = ssl_acceptor().unwrap(); + + let mut srv = TestServer::new(move || { + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)) + .and_then( + HttpService::build() + .h2(fn_cfg_factory(|_: &ServerConfig| { + Ok::<_, ()>(|_| { + let broken_header = Bytes::from_static(b"\0\0\0"); + ok::<_, ()>( + Response::Ok() + .header(http::header::CONTENT_TYPE, broken_header) + .body(STR), + ) + }) + })) + .map_err(|_| ()), + ) + }); + + let req = srv.sget().finish().unwrap(); + let mut response = srv.send_request(req).unwrap(); + assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert!(bytes.is_empty()); +} + +#[test] +fn test_h1_service_error() { + let mut srv = TestServer::new(|| { + HttpService::build() + .h1(|_| Err::(error::ErrorBadRequest("error"))) + }); + + let req = srv.get().finish().unwrap(); + let mut response = srv.send_request(req).unwrap(); + assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert!(bytes.is_empty()); +} + +#[test] +fn test_h2_service_error() { + let openssl = ssl_acceptor().unwrap(); + + let mut srv = TestServer::new(move || { + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)) + .and_then( + HttpService::build() + .h2(|_| Err::(error::ErrorBadRequest("error"))) + .map_err(|_| ()), + ) + }); + + let req = srv.sget().finish().unwrap(); + let mut response = srv.send_request(req).unwrap(); + assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert!(bytes.is_empty()); +} From 409888fcd52a865549e71149bf8c28423cf1b0ea Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 11 Mar 2019 16:47:12 -0700 Subject: [PATCH 271/427] remove debug print, remove unused flags --- src/h2/dispatcher.rs | 9 --------- src/h2/mod.rs | 5 +---- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/h2/dispatcher.rs b/src/h2/dispatcher.rs index 28465b509..cbba34c0d 100644 --- a/src/h2/dispatcher.rs +++ b/src/h2/dispatcher.rs @@ -28,20 +28,12 @@ use crate::response::Response; const CHUNK_SIZE: usize = 16_384; -bitflags! { - struct Flags: u8 { - const DISCONNECTED = 0b0000_0001; - const SHUTDOWN = 0b0000_0010; - } -} - /// Dispatcher for HTTP/2 protocol pub struct Dispatcher< T: AsyncRead + AsyncWrite, S: Service + 'static, B: MessageBody, > { - flags: Flags, service: CloneableService, connection: Connection, config: ServiceConfig, @@ -86,7 +78,6 @@ where ka_expire, ka_timer, connection, - flags: Flags::empty(), _t: PhantomData, } } diff --git a/src/h2/mod.rs b/src/h2/mod.rs index 919317e05..c5972123f 100644 --- a/src/h2/mod.rs +++ b/src/h2/mod.rs @@ -40,10 +40,7 @@ impl Stream for Payload { } Ok(Async::Ready(None)) => Ok(Async::Ready(None)), Ok(Async::NotReady) => Ok(Async::NotReady), - Err(err) => { - println!("======== {:?}", err); - Err(err.into()) - } + Err(err) => Err(err.into()), } } } From 00d47acedc9cebdaa73f773a41991bd0a6b3b6af Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 11 Mar 2019 17:56:48 -0700 Subject: [PATCH 272/427] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a5a255672..467e67a9d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Actix http [![Build Status](https://travis-ci.org/actix/actix-http.svg?branch=master)](https://travis-ci.org/actix/actix-http) [![Build status](https://ci.appveyor.com/api/projects/status/i51p65u2bukstgwg/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-http-b1qsn/branch/master) [![codecov](https://codecov.io/gh/actix/actix-http/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-http) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# Actix http [![Build Status](https://travis-ci.org/actix/actix-http.svg?branch=master)](https://travis-ci.org/actix/actix-http) [![codecov](https://codecov.io/gh/actix/actix-http/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-http) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Actix http From a2c4639074baa0b1275d0b46b9560eafb88d0f28 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 11 Mar 2019 23:11:51 -0700 Subject: [PATCH 273/427] move blocking code to actix-rt --- Cargo.toml | 5 +-- src/blocking.rs | 93 ------------------------------------------------- src/error.rs | 2 -- src/lib.rs | 7 ++-- 4 files changed, 4 insertions(+), 103 deletions(-) delete mode 100644 src/blocking.rs diff --git a/Cargo.toml b/Cargo.toml index c64f4bc68..e24392941 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ actix-codec = "0.1.0" actix-service = "0.3.3" actix-utils = "0.3.3" actix-router = "0.1.0" -actix-rt = "0.2.0" +actix-rt = "0.2.1" actix-web-codegen = { path="actix-web-codegen" } actix-http = { git = "https://github.com/actix/actix-http.git" } @@ -78,16 +78,13 @@ encoding = "0.2" futures = "0.1" hashbrown = "0.1.8" log = "0.4" -lazy_static = "1.2" mime = "0.3" net2 = "0.2.33" -num_cpus = "1.10" parking_lot = "0.7" regex = "1.0" serde = "1.0" serde_json = "1.0" serde_urlencoded = "^0.5.3" -threadpool = "1.7" time = "0.1" url = { version="1.7", features=["query_encoding"] } diff --git a/src/blocking.rs b/src/blocking.rs deleted file mode 100644 index fc9cec299..000000000 --- a/src/blocking.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! Thread pool for blocking operations - -use std::fmt; - -use derive_more::Display; -use futures::sync::oneshot; -use futures::{Async, Future, Poll}; -use parking_lot::Mutex; -use threadpool::ThreadPool; - -use crate::ResponseError; - -/// Env variable for default cpu pool size -const ENV_CPU_POOL_VAR: &str = "ACTIX_CPU_POOL"; - -lazy_static::lazy_static! { - pub(crate) static ref DEFAULT_POOL: Mutex = { - let default = match std::env::var(ENV_CPU_POOL_VAR) { - Ok(val) => { - if let Ok(val) = val.parse() { - val - } else { - log::error!("Can not parse ACTIX_CPU_POOL value"); - num_cpus::get() * 5 - } - } - Err(_) => num_cpus::get() * 5, - }; - Mutex::new( - threadpool::Builder::new() - .thread_name("actix-web".to_owned()) - .num_threads(default) - .build(), - ) - }; -} - -thread_local! { - static POOL: ThreadPool = { - DEFAULT_POOL.lock().clone() - }; -} - -/// Blocking operation execution error -#[derive(Debug, Display)] -pub enum BlockingError { - #[display(fmt = "{:?}", _0)] - Error(E), - #[display(fmt = "Thread pool is gone")] - Canceled, -} - -impl ResponseError for BlockingError {} - -/// Execute blocking function on a thread pool, returns future that resolves -/// to result of the function execution. -pub fn run(f: F) -> CpuFuture -where - F: FnOnce() -> Result + Send + 'static, - I: Send + 'static, - E: Send + fmt::Debug + 'static, -{ - let (tx, rx) = oneshot::channel(); - POOL.with(|pool| { - pool.execute(move || { - if !tx.is_canceled() { - let _ = tx.send(f()); - } - }) - }); - - CpuFuture { rx } -} - -/// Blocking operation completion future. It resolves with results -/// of blocking function execution. -pub struct CpuFuture { - rx: oneshot::Receiver>, -} - -impl Future for CpuFuture { - type Item = I; - type Error = BlockingError; - - fn poll(&mut self) -> Poll { - let res = - futures::try_ready!(self.rx.poll().map_err(|_| BlockingError::Canceled)); - match res { - Ok(val) => Ok(Async::Ready(val)), - Err(err) => Err(BlockingError::Error(err)), - } - } -} diff --git a/src/error.rs b/src/error.rs index fd0ee998f..54ca74dc2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,8 +4,6 @@ pub use actix_http::error::*; use derive_more::{Display, From}; use url::ParseError as UrlParseError; -pub use crate::blocking::BlockingError; - /// Errors which can occur when attempting to generate resource uri. #[derive(Debug, PartialEq, Display, From)] pub enum UrlGenerationError { diff --git a/src/lib.rs b/src/lib.rs index 62f6399d6..0094818d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,6 @@ mod app; mod app_service; -mod blocking; mod config; pub mod error; mod extract; @@ -53,7 +52,6 @@ pub mod dev { //! ``` pub use crate::app::AppRouter; - pub use crate::blocking::CpuFuture; pub use crate::config::{AppConfig, ServiceConfig}; pub use crate::info::ConnectionInfo; pub use crate::rmap::ResourceMap; @@ -67,6 +65,7 @@ pub mod dev { Extensions, Payload, PayloadStream, RequestHead, ResponseHead, }; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; + pub use actix_rt::blocking::CpuFuture; pub use actix_server::Server; pub(crate) fn insert_slash(path: &str) -> String { @@ -80,12 +79,12 @@ pub mod dev { pub mod web { use actix_http::{http::Method, Response}; + use actix_rt::blocking::{self, CpuFuture}; use futures::IntoFuture; pub use actix_http::Response as HttpResponse; pub use bytes::{Bytes, BytesMut}; - use crate::blocking::CpuFuture; use crate::extract::FromRequest; use crate::handler::{AsyncFactory, Factory}; use crate::resource::Resource; @@ -258,6 +257,6 @@ pub mod web { I: Send + 'static, E: Send + std::fmt::Debug + 'static, { - crate::blocking::run(f) + blocking::run(f) } } From 7242d967014ae0266191d471210df52569cbc0b4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 11 Mar 2019 23:19:05 -0700 Subject: [PATCH 274/427] map BlockingError --- actix-files/src/lib.rs | 10 +++++----- src/error.rs | 21 +++++++++++++++++++++ src/lib.rs | 11 +++++------ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 07fc00631..b92400099 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -17,8 +17,8 @@ use v_htmlescape::escape as escape_html_entity; use actix_service::{boxed::BoxedNewService, NewService, Service}; use actix_web::dev::{ - CpuFuture, HttpServiceFactory, ResourceDef, ServiceConfig, ServiceFromRequest, - ServiceRequest, ServiceResponse, + HttpServiceFactory, ResourceDef, ServiceConfig, ServiceFromRequest, ServiceRequest, + ServiceResponse, }; use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder}; @@ -52,7 +52,7 @@ pub struct ChunkedReadFile { size: u64, offset: u64, file: Option, - fut: Option>, + fut: Option>>>, counter: u64, } @@ -89,7 +89,7 @@ impl Stream for ChunkedReadFile { Ok(Async::Ready(None)) } else { let mut file = self.file.take().expect("Use after completion"); - self.fut = Some(web::block(move || { + self.fut = Some(Box::new(web::block(move || { let max_bytes: usize; max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; let mut buf = Vec::with_capacity(max_bytes); @@ -100,7 +100,7 @@ impl Stream for ChunkedReadFile { return Err(io::ErrorKind::UnexpectedEof.into()); } Ok((file, Bytes::from(buf))) - })); + }))); self.poll() } } diff --git a/src/error.rs b/src/error.rs index 54ca74dc2..068407086 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,5 @@ //! Error and Result module +use std::fmt; pub use actix_http::error::*; use derive_more::{Display, From}; @@ -20,3 +21,23 @@ pub enum UrlGenerationError { /// `InternalServerError` for `UrlGeneratorError` impl ResponseError for UrlGenerationError {} + +/// Blocking operation execution error +#[derive(Debug, Display)] +pub enum BlockingError { + #[display(fmt = "{:?}", _0)] + Error(E), + #[display(fmt = "Thread pool is gone")] + Canceled, +} + +impl ResponseError for BlockingError {} + +impl From> for BlockingError { + fn from(err: actix_rt::blocking::BlockingError) -> Self { + match err { + actix_rt::blocking::BlockingError::Error(e) => BlockingError::Error(e), + actix_rt::blocking::BlockingError::Canceled => BlockingError::Canceled, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 0094818d3..d653fd1c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,7 +65,6 @@ pub mod dev { Extensions, Payload, PayloadStream, RequestHead, ResponseHead, }; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; - pub use actix_rt::blocking::CpuFuture; pub use actix_server::Server; pub(crate) fn insert_slash(path: &str) -> String { @@ -79,8 +78,8 @@ pub mod dev { pub mod web { use actix_http::{http::Method, Response}; - use actix_rt::blocking::{self, CpuFuture}; - use futures::IntoFuture; + use actix_rt::blocking; + use futures::{Future, IntoFuture}; pub use actix_http::Response as HttpResponse; pub use bytes::{Bytes, BytesMut}; @@ -92,7 +91,7 @@ pub mod web { use crate::route::Route; use crate::scope::Scope; - pub use crate::error::Error; + pub use crate::error::{BlockingError, Error}; pub use crate::extract::{Form, Json, Path, Payload, Query}; pub use crate::extract::{FormConfig, JsonConfig, PayloadConfig}; pub use crate::request::HttpRequest; @@ -251,12 +250,12 @@ pub mod web { /// Execute blocking function on a thread pool, returns future that resolves /// to result of the function execution. - pub fn block(f: F) -> CpuFuture + pub fn block(f: F) -> impl Future> where F: FnOnce() -> Result + Send + 'static, I: Send + 'static, E: Send + std::fmt::Debug + 'static, { - blocking::run(f) + blocking::run(f).from_err() } } From 402a40ab27562bbd0927f25a28753bff5996ba86 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 12 Mar 2019 16:55:16 -0700 Subject: [PATCH 275/427] update actix-server dep --- Cargo.toml | 7 ++++--- examples/framed_hello.rs | 3 ++- test-server/Cargo.toml | 6 +++--- tests/test_server.rs | 4 +++- tests/test_ws.rs | 3 ++- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ae81f1520..9c0f84c52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,11 +38,11 @@ ssl = ["openssl", "actix-connector/ssl"] fail = ["failure"] [dependencies] -actix-service = "0.3.3" +actix-service = "0.3.4" actix-codec = "0.1.1" actix-connector = "0.3.0" -actix-utils = "0.3.3" -actix-server-config = { git="https://github.com/actix/actix-net.git" } +actix-utils = "0.3.4" +actix-server-config = "0.1.0" base64 = "0.10" backtrace = "0.3" @@ -91,3 +91,4 @@ actix-http-test = { path="test-server", features=["ssl"] } env_logger = "0.6" serde_derive = "1.0" openssl = { version="0.10" } +tokio-tcp = "0.1" \ No newline at end of file diff --git a/examples/framed_hello.rs b/examples/framed_hello.rs index 74b0f7dfd..7d4c13d34 100644 --- a/examples/framed_hello.rs +++ b/examples/framed_hello.rs @@ -7,6 +7,7 @@ use actix_service::{fn_service, NewService}; use actix_utils::framed::IntoFramed; use actix_utils::stream::TakeItem; use futures::Future; +use tokio_tcp::TcpStream; fn main() -> io::Result<()> { env::set_var("RUST_LOG", "framed_hello=info"); @@ -14,7 +15,7 @@ fn main() -> io::Result<()> { Server::build() .bind("framed_hello", "127.0.0.1:8080", || { - fn_service(|io: Io<_>| Ok(io.into_parts().0)) + fn_service(|io: Io| Ok(io.into_parts().0)) .and_then(IntoFramed::new(|| h1::Codec::new(ServiceConfig::default()))) .and_then(TakeItem::new().map_err(|_| ())) .and_then(|(_req, _framed): (_, Framed<_, _>)| { diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 49f18f04e..2b4697736 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -33,12 +33,12 @@ ssl = ["openssl", "actix-http/ssl", "actix-server/ssl"] [dependencies] actix-codec = "0.1.1" -actix-rt = "0.2.0" +actix-rt = "0.2.1" actix-http = { path=".." } -actix-service = "0.3.3" +actix-service = "0.3.4" #actix-server = "0.3.0" actix-server = { git="https://github.com/actix/actix-net.git" } -actix-utils = "0.3.2" +actix-utils = "0.3.4" base64 = "0.10" bytes = "0.4" diff --git a/tests/test_server.rs b/tests/test_server.rs index 49a943dbb..8266bb9af 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -2,6 +2,7 @@ use std::io::{Read, Write}; use std::time::Duration; use std::{net, thread}; +use actix_codec::{AsyncRead, AsyncWrite}; use actix_http_test::TestServer; use actix_server_config::ServerConfig; use actix_service::{fn_cfg_factory, NewService}; @@ -50,7 +51,8 @@ fn test_h1_2() { } #[cfg(feature = "ssl")] -fn ssl_acceptor() -> std::io::Result> { +fn ssl_acceptor( +) -> std::io::Result> { use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; // load ssl keys let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); diff --git a/tests/test_ws.rs b/tests/test_ws.rs index 634b9acdd..d8942fb17 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -9,6 +9,7 @@ use actix_utils::stream::TakeItem; use bytes::{Bytes, BytesMut}; use futures::future::{ok, Either}; use futures::{Future, Sink, Stream}; +use tokio_tcp::TcpStream; use actix_http::{h1, ws, ResponseError, SendResponse, ServiceConfig}; @@ -36,7 +37,7 @@ fn ws_service(req: ws::Frame) -> impl Future| Ok(io.into_parts().0)) + fn_service(|io: Io| Ok(io.into_parts().0)) .and_then(IntoFramed::new(|| h1::Codec::new(ServiceConfig::default()))) .and_then(TakeItem::new().map_err(|_| ())) .and_then(|(req, framed): (_, Framed<_, _>)| { From f627d0105552ee44cd75bd799143107f4ae57e54 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 12 Mar 2019 17:04:08 -0700 Subject: [PATCH 276/427] update actix-server --- Cargo.toml | 3 +-- test-server/Cargo.toml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9c0f84c52..8e0f3a68a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,8 +83,7 @@ failure = { version = "0.1.5", optional = true } [dev-dependencies] actix-rt = "0.2.0" -#actix-server = { version = "0.3.0", features=["ssl"] } -actix-server = { git="https://github.com/actix/actix-net.git", features=["ssl"] } +actix-server = { version = "0.4.0", features=["ssl"] } actix-connector = { version = "0.3.0", features=["ssl"] } actix-http-test = { path="test-server", features=["ssl"] } diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 2b4697736..b7535f99c 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -36,8 +36,7 @@ actix-codec = "0.1.1" actix-rt = "0.2.1" actix-http = { path=".." } actix-service = "0.3.4" -#actix-server = "0.3.0" -actix-server = { git="https://github.com/actix/actix-net.git" } +actix-server = "0.4.0" actix-utils = "0.3.4" base64 = "0.10" From 28f01beaec78568f38ddaea3d39ff13085b59923 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 12 Mar 2019 17:06:08 -0700 Subject: [PATCH 277/427] update deps --- Cargo.toml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e24392941..87a54b6ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,15 +61,14 @@ ssl = ["openssl", "actix-server/ssl"] [dependencies] actix-codec = "0.1.0" -actix-service = "0.3.3" -actix-utils = "0.3.3" +actix-service = "0.3.4" +actix-utils = "0.3.4" actix-router = "0.1.0" actix-rt = "0.2.1" actix-web-codegen = { path="actix-web-codegen" } - actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-server = { git = "https://github.com/actix/actix-net.git" } -actix-server-config = { git = "https://github.com/actix/actix-net.git" } +actix-server = "0.4.0" +actix-server-config = "0.1.0" bytes = "0.4" cookie = { version="0.11", features=["percent-encode"] } From 86405cfe7a3e4784200f41fa7ec2f29f572d7819 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 12 Mar 2019 22:57:09 -0700 Subject: [PATCH 278/427] more tests --- src/responder.rs | 95 +++++++++++++++++++++++++++++++++++++--- src/server.rs | 4 +- src/test.rs | 13 +++++- tests/test_httpserver.rs | 75 +++++++++++++++++++++++++++++++ 4 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 tests/test_httpserver.rs diff --git a/src/responder.rs b/src/responder.rs index 6dce300a6..ace360c62 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -261,7 +261,8 @@ where type Future = Result; fn respond_to(self, _: &HttpRequest) -> Self::Future { - Err(self.into()) + let err: Error = self.into(); + Ok(err.into()) } } @@ -289,12 +290,13 @@ where #[cfg(test)] mod tests { use actix_service::Service; - use bytes::Bytes; + use bytes::{Bytes, BytesMut}; + use super::*; use crate::dev::{Body, ResponseBody}; - use crate::http::StatusCode; - use crate::test::{init_service, TestRequest}; - use crate::{web, App}; + use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode}; + use crate::test::{block_on, init_service, TestRequest}; + use crate::{error, web, App, HttpResponse}; #[test] fn test_option_responder() { @@ -319,4 +321,87 @@ mod tests { _ => panic!(), } } + + trait BodyTest { + fn bin_ref(&self) -> &[u8]; + fn body(&self) -> &Body; + } + + impl BodyTest for ResponseBody { + fn bin_ref(&self) -> &[u8] { + match self { + ResponseBody::Body(ref b) => match b { + Body::Bytes(ref bin) => &bin, + _ => panic!(), + }, + ResponseBody::Other(ref b) => match b { + Body::Bytes(ref bin) => &bin, + _ => panic!(), + }, + } + } + fn body(&self) -> &Body { + match self { + ResponseBody::Body(ref b) => b, + ResponseBody::Other(ref b) => b, + } + } + } + + #[test] + fn test_responder() { + let req = TestRequest::default().to_http_request(); + + let resp: HttpResponse = block_on(().respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(*resp.body().body(), Body::Empty); + + let resp: HttpResponse = block_on("test".respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.body().bin_ref(), b"test"); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + + let resp: HttpResponse = block_on(b"test".respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.body().bin_ref(), b"test"); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("application/octet-stream") + ); + + let resp: HttpResponse = block_on("test".to_string().respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.body().bin_ref(), b"test"); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + + let resp: HttpResponse = + block_on(Bytes::from_static(b"test").respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.body().bin_ref(), b"test"); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("application/octet-stream") + ); + + let resp: HttpResponse = + block_on(BytesMut::from(b"test".as_ref()).respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.body().bin_ref(), b"test"); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("application/octet-stream") + ); + + let resp: HttpResponse = + error::InternalError::new("err", StatusCode::BAD_REQUEST) + .respond_to(&req) + .unwrap(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } } diff --git a/src/server.rs b/src/server.rs index f80e5a0e5..9055be308 100644 --- a/src/server.rs +++ b/src/server.rs @@ -182,8 +182,8 @@ where /// Host name is used by application router aa a hostname for url /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. /// html#method.host) documentation for more information. - pub fn server_hostname(mut self, val: String) -> Self { - self.host = Some(val); + pub fn server_hostname>(mut self, val: T) -> Self { + self.host = Some(val.as_ref().to_owned()); self } diff --git a/src/test.rs b/src/test.rs index 03700b679..57a6d3961 100644 --- a/src/test.rs +++ b/src/test.rs @@ -12,7 +12,7 @@ use actix_server_config::ServerConfig; use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; use cookie::Cookie; -use futures::Future; +use futures::future::{lazy, Future}; use crate::config::{AppConfig, AppConfigInner}; use crate::rmap::ResourceMap; @@ -42,6 +42,17 @@ where RT.with(move |rt| rt.borrow_mut().block_on(f)) } +/// Runs the provided function, with runtime enabled. +/// +/// Note that this function is intended to be used only for testing purpose. +/// This function panics on nested call. +pub fn run_on(f: F) -> Result +where + F: Fn() -> Result, +{ + RT.with(move |rt| rt.borrow_mut().block_on(lazy(|| f()))) +} + /// This method accepts application builder instance, and constructs /// service. /// diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs new file mode 100644 index 000000000..d2bc07ac1 --- /dev/null +++ b/tests/test_httpserver.rs @@ -0,0 +1,75 @@ +use net2::TcpBuilder; +use std::sync::mpsc; +use std::{net, thread, time::Duration}; + +use actix_http::{client, Response}; + +use actix_web::{test, web, App, HttpServer}; + +fn unused_addr() -> net::SocketAddr { + let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); + let socket = TcpBuilder::new_v4().unwrap(); + socket.bind(&addr).unwrap(); + socket.reuse_address(true).unwrap(); + let tcp = socket.to_tcp_listener().unwrap(); + tcp.local_addr().unwrap() +} + +#[test] +#[cfg(unix)] +fn test_start() { + let addr = unused_addr(); + let (tx, rx) = mpsc::channel(); + + thread::spawn(move || { + let sys = actix_rt::System::new("test"); + + let srv = HttpServer::new(|| { + App::new().service( + web::resource("/").route(web::to(|| Response::Ok().body("test"))), + ) + }) + .workers(1) + .backlog(1) + .maxconn(10) + .maxconnrate(10) + .keep_alive(10) + .client_timeout(5000) + .client_shutdown(0) + .server_hostname("localhost") + .system_exit() + .disable_signals() + .bind(format!("{}", addr)) + .unwrap() + .start(); + + let _ = tx.send((srv, actix_rt::System::current())); + let _ = sys.run(); + }); + let (srv, sys) = rx.recv().unwrap(); + + let mut connector = test::run_on(|| { + Ok::<_, ()>( + client::Connector::default() + .timeout(Duration::from_millis(100)) + .service(), + ) + }) + .unwrap(); + let host = format!("http://{}", addr); + + let response = test::block_on( + client::ClientRequest::get(host.clone()) + .finish() + .unwrap() + .send(&mut connector), + ) + .unwrap(); + assert!(response.status().is_success()); + + // stop + let _ = srv.stop(false); + + thread::sleep(Duration::from_millis(100)); + let _ = sys.stop(); +} From 17ecdd63d23f1b6ee424f67a0ad0852d9125f1af Mon Sep 17 00:00:00 2001 From: Luca Bruno Date: Wed, 13 Mar 2019 15:20:18 +0100 Subject: [PATCH 279/427] httpresponse: add constructor for HttpResponseBuilder (#697) --- src/httpresponse.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/httpresponse.rs b/src/httpresponse.rs index 226c847f3..48f7b4c2c 100644 --- a/src/httpresponse.rs +++ b/src/httpresponse.rs @@ -48,10 +48,10 @@ impl HttpResponse { self.0.as_mut() } - /// Create http response builder with specific status. + /// Create a new HTTP response builder with specific status. #[inline] pub fn build(status: StatusCode) -> HttpResponseBuilder { - HttpResponsePool::get(status) + HttpResponseBuilder::new(status) } /// Create http response builder @@ -346,6 +346,12 @@ pub struct HttpResponseBuilder { } impl HttpResponseBuilder { + /// Create a new HTTP response builder with specific status. + #[inline] + pub fn new(status: StatusCode) -> HttpResponseBuilder { + HttpResponsePool::get(status) + } + /// Set HTTP status code of this response. #[inline] pub fn status(&mut self, status: StatusCode) -> &mut Self { @@ -1177,6 +1183,14 @@ mod tests { assert_eq!((v.name(), v.value()), ("cookie3", "val300")); } + #[test] + fn test_builder_new() { + let resp = HttpResponseBuilder::new(StatusCode::BAD_REQUEST) + .finish(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + + #[test] fn test_basic_builder() { let resp = HttpResponse::Ok() From 1941aa021798563b516d788bc60fe633b7f47944 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 13 Mar 2019 14:41:40 -0700 Subject: [PATCH 280/427] use actix-connect crate --- Cargo.toml | 6 +- examples/client.rs | 2 +- src/client/connect.rs | 23 ++-- src/client/connector.rs | 255 ++++++++++++++++++--------------------- src/client/error.rs | 53 +++++--- src/client/h1proto.rs | 4 +- src/client/mod.rs | 2 +- src/client/pool.rs | 42 +++---- src/client/request.rs | 27 +++-- src/ws/client/error.rs | 4 +- src/ws/client/mod.rs | 2 +- src/ws/client/service.rs | 40 +++--- src/ws/mod.rs | 2 +- test-server/src/lib.rs | 108 +++++++---------- tests/test_client.rs | 6 +- tests/test_server.rs | 4 +- 16 files changed, 280 insertions(+), 300 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8e0f3a68a..ee9d28ac7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ path = "src/lib.rs" default = ["fail"] # openssl -ssl = ["openssl", "actix-connector/ssl"] +ssl = ["openssl", "actix-connect/ssl"] # failure integration. it is on by default, it will be off in future versions # actix itself does not use failure anymore @@ -40,7 +40,8 @@ fail = ["failure"] [dependencies] actix-service = "0.3.4" actix-codec = "0.1.1" -actix-connector = "0.3.0" +#actix-connector = "0.3.0" +actix-connect = { path="../actix-net/actix-connect" } actix-utils = "0.3.4" actix-server-config = "0.1.0" @@ -71,6 +72,7 @@ sha1 = "0.6" slab = "0.4" serde_urlencoded = "0.5.3" time = "0.1" +tokio-tcp = "0.1.3" tokio-timer = "0.2" tokio-current-thread = "0.1" trust-dns-resolver = { version="0.11.0-alpha.2", default-features = false } diff --git a/examples/client.rs b/examples/client.rs index 06b708e20..7f5f8c91a 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -8,7 +8,7 @@ fn main() -> Result<(), Error> { env_logger::init(); System::new("test").block_on(lazy(|| { - let mut connector = client::Connector::default().service(); + let mut connector = client::Connector::new().service(); client::ClientRequest::get("https://www.rust-lang.org/") // <- Create request builder .header("User-Agent", "Actix-web") diff --git a/src/client/connect.rs b/src/client/connect.rs index f4112cfaa..43be57703 100644 --- a/src/client/connect.rs +++ b/src/client/connect.rs @@ -1,8 +1,7 @@ -use actix_connector::{RequestHost, RequestPort}; use http::uri::Uri; -use http::{Error as HttpError, HttpTryFrom}; +use http::HttpTryFrom; -use super::error::{ConnectorError, InvalidUrlKind}; +use super::error::InvalidUrl; use super::pool::Key; #[derive(Debug)] @@ -19,7 +18,7 @@ impl Connect { } /// Construct `Uri` instance and create `Connect` message. - pub fn try_from(uri: U) -> Result + pub fn try_from(uri: U) -> Result where Uri: HttpTryFrom, { @@ -40,30 +39,26 @@ impl Connect { self.uri.authority_part().unwrap().clone().into() } - pub(crate) fn validate(&self) -> Result<(), ConnectorError> { + pub(crate) fn validate(&self) -> Result<(), InvalidUrl> { if self.uri.host().is_none() { - Err(ConnectorError::InvalidUrl(InvalidUrlKind::MissingHost)) + Err(InvalidUrl::MissingHost) } else if self.uri.scheme_part().is_none() { - Err(ConnectorError::InvalidUrl(InvalidUrlKind::MissingScheme)) + Err(InvalidUrl::MissingScheme) } else if let Some(scheme) = self.uri.scheme_part() { match scheme.as_str() { "http" | "ws" | "https" | "wss" => Ok(()), - _ => Err(ConnectorError::InvalidUrl(InvalidUrlKind::UnknownScheme)), + _ => Err(InvalidUrl::UnknownScheme), } } else { Ok(()) } } -} -impl RequestHost for Connect { - fn host(&self) -> &str { + pub(crate) fn host(&self) -> &str { &self.uri.host().unwrap() } -} -impl RequestPort for Connect { - fn port(&self) -> u16 { + pub(crate) fn port(&self) -> u16 { if let Some(port) = self.uri.port() { port } else if let Some(scheme) = self.uri.scheme_part() { diff --git a/src/client/connector.rs b/src/client/connector.rs index ccb5dbce5..1579cd5ef 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -1,14 +1,16 @@ +use std::fmt; +use std::marker::PhantomData; use std::time::Duration; use actix_codec::{AsyncRead, AsyncWrite}; -use actix_connector::{Resolver, TcpConnector}; -use actix_service::{Service, ServiceExt}; +use actix_connect::{default_connector, Stream}; +use actix_service::{apply_fn, Service, ServiceExt}; use actix_utils::timeout::{TimeoutError, TimeoutService}; -use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; +use tokio_tcp::TcpStream; use super::connect::Connect; use super::connection::Connection; -use super::error::ConnectorError; +use super::error::ConnectError; use super::pool::{ConnectionPool, Protocol}; #[cfg(feature = "ssl")] @@ -19,20 +21,28 @@ type SslConnector = (); /// Http client connector builde instance. /// `Connector` type uses builder-like pattern for connector service construction. -pub struct Connector { - resolver: Resolver, +pub struct Connector { + connector: T, timeout: Duration, conn_lifetime: Duration, conn_keep_alive: Duration, disconnect_timeout: Duration, limit: usize, #[allow(dead_code)] - connector: SslConnector, + ssl: SslConnector, + _t: PhantomData, } -impl Default for Connector { - fn default() -> Connector { - let connector = { +impl Connector<(), ()> { + pub fn new() -> Connector< + impl Service< + Request = actix_connect::Connect, + Response = Stream, + Error = actix_connect::ConnectError, + > + Clone, + TcpStream, + > { + let ssl = { #[cfg(feature = "ssl")] { use log::error; @@ -49,30 +59,51 @@ impl Default for Connector { }; Connector { - connector, - resolver: Resolver::default(), + ssl, + connector: default_connector(), timeout: Duration::from_secs(1), conn_lifetime: Duration::from_secs(75), conn_keep_alive: Duration::from_secs(15), disconnect_timeout: Duration::from_millis(3000), limit: 100, + _t: PhantomData, } } } -impl Connector { - /// Use custom resolver. - pub fn resolver(mut self, resolver: Resolver) -> Self { - self.resolver = resolver;; - self - } - - /// Use custom resolver configuration. - pub fn resolver_config(mut self, cfg: ResolverConfig, opts: ResolverOpts) -> Self { - self.resolver = Resolver::new(cfg, opts); - self +impl Connector { + /// Use custom connector. + pub fn connector(self, connector: T1) -> Connector + where + U1: AsyncRead + AsyncWrite + fmt::Debug, + T1: Service< + Request = actix_connect::Connect, + Response = Stream, + Error = actix_connect::ConnectError, + > + Clone, + { + Connector { + connector, + timeout: self.timeout, + conn_lifetime: self.conn_lifetime, + conn_keep_alive: self.conn_keep_alive, + disconnect_timeout: self.disconnect_timeout, + limit: self.limit, + ssl: self.ssl, + _t: PhantomData, + } } +} +impl Connector +where + U: AsyncRead + AsyncWrite + fmt::Debug + 'static, + T: Service< + Request = actix_connect::Connect, + Response = Stream, + Error = actix_connect::ConnectError, + > + Clone, +{ /// Connection timeout, i.e. max time to connect to remote host including dns name resolution. /// Set to 1 second by default. pub fn timeout(mut self, timeout: Duration) -> Self { @@ -83,7 +114,7 @@ impl Connector { #[cfg(feature = "ssl")] /// Use custom `SslConnector` instance. pub fn ssl(mut self, connector: SslConnector) -> Self { - self.connector = connector; + self.ssl = connector; self } @@ -133,24 +164,18 @@ impl Connector { /// Finish configuration process and create connector service. pub fn service( self, - ) -> impl Service< - Request = Connect, - Response = impl Connection, - Error = ConnectorError, - > + Clone { + ) -> impl Service + + Clone { #[cfg(not(feature = "ssl"))] { let connector = TimeoutService::new( self.timeout, - self.resolver.map_err(ConnectorError::from).and_then( - TcpConnector::default() - .from_err() - .map(|(msg, io)| (msg, io, Protocol::Http1)), - ), + self.connector + .map(|stream| (stream.into_parts().0, Protocol::Http1)), ) .map_err(|e| match e { TimeoutError::Service(e) => e, - TimeoutError::Timeout => ConnectorError::Timeout, + TimeoutError::Timeout => ConnectError::Timeout, }); connect_impl::InnerConnector { @@ -166,48 +191,49 @@ impl Connector { #[cfg(feature = "ssl")] { const H2: &[u8] = b"h2"; - use actix_connector::ssl::OpensslConnector; + use actix_connect::ssl::OpensslConnector; let ssl_service = TimeoutService::new( self.timeout, - self.resolver - .clone() - .map_err(ConnectorError::from) - .and_then(TcpConnector::default().from_err()) - .and_then( - OpensslConnector::service(self.connector) - .map_err(ConnectorError::from) - .map(|(msg, io)| { - let h2 = io - .get_ref() - .ssl() - .selected_alpn_protocol() - .map(|protos| protos.windows(2).any(|w| w == H2)) - .unwrap_or(false); - if h2 { - (msg, io, Protocol::Http2) - } else { - (msg, io, Protocol::Http1) - } - }), - ), - ) - .map_err(|e| match e { - TimeoutError::Service(e) => e, - TimeoutError::Timeout => ConnectorError::Timeout, - }); - - let tcp_service = TimeoutService::new( - self.timeout, - self.resolver.map_err(ConnectorError::from).and_then( - TcpConnector::default() - .from_err() - .map(|(msg, io)| (msg, io, Protocol::Http1)), + apply_fn(self.connector.clone(), |msg: Connect, srv| { + srv.call(actix_connect::Connect::new(msg.host(), msg.port())) + }) + .map_err(ConnectError::from) + .and_then( + OpensslConnector::service(self.ssl) + .map_err(ConnectError::from) + .map(|stream| { + let sock = stream.into_parts().0; + let h2 = sock + .get_ref() + .ssl() + .selected_alpn_protocol() + .map(|protos| protos.windows(2).any(|w| w == H2)) + .unwrap_or(false); + if h2 { + (sock, Protocol::Http2) + } else { + (sock, Protocol::Http1) + } + }), ), ) .map_err(|e| match e { TimeoutError::Service(e) => e, - TimeoutError::Timeout => ConnectorError::Timeout, + TimeoutError::Timeout => ConnectError::Timeout, + }); + + let tcp_service = TimeoutService::new( + self.timeout, + apply_fn(self.connector.clone(), |msg: Connect, srv| { + srv.call(actix_connect::Connect::new(msg.host(), msg.port())) + }) + .map_err(ConnectError::from) + .map(|stream| (stream.into_parts().0, Protocol::Http1)), + ) + .map_err(|e| match e { + TimeoutError::Service(e) => e, + TimeoutError::Timeout => ConnectError::Timeout, }); connect_impl::InnerConnector { @@ -253,11 +279,8 @@ mod connect_impl { impl Clone for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service< - Request = Connect, - Response = (Connect, Io, Protocol), - Error = ConnectorError, - > + Clone, + T: Service + + Clone, { fn clone(&self) -> Self { InnerConnector { @@ -269,11 +292,7 @@ mod connect_impl { impl Service for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service< - Request = Connect, - Response = (Connect, Io, Protocol), - Error = ConnectorError, - >, + T: Service, { type Request = Connect; type Response = IoConnection; @@ -289,7 +308,7 @@ mod connect_impl { fn call(&mut self, req: Connect) -> Self::Future { if req.is_secure() { - Either::B(err(ConnectorError::SslIsNotSupported)) + Either::B(err(ConnectError::SslIsNotSupported)) } else if let Err(e) = req.validate() { Either::B(err(e)) } else { @@ -303,7 +322,7 @@ mod connect_impl { mod connect_impl { use std::marker::PhantomData; - use futures::future::{err, Either, FutureResult}; + use futures::future::{Either, FutureResult}; use futures::{Async, Future, Poll}; use super::*; @@ -313,16 +332,8 @@ mod connect_impl { where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service< - Request = Connect, - Response = (Connect, Io1, Protocol), - Error = ConnectorError, - >, - T2: Service< - Request = Connect, - Response = (Connect, Io2, Protocol), - Error = ConnectorError, - >, + T1: Service, + T2: Service, { pub(crate) tcp_pool: ConnectionPool, pub(crate) ssl_pool: ConnectionPool, @@ -332,16 +343,10 @@ mod connect_impl { where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service< - Request = Connect, - Response = (Connect, Io1, Protocol), - Error = ConnectorError, - > + Clone, - T2: Service< - Request = Connect, - Response = (Connect, Io2, Protocol), - Error = ConnectorError, - > + Clone, + T1: Service + + Clone, + T2: Service + + Clone, { fn clone(&self) -> Self { InnerConnector { @@ -355,20 +360,12 @@ mod connect_impl { where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service< - Request = Connect, - Response = (Connect, Io1, Protocol), - Error = ConnectorError, - >, - T2: Service< - Request = Connect, - Response = (Connect, Io2, Protocol), - Error = ConnectorError, - >, + T1: Service, + T2: Service, { type Request = Connect; type Response = EitherConnection; - type Error = ConnectorError; + type Error = ConnectError; type Future = Either< FutureResult, Either< @@ -382,9 +379,7 @@ mod connect_impl { } fn call(&mut self, req: Connect) -> Self::Future { - if let Err(e) = req.validate() { - Either::A(err(e)) - } else if req.is_secure() { + if req.is_secure() { Either::B(Either::B(InnerConnectorResponseB { fut: self.ssl_pool.call(req), _t: PhantomData, @@ -401,11 +396,7 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseA where Io1: AsyncRead + AsyncWrite + 'static, - T: Service< - Request = Connect, - Response = (Connect, Io1, Protocol), - Error = ConnectorError, - >, + T: Service, { fut: as Service>::Future, _t: PhantomData, @@ -413,16 +404,12 @@ mod connect_impl { impl Future for InnerConnectorResponseA where - T: Service< - Request = Connect, - Response = (Connect, Io1, Protocol), - Error = ConnectorError, - >, + T: Service, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { type Item = EitherConnection; - type Error = ConnectorError; + type Error = ConnectError; fn poll(&mut self) -> Poll { match self.fut.poll()? { @@ -435,11 +422,7 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseB where Io2: AsyncRead + AsyncWrite + 'static, - T: Service< - Request = Connect, - Response = (Connect, Io2, Protocol), - Error = ConnectorError, - >, + T: Service, { fut: as Service>::Future, _t: PhantomData, @@ -447,16 +430,12 @@ mod connect_impl { impl Future for InnerConnectorResponseB where - T: Service< - Request = Connect, - Response = (Connect, Io2, Protocol), - Error = ConnectorError, - >, + T: Service, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { type Item = EitherConnection; - type Error = ConnectorError; + type Error = ConnectError; fn poll(&mut self) -> Poll { match self.fut.poll()? { diff --git a/src/client/error.rs b/src/client/error.rs index 6c91ff976..4fce904f1 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -11,11 +11,7 @@ use crate::response::Response; /// A set of errors that can occur while connecting to an HTTP host #[derive(Debug, Display, From)] -pub enum ConnectorError { - /// Invalid URL - #[display(fmt = "Invalid URL")] - InvalidUrl(InvalidUrlKind), - +pub enum ConnectError { /// SSL feature is not enabled #[display(fmt = "SSL is not supported")] SslIsNotSupported, @@ -45,24 +41,30 @@ pub enum ConnectorError { #[display(fmt = "Internal error: connector has been disconnected")] Disconnected, + /// Unresolved host name + #[display(fmt = "Connector received `Connect` method with unresolved host")] + Unresolverd, + /// Connection io error #[display(fmt = "{}", _0)] Io(io::Error), } -#[derive(Debug, Display)] -pub enum InvalidUrlKind { - #[display(fmt = "Missing url scheme")] - MissingScheme, - #[display(fmt = "Unknown url scheme")] - UnknownScheme, - #[display(fmt = "Missing host name")] - MissingHost, +impl From for ConnectError { + fn from(err: actix_connect::ConnectError) -> ConnectError { + match err { + actix_connect::ConnectError::Resolver(e) => ConnectError::Resolver(e), + actix_connect::ConnectError::NoRecords => ConnectError::NoRecords, + actix_connect::ConnectError::InvalidInput => panic!(), + actix_connect::ConnectError::Unresolverd => ConnectError::Unresolverd, + actix_connect::ConnectError::Io(e) => ConnectError::Io(e), + } + } } #[cfg(feature = "ssl")] -impl From> for ConnectorError { - fn from(err: HandshakeError) -> ConnectorError { +impl From> for ConnectError { + fn from(err: HandshakeError) -> ConnectError { match err { HandshakeError::SetupFailure(stack) => SslError::from(stack).into(), HandshakeError::Failure(stream) => stream.into_error().into(), @@ -71,12 +73,27 @@ impl From> for ConnectorError { } } +#[derive(Debug, Display, From)] +pub enum InvalidUrl { + #[display(fmt = "Missing url scheme")] + MissingScheme, + #[display(fmt = "Unknown url scheme")] + UnknownScheme, + #[display(fmt = "Missing host name")] + MissingHost, + #[display(fmt = "Url parse error: {}", _0)] + HttpError(http::Error), +} + /// A set of errors that can occur during request sending and response reading #[derive(Debug, Display, From)] pub enum SendRequestError { + /// Invalid URL + #[display(fmt = "Invalid URL: {}", _0)] + Url(InvalidUrl), /// Failed to connect to host #[display(fmt = "Failed to connect to host: {}", _0)] - Connector(ConnectorError), + Connect(ConnectError), /// Error sending request Send(io::Error), /// Error parsing response @@ -92,10 +109,10 @@ pub enum SendRequestError { impl ResponseError for SendRequestError { fn error_response(&self) -> Response { match *self { - SendRequestError::Connector(ConnectorError::Timeout) => { + SendRequestError::Connect(ConnectError::Timeout) => { Response::GatewayTimeout() } - SendRequestError::Connector(_) => Response::BadGateway(), + SendRequestError::Connect(_) => Response::BadGateway(), _ => Response::InternalServerError(), } .into() diff --git a/src/client/h1proto.rs b/src/client/h1proto.rs index 3329fcfec..34521cc2f 100644 --- a/src/client/h1proto.rs +++ b/src/client/h1proto.rs @@ -6,7 +6,7 @@ use futures::future::{err, ok, Either}; use futures::{Async, Future, Poll, Sink, Stream}; use super::connection::{ConnectionLifetime, ConnectionType, IoConnection}; -use super::error::{ConnectorError, SendRequestError}; +use super::error::{ConnectError, SendRequestError}; use super::pool::Acquired; use super::response::ClientResponse; use crate::body::{BodyLength, MessageBody}; @@ -62,7 +62,7 @@ where } ok(res) } else { - err(ConnectorError::Disconnected.into()) + err(ConnectError::Disconnected.into()) } }) }) diff --git a/src/client/mod.rs b/src/client/mod.rs index 8d041827f..0bff97e49 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -12,6 +12,6 @@ mod response; pub use self::connect::Connect; pub use self::connection::Connection; pub use self::connector::Connector; -pub use self::error::{ConnectorError, InvalidUrlKind, SendRequestError}; +pub use self::error::{ConnectError, InvalidUrl, SendRequestError}; pub use self::request::{ClientRequest, ClientRequestBuilder}; pub use self::response::ClientResponse; diff --git a/src/client/pool.rs b/src/client/pool.rs index 188980cb3..214b7a382 100644 --- a/src/client/pool.rs +++ b/src/client/pool.rs @@ -20,7 +20,7 @@ use tokio_timer::{sleep, Delay}; use super::connect::Connect; use super::connection::{ConnectionType, IoConnection}; -use super::error::ConnectorError; +use super::error::ConnectError; #[derive(Clone, Copy, PartialEq)] pub enum Protocol { @@ -48,11 +48,7 @@ pub(crate) struct ConnectionPool( impl ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service< - Request = Connect, - Response = (Connect, Io, Protocol), - Error = ConnectorError, - >, + T: Service, { pub(crate) fn new( connector: T, @@ -91,17 +87,13 @@ where impl Service for ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service< - Request = Connect, - Response = (Connect, Io, Protocol), - Error = ConnectorError, - >, + T: Service, { type Request = Connect; type Response = IoConnection; - type Error = ConnectorError; + type Error = ConnectError; type Future = Either< - FutureResult, ConnectorError>, + FutureResult, Either, OpenConnection>, >; @@ -151,7 +143,7 @@ where { key: Key, token: usize, - rx: oneshot::Receiver, ConnectorError>>, + rx: oneshot::Receiver, ConnectError>>, inner: Option>>>, } @@ -173,7 +165,7 @@ where Io: AsyncRead + AsyncWrite, { type Item = IoConnection; - type Error = ConnectorError; + type Error = ConnectError; fn poll(&mut self) -> Poll { match self.rx.poll() { @@ -187,7 +179,7 @@ where Ok(Async::NotReady) => Ok(Async::NotReady), Err(_) => { let _ = self.inner.take(); - Err(ConnectorError::Disconnected) + Err(ConnectError::Disconnected) } } } @@ -206,7 +198,7 @@ where impl OpenConnection where - F: Future, + F: Future, Io: AsyncRead + AsyncWrite + 'static, { fn new(key: Key, inner: Rc>>, fut: F) -> Self { @@ -234,11 +226,11 @@ where impl Future for OpenConnection where - F: Future, + F: Future, Io: AsyncRead + AsyncWrite, { type Item = IoConnection; - type Error = ConnectorError; + type Error = ConnectError; fn poll(&mut self) -> Poll { if let Some(ref mut h2) = self.h2 { @@ -258,7 +250,7 @@ where match self.fut.poll() { Err(err) => Err(err), - Ok(Async::Ready((_, io, proto))) => { + Ok(Async::Ready((io, proto))) => { let _ = self.inner.take(); if proto == Protocol::Http1 { Ok(Async::Ready(IoConnection::new( @@ -289,7 +281,7 @@ where // impl OpenWaitingConnection // where -// F: Future + 'static, +// F: Future + 'static, // Io: AsyncRead + AsyncWrite + 'static, // { // fn spawn( @@ -323,7 +315,7 @@ where // impl Future for OpenWaitingConnection // where -// F: Future, +// F: Future, // Io: AsyncRead + AsyncWrite, // { // type Item = (); @@ -402,7 +394,7 @@ pub(crate) struct Inner { available: HashMap>>, waiters: Slab<( Connect, - oneshot::Sender, ConnectorError>>, + oneshot::Sender, ConnectError>>, )>, waiters_queue: IndexSet<(Key, usize)>, task: AtomicTask, @@ -444,7 +436,7 @@ where &mut self, connect: Connect, ) -> ( - oneshot::Receiver, ConnectorError>>, + oneshot::Receiver, ConnectError>>, usize, ) { let (tx, rx) = oneshot::channel(); @@ -534,7 +526,7 @@ where // impl Future for ConnectorPoolSupport // where // Io: AsyncRead + AsyncWrite + 'static, -// T: Service, +// T: Service, // T::Future: 'static, // { // type Item = (); diff --git a/src/client/request.rs b/src/client/request.rs index efae5b94e..199e13b93 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -5,6 +5,7 @@ use std::io::Write; use actix_service::Service; use bytes::{BufMut, Bytes, BytesMut}; use cookie::{Cookie, CookieJar}; +use futures::future::{err, Either}; use futures::{Future, Stream}; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use serde::Serialize; @@ -21,7 +22,7 @@ use crate::message::{ConnectionType, Head, RequestHead}; use super::connection::Connection; use super::response::ClientResponse; -use super::{Connect, ConnectorError, SendRequestError}; +use super::{Connect, ConnectError, SendRequestError}; /// An HTTP Client Request /// @@ -32,7 +33,8 @@ use super::{Connect, ConnectorError, SendRequestError}; /// /// fn main() { /// System::new("test").block_on(lazy(|| { -/// let mut connector = client::Connector::default().service(); +/// let mut connector = client::Connector::new().service(); +/// /// client::ClientRequest::get("http://www.rust-lang.org") // <- Create request builder /// .header("User-Agent", "Actix-web") /// .finish().unwrap() @@ -178,17 +180,24 @@ where ) -> impl Future where B: 'static, - T: Service, + T: Service, I: Connection, { let Self { head, body } = self; - connector - // connect to the host - .call(Connect::new(head.uri.clone())) - .from_err() - // send request - .and_then(move |connection| connection.send_request(head, body)) + let connect = Connect::new(head.uri.clone()); + if let Err(e) = connect.validate() { + Either::A(err(e.into())) + } else { + Either::B( + connector + // connect to the host + .call(connect) + .from_err() + // send request + .and_then(move |connection| connection.send_request(head, body)), + ) + } } } diff --git a/src/ws/client/error.rs b/src/ws/client/error.rs index 1eccb0b95..ae1e39967 100644 --- a/src/ws/client/error.rs +++ b/src/ws/client/error.rs @@ -1,7 +1,7 @@ //! Http client request use std::io; -use actix_connector::ConnectorError; +use actix_connect::ConnectError; use derive_more::{Display, From}; use http::{header::HeaderValue, Error as HttpError, StatusCode}; @@ -43,7 +43,7 @@ pub enum ClientError { Protocol(ProtocolError), /// Connect error #[display(fmt = "Connector error: {:?}", _0)] - Connect(ConnectorError), + Connect(ConnectError), /// IO Error #[display(fmt = "{}", _0)] Io(io::Error), diff --git a/src/ws/client/mod.rs b/src/ws/client/mod.rs index 12c7229b9..0dbf081c6 100644 --- a/src/ws/client/mod.rs +++ b/src/ws/client/mod.rs @@ -4,7 +4,7 @@ mod service; pub use self::connect::Connect; pub use self::error::ClientError; -pub use self::service::{Client, DefaultClient}; +pub use self::service::Client; #[derive(PartialEq, Hash, Debug, Clone, Copy)] pub(crate) enum Protocol { diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index 586873d19..e3781e15f 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -2,8 +2,8 @@ use std::marker::PhantomData; use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_connector::{Connect as TcpConnect, ConnectorError, DefaultConnector}; -use actix_service::Service; +use actix_connect::{default_connector, Connect as TcpConnect, ConnectError}; +use actix_service::{apply_fn, Service}; use base64; use futures::future::{err, Either, FutureResult}; use futures::{try_ready, Async, Future, Poll, Sink, Stream}; @@ -20,21 +20,29 @@ use crate::ws::Codec; use super::{ClientError, Connect, Protocol}; -/// Default client, uses default connector. -pub type DefaultClient = Client; - /// WebSocket's client -pub struct Client -where - T: Service, - T::Response: AsyncRead + AsyncWrite, -{ +pub struct Client { connector: T, } +impl Client<()> { + /// Create client with default connector. + pub fn default() -> Client< + impl Service< + Request = TcpConnect, + Response = impl AsyncRead + AsyncWrite, + Error = ConnectError, + > + Clone, + > { + Client::new(apply_fn(default_connector(), |msg: TcpConnect, srv| { + srv.call(msg).map(|stream| stream.into_parts().0) + })) + } +} + impl Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite, { /// Create new websocket's client factory @@ -43,15 +51,9 @@ where } } -impl Default for Client { - fn default() -> Self { - Client::new(DefaultConnector::default()) - } -} - impl Clone for Client where - T: Service + Clone, + T: Service + Clone, T::Response: AsyncRead + AsyncWrite, { fn clone(&self) -> Self { @@ -63,7 +65,7 @@ where impl Service for Client where - T: Service, + T: Service, T::Response: AsyncRead + AsyncWrite + 'static, T::Future: 'static, { diff --git a/src/ws/mod.rs b/src/ws/mod.rs index a8de59dd4..8f9bc83ba 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -21,7 +21,7 @@ mod proto; mod service; mod transport; -pub use self::client::{Client, ClientError, Connect, DefaultClient}; +pub use self::client::{Client, ClientError, Connect}; pub use self::codec::{Codec, Frame, Message}; pub use self::frame::Parser; pub use self::proto::{CloseCode, CloseReason, OpCode}; diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index d810d8915..3bb5feffb 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -1,18 +1,17 @@ //! Various helpers for Actix applications to use during testing. use std::sync::mpsc; -use std::{net, thread}; +use std::{net, thread, time}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_http::body::MessageBody; use actix_http::client::{ - ClientRequest, ClientRequestBuilder, ClientResponse, Connect, Connection, Connector, - ConnectorError, SendRequestError, + ClientRequest, ClientRequestBuilder, ClientResponse, Connect, ConnectError, + Connection, Connector, SendRequestError, }; use actix_http::ws; use actix_rt::{Runtime, System}; use actix_server::{Server, StreamServiceFactory}; use actix_service::Service; - use futures::future::{lazy, Future}; use http::Method; use net2::TcpBuilder; @@ -44,21 +43,15 @@ use net2::TcpBuilder; /// ``` pub struct TestServer; -/// -pub struct TestServerRuntime { +/// Test server controller +pub struct TestServerRuntime { addr: net::SocketAddr, - conn: T, rt: Runtime, } impl TestServer { /// Start new test server with application factory - pub fn new( - factory: F, - ) -> TestServerRuntime< - impl Service - + Clone, - > { + pub fn new(factory: F) -> TestServerRuntime { let (tx, rx) = mpsc::channel(); // run server in separate thread @@ -79,35 +72,9 @@ impl TestServer { let (system, addr) = rx.recv().unwrap(); System::set_current(system); - - let mut rt = Runtime::new().unwrap(); - let conn = rt - .block_on(lazy(|| Ok::<_, ()>(TestServer::new_connector()))) - .unwrap(); - - TestServerRuntime { addr, conn, rt } - } - - fn new_connector( - ) -> impl Service< - Request = Connect, - Response = impl Connection, - Error = ConnectorError, - > + Clone { - #[cfg(feature = "ssl")] - { - use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; - - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - let _ = builder - .set_alpn_protos(b"\x02h2\x08http/1.1") - .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); - Connector::default().ssl(builder.build()).service() - } - #[cfg(not(feature = "ssl"))] - { - Connector::default().service() + TestServerRuntime { + addr, + rt: Runtime::new().unwrap(), } } @@ -122,7 +89,7 @@ impl TestServer { } } -impl TestServerRuntime { +impl TestServerRuntime { /// Execute future on current core pub fn block_on(&mut self, fut: F) -> Result where @@ -131,12 +98,12 @@ impl TestServerRuntime { self.rt.block_on(fut) } - /// Execute future on current core - pub fn execute(&mut self, fut: F) -> Result + /// Execute function on current core + pub fn execute(&mut self, fut: F) -> R where - F: Future, + F: FnOnce() -> R, { - self.rt.block_on(fut) + self.rt.block_on(lazy(|| Ok::<_, ()>(fut()))).unwrap() } /// Construct test server url @@ -190,17 +157,37 @@ impl TestServerRuntime { .take() } - /// Http connector - pub fn connector(&mut self) -> &mut T { - &mut self.conn + fn new_connector( + ) -> impl Service + + Clone { + #[cfg(feature = "ssl")] + { + use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; + + let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); + builder.set_verify(SslVerifyMode::NONE); + let _ = builder + .set_alpn_protos(b"\x02h2\x08http/1.1") + .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); + Connector::new() + .timeout(time::Duration::from_millis(500)) + .ssl(builder.build()) + .service() + } + #[cfg(not(feature = "ssl"))] + { + Connector::new() + .timeout(time::Duration::from_millis(500)) + .service() + } } /// Http connector - pub fn new_connector(&mut self) -> T - where - T: Clone, - { - self.conn.clone() + pub fn connector( + &mut self, + ) -> impl Service + + Clone { + self.execute(|| TestServerRuntime::new_connector()) } /// Stop http server @@ -209,11 +196,7 @@ impl TestServerRuntime { } } -impl TestServerRuntime -where - T: Service + Clone, - T::Response: Connection, -{ +impl TestServerRuntime { /// Connect to websocket server at a given path pub fn ws_at( &mut self, @@ -236,11 +219,12 @@ where &mut self, req: ClientRequest, ) -> Result { - self.rt.block_on(req.send(&mut self.conn)) + let mut conn = self.connector(); + self.rt.block_on(req.send(&mut conn)) } } -impl Drop for TestServerRuntime { +impl Drop for TestServerRuntime { fn drop(&mut self) { self.stop() } diff --git a/tests/test_client.rs b/tests/test_client.rs index 782e487ca..90e1a4f4a 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -36,7 +36,7 @@ fn test_h1_v2() { .finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) .map(|_| ()) }); - let mut connector = srv.new_connector(); + let mut connector = srv.connector(); let request = srv.get().finish().unwrap(); let response = srv.block_on(request.send(&mut connector)).unwrap(); @@ -70,7 +70,7 @@ fn test_connection_close() { .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .map(|_| ()) }); - let mut connector = srv.new_connector(); + let mut connector = srv.connector(); let request = srv.get().close().finish().unwrap(); let response = srv.block_on(request.send(&mut connector)).unwrap(); @@ -90,7 +90,7 @@ fn test_with_query_parameter() { }) .map(|_| ()) }); - let mut connector = srv.new_connector(); + let mut connector = srv.connector(); let request = client::ClientRequest::get(srv.url("/?qp=5")) .finish() diff --git a/tests/test_server.rs b/tests/test_server.rs index 8266bb9af..98f740941 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -432,7 +432,7 @@ fn test_h1_headers() { future::ok::<_, ()>(builder.body(data.clone())) }) }); - let mut connector = srv.new_connector(); + let mut connector = srv.connector(); let req = srv.get().finish().unwrap(); @@ -479,7 +479,7 @@ fn test_h2_headers() { future::ok::<_, ()>(builder.body(data.clone())) }).map_err(|_| ())) }); - let mut connector = srv.new_connector(); + let mut connector = srv.connector(); let req = client::ClientRequest::get(srv.surl("/")).finish().unwrap(); let mut response = srv.block_on(req.send(&mut connector)).unwrap(); From 033a8d890cc276ee4ebdbbbd614aa2c20d085c49 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 13 Mar 2019 15:57:33 -0700 Subject: [PATCH 281/427] update actix connect --- Cargo.toml | 3 +-- src/client/connect.rs | 7 +++++-- src/client/connector.rs | 32 ++++++++++++++++---------------- src/ws/client/service.rs | 16 ++++++++-------- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ee9d28ac7..11f532c89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,8 +40,7 @@ fail = ["failure"] [dependencies] actix-service = "0.3.4" actix-codec = "0.1.1" -#actix-connector = "0.3.0" -actix-connect = { path="../actix-net/actix-connect" } +actix-connect = { git = "https://github.com/actix/actix-net.git" } actix-utils = "0.3.4" actix-server-config = "0.1.0" diff --git a/src/client/connect.rs b/src/client/connect.rs index 43be57703..93626b0a7 100644 --- a/src/client/connect.rs +++ b/src/client/connect.rs @@ -1,3 +1,4 @@ +use actix_connect::Address; use http::uri::Uri; use http::HttpTryFrom; @@ -53,12 +54,14 @@ impl Connect { Ok(()) } } +} - pub(crate) fn host(&self) -> &str { +impl Address for Connect { + fn host(&self) -> &str { &self.uri.host().unwrap() } - pub(crate) fn port(&self) -> u16 { + fn port(&self) -> u16 { if let Some(port) = self.uri.port() { port } else if let Some(scheme) = self.uri.scheme_part() { diff --git a/src/client/connector.rs b/src/client/connector.rs index 1579cd5ef..aaa88abc0 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -3,7 +3,9 @@ use std::marker::PhantomData; use std::time::Duration; use actix_codec::{AsyncRead, AsyncWrite}; -use actix_connect::{default_connector, Stream}; +use actix_connect::{ + default_connector, Connect as TcpConnect, Connection as TcpConnection, +}; use actix_service::{apply_fn, Service, ServiceExt}; use actix_utils::timeout::{TimeoutError, TimeoutService}; use tokio_tcp::TcpStream; @@ -36,8 +38,8 @@ pub struct Connector { impl Connector<(), ()> { pub fn new() -> Connector< impl Service< - Request = actix_connect::Connect, - Response = Stream, + Request = TcpConnect, + Response = TcpConnection, Error = actix_connect::ConnectError, > + Clone, TcpStream, @@ -77,8 +79,8 @@ impl Connector { where U1: AsyncRead + AsyncWrite + fmt::Debug, T1: Service< - Request = actix_connect::Connect, - Response = Stream, + Request = TcpConnect, + Response = TcpConnection, Error = actix_connect::ConnectError, > + Clone, { @@ -99,8 +101,8 @@ impl Connector where U: AsyncRead + AsyncWrite + fmt::Debug + 'static, T: Service< - Request = actix_connect::Connect, - Response = Stream, + Request = TcpConnect, + Response = TcpConnection, Error = actix_connect::ConnectError, > + Clone, { @@ -170,8 +172,10 @@ where { let connector = TimeoutService::new( self.timeout, - self.connector - .map(|stream| (stream.into_parts().0, Protocol::Http1)), + apply_fn(self.connector, |msg: Connect, srv| { + srv.call(actix_connect::Connect::with_request(msg)) + }) + .map(|stream| (stream.into_parts().0, Protocol::Http1)), ) .map_err(|e| match e { TimeoutError::Service(e) => e, @@ -196,7 +200,7 @@ where let ssl_service = TimeoutService::new( self.timeout, apply_fn(self.connector.clone(), |msg: Connect, srv| { - srv.call(actix_connect::Connect::new(msg.host(), msg.port())) + srv.call(actix_connect::Connect::with_request(msg)) }) .map_err(ConnectError::from) .and_then( @@ -226,7 +230,7 @@ where let tcp_service = TimeoutService::new( self.timeout, apply_fn(self.connector.clone(), |msg: Connect, srv| { - srv.call(actix_connect::Connect::new(msg.host(), msg.port())) + srv.call(actix_connect::Connect::with_request(msg)) }) .map_err(ConnectError::from) .map(|stream| (stream.into_parts().0, Protocol::Http1)), @@ -267,11 +271,7 @@ mod connect_impl { pub(crate) struct InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service< - Request = Connect, - Response = (Connect, Io, Protocol), - Error = ConnectorError, - >, + T: Service, { pub(crate) tcp_pool: ConnectionPool, } diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index e3781e15f..7be30993b 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_connect::{default_connector, Connect as TcpConnect, ConnectError}; +use actix_connect::{default_connector, Address, Connect as TcpConnect, ConnectError}; use actix_service::{apply_fn, Service}; use base64; use futures::future::{err, Either, FutureResult}; @@ -29,12 +29,12 @@ impl Client<()> { /// Create client with default connector. pub fn default() -> Client< impl Service< - Request = TcpConnect, + Request = TcpConnect<(String, u16)>, Response = impl AsyncRead + AsyncWrite, Error = ConnectError, > + Clone, > { - Client::new(apply_fn(default_connector(), |msg: TcpConnect, srv| { + Client::new(apply_fn(default_connector(), |msg: TcpConnect<_>, srv| { srv.call(msg).map(|stream| stream.into_parts().0) })) } @@ -42,7 +42,7 @@ impl Client<()> { impl Client where - T: Service, + T: Service, Error = ConnectError>, T::Response: AsyncRead + AsyncWrite, { /// Create new websocket's client factory @@ -53,7 +53,7 @@ where impl Clone for Client where - T: Service + Clone, + T: Service, Error = ConnectError> + Clone, T::Response: AsyncRead + AsyncWrite, { fn clone(&self) -> Self { @@ -65,7 +65,7 @@ where impl Service for Client where - T: Service, + T: Service, Error = ConnectError>, T::Response: AsyncRead + AsyncWrite + 'static, T::Future: 'static, { @@ -130,8 +130,8 @@ where ); // prep connection - let connect = TcpConnect::new( - request.uri().host().unwrap(), + let connect = TcpConnect::from_string( + request.uri().host().unwrap().to_string(), request.uri().port().unwrap_or_else(|| proto.port()), ); From 3a24a75d137fa41ba9db8b5b099b1b76c92b40d8 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 13 Mar 2019 16:56:11 -0700 Subject: [PATCH 282/427] update dep --- src/client/connector.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/connector.rs b/src/client/connector.rs index aaa88abc0..b8b583a9d 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -173,7 +173,7 @@ where let connector = TimeoutService::new( self.timeout, apply_fn(self.connector, |msg: Connect, srv| { - srv.call(actix_connect::Connect::with_request(msg)) + srv.call(actix_connect::Connect::with(msg)) }) .map(|stream| (stream.into_parts().0, Protocol::Http1)), ) @@ -200,7 +200,7 @@ where let ssl_service = TimeoutService::new( self.timeout, apply_fn(self.connector.clone(), |msg: Connect, srv| { - srv.call(actix_connect::Connect::with_request(msg)) + srv.call(actix_connect::Connect::with(msg)) }) .map_err(ConnectError::from) .and_then( @@ -230,7 +230,7 @@ where let tcp_service = TimeoutService::new( self.timeout, apply_fn(self.connector.clone(), |msg: Connect, srv| { - srv.call(actix_connect::Connect::with_request(msg)) + srv.call(actix_connect::Connect::with(msg)) }) .map_err(ConnectError::from) .map(|stream| (stream.into_parts().0, Protocol::Http1)), From d2c755bb47907f35e5883507b0a56cb23e4f4bf5 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 13 Mar 2019 22:57:28 -0700 Subject: [PATCH 283/427] update client connector --- src/client/connect.rs | 7 ++++--- src/client/connector.rs | 6 +++--- src/ws/client/service.rs | 14 ++++++-------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/client/connect.rs b/src/client/connect.rs index 93626b0a7..82e5e45c0 100644 --- a/src/client/connect.rs +++ b/src/client/connect.rs @@ -61,8 +61,8 @@ impl Address for Connect { &self.uri.host().unwrap() } - fn port(&self) -> u16 { - if let Some(port) = self.uri.port() { + fn port(&self) -> Option { + let port = if let Some(port) = self.uri.port() { port } else if let Some(scheme) = self.uri.scheme_part() { match scheme.as_str() { @@ -72,6 +72,7 @@ impl Address for Connect { } } else { 80 - } + }; + Some(port) } } diff --git a/src/client/connector.rs b/src/client/connector.rs index b8b583a9d..c764b93c4 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -173,7 +173,7 @@ where let connector = TimeoutService::new( self.timeout, apply_fn(self.connector, |msg: Connect, srv| { - srv.call(actix_connect::Connect::with(msg)) + srv.call(actix_connect::Connect::new(msg)) }) .map(|stream| (stream.into_parts().0, Protocol::Http1)), ) @@ -200,7 +200,7 @@ where let ssl_service = TimeoutService::new( self.timeout, apply_fn(self.connector.clone(), |msg: Connect, srv| { - srv.call(actix_connect::Connect::with(msg)) + srv.call(actix_connect::Connect::new(msg)) }) .map_err(ConnectError::from) .and_then( @@ -230,7 +230,7 @@ where let tcp_service = TimeoutService::new( self.timeout, apply_fn(self.connector.clone(), |msg: Connect, srv| { - srv.call(actix_connect::Connect::with(msg)) + srv.call(actix_connect::Connect::new(msg)) }) .map_err(ConnectError::from) .map(|stream| (stream.into_parts().0, Protocol::Http1)), diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index 7be30993b..1aa391249 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -29,7 +29,7 @@ impl Client<()> { /// Create client with default connector. pub fn default() -> Client< impl Service< - Request = TcpConnect<(String, u16)>, + Request = TcpConnect, Response = impl AsyncRead + AsyncWrite, Error = ConnectError, > + Clone, @@ -42,7 +42,7 @@ impl Client<()> { impl Client where - T: Service, Error = ConnectError>, + T: Service, Error = ConnectError>, T::Response: AsyncRead + AsyncWrite, { /// Create new websocket's client factory @@ -53,7 +53,7 @@ where impl Clone for Client where - T: Service, Error = ConnectError> + Clone, + T: Service, Error = ConnectError> + Clone, T::Response: AsyncRead + AsyncWrite, { fn clone(&self) -> Self { @@ -65,7 +65,7 @@ where impl Service for Client where - T: Service, Error = ConnectError>, + T: Service, Error = ConnectError>, T::Response: AsyncRead + AsyncWrite + 'static, T::Future: 'static, { @@ -130,10 +130,8 @@ where ); // prep connection - let connect = TcpConnect::from_string( - request.uri().host().unwrap().to_string(), - request.uri().port().unwrap_or_else(|| proto.port()), - ); + let connect = TcpConnect::new(request.uri().host().unwrap().to_string()) + .set_port(request.uri().port().unwrap_or_else(|| proto.port())); let fut = Box::new( self.connector From bf8262196f2d05f053ebcac774d2578c86b815af Mon Sep 17 00:00:00 2001 From: Jannik Keye Date: Thu, 14 Mar 2019 09:36:10 +0100 Subject: [PATCH 284/427] feat: enable use of patch as request method (#718) --- CHANGES.md | 2 ++ src/client/mod.rs | 7 +++++++ src/client/request.rs | 7 +++++++ src/resource.rs | 6 ++++++ src/test.rs | 5 +++++ tests/test_server.rs | 8 ++++++++ 6 files changed, 35 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 76f3465ef..57333613b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ * Add client HTTP Authentication methods `.basic_auth()` and `.bearer_auth()`. #540 +* Add support for PATCH HTTP method + ### Fixed * Ignored the `If-Modified-Since` if `If-None-Match` is specified. #680 diff --git a/src/client/mod.rs b/src/client/mod.rs index 5321e4b05..8c15fae4a 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -105,6 +105,13 @@ pub fn post>(uri: U) -> ClientRequestBuilder { builder } +/// Create request builder for `PATCH` requests +pub fn patch>(uri: U) -> ClientRequestBuilder { + let mut builder = ClientRequest::build(); + builder.method(Method::PATCH).uri(uri); + builder +} + /// Create request builder for `PUT` requests pub fn put>(uri: U) -> ClientRequestBuilder { let mut builder = ClientRequest::build(); diff --git a/src/client/request.rs b/src/client/request.rs index 89789933c..bf5145df4 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -112,6 +112,13 @@ impl ClientRequest { builder } + /// Create request builder for `PATCH` request + pub fn patch>(uri: U) -> ClientRequestBuilder { + let mut builder = ClientRequest::build(); + builder.method(Method::PATCH).uri(uri); + builder + } + /// Create request builder for `PUT` request pub fn put>(uri: U) -> ClientRequestBuilder { let mut builder = ClientRequest::build(); diff --git a/src/resource.rs b/src/resource.rs index d884dd447..78aea07cc 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -107,6 +107,12 @@ impl Resource { self.routes.last_mut().unwrap().filter(pred::Post()) } + /// Register a new `PATCH` route. + pub fn patch(&mut self) -> &mut Route { + self.routes.push(Route::default()); + self.routes.last_mut().unwrap().filter(pred::Patch()) + } + /// Register a new `PUT` route. pub fn put(&mut self) -> &mut Route { self.routes.push(Route::default()); diff --git a/src/test.rs b/src/test.rs index 1d86db9ff..584c02a58 100644 --- a/src/test.rs +++ b/src/test.rs @@ -239,6 +239,11 @@ impl TestServer { ClientRequest::post(self.url("/").as_str()) } + /// Create `PATCH` request + pub fn patch(&self) -> ClientRequestBuilder { + ClientRequest::patch(self.url("/").as_str()) + } + /// Create `HEAD` request pub fn head(&self) -> ClientRequestBuilder { ClientRequest::head(self.url("/").as_str()) diff --git a/tests/test_server.rs b/tests/test_server.rs index f3c9bf9dd..68482bb12 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1398,3 +1398,11 @@ fn test_content_length() { assert_eq!(response.headers().get(&header), Some(&value)); } } + +#[test] +fn test_patch_method() { + let mut srv = test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok())); + let req = srv.patch().finish().unwrap(); + let response = srv.execute(req.send()).unwrap(); + assert!(response.status().is_success()); +} \ No newline at end of file From b8bfd29d2c5e9b5a9ce10e27a1e1898d54e40444 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 14 Mar 2019 11:52:52 -0700 Subject: [PATCH 285/427] use Uri as client connect message --- Cargo.toml | 2 +- src/client/connect.rs | 78 ---------------------- src/client/connector.rs | 136 +++++++++++++++++++-------------------- src/client/mod.rs | 2 - src/client/pool.rs | 132 ++++--------------------------------- src/client/request.rs | 37 +++++++---- src/ws/client/service.rs | 4 +- test-server/src/lib.rs | 14 ++-- 8 files changed, 112 insertions(+), 293 deletions(-) delete mode 100644 src/client/connect.rs diff --git a/Cargo.toml b/Cargo.toml index 11f532c89..3b9a84984 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ encoding = "0.2" futures = "0.1" hashbrown = "0.1.8" h2 = "0.1.16" -http = "0.1.8" +http = "0.1.16" httparse = "1.3" indexmap = "1.0" lazy_static = "1.0" diff --git a/src/client/connect.rs b/src/client/connect.rs deleted file mode 100644 index 82e5e45c0..000000000 --- a/src/client/connect.rs +++ /dev/null @@ -1,78 +0,0 @@ -use actix_connect::Address; -use http::uri::Uri; -use http::HttpTryFrom; - -use super::error::InvalidUrl; -use super::pool::Key; - -#[derive(Debug)] -/// `Connect` type represents a message that can be sent to -/// `Connector` with a connection request. -pub struct Connect { - pub(crate) uri: Uri, -} - -impl Connect { - /// Create `Connect` message for specified `Uri` - pub fn new(uri: Uri) -> Connect { - Connect { uri } - } - - /// Construct `Uri` instance and create `Connect` message. - pub fn try_from(uri: U) -> Result - where - Uri: HttpTryFrom, - { - Ok(Connect { - uri: Uri::try_from(uri).map_err(|e| e.into())?, - }) - } - - pub(crate) fn is_secure(&self) -> bool { - if let Some(scheme) = self.uri.scheme_part() { - scheme.as_str() == "https" - } else { - false - } - } - - pub(crate) fn key(&self) -> Key { - self.uri.authority_part().unwrap().clone().into() - } - - pub(crate) fn validate(&self) -> Result<(), InvalidUrl> { - if self.uri.host().is_none() { - Err(InvalidUrl::MissingHost) - } else if self.uri.scheme_part().is_none() { - Err(InvalidUrl::MissingScheme) - } else if let Some(scheme) = self.uri.scheme_part() { - match scheme.as_str() { - "http" | "ws" | "https" | "wss" => Ok(()), - _ => Err(InvalidUrl::UnknownScheme), - } - } else { - Ok(()) - } - } -} - -impl Address for Connect { - fn host(&self) -> &str { - &self.uri.host().unwrap() - } - - fn port(&self) -> Option { - let port = if let Some(port) = self.uri.port() { - port - } else if let Some(scheme) = self.uri.scheme_part() { - match scheme.as_str() { - "http" | "ws" => 80, - "https" | "wss" => 443, - _ => 80, - } - } else { - 80 - }; - Some(port) - } -} diff --git a/src/client/connector.rs b/src/client/connector.rs index c764b93c4..b8054151b 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -8,9 +8,9 @@ use actix_connect::{ }; use actix_service::{apply_fn, Service, ServiceExt}; use actix_utils::timeout::{TimeoutError, TimeoutService}; +use http::Uri; use tokio_tcp::TcpStream; -use super::connect::Connect; use super::connection::Connection; use super::error::ConnectError; use super::pool::{ConnectionPool, Protocol}; @@ -38,8 +38,8 @@ pub struct Connector { impl Connector<(), ()> { pub fn new() -> Connector< impl Service< - Request = TcpConnect, - Response = TcpConnection, + Request = TcpConnect, + Response = TcpConnection, Error = actix_connect::ConnectError, > + Clone, TcpStream, @@ -79,8 +79,8 @@ impl Connector { where U1: AsyncRead + AsyncWrite + fmt::Debug, T1: Service< - Request = TcpConnect, - Response = TcpConnection, + Request = TcpConnect, + Response = TcpConnection, Error = actix_connect::ConnectError, > + Clone, { @@ -101,8 +101,8 @@ impl Connector where U: AsyncRead + AsyncWrite + fmt::Debug + 'static, T: Service< - Request = TcpConnect, - Response = TcpConnection, + Request = TcpConnect, + Response = TcpConnection, Error = actix_connect::ConnectError, > + Clone, { @@ -166,16 +166,14 @@ where /// Finish configuration process and create connector service. pub fn service( self, - ) -> impl Service - + Clone { + ) -> impl Service + Clone + { #[cfg(not(feature = "ssl"))] { let connector = TimeoutService::new( self.timeout, - apply_fn(self.connector, |msg: Connect, srv| { - srv.call(actix_connect::Connect::new(msg)) - }) - .map(|stream| (stream.into_parts().0, Protocol::Http1)), + apply_fn(self.connector, |msg: Uri, srv| srv.call(msg.into())) + .map(|stream| (stream.into_parts().0, Protocol::Http1)), ) .map_err(|e| match e { TimeoutError::Service(e) => e, @@ -199,28 +197,26 @@ where let ssl_service = TimeoutService::new( self.timeout, - apply_fn(self.connector.clone(), |msg: Connect, srv| { - srv.call(actix_connect::Connect::new(msg)) - }) - .map_err(ConnectError::from) - .and_then( - OpensslConnector::service(self.ssl) - .map_err(ConnectError::from) - .map(|stream| { - let sock = stream.into_parts().0; - let h2 = sock - .get_ref() - .ssl() - .selected_alpn_protocol() - .map(|protos| protos.windows(2).any(|w| w == H2)) - .unwrap_or(false); - if h2 { - (sock, Protocol::Http2) - } else { - (sock, Protocol::Http1) - } - }), - ), + apply_fn(self.connector.clone(), |msg: Uri, srv| srv.call(msg.into())) + .map_err(ConnectError::from) + .and_then( + OpensslConnector::service(self.ssl) + .map_err(ConnectError::from) + .map(|stream| { + let sock = stream.into_parts().0; + let h2 = sock + .get_ref() + .ssl() + .selected_alpn_protocol() + .map(|protos| protos.windows(2).any(|w| w == H2)) + .unwrap_or(false); + if h2 { + (sock, Protocol::Http2) + } else { + (sock, Protocol::Http1) + } + }), + ), ) .map_err(|e| match e { TimeoutError::Service(e) => e, @@ -229,11 +225,9 @@ where let tcp_service = TimeoutService::new( self.timeout, - apply_fn(self.connector.clone(), |msg: Connect, srv| { - srv.call(actix_connect::Connect::new(msg)) - }) - .map_err(ConnectError::from) - .map(|stream| (stream.into_parts().0, Protocol::Http1)), + apply_fn(self.connector.clone(), |msg: Uri, srv| srv.call(msg.into())) + .map_err(ConnectError::from) + .map(|stream| (stream.into_parts().0, Protocol::Http1)), ) .map_err(|e| match e { TimeoutError::Service(e) => e, @@ -271,7 +265,7 @@ mod connect_impl { pub(crate) struct InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { pub(crate) tcp_pool: ConnectionPool, } @@ -279,7 +273,7 @@ mod connect_impl { impl Clone for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service + T: Service + Clone, { fn clone(&self) -> Self { @@ -292,9 +286,9 @@ mod connect_impl { impl Service for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { - type Request = Connect; + type Request = Uri; type Response = IoConnection; type Error = ConnectorError; type Future = Either< @@ -306,13 +300,12 @@ mod connect_impl { self.tcp_pool.poll_ready() } - fn call(&mut self, req: Connect) -> Self::Future { - if req.is_secure() { - Either::B(err(ConnectError::SslIsNotSupported)) - } else if let Err(e) = req.validate() { - Either::B(err(e)) - } else { - Either::A(self.tcp_pool.call(req)) + fn call(&mut self, req: Uri) -> Self::Future { + match req.scheme_str() { + Some("https") | Some("wss") => { + Either::B(err(ConnectError::SslIsNotSupported)) + } + _ => Either::A(self.tcp_pool.call(req)), } } } @@ -332,8 +325,8 @@ mod connect_impl { where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service, - T2: Service, + T1: Service, + T2: Service, { pub(crate) tcp_pool: ConnectionPool, pub(crate) ssl_pool: ConnectionPool, @@ -343,9 +336,9 @@ mod connect_impl { where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service + T1: Service + Clone, - T2: Service + T2: Service + Clone, { fn clone(&self) -> Self { @@ -360,10 +353,10 @@ mod connect_impl { where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service, - T2: Service, + T1: Service, + T2: Service, { - type Request = Connect; + type Request = Uri; type Response = EitherConnection; type Error = ConnectError; type Future = Either< @@ -378,17 +371,18 @@ mod connect_impl { self.tcp_pool.poll_ready() } - fn call(&mut self, req: Connect) -> Self::Future { - if req.is_secure() { - Either::B(Either::B(InnerConnectorResponseB { - fut: self.ssl_pool.call(req), - _t: PhantomData, - })) - } else { - Either::B(Either::A(InnerConnectorResponseA { + fn call(&mut self, req: Uri) -> Self::Future { + match req.scheme_str() { + Some("https") | Some("wss") => { + Either::B(Either::B(InnerConnectorResponseB { + fut: self.ssl_pool.call(req), + _t: PhantomData, + })) + } + _ => Either::B(Either::A(InnerConnectorResponseA { fut: self.tcp_pool.call(req), _t: PhantomData, - })) + })), } } } @@ -396,7 +390,7 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseA where Io1: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { fut: as Service>::Future, _t: PhantomData, @@ -404,7 +398,7 @@ mod connect_impl { impl Future for InnerConnectorResponseA where - T: Service, + T: Service, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { @@ -422,7 +416,7 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseB where Io2: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { fut: as Service>::Future, _t: PhantomData, @@ -430,7 +424,7 @@ mod connect_impl { impl Future for InnerConnectorResponseB where - T: Service, + T: Service, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { diff --git a/src/client/mod.rs b/src/client/mod.rs index 0bff97e49..86b1a0cc0 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,5 +1,4 @@ //! Http client api -mod connect; mod connection; mod connector; mod error; @@ -9,7 +8,6 @@ mod pool; mod request; mod response; -pub use self::connect::Connect; pub use self::connection::Connection; pub use self::connector::Connector; pub use self::error::{ConnectError, InvalidUrl, SendRequestError}; diff --git a/src/client/pool.rs b/src/client/pool.rs index 214b7a382..a94b1e52a 100644 --- a/src/client/pool.rs +++ b/src/client/pool.rs @@ -7,18 +7,17 @@ use std::time::{Duration, Instant}; use actix_codec::{AsyncRead, AsyncWrite}; use actix_service::Service; use bytes::Bytes; -use futures::future::{ok, Either, FutureResult}; +use futures::future::{err, ok, Either, FutureResult}; use futures::task::AtomicTask; use futures::unsync::oneshot; use futures::{Async, Future, Poll}; use h2::client::{handshake, Handshake}; use hashbrown::HashMap; -use http::uri::Authority; +use http::uri::{Authority, Uri}; use indexmap::IndexSet; use slab::Slab; use tokio_timer::{sleep, Delay}; -use super::connect::Connect; use super::connection::{ConnectionType, IoConnection}; use super::error::ConnectError; @@ -48,7 +47,7 @@ pub(crate) struct ConnectionPool( impl ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { pub(crate) fn new( connector: T, @@ -87,9 +86,9 @@ where impl Service for ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { - type Request = Connect; + type Request = Uri; type Response = IoConnection; type Error = ConnectError; type Future = Either< @@ -101,8 +100,12 @@ where self.0.poll_ready() } - fn call(&mut self, req: Connect) -> Self::Future { - let key = req.key(); + fn call(&mut self, req: Uri) -> Self::Future { + let key = if let Some(authority) = req.authority_part() { + authority.clone().into() + } else { + return Either::A(err(ConnectError::Unresolverd)); + }; // acquire connection match self.1.as_ref().borrow_mut().acquire(&key) { @@ -268,110 +271,6 @@ where } } -// struct OpenWaitingConnection -// where -// Io: AsyncRead + AsyncWrite + 'static, -// { -// fut: F, -// key: Key, -// h2: Option>, -// rx: Option, ConnectorError>>>, -// inner: Option>>>, -// } - -// impl OpenWaitingConnection -// where -// F: Future + 'static, -// Io: AsyncRead + AsyncWrite + 'static, -// { -// fn spawn( -// key: Key, -// rx: oneshot::Sender, ConnectorError>>, -// inner: Rc>>, -// fut: F, -// ) { -// tokio_current_thread::spawn(OpenWaitingConnection { -// key, -// fut, -// h2: None, -// rx: Some(rx), -// inner: Some(inner), -// }) -// } -// } - -// impl Drop for OpenWaitingConnection -// where -// Io: AsyncRead + AsyncWrite + 'static, -// { -// fn drop(&mut self) { -// if let Some(inner) = self.inner.take() { -// let mut inner = inner.as_ref().borrow_mut(); -// inner.release(); -// inner.check_availibility(); -// } -// } -// } - -// impl Future for OpenWaitingConnection -// where -// F: Future, -// Io: AsyncRead + AsyncWrite, -// { -// type Item = (); -// type Error = (); - -// fn poll(&mut self) -> Poll { -// if let Some(ref mut h2) = self.h2 { -// return match h2.poll() { -// Ok(Async::Ready((snd, connection))) => { -// tokio_current_thread::spawn(connection.map_err(|_| ())); -// let _ = self.rx.take().unwrap().send(Ok(IoConnection::new( -// ConnectionType::H2(snd), -// Instant::now(), -// Some(Acquired(self.key.clone(), self.inner.clone())), -// ))); -// Ok(Async::Ready(())) -// } -// Ok(Async::NotReady) => Ok(Async::NotReady), -// Err(e) => { -// let _ = self.inner.take(); -// if let Some(rx) = self.rx.take() { -// let _ = rx.send(Err(e.into())); -// } - -// Err(()) -// } -// }; -// } - -// match self.fut.poll() { -// Err(err) => { -// let _ = self.inner.take(); -// if let Some(rx) = self.rx.take() { -// let _ = rx.send(Err(err)); -// } -// Err(()) -// } -// Ok(Async::Ready((_, io, proto))) => { -// let _ = self.inner.take(); -// if proto == Protocol::Http1 { -// let _ = self.rx.take().unwrap().send(Ok(IoConnection::new( -// ConnectionType::H1(io), -// Instant::now(), -// Some(Acquired(self.key.clone(), self.inner.clone())), -// ))); -// } else { -// self.h2 = Some(handshake(io)); -// return self.poll(); -// } -// Ok(Async::Ready(())) -// } -// Ok(Async::NotReady) => Ok(Async::NotReady), -// } -// } -// } - enum Acquire { Acquired(ConnectionType, Instant), Available, @@ -392,10 +291,7 @@ pub(crate) struct Inner { limit: usize, acquired: usize, available: HashMap>>, - waiters: Slab<( - Connect, - oneshot::Sender, ConnectError>>, - )>, + waiters: Slab<(Uri, oneshot::Sender, ConnectError>>)>, waiters_queue: IndexSet<(Key, usize)>, task: AtomicTask, } @@ -434,14 +330,14 @@ where /// connection is not available, wait fn wait_for( &mut self, - connect: Connect, + connect: Uri, ) -> ( oneshot::Receiver, ConnectError>>, usize, ) { let (tx, rx) = oneshot::channel(); - let key = connect.key(); + let key: Key = connect.authority_part().unwrap().clone().into(); let entry = self.waiters.vacant_entry(); let token = entry.key(); entry.insert((connect, tx)); diff --git a/src/client/request.rs b/src/client/request.rs index 199e13b93..7c7079fb7 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -21,8 +21,8 @@ use crate::http::{ use crate::message::{ConnectionType, Head, RequestHead}; use super::connection::Connection; +use super::error::{ConnectError, InvalidUrl, SendRequestError}; use super::response::ClientResponse; -use super::{Connect, ConnectError, SendRequestError}; /// An HTTP Client Request /// @@ -180,23 +180,32 @@ where ) -> impl Future where B: 'static, - T: Service, + T: Service, I: Connection, { let Self { head, body } = self; - let connect = Connect::new(head.uri.clone()); - if let Err(e) = connect.validate() { - Either::A(err(e.into())) + let uri = head.uri.clone(); + + // validate uri + if uri.host().is_none() { + Either::A(err(InvalidUrl::MissingHost.into())) + } else if uri.scheme_part().is_none() { + Either::A(err(InvalidUrl::MissingScheme.into())) + } else if let Some(scheme) = uri.scheme_part() { + match scheme.as_str() { + "http" | "ws" | "https" | "wss" => Either::B( + connector + // connect to the host + .call(uri) + .from_err() + // send request + .and_then(move |connection| connection.send_request(head, body)), + ), + _ => Either::A(err(InvalidUrl::UnknownScheme.into())), + } } else { - Either::B( - connector - // connect to the host - .call(connect) - .from_err() - // send request - .and_then(move |connection| connection.send_request(head, body)), - ) + Either::A(err(InvalidUrl::UnknownScheme.into())) } } } @@ -529,7 +538,7 @@ impl ClientRequestBuilder { if !parts.headers.contains_key(header::HOST) { let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); - let _ = match parts.uri.port() { + let _ = match parts.uri.port_u16() { None | Some(80) | Some(443) => write!(wrt, "{}", host), Some(port) => write!(wrt, "{}:{}", host, port), }; diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index 1aa391249..a0a9b2030 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_connect::{default_connector, Address, Connect as TcpConnect, ConnectError}; +use actix_connect::{default_connector, Connect as TcpConnect, ConnectError}; use actix_service::{apply_fn, Service}; use base64; use futures::future::{err, Either, FutureResult}; @@ -131,7 +131,7 @@ where // prep connection let connect = TcpConnect::new(request.uri().host().unwrap().to_string()) - .set_port(request.uri().port().unwrap_or_else(|| proto.port())); + .set_port(request.uri().port_u16().unwrap_or_else(|| proto.port())); let fut = Box::new( self.connector diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 3bb5feffb..26bca787e 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -5,10 +5,10 @@ use std::{net, thread, time}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_http::body::MessageBody; use actix_http::client::{ - ClientRequest, ClientRequestBuilder, ClientResponse, Connect, ConnectError, - Connection, Connector, SendRequestError, + ClientRequest, ClientRequestBuilder, ClientResponse, ConnectError, Connection, + Connector, SendRequestError, }; -use actix_http::ws; +use actix_http::{http::Uri, ws}; use actix_rt::{Runtime, System}; use actix_server::{Server, StreamServiceFactory}; use actix_service::Service; @@ -158,8 +158,8 @@ impl TestServerRuntime { } fn new_connector( - ) -> impl Service - + Clone { + ) -> impl Service + Clone + { #[cfg(feature = "ssl")] { use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; @@ -185,8 +185,8 @@ impl TestServerRuntime { /// Http connector pub fn connector( &mut self, - ) -> impl Service - + Clone { + ) -> impl Service + Clone + { self.execute(|| TestServerRuntime::new_connector()) } From 1f9467e880aaa53b3c2186070a0f1dca447499c5 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 14 Mar 2019 12:01:35 -0700 Subject: [PATCH 286/427] update tests --- tests/test_httpserver.rs | 2 +- tests/test_server.rs | 40 ++++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index d2bc07ac1..764d50ca2 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -50,7 +50,7 @@ fn test_start() { let mut connector = test::run_on(|| { Ok::<_, ()>( - client::Connector::default() + client::Connector::new() .timeout(Duration::from_millis(100)) .service(), ) diff --git a/tests/test_server.rs b/tests/test_server.rs index ebe968fa5..ffdc473a1 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -69,7 +69,7 @@ fn test_body_gzip() { assert!(response.status().is_success()); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -100,7 +100,7 @@ fn test_body_gzip_large() { assert!(response.status().is_success()); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -134,7 +134,7 @@ fn test_body_gzip_large_random() { assert!(response.status().is_success()); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -167,7 +167,7 @@ fn test_body_chunked_implicit() { ); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -195,7 +195,7 @@ fn test_body_br_streaming() { assert!(response.status().is_success()); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode br let mut e = BrotliDecoder::new(Vec::with_capacity(2048)); @@ -222,7 +222,7 @@ fn test_head_binary() { } // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert!(bytes.is_empty()); } @@ -245,7 +245,7 @@ fn test_no_chunking() { assert!(!response.headers().contains_key(TRANSFER_ENCODING)); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -267,7 +267,7 @@ fn test_body_deflate() { assert!(response.status().is_success()); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode deflate let mut e = ZlibDecoder::new(Vec::new()); @@ -294,7 +294,7 @@ fn test_body_brotli() { assert!(response.status().is_success()); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode brotli let mut e = BrotliDecoder::new(Vec::with_capacity(2048)); @@ -327,11 +327,11 @@ fn test_body_brotli() { // .header(http::header::CONTENT_ENCODING, "gzip") // .body(enc.clone()) // .unwrap(); -// let response = srv.execute(request.send()).unwrap(); +// let response = srv.block_on(request.send()).unwrap(); // assert!(response.status().is_success()); // // read response -// let bytes = srv.execute(response.body()).unwrap(); +// let bytes = srv.block_on(response.body()).unwrap(); // assert_eq!(bytes, Bytes::from_static(STR.as_ref())); // } @@ -360,11 +360,11 @@ fn test_body_brotli() { // .header(http::header::CONTENT_ENCODING, "gzip") // .body(enc.clone()) // .unwrap(); -// let response = srv.execute(request.send()).unwrap(); +// let response = srv.block_on(request.send()).unwrap(); // assert!(response.status().is_success()); // // read response -// let bytes = srv.execute(response.body()).unwrap(); +// let bytes = srv.block_on(response.body()).unwrap(); // assert_eq!(bytes, Bytes::from(data)); // } @@ -397,11 +397,11 @@ fn test_body_brotli() { // .header(http::header::CONTENT_ENCODING, "gzip") // .body(enc.clone()) // .unwrap(); -// let response = srv.execute(request.send()).unwrap(); +// let response = srv.block_on(request.send()).unwrap(); // assert!(response.status().is_success()); // // read response -// let bytes = srv.execute(response.body()).unwrap(); +// let bytes = srv.block_on(response.body()).unwrap(); // assert_eq!(bytes.len(), data.len()); // assert_eq!(bytes, Bytes::from(data)); // } @@ -430,11 +430,11 @@ fn test_body_brotli() { // .header(http::header::CONTENT_ENCODING, "deflate") // .body(enc) // .unwrap(); -// let response = srv.execute(request.send()).unwrap(); +// let response = srv.block_on(request.send()).unwrap(); // assert!(response.status().is_success()); // // read response -// let bytes = srv.execute(response.body()).unwrap(); +// let bytes = srv.block_on(response.body()).unwrap(); // assert_eq!(bytes, Bytes::from_static(STR.as_ref())); // } @@ -463,11 +463,11 @@ fn test_body_brotli() { // .header(http::header::CONTENT_ENCODING, "deflate") // .body(enc) // .unwrap(); -// let response = srv.execute(request.send()).unwrap(); +// let response = srv.block_on(request.send()).unwrap(); // assert!(response.status().is_success()); // // read response -// let bytes = srv.execute(response.body()).unwrap(); +// let bytes = srv.block_on(response.body()).unwrap(); // assert_eq!(bytes, Bytes::from(data)); // } @@ -500,7 +500,7 @@ fn test_body_brotli() { // .header(http::header::CONTENT_ENCODING, "deflate") // .body(enc) // .unwrap(); -// let response = srv.execute(request.send()).unwrap(); +// let response = srv.block_on(request.send()).unwrap(); // assert!(response.status().is_success()); // // read response From 76bb30dc3ae263791b10f055826696f8d605e987 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 14 Mar 2019 13:06:29 -0700 Subject: [PATCH 287/427] fix names --- src/client/connector.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/connector.rs b/src/client/connector.rs index b8054151b..de1615066 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -265,7 +265,7 @@ mod connect_impl { pub(crate) struct InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { pub(crate) tcp_pool: ConnectionPool, } @@ -286,14 +286,14 @@ mod connect_impl { impl Service for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service, { type Request = Uri; type Response = IoConnection; - type Error = ConnectorError; + type Error = ConnectError; type Future = Either< as Service>::Future, - FutureResult, ConnectorError>, + FutureResult, ConnectError>, >; fn poll_ready(&mut self) -> Poll<(), Self::Error> { From 15ba40d3ab1c4335d0b887ab8d55939ad20761a3 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 14 Mar 2019 13:08:05 -0700 Subject: [PATCH 288/427] fix non ssl connector --- src/client/connector.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/connector.rs b/src/client/connector.rs index de1615066..804756ced 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -173,6 +173,7 @@ where let connector = TimeoutService::new( self.timeout, apply_fn(self.connector, |msg: Uri, srv| srv.call(msg.into())) + .map_err(ConnectError::from) .map(|stream| (stream.into_parts().0, Protocol::Http1)), ) .map_err(|e| match e { From ce4a2629f35c28ce80a09423e9386764f35df14e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 14 Mar 2019 22:56:06 -0700 Subject: [PATCH 289/427] update actix-connect --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3b9a84984..a68489f51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ fail = ["failure"] [dependencies] actix-service = "0.3.4" actix-codec = "0.1.1" -actix-connect = { git = "https://github.com/actix/actix-net.git" } +actix-connect = "0.1.0" actix-utils = "0.3.4" actix-server-config = "0.1.0" @@ -85,7 +85,7 @@ failure = { version = "0.1.5", optional = true } [dev-dependencies] actix-rt = "0.2.0" actix-server = { version = "0.4.0", features=["ssl"] } -actix-connector = { version = "0.3.0", features=["ssl"] } +actix-connect = { version = "0.1.0", features=["ssl"] } actix-http-test = { path="test-server", features=["ssl"] } env_logger = "0.6" From 414614e1b50d3cc14c44341bcc0ea7cce21c7b43 Mon Sep 17 00:00:00 2001 From: lagudomeze Date: Sat, 16 Mar 2019 12:08:39 +0800 Subject: [PATCH 290/427] change marco import (#727) --- examples/basic.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index f8b816480..e8591f77e 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,9 +1,6 @@ use futures::IntoFuture; -#[macro_use] -extern crate actix_web; - -use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; +use actix_web::{get, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; #[get("/resource1/{name}/index.html")] fn index(req: HttpRequest, name: web::Path) -> String { From d93fe157b9cbb568860c7830209248eda3eeeb11 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 11:58:01 -0700 Subject: [PATCH 291/427] use better name Route::data instead of Route::config --- src/extract/form.rs | 2 +- src/extract/json.rs | 2 +- src/extract/mod.rs | 2 +- src/extract/payload.rs | 2 +- src/route.rs | 50 ++++++++++++++---------------------------- src/test.rs | 4 ++-- 6 files changed, 22 insertions(+), 40 deletions(-) diff --git a/src/extract/form.rs b/src/extract/form.rs index 19849ac8b..6b13c5f80 100644 --- a/src/extract/form.rs +++ b/src/extract/form.rs @@ -130,7 +130,7 @@ impl fmt::Display for Form { /// web::resource("/index.html") /// .route(web::get() /// // change `Form` extractor configuration -/// .config(web::FormConfig::default().limit(4097)) +/// .data(web::FormConfig::default().limit(4097)) /// .to(index)) /// ); /// } diff --git a/src/extract/json.rs b/src/extract/json.rs index f74b082d1..3847e71a8 100644 --- a/src/extract/json.rs +++ b/src/extract/json.rs @@ -215,7 +215,7 @@ where /// fn main() { /// let app = App::new().service( /// web::resource("/index.html").route( -/// web::post().config( +/// web::post().data( /// // change json extractor configuration /// web::JsonConfig::default().limit(4096) /// .error_handler(|err, req| { // <- create custom error response diff --git a/src/extract/mod.rs b/src/extract/mod.rs index 25a046d47..738f89187 100644 --- a/src/extract/mod.rs +++ b/src/extract/mod.rs @@ -262,7 +262,7 @@ mod tests { header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) - .config(FormConfig::default().limit(4096)) + .route_data(FormConfig::default().limit(4096)) .to_from(); let r = block_on(Option::>::from_request(&mut req)).unwrap(); diff --git a/src/extract/payload.rs b/src/extract/payload.rs index 13532eeef..3fc0c964e 100644 --- a/src/extract/payload.rs +++ b/src/extract/payload.rs @@ -177,7 +177,7 @@ where /// let app = App::new().service( /// web::resource("/index.html").route( /// web::get() -/// .config(web::PayloadConfig::new(4096)) // <- limit size of the payload +/// .data(web::PayloadConfig::new(4096)) // <- limit size of the payload /// .to(index)) // <- register handler with extractor params /// ); /// } diff --git a/src/route.rs b/src/route.rs index 5d339a3bb..30905d409 100644 --- a/src/route.rs +++ b/src/route.rs @@ -40,28 +40,28 @@ type BoxedRouteNewService = Box< pub struct Route

    { service: BoxedRouteNewService, ServiceResponse>, guards: Rc>>, - config: Option, - config_ref: Rc>>>, + data: Option, + data_ref: Rc>>>, } impl Route

    { /// Create new route which matches any request. pub fn new() -> Route

    { - let config_ref = Rc::new(RefCell::new(None)); + let data_ref = Rc::new(RefCell::new(None)); Route { service: Box::new(RouteNewService::new( - Extract::new(config_ref.clone()).and_then( + Extract::new(data_ref.clone()).and_then( Handler::new(HttpResponse::NotFound).map_err(|_| panic!()), ), )), guards: Rc::new(Vec::new()), - config: None, - config_ref, + data: None, + data_ref, } } pub(crate) fn finish(mut self) -> Self { - *self.config_ref.borrow_mut() = self.config.take().map(|e| Rc::new(e)); + *self.data_ref.borrow_mut() = self.data.take().map(|e| Rc::new(e)); self } @@ -180,24 +180,6 @@ impl Route

    { self } - // pub fn map>( - // self, - // md: F, - // ) -> RouteServiceBuilder - // where - // T: NewService< - // Request = HandlerRequest, - // Response = HandlerRequest, - // InitError = Error, - // >, - // { - // RouteServiceBuilder { - // service: md.into_new_service(), - // guards: self.guards, - // _t: PhantomData, - // } - // } - /// Set handler function, use request extractors for parameters. /// /// ```rust @@ -253,7 +235,7 @@ impl Route

    { R: Responder + 'static, { self.service = Box::new(RouteNewService::new( - Extract::new(self.config_ref.clone()) + Extract::new(self.data_ref.clone()) .and_then(Handler::new(handler).map_err(|_| panic!())), )); self @@ -295,14 +277,14 @@ impl Route

    { R::Error: Into, { self.service = Box::new(RouteNewService::new( - Extract::new(self.config_ref.clone()) + Extract::new(self.data_ref.clone()) .and_then(AsyncHandler::new(handler).map_err(|_| panic!())), )); self } - /// This method allows to add extractor configuration - /// for specific route. + /// Provide route specific data. This method allows to add extractor + /// configuration or specific state available via `RouteData` extractor. /// /// ```rust /// use actix_web::{web, App}; @@ -317,17 +299,17 @@ impl Route

    { /// web::resource("/index.html").route( /// web::get() /// // limit size of the payload - /// .config(web::PayloadConfig::new(4096)) + /// .data(web::PayloadConfig::new(4096)) /// // register handler /// .to(index) /// )); /// } /// ``` - pub fn config(mut self, config: C) -> Self { - if self.config.is_none() { - self.config = Some(Extensions::new()); + pub fn data(mut self, data: C) -> Self { + if self.data.is_none() { + self.data = Some(Extensions::new()); } - self.config.as_mut().unwrap().insert(config); + self.data.as_mut().unwrap().insert(data); self } } diff --git a/src/test.rs b/src/test.rs index 57a6d3961..4db268f14 100644 --- a/src/test.rs +++ b/src/test.rs @@ -265,8 +265,8 @@ impl TestRequest { self } - /// Set request config - pub fn config(self, data: T) -> Self { + /// Set route data + pub fn route_data(self, data: T) -> Self { self.config.extensions.borrow_mut().insert(data); self } From b1e267bce41be057d4c620c31c098fe099698f8f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 20:17:27 -0700 Subject: [PATCH 292/427] rename State to a Data --- src/app.rs | 75 +++++++++++++++++++-------------------- src/app_service.rs | 16 ++++----- src/{state.rs => data.rs} | 52 +++++++++++++-------------- src/lib.rs | 6 ++-- 4 files changed, 74 insertions(+), 75 deletions(-) rename src/{state.rs => data.rs} (63%) diff --git a/src/app.rs b/src/app.rs index 2e2a8c2dd..b146fb4c8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,6 +12,7 @@ use futures::IntoFuture; use crate::app_service::{AppChain, AppEntry, AppInit, AppRouting, AppRoutingFactory}; use crate::config::{AppConfig, AppConfigInner}; +use crate::data::{Data, DataFactory}; use crate::dev::{PayloadStream, ResourceDef}; use crate::error::Error; use crate::resource::Resource; @@ -20,7 +21,6 @@ use crate::service::{ HttpServiceFactory, ServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, }; -use crate::state::{State, StateFactory}; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; @@ -32,18 +32,17 @@ where T: NewService>, { chain: T, - state: Vec>, + data: Vec>, config: AppConfigInner, _t: PhantomData<(P,)>, } impl App { - /// Create application builder with empty state. Application can - /// be configured with a builder-like pattern. + /// Create application builder. Application can be configured with a builder-like pattern. pub fn new() -> Self { App { chain: AppChain, - state: Vec::new(), + data: Vec::new(), config: AppConfigInner::default(), _t: PhantomData, } @@ -60,51 +59,51 @@ where InitError = (), >, { - /// Set application state. Applicatin state could be accessed - /// by using `State` extractor where `T` is state type. + /// Set application data. Applicatin data could be accessed + /// by using `Data` extractor where `T` is data type. /// /// **Note**: http server accepts an application factory rather than /// an application instance. Http server constructs an application - /// instance for each thread, thus application state must be constructed - /// multiple times. If you want to share state between different + /// instance for each thread, thus application data must be constructed + /// multiple times. If you want to share data between different /// threads, a shared object should be used, e.g. `Arc`. Application - /// state does not need to be `Send` or `Sync`. + /// data does not need to be `Send` or `Sync`. /// /// ```rust /// use std::cell::Cell; /// use actix_web::{web, App}; /// - /// struct MyState { + /// struct MyData { /// counter: Cell, /// } /// - /// fn index(state: web::State) { - /// state.counter.set(state.counter.get() + 1); + /// fn index(data: web::Data) { + /// data.counter.set(data.counter.get() + 1); /// } /// /// fn main() { /// let app = App::new() - /// .state(MyState{ counter: Cell::new(0) }) + /// .data(MyData{ counter: Cell::new(0) }) /// .service( /// web::resource("/index.html").route( /// web::get().to(index))); /// } /// ``` - pub fn state(mut self, state: S) -> Self { - self.state.push(Box::new(State::new(state))); + pub fn data(mut self, data: S) -> Self { + self.data.push(Box::new(Data::new(data))); self } - /// Set application state factory. This function is - /// similar to `.state()` but it accepts state factory. State get + /// Set application data factory. This function is + /// similar to `.data()` but it accepts data factory. Data object get /// constructed asynchronously during application initialization. - pub fn state_factory(mut self, state: F) -> Self + pub fn data_factory(mut self, data: F) -> Self where F: Fn() -> Out + 'static, Out: IntoFuture + 'static, Out::Error: std::fmt::Debug, { - self.state.push(Box::new(state)); + self.data.push(Box::new(data)); self } @@ -138,7 +137,7 @@ where AppRouter { endpoint, chain: self.chain, - state: self.state, + data: self.data, services: Vec::new(), default: None, factory_ref: fref, @@ -174,7 +173,7 @@ where let chain = self.chain.and_then(chain.into_new_service()); App { chain, - state: self.state, + data: self.data, config: self.config, _t: PhantomData, } @@ -183,7 +182,7 @@ where /// Configure route for a specific path. /// /// This is a simplified version of the `App::service()` method. - /// This method can not be could multiple times, in that case + /// This method can be used multiple times with same path, in that case /// multiple resources with one route would be registered for same resource path. /// /// ```rust @@ -223,7 +222,7 @@ where default: None, endpoint: AppEntry::new(fref.clone()), factory_ref: fref, - state: self.state, + data: self.data, config: self.config, services: vec![Box::new(ServiceFactoryWrapper::new(service))], external: Vec::new(), @@ -233,7 +232,7 @@ where /// Set server host name. /// - /// Host name is used by application router aa a hostname for url + /// Host name is used by application router as a hostname for url /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. /// html#method.host) documentation for more information. /// @@ -252,7 +251,7 @@ pub struct AppRouter { services: Vec>>, default: Option>>, factory_ref: Rc>>>, - state: Vec>, + data: Vec>, config: AppConfigInner, external: Vec, _t: PhantomData<(P, B)>, @@ -344,7 +343,7 @@ where AppRouter { endpoint, chain: self.chain, - state: self.state, + data: self.data, services: self.services, default: self.default, factory_ref: self.factory_ref, @@ -429,7 +428,7 @@ where fn into_new_service(self) -> AppInit { AppInit { chain: self.chain, - state: self.state, + data: self.data, endpoint: self.endpoint, services: RefCell::new(self.services), external: RefCell::new(self.external), @@ -489,10 +488,10 @@ mod tests { } #[test] - fn test_state() { + fn test_data() { let mut srv = - init_service(App::new().state(10usize).service( - web::resource("/").to(|_: web::State| HttpResponse::Ok()), + init_service(App::new().data(10usize).service( + web::resource("/").to(|_: web::Data| HttpResponse::Ok()), )); let req = TestRequest::default().to_request(); @@ -500,8 +499,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); let mut srv = - init_service(App::new().state(10u32).service( - web::resource("/").to(|_: web::State| HttpResponse::Ok()), + init_service(App::new().data(10u32).service( + web::resource("/").to(|_: web::Data| HttpResponse::Ok()), )); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -509,18 +508,18 @@ mod tests { } #[test] - fn test_state_factory() { + fn test_data_factory() { let mut srv = - init_service(App::new().state_factory(|| Ok::<_, ()>(10usize)).service( - web::resource("/").to(|_: web::State| HttpResponse::Ok()), + init_service(App::new().data_factory(|| Ok::<_, ()>(10usize)).service( + web::resource("/").to(|_: web::Data| HttpResponse::Ok()), )); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let mut srv = - init_service(App::new().state_factory(|| Ok::<_, ()>(10u32)).service( - web::resource("/").to(|_: web::State| HttpResponse::Ok()), + init_service(App::new().data_factory(|| Ok::<_, ()>(10u32)).service( + web::resource("/").to(|_: web::Data| HttpResponse::Ok()), )); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); diff --git a/src/app_service.rs b/src/app_service.rs index c59b80bcc..0bf3d3095 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -11,11 +11,11 @@ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, Poll}; use crate::config::{AppConfig, ServiceConfig}; +use crate::data::{DataFactory, DataFactoryResult}; use crate::error::Error; use crate::guard::Guard; use crate::rmap::ResourceMap; use crate::service::{ServiceFactory, ServiceRequest, ServiceResponse}; -use crate::state::{StateFactory, StateFactoryResult}; type Guards = Vec>; type HttpService

    = BoxedService, ServiceResponse, Error>; @@ -24,7 +24,7 @@ type HttpNewService

    = type BoxedResponse = Box>; /// Service factory to convert `Request` to a `ServiceRequest`. -/// It also executes state factories. +/// It also executes data factories. pub struct AppInit where C: NewService>, @@ -37,7 +37,7 @@ where { pub(crate) chain: C, pub(crate) endpoint: T, - pub(crate) state: Vec>, + pub(crate) data: Vec>, pub(crate) config: RefCell, pub(crate) services: RefCell>>>, pub(crate) default: Option>>, @@ -121,7 +121,7 @@ where chain_fut: self.chain.new_service(&()), endpoint: None, endpoint_fut: self.endpoint.new_service(&()), - state: self.state.iter().map(|s| s.construct()).collect(), + data: self.data.iter().map(|s| s.construct()).collect(), config: self.config.borrow().clone(), rmap, _t: PhantomData, @@ -139,7 +139,7 @@ where chain_fut: C::Future, endpoint_fut: T::Future, rmap: Rc, - state: Vec>, + data: Vec>, config: AppConfig, _t: PhantomData<(P, B)>, } @@ -165,9 +165,9 @@ where fn poll(&mut self) -> Poll { let mut idx = 0; let mut extensions = self.config.0.extensions.borrow_mut(); - while idx < self.state.len() { - if let Async::Ready(_) = self.state[idx].poll_result(&mut extensions)? { - self.state.remove(idx); + while idx < self.data.len() { + if let Async::Ready(_) = self.data[idx].poll_result(&mut extensions)? { + self.data.remove(idx); } else { idx += 1; } diff --git a/src/state.rs b/src/data.rs similarity index 63% rename from src/state.rs rename to src/data.rs index b70540c07..a172cb357 100644 --- a/src/state.rs +++ b/src/data.rs @@ -8,21 +8,21 @@ use futures::{Async, Future, IntoFuture, Poll}; use crate::extract::FromRequest; use crate::service::ServiceFromRequest; -/// Application state factory -pub(crate) trait StateFactory { - fn construct(&self) -> Box; +/// Application data factory +pub(crate) trait DataFactory { + fn construct(&self) -> Box; } -pub(crate) trait StateFactoryResult { +pub(crate) trait DataFactoryResult { fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()>; } /// Application state -pub struct State(Arc); +pub struct Data(Arc); -impl State { - pub(crate) fn new(state: T) -> State { - State(Arc::new(state)) +impl Data { + pub(crate) fn new(state: T) -> Data { + Data(Arc::new(state)) } /// Get referecnce to inner state type. @@ -31,7 +31,7 @@ impl State { } } -impl Deref for State { +impl Deref for Data { type Target = T; fn deref(&self) -> &T { @@ -39,19 +39,19 @@ impl Deref for State { } } -impl Clone for State { - fn clone(&self) -> State { - State(self.0.clone()) +impl Clone for Data { + fn clone(&self) -> Data { + Data(self.0.clone()) } } -impl FromRequest

    for State { +impl FromRequest

    for Data { type Error = Error; type Future = Result; #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - if let Some(st) = req.config().extensions().get::>() { + if let Some(st) = req.config().extensions().get::>() { Ok(st.clone()) } else { Err(ErrorInternalServerError( @@ -61,37 +61,37 @@ impl FromRequest

    for State { } } -impl StateFactory for State { - fn construct(&self) -> Box { - Box::new(StateFut { st: self.clone() }) +impl DataFactory for Data { + fn construct(&self) -> Box { + Box::new(DataFut { st: self.clone() }) } } -struct StateFut { - st: State, +struct DataFut { + st: Data, } -impl StateFactoryResult for StateFut { +impl DataFactoryResult for DataFut { fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()> { extensions.insert(self.st.clone()); Ok(Async::Ready(())) } } -impl StateFactory for F +impl DataFactory for F where F: Fn() -> Out + 'static, Out: IntoFuture + 'static, Out::Error: std::fmt::Debug, { - fn construct(&self) -> Box { - Box::new(StateFactoryFut { + fn construct(&self) -> Box { + Box::new(DataFactoryFut { fut: (*self)().into_future(), }) } } -struct StateFactoryFut +struct DataFactoryFut where F: Future, F::Error: std::fmt::Debug, @@ -99,7 +99,7 @@ where fut: F, } -impl StateFactoryResult for StateFactoryFut +impl DataFactoryResult for DataFactoryFut where F: Future, F::Error: std::fmt::Debug, @@ -107,7 +107,7 @@ where fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()> { match self.fut.poll() { Ok(Async::Ready(s)) => { - extensions.insert(State::new(s)); + extensions.insert(Data::new(s)); Ok(Async::Ready(())) } Ok(Async::NotReady) => Ok(Async::NotReady), diff --git a/src/lib.rs b/src/lib.rs index d653fd1c2..843ad1035 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ mod app; mod app_service; mod config; +mod data; pub mod error; mod extract; pub mod guard; @@ -17,7 +18,6 @@ mod route; mod scope; mod server; mod service; -mod state; pub mod test; #[allow(unused_imports)] @@ -37,7 +37,6 @@ pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::{Either, Responder}; pub use crate::route::Route; -pub use crate::scope::Scope; pub use crate::server::HttpServer; pub mod dev { @@ -77,6 +76,7 @@ pub mod dev { } pub mod web { + //! Various types use actix_http::{http::Method, Response}; use actix_rt::blocking; use futures::{Future, IntoFuture}; @@ -91,11 +91,11 @@ pub mod web { use crate::route::Route; use crate::scope::Scope; + pub use crate::data::Data; pub use crate::error::{BlockingError, Error}; pub use crate::extract::{Form, Json, Path, Payload, Query}; pub use crate::extract::{FormConfig, JsonConfig, PayloadConfig}; pub use crate::request::HttpRequest; - pub use crate::state::State; /// Create resource for a specific path. /// From 60386f1791e6bd005889d566eba1ba0b76699401 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 21:09:11 -0700 Subject: [PATCH 293/427] introduce RouteData extractor --- examples/basic.rs | 4 +- src/app.rs | 20 ----- src/data.rs | 182 ++++++++++++++++++++++++++++++++++++++++- src/extract/form.rs | 2 +- src/extract/json.rs | 2 +- src/extract/payload.rs | 4 +- src/lib.rs | 2 +- src/route.rs | 3 +- src/service.rs | 16 ++-- 9 files changed, 198 insertions(+), 37 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index e8591f77e..756f1b796 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,6 +1,8 @@ use futures::IntoFuture; -use actix_web::{get, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; +use actix_web::{ + get, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer, +}; #[get("/resource1/{name}/index.html")] fn index(req: HttpRequest, name: web::Path) -> String { diff --git a/src/app.rs b/src/app.rs index b146fb4c8..8c4168085 100644 --- a/src/app.rs +++ b/src/app.rs @@ -487,26 +487,6 @@ mod tests { assert_eq!(resp.status(), StatusCode::CREATED); } - #[test] - fn test_data() { - let mut srv = - init_service(App::new().data(10usize).service( - web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - )); - - let req = TestRequest::default().to_request(); - let resp = block_on(srv.call(req)).unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - - let mut srv = - init_service(App::new().data(10u32).service( - web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - )); - let req = TestRequest::default().to_request(); - let resp = block_on(srv.call(req)).unwrap(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - #[test] fn test_data_factory() { let mut srv = diff --git a/src/data.rs b/src/data.rs index a172cb357..6fb8e0b9f 100644 --- a/src/data.rs +++ b/src/data.rs @@ -17,7 +17,45 @@ pub(crate) trait DataFactoryResult { fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()>; } -/// Application state +/// Application data. +/// +/// Application data is an arbitrary data attached to the app. +/// Application data is available to all routes and could be added +/// during application configuration process +/// with `App::data()` method. +/// +/// Applicatin data could be accessed by using `Data` +/// extractor where `T` is data type. +/// +/// **Note**: http server accepts an application factory rather than +/// an application instance. Http server constructs an application +/// instance for each thread, thus application data must be constructed +/// multiple times. If you want to share data between different +/// threads, a shared object should be used, e.g. `Arc`. Application +/// data does not need to be `Send` or `Sync`. +/// +/// ```rust +/// use std::cell::Cell; +/// use actix_web::{web, App}; +/// +/// struct MyData { +/// counter: Cell, +/// } +/// +/// /// Use `Data` extractor to access data in handler. +/// fn index(data: web::Data) { +/// data.counter.set(data.counter.get() + 1); +/// } +/// +/// fn main() { +/// let app = App::new() +/// // Store `MyData` in application storage. +/// .data(MyData{ counter: Cell::new(0) }) +/// .service( +/// web::resource("/index.html").route( +/// web::get().to(index))); +/// } +/// ``` pub struct Data(Arc); impl Data { @@ -25,7 +63,7 @@ impl Data { Data(Arc::new(state)) } - /// Get referecnce to inner state type. + /// Get referecnce to inner app data. pub fn get_ref(&self) -> &T { self.0.as_ref() } @@ -55,7 +93,7 @@ impl FromRequest

    for Data { Ok(st.clone()) } else { Err(ErrorInternalServerError( - "State is not configured, to configure use App::state()", + "App data is not configured, to configure use App::data()", )) } } @@ -118,3 +156,141 @@ where } } } + +/// Route data. +/// +/// Route data is an arbitrary data attached to specific route. +/// Route data could be added to route during route configuration process +/// with `Route::data()` method. Route data is also used as an extractor +/// configuration storage. Route data could be accessed in handler +/// via `RouteData` extractor. +/// +/// ```rust +/// # use std::cell::Cell; +/// use actix_web::{web, App}; +/// +/// struct MyData { +/// counter: Cell, +/// } +/// +/// /// Use `RouteData` extractor to access data in handler. +/// fn index(data: web::RouteData) { +/// data.counter.set(data.counter.get() + 1); +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get() +/// // Store `MyData` in route storage +/// .data(MyData{ counter: Cell::new(0) }) +/// // Route data could be used as extractor configuration storage, +/// // limit size of the payload +/// .data(web::PayloadConfig::new(4096)) +/// // register handler +/// .to(index) +/// )); +/// } +/// ``` +/// +/// If route data is not set for a handler, using `RouteData` extractor +/// would cause `Internal Server error` response. +pub struct RouteData(Arc); + +impl RouteData { + pub(crate) fn new(state: T) -> RouteData { + RouteData(Arc::new(state)) + } + + /// Get referecnce to inner data object. + pub fn get_ref(&self) -> &T { + self.0.as_ref() + } +} + +impl Deref for RouteData { + type Target = T; + + fn deref(&self) -> &T { + self.0.as_ref() + } +} + +impl Clone for RouteData { + fn clone(&self) -> RouteData { + RouteData(self.0.clone()) + } +} + +impl FromRequest

    for RouteData { + type Error = Error; + type Future = Result; + + #[inline] + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + if let Some(st) = req.route_data::() { + Ok(st.clone()) + } else { + Err(ErrorInternalServerError( + "Route data is not configured, to configure use Route::data()", + )) + } + } +} + +#[cfg(test)] +mod tests { + use actix_service::Service; + + use crate::http::StatusCode; + use crate::test::{block_on, init_service, TestRequest}; + use crate::{web, App, HttpResponse}; + + #[test] + fn test_data_extractor() { + let mut srv = + init_service(App::new().data(10usize).service( + web::resource("/").to(|_: web::Data| HttpResponse::Ok()), + )); + + let req = TestRequest::default().to_request(); + let resp = block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let mut srv = + init_service(App::new().data(10u32).service( + web::resource("/").to(|_: web::Data| HttpResponse::Ok()), + )); + let req = TestRequest::default().to_request(); + let resp = block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + } + + #[test] + fn test_route_data_extractor() { + let mut srv = init_service(App::new().service(web::resource("/").route( + web::get().data(10usize).to(|data: web::RouteData| { + let _ = data.clone(); + HttpResponse::Ok() + }), + ))); + + let req = TestRequest::default().to_request(); + let resp = block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + // different type + let mut srv = init_service( + App::new().service( + web::resource("/").route( + web::get() + .data(10u32) + .to(|_: web::RouteData| HttpResponse::Ok()), + ), + ), + ); + let req = TestRequest::default().to_request(); + let resp = block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + } +} diff --git a/src/extract/form.rs b/src/extract/form.rs index 6b13c5f80..4a5e97299 100644 --- a/src/extract/form.rs +++ b/src/extract/form.rs @@ -77,7 +77,7 @@ where fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { let req2 = req.clone(); let (limit, err) = req - .load_config::() + .route_data::() .map(|c| (c.limit, c.ehandler.clone())) .unwrap_or((16384, None)); diff --git a/src/extract/json.rs b/src/extract/json.rs index 3847e71a8..92b7f20f6 100644 --- a/src/extract/json.rs +++ b/src/extract/json.rs @@ -177,7 +177,7 @@ where fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { let req2 = req.clone(); let (limit, err) = req - .load_config::() + .route_data::() .map(|c| (c.limit, c.ehandler.clone())) .unwrap_or((32768, None)); diff --git a/src/extract/payload.rs b/src/extract/payload.rs index 3fc0c964e..7164a544f 100644 --- a/src/extract/payload.rs +++ b/src/extract/payload.rs @@ -140,7 +140,7 @@ where #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { let mut tmp; - let cfg = if let Some(cfg) = req.load_config::() { + let cfg = if let Some(cfg) = req.route_data::() { cfg } else { tmp = PayloadConfig::default(); @@ -193,7 +193,7 @@ where #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { let mut tmp; - let cfg = if let Some(cfg) = req.load_config::() { + let cfg = if let Some(cfg) = req.route_data::() { cfg } else { tmp = PayloadConfig::default(); diff --git a/src/lib.rs b/src/lib.rs index 843ad1035..b22e05da0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,7 +91,7 @@ pub mod web { use crate::route::Route; use crate::scope::Scope; - pub use crate::data::Data; + pub use crate::data::{Data, RouteData}; pub use crate::error::{BlockingError, Error}; pub use crate::extract::{Form, Json, Path, Payload, Query}; pub use crate::extract::{FormConfig, JsonConfig, PayloadConfig}; diff --git a/src/route.rs b/src/route.rs index 30905d409..c44ed713f 100644 --- a/src/route.rs +++ b/src/route.rs @@ -6,6 +6,7 @@ use actix_http::{http::Method, Error, Extensions, Response}; use actix_service::{NewService, Service}; use futures::{Async, Future, IntoFuture, Poll}; +use crate::data::RouteData; use crate::extract::FromRequest; use crate::guard::{self, Guard}; use crate::handler::{AsyncFactory, AsyncHandler, Extract, Factory, Handler}; @@ -309,7 +310,7 @@ impl Route

    { if self.data.is_none() { self.data = Some(Extensions::new()); } - self.data.as_mut().unwrap().insert(data); + self.data.as_mut().unwrap().insert(RouteData::new(data)); self } } diff --git a/src/service.rs b/src/service.rs index e907a1abc..b8c3a1584 100644 --- a/src/service.rs +++ b/src/service.rs @@ -13,6 +13,7 @@ use actix_router::{Path, Resource, Url}; use futures::future::{ok, FutureResult, IntoFuture}; use crate::config::{AppConfig, ServiceConfig}; +use crate::data::RouteData; use crate::request::HttpRequest; use crate::rmap::ResourceMap; @@ -241,15 +242,15 @@ impl

    fmt::Debug for ServiceRequest

    { pub struct ServiceFromRequest

    { req: HttpRequest, payload: Payload

    , - config: Option>, + data: Option>, } impl

    ServiceFromRequest

    { - pub(crate) fn new(req: ServiceRequest

    , config: Option>) -> Self { + pub(crate) fn new(req: ServiceRequest

    , data: Option>) -> Self { Self { req: req.req, payload: req.payload, - config, + data, } } @@ -269,10 +270,11 @@ impl

    ServiceFromRequest

    { ServiceResponse::new(self.req, err.into().into()) } - /// Load extractor configuration - pub fn load_config(&self) -> Option<&T> { - if let Some(ref ext) = self.config { - ext.get::() + /// Load route data. Route data could be set during + /// route configuration with `Route::data()` method. + pub fn route_data(&self) -> Option<&RouteData> { + if let Some(ref ext) = self.data { + ext.get::>() } else { None } From e396c90c9ec0cdfb3b384dd4c16ea8a5b6e96757 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 21:13:16 -0700 Subject: [PATCH 294/427] update api doc --- src/data.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/data.rs b/src/data.rs index 6fb8e0b9f..a53015c23 100644 --- a/src/data.rs +++ b/src/data.rs @@ -34,6 +34,9 @@ pub(crate) trait DataFactoryResult { /// threads, a shared object should be used, e.g. `Arc`. Application /// data does not need to be `Send` or `Sync`. /// +/// If route data is not set for a handler, using `Data` extractor would +/// cause *Internal Server Error* response. +/// /// ```rust /// use std::cell::Cell; /// use actix_web::{web, App}; @@ -165,6 +168,9 @@ where /// configuration storage. Route data could be accessed in handler /// via `RouteData` extractor. /// +/// If route data is not set for a handler, using `RouteData` extractor +/// would cause *Internal Server Error* response. +/// /// ```rust /// # use std::cell::Cell; /// use actix_web::{web, App}; @@ -192,9 +198,6 @@ where /// )); /// } /// ``` -/// -/// If route data is not set for a handler, using `RouteData` extractor -/// would cause `Internal Server error` response. pub struct RouteData(Arc); impl RouteData { From 4a4826b23a72c539c26c9e69b9a69ec38a3fd828 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 21:35:02 -0700 Subject: [PATCH 295/427] cleanup doc strings and clippy warnings --- src/app.rs | 15 ++++++++++----- src/info.rs | 4 ++-- src/lib.rs | 4 ++-- src/resource.rs | 13 +++++++------ src/route.rs | 2 +- src/scope.rs | 26 ++++++++++++++++++-------- src/test.rs | 5 +++-- 7 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8c4168085..c4f2e33bd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -148,7 +148,7 @@ where } /// Register a request modifier. It can modify any request parameters - /// including payload stream. + /// including payload stream type. pub fn chain( self, chain: C, @@ -211,6 +211,14 @@ where } /// Register http service. + /// + /// Http service is any type that implements `HttpServiceFactory` trait. + /// + /// Actix web provides several services implementations: + /// + /// * *Resource* is an entry in resource table which corresponds to requested URL. + /// * *Scope* is a set of resources with common root path. + /// * "StaticFiles" is a service for static files support pub fn service(self, service: F) -> AppRouter> where F: HttpServiceFactory

    + 'static, @@ -301,7 +309,7 @@ where /// /// Actix web provides several services implementations: /// - /// * *Resource* is an entry in route table which corresponds to requested URL. + /// * *Resource* is an entry in resource table which corresponds to requested URL. /// * *Scope* is a set of resources with common root path. /// * "StaticFiles" is a service for static files support pub fn service(mut self, factory: F) -> Self @@ -354,9 +362,6 @@ where } /// Default resource to be used if no matching route could be found. - /// - /// Default resource works with resources only and does not work with - /// custom services. pub fn default_resource(mut self, f: F) -> Self where F: FnOnce(Resource

    ) -> Resource, diff --git a/src/info.rs b/src/info.rs index c058bd517..9a97c3353 100644 --- a/src/info.rs +++ b/src/info.rs @@ -25,7 +25,7 @@ impl ConnectionInfo { Ref::map(req.extensions(), |e| e.get().unwrap()) } - #[cfg_attr(feature = "cargo-clippy", allow(cyclomatic_complexity))] + #[allow(clippy::cyclomatic_complexity)] fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { let mut host = None; let mut scheme = None; @@ -123,10 +123,10 @@ impl ConnectionInfo { } ConnectionInfo { + peer, scheme: scheme.unwrap_or("http").to_owned(), host: host.unwrap_or("localhost").to_owned(), remote: remote.map(|s| s.to_owned()), - peer: peer, } } diff --git a/src/lib.rs b/src/lib.rs index b22e05da0..509fcc608 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::type_complexity)] +#![allow(clippy::type_complexity, clippy::new_without_default)] mod app; mod app_service; @@ -84,6 +84,7 @@ pub mod web { pub use actix_http::Response as HttpResponse; pub use bytes::{Bytes, BytesMut}; + use crate::error::{BlockingError, Error}; use crate::extract::FromRequest; use crate::handler::{AsyncFactory, Factory}; use crate::resource::Resource; @@ -92,7 +93,6 @@ pub mod web { use crate::scope::Scope; pub use crate::data::{Data, RouteData}; - pub use crate::error::{BlockingError, Error}; pub use crate::extract::{Form, Json, Path, Payload, Query}; pub use crate::extract::{FormConfig, JsonConfig, PayloadConfig}; pub use crate::request::HttpRequest; diff --git a/src/resource.rs b/src/resource.rs index e4fe65c05..46b3e2a8f 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -21,7 +21,7 @@ type HttpService

    = BoxedService, ServiceResponse, Error>; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; -/// *Resource* is an entry in route table which corresponds to requested URL. +/// *Resource* is an entry in resources table which corresponds to requested URL. /// /// Resource in turn has at least one route. /// Route consists of an handlers objects and list of guards @@ -132,7 +132,8 @@ where /// } /// ``` /// - /// Multiple routes could be added to a resource. + /// Multiple routes could be added to a resource. Resource object uses + /// match guards for route selection. /// /// ```rust /// use actix_web::{web, guard, App, HttpResponse}; @@ -220,11 +221,11 @@ where self } - /// Register a resource middleware + /// Register a resource middleware. /// - /// This is similar to `App's` middlewares, but - /// middleware is not allowed to change response type (i.e modify response's body). - /// Middleware get invoked on resource level. + /// This is similar to `App's` middlewares, but middleware get invoked on resource level. + /// Resource level middlewares are not allowed to change response + /// type (i.e modify response's body). pub fn middleware( self, mw: F, diff --git a/src/route.rs b/src/route.rs index c44ed713f..626b09514 100644 --- a/src/route.rs +++ b/src/route.rs @@ -62,7 +62,7 @@ impl Route

    { } pub(crate) fn finish(mut self) -> Self { - *self.data_ref.borrow_mut() = self.data.take().map(|e| Rc::new(e)); + *self.data_ref.borrow_mut() = self.data.take().map(Rc::new); self } diff --git a/src/scope.rs b/src/scope.rs index 9f5b650cd..bf3261f2f 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -26,7 +26,7 @@ type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; type BoxedResponse = Box>; -/// Resources scope +/// Resources scope. /// /// Scope is a set of resources with common root path. /// Scopes collect multiple paths under a common path prefix. @@ -114,7 +114,15 @@ where self } - /// Create nested service. + /// Register http service. + /// + /// This is similar to `App's` service registration. + /// + /// Actix web provides several services implementations: + /// + /// * *Resource* is an entry in resource table which corresponds to requested URL. + /// * *Scope* is a set of resources with common root path. + /// * "StaticFiles" is a service for static files support /// /// ```rust /// use actix_web::{web, App, HttpRequest}; @@ -145,7 +153,7 @@ where /// Configure route for a specific path. /// /// This is a simplified version of the `Scope::service()` method. - /// This method can not be could multiple times, in that case + /// This method can be called multiple times, in that case /// multiple resources with one route would be registered for same resource path. /// /// ```rust @@ -172,6 +180,8 @@ where } /// Default resource to be used if no matching route could be found. + /// + /// If default resource is not registered, app's default resource is being used. pub fn default_resource(mut self, f: F) -> Self where F: FnOnce(Resource

    ) -> Resource, @@ -190,11 +200,11 @@ where self } - /// Register a scope middleware + /// Register a scope level middleware. /// - /// This is similar to `App's` middlewares, but - /// middleware is not allowed to change response type (i.e modify response's body). - /// Middleware get invoked on scope level. + /// This is similar to `App's` middlewares, but middleware get invoked on scope level. + /// Scope level middlewares are not allowed to change response + /// type (i.e modify response's body). pub fn middleware( self, mw: F, @@ -322,7 +332,7 @@ impl NewService for ScopeFactory

    { } } -/// Create app service +/// Create scope service #[doc(hidden)] pub struct ScopeFactoryResponse

    { fut: Vec>, diff --git a/src/test.rs b/src/test.rs index 4db268f14..13db59771 100644 --- a/src/test.rs +++ b/src/test.rs @@ -50,7 +50,7 @@ pub fn run_on(f: F) -> Result where F: Fn() -> Result, { - RT.with(move |rt| rt.borrow_mut().block_on(lazy(|| f()))) + RT.with(move |rt| rt.borrow_mut().block_on(lazy(f))) } /// This method accepts application builder instance, and constructs @@ -169,6 +169,7 @@ impl Default for TestRequest { } } +#[allow(clippy::wrong_self_convention)] impl TestRequest { /// Create TestRequest and set request uri pub fn with_uri(path: &str) -> TestRequest { @@ -254,7 +255,7 @@ impl TestRequest { } /// Set cookie for this request - pub fn cookie<'a>(mut self, cookie: Cookie<'a>) -> Self { + pub fn cookie(mut self, cookie: Cookie) -> Self { self.req.cookie(cookie); self } From 725ee3d3961f9a76105c45579d1e7e8c999fee3c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 21:43:48 -0700 Subject: [PATCH 296/427] rename extract to types --- src/{extract/mod.rs => extract.rs} | 13 +------------ src/lib.rs | 5 +++-- src/{extract => types}/form.rs | 0 src/{extract => types}/json.rs | 0 src/types/mod.rs | 13 +++++++++++++ src/{extract => types}/path.rs | 3 +-- src/{extract => types}/payload.rs | 0 src/{extract => types}/query.rs | 0 8 files changed, 18 insertions(+), 16 deletions(-) rename src/{extract/mod.rs => extract.rs} (97%) rename src/{extract => types}/form.rs (100%) rename src/{extract => types}/json.rs (100%) create mode 100644 src/types/mod.rs rename src/{extract => types}/path.rs (99%) rename src/{extract => types}/payload.rs (100%) rename src/{extract => types}/query.rs (100%) diff --git a/src/extract/mod.rs b/src/extract.rs similarity index 97% rename from src/extract/mod.rs rename to src/extract.rs index 738f89187..4cd04be2b 100644 --- a/src/extract/mod.rs +++ b/src/extract.rs @@ -6,18 +6,6 @@ use futures::{future, Async, Future, IntoFuture, Poll}; use crate::service::ServiceFromRequest; -mod form; -mod json; -mod path; -mod payload; -mod query; - -pub use self::form::{Form, FormConfig}; -pub use self::json::{Json, JsonConfig}; -pub use self::path::Path; -pub use self::payload::{Payload, PayloadConfig}; -pub use self::query::Query; - /// Trait implemented by types that can be extracted from request. /// /// Types that implement this trait can be used with `Route` handlers. @@ -250,6 +238,7 @@ mod tests { use super::*; use crate::test::{block_on, TestRequest}; + use crate::types::{Form, FormConfig, Path, Query}; #[derive(Deserialize, Debug, PartialEq)] struct Info { diff --git a/src/lib.rs b/src/lib.rs index 509fcc608..dc0493a81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ mod scope; mod server; mod service; pub mod test; +mod types; #[allow(unused_imports)] #[macro_use] @@ -93,9 +94,9 @@ pub mod web { use crate::scope::Scope; pub use crate::data::{Data, RouteData}; - pub use crate::extract::{Form, Json, Path, Payload, Query}; - pub use crate::extract::{FormConfig, JsonConfig, PayloadConfig}; pub use crate::request::HttpRequest; + pub use crate::types::{Form, Json, Path, Payload, Query}; + pub use crate::types::{FormConfig, JsonConfig, PayloadConfig}; /// Create resource for a specific path. /// diff --git a/src/extract/form.rs b/src/types/form.rs similarity index 100% rename from src/extract/form.rs rename to src/types/form.rs diff --git a/src/extract/json.rs b/src/types/json.rs similarity index 100% rename from src/extract/json.rs rename to src/types/json.rs diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 000000000..b5f8de603 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,13 @@ +//! Helper types + +mod form; +mod json; +mod path; +mod payload; +mod query; + +pub use self::form::{Form, FormConfig}; +pub use self::json::{Json, JsonConfig}; +pub use self::path::Path; +pub use self::payload::{Payload, PayloadConfig}; +pub use self::query::Query; diff --git a/src/extract/path.rs b/src/types/path.rs similarity index 99% rename from src/extract/path.rs rename to src/types/path.rs index fc6811c78..4e6784794 100644 --- a/src/extract/path.rs +++ b/src/types/path.rs @@ -8,8 +8,7 @@ use serde::de; use crate::request::HttpRequest; use crate::service::ServiceFromRequest; - -use super::FromRequest; +use crate::FromRequest; #[derive(PartialEq, Eq, PartialOrd, Ord)] /// Extract typed information from the request's path. diff --git a/src/extract/payload.rs b/src/types/payload.rs similarity index 100% rename from src/extract/payload.rs rename to src/types/payload.rs diff --git a/src/extract/query.rs b/src/types/query.rs similarity index 100% rename from src/extract/query.rs rename to src/types/query.rs From c80884904ccc66ba926a2da69127e8e1d08ddb7b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 22:04:09 -0700 Subject: [PATCH 297/427] move JsonBody from actix-http --- src/lib.rs | 4 +- src/types/json.rs | 195 ++++++++++++++++++++++++++++++++++++++++++++-- src/types/mod.rs | 2 +- 3 files changed, 192 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index dc0493a81..18cf93f43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,6 +58,7 @@ pub mod dev { pub use crate::service::{ HttpServiceFactory, ServiceFromRequest, ServiceRequest, ServiceResponse, }; + pub use crate::types::json::JsonBody; pub use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder; @@ -95,8 +96,7 @@ pub mod web { pub use crate::data::{Data, RouteData}; pub use crate::request::HttpRequest; - pub use crate::types::{Form, Json, Path, Payload, Query}; - pub use crate::types::{FormConfig, JsonConfig, PayloadConfig}; + pub use crate::types::*; /// Create resource for a specific path. /// diff --git a/src/types/json.rs b/src/types/json.rs index 92b7f20f6..74ee5eb21 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -3,16 +3,15 @@ use std::rc::Rc; use std::{fmt, ops}; -use bytes::Bytes; -use futures::{Future, Stream}; +use bytes::{Bytes, BytesMut}; +use futures::{Future, Poll, Stream}; use serde::de::DeserializeOwned; use serde::Serialize; use serde_json; -use actix_http::dev::JsonBody; -use actix_http::error::{Error, JsonPayloadError}; -use actix_http::http::StatusCode; -use actix_http::Response; +use actix_http::error::{Error, JsonPayloadError, PayloadError}; +use actix_http::http::{header::CONTENT_LENGTH, StatusCode}; +use actix_http::{HttpMessage, Payload, Response}; use crate::extract::FromRequest; use crate::request::HttpRequest; @@ -257,3 +256,187 @@ impl Default for JsonConfig { } } } + +/// Request's payload json parser, it resolves to a deserialized `T` value. +/// This future could be used with `ServiceRequest` and `ServiceFromRequest`. +/// +/// Returns error: +/// +/// * content type is not `application/json` +/// * content length is greater than 256k +pub struct JsonBody { + limit: usize, + length: Option, + stream: Payload, + err: Option, + fut: Option>>, +} + +impl JsonBody +where + T: HttpMessage, + T::Stream: Stream + 'static, + U: DeserializeOwned + 'static, +{ + /// Create `JsonBody` for request. + pub fn new(req: &mut T) -> Self { + // check content-type + let json = if let Ok(Some(mime)) = req.mime_type() { + mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) + } else { + false + }; + if !json { + return JsonBody { + limit: 262_144, + length: None, + stream: Payload::None, + fut: None, + err: Some(JsonPayloadError::ContentType), + }; + } + + let mut len = None; + if let Some(l) = req.headers().get(CONTENT_LENGTH) { + if let Ok(s) = l.to_str() { + if let Ok(l) = s.parse::() { + len = Some(l) + } + } + } + + JsonBody { + limit: 262_144, + length: len, + stream: req.take_payload(), + fut: None, + err: None, + } + } + + /// Change max size of payload. By default max size is 256Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } +} + +impl Future for JsonBody +where + T: HttpMessage, + T::Stream: Stream + 'static, + U: DeserializeOwned + 'static, +{ + type Item = U; + type Error = JsonPayloadError; + + fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.fut { + return fut.poll(); + } + + if let Some(err) = self.err.take() { + return Err(err); + } + + let limit = self.limit; + if let Some(len) = self.length.take() { + if len > limit { + return Err(JsonPayloadError::Overflow); + } + } + + let fut = std::mem::replace(&mut self.stream, Payload::None) + .from_err() + .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { + if (body.len() + chunk.len()) > limit { + Err(JsonPayloadError::Overflow) + } else { + body.extend_from_slice(&chunk); + Ok(body) + } + }) + .and_then(|body| Ok(serde_json::from_slice::(&body)?)); + self.fut = Some(Box::new(fut)); + self.poll() + } +} + +#[cfg(test)] +mod tests { + use bytes::Bytes; + use serde_derive::{Deserialize, Serialize}; + + use super::*; + use crate::http::header; + use crate::test::{block_on, TestRequest}; + + fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { + match err { + JsonPayloadError::Overflow => match other { + JsonPayloadError::Overflow => true, + _ => false, + }, + JsonPayloadError::ContentType => match other { + JsonPayloadError::ContentType => true, + _ => false, + }, + _ => false, + } + } + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct MyObject { + name: String, + } + + #[test] + fn test_json_body() { + let mut req = TestRequest::default().to_request(); + let json = block_on(req.json::()); + assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); + + let mut req = TestRequest::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/text"), + ) + .to_request(); + let json = block_on(req.json::()); + assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); + + let mut req = TestRequest::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ) + .header( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("10000"), + ) + .to_request(); + + let json = block_on(req.json::().limit(100)); + assert!(json_eq(json.err().unwrap(), JsonPayloadError::Overflow)); + + let mut req = TestRequest::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ) + .header( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + ) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .to_request(); + + let json = block_on(req.json::()); + assert_eq!( + json.ok().unwrap(), + MyObject { + name: "test".to_owned() + } + ); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index b5f8de603..2fc3ca938 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,7 +1,7 @@ //! Helper types mod form; -mod json; +pub(crate) mod json; mod path; mod payload; mod query; From fd141ef9b109c11ba6fb7007b261364037608ed8 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 22:10:15 -0700 Subject: [PATCH 298/427] move json to actix-web --- src/httpmessage.rs | 40 -------- src/json.rs | 222 --------------------------------------------- src/lib.rs | 2 - 3 files changed, 264 deletions(-) delete mode 100644 src/json.rs diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 3c049c09b..8573e917d 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -19,7 +19,6 @@ use crate::error::{ }; use crate::extensions::Extensions; use crate::header::Header; -use crate::json::JsonBody; use crate::payload::Payload; struct Cookies(Vec>); @@ -219,45 +218,6 @@ pub trait HttpMessage: Sized { UrlEncoded::new(self) } - /// Parse `application/json` encoded body. - /// Return `JsonBody` future. It resolves to a `T` value. - /// - /// Returns error: - /// - /// * content type is not `application/json` - /// * content length is greater than 256k - /// - /// ## Server example - /// - /// ```rust,ignore - /// # extern crate actix_web; - /// # extern crate futures; - /// # #[macro_use] extern crate serde_derive; - /// use actix_web::*; - /// use futures::future::{ok, Future}; - /// - /// #[derive(Deserialize, Debug)] - /// struct MyObj { - /// name: String, - /// } - /// - /// fn index(mut req: HttpRequest) -> Box> { - /// req.json() // <- get JsonBody future - /// .from_err() - /// .and_then(|val: MyObj| { // <- deserialized value - /// println!("==== BODY ==== {:?}", val); - /// Ok(Response::Ok().into()) - /// }).responder() - /// } - /// # fn main() {} - /// ``` - fn json(&mut self) -> JsonBody - where - Self::Stream: Stream + 'static, - { - JsonBody::new(self) - } - /// Return stream of lines. fn readlines(&mut self) -> Readlines where diff --git a/src/json.rs b/src/json.rs deleted file mode 100644 index 026ecb871..000000000 --- a/src/json.rs +++ /dev/null @@ -1,222 +0,0 @@ -use bytes::BytesMut; -use futures::{Future, Poll, Stream}; -use http::header::CONTENT_LENGTH; - -use bytes::Bytes; -use mime; -use serde::de::DeserializeOwned; -use serde_json; - -use crate::error::{JsonPayloadError, PayloadError}; -use crate::httpmessage::HttpMessage; -use crate::payload::Payload; - -/// Request payload json parser that resolves to a deserialized `T` value. -/// -/// Returns error: -/// -/// * content type is not `application/json` -/// * content length is greater than 256k -/// -/// # Server example -/// -/// ```rust,ignore -/// # extern crate actix_web; -/// # extern crate futures; -/// # #[macro_use] extern crate serde_derive; -/// use actix_web::{AsyncResponder, Error, HttpMessage, HttpRequest, Response}; -/// use futures::future::Future; -/// -/// #[derive(Deserialize, Debug)] -/// struct MyObj { -/// name: String, -/// } -/// -/// fn index(mut req: HttpRequest) -> Box> { -/// req.json() // <- get JsonBody future -/// .from_err() -/// .and_then(|val: MyObj| { // <- deserialized value -/// println!("==== BODY ==== {:?}", val); -/// Ok(Response::Ok().into()) -/// }).responder() -/// } -/// # fn main() {} -/// ``` -pub struct JsonBody { - limit: usize, - length: Option, - stream: Payload, - err: Option, - fut: Option>>, -} - -impl JsonBody -where - T: HttpMessage, - T::Stream: Stream + 'static, - U: DeserializeOwned + 'static, -{ - /// Create `JsonBody` for request. - pub fn new(req: &mut T) -> Self { - // check content-type - let json = if let Ok(Some(mime)) = req.mime_type() { - mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) - } else { - false - }; - if !json { - return JsonBody { - limit: 262_144, - length: None, - stream: Payload::None, - fut: None, - err: Some(JsonPayloadError::ContentType), - }; - } - - let mut len = None; - if let Some(l) = req.headers().get(CONTENT_LENGTH) { - if let Ok(s) = l.to_str() { - if let Ok(l) = s.parse::() { - len = Some(l) - } - } - } - - JsonBody { - limit: 262_144, - length: len, - stream: req.take_payload(), - fut: None, - err: None, - } - } - - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } -} - -impl Future for JsonBody -where - T: HttpMessage, - T::Stream: Stream + 'static, - U: DeserializeOwned + 'static, -{ - type Item = U; - type Error = JsonPayloadError; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut { - return fut.poll(); - } - - if let Some(err) = self.err.take() { - return Err(err); - } - - let limit = self.limit; - if let Some(len) = self.length.take() { - if len > limit { - return Err(JsonPayloadError::Overflow); - } - } - - let fut = std::mem::replace(&mut self.stream, Payload::None) - .from_err() - .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { - if (body.len() + chunk.len()) > limit { - Err(JsonPayloadError::Overflow) - } else { - body.extend_from_slice(&chunk); - Ok(body) - } - }) - .and_then(|body| Ok(serde_json::from_slice::(&body)?)); - self.fut = Some(Box::new(fut)); - self.poll() - } -} - -#[cfg(test)] -mod tests { - use bytes::Bytes; - use futures::Async; - use http::header; - use serde_derive::{Deserialize, Serialize}; - - use super::*; - use crate::test::TestRequest; - - impl PartialEq for JsonPayloadError { - fn eq(&self, other: &JsonPayloadError) -> bool { - match *self { - JsonPayloadError::Overflow => match *other { - JsonPayloadError::Overflow => true, - _ => false, - }, - JsonPayloadError::ContentType => match *other { - JsonPayloadError::ContentType => true, - _ => false, - }, - _ => false, - } - } - } - - #[derive(Serialize, Deserialize, PartialEq, Debug)] - struct MyObject { - name: String, - } - - #[test] - fn test_json_body() { - let mut req = TestRequest::default().finish(); - let mut json = req.json::(); - assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); - - let mut req = TestRequest::default() - .header( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/text"), - ) - .finish(); - let mut json = req.json::(); - assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); - - let mut req = TestRequest::default() - .header( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - ) - .header( - header::CONTENT_LENGTH, - header::HeaderValue::from_static("10000"), - ) - .finish(); - let mut json = req.json::().limit(100); - assert_eq!(json.poll().err().unwrap(), JsonPayloadError::Overflow); - - let mut req = TestRequest::default() - .header( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - ) - .header( - header::CONTENT_LENGTH, - header::HeaderValue::from_static("16"), - ) - .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) - .finish(); - - let mut json = req.json::(); - assert_eq!( - json.poll().ok().unwrap(), - Async::Ready(MyObject { - name: "test".to_owned() - }) - ); - } -} diff --git a/src/lib.rs b/src/lib.rs index 41ee55fec..443266a9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,7 +77,6 @@ mod header; mod helpers; mod httpcodes; pub mod httpmessage; -mod json; mod message; mod payload; mod request; @@ -113,7 +112,6 @@ pub mod dev { //! ``` pub use crate::httpmessage::{MessageBody, Readlines, UrlEncoded}; - pub use crate::json::JsonBody; pub use crate::response::ResponseBuilder; } From 9012c46fe1e2ced25480e3aa4fd200899368e81a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 00:48:40 -0700 Subject: [PATCH 299/427] move payload futures from actix-http --- Cargo.toml | 2 +- src/error.rs | 101 +++++++++++++++++ src/lib.rs | 3 + src/types/form.rs | 238 ++++++++++++++++++++++++++++++++++++++--- src/types/json.rs | 8 +- src/types/mod.rs | 5 +- src/types/payload.rs | 145 ++++++++++++++++++++++++- src/types/readlines.rs | 210 ++++++++++++++++++++++++++++++++++++ 8 files changed, 688 insertions(+), 24 deletions(-) create mode 100644 src/types/readlines.rs diff --git a/Cargo.toml b/Cargo.toml index 87a54b6ad..ba36dd2c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,7 @@ mime = "0.3" net2 = "0.2.33" parking_lot = "0.7" regex = "1.0" -serde = "1.0" +serde = { version = "1.0", features=["derive"] } serde_json = "1.0" serde_urlencoded = "^0.5.3" time = "0.1" diff --git a/src/error.rs b/src/error.rs index 068407086..bf224a223 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,8 +3,12 @@ use std::fmt; pub use actix_http::error::*; use derive_more::{Display, From}; +use serde_json::error::Error as JsonError; use url::ParseError as UrlParseError; +use crate::http::StatusCode; +use crate::HttpResponse; + /// Errors which can occur when attempting to generate resource uri. #[derive(Debug, PartialEq, Display, From)] pub enum UrlGenerationError { @@ -41,3 +45,100 @@ impl From> for BlockingError } } } + +/// A set of errors that can occur during parsing urlencoded payloads +#[derive(Debug, Display, From)] +pub enum UrlencodedError { + /// Can not decode chunked transfer encoding + #[display(fmt = "Can not decode chunked transfer encoding")] + Chunked, + /// Payload size is bigger than allowed. (default: 256kB) + #[display(fmt = "Urlencoded payload size is bigger than allowed. (default: 256kB)")] + Overflow, + /// Payload size is now known + #[display(fmt = "Payload size is now known")] + UnknownLength, + /// Content type error + #[display(fmt = "Content type error")] + ContentType, + /// Parse error + #[display(fmt = "Parse error")] + Parse, + /// Payload error + #[display(fmt = "Error that occur during reading payload: {}", _0)] + Payload(PayloadError), +} + +/// Return `BadRequest` for `UrlencodedError` +impl ResponseError for UrlencodedError { + fn error_response(&self) -> HttpResponse { + match *self { + UrlencodedError::Overflow => { + HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) + } + UrlencodedError::UnknownLength => { + HttpResponse::new(StatusCode::LENGTH_REQUIRED) + } + _ => HttpResponse::new(StatusCode::BAD_REQUEST), + } + } +} + +/// A set of errors that can occur during parsing json payloads +#[derive(Debug, Display, From)] +pub enum JsonPayloadError { + /// Payload size is bigger than allowed. (default: 256kB) + #[display(fmt = "Json payload size is bigger than allowed. (default: 256kB)")] + Overflow, + /// Content type error + #[display(fmt = "Content type error")] + ContentType, + /// Deserialize error + #[display(fmt = "Json deserialize error: {}", _0)] + Deserialize(JsonError), + /// Payload error + #[display(fmt = "Error that occur during reading payload: {}", _0)] + Payload(PayloadError), +} + +/// Return `BadRequest` for `UrlencodedError` +impl ResponseError for JsonPayloadError { + fn error_response(&self) -> HttpResponse { + match *self { + JsonPayloadError::Overflow => { + HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) + } + _ => HttpResponse::new(StatusCode::BAD_REQUEST), + } + } +} + +/// Error type returned when reading body as lines. +#[derive(From, Display, Debug)] +pub enum ReadlinesError { + /// Error when decoding a line. + #[display(fmt = "Encoding error")] + /// Payload size is bigger than allowed. (default: 256kB) + EncodingError, + /// Payload error. + #[display(fmt = "Error that occur during reading payload: {}", _0)] + Payload(PayloadError), + /// Line limit exceeded. + #[display(fmt = "Line limit exceeded")] + LimitOverflow, + /// ContentType error. + #[display(fmt = "Content-type error")] + ContentTypeError(ContentTypeError), +} + +/// Return `BadRequest` for `ReadlinesError` +impl ResponseError for ReadlinesError { + fn error_response(&self) -> HttpResponse { + match *self { + ReadlinesError::LimitOverflow => { + HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) + } + _ => HttpResponse::new(StatusCode::BAD_REQUEST), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 18cf93f43..d6bcf4e32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,10 @@ pub mod dev { pub use crate::service::{ HttpServiceFactory, ServiceFromRequest, ServiceRequest, ServiceResponse, }; + pub use crate::types::form::UrlEncoded; pub use crate::types::json::JsonBody; + pub use crate::types::payload::HttpMessageBody; + pub use crate::types::readlines::Readlines; pub use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder; diff --git a/src/types/form.rs b/src/types/form.rs index 4a5e97299..58fa37611 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -3,13 +3,17 @@ use std::rc::Rc; use std::{fmt, ops}; -use actix_http::dev::UrlEncoded; -use actix_http::error::{Error, UrlencodedError}; -use bytes::Bytes; -use futures::{Future, Stream}; +use actix_http::error::{Error, PayloadError, UrlencodedError}; +use actix_http::{HttpMessage, Payload}; +use bytes::{Bytes, BytesMut}; +use encoding::all::UTF_8; +use encoding::types::{DecoderTrap, Encoding}; +use encoding::EncodingRef; +use futures::{Future, Poll, Stream}; use serde::de::DeserializeOwned; use crate::extract::FromRequest; +use crate::http::header::CONTENT_LENGTH; use crate::request::HttpRequest; use crate::service::ServiceFromRequest; @@ -167,13 +171,145 @@ impl Default for FormConfig { } } +/// Future that resolves to a parsed urlencoded values. +/// +/// Parse `application/x-www-form-urlencoded` encoded request's body. +/// Return `UrlEncoded` future. Form can be deserialized to any type that +/// implements `Deserialize` trait from *serde*. +/// +/// Returns error: +/// +/// * content type is not `application/x-www-form-urlencoded` +/// * content-length is greater than 32k +/// +pub struct UrlEncoded { + stream: Payload, + limit: usize, + length: Option, + encoding: EncodingRef, + err: Option, + fut: Option>>, +} + +impl UrlEncoded +where + T: HttpMessage, + T::Stream: Stream, +{ + /// Create a new future to URL encode a request + pub fn new(req: &mut T) -> UrlEncoded { + // check content type + if req.content_type().to_lowercase() != "application/x-www-form-urlencoded" { + return Self::err(UrlencodedError::ContentType); + } + let encoding = match req.encoding() { + Ok(enc) => enc, + Err(_) => return Self::err(UrlencodedError::ContentType), + }; + + let mut len = None; + if let Some(l) = req.headers().get(CONTENT_LENGTH) { + if let Ok(s) = l.to_str() { + if let Ok(l) = s.parse::() { + len = Some(l) + } else { + return Self::err(UrlencodedError::UnknownLength); + } + } else { + return Self::err(UrlencodedError::UnknownLength); + } + }; + + UrlEncoded { + encoding, + stream: req.take_payload(), + limit: 32_768, + length: len, + fut: None, + err: None, + } + } + + fn err(e: UrlencodedError) -> Self { + UrlEncoded { + stream: Payload::None, + limit: 32_768, + fut: None, + err: Some(e), + length: None, + encoding: UTF_8, + } + } + + /// Change max size of payload. By default max size is 256Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } +} + +impl Future for UrlEncoded +where + T: HttpMessage, + T::Stream: Stream + 'static, + U: DeserializeOwned + 'static, +{ + type Item = U; + type Error = UrlencodedError; + + fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.fut { + return fut.poll(); + } + + if let Some(err) = self.err.take() { + return Err(err); + } + + // payload size + let limit = self.limit; + if let Some(len) = self.length.take() { + if len > limit { + return Err(UrlencodedError::Overflow); + } + } + + // future + let encoding = self.encoding; + let fut = std::mem::replace(&mut self.stream, Payload::None) + .from_err() + .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { + if (body.len() + chunk.len()) > limit { + Err(UrlencodedError::Overflow) + } else { + body.extend_from_slice(&chunk); + Ok(body) + } + }) + .and_then(move |body| { + if (encoding as *const Encoding) == UTF_8 { + serde_urlencoded::from_bytes::(&body) + .map_err(|_| UrlencodedError::Parse) + } else { + let body = encoding + .decode(&body, DecoderTrap::Strict) + .map_err(|_| UrlencodedError::Parse)?; + serde_urlencoded::from_str::(&body) + .map_err(|_| UrlencodedError::Parse) + } + }); + self.fut = Some(Box::new(fut)); + self.poll() + } +} + #[cfg(test)] mod tests { - use actix_http::http::header; use bytes::Bytes; - use serde_derive::Deserialize; + use serde::Deserialize; use super::*; + use crate::http::header::CONTENT_TYPE; use crate::test::{block_on, TestRequest}; #[derive(Deserialize, Debug, PartialEq)] @@ -183,15 +319,91 @@ mod tests { #[test] fn test_form() { - let mut req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ) - .header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .to_from(); + let mut req = + TestRequest::with_header(CONTENT_TYPE, "application/x-www-form-urlencoded") + .header(CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .to_from(); let s = block_on(Form::::from_request(&mut req)).unwrap(); assert_eq!(s.hello, "world"); } + + fn eq(err: UrlencodedError, other: UrlencodedError) -> bool { + match err { + UrlencodedError::Chunked => match other { + UrlencodedError::Chunked => true, + _ => false, + }, + UrlencodedError::Overflow => match other { + UrlencodedError::Overflow => true, + _ => false, + }, + UrlencodedError::UnknownLength => match other { + UrlencodedError::UnknownLength => true, + _ => false, + }, + UrlencodedError::ContentType => match other { + UrlencodedError::ContentType => true, + _ => false, + }, + _ => false, + } + } + + #[test] + fn test_urlencoded_error() { + let mut req = + TestRequest::with_header(CONTENT_TYPE, "application/x-www-form-urlencoded") + .header(CONTENT_LENGTH, "xxxx") + .to_request(); + let info = block_on(UrlEncoded::<_, Info>::new(&mut req)); + assert!(eq(info.err().unwrap(), UrlencodedError::UnknownLength)); + + let mut req = + TestRequest::with_header(CONTENT_TYPE, "application/x-www-form-urlencoded") + .header(CONTENT_LENGTH, "1000000") + .to_request(); + let info = block_on(UrlEncoded::<_, Info>::new(&mut req)); + assert!(eq(info.err().unwrap(), UrlencodedError::Overflow)); + + let mut req = TestRequest::with_header(CONTENT_TYPE, "text/plain") + .header(CONTENT_LENGTH, "10") + .to_request(); + let info = block_on(UrlEncoded::<_, Info>::new(&mut req)); + assert!(eq(info.err().unwrap(), UrlencodedError::ContentType)); + } + + #[test] + fn test_urlencoded() { + let mut req = + TestRequest::with_header(CONTENT_TYPE, "application/x-www-form-urlencoded") + .header(CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .to_request(); + + let info = block_on(UrlEncoded::<_, Info>::new(&mut req)).unwrap(); + assert_eq!( + info, + Info { + hello: "world".to_owned() + } + ); + + let mut req = TestRequest::with_header( + CONTENT_TYPE, + "application/x-www-form-urlencoded; charset=utf-8", + ) + .header(CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .to_request(); + + let info = block_on(UrlEncoded::<_, Info>::new(&mut req)).unwrap(); + assert_eq!( + info, + Info { + hello: "world".to_owned() + } + ); + } } diff --git a/src/types/json.rs b/src/types/json.rs index 74ee5eb21..18a6be909 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -393,7 +393,7 @@ mod tests { #[test] fn test_json_body() { let mut req = TestRequest::default().to_request(); - let json = block_on(req.json::()); + let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); let mut req = TestRequest::default() @@ -402,7 +402,7 @@ mod tests { header::HeaderValue::from_static("application/text"), ) .to_request(); - let json = block_on(req.json::()); + let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); let mut req = TestRequest::default() @@ -416,7 +416,7 @@ mod tests { ) .to_request(); - let json = block_on(req.json::().limit(100)); + let json = block_on(JsonBody::<_, MyObject>::new(&mut req).limit(100)); assert!(json_eq(json.err().unwrap(), JsonPayloadError::Overflow)); let mut req = TestRequest::default() @@ -431,7 +431,7 @@ mod tests { .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .to_request(); - let json = block_on(req.json::()); + let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); assert_eq!( json.ok().unwrap(), MyObject { diff --git a/src/types/mod.rs b/src/types/mod.rs index 2fc3ca938..30ee73091 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,10 +1,11 @@ //! Helper types -mod form; +pub(crate) mod form; pub(crate) mod json; mod path; -mod payload; +pub(crate) mod payload; mod query; +pub(crate) mod readlines; pub use self::form::{Form, FormConfig}; pub use self::json::{Json, JsonConfig}; diff --git a/src/types/payload.rs b/src/types/payload.rs index 7164a544f..402486b66 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -1,10 +1,9 @@ //! Payload/Bytes/String extractors use std::str; -use actix_http::dev::MessageBody; use actix_http::error::{Error, ErrorBadRequest, PayloadError}; use actix_http::HttpMessage; -use bytes::Bytes; +use bytes::{Bytes, BytesMut}; use encoding::all::UTF_8; use encoding::types::{DecoderTrap, Encoding}; use futures::future::{err, Either, FutureResult}; @@ -12,6 +11,7 @@ use futures::{Future, Poll, Stream}; use mime::Mime; use crate::extract::FromRequest; +use crate::http::header; use crate::service::ServiceFromRequest; /// Payload extractor returns request 's payload stream. @@ -152,7 +152,7 @@ where } let limit = cfg.limit; - Either::A(Box::new(MessageBody::new(req).limit(limit).from_err())) + Either::A(Box::new(HttpMessageBody::new(req).limit(limit).from_err())) } } @@ -213,7 +213,7 @@ where let limit = cfg.limit; Either::A(Box::new( - MessageBody::new(req) + HttpMessageBody::new(req) .limit(limit) .from_err() .and_then(move |body| { @@ -287,6 +287,109 @@ impl Default for PayloadConfig { } } +/// Future that resolves to a complete http message body. +/// +/// Load http message body. +/// +/// By default only 256Kb payload reads to a memory, then +/// `PayloadError::Overflow` get returned. Use `MessageBody::limit()` +/// method to change upper limit. +pub struct HttpMessageBody { + limit: usize, + length: Option, + stream: actix_http::Payload, + err: Option, + fut: Option>>, +} + +impl HttpMessageBody +where + T: HttpMessage, + T::Stream: Stream, +{ + /// Create `MessageBody` for request. + pub fn new(req: &mut T) -> HttpMessageBody { + let mut len = None; + if let Some(l) = req.headers().get(header::CONTENT_LENGTH) { + if let Ok(s) = l.to_str() { + if let Ok(l) = s.parse::() { + len = Some(l) + } else { + return Self::err(PayloadError::UnknownLength); + } + } else { + return Self::err(PayloadError::UnknownLength); + } + } + + HttpMessageBody { + stream: req.take_payload(), + limit: 262_144, + length: len, + fut: None, + err: None, + } + } + + /// Change max size of payload. By default max size is 256Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + fn err(e: PayloadError) -> Self { + HttpMessageBody { + stream: actix_http::Payload::None, + limit: 262_144, + fut: None, + err: Some(e), + length: None, + } + } +} + +impl Future for HttpMessageBody +where + T: HttpMessage, + T::Stream: Stream + 'static, +{ + type Item = Bytes; + type Error = PayloadError; + + fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.fut { + return fut.poll(); + } + + if let Some(err) = self.err.take() { + return Err(err); + } + + if let Some(len) = self.length.take() { + if len > self.limit { + return Err(PayloadError::Overflow); + } + } + + // future + let limit = self.limit; + self.fut = Some(Box::new( + std::mem::replace(&mut self.stream, actix_http::Payload::None) + .from_err() + .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { + if (body.len() + chunk.len()) > limit { + Err(PayloadError::Overflow) + } else { + body.extend_from_slice(&chunk); + Ok(body) + } + }) + .map(|body| body.freeze()), + )); + self.poll() + } +} + #[cfg(test)] mod tests { use bytes::Bytes; @@ -332,4 +435,38 @@ mod tests { let s = block_on(String::from_request(&mut req)).unwrap(); assert_eq!(s, "hello=world"); } + + #[test] + fn test_message_body() { + let mut req = + TestRequest::with_header(header::CONTENT_LENGTH, "xxxx").to_request(); + let res = block_on(HttpMessageBody::new(&mut req)); + match res.err().unwrap() { + PayloadError::UnknownLength => (), + _ => unreachable!("error"), + } + + let mut req = + TestRequest::with_header(header::CONTENT_LENGTH, "1000000").to_request(); + let res = block_on(HttpMessageBody::new(&mut req)); + match res.err().unwrap() { + PayloadError::Overflow => (), + _ => unreachable!("error"), + } + + let mut req = TestRequest::default() + .set_payload(Bytes::from_static(b"test")) + .to_request(); + let res = block_on(HttpMessageBody::new(&mut req)); + assert_eq!(res.ok().unwrap(), Bytes::from_static(b"test")); + + let mut req = TestRequest::default() + .set_payload(Bytes::from_static(b"11111111111111")) + .to_request(); + let res = block_on(HttpMessageBody::new(&mut req).limit(5)); + match res.err().unwrap() { + PayloadError::Overflow => (), + _ => unreachable!("error"), + } + } } diff --git a/src/types/readlines.rs b/src/types/readlines.rs new file mode 100644 index 000000000..2c7f699a7 --- /dev/null +++ b/src/types/readlines.rs @@ -0,0 +1,210 @@ +use std::str; + +use bytes::{Bytes, BytesMut}; +use encoding::all::UTF_8; +use encoding::types::{DecoderTrap, Encoding}; +use encoding::EncodingRef; +use futures::{Async, Poll, Stream}; + +use crate::dev::Payload; +use crate::error::{PayloadError, ReadlinesError}; +use crate::HttpMessage; + +/// Stream to read request line by line. +pub struct Readlines { + stream: Payload, + buff: BytesMut, + limit: usize, + checked_buff: bool, + encoding: EncodingRef, + err: Option, +} + +impl Readlines +where + T: HttpMessage, + T::Stream: Stream, +{ + /// Create a new stream to read request line by line. + pub fn new(req: &mut T) -> Self { + let encoding = match req.encoding() { + Ok(enc) => enc, + Err(err) => return Self::err(err.into()), + }; + + Readlines { + stream: req.take_payload(), + buff: BytesMut::with_capacity(262_144), + limit: 262_144, + checked_buff: true, + err: None, + encoding, + } + } + + /// Change max line size. By default max size is 256Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + fn err(err: ReadlinesError) -> Self { + Readlines { + stream: Payload::None, + buff: BytesMut::new(), + limit: 262_144, + checked_buff: true, + encoding: UTF_8, + err: Some(err), + } + } +} + +impl Stream for Readlines +where + T: HttpMessage, + T::Stream: Stream, +{ + type Item = String; + type Error = ReadlinesError; + + fn poll(&mut self) -> Poll, Self::Error> { + if let Some(err) = self.err.take() { + return Err(err); + } + + // check if there is a newline in the buffer + if !self.checked_buff { + let mut found: Option = None; + for (ind, b) in self.buff.iter().enumerate() { + if *b == b'\n' { + found = Some(ind); + break; + } + } + if let Some(ind) = found { + // check if line is longer than limit + if ind + 1 > self.limit { + return Err(ReadlinesError::LimitOverflow); + } + let enc: *const Encoding = self.encoding as *const Encoding; + let line = if enc == UTF_8 { + str::from_utf8(&self.buff.split_to(ind + 1)) + .map_err(|_| ReadlinesError::EncodingError)? + .to_owned() + } else { + self.encoding + .decode(&self.buff.split_to(ind + 1), DecoderTrap::Strict) + .map_err(|_| ReadlinesError::EncodingError)? + }; + return Ok(Async::Ready(Some(line))); + } + self.checked_buff = true; + } + // poll req for more bytes + match self.stream.poll() { + Ok(Async::Ready(Some(mut bytes))) => { + // check if there is a newline in bytes + let mut found: Option = None; + for (ind, b) in bytes.iter().enumerate() { + if *b == b'\n' { + found = Some(ind); + break; + } + } + if let Some(ind) = found { + // check if line is longer than limit + if ind + 1 > self.limit { + return Err(ReadlinesError::LimitOverflow); + } + let enc: *const Encoding = self.encoding as *const Encoding; + let line = if enc == UTF_8 { + str::from_utf8(&bytes.split_to(ind + 1)) + .map_err(|_| ReadlinesError::EncodingError)? + .to_owned() + } else { + self.encoding + .decode(&bytes.split_to(ind + 1), DecoderTrap::Strict) + .map_err(|_| ReadlinesError::EncodingError)? + }; + // extend buffer with rest of the bytes; + self.buff.extend_from_slice(&bytes); + self.checked_buff = false; + return Ok(Async::Ready(Some(line))); + } + self.buff.extend_from_slice(&bytes); + Ok(Async::NotReady) + } + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(None)) => { + if self.buff.is_empty() { + return Ok(Async::Ready(None)); + } + if self.buff.len() > self.limit { + return Err(ReadlinesError::LimitOverflow); + } + let enc: *const Encoding = self.encoding as *const Encoding; + let line = if enc == UTF_8 { + str::from_utf8(&self.buff) + .map_err(|_| ReadlinesError::EncodingError)? + .to_owned() + } else { + self.encoding + .decode(&self.buff, DecoderTrap::Strict) + .map_err(|_| ReadlinesError::EncodingError)? + }; + self.buff.clear(); + Ok(Async::Ready(Some(line))) + } + Err(e) => Err(ReadlinesError::from(e)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::{block_on, TestRequest}; + + #[test] + fn test_readlines() { + let mut req = TestRequest::default() + .set_payload(Bytes::from_static( + b"Lorem Ipsum is simply dummy text of the printing and typesetting\n\ + industry. Lorem Ipsum has been the industry's standard dummy\n\ + Contrary to popular belief, Lorem Ipsum is not simply random text.", + )) + .to_request(); + let stream = match block_on(Readlines::new(&mut req).into_future()) { + Ok((Some(s), stream)) => { + assert_eq!( + s, + "Lorem Ipsum is simply dummy text of the printing and typesetting\n" + ); + stream + } + _ => unreachable!("error"), + }; + + let stream = match block_on(stream.into_future()) { + Ok((Some(s), stream)) => { + assert_eq!( + s, + "industry. Lorem Ipsum has been the industry's standard dummy\n" + ); + stream + } + _ => unreachable!("error"), + }; + + match block_on(stream.into_future()) { + Ok((Some(s), stream)) => { + assert_eq!( + s, + "Contrary to popular belief, Lorem Ipsum is not simply random text." + ); + } + _ => unreachable!("error"), + } + } +} From fa66a07ec5070621292b411ab48b501cad18002e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 01:02:51 -0700 Subject: [PATCH 300/427] move httpmessage futures to actix-web --- examples/echo.rs | 27 +- examples/echo2.rs | 25 +- src/error.rs | 74 ----- src/httpmessage.rs | 627 +------------------------------------------ src/lib.rs | 17 +- tests/test_client.rs | 22 +- tests/test_server.rs | 54 ++-- 7 files changed, 84 insertions(+), 762 deletions(-) diff --git a/examples/echo.rs b/examples/echo.rs index 8ec0e6a97..c36292c44 100644 --- a/examples/echo.rs +++ b/examples/echo.rs @@ -1,10 +1,9 @@ use std::{env, io}; -use actix_http::HttpMessage; -use actix_http::{HttpService, Request, Response}; +use actix_http::{error::PayloadError, HttpService, Request, Response}; use actix_server::Server; -use bytes::Bytes; -use futures::Future; +use bytes::BytesMut; +use futures::{Future, Stream}; use http::header::HeaderValue; use log::info; @@ -18,12 +17,20 @@ fn main() -> io::Result<()> { .client_timeout(1000) .client_disconnect(1000) .finish(|mut req: Request| { - req.body().limit(512).and_then(|bytes: Bytes| { - info!("request body: {:?}", bytes); - let mut res = Response::Ok(); - res.header("x-head", HeaderValue::from_static("dummy value!")); - Ok(res.body(bytes)) - }) + req.take_payload() + .fold(BytesMut::new(), move |mut body, chunk| { + body.extend_from_slice(&chunk); + Ok::<_, PayloadError>(body) + }) + .and_then(|bytes| { + info!("request body: {:?}", bytes); + let mut res = Response::Ok(); + res.header( + "x-head", + HeaderValue::from_static("dummy value!"), + ); + Ok(res.body(bytes)) + }) }) })? .run() diff --git a/examples/echo2.rs b/examples/echo2.rs index 101adc1cf..b239796b4 100644 --- a/examples/echo2.rs +++ b/examples/echo2.rs @@ -1,20 +1,25 @@ use std::{env, io}; use actix_http::http::HeaderValue; -use actix_http::HttpMessage; -use actix_http::{Error, HttpService, Request, Response}; +use actix_http::{error::PayloadError, Error, HttpService, Request, Response}; use actix_server::Server; -use bytes::Bytes; -use futures::Future; +use bytes::BytesMut; +use futures::{Future, Stream}; use log::info; fn handle_request(mut req: Request) -> impl Future { - req.body().limit(512).from_err().and_then(|bytes: Bytes| { - info!("request body: {:?}", bytes); - let mut res = Response::Ok(); - res.header("x-head", HeaderValue::from_static("dummy value!")); - Ok(res.body(bytes)) - }) + req.take_payload() + .fold(BytesMut::new(), move |mut body, chunk| { + body.extend_from_slice(&chunk); + Ok::<_, PayloadError>(body) + }) + .from_err() + .and_then(|bytes| { + info!("request body: {:?}", bytes); + let mut res = Response::Ok(); + res.header("x-head", HeaderValue::from_static("dummy value!")); + Ok(res.body(bytes)) + }) } fn main() -> io::Result<()> { diff --git a/src/error.rs b/src/error.rs index 696162f86..e0a416ef8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -390,80 +390,6 @@ impl ResponseError for ContentTypeError { } } -/// A set of errors that can occur during parsing urlencoded payloads -#[derive(Debug, Display, From)] -pub enum UrlencodedError { - /// Can not decode chunked transfer encoding - #[display(fmt = "Can not decode chunked transfer encoding")] - Chunked, - /// Payload size is bigger than allowed. (default: 256kB) - #[display(fmt = "Urlencoded payload size is bigger than allowed. (default: 256kB)")] - Overflow, - /// Payload size is now known - #[display(fmt = "Payload size is now known")] - UnknownLength, - /// Content type error - #[display(fmt = "Content type error")] - ContentType, - /// Parse error - #[display(fmt = "Parse error")] - Parse, - /// Payload error - #[display(fmt = "Error that occur during reading payload: {}", _0)] - Payload(PayloadError), -} - -/// Return `BadRequest` for `UrlencodedError` -impl ResponseError for UrlencodedError { - fn error_response(&self) -> Response { - match *self { - UrlencodedError::Overflow => Response::new(StatusCode::PAYLOAD_TOO_LARGE), - UrlencodedError::UnknownLength => Response::new(StatusCode::LENGTH_REQUIRED), - _ => Response::new(StatusCode::BAD_REQUEST), - } - } -} - -/// A set of errors that can occur during parsing json payloads -#[derive(Debug, Display, From)] -pub enum JsonPayloadError { - /// Payload size is bigger than allowed. (default: 256kB) - #[display(fmt = "Json payload size is bigger than allowed. (default: 256kB)")] - Overflow, - /// Content type error - #[display(fmt = "Content type error")] - ContentType, - /// Deserialize error - #[display(fmt = "Json deserialize error: {}", _0)] - Deserialize(JsonError), - /// Payload error - #[display(fmt = "Error that occur during reading payload: {}", _0)] - Payload(PayloadError), -} - -/// Return `BadRequest` for `UrlencodedError` -impl ResponseError for JsonPayloadError { - fn error_response(&self) -> Response { - match *self { - JsonPayloadError::Overflow => Response::new(StatusCode::PAYLOAD_TOO_LARGE), - _ => Response::new(StatusCode::BAD_REQUEST), - } - } -} - -/// Error type returned when reading body as lines. -#[derive(From)] -pub enum ReadlinesError { - /// Error when decoding a line. - EncodingError, - /// Payload error. - PayloadError(PayloadError), - /// Line limit exceeded. - LimitOverflow, - /// ContentType error. - ContentTypeError(ContentTypeError), -} - /// Helper type that can wrap any error and generate custom response. /// /// In following example any `io::Error` will be converted into "BAD REQUEST" diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 8573e917d..117e10a81 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -1,22 +1,14 @@ use std::cell::{Ref, RefMut}; use std::str; -use bytes::{Bytes, BytesMut}; use cookie::Cookie; use encoding::all::UTF_8; use encoding::label::encoding_from_whatwg_label; -use encoding::types::{DecoderTrap, Encoding}; use encoding::EncodingRef; -use futures::{Async, Future, Poll, Stream}; use http::{header, HeaderMap}; use mime::Mime; -use serde::de::DeserializeOwned; -use serde_urlencoded; -use crate::error::{ - ContentTypeError, CookieParseError, ParseError, PayloadError, ReadlinesError, - UrlencodedError, -}; +use crate::error::{ContentTypeError, CookieParseError, ParseError}; use crate::extensions::Extensions; use crate::header::Header; use crate::payload::Payload; @@ -143,88 +135,6 @@ pub trait HttpMessage: Sized { } None } - - /// Load http message body. - /// - /// By default only 256Kb payload reads to a memory, then - /// `PayloadError::Overflow` get returned. Use `MessageBody::limit()` - /// method to change upper limit. - /// - /// ## Server example - /// - /// ```rust,ignore - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; - /// # #[macro_use] extern crate serde_derive; - /// use actix_web::{ - /// AsyncResponder, FutureResponse, HttpMessage, HttpRequest, Response, - /// }; - /// use bytes::Bytes; - /// use futures::future::Future; - /// - /// fn index(mut req: HttpRequest) -> FutureResponse { - /// req.body() // <- get Body future - /// .limit(1024) // <- change max size of the body to a 1kb - /// .from_err() - /// .and_then(|bytes: Bytes| { // <- complete body - /// println!("==== BODY ==== {:?}", bytes); - /// Ok(Response::Ok().into()) - /// }).responder() - /// } - /// # fn main() {} - /// ``` - fn body(&mut self) -> MessageBody - where - Self::Stream: Stream + Sized, - { - MessageBody::new(self) - } - - /// Parse `application/x-www-form-urlencoded` encoded request's body. - /// Return `UrlEncoded` future. Form can be deserialized to any type that - /// implements `Deserialize` trait from *serde*. - /// - /// Returns error: - /// - /// * content type is not `application/x-www-form-urlencoded` - /// * content-length is greater than 256k - /// - /// ## Server example - /// - /// ```rust,ignore - /// # extern crate actix_web; - /// # extern crate futures; - /// # use futures::Future; - /// # use std::collections::HashMap; - /// use actix_web::{FutureResponse, HttpMessage, HttpRequest, Response}; - /// - /// fn index(mut req: HttpRequest) -> FutureResponse { - /// Box::new( - /// req.urlencoded::>() // <- get UrlEncoded future - /// .from_err() - /// .and_then(|params| { // <- url encoded parameters - /// println!("==== BODY ==== {:?}", params); - /// Ok(Response::Ok().into()) - /// }), - /// ) - /// } - /// # fn main() {} - /// ``` - fn urlencoded(&mut self) -> UrlEncoded - where - Self::Stream: Stream, - { - UrlEncoded::new(self) - } - - /// Return stream of lines. - fn readlines(&mut self) -> Readlines - where - Self::Stream: Stream + 'static, - { - Readlines::new(self) - } } impl<'a, T> HttpMessage for &'a mut T @@ -253,383 +163,12 @@ where } } -/// Stream to read request line by line. -pub struct Readlines { - stream: Payload, - buff: BytesMut, - limit: usize, - checked_buff: bool, - encoding: EncodingRef, - err: Option, -} - -impl Readlines -where - T: HttpMessage, - T::Stream: Stream, -{ - /// Create a new stream to read request line by line. - fn new(req: &mut T) -> Self { - let encoding = match req.encoding() { - Ok(enc) => enc, - Err(err) => return Self::err(err.into()), - }; - - Readlines { - stream: req.take_payload(), - buff: BytesMut::with_capacity(262_144), - limit: 262_144, - checked_buff: true, - err: None, - encoding, - } - } - - /// Change max line size. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - fn err(err: ReadlinesError) -> Self { - Readlines { - stream: Payload::None, - buff: BytesMut::new(), - limit: 262_144, - checked_buff: true, - encoding: UTF_8, - err: Some(err), - } - } -} - -impl Stream for Readlines -where - T: HttpMessage, - T::Stream: Stream, -{ - type Item = String; - type Error = ReadlinesError; - - fn poll(&mut self) -> Poll, Self::Error> { - if let Some(err) = self.err.take() { - return Err(err); - } - - // check if there is a newline in the buffer - if !self.checked_buff { - let mut found: Option = None; - for (ind, b) in self.buff.iter().enumerate() { - if *b == b'\n' { - found = Some(ind); - break; - } - } - if let Some(ind) = found { - // check if line is longer than limit - if ind + 1 > self.limit { - return Err(ReadlinesError::LimitOverflow); - } - let enc: *const Encoding = self.encoding as *const Encoding; - let line = if enc == UTF_8 { - str::from_utf8(&self.buff.split_to(ind + 1)) - .map_err(|_| ReadlinesError::EncodingError)? - .to_owned() - } else { - self.encoding - .decode(&self.buff.split_to(ind + 1), DecoderTrap::Strict) - .map_err(|_| ReadlinesError::EncodingError)? - }; - return Ok(Async::Ready(Some(line))); - } - self.checked_buff = true; - } - // poll req for more bytes - match self.stream.poll() { - Ok(Async::Ready(Some(mut bytes))) => { - // check if there is a newline in bytes - let mut found: Option = None; - for (ind, b) in bytes.iter().enumerate() { - if *b == b'\n' { - found = Some(ind); - break; - } - } - if let Some(ind) = found { - // check if line is longer than limit - if ind + 1 > self.limit { - return Err(ReadlinesError::LimitOverflow); - } - let enc: *const Encoding = self.encoding as *const Encoding; - let line = if enc == UTF_8 { - str::from_utf8(&bytes.split_to(ind + 1)) - .map_err(|_| ReadlinesError::EncodingError)? - .to_owned() - } else { - self.encoding - .decode(&bytes.split_to(ind + 1), DecoderTrap::Strict) - .map_err(|_| ReadlinesError::EncodingError)? - }; - // extend buffer with rest of the bytes; - self.buff.extend_from_slice(&bytes); - self.checked_buff = false; - return Ok(Async::Ready(Some(line))); - } - self.buff.extend_from_slice(&bytes); - Ok(Async::NotReady) - } - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(None)) => { - if self.buff.is_empty() { - return Ok(Async::Ready(None)); - } - if self.buff.len() > self.limit { - return Err(ReadlinesError::LimitOverflow); - } - let enc: *const Encoding = self.encoding as *const Encoding; - let line = if enc == UTF_8 { - str::from_utf8(&self.buff) - .map_err(|_| ReadlinesError::EncodingError)? - .to_owned() - } else { - self.encoding - .decode(&self.buff, DecoderTrap::Strict) - .map_err(|_| ReadlinesError::EncodingError)? - }; - self.buff.clear(); - Ok(Async::Ready(Some(line))) - } - Err(e) => Err(ReadlinesError::from(e)), - } - } -} - -/// Future that resolves to a complete http message body. -pub struct MessageBody { - limit: usize, - length: Option, - stream: Payload, - err: Option, - fut: Option>>, -} - -impl MessageBody -where - T: HttpMessage, - T::Stream: Stream, -{ - /// Create `MessageBody` for request. - pub fn new(req: &mut T) -> MessageBody { - let mut len = None; - if let Some(l) = req.headers().get(header::CONTENT_LENGTH) { - if let Ok(s) = l.to_str() { - if let Ok(l) = s.parse::() { - len = Some(l) - } else { - return Self::err(PayloadError::UnknownLength); - } - } else { - return Self::err(PayloadError::UnknownLength); - } - } - - MessageBody { - stream: req.take_payload(), - limit: 262_144, - length: len, - fut: None, - err: None, - } - } - - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - fn err(e: PayloadError) -> Self { - MessageBody { - stream: Payload::None, - limit: 262_144, - fut: None, - err: Some(e), - length: None, - } - } -} - -impl Future for MessageBody -where - T: HttpMessage, - T::Stream: Stream + 'static, -{ - type Item = Bytes; - type Error = PayloadError; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut { - return fut.poll(); - } - - if let Some(err) = self.err.take() { - return Err(err); - } - - if let Some(len) = self.length.take() { - if len > self.limit { - return Err(PayloadError::Overflow); - } - } - - // future - let limit = self.limit; - self.fut = Some(Box::new( - std::mem::replace(&mut self.stream, Payload::None) - .from_err() - .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { - if (body.len() + chunk.len()) > limit { - Err(PayloadError::Overflow) - } else { - body.extend_from_slice(&chunk); - Ok(body) - } - }) - .map(|body| body.freeze()), - )); - self.poll() - } -} - -/// Future that resolves to a parsed urlencoded values. -pub struct UrlEncoded { - stream: Payload, - limit: usize, - length: Option, - encoding: EncodingRef, - err: Option, - fut: Option>>, -} - -impl UrlEncoded -where - T: HttpMessage, - T::Stream: Stream, -{ - /// Create a new future to URL encode a request - pub fn new(req: &mut T) -> UrlEncoded { - // check content type - if req.content_type().to_lowercase() != "application/x-www-form-urlencoded" { - return Self::err(UrlencodedError::ContentType); - } - let encoding = match req.encoding() { - Ok(enc) => enc, - Err(_) => return Self::err(UrlencodedError::ContentType), - }; - - let mut len = None; - if let Some(l) = req.headers().get(header::CONTENT_LENGTH) { - if let Ok(s) = l.to_str() { - if let Ok(l) = s.parse::() { - len = Some(l) - } else { - return Self::err(UrlencodedError::UnknownLength); - } - } else { - return Self::err(UrlencodedError::UnknownLength); - } - }; - - UrlEncoded { - encoding, - stream: req.take_payload(), - limit: 262_144, - length: len, - fut: None, - err: None, - } - } - - fn err(e: UrlencodedError) -> Self { - UrlEncoded { - stream: Payload::None, - limit: 262_144, - fut: None, - err: Some(e), - length: None, - encoding: UTF_8, - } - } - - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } -} - -impl Future for UrlEncoded -where - T: HttpMessage, - T::Stream: Stream + 'static, - U: DeserializeOwned + 'static, -{ - type Item = U; - type Error = UrlencodedError; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut { - return fut.poll(); - } - - if let Some(err) = self.err.take() { - return Err(err); - } - - // payload size - let limit = self.limit; - if let Some(len) = self.length.take() { - if len > limit { - return Err(UrlencodedError::Overflow); - } - } - - // future - let encoding = self.encoding; - let fut = std::mem::replace(&mut self.stream, Payload::None) - .from_err() - .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { - if (body.len() + chunk.len()) > limit { - Err(UrlencodedError::Overflow) - } else { - body.extend_from_slice(&chunk); - Ok(body) - } - }) - .and_then(move |body| { - if (encoding as *const Encoding) == UTF_8 { - serde_urlencoded::from_bytes::(&body) - .map_err(|_| UrlencodedError::Parse) - } else { - let body = encoding - .decode(&body, DecoderTrap::Strict) - .map_err(|_| UrlencodedError::Parse)?; - serde_urlencoded::from_str::(&body) - .map_err(|_| UrlencodedError::Parse) - } - }); - self.fut = Some(Box::new(fut)); - self.poll() - } -} - #[cfg(test)] mod tests { + use bytes::Bytes; use encoding::all::ISO_8859_2; use encoding::Encoding; - use futures::Async; use mime; - use serde_derive::Deserialize; use super::*; use crate::test::TestRequest; @@ -720,166 +259,4 @@ mod tests { .finish(); assert!(req.chunked().is_err()); } - - impl PartialEq for UrlencodedError { - fn eq(&self, other: &UrlencodedError) -> bool { - match *self { - UrlencodedError::Chunked => match *other { - UrlencodedError::Chunked => true, - _ => false, - }, - UrlencodedError::Overflow => match *other { - UrlencodedError::Overflow => true, - _ => false, - }, - UrlencodedError::UnknownLength => match *other { - UrlencodedError::UnknownLength => true, - _ => false, - }, - UrlencodedError::ContentType => match *other { - UrlencodedError::ContentType => true, - _ => false, - }, - _ => false, - } - } - } - - #[derive(Deserialize, Debug, PartialEq)] - struct Info { - hello: String, - } - - #[test] - fn test_urlencoded_error() { - let mut req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ) - .header(header::CONTENT_LENGTH, "xxxx") - .finish(); - assert_eq!( - req.urlencoded::().poll().err().unwrap(), - UrlencodedError::UnknownLength - ); - - let mut req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ) - .header(header::CONTENT_LENGTH, "1000000") - .finish(); - assert_eq!( - req.urlencoded::().poll().err().unwrap(), - UrlencodedError::Overflow - ); - - let mut req = TestRequest::with_header(header::CONTENT_TYPE, "text/plain") - .header(header::CONTENT_LENGTH, "10") - .finish(); - assert_eq!( - req.urlencoded::().poll().err().unwrap(), - UrlencodedError::ContentType - ); - } - - #[test] - fn test_urlencoded() { - let mut req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ) - .header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); - - let result = req.urlencoded::().poll().ok().unwrap(); - assert_eq!( - result, - Async::Ready(Info { - hello: "world".to_owned() - }) - ); - - let mut req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded; charset=utf-8", - ) - .header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); - - let result = req.urlencoded().poll().ok().unwrap(); - assert_eq!( - result, - Async::Ready(Info { - hello: "world".to_owned() - }) - ); - } - - #[test] - fn test_message_body() { - let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "xxxx").finish(); - match req.body().poll().err().unwrap() { - PayloadError::UnknownLength => (), - _ => unreachable!("error"), - } - - let mut req = - TestRequest::with_header(header::CONTENT_LENGTH, "1000000").finish(); - match req.body().poll().err().unwrap() { - PayloadError::Overflow => (), - _ => unreachable!("error"), - } - - let mut req = TestRequest::default() - .set_payload(Bytes::from_static(b"test")) - .finish(); - match req.body().poll().ok().unwrap() { - Async::Ready(bytes) => assert_eq!(bytes, Bytes::from_static(b"test")), - _ => unreachable!("error"), - } - - let mut req = TestRequest::default() - .set_payload(Bytes::from_static(b"11111111111111")) - .finish(); - match req.body().limit(5).poll().err().unwrap() { - PayloadError::Overflow => (), - _ => unreachable!("error"), - } - } - - #[test] - fn test_readlines() { - let mut req = TestRequest::default() - .set_payload(Bytes::from_static( - b"Lorem Ipsum is simply dummy text of the printing and typesetting\n\ - industry. Lorem Ipsum has been the industry's standard dummy\n\ - Contrary to popular belief, Lorem Ipsum is not simply random text.", - )) - .finish(); - let mut r = Readlines::new(&mut req); - match r.poll().ok().unwrap() { - Async::Ready(Some(s)) => assert_eq!( - s, - "Lorem Ipsum is simply dummy text of the printing and typesetting\n" - ), - _ => unreachable!("error"), - } - match r.poll().ok().unwrap() { - Async::Ready(Some(s)) => assert_eq!( - s, - "industry. Lorem Ipsum has been the industry's standard dummy\n" - ), - _ => unreachable!("error"), - } - match r.poll().ok().unwrap() { - Async::Ready(Some(s)) => assert_eq!( - s, - "Contrary to popular belief, Lorem Ipsum is not simply random text." - ), - _ => unreachable!("error"), - } - } } diff --git a/src/lib.rs b/src/lib.rs index 443266a9b..9a87b77f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,24 +97,9 @@ pub use self::httpmessage::HttpMessage; pub use self::message::{Head, Message, RequestHead, ResponseHead}; pub use self::payload::{Payload, PayloadStream}; pub use self::request::Request; -pub use self::response::Response; +pub use self::response::{Response, ResponseBuilder}; pub use self::service::{HttpService, SendError, SendResponse}; -pub mod dev { - //! The `actix-web` prelude for library developers - //! - //! The purpose of this module is to alleviate imports of many common actix - //! traits by adding a glob import to the top of actix heavy modules: - //! - //! ``` - //! # #![allow(unused_imports)] - //! use actix_http::dev::*; - //! ``` - - pub use crate::httpmessage::{MessageBody, Readlines, UrlEncoded}; - pub use crate::response::ResponseBuilder; -} - pub mod http { //! Various HTTP related types diff --git a/tests/test_client.rs b/tests/test_client.rs index 90e1a4f4a..2832b1b70 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -1,9 +1,11 @@ use actix_service::NewService; -use bytes::Bytes; +use bytes::{Bytes, BytesMut}; use futures::future::{self, ok}; +use futures::{Future, Stream}; -use actix_http::HttpMessage; -use actix_http::{client, HttpService, Request, Response}; +use actix_http::{ + client, error::PayloadError, HttpMessage, HttpService, Request, Response, +}; use actix_http_test::TestServer; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ @@ -28,6 +30,16 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World"; +fn load_body(stream: S) -> impl Future +where + S: Stream, +{ + stream.fold(BytesMut::new(), move |mut body, chunk| { + body.extend_from_slice(&chunk); + Ok::<_, PayloadError>(body) + }) +} + #[test] fn test_h1_v2() { env_logger::init(); @@ -51,7 +63,7 @@ fn test_h1_v2() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); let request = srv.post().finish().unwrap(); @@ -59,7 +71,7 @@ fn test_h1_v2() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } diff --git a/tests/test_server.rs b/tests/test_server.rs index 98f740941..8a7316cdf 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -6,16 +6,27 @@ use actix_codec::{AsyncRead, AsyncWrite}; use actix_http_test::TestServer; use actix_server_config::ServerConfig; use actix_service::{fn_cfg_factory, NewService}; -use bytes::Bytes; +use bytes::{Bytes, BytesMut}; use futures::future::{self, ok, Future}; -use futures::stream::once; +use futures::stream::{once, Stream}; use actix_http::body::Body; +use actix_http::error::PayloadError; use actix_http::{ body, client, error, http, http::header, Error, HttpMessage as HttpMessage2, HttpService, KeepAlive, Request, Response, }; +fn load_body(stream: S) -> impl Future +where + S: Stream, +{ + stream.fold(BytesMut::new(), move |mut body, chunk| { + body.extend_from_slice(&chunk); + Ok::<_, PayloadError>(body) + }) +} + #[test] fn test_h1() { let mut srv = TestServer::new(|| { @@ -131,8 +142,7 @@ fn test_h2_body() -> std::io::Result<()> { .and_then( HttpService::build() .h2(|mut req: Request<_>| { - req.body() - .limit(1024 * 1024) + load_body(req.take_payload()) .and_then(|body| Ok(Response::Ok().body(body))) }) .map_err(|_| ()), @@ -145,7 +155,7 @@ fn test_h2_body() -> std::io::Result<()> { let mut response = srv.send_request(req).unwrap(); assert!(response.status().is_success()); - let body = srv.block_on(response.body().limit(1024 * 1024)).unwrap(); + let body = srv.block_on(load_body(response.take_payload())).unwrap(); assert_eq!(&body, data.as_bytes()); Ok(()) } @@ -440,7 +450,7 @@ fn test_h1_headers() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert_eq!(bytes, Bytes::from(data2)); } @@ -486,7 +496,7 @@ fn test_h2_headers() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert_eq!(bytes, Bytes::from(data2)); } @@ -523,7 +533,7 @@ fn test_h1_body() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -546,7 +556,7 @@ fn test_h2_body2() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -569,7 +579,7 @@ fn test_h1_head_empty() { } // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert!(bytes.is_empty()); } @@ -601,7 +611,7 @@ fn test_h2_head_empty() { } // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert!(bytes.is_empty()); } @@ -626,7 +636,7 @@ fn test_h1_head_binary() { } // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert!(bytes.is_empty()); } @@ -661,7 +671,7 @@ fn test_h2_head_binary() { } // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert!(bytes.is_empty()); } @@ -728,7 +738,7 @@ fn test_h1_body_length() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -756,7 +766,7 @@ fn test_h2_body_length() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -787,7 +797,7 @@ fn test_h1_body_chunked_explicit() { ); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); // decode assert_eq!(bytes, Bytes::from_static(STR.as_ref())); @@ -821,7 +831,7 @@ fn test_h2_body_chunked_explicit() { assert!(!response.headers().contains_key(header::TRANSFER_ENCODING)); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); // decode assert_eq!(bytes, Bytes::from_static(STR.as_ref())); @@ -850,7 +860,7 @@ fn test_h1_body_chunked_implicit() { ); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -874,7 +884,7 @@ fn test_h1_response_http_error_handling() { assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert!(bytes.is_empty()); } @@ -907,7 +917,7 @@ fn test_h2_response_http_error_handling() { assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert!(bytes.is_empty()); } @@ -923,7 +933,7 @@ fn test_h1_service_error() { assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert!(bytes.is_empty()); } @@ -947,6 +957,6 @@ fn test_h2_service_error() { assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert!(bytes.is_empty()); } From b550f9ecf449a71274f118f1b91e8c9a121e8986 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 01:08:56 -0700 Subject: [PATCH 301/427] update imports --- src/lib.rs | 2 +- src/responder.rs | 2 +- src/types/form.rs | 3 ++- src/types/json.rs | 2 +- src/types/readlines.rs | 2 +- tests/test_server.rs | 24 ++++++++++++------------ 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d6bcf4e32..dbf8f7439 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,7 +64,7 @@ pub mod dev { pub use crate::types::readlines::Readlines; pub use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; - pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder; + pub use actix_http::ResponseBuilder as HttpResponseBuilder; pub use actix_http::{ Extensions, Payload, PayloadStream, RequestHead, ResponseHead, }; diff --git a/src/responder.rs b/src/responder.rs index ace360c62..5f98e6e83 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,5 +1,5 @@ use actix_http::error::InternalError; -use actix_http::{dev::ResponseBuilder, http::StatusCode, Error, Response}; +use actix_http::{http::StatusCode, Error, Response, ResponseBuilder}; use bytes::{Bytes, BytesMut}; use futures::future::{err, ok, Either as EitherFuture, FutureResult}; use futures::{Future, IntoFuture, Poll}; diff --git a/src/types/form.rs b/src/types/form.rs index 58fa37611..cd4d09bb5 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use std::{fmt, ops}; -use actix_http::error::{Error, PayloadError, UrlencodedError}; +use actix_http::error::{Error, PayloadError}; use actix_http::{HttpMessage, Payload}; use bytes::{Bytes, BytesMut}; use encoding::all::UTF_8; @@ -12,6 +12,7 @@ use encoding::EncodingRef; use futures::{Future, Poll, Stream}; use serde::de::DeserializeOwned; +use crate::error::UrlencodedError; use crate::extract::FromRequest; use crate::http::header::CONTENT_LENGTH; use crate::request::HttpRequest; diff --git a/src/types/json.rs b/src/types/json.rs index 18a6be909..4fc2748f0 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -9,10 +9,10 @@ use serde::de::DeserializeOwned; use serde::Serialize; use serde_json; -use actix_http::error::{Error, JsonPayloadError, PayloadError}; use actix_http::http::{header::CONTENT_LENGTH, StatusCode}; use actix_http::{HttpMessage, Payload, Response}; +use crate::error::{Error, JsonPayloadError, PayloadError}; use crate::extract::FromRequest; use crate::request::HttpRequest; use crate::responder::Responder; diff --git a/src/types/readlines.rs b/src/types/readlines.rs index 2c7f699a7..c23b84434 100644 --- a/src/types/readlines.rs +++ b/src/types/readlines.rs @@ -198,7 +198,7 @@ mod tests { }; match block_on(stream.into_future()) { - Ok((Some(s), stream)) => { + Ok((Some(s), _)) => { assert_eq!( s, "Contrary to popular belief, Lorem Ipsum is not simply random text." diff --git a/tests/test_server.rs b/tests/test_server.rs index ffdc473a1..965d444fc 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -3,7 +3,7 @@ use std::io::{Read, Write}; use actix_http::http::header::{ ContentEncoding, ACCEPT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING, }; -use actix_http::{h1, Error, HttpMessage, Response}; +use actix_http::{h1, Error, Response}; use actix_http_test::TestServer; use brotli2::write::BrotliDecoder; use bytes::Bytes; @@ -12,7 +12,7 @@ use flate2::write::ZlibDecoder; use futures::stream::once; //Future, Stream use rand::{distributions::Alphanumeric, Rng}; -use actix_web::{middleware, web, App}; +use actix_web::{dev::HttpMessageBody, middleware, web, App}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -50,7 +50,7 @@ fn test_body() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -69,7 +69,7 @@ fn test_body_gzip() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -100,7 +100,7 @@ fn test_body_gzip_large() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -134,7 +134,7 @@ fn test_body_gzip_large_random() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -167,7 +167,7 @@ fn test_body_chunked_implicit() { ); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -195,7 +195,7 @@ fn test_body_br_streaming() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); // decode br let mut e = BrotliDecoder::new(Vec::with_capacity(2048)); @@ -222,7 +222,7 @@ fn test_head_binary() { } // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); assert!(bytes.is_empty()); } @@ -245,7 +245,7 @@ fn test_no_chunking() { assert!(!response.headers().contains_key(TRANSFER_ENCODING)); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -267,7 +267,7 @@ fn test_body_deflate() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); // decode deflate let mut e = ZlibDecoder::new(Vec::new()); @@ -294,7 +294,7 @@ fn test_body_brotli() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); // decode brotli let mut e = BrotliDecoder::new(Vec::with_capacity(2048)); From 7435c5e9bf8dc1db407a8a1659a9bea2934dc427 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 01:49:00 -0700 Subject: [PATCH 302/427] temp fix for tarpaulin --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 55a03ec8c..32e6c136c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,7 +49,7 @@ after_success: fi - | if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then - cargo tarpaulin --out Xml --all + taskset -c 0 cargo tarpaulin --out Xml --all bash <(curl -s https://codecov.io/bash) echo "Uploaded code coverage" fi From 2b5f9f05118efda14a08febf386f665519596a04 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 08:50:25 -0700 Subject: [PATCH 303/427] temp fix for tarpaulin --- .travis.yml | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index 283437ce6..ff8815059 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,9 +10,9 @@ matrix: include: - rust: stable - rust: beta - - rust: nightly + - rust: nightly-2019-03-02 allow_failures: - - rust: nightly + - rust: nightly-2019-03-02 env: global: @@ -25,8 +25,8 @@ before_install: - sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev before_cache: | - if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then - RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin -f --version 0.6.7 + if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then + RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin fi script: @@ -34,20 +34,19 @@ script: - cargo build --features="ssl" - cargo test --features="ssl" -after_success: | - if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then - cargo tarpaulin --features="ssl" --out Xml - bash <(curl -s https://codecov.io/bash) - echo "Uploaded code coverage" - fi - # Upload docs -#after_success: -# - | -# if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then -# cargo doc --features "session" --no-deps && -# echo "" > target/doc/index.html && -# git clone https://github.com/davisp/ghp-import.git && -# ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc && -# echo "Uploaded documentation" -# fi +after_success: + - | + if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then + cargo doc --no-deps && + echo "" > target/doc/index.html && + git clone https://github.com/davisp/ghp-import.git && + ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc && + echo "Uploaded documentation" + fi + - | + if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then + taskset -c 0 cargo tarpaulin --features="ssl" --out Xml + bash <(curl -s https://codecov.io/bash) + echo "Uploaded code coverage" + fi From c14c66d2b00427482f7fd3b3e54af80a257a2641 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 09:52:41 -0700 Subject: [PATCH 304/427] add json extractor tests --- .travis.yml | 2 +- src/error.rs | 4 +-- src/responder.rs | 4 +-- src/test.rs | 24 +++++++++++--- src/types/json.rs | 83 +++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 105 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 32e6c136c..9caaac1b5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ matrix: - rust: beta - rust: nightly-2019-03-02 allow_failures: - - rust: nightly + - rust: nightly-2019-03-02 env: global: diff --git a/src/error.rs b/src/error.rs index bf224a223..2231473f2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -87,8 +87,8 @@ impl ResponseError for UrlencodedError { /// A set of errors that can occur during parsing json payloads #[derive(Debug, Display, From)] pub enum JsonPayloadError { - /// Payload size is bigger than allowed. (default: 256kB) - #[display(fmt = "Json payload size is bigger than allowed. (default: 256kB)")] + /// Payload size is bigger than allowed. (default: 32kB) + #[display(fmt = "Json payload size is bigger than allowed.")] Overflow, /// Content type error #[display(fmt = "Content type error")] diff --git a/src/responder.rs b/src/responder.rs index 5f98e6e83..871670bdf 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -288,7 +288,7 @@ where } #[cfg(test)] -mod tests { +pub(crate) mod tests { use actix_service::Service; use bytes::{Bytes, BytesMut}; @@ -322,7 +322,7 @@ mod tests { } } - trait BodyTest { + pub(crate) trait BodyTest { fn bin_ref(&self) -> &[u8]; fn body(&self) -> &Body; } diff --git a/src/test.rs b/src/test.rs index 13db59771..fe9fb0247 100644 --- a/src/test.rs +++ b/src/test.rs @@ -5,7 +5,7 @@ use std::rc::Rc; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, Version}; use actix_http::test::TestRequest as HttpTestRequest; -use actix_http::{PayloadStream, Request}; +use actix_http::{Extensions, PayloadStream, Request}; use actix_router::{Path, ResourceDef, Url}; use actix_rt::Runtime; use actix_server_config::ServerConfig; @@ -15,6 +15,7 @@ use cookie::Cookie; use futures::future::{lazy, Future}; use crate::config::{AppConfig, AppConfigInner}; +use crate::data::RouteData; use crate::rmap::ResourceMap; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; use crate::{HttpRequest, HttpResponse}; @@ -157,6 +158,7 @@ pub struct TestRequest { req: HttpTestRequest, rmap: ResourceMap, config: AppConfigInner, + route_data: Extensions, } impl Default for TestRequest { @@ -165,6 +167,7 @@ impl Default for TestRequest { req: HttpTestRequest::default(), rmap: ResourceMap::new(ResourceDef::new("")), config: AppConfigInner::default(), + route_data: Extensions::new(), } } } @@ -177,6 +180,7 @@ impl TestRequest { req: HttpTestRequest::default().uri(path).take(), rmap: ResourceMap::new(ResourceDef::new("")), config: AppConfigInner::default(), + route_data: Extensions::new(), } } @@ -186,6 +190,7 @@ impl TestRequest { req: HttpTestRequest::default().set(hdr).take(), config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), + route_data: Extensions::new(), } } @@ -199,6 +204,7 @@ impl TestRequest { req: HttpTestRequest::default().header(key, value).take(), config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), + route_data: Extensions::new(), } } @@ -208,6 +214,7 @@ impl TestRequest { req: HttpTestRequest::default().method(Method::GET).take(), config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), + route_data: Extensions::new(), } } @@ -217,6 +224,7 @@ impl TestRequest { req: HttpTestRequest::default().method(Method::POST).take(), config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), + route_data: Extensions::new(), } } @@ -266,12 +274,20 @@ impl TestRequest { self } - /// Set route data - pub fn route_data(self, data: T) -> Self { + /// Set application data. This is equivalent of `App::data()` method + /// for testing purpose. + pub fn app_data(self, data: T) -> Self { self.config.extensions.borrow_mut().insert(data); self } + /// Set route data. This is equivalent of `Route::data()` method + /// for testing purpose. + pub fn route_data(mut self, data: T) -> Self { + self.route_data.insert(RouteData::new(data)); + self + } + #[cfg(test)] /// Set request config pub(crate) fn rmap(mut self, rmap: ResourceMap) -> Self { @@ -324,7 +340,7 @@ impl TestRequest { Rc::new(self.rmap), AppConfig::new(self.config), ); - ServiceFromRequest::new(req, None) + ServiceFromRequest::new(req, Some(Rc::new(self.route_data))) } /// Runs the provided future, blocking the current thread until the future diff --git a/src/types/json.rs b/src/types/json.rs index 4fc2748f0..9e13d994e 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -371,6 +371,11 @@ mod tests { use crate::http::header; use crate::test::{block_on, TestRequest}; + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct MyObject { + name: String, + } + fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { match err { JsonPayloadError::Overflow => match other { @@ -385,9 +390,81 @@ mod tests { } } - #[derive(Serialize, Deserialize, PartialEq, Debug)] - struct MyObject { - name: String, + #[test] + fn test_responder() { + let req = TestRequest::default().to_http_request(); + + let j = Json(MyObject { + name: "test".to_string(), + }); + let resp = j.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + header::HeaderValue::from_static("application/json") + ); + + use crate::responder::tests::BodyTest; + assert_eq!(resp.body().bin_ref(), b"{\"name\":\"test\"}"); + } + + #[test] + fn test_extract() { + let mut req = TestRequest::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ) + .header( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + ) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .to_from(); + + let s = block_on(Json::::from_request(&mut req)).unwrap(); + assert_eq!(s.name, "test"); + assert_eq!( + s.into_inner(), + MyObject { + name: "test".to_string() + } + ); + + let mut req = TestRequest::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ) + .header( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + ) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .route_data(JsonConfig::default().limit(10)) + .to_from(); + let s = block_on(Json::::from_request(&mut req)); + assert!(format!("{}", s.err().unwrap()) + .contains("Json payload size is bigger than allowed.")); + + let mut req = TestRequest::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ) + .header( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + ) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .route_data( + JsonConfig::default() + .limit(10) + .error_handler(|_, _| JsonPayloadError::ContentType.into()), + ) + .to_from(); + let s = block_on(Json::::from_request(&mut req)); + assert!(format!("{}", s.err().unwrap()).contains("Content type error")); } #[test] From 9bd0f29ca3396924a607e8a1c5312f79ae157cab Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 10:11:10 -0700 Subject: [PATCH 305/427] add tests for error and some responders --- src/error.rs | 31 +++++++++++++++++++++++++++++++ src/responder.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/error.rs b/src/error.rs index 2231473f2..fc0f9fdf3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -142,3 +142,34 @@ impl ResponseError for ReadlinesError { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_urlencoded_error() { + let resp: HttpResponse = UrlencodedError::Overflow.error_response(); + assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); + let resp: HttpResponse = UrlencodedError::UnknownLength.error_response(); + assert_eq!(resp.status(), StatusCode::LENGTH_REQUIRED); + let resp: HttpResponse = UrlencodedError::ContentType.error_response(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + + #[test] + fn test_json_payload_error() { + let resp: HttpResponse = JsonPayloadError::Overflow.error_response(); + assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); + let resp: HttpResponse = JsonPayloadError::ContentType.error_response(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + + #[test] + fn test_readlines_error() { + let resp: HttpResponse = ReadlinesError::LimitOverflow.error_response(); + assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); + let resp: HttpResponse = ReadlinesError::EncodingError.error_response(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } +} diff --git a/src/responder.rs b/src/responder.rs index 871670bdf..50467883c 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -380,6 +380,15 @@ pub(crate) mod tests { HeaderValue::from_static("text/plain; charset=utf-8") ); + let resp: HttpResponse = + block_on((&"test".to_string()).respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.body().bin_ref(), b"test"); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + let resp: HttpResponse = block_on(Bytes::from_static(b"test").respond_to(&req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); @@ -398,10 +407,32 @@ pub(crate) mod tests { HeaderValue::from_static("application/octet-stream") ); + // InternalError let resp: HttpResponse = error::InternalError::new("err", StatusCode::BAD_REQUEST) .respond_to(&req) .unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } + + #[test] + fn test_result_responder() { + let req = TestRequest::default().to_http_request(); + + // Result + let resp: HttpResponse = + block_on(Ok::<_, Error>("test".to_string()).respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.body().bin_ref(), b"test"); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + + let res = block_on( + Err::(error::InternalError::new("err", StatusCode::BAD_REQUEST)) + .respond_to(&req), + ); + assert!(res.is_err()); + } } From 6b66681827aa1f42041fef9c55bd13bc0d80990c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 13:47:20 -0700 Subject: [PATCH 306/427] add basic actors integration --- Cargo.toml | 1 + actix-web-actors/Cargo.toml | 28 ++++ actix-web-actors/src/context.rs | 254 ++++++++++++++++++++++++++++++++ actix-web-actors/src/lib.rs | 4 + 4 files changed, 287 insertions(+) create mode 100644 actix-web-actors/Cargo.toml create mode 100644 actix-web-actors/src/context.rs create mode 100644 actix-web-actors/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index ba36dd2c9..df06f3a12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ ".", "actix-files", "actix-session", + "actix-web-actors", "actix-web-codegen", ] diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml new file mode 100644 index 000000000..698acc1c7 --- /dev/null +++ b/actix-web-actors/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "actix-web-actors" +version = "0.1.0" +authors = ["Nikolay Kim "] +description = "Actix actors support for actix web framework." +readme = "README.md" +keywords = ["http", "web", "framework", "async", "futures"] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web.git" +documentation = "https://docs.rs/actix-web-actors/" +license = "MIT/Apache-2.0" +exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] +workspace = ".." +edition = "2018" + +[lib] +name = "actix_web_actors" +path = "src/lib.rs" + +[dependencies] +actix-web = { path=".." } +actix = { git = "https://github.com/actix/actix.git" } + +bytes = "0.4" +futures = "0.1" + +[dev-dependencies] +actix-rt = "0.2.0" diff --git a/actix-web-actors/src/context.rs b/actix-web-actors/src/context.rs new file mode 100644 index 000000000..646698efa --- /dev/null +++ b/actix-web-actors/src/context.rs @@ -0,0 +1,254 @@ +use std::collections::VecDeque; + +use actix::dev::{ + AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope, +}; +use actix::fut::ActorFuture; +use actix::{ + Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message, SpawnHandle, +}; +use actix_web::error::{Error, ErrorInternalServerError}; +use bytes::Bytes; +use futures::sync::oneshot::Sender; +use futures::{Async, Future, Poll, Stream}; + +/// Execution context for http actors +pub struct HttpContext +where + A: Actor>, +{ + inner: ContextParts, + stream: VecDeque>, +} + +impl ActorContext for HttpContext +where + A: Actor, +{ + fn stop(&mut self) { + self.inner.stop(); + } + fn terminate(&mut self) { + self.inner.terminate() + } + fn state(&self) -> ActorState { + self.inner.state() + } +} + +impl AsyncContext for HttpContext +where + A: Actor, +{ + #[inline] + fn spawn(&mut self, fut: F) -> SpawnHandle + where + F: ActorFuture + 'static, + { + self.inner.spawn(fut) + } + + #[inline] + fn wait(&mut self, fut: F) + where + F: ActorFuture + 'static, + { + self.inner.wait(fut) + } + + #[doc(hidden)] + #[inline] + fn waiting(&self) -> bool { + self.inner.waiting() + || self.inner.state() == ActorState::Stopping + || self.inner.state() == ActorState::Stopped + } + + #[inline] + fn cancel_future(&mut self, handle: SpawnHandle) -> bool { + self.inner.cancel_future(handle) + } + + #[inline] + fn address(&self) -> Addr { + self.inner.address() + } +} + +impl HttpContext +where + A: Actor, +{ + #[inline] + /// Create a new HTTP Context from a request and an actor + pub fn create(actor: A) -> impl Stream { + let mb = Mailbox::default(); + let ctx = HttpContext { + inner: ContextParts::new(mb.sender_producer()), + stream: VecDeque::new(), + }; + HttpContextFut::new(ctx, actor, mb) + } + + /// Create a new HTTP Context + pub fn with_factory(f: F) -> impl Stream + where + F: FnOnce(&mut Self) -> A + 'static, + { + let mb = Mailbox::default(); + let mut ctx = HttpContext { + inner: ContextParts::new(mb.sender_producer()), + stream: VecDeque::new(), + }; + + let act = f(&mut ctx); + HttpContextFut::new(ctx, act, mb) + } +} + +impl HttpContext +where + A: Actor, +{ + /// Write payload + #[inline] + pub fn write(&mut self, data: Bytes) { + self.stream.push_back(Some(data)); + } + + /// Indicate end of streaming payload. Also this method calls `Self::close`. + #[inline] + pub fn write_eof(&mut self) { + self.stream.push_back(None); + } + + /// Handle of the running future + /// + /// SpawnHandle is the handle returned by `AsyncContext::spawn()` method. + pub fn handle(&self) -> SpawnHandle { + self.inner.curr_handle() + } +} + +impl AsyncContextParts for HttpContext +where + A: Actor, +{ + fn parts(&mut self) -> &mut ContextParts { + &mut self.inner + } +} + +struct HttpContextFut +where + A: Actor>, +{ + fut: ContextFut>, +} + +impl HttpContextFut +where + A: Actor>, +{ + fn new(ctx: HttpContext, act: A, mailbox: Mailbox) -> Self { + let fut = ContextFut::new(ctx, act, mailbox); + HttpContextFut { fut } + } +} + +impl Stream for HttpContextFut +where + A: Actor>, +{ + type Item = Bytes; + type Error = Error; + + fn poll(&mut self) -> Poll, Error> { + if self.fut.alive() { + match self.fut.poll() { + Ok(Async::NotReady) | Ok(Async::Ready(())) => (), + Err(_) => return Err(ErrorInternalServerError("error")), + } + } + + // frames + if let Some(data) = self.fut.ctx().stream.pop_front() { + Ok(Async::Ready(data)) + } else if self.fut.alive() { + Ok(Async::NotReady) + } else { + Ok(Async::Ready(None)) + } + } +} + +impl ToEnvelope for HttpContext +where + A: Actor> + Handler, + M: Message + Send + 'static, + M::Result: Send, +{ + fn pack(msg: M, tx: Option>) -> Envelope { + Envelope::new(msg, tx) + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use actix::Actor; + use actix_web::dev::HttpMessageBody; + use actix_web::http::StatusCode; + use actix_web::test::{block_on, call_success, init_service, TestRequest}; + use actix_web::{web, App, HttpRequest, HttpResponse}; + use bytes::{Bytes, BytesMut}; + + use super::*; + + struct MyActor { + count: usize, + } + + impl Actor for MyActor { + type Context = HttpContext; + + fn started(&mut self, ctx: &mut Self::Context) { + ctx.run_later(Duration::from_millis(100), |slf, ctx| slf.write(ctx)); + } + } + + impl MyActor { + fn write(&mut self, ctx: &mut HttpContext) { + self.count += 1; + if self.count > 3 { + ctx.write_eof() + } else { + ctx.write(Bytes::from(format!("LINE-{}", self.count).as_bytes())); + ctx.run_later(Duration::from_millis(100), |slf, ctx| slf.write(ctx)); + } + } + } + + #[test] + fn test_default_resource() { + let mut srv = + init_service(App::new().service(web::resource("/test").to(|| { + HttpResponse::Ok().streaming(HttpContext::create(MyActor { count: 0 })) + }))); + + let req = TestRequest::with_uri("/test").to_request(); + let mut resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + + let body = block_on(resp.take_body().fold( + BytesMut::new(), + move |mut body, chunk| { + body.extend_from_slice(&chunk); + Ok::<_, Error>(body) + }, + )) + .unwrap(); + assert_eq!(body.freeze(), Bytes::from_static(b"LINE-1LINE-2LINE-3")); + } +} diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs new file mode 100644 index 000000000..47adf5f0a --- /dev/null +++ b/actix-web-actors/src/lib.rs @@ -0,0 +1,4 @@ +//! Actix actors integration for Actix web framework +mod context; + +pub use self::context::HttpContext; From a07ea00cc4811466a09b8afda7b88e4bc115e663 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 13:55:03 -0700 Subject: [PATCH 307/427] add basic test for proc macro --- tests/test_macro.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/test_macro.rs diff --git a/tests/test_macro.rs b/tests/test_macro.rs new file mode 100644 index 000000000..62b5d618f --- /dev/null +++ b/tests/test_macro.rs @@ -0,0 +1,17 @@ +use actix_http::HttpService; +use actix_http_test::TestServer; +use actix_web::{get, App, HttpResponse, Responder}; + +#[get("/test")] +fn test() -> impl Responder { + HttpResponse::Ok() +} + +#[test] +fn test_body() { + let mut srv = TestServer::new(|| HttpService::new(App::new().service(test))); + + let request = srv.get().uri(srv.url("/test")).finish().unwrap(); + let response = srv.send_request(request).unwrap(); + assert!(response.status().is_success()); +} From 88152740c690d42b21f9bb5c90426661e4455885 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 20:20:10 -0700 Subject: [PATCH 308/427] move macros tests to codegen crate --- actix-web-codegen/Cargo.toml | 5 +++++ {tests => actix-web-codegen/tests}/test_macro.rs | 0 2 files changed, 5 insertions(+) rename {tests => actix-web-codegen/tests}/test_macro.rs (100%) diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 24ed36b77..d87b71ba9 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -13,3 +13,8 @@ proc-macro = true [dependencies] quote = "0.6" syn = { version = "0.15", features = ["full", "parsing"] } + +[dev-dependencies] +actix-web = { path = ".." } +actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } diff --git a/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs similarity index 100% rename from tests/test_macro.rs rename to actix-web-codegen/tests/test_macro.rs From 85c2887b3086c2f207f6be4c42ec83522052fb72 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 21:09:50 -0700 Subject: [PATCH 309/427] export ws::hash_key --- src/error.rs | 43 ------------------------------------------- src/ws/mod.rs | 2 +- src/ws/proto.rs | 2 +- 3 files changed, 2 insertions(+), 45 deletions(-) diff --git a/src/error.rs b/src/error.rs index e0a416ef8..6bc401332 100644 --- a/src/error.rs +++ b/src/error.rs @@ -981,23 +981,6 @@ mod tests { from!(httparse::Error::Version => ParseError::Version); } - // #[test] - // fn failure_error() { - // const NAME: &str = "RUST_BACKTRACE"; - // let old_tb = env::var(NAME); - // env::set_var(NAME, "0"); - // let error = failure::err_msg("Hello!"); - // let resp: Error = error.into(); - // assert_eq!( - // format!("{:?}", resp), - // "Compat { error: ErrorMessage { msg: \"Hello!\" } }\n\n" - // ); - // match old_tb { - // Ok(x) => env::set_var(NAME, x), - // _ => env::remove_var(NAME), - // } - // } - #[test] fn test_internal_error() { let err = @@ -1006,32 +989,6 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } - // #[test] - // fn test_error_downcasting_direct() { - // #[derive(Debug, Display)] - // #[display(fmt = "demo error")] - // struct DemoError; - - // impl ResponseError for DemoError {} - - // let err: Error = DemoError.into(); - // let err_ref: &DemoError = err.downcast_ref().unwrap(); - // assert_eq!(err_ref.to_string(), "demo error"); - // } - - // #[test] - // fn test_error_downcasting_compat() { - // #[derive(Debug, Display)] - // #[display(fmt = "demo error")] - // struct DemoError; - - // impl ResponseError for DemoError {} - - // let err: Error = failure::Error::from(DemoError).into(); - // let err_ref: &DemoError = err.downcast_ref().unwrap(); - // assert_eq!(err_ref.to_string(), "demo error"); - // } - #[test] fn test_error_helpers() { let r: Response = ErrorBadRequest("err").into(); diff --git a/src/ws/mod.rs b/src/ws/mod.rs index 8f9bc83ba..3d3f5b925 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -24,7 +24,7 @@ mod transport; pub use self::client::{Client, ClientError, Connect}; pub use self::codec::{Codec, Frame, Message}; pub use self::frame::Parser; -pub use self::proto::{CloseCode, CloseReason, OpCode}; +pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode}; pub use self::service::VerifyWebSockets; pub use self::transport::Transport; diff --git a/src/ws/proto.rs b/src/ws/proto.rs index 35fbbe3ee..eef874741 100644 --- a/src/ws/proto.rs +++ b/src/ws/proto.rs @@ -209,7 +209,7 @@ impl> From<(CloseCode, T)> for CloseReason { static WS_GUID: &'static str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // TODO: hash is always same size, we dont need String -pub(crate) fn hash_key(key: &[u8]) -> String { +pub fn hash_key(key: &[u8]) -> String { let mut hasher = sha1::Sha1::new(); hasher.update(key); From f26d4b6a23de57c7fec54476f711599f63906f27 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 21:57:53 -0700 Subject: [PATCH 310/427] do not chunk websocket stream --- src/h1/client.rs | 1 + src/h1/codec.rs | 1 + src/h1/dispatcher.rs | 10 +++++----- src/h1/encoder.rs | 3 ++- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/h1/client.rs b/src/h1/client.rs index de4d10e1b..851851266 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -211,6 +211,7 @@ impl Encoder for ClientCodec { dst, &mut msg, false, + false, inner.version, length, inner.ctype, diff --git a/src/h1/codec.rs b/src/h1/codec.rs index 23feda505..c66364c02 100644 --- a/src/h1/codec.rs +++ b/src/h1/codec.rs @@ -170,6 +170,7 @@ impl Encoder for Codec { dst, &mut res, self.flags.contains(Flags::HEAD), + self.flags.contains(Flags::STREAM), self.version, length, self.ctype, diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 8543aa213..82813a526 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -323,17 +323,17 @@ where match msg { Message::Item(mut req) => { match self.framed.get_codec().message_type() { - MessageType::Payload => { + MessageType::Payload | MessageType::Stream => { let (ps, pl) = Payload::create(false); let (req1, _) = req.replace_payload(crate::Payload::H1(pl)); req = req1; self.payload = Some(ps); } - MessageType::Stream => { - self.unhandled = Some(req); - return Ok(updated); - } + //MessageType::Stream => { + // self.unhandled = Some(req); + // return Ok(updated); + //} _ => (), } diff --git a/src/h1/encoder.rs b/src/h1/encoder.rs index 9fe5ba69a..712d123eb 100644 --- a/src/h1/encoder.rs +++ b/src/h1/encoder.rs @@ -241,6 +241,7 @@ impl MessageEncoder { dst: &mut BytesMut, message: &mut T, head: bool, + stream: bool, version: Version, length: BodyLength, ctype: ConnectionType, @@ -253,7 +254,7 @@ impl MessageEncoder { BodyLength::Sized(len) => TransferEncoding::length(len as u64), BodyLength::Sized64(len) => TransferEncoding::length(len), BodyLength::Stream => { - if message.chunked() { + if message.chunked() && !stream { TransferEncoding::chunked() } else { TransferEncoding::eof() From fd3e351c318c31e98dbd0354afb0cba654b0cdbf Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 22:02:03 -0700 Subject: [PATCH 311/427] add websockets context --- Cargo.toml | 4 +- actix-web-actors/Cargo.toml | 7 +- actix-web-actors/src/context.rs | 3 +- actix-web-actors/src/lib.rs | 6 + actix-web-actors/src/ws.rs | 408 ++++++++++++++++++++++++++++++ actix-web-actors/tests/test_ws.rs | 67 +++++ src/lib.rs | 2 +- 7 files changed, 490 insertions(+), 7 deletions(-) create mode 100644 actix-web-actors/src/ws.rs create mode 100644 actix-web-actors/tests/test_ws.rs diff --git a/Cargo.toml b/Cargo.toml index df06f3a12..d98c926bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,14 +61,14 @@ ssl = ["openssl", "actix-server/ssl"] # rust-tls = ["rustls", "actix-server/rustls"] [dependencies] -actix-codec = "0.1.0" +actix-codec = "0.1.1" actix-service = "0.3.4" actix-utils = "0.3.4" actix-router = "0.1.0" actix-rt = "0.2.1" actix-web-codegen = { path="actix-web-codegen" } actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-server = "0.4.0" +actix-server = "0.4.1" actix-server-config = "0.1.0" bytes = "0.4" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 698acc1c7..db42a1a23 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -20,9 +20,12 @@ path = "src/lib.rs" [dependencies] actix-web = { path=".." } actix = { git = "https://github.com/actix/actix.git" } - +actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-codec = "0.1.1" bytes = "0.4" futures = "0.1" [dev-dependencies] -actix-rt = "0.2.0" +env_logger = "0.6" +actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } diff --git a/actix-web-actors/src/context.rs b/actix-web-actors/src/context.rs index 646698efa..da473ff3f 100644 --- a/actix-web-actors/src/context.rs +++ b/actix-web-actors/src/context.rs @@ -198,10 +198,9 @@ mod tests { use std::time::Duration; use actix::Actor; - use actix_web::dev::HttpMessageBody; use actix_web::http::StatusCode; use actix_web::test::{block_on, call_success, init_service, TestRequest}; - use actix_web::{web, App, HttpRequest, HttpResponse}; + use actix_web::{web, App, HttpResponse}; use bytes::{Bytes, BytesMut}; use super::*; diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs index 47adf5f0a..0d4478654 100644 --- a/actix-web-actors/src/lib.rs +++ b/actix-web-actors/src/lib.rs @@ -1,4 +1,10 @@ //! Actix actors integration for Actix web framework mod context; +mod ws; pub use self::context::HttpContext; +pub use self::ws::{ws_handshake, ws_start, WebsocketContext}; + +pub use actix_http::ws::CloseCode as WsCloseCode; +pub use actix_http::ws::ProtocolError as WsProtocolError; +pub use actix_http::ws::{Frame as WsFrame, Message as WsMessage}; diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs new file mode 100644 index 000000000..dca0f46df --- /dev/null +++ b/actix-web-actors/src/ws.rs @@ -0,0 +1,408 @@ +use std::collections::VecDeque; +use std::io; + +use actix::dev::{ + AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, + ToEnvelope, +}; +use actix::fut::ActorFuture; +use actix::{ + Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, + Message as ActixMessage, SpawnHandle, +}; +use actix_codec::{Decoder, Encoder}; +use actix_http::ws::{ + hash_key, CloseReason, Codec, Frame, HandshakeError, Message, ProtocolError, +}; +use actix_web::dev::{Head, HttpResponseBuilder}; +use actix_web::error::{Error, ErrorInternalServerError, PayloadError}; +use actix_web::http::{header, Method, StatusCode}; +use actix_web::{HttpMessage, HttpRequest, HttpResponse}; +use bytes::{Bytes, BytesMut}; +use futures::sync::oneshot::Sender; +use futures::{Async, Future, Poll, Stream}; + +/// Do websocket handshake and start ws actor. +pub fn ws_start( + actor: A, + req: &HttpRequest, + stream: T, +) -> Result +where + A: Actor> + StreamHandler, + T: Stream + 'static, +{ + let mut res = ws_handshake(req)?; + Ok(res.streaming(WebsocketContext::create(actor, stream))) +} + +/// Prepare `WebSocket` handshake response. +/// +/// This function returns handshake `HttpResponse`, ready to send to peer. +/// It does not perform any IO. +/// +// /// `protocols` is a sequence of known protocols. On successful handshake, +// /// the returned response headers contain the first protocol in this list +// /// which the server also knows. +pub fn ws_handshake(req: &HttpRequest) -> Result { + // WebSocket accepts only GET + if *req.method() != Method::GET { + return Err(HandshakeError::GetMethodRequired); + } + + // Check for "UPGRADE" to websocket header + let has_hdr = if let Some(hdr) = req.headers().get(header::UPGRADE) { + if let Ok(s) = hdr.to_str() { + s.to_lowercase().contains("websocket") + } else { + false + } + } else { + false + }; + if !has_hdr { + return Err(HandshakeError::NoWebsocketUpgrade); + } + + // Upgrade connection + if !req.upgrade() { + return Err(HandshakeError::NoConnectionUpgrade); + } + + // check supported version + if !req.headers().contains_key(header::SEC_WEBSOCKET_VERSION) { + return Err(HandshakeError::NoVersionHeader); + } + let supported_ver = { + if let Some(hdr) = req.headers().get(header::SEC_WEBSOCKET_VERSION) { + hdr == "13" || hdr == "8" || hdr == "7" + } else { + false + } + }; + if !supported_ver { + return Err(HandshakeError::UnsupportedVersion); + } + + // check client handshake for validity + if !req.headers().contains_key(header::SEC_WEBSOCKET_KEY) { + return Err(HandshakeError::BadWebsocketKey); + } + let key = { + let key = req.headers().get(header::SEC_WEBSOCKET_KEY).unwrap(); + hash_key(key.as_ref()) + }; + + Ok(HttpResponse::build(StatusCode::SWITCHING_PROTOCOLS) + .header(header::CONNECTION, "upgrade") + .header(header::UPGRADE, "websocket") + .header(header::TRANSFER_ENCODING, "chunked") + .header(header::SEC_WEBSOCKET_ACCEPT, key.as_str()) + .take()) +} + +/// Execution context for `WebSockets` actors +pub struct WebsocketContext +where + A: Actor>, +{ + inner: ContextParts, + messages: VecDeque>, +} + +impl ActorContext for WebsocketContext +where + A: Actor, +{ + fn stop(&mut self) { + self.inner.stop(); + } + + fn terminate(&mut self) { + self.inner.terminate() + } + + fn state(&self) -> ActorState { + self.inner.state() + } +} + +impl AsyncContext for WebsocketContext +where + A: Actor, +{ + fn spawn(&mut self, fut: F) -> SpawnHandle + where + F: ActorFuture + 'static, + { + self.inner.spawn(fut) + } + + fn wait(&mut self, fut: F) + where + F: ActorFuture + 'static, + { + self.inner.wait(fut) + } + + #[doc(hidden)] + #[inline] + fn waiting(&self) -> bool { + self.inner.waiting() + || self.inner.state() == ActorState::Stopping + || self.inner.state() == ActorState::Stopped + } + + fn cancel_future(&mut self, handle: SpawnHandle) -> bool { + self.inner.cancel_future(handle) + } + + #[inline] + fn address(&self) -> Addr { + self.inner.address() + } +} + +impl WebsocketContext +where + A: Actor, +{ + #[inline] + /// Create a new Websocket context from a request and an actor + pub fn create(actor: A, stream: S) -> impl Stream + where + A: StreamHandler, + S: Stream + 'static, + { + let mb = Mailbox::default(); + let mut ctx = WebsocketContext { + inner: ContextParts::new(mb.sender_producer()), + messages: VecDeque::new(), + }; + ctx.add_stream(WsStream::new(stream)); + + WebsocketContextFut::new(ctx, actor, mb) + } + + /// Create a new Websocket context + pub fn with_factory( + stream: S, + f: F, + ) -> impl Stream + where + F: FnOnce(&mut Self) -> A + 'static, + A: StreamHandler, + S: Stream + 'static, + { + let mb = Mailbox::default(); + let mut ctx = WebsocketContext { + inner: ContextParts::new(mb.sender_producer()), + messages: VecDeque::new(), + }; + ctx.add_stream(WsStream::new(stream)); + + let act = f(&mut ctx); + + WebsocketContextFut::new(ctx, act, mb) + } +} + +impl WebsocketContext +where + A: Actor, +{ + /// Write payload + /// + /// This is a low-level function that accepts framed messages that should + /// be created using `Frame::message()`. If you want to send text or binary + /// data you should prefer the `text()` or `binary()` convenience functions + /// that handle the framing for you. + #[inline] + pub fn write_raw(&mut self, msg: Message) { + self.messages.push_back(Some(msg)); + } + + /// Send text frame + #[inline] + pub fn text>(&mut self, text: T) { + self.write_raw(Message::Text(text.into())); + } + + /// Send binary frame + #[inline] + pub fn binary>(&mut self, data: B) { + self.write_raw(Message::Binary(data.into())); + } + + /// Send ping frame + #[inline] + pub fn ping(&mut self, message: &str) { + self.write_raw(Message::Ping(message.to_string())); + } + + /// Send pong frame + #[inline] + pub fn pong(&mut self, message: &str) { + self.write_raw(Message::Pong(message.to_string())); + } + + /// Send close frame + #[inline] + pub fn close(&mut self, reason: Option) { + self.write_raw(Message::Close(reason)); + } + + /// Handle of the running future + /// + /// SpawnHandle is the handle returned by `AsyncContext::spawn()` method. + pub fn handle(&self) -> SpawnHandle { + self.inner.curr_handle() + } + + /// Set mailbox capacity + /// + /// By default mailbox capacity is 16 messages. + pub fn set_mailbox_capacity(&mut self, cap: usize) { + self.inner.set_mailbox_capacity(cap) + } +} + +impl AsyncContextParts for WebsocketContext +where + A: Actor, +{ + fn parts(&mut self) -> &mut ContextParts { + &mut self.inner + } +} + +struct WebsocketContextFut +where + A: Actor>, +{ + fut: ContextFut>, + encoder: Codec, + buf: BytesMut, + closed: bool, +} + +impl WebsocketContextFut +where + A: Actor>, +{ + fn new(ctx: WebsocketContext, act: A, mailbox: Mailbox) -> Self { + let fut = ContextFut::new(ctx, act, mailbox); + WebsocketContextFut { + fut, + encoder: Codec::new(), + buf: BytesMut::new(), + closed: false, + } + } +} + +impl Stream for WebsocketContextFut +where + A: Actor>, +{ + type Item = Bytes; + type Error = Error; + + fn poll(&mut self) -> Poll, Error> { + if self.fut.alive() && self.fut.poll().is_err() { + return Err(ErrorInternalServerError("error")); + } + + // encode messages + while let Some(item) = self.fut.ctx().messages.pop_front() { + if let Some(msg) = item { + self.encoder.encode(msg, &mut self.buf)?; + } else { + self.closed = true; + break; + } + } + + if !self.buf.is_empty() { + Ok(Async::Ready(Some(self.buf.take().freeze()))) + } else if self.fut.alive() && !self.closed { + Ok(Async::NotReady) + } else { + Ok(Async::Ready(None)) + } + } +} + +impl ToEnvelope for WebsocketContext +where + A: Actor> + Handler, + M: ActixMessage + Send + 'static, + M::Result: Send, +{ + fn pack(msg: M, tx: Option>) -> Envelope { + Envelope::new(msg, tx) + } +} + +struct WsStream { + stream: S, + decoder: Codec, + buf: BytesMut, + closed: bool, +} + +impl WsStream +where + S: Stream, +{ + fn new(stream: S) -> Self { + Self { + stream, + decoder: Codec::new(), + buf: BytesMut::new(), + closed: false, + } + } +} + +impl Stream for WsStream +where + S: Stream, +{ + type Item = Frame; + type Error = ProtocolError; + + fn poll(&mut self) -> Poll, Self::Error> { + if !self.closed { + loop { + match self.stream.poll() { + Ok(Async::Ready(Some(chunk))) => { + self.buf.extend_from_slice(&chunk[..]); + } + Ok(Async::Ready(None)) => { + self.closed = true; + break; + } + Ok(Async::NotReady) => break, + Err(e) => { + return Err(ProtocolError::Io(io::Error::new( + io::ErrorKind::Other, + format!("{}", e), + ))); + } + } + } + } + + match self.decoder.decode(&mut self.buf)? { + None => { + if self.closed { + Ok(Async::Ready(None)) + } else { + Ok(Async::NotReady) + } + } + Some(frm) => Ok(Async::Ready(Some(frm))), + } + } +} diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs new file mode 100644 index 000000000..0a214644f --- /dev/null +++ b/actix-web-actors/tests/test_ws.rs @@ -0,0 +1,67 @@ +use actix::prelude::*; +use actix_http::HttpService; +use actix_http_test::TestServer; +use actix_web::{web, App, HttpRequest}; +use actix_web_actors::*; +use bytes::{Bytes, BytesMut}; +use futures::{Sink, Stream}; + +struct Ws; + +impl Actor for Ws { + type Context = WebsocketContext; +} + +impl StreamHandler for Ws { + fn handle(&mut self, msg: WsFrame, ctx: &mut Self::Context) { + match msg { + WsFrame::Ping(msg) => ctx.pong(&msg), + WsFrame::Text(text) => { + ctx.text(String::from_utf8_lossy(&text.unwrap())).to_owned() + } + WsFrame::Binary(bin) => ctx.binary(bin.unwrap()), + WsFrame::Close(reason) => ctx.close(reason), + _ => (), + } + } +} + +#[test] +fn test_simple() { + let mut srv = + TestServer::new(|| { + HttpService::new(App::new().service(web::resource("/").to( + |req: HttpRequest, stream: web::Payload<_>| ws_start(Ws, &req, stream), + ))) + }); + + // client service + let framed = srv.ws().unwrap(); + let framed = srv + .block_on(framed.send(WsMessage::Text("text".to_string()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!(item, Some(WsFrame::Text(Some(BytesMut::from("text"))))); + + let framed = srv + .block_on(framed.send(WsMessage::Binary("text".into()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!( + item, + Some(WsFrame::Binary(Some(Bytes::from_static(b"text").into()))) + ); + + let framed = srv + .block_on(framed.send(WsMessage::Ping("text".into()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!(item, Some(WsFrame::Pong("text".to_string().into()))); + + let framed = srv + .block_on(framed.send(WsMessage::Close(Some(WsCloseCode::Normal.into())))) + .unwrap(); + + let (item, _framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!(item, Some(WsFrame::Close(Some(WsCloseCode::Normal.into())))); +} diff --git a/src/lib.rs b/src/lib.rs index dbf8f7439..f59cbc265 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,7 +66,7 @@ pub mod dev { pub use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; pub use actix_http::ResponseBuilder as HttpResponseBuilder; pub use actix_http::{ - Extensions, Payload, PayloadStream, RequestHead, ResponseHead, + Extensions, Head, Payload, PayloadStream, RequestHead, ResponseHead, }; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::Server; From 6ab76658680b7a1b1f9c4ed01b68b1c00a3140bc Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 22:11:50 -0700 Subject: [PATCH 312/427] export ws module --- actix-web-actors/src/lib.rs | 7 +---- actix-web-actors/src/ws.rs | 17 ++++++------ actix-web-actors/tests/test_ws.rs | 44 ++++++++++++++++--------------- 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs index 0d4478654..5b64d7e0a 100644 --- a/actix-web-actors/src/lib.rs +++ b/actix-web-actors/src/lib.rs @@ -1,10 +1,5 @@ //! Actix actors integration for Actix web framework mod context; -mod ws; +pub mod ws; pub use self::context::HttpContext; -pub use self::ws::{ws_handshake, ws_start, WebsocketContext}; - -pub use actix_http::ws::CloseCode as WsCloseCode; -pub use actix_http::ws::ProtocolError as WsProtocolError; -pub use actix_http::ws::{Frame as WsFrame, Message as WsMessage}; diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index dca0f46df..b5f5c08c2 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -1,3 +1,4 @@ +//! Websocket integration use std::collections::VecDeque; use std::io; @@ -11,9 +12,11 @@ use actix::{ Message as ActixMessage, SpawnHandle, }; use actix_codec::{Decoder, Encoder}; -use actix_http::ws::{ - hash_key, CloseReason, Codec, Frame, HandshakeError, Message, ProtocolError, +use actix_http::ws::hash_key; +pub use actix_http::ws::{ + CloseCode, CloseReason, Codec, Frame, HandshakeError, Message, ProtocolError, }; + use actix_web::dev::{Head, HttpResponseBuilder}; use actix_web::error::{Error, ErrorInternalServerError, PayloadError}; use actix_web::http::{header, Method, StatusCode}; @@ -23,16 +26,12 @@ use futures::sync::oneshot::Sender; use futures::{Async, Future, Poll, Stream}; /// Do websocket handshake and start ws actor. -pub fn ws_start( - actor: A, - req: &HttpRequest, - stream: T, -) -> Result +pub fn start(actor: A, req: &HttpRequest, stream: T) -> Result where A: Actor> + StreamHandler, T: Stream + 'static, { - let mut res = ws_handshake(req)?; + let mut res = handshake(req)?; Ok(res.streaming(WebsocketContext::create(actor, stream))) } @@ -44,7 +43,7 @@ where // /// `protocols` is a sequence of known protocols. On successful handshake, // /// the returned response headers contain the first protocol in this list // /// which the server also knows. -pub fn ws_handshake(req: &HttpRequest) -> Result { +pub fn handshake(req: &HttpRequest) -> Result { // WebSocket accepts only GET if *req.method() != Method::GET { return Err(HandshakeError::GetMethodRequired); diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs index 0a214644f..ea9c8d8fe 100644 --- a/actix-web-actors/tests/test_ws.rs +++ b/actix-web-actors/tests/test_ws.rs @@ -9,18 +9,18 @@ use futures::{Sink, Stream}; struct Ws; impl Actor for Ws { - type Context = WebsocketContext; + type Context = ws::WebsocketContext; } -impl StreamHandler for Ws { - fn handle(&mut self, msg: WsFrame, ctx: &mut Self::Context) { +impl StreamHandler for Ws { + fn handle(&mut self, msg: ws::Frame, ctx: &mut Self::Context) { match msg { - WsFrame::Ping(msg) => ctx.pong(&msg), - WsFrame::Text(text) => { + ws::Frame::Ping(msg) => ctx.pong(&msg), + ws::Frame::Text(text) => { ctx.text(String::from_utf8_lossy(&text.unwrap())).to_owned() } - WsFrame::Binary(bin) => ctx.binary(bin.unwrap()), - WsFrame::Close(reason) => ctx.close(reason), + ws::Frame::Binary(bin) => ctx.binary(bin.unwrap()), + ws::Frame::Close(reason) => ctx.close(reason), _ => (), } } @@ -28,40 +28,42 @@ impl StreamHandler for Ws { #[test] fn test_simple() { - let mut srv = - TestServer::new(|| { - HttpService::new(App::new().service(web::resource("/").to( - |req: HttpRequest, stream: web::Payload<_>| ws_start(Ws, &req, stream), - ))) - }); + let mut srv = TestServer::new(|| { + HttpService::new(App::new().service(web::resource("/").to( + |req: HttpRequest, stream: web::Payload<_>| ws::start(Ws, &req, stream), + ))) + }); // client service let framed = srv.ws().unwrap(); let framed = srv - .block_on(framed.send(WsMessage::Text("text".to_string()))) + .block_on(framed.send(ws::Message::Text("text".to_string()))) .unwrap(); let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); - assert_eq!(item, Some(WsFrame::Text(Some(BytesMut::from("text"))))); + assert_eq!(item, Some(ws::Frame::Text(Some(BytesMut::from("text"))))); let framed = srv - .block_on(framed.send(WsMessage::Binary("text".into()))) + .block_on(framed.send(ws::Message::Binary("text".into()))) .unwrap(); let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); assert_eq!( item, - Some(WsFrame::Binary(Some(Bytes::from_static(b"text").into()))) + Some(ws::Frame::Binary(Some(Bytes::from_static(b"text").into()))) ); let framed = srv - .block_on(framed.send(WsMessage::Ping("text".into()))) + .block_on(framed.send(ws::Message::Ping("text".into()))) .unwrap(); let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); - assert_eq!(item, Some(WsFrame::Pong("text".to_string().into()))); + assert_eq!(item, Some(ws::Frame::Pong("text".to_string().into()))); let framed = srv - .block_on(framed.send(WsMessage::Close(Some(WsCloseCode::Normal.into())))) + .block_on(framed.send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))) .unwrap(); let (item, _framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); - assert_eq!(item, Some(WsFrame::Close(Some(WsCloseCode::Normal.into())))); + assert_eq!( + item, + Some(ws::Frame::Close(Some(ws::CloseCode::Normal.into()))) + ); } From b0343eb22d8a371fb84cdf304e4eb58f94dad700 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 22:31:10 -0700 Subject: [PATCH 313/427] simplify ws stream interface --- actix-web-actors/src/ws.rs | 35 ++++++++++++++++++++++++------- actix-web-actors/tests/test_ws.rs | 14 ++++++------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index b5f5c08c2..546326272 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -12,9 +12,9 @@ use actix::{ Message as ActixMessage, SpawnHandle, }; use actix_codec::{Decoder, Encoder}; -use actix_http::ws::hash_key; +use actix_http::ws::{hash_key, Codec}; pub use actix_http::ws::{ - CloseCode, CloseReason, Codec, Frame, HandshakeError, Message, ProtocolError, + CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError, }; use actix_web::dev::{Head, HttpResponseBuilder}; @@ -28,7 +28,7 @@ use futures::{Async, Future, Poll, Stream}; /// Do websocket handshake and start ws actor. pub fn start(actor: A, req: &HttpRequest, stream: T) -> Result where - A: Actor> + StreamHandler, + A: Actor> + StreamHandler, T: Stream + 'static, { let mut res = handshake(req)?; @@ -170,7 +170,7 @@ where /// Create a new Websocket context from a request and an actor pub fn create(actor: A, stream: S) -> impl Stream where - A: StreamHandler, + A: StreamHandler, S: Stream + 'static, { let mb = Mailbox::default(); @@ -190,7 +190,7 @@ where ) -> impl Stream where F: FnOnce(&mut Self) -> A + 'static, - A: StreamHandler, + A: StreamHandler, S: Stream + 'static, { let mb = Mailbox::default(); @@ -368,7 +368,7 @@ impl Stream for WsStream where S: Stream, { - type Item = Frame; + type Item = Message; type Error = ProtocolError; fn poll(&mut self) -> Poll, Self::Error> { @@ -401,7 +401,28 @@ where Ok(Async::NotReady) } } - Some(frm) => Ok(Async::Ready(Some(frm))), + Some(frm) => { + let msg = match frm { + Frame::Text(data) => { + if let Some(data) = data { + Message::Text( + std::str::from_utf8(&data) + .map_err(|_| ProtocolError::BadEncoding)? + .to_string(), + ) + } else { + Message::Text(String::new()) + } + } + Frame::Binary(data) => Message::Binary( + data.map(|b| b.freeze()).unwrap_or_else(|| Bytes::new()), + ), + Frame::Ping(s) => Message::Ping(s), + Frame::Pong(s) => Message::Pong(s), + Frame::Close(reason) => Message::Close(reason), + }; + Ok(Async::Ready(Some(msg))) + } } } } diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs index ea9c8d8fe..202d562ca 100644 --- a/actix-web-actors/tests/test_ws.rs +++ b/actix-web-actors/tests/test_ws.rs @@ -12,15 +12,13 @@ impl Actor for Ws { type Context = ws::WebsocketContext; } -impl StreamHandler for Ws { - fn handle(&mut self, msg: ws::Frame, ctx: &mut Self::Context) { +impl StreamHandler for Ws { + fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { match msg { - ws::Frame::Ping(msg) => ctx.pong(&msg), - ws::Frame::Text(text) => { - ctx.text(String::from_utf8_lossy(&text.unwrap())).to_owned() - } - ws::Frame::Binary(bin) => ctx.binary(bin.unwrap()), - ws::Frame::Close(reason) => ctx.close(reason), + ws::Message::Ping(msg) => ctx.pong(&msg), + ws::Message::Text(text) => ctx.text(text), + ws::Message::Binary(bin) => ctx.binary(bin), + ws::Message::Close(reason) => ctx.close(reason), _ => (), } } From 3301a46264fb5c49456b38d0b275845ec77d2784 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 22:56:13 -0700 Subject: [PATCH 314/427] proper connection upgrade check --- src/message.rs | 12 ++++++++++-- src/ws/mod.rs | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/message.rs b/src/message.rs index f9dfe9736..bf465ee6d 100644 --- a/src/message.rs +++ b/src/message.rs @@ -3,7 +3,7 @@ use std::collections::VecDeque; use std::rc::Rc; use crate::extensions::Extensions; -use crate::http::{HeaderMap, Method, StatusCode, Uri, Version}; +use crate::http::{header, HeaderMap, Method, StatusCode, Uri, Version}; /// Represents various types of connection #[derive(Copy, Clone, PartialEq, Debug)] @@ -33,7 +33,15 @@ pub trait Head: Default + 'static { fn set_connection_type(&mut self, ctype: ConnectionType); fn upgrade(&self) -> bool { - self.connection_type() == ConnectionType::Upgrade + if let Some(hdr) = self.headers().get(header::CONNECTION) { + if let Ok(s) = hdr.to_str() { + s.to_ascii_lowercase().contains("upgrade") + } else { + false + } + } else { + false + } } /// Check if keep-alive is enabled diff --git a/src/ws/mod.rs b/src/ws/mod.rs index 3d3f5b925..88fabde94 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -132,7 +132,7 @@ pub fn verify_handshake(req: &Request) -> Result<(), HandshakeError> { // Check for "UPGRADE" to websocket header let has_hdr = if let Some(hdr) = req.headers().get(header::UPGRADE) { if let Ok(s) = hdr.to_str() { - s.to_lowercase().contains("websocket") + s.to_ascii_lowercase().contains("websocket") } else { false } From efe3025395ac2202a22639e3ac98f67617af0685 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 22:57:27 -0700 Subject: [PATCH 315/427] add handshake test --- actix-web-actors/src/ws.rs | 122 ++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index 546326272..a5d266236 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -52,7 +52,7 @@ pub fn handshake(req: &HttpRequest) -> Result Date: Mon, 18 Mar 2019 05:26:12 -0700 Subject: [PATCH 316/427] fix response upgrade type --- src/message.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/message.rs b/src/message.rs index bf465ee6d..4e46093f6 100644 --- a/src/message.rs +++ b/src/message.rs @@ -106,6 +106,18 @@ impl Head for RequestHead { } } + fn upgrade(&self) -> bool { + if let Some(hdr) = self.headers().get(header::CONNECTION) { + if let Ok(s) = hdr.to_str() { + s.to_ascii_lowercase().contains("upgrade") + } else { + false + } + } else { + false + } + } + fn pool() -> &'static MessagePool { REQUEST_POOL.with(|p| *p) } @@ -194,6 +206,10 @@ impl Head for ResponseHead { } } + fn upgrade(&self) -> bool { + self.connection_type() == ConnectionType::Upgrade + } + fn pool() -> &'static MessagePool { RESPONSE_POOL.with(|p| *p) } From 8872f3b590266b08b9ada4502b05397863d3e200 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 18 Mar 2019 05:30:18 -0700 Subject: [PATCH 317/427] fix ws upgrade --- actix-web-actors/src/ws.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index a5d266236..cef5080c6 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -93,8 +93,7 @@ pub fn handshake(req: &HttpRequest) -> Result Date: Mon, 18 Mar 2019 09:44:48 -0700 Subject: [PATCH 318/427] handle socket shutdown for h1 connections --- src/client/h2proto.rs | 6 +-- src/config.rs | 10 ++-- src/h1/dispatcher.rs | 112 ++++++++++++++++++++++------------------- src/h2/dispatcher.rs | 2 +- src/payload.rs | 2 +- src/service/service.rs | 2 +- src/ws/client/mod.rs | 24 ++++----- 7 files changed, 81 insertions(+), 77 deletions(-) diff --git a/src/client/h2proto.rs b/src/client/h2proto.rs index c05aeddbe..bf2d3e1b2 100644 --- a/src/client/h2proto.rs +++ b/src/client/h2proto.rs @@ -5,7 +5,7 @@ use bytes::Bytes; use futures::future::{err, Either}; use futures::{Async, Future, Poll}; use h2::{client::SendRequest, SendStream}; -use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; +use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING}; use http::{request::Request, HttpTryFrom, Method, Version}; use crate::body::{BodyLength, MessageBody}; @@ -45,7 +45,7 @@ where *req.version_mut() = Version::HTTP_2; let mut skip_len = true; - let mut has_date = false; + // let mut has_date = false; // Content length let _ = match length { @@ -72,7 +72,7 @@ where match *key { CONNECTION | TRANSFER_ENCODING => continue, // http2 specific CONTENT_LENGTH if skip_len => continue, - DATE => has_date = true, + // DATE => has_date = true, _ => (), } req.headers_mut().append(key, value.clone()); diff --git a/src/config.rs b/src/config.rs index 3c7df2feb..f7d7f5f94 100644 --- a/src/config.rs +++ b/src/config.rs @@ -85,7 +85,7 @@ impl ServiceConfig { ka_enabled, client_timeout, client_disconnect, - timer: DateService::with(Duration::from_millis(500)), + timer: DateService::new(), })) } @@ -204,14 +204,12 @@ impl fmt::Write for Date { struct DateService(Rc); struct DateServiceInner { - interval: Duration, current: UnsafeCell>, } impl DateServiceInner { - fn new(interval: Duration) -> Self { + fn new() -> Self { DateServiceInner { - interval, current: UnsafeCell::new(None), } } @@ -232,8 +230,8 @@ impl DateServiceInner { } impl DateService { - fn with(resolution: Duration) -> Self { - DateService(Rc::new(DateServiceInner::new(resolution))) + fn new() -> Self { + DateService(Rc::new(DateServiceInner::new())) } fn check_date(&self) { diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 82813a526..8e21e9b09 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -7,7 +7,7 @@ use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_service::Service; use actix_utils::cloneable::CloneableService; use bitflags::bitflags; -use futures::{try_ready, Async, Future, Poll, Sink, Stream}; +use futures::{Async, Future, Poll, Sink, Stream}; use log::{debug, error, trace}; use tokio_timer::Delay; @@ -32,6 +32,7 @@ bitflags! { const POLLED = 0b0000_1000; const SHUTDOWN = 0b0010_0000; const DISCONNECTED = 0b0100_0000; + const DROPPING = 0b1000_0000; } } @@ -56,7 +57,6 @@ where state: State, payload: Option, messages: VecDeque, - unhandled: Option, ka_expire: Instant, ka_timer: Option, @@ -131,7 +131,6 @@ where state: State::None, error: None, messages: VecDeque::new(), - unhandled: None, service, flags, config, @@ -411,8 +410,19 @@ where /// keep-alive timer fn poll_keepalive(&mut self) -> Result<(), DispatchError> { if self.ka_timer.is_none() { - return Ok(()); + // shutdown timeout + if self.flags.contains(Flags::SHUTDOWN) { + if let Some(interval) = self.config.client_disconnect_timer() { + self.ka_timer = Some(Delay::new(interval)); + } else { + self.flags.insert(Flags::DISCONNECTED); + return Ok(()); + } + } else { + return Ok(()); + } } + match self.ka_timer.as_mut().unwrap().poll().map_err(|e| { error!("Timer error {:?}", e); DispatchError::Unknown @@ -436,6 +446,8 @@ where let _ = timer.poll(); } } else { + // no shutdown timeout, drop socket + self.flags.insert(Flags::DISCONNECTED); return Ok(()); } } else { @@ -483,61 +495,55 @@ where #[inline] fn poll(&mut self) -> Poll { - let shutdown = if let Some(ref mut inner) = self.inner { - if inner.flags.contains(Flags::SHUTDOWN) { - inner.poll_keepalive()?; - try_ready!(inner.poll_flush()); - true + let inner = self.inner.as_mut().unwrap(); + + if inner.flags.contains(Flags::SHUTDOWN) { + inner.poll_keepalive()?; + if inner.flags.contains(Flags::DISCONNECTED) { + Ok(Async::Ready(())) } else { - inner.poll_keepalive()?; - inner.poll_request()?; - loop { - inner.poll_response()?; - if let Async::Ready(false) = inner.poll_flush()? { - break; - } - } - - if inner.flags.contains(Flags::DISCONNECTED) { - return Ok(Async::Ready(())); - } - - // keep-alive and stream errors - if inner.state.is_empty() && inner.framed.is_write_buf_empty() { - if let Some(err) = inner.error.take() { - return Err(err); - } - // unhandled request (upgrade or connect) - else if inner.unhandled.is_some() { - false - } - // disconnect if keep-alive is not enabled - else if inner.flags.contains(Flags::STARTED) - && !inner.flags.intersects(Flags::KEEPALIVE) - { - true - } - // disconnect if shutdown - else if inner.flags.contains(Flags::SHUTDOWN) { - true - } else { - return Ok(Async::NotReady); - } - } else { - return Ok(Async::NotReady); + // try_ready!(inner.poll_flush()); + match inner.framed.get_mut().shutdown()? { + Async::Ready(_) => Ok(Async::Ready(())), + Async::NotReady => Ok(Async::NotReady), } } } else { - unreachable!() - }; + inner.poll_keepalive()?; + inner.poll_request()?; + loop { + inner.poll_response()?; + if let Async::Ready(false) = inner.poll_flush()? { + break; + } + } - let mut inner = self.inner.take().unwrap(); + if inner.flags.contains(Flags::DISCONNECTED) { + return Ok(Async::Ready(())); + } - // TODO: shutdown - Ok(Async::Ready(())) - //Ok(Async::Ready(HttpServiceResult::Shutdown( - // inner.framed.into_inner(), - //))) + // keep-alive and stream errors + if inner.state.is_empty() && inner.framed.is_write_buf_empty() { + if let Some(err) = inner.error.take() { + return Err(err); + } + // disconnect if keep-alive is not enabled + else if inner.flags.contains(Flags::STARTED) + && !inner.flags.intersects(Flags::KEEPALIVE) + { + inner.flags.insert(Flags::SHUTDOWN); + self.poll() + } + // disconnect if shutdown + else if inner.flags.contains(Flags::SHUTDOWN) { + self.poll() + } else { + return Ok(Async::NotReady); + } + } else { + return Ok(Async::NotReady); + } + } } } diff --git a/src/h2/dispatcher.rs b/src/h2/dispatcher.rs index cbba34c0d..ea63dc2bc 100644 --- a/src/h2/dispatcher.rs +++ b/src/h2/dispatcher.rs @@ -56,7 +56,7 @@ where config: ServiceConfig, timeout: Option, ) -> Self { - let keepalive = config.keep_alive_enabled(); + // let keepalive = config.keep_alive_enabled(); // let flags = if keepalive { // Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED // } else { diff --git a/src/payload.rs b/src/payload.rs index 8f96bab95..91e6b5c95 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -41,7 +41,7 @@ impl From for Payload { impl Payload { /// Takes current payload and replaces it with `None` value - fn take(&mut self) -> Payload { + pub fn take(&mut self) -> Payload { std::mem::replace(self, Payload::None) } } diff --git a/src/service/service.rs b/src/service/service.rs index 3ddf55739..0bc1634d8 100644 --- a/src/service/service.rs +++ b/src/service/service.rs @@ -169,7 +169,7 @@ where } fn call(&mut self, req: Self::Request) -> Self::Future { - let (io, params, proto) = req.into_parts(); + let (io, _, proto) = req.into_parts(); match proto { Protocol::Http2 => { let io = Io { diff --git a/src/ws/client/mod.rs b/src/ws/client/mod.rs index 0dbf081c6..a5c221967 100644 --- a/src/ws/client/mod.rs +++ b/src/ws/client/mod.rs @@ -25,19 +25,19 @@ impl Protocol { } } - fn is_http(self) -> bool { - match self { - Protocol::Https | Protocol::Http => true, - _ => false, - } - } + // fn is_http(self) -> bool { + // match self { + // Protocol::Https | Protocol::Http => true, + // _ => false, + // } + // } - fn is_secure(self) -> bool { - match self { - Protocol::Https | Protocol::Wss => true, - _ => false, - } - } + // fn is_secure(self) -> bool { + // match self { + // Protocol::Https | Protocol::Wss => true, + // _ => false, + // } + // } fn port(self) -> u16 { match self { From c5c7b244be264154ad9b60335a88722b0139206b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 23 Mar 2019 09:40:20 -0700 Subject: [PATCH 319/427] cookie is optional --- .travis.yml | 4 +- Cargo.toml | 24 +++++------ src/client/request.rs | 37 +++++++++++------ src/error.rs | 17 +++++--- src/h1/dispatcher.rs | 3 +- src/httpmessage.rs | 11 ++++- src/lib.rs | 1 + src/response.rs | 89 ++++++++++++++++++++++++++-------------- src/test.rs | 57 +++++++++++++------------ src/ws/client/connect.rs | 2 + 10 files changed, 151 insertions(+), 94 deletions(-) diff --git a/.travis.yml b/.travis.yml index ff8815059..02fbd42c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,8 +31,8 @@ before_cache: | script: - cargo clean -- cargo build --features="ssl" -- cargo test --features="ssl" +- cargo build --all-features +- cargo test --all-features # Upload docs after_success: diff --git a/Cargo.toml b/Cargo.toml index a68489f51..84b974be5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,20 +7,20 @@ readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-http.git" -documentation = "https://actix.rs/api/actix-http/stable/actix_http/" +documentation = "https://docs.rs/actix-http/" categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] -license = "Apache-2.0" +license = "MIT/Apache-2.0" exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] edition = "2018" [package.metadata.docs.rs] -features = ["session"] +features = ["ssl", "fail", "cookie"] [badges] travis-ci = { repository = "actix/actix-http", branch = "master" } -appveyor = { repository = "fafhrd91/actix-http-b1qsn" } +# appveyor = { repository = "fafhrd91/actix-http-b1qsn" } codecov = { repository = "actix/actix-http", branch = "master", service = "github" } [lib] @@ -28,13 +28,15 @@ name = "actix_http" path = "src/lib.rs" [features] -default = ["fail"] +default = [] # openssl ssl = ["openssl", "actix-connect/ssl"] -# failure integration. it is on by default, it will be off in future versions -# actix itself does not use failure anymore +# cookies integration +cookies = ["cookie"] + +# failure integration. actix does not use failure anymore fail = ["failure"] [dependencies] @@ -49,7 +51,6 @@ backtrace = "0.3" bitflags = "1.0" bytes = "0.4" byteorder = "1.2" -cookie = { version="0.11", features=["percent-encode"] } derive_more = "0.14" encoding = "0.2" futures = "0.1" @@ -76,11 +77,10 @@ tokio-timer = "0.2" tokio-current-thread = "0.1" trust-dns-resolver = { version="0.11.0-alpha.2", default-features = false } -# openssl -openssl = { version="0.10", optional = true } - -# failure is optional +# optional deps +cookie = { version="0.11", features=["percent-encode"], optional = true } failure = { version = "0.1.5", optional = true } +openssl = { version="0.10", optional = true } [dev-dependencies] actix-rt = "0.2.0" diff --git a/src/client/request.rs b/src/client/request.rs index 7c7079fb7..26713aa46 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -1,13 +1,12 @@ use std::fmt; -use std::fmt::Write as FmtWrite; use std::io::Write; use actix_service::Service; use bytes::{BufMut, Bytes, BytesMut}; +#[cfg(feature = "cookies")] use cookie::{Cookie, CookieJar}; use futures::future::{err, Either}; use futures::{Future, Stream}; -use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use serde::Serialize; use serde_json; @@ -58,6 +57,7 @@ impl ClientRequest<()> { ClientRequestBuilder { head: Some(RequestHead::default()), err: None, + #[cfg(feature = "cookies")] cookies: None, default_headers: true, } @@ -235,6 +235,7 @@ where pub struct ClientRequestBuilder { head: Option, err: Option, + #[cfg(feature = "cookies")] cookies: Option, default_headers: bool, } @@ -441,6 +442,7 @@ impl ClientRequestBuilder { self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze()) } + #[cfg(feature = "cookies")] /// Set a cookie /// /// ```rust @@ -560,20 +562,28 @@ impl ClientRequestBuilder { ); } + #[allow(unused_mut)] let mut head = self.head.take().expect("cannot reuse request builder"); - // set cookies - if let Some(ref mut jar) = self.cookies { - let mut cookie = String::new(); - for c in jar.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); - let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); - let _ = write!(&mut cookie, "; {}={}", name, value); + #[cfg(feature = "cookies")] + { + use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; + use std::fmt::Write; + + // set cookies + if let Some(ref mut jar) = self.cookies { + let mut cookie = String::new(); + for c in jar.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = + percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); + } + head.headers.insert( + header::COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); } - head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); } Ok(ClientRequest { head, body }) } @@ -646,6 +656,7 @@ impl ClientRequestBuilder { ClientRequestBuilder { head: self.head.take(), err: self.err.take(), + #[cfg(feature = "cookies")] cookies: self.cookies.take(), default_headers: self.default_headers, } diff --git a/src/error.rs b/src/error.rs index 6bc401332..820071b1e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,6 +8,7 @@ use std::{fmt, io, result}; // use actix::MailboxError; use actix_utils::timeout::TimeoutError; use backtrace::Backtrace; +#[cfg(feature = "cookies")] use cookie; use derive_more::{Display, From}; use futures::Canceled; @@ -19,7 +20,8 @@ use serde_json::error::Error as JsonError; use serde_urlencoded::ser::Error as FormError; use tokio_timer::Error as TimerError; -// re-exports +// re-export for convinience +#[cfg(feature = "cookies")] pub use cookie::ParseError as CookieParseError; use crate::body::Body; @@ -322,6 +324,7 @@ impl ResponseError for PayloadError { } /// Return `BadRequest` for `cookie::ParseError` +#[cfg(feature = "cookies")] impl ResponseError for cookie::ParseError { fn error_response(&self) -> Response { Response::new(StatusCode::BAD_REQUEST) @@ -889,7 +892,6 @@ mod failure_integration { #[cfg(test)] mod tests { use super::*; - use cookie::ParseError as CookieParseError; use http::{Error as HttpError, StatusCode}; use httparse; use std::error::Error as StdError; @@ -900,14 +902,19 @@ mod tests { let resp: Response = ParseError::Incomplete.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: Response = CookieParseError::EmptyName.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into(); let resp: Response = err.error_response(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + #[cfg(feature = "cookies")] + #[test] + fn test_cookie_parse() { + use cookie::ParseError as CookieParseError; + let resp: Response = CookieParseError::EmptyName.error_response(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + #[test] fn test_as_response() { let orig = io::Error::new(io::ErrorKind::Other, "other"); diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index 8e21e9b09..afeabc825 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -613,13 +613,12 @@ mod tests { let mut sys = actix_rt::System::new("test"); let _ = sys.block_on(lazy(|| { let buf = Buffer::new("GET /test HTTP/1\r\n\r\n"); - let readbuf = BytesMut::new(); let mut h1 = Dispatcher::new( buf, ServiceConfig::default(), CloneableService::new( - (|req| ok::<_, Error>(Response::Ok().finish())).into_service(), + (|_| ok::<_, Error>(Response::Ok().finish())).into_service(), ), ); assert!(h1.poll().is_ok()); diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 117e10a81..60821d300 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -1,18 +1,23 @@ use std::cell::{Ref, RefMut}; use std::str; -use cookie::Cookie; use encoding::all::UTF_8; use encoding::label::encoding_from_whatwg_label; use encoding::EncodingRef; use http::{header, HeaderMap}; use mime::Mime; -use crate::error::{ContentTypeError, CookieParseError, ParseError}; +use crate::error::{ContentTypeError, ParseError}; use crate::extensions::Extensions; use crate::header::Header; use crate::payload::Payload; +#[cfg(feature = "cookies")] +use crate::error::CookieParseError; +#[cfg(feature = "cookies")] +use cookie::Cookie; + +#[cfg(feature = "cookies")] struct Cookies(Vec>); /// Trait that implements general purpose operations on http messages @@ -105,6 +110,7 @@ pub trait HttpMessage: Sized { /// Load request cookies. #[inline] + #[cfg(feature = "cookies")] fn cookies(&self) -> Result>>, CookieParseError> { if self.extensions().get::().is_none() { let mut cookies = Vec::new(); @@ -125,6 +131,7 @@ pub trait HttpMessage: Sized { } /// Return request cookie. + #[cfg(feature = "cookies")] fn cookie(&self, name: &str) -> Option> { if let Ok(cookies) = self.cookies() { for cookie in cookies.iter() { diff --git a/src/lib.rs b/src/lib.rs index 9a87b77f1..85efe5e44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,6 +113,7 @@ pub mod http { #[doc(hidden)] pub use http::uri::PathAndQuery; + #[cfg(feature = "cookies")] pub use cookie::{Cookie, CookieBuilder}; /// Various http headers diff --git a/src/response.rs b/src/response.rs index 34ac54ea7..5281c2d95 100644 --- a/src/response.rs +++ b/src/response.rs @@ -3,6 +3,7 @@ use std::io::Write; use std::{fmt, str}; use bytes::{BufMut, Bytes, BytesMut}; +#[cfg(feature = "cookies")] use cookie::{Cookie, CookieJar}; use futures::future::{ok, FutureResult, IntoFuture}; use futures::Stream; @@ -128,6 +129,7 @@ impl Response { /// Get an iterator for the cookies set by this response #[inline] + #[cfg(feature = "cookies")] pub fn cookies(&self) -> CookieIter { CookieIter { iter: self.head.headers.get_all(header::SET_COOKIE).iter(), @@ -136,6 +138,7 @@ impl Response { /// Add a cookie to this response #[inline] + #[cfg(feature = "cookies")] pub fn add_cookie(&mut self, cookie: &Cookie) -> Result<(), HttpError> { let h = &mut self.head.headers; HeaderValue::from_str(&cookie.to_string()) @@ -148,6 +151,7 @@ impl Response { /// Remove all cookies with the given name from this response. Returns /// the number of cookies removed. #[inline] + #[cfg(feature = "cookies")] pub fn del_cookie(&mut self, name: &str) -> usize { let h = &mut self.head.headers; let vals: Vec = h @@ -267,10 +271,12 @@ impl IntoFuture for Response { } } +#[cfg(feature = "cookies")] pub struct CookieIter<'a> { iter: header::ValueIter<'a, HeaderValue>, } +#[cfg(feature = "cookies")] impl<'a> Iterator for CookieIter<'a> { type Item = Cookie<'a>; @@ -292,6 +298,7 @@ impl<'a> Iterator for CookieIter<'a> { pub struct ResponseBuilder { head: Option>, err: Option, + #[cfg(feature = "cookies")] cookies: Option, } @@ -304,6 +311,7 @@ impl ResponseBuilder { ResponseBuilder { head: Some(head), err: None, + #[cfg(feature = "cookies")] cookies: None, } } @@ -503,6 +511,7 @@ impl ResponseBuilder { /// .finish() /// } /// ``` + #[cfg(feature = "cookies")] pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { if self.cookies.is_none() { let mut jar = CookieJar::new(); @@ -530,6 +539,7 @@ impl ResponseBuilder { /// builder.finish() /// } /// ``` + #[cfg(feature = "cookies")] pub fn del_cookie<'a>(&mut self, cookie: &Cookie<'a>) -> &mut Self { { if self.cookies.is_none() { @@ -582,15 +592,20 @@ impl ResponseBuilder { return Response::from(Error::from(e)).into_body(); } + #[allow(unused_mut)] let mut response = self.head.take().expect("cannot reuse response builder"); - if let Some(ref jar) = self.cookies { - for cookie in jar.delta() { - match HeaderValue::from_str(&cookie.to_string()) { - Ok(val) => { - let _ = response.headers.append(header::SET_COOKIE, val); - } - Err(e) => return Response::from(Error::from(e)).into_body(), - }; + + #[cfg(feature = "cookies")] + { + if let Some(ref jar) = self.cookies { + for cookie in jar.delta() { + match HeaderValue::from_str(&cookie.to_string()) { + Ok(val) => { + let _ = response.headers.append(header::SET_COOKIE, val); + } + Err(e) => return Response::from(Error::from(e)).into_body(), + }; + } } } @@ -654,6 +669,7 @@ impl ResponseBuilder { ResponseBuilder { head: self.head.take(), err: self.err.take(), + #[cfg(feature = "cookies")] cookies: self.cookies.take(), } } @@ -674,7 +690,9 @@ fn parts<'a>( impl From> for ResponseBuilder { fn from(res: Response) -> ResponseBuilder { // If this response has cookies, load them into a jar + #[cfg(feature = "cookies")] let mut jar: Option = None; + #[cfg(feature = "cookies")] for c in res.cookies() { if let Some(ref mut j) = jar { j.add_original(c.into_owned()); @@ -688,6 +706,7 @@ impl From> for ResponseBuilder { ResponseBuilder { head: Some(res.head), err: None, + #[cfg(feature = "cookies")] cookies: jar, } } @@ -697,17 +716,22 @@ impl From> for ResponseBuilder { impl<'a> From<&'a ResponseHead> for ResponseBuilder { fn from(head: &'a ResponseHead) -> ResponseBuilder { // If this response has cookies, load them into a jar + #[cfg(feature = "cookies")] let mut jar: Option = None; - let cookies = CookieIter { - iter: head.headers.get_all(header::SET_COOKIE).iter(), - }; - for c in cookies { - if let Some(ref mut j) = jar { - j.add_original(c.into_owned()); - } else { - let mut j = CookieJar::new(); - j.add_original(c.into_owned()); - jar = Some(j); + + #[cfg(feature = "cookies")] + { + let cookies = CookieIter { + iter: head.headers.get_all(header::SET_COOKIE).iter(), + }; + for c in cookies { + if let Some(ref mut j) = jar { + j.add_original(c.into_owned()); + } else { + let mut j = CookieJar::new(); + j.add_original(c.into_owned()); + jar = Some(j); + } } } @@ -721,6 +745,7 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder { ResponseBuilder { head: Some(msg), err: None, + #[cfg(feature = "cookies")] cookies: jar, } } @@ -802,14 +827,9 @@ impl From for Response { #[cfg(test)] mod tests { - use time::Duration; - use super::*; use crate::body::Body; - use crate::http; use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; - use crate::httpmessage::HttpMessage; - use crate::test::TestRequest; #[test] fn test_debug() { @@ -822,8 +842,11 @@ mod tests { } #[test] + #[cfg(feature = "cookies")] fn test_response_cookies() { - let req = TestRequest::default() + use crate::httpmessage::HttpMessage; + + let req = crate::test::TestRequest::default() .header(COOKIE, "cookie1=value1") .header(COOKIE, "cookie2=value2") .finish(); @@ -831,11 +854,11 @@ mod tests { let resp = Response::Ok() .cookie( - http::Cookie::build("name", "value") + crate::http::Cookie::build("name", "value") .domain("www.rust-lang.org") .path("/test") .http_only(true) - .max_age(Duration::days(1)) + .max_age(time::Duration::days(1)) .finish(), ) .del_cookie(&cookies[0]) @@ -856,16 +879,17 @@ mod tests { } #[test] + #[cfg(feature = "cookies")] fn test_update_response_cookies() { let mut r = Response::Ok() - .cookie(http::Cookie::new("original", "val100")) + .cookie(crate::http::Cookie::new("original", "val100")) .finish(); - r.add_cookie(&http::Cookie::new("cookie2", "val200")) + r.add_cookie(&crate::http::Cookie::new("cookie2", "val200")) .unwrap(); - r.add_cookie(&http::Cookie::new("cookie2", "val250")) + r.add_cookie(&crate::http::Cookie::new("cookie2", "val250")) .unwrap(); - r.add_cookie(&http::Cookie::new("cookie3", "val300")) + r.add_cookie(&crate::http::Cookie::new("cookie3", "val300")) .unwrap(); assert_eq!(r.cookies().count(), 4); @@ -1016,11 +1040,14 @@ mod tests { } #[test] + #[cfg(feature = "cookies")] fn test_into_builder() { + use crate::httpmessage::HttpMessage; + let mut resp: Response = "test".into(); assert_eq!(resp.status(), StatusCode::OK); - resp.add_cookie(&http::Cookie::new("cookie1", "val100")) + resp.add_cookie(&crate::http::Cookie::new("cookie1", "val100")) .unwrap(); let mut builder: ResponseBuilder = resp.into(); diff --git a/src/test.rs b/src/test.rs index c60d2d01c..2d4b3d0f9 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,12 +1,11 @@ //! Test Various helpers for Actix applications to use during testing. -use std::fmt::Write as FmtWrite; use std::str::FromStr; use bytes::Bytes; +#[cfg(feature = "cookies")] use cookie::{Cookie, CookieJar}; -use http::header::{self, HeaderName, HeaderValue}; +use http::header::HeaderName; use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; -use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use crate::header::{Header, IntoHeaderValue}; use crate::payload::Payload; @@ -46,6 +45,7 @@ struct Inner { method: Method, uri: Uri, headers: HeaderMap, + #[cfg(feature = "cookies")] cookies: CookieJar, payload: Option, } @@ -57,6 +57,7 @@ impl Default for TestRequest { uri: Uri::from_str("/").unwrap(), version: Version::HTTP_11, headers: HeaderMap::new(), + #[cfg(feature = "cookies")] cookies: CookieJar::new(), payload: None, })) @@ -126,6 +127,7 @@ impl TestRequest { } /// Set cookie for this request + #[cfg(feature = "cookies")] pub fn cookie<'a>(&mut self, cookie: Cookie<'a>) -> &mut Self { parts(&mut self.0).cookies.add(cookie.into_owned()); self @@ -145,38 +147,39 @@ impl TestRequest { /// Complete request creation and generate `Request` instance pub fn finish(&mut self) -> Request { - let Inner { - method, - uri, - version, - headers, - payload, - cookies, - } = self.0.take().expect("cannot reuse test request builder");; + let inner = self.0.take().expect("cannot reuse test request builder");; - let mut req = if let Some(pl) = payload { + let mut req = if let Some(pl) = inner.payload { Request::with_payload(pl) } else { Request::with_payload(crate::h1::Payload::empty().into()) }; let head = req.head_mut(); - head.uri = uri; - head.method = method; - head.version = version; - head.headers = headers; + head.uri = inner.uri; + head.method = inner.method; + head.version = inner.version; + head.headers = inner.headers; - let mut cookie = String::new(); - for c in cookies.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); - let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - if !cookie.is_empty() { - head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); + #[cfg(feature = "cookies")] + { + use std::fmt::Write as FmtWrite; + + use http::header::{self, HeaderValue}; + use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; + + let mut cookie = String::new(); + for c in inner.cookies.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); + } + if !cookie.is_empty() { + head.headers.insert( + header::COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); + } } req diff --git a/src/ws/client/connect.rs b/src/ws/client/connect.rs index 09d025631..5e877a645 100644 --- a/src/ws/client/connect.rs +++ b/src/ws/client/connect.rs @@ -1,6 +1,7 @@ //! Http client request use std::str; +#[cfg(feature = "cookies")] use cookie::Cookie; use http::header::{HeaderName, HeaderValue}; use http::{Error as HttpError, HttpTryFrom}; @@ -50,6 +51,7 @@ impl Connect { self } + #[cfg(feature = "cookies")] /// Set cookie for handshake request pub fn cookie(mut self, cookie: Cookie) -> Self { self.request.cookie(cookie); From 535b407ac0628b71016a9dc181aa2d92104d195d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 23 Mar 2019 10:06:54 -0700 Subject: [PATCH 320/427] make cookies optional --- Cargo.toml | 13 ++++++------- src/request.rs | 2 ++ src/test.rs | 2 ++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d98c926bb..6920bc09f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,10 +34,10 @@ members = [ ] [package.metadata.docs.rs] -features = ["ssl", "tls", "rust-tls", "session"] +features = ["ssl", "tls", "rust-tls", "brotli", "flate2-c", "cookies"] [features] -default = ["brotli", "flate2-c", "session"] +default = ["brotli", "flate2-c", "cookies"] # brotli encoding, requires c compiler brotli = ["brotli2"] @@ -49,7 +49,7 @@ flate2-c = ["flate2/miniz-sys"] flate2-rust = ["flate2/rust_backend"] # sessions feature, session require "ring" crate and c compiler -session = ["cookie/secure"] +cookies = ["cookie", "actix-http/cookies"] # tls tls = ["native-tls", "actix-server/ssl"] @@ -67,12 +67,11 @@ actix-utils = "0.3.4" actix-router = "0.1.0" actix-rt = "0.2.1" actix-web-codegen = { path="actix-web-codegen" } -actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-http = { git = "https://github.com/actix/actix-http.git", features=["fail"] } actix-server = "0.4.1" actix-server-config = "0.1.0" bytes = "0.4" -cookie = { version="0.11", features=["percent-encode"] } derive_more = "0.14" encoding = "0.2" futures = "0.1" @@ -88,8 +87,8 @@ serde_urlencoded = "^0.5.3" time = "0.1" url = { version="1.7", features=["query_encoding"] } -# middlewares -# actix-session = { path="session", optional = true } +# cookies support +cookie = { version="0.11", features=["secure", "percent-encode"], optional = true } # compression brotli2 = { version="^0.3.2", optional = true } diff --git a/src/request.rs b/src/request.rs index 5517302f0..1722925f8 100644 --- a/src/request.rs +++ b/src/request.rs @@ -251,12 +251,14 @@ mod tests { } #[test] + #[cfg(feature = "cookies")] fn test_no_request_cookies() { let req = TestRequest::default().to_http_request(); assert!(req.cookies().unwrap().is_empty()); } #[test] + #[cfg(feature = "cookies")] fn test_request_cookies() { let req = TestRequest::default() .header(header::COOKIE, "cookie1=value1") diff --git a/src/test.rs b/src/test.rs index fe9fb0247..ed9cf27cc 100644 --- a/src/test.rs +++ b/src/test.rs @@ -11,6 +11,7 @@ use actix_rt::Runtime; use actix_server_config::ServerConfig; use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; +#[cfg(feature = "cookies")] use cookie::Cookie; use futures::future::{lazy, Future}; @@ -262,6 +263,7 @@ impl TestRequest { self } + #[cfg(feature = "cookies")] /// Set cookie for this request pub fn cookie(mut self, cookie: Cookie) -> Self { self.req.cookie(cookie); From 60050307bd330a78db2c19878153153950cd86a5 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 23 Mar 2019 11:18:31 -0700 Subject: [PATCH 321/427] session feature is renamed to cookies --- src/middleware/mod.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 6e55cd67e..9b13a20ab 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -11,8 +11,5 @@ pub use self::defaultheaders::DefaultHeaders; pub use self::errhandlers::{ErrorHandlerResponse, ErrorHandlers}; pub use self::logger::Logger; -// #[cfg(feature = "session")] -// pub use actix_session as session; - -#[cfg(feature = "session")] +#[cfg(feature = "cookies")] pub mod identity; From 5b06f2bee5663ed51cfb964e25360f7d10390515 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 23 Mar 2019 21:29:16 -0700 Subject: [PATCH 322/427] port cors middleware --- README.md | 23 +- src/middleware/cors.rs | 1173 ++++++++++++++---------------- src/middleware/defaultheaders.rs | 7 +- src/middleware/mod.rs | 1 + src/test.rs | 27 +- 5 files changed, 582 insertions(+), 649 deletions(-) diff --git a/README.md b/README.md index c7e195de9..ce9efbb71 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust. -* Supported *HTTP/1.x* and [*HTTP/2.0*](https://actix.rs/docs/http2/) protocols +* Supported *HTTP/1.x* and *HTTP/2.0* protocols * Streaming and pipelining * Keep-alive and slow requests handling * Client/server [WebSockets](https://actix.rs/docs/websockets/) support @@ -13,33 +13,33 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust. * SSL support with OpenSSL or `native-tls` * Middlewares ([Logger, Session, CORS, CSRF, etc](https://actix.rs/docs/middleware/)) * Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) -* Built on top of [Actix actor framework](https://github.com/actix/actix) +* Supports [Actix actor framework](https://github.com/actix/actix) * Experimental [Async/Await](https://github.com/mehcode/actix-web-async-await) support. ## Documentation & community resources * [User Guide](https://actix.rs/docs/) * [API Documentation (Development)](https://actix.rs/actix-web/actix_web/) -* [API Documentation (Releases)](https://actix.rs/api/actix-web/stable/actix_web/) +* [API Documentation (Releases)](https://docs.rs/actix-web/) * [Chat on gitter](https://gitter.im/actix/actix) * Cargo package: [actix-web](https://crates.io/crates/actix-web) -* Minimum supported Rust version: 1.31 or later +* Minimum supported Rust version: 1.32 or later ## Example ```rust -extern crate actix_web; -use actix_web::{http, server, App, Path, Responder}; +use actix_web::{web, App, HttpServer, Responder}; -fn index(info: Path<(u32, String)>) -> impl Responder { +fn index(info: web::Path<(u32, String)>) -> impl Responder { format!("Hello {}! id:{}", info.1, info.0) } -fn main() { - server::new( +fn main() -> std::io::Result<()> { + HttpServer::new( || App::new() - .route("/{id}/{name}/index.html", http::Method::GET, index)) - .bind("127.0.0.1:8080").unwrap() + .service(web::resource("/{id}/{name}/index.html") + .route(web::get().to(index))) + .bind("127.0.0.1:8080")? .run(); } ``` @@ -48,7 +48,6 @@ fn main() { * [Basics](https://github.com/actix/examples/tree/master/basics/) * [Stateful](https://github.com/actix/examples/tree/master/state/) -* [Protobuf support](https://github.com/actix/examples/tree/master/protobuf/) * [Multipart streams](https://github.com/actix/examples/tree/master/multipart/) * [Simple websocket](https://github.com/actix/examples/tree/master/websocket/) * [Tera](https://github.com/actix/examples/tree/master/template_tera/) / diff --git a/src/middleware/cors.rs b/src/middleware/cors.rs index 80ee5b193..8f33d69b0 100644 --- a/src/middleware/cors.rs +++ b/src/middleware/cors.rs @@ -1,45 +1,36 @@ //! Cross-origin resource sharing (CORS) for Actix applications //! //! CORS middleware could be used with application and with resource. -//! First you need to construct CORS middleware instance. -//! -//! To construct a cors: -//! -//! 1. Call [`Cors::build`](struct.Cors.html#method.build) to start building. -//! 2. Use any of the builder methods to set fields in the backend. -//! 3. Call [finish](struct.Cors.html#method.finish) to retrieve the -//! constructed backend. -//! -//! Cors middleware could be used as parameter for `App::middleware()` or -//! `Resource::middleware()` methods. But you have to use -//! `Cors::for_app()` method to support *preflight* OPTIONS request. -//! +//! Cors middleware could be used as parameter for `App::middleware()`, +//! `Resource::middleware()` or `Scope::middleware()` methods. //! //! # Example //! //! ```rust -//! # extern crate actix_web; //! use actix_web::middleware::cors::Cors; -//! use actix_web::{http, App, HttpRequest, HttpResponse}; +//! use actix_web::{http, web, App, HttpRequest, HttpResponse, HttpServer}; //! -//! fn index(mut req: HttpRequest) -> &'static str { +//! fn index(req: HttpRequest) -> &'static str { //! "Hello world" //! } //! -//! fn main() { -//! let app = App::new().configure(|app| { -//! Cors::for_app(app) // <- Construct CORS middleware builder -//! .allowed_origin("https://www.rust-lang.org/") -//! .allowed_methods(vec!["GET", "POST"]) -//! .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) -//! .allowed_header(http::header::CONTENT_TYPE) -//! .max_age(3600) -//! .resource("/index.html", |r| { -//! r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -//! r.method(http::Method::HEAD).f(|_| HttpResponse::MethodNotAllowed()); -//! }) -//! .register() -//! }); +//! fn main() -> std::io::Result<()> { +//! HttpServer::new(|| App::new() +//! .middleware( +//! Cors::new() // <- Construct CORS middleware builder +//! .allowed_origin("https://www.rust-lang.org/") +//! .allowed_methods(vec!["GET", "POST"]) +//! .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) +//! .allowed_header(http::header::CONTENT_TYPE) +//! .max_age(3600)) +//! .service( +//! web::resource("/index.html") +//! .route(web::get().to(index)) +//! .route(web::head().to(|| HttpResponse::MethodNotAllowed())) +//! )) +//! .bind("127.0.0.1:8080")?; +//! +//! Ok(()) //! } //! ``` //! In this example custom *CORS* middleware get registered for "/index.html" @@ -50,68 +41,67 @@ use std::collections::HashSet; use std::iter::FromIterator; use std::rc::Rc; -use http::header::{self, HeaderName, HeaderValue}; -use http::{self, HttpTryFrom, Method, StatusCode, Uri}; +use actix_service::{IntoTransform, Service, Transform}; +use derive_more::Display; +use futures::future::{ok, Either, Future, FutureResult}; +use futures::Poll; -use application::App; -use error::{ResponseError, Result}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; -use resource::Resource; -use router::ResourceDef; -use server::Request; +use crate::dev::{Head, RequestHead}; +use crate::error::{ResponseError, Result}; +use crate::http::header::{self, HeaderName, HeaderValue}; +use crate::http::{self, HttpTryFrom, Method, StatusCode, Uri}; +use crate::service::{ServiceRequest, ServiceResponse}; +use crate::{HttpMessage, HttpResponse}; /// A set of errors that can occur during processing CORS -#[derive(Debug, Fail)] +#[derive(Debug, Display)] pub enum CorsError { /// The HTTP request header `Origin` is required but was not provided - #[fail( - display = "The HTTP request header `Origin` is required but was not provided" + #[display( + fmt = "The HTTP request header `Origin` is required but was not provided" )] MissingOrigin, /// The HTTP request header `Origin` could not be parsed correctly. - #[fail(display = "The HTTP request header `Origin` could not be parsed correctly.")] + #[display(fmt = "The HTTP request header `Origin` could not be parsed correctly.")] BadOrigin, /// The request header `Access-Control-Request-Method` is required but is /// missing - #[fail( - display = "The request header `Access-Control-Request-Method` is required but is missing" + #[display( + fmt = "The request header `Access-Control-Request-Method` is required but is missing" )] MissingRequestMethod, /// The request header `Access-Control-Request-Method` has an invalid value - #[fail( - display = "The request header `Access-Control-Request-Method` has an invalid value" + #[display( + fmt = "The request header `Access-Control-Request-Method` has an invalid value" )] BadRequestMethod, /// The request header `Access-Control-Request-Headers` has an invalid /// value - #[fail( - display = "The request header `Access-Control-Request-Headers` has an invalid value" + #[display( + fmt = "The request header `Access-Control-Request-Headers` has an invalid value" )] BadRequestHeaders, /// The request header `Access-Control-Request-Headers` is required but is /// missing. - #[fail( - display = "The request header `Access-Control-Request-Headers` is required but is + #[display( + fmt = "The request header `Access-Control-Request-Headers` is required but is missing" )] MissingRequestHeaders, /// Origin is not allowed to make this request - #[fail(display = "Origin is not allowed to make this request")] + #[display(fmt = "Origin is not allowed to make this request")] OriginNotAllowed, /// Requested method is not allowed - #[fail(display = "Requested method is not allowed")] + #[display(fmt = "Requested method is not allowed")] MethodNotAllowed, /// One or more headers requested are not allowed - #[fail(display = "One or more headers requested are not allowed")] + #[display(fmt = "One or more headers requested are not allowed")] HeadersNotAllowed, } impl ResponseError for CorsError { fn error_response(&self) -> HttpResponse { - HttpResponse::with_body(StatusCode::BAD_REQUEST, format!("{}", self)) + HttpResponse::with_body(StatusCode::BAD_REQUEST, format!("{}", self).into()) } } @@ -156,327 +146,6 @@ impl AllOrSome { } } -/// `Middleware` for Cross-origin resource sharing support -/// -/// The Cors struct contains the settings for CORS requests to be validated and -/// for responses to be generated. -#[derive(Clone)] -pub struct Cors { - inner: Rc, -} - -struct Inner { - methods: HashSet, - origins: AllOrSome>, - origins_str: Option, - headers: AllOrSome>, - expose_hdrs: Option, - max_age: Option, - preflight: bool, - send_wildcard: bool, - supports_credentials: bool, - vary_header: bool, -} - -impl Default for Cors { - fn default() -> Cors { - let inner = Inner { - origins: AllOrSome::default(), - origins_str: None, - methods: HashSet::from_iter( - vec![ - Method::GET, - Method::HEAD, - Method::POST, - Method::OPTIONS, - Method::PUT, - Method::PATCH, - Method::DELETE, - ].into_iter(), - ), - headers: AllOrSome::All, - expose_hdrs: None, - max_age: None, - preflight: true, - send_wildcard: false, - supports_credentials: false, - vary_header: true, - }; - Cors { - inner: Rc::new(inner), - } - } -} - -impl Cors { - /// Build a new CORS middleware instance - pub fn build() -> CorsBuilder<()> { - CorsBuilder { - cors: Some(Inner { - origins: AllOrSome::All, - origins_str: None, - methods: HashSet::new(), - headers: AllOrSome::All, - expose_hdrs: None, - max_age: None, - preflight: true, - send_wildcard: false, - supports_credentials: false, - vary_header: true, - }), - methods: false, - error: None, - expose_hdrs: HashSet::new(), - resources: Vec::new(), - app: None, - } - } - - /// Create CorsBuilder for a specified application. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::middleware::cors::Cors; - /// use actix_web::{http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().configure( - /// |app| { - /// Cors::for_app(app) // <- Construct CORS builder - /// .allowed_origin("https://www.rust-lang.org/") - /// .resource("/resource", |r| { // register resource - /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); - /// }) - /// .register() - /// }, // construct CORS and return application instance - /// ); - /// } - /// ``` - pub fn for_app(app: App) -> CorsBuilder { - CorsBuilder { - cors: Some(Inner { - origins: AllOrSome::All, - origins_str: None, - methods: HashSet::new(), - headers: AllOrSome::All, - expose_hdrs: None, - max_age: None, - preflight: true, - send_wildcard: false, - supports_credentials: false, - vary_header: true, - }), - methods: false, - error: None, - expose_hdrs: HashSet::new(), - resources: Vec::new(), - app: Some(app), - } - } - - /// This method register cors middleware with resource and - /// adds route for *OPTIONS* preflight requests. - /// - /// It is possible to register *Cors* middleware with - /// `Resource::middleware()` method, but in that case *Cors* - /// middleware wont be able to handle *OPTIONS* requests. - pub fn register(self, resource: &mut Resource) { - resource - .method(Method::OPTIONS) - .h(|_: &_| HttpResponse::Ok()); - resource.middleware(self); - } - - fn validate_origin(&self, req: &Request) -> Result<(), CorsError> { - if let Some(hdr) = req.headers().get(header::ORIGIN) { - if let Ok(origin) = hdr.to_str() { - return match self.inner.origins { - AllOrSome::All => Ok(()), - AllOrSome::Some(ref allowed_origins) => allowed_origins - .get(origin) - .and_then(|_| Some(())) - .ok_or_else(|| CorsError::OriginNotAllowed), - }; - } - Err(CorsError::BadOrigin) - } else { - return match self.inner.origins { - AllOrSome::All => Ok(()), - _ => Err(CorsError::MissingOrigin), - }; - } - } - - fn access_control_allow_origin(&self, req: &Request) -> Option { - match self.inner.origins { - AllOrSome::All => { - if self.inner.send_wildcard { - Some(HeaderValue::from_static("*")) - } else if let Some(origin) = req.headers().get(header::ORIGIN) { - Some(origin.clone()) - } else { - None - } - } - AllOrSome::Some(ref origins) => { - if let Some(origin) = req.headers().get(header::ORIGIN).filter(|o| { - match o.to_str() { - Ok(os) => origins.contains(os), - _ => false - } - }) { - Some(origin.clone()) - } else { - Some(self.inner.origins_str.as_ref().unwrap().clone()) - } - } - } - } - - fn validate_allowed_method(&self, req: &Request) -> Result<(), CorsError> { - if let Some(hdr) = req.headers().get(header::ACCESS_CONTROL_REQUEST_METHOD) { - if let Ok(meth) = hdr.to_str() { - if let Ok(method) = Method::try_from(meth) { - return self - .inner - .methods - .get(&method) - .and_then(|_| Some(())) - .ok_or_else(|| CorsError::MethodNotAllowed); - } - } - Err(CorsError::BadRequestMethod) - } else { - Err(CorsError::MissingRequestMethod) - } - } - - fn validate_allowed_headers(&self, req: &Request) -> Result<(), CorsError> { - match self.inner.headers { - AllOrSome::All => Ok(()), - AllOrSome::Some(ref allowed_headers) => { - if let Some(hdr) = - req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) - { - if let Ok(headers) = hdr.to_str() { - let mut hdrs = HashSet::new(); - for hdr in headers.split(',') { - match HeaderName::try_from(hdr.trim()) { - Ok(hdr) => hdrs.insert(hdr), - Err(_) => return Err(CorsError::BadRequestHeaders), - }; - } - - if !hdrs.is_empty() && !hdrs.is_subset(allowed_headers) { - return Err(CorsError::HeadersNotAllowed); - } - return Ok(()); - } - Err(CorsError::BadRequestHeaders) - } else { - Err(CorsError::MissingRequestHeaders) - } - } - } - } -} - -impl Middleware for Cors { - fn start(&self, req: &HttpRequest) -> Result { - if self.inner.preflight && Method::OPTIONS == *req.method() { - self.validate_origin(req)?; - self.validate_allowed_method(&req)?; - self.validate_allowed_headers(&req)?; - - // allowed headers - let headers = if let Some(headers) = self.inner.headers.as_ref() { - Some( - HeaderValue::try_from( - &headers - .iter() - .fold(String::new(), |s, v| s + "," + v.as_str()) - .as_str()[1..], - ).unwrap(), - ) - } else if let Some(hdr) = - req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) - { - Some(hdr.clone()) - } else { - None - }; - - Ok(Started::Response( - HttpResponse::Ok() - .if_some(self.inner.max_age.as_ref(), |max_age, resp| { - let _ = resp.header( - header::ACCESS_CONTROL_MAX_AGE, - format!("{}", max_age).as_str(), - ); - }).if_some(headers, |headers, resp| { - let _ = - resp.header(header::ACCESS_CONTROL_ALLOW_HEADERS, headers); - }).if_some(self.access_control_allow_origin(&req), |origin, resp| { - let _ = - resp.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin); - }).if_true(self.inner.supports_credentials, |resp| { - resp.header(header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - }).header( - header::ACCESS_CONTROL_ALLOW_METHODS, - &self - .inner - .methods - .iter() - .fold(String::new(), |s, v| s + "," + v.as_str()) - .as_str()[1..], - ).finish(), - )) - } else { - // Only check requests with a origin header. - if req.headers().contains_key(header::ORIGIN) { - self.validate_origin(req)?; - } - - Ok(Started::Done) - } - } - - fn response( - &self, req: &HttpRequest, mut resp: HttpResponse, - ) -> Result { - - if let Some(origin) = self.access_control_allow_origin(req) { - resp.headers_mut() - .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone()); - }; - - if let Some(ref expose) = self.inner.expose_hdrs { - resp.headers_mut().insert( - header::ACCESS_CONTROL_EXPOSE_HEADERS, - HeaderValue::try_from(expose.as_str()).unwrap(), - ); - } - if self.inner.supports_credentials { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_CREDENTIALS, - HeaderValue::from_static("true"), - ); - } - if self.inner.vary_header { - let value = if let Some(hdr) = resp.headers_mut().get(header::VARY) { - let mut val: Vec = Vec::with_capacity(hdr.as_bytes().len() + 8); - val.extend(hdr.as_bytes()); - val.extend(b", Origin"); - HeaderValue::try_from(&val[..]).unwrap() - } else { - HeaderValue::from_static("Origin") - }; - resp.headers_mut().insert(header::VARY, value); - } - Ok(Response::Done(resp)) - } -} - /// Structure that follows the builder pattern for building `Cors` middleware /// structs. /// @@ -490,40 +159,77 @@ impl Middleware for Cors { /// # Example /// /// ```rust -/// # extern crate http; -/// # extern crate actix_web; +/// use actix_web::http::header; /// use actix_web::middleware::cors; -/// use http::header; /// /// # fn main() { -/// let cors = cors::Cors::build() +/// let cors = cors::Cors::new() /// .allowed_origin("https://www.rust-lang.org/") /// .allowed_methods(vec!["GET", "POST"]) /// .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) /// .allowed_header(header::CONTENT_TYPE) -/// .max_age(3600) -/// .finish(); +/// .max_age(3600); /// # } /// ``` -pub struct CorsBuilder { +pub struct Cors { cors: Option, methods: bool, error: Option, expose_hdrs: HashSet, - resources: Vec>, - app: Option>, } -fn cors<'a>( - parts: &'a mut Option, err: &Option, -) -> Option<&'a mut Inner> { - if err.is_some() { - return None; +impl Cors { + /// Build a new CORS middleware instance + pub fn new() -> Cors { + Cors { + cors: Some(Inner { + origins: AllOrSome::All, + origins_str: None, + methods: HashSet::new(), + headers: AllOrSome::All, + expose_hdrs: None, + max_age: None, + preflight: true, + send_wildcard: false, + supports_credentials: false, + vary_header: true, + }), + methods: false, + error: None, + expose_hdrs: HashSet::new(), + } + } + + /// Build a new CORS default middleware + pub fn default() -> CorsFactory { + let inner = Inner { + origins: AllOrSome::default(), + origins_str: None, + methods: HashSet::from_iter( + vec![ + Method::GET, + Method::HEAD, + Method::POST, + Method::OPTIONS, + Method::PUT, + Method::PATCH, + Method::DELETE, + ] + .into_iter(), + ), + headers: AllOrSome::All, + expose_hdrs: None, + max_age: None, + preflight: true, + send_wildcard: false, + supports_credentials: false, + vary_header: true, + }; + CorsFactory { + inner: Rc::new(inner), + } } - parts.as_mut() -} -impl CorsBuilder { /// Add an origin that are allowed to make requests. /// Will be verified against the `Origin` request header. /// @@ -541,7 +247,7 @@ impl CorsBuilder { /// Defaults to `All`. /// /// Builder panics if supplied origin is not valid uri. - pub fn allowed_origin(&mut self, origin: &str) -> &mut CorsBuilder { + pub fn allowed_origin(mut self, origin: &str) -> Cors { if let Some(cors) = cors(&mut self.cors, &self.error) { match Uri::try_from(origin) { Ok(_) => { @@ -567,7 +273,7 @@ impl CorsBuilder { /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). /// /// Defaults to `[GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE]` - pub fn allowed_methods(&mut self, methods: U) -> &mut CorsBuilder + pub fn allowed_methods(mut self, methods: U) -> Cors where U: IntoIterator, Method: HttpTryFrom, @@ -590,7 +296,7 @@ impl CorsBuilder { } /// Set an allowed header - pub fn allowed_header(&mut self, header: H) -> &mut CorsBuilder + pub fn allowed_header(mut self, header: H) -> Cors where HeaderName: HttpTryFrom, { @@ -621,7 +327,7 @@ impl CorsBuilder { /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). /// /// Defaults to `All`. - pub fn allowed_headers(&mut self, headers: U) -> &mut CorsBuilder + pub fn allowed_headers(mut self, headers: U) -> Cors where U: IntoIterator, HeaderName: HttpTryFrom, @@ -655,7 +361,7 @@ impl CorsBuilder { /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). /// /// This defaults to an empty set. - pub fn expose_headers(&mut self, headers: U) -> &mut CorsBuilder + pub fn expose_headers(mut self, headers: U) -> Cors where U: IntoIterator, HeaderName: HttpTryFrom, @@ -678,7 +384,7 @@ impl CorsBuilder { /// This value is set as the `Access-Control-Max-Age` header. /// /// This defaults to `None` (unset). - pub fn max_age(&mut self, max_age: usize) -> &mut CorsBuilder { + pub fn max_age(mut self, max_age: usize) -> Cors { if let Some(cors) = cors(&mut self.cors, &self.error) { cors.max_age = Some(max_age) } @@ -700,7 +406,7 @@ impl CorsBuilder { /// CredentialsWithWildcardOrigin` error during actix launch or runtime. /// /// Defaults to `false`. - pub fn send_wildcard(&mut self) -> &mut CorsBuilder { + pub fn send_wildcard(mut self) -> Cors { if let Some(cors) = cors(&mut self.cors, &self.error) { cors.send_wildcard = true } @@ -720,7 +426,7 @@ impl CorsBuilder { /// /// Builder panics if credentials are allowed, but the Origin is set to "*". /// This is not allowed by W3C - pub fn supports_credentials(&mut self) -> &mut CorsBuilder { + pub fn supports_credentials(mut self) -> Cors { if let Some(cors) = cors(&mut self.cors, &self.error) { cors.supports_credentials = true } @@ -738,7 +444,7 @@ impl CorsBuilder { /// caches that the CORS headers are dynamic, and cannot be cached. /// /// By default `vary` header support is enabled. - pub fn disable_vary_header(&mut self) -> &mut CorsBuilder { + pub fn disable_vary_header(mut self) -> Cors { if let Some(cors) = cors(&mut self.cors, &self.error) { cors.vary_header = false } @@ -751,57 +457,32 @@ impl CorsBuilder { /// This is useful application level middleware. /// /// By default *preflight* support is enabled. - pub fn disable_preflight(&mut self) -> &mut CorsBuilder { + pub fn disable_preflight(mut self) -> Cors { if let Some(cors) = cors(&mut self.cors, &self.error) { cors.preflight = false } self } +} - /// Configure resource for a specific path. - /// - /// This is similar to a `App::resource()` method. Except, cors middleware - /// get registered for the resource. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::middleware::cors::Cors; - /// use actix_web::{http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().configure( - /// |app| { - /// Cors::for_app(app) // <- Construct CORS builder - /// .allowed_origin("https://www.rust-lang.org/") - /// .allowed_methods(vec!["GET", "POST"]) - /// .allowed_header(http::header::CONTENT_TYPE) - /// .max_age(3600) - /// .resource("/resource1", |r| { // register resource - /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); - /// }) - /// .resource("/resource2", |r| { // register another resource - /// r.method(http::Method::HEAD) - /// .f(|_| HttpResponse::MethodNotAllowed()); - /// }) - /// .register() - /// }, // construct CORS and return application instance - /// ); - /// } - /// ``` - pub fn resource(&mut self, path: &str, f: F) -> &mut CorsBuilder - where - F: FnOnce(&mut Resource) -> R + 'static, - { - // add resource handler - let mut resource = Resource::new(ResourceDef::new(path)); - f(&mut resource); - - self.resources.push(resource); - self +fn cors<'a>( + parts: &'a mut Option, + err: &Option, +) -> Option<&'a mut Inner> { + if err.is_some() { + return None; } + parts.as_mut() +} - fn construct(&mut self) -> Cors { - if !self.methods { +impl IntoTransform for Cors +where + S: Service, Response = ServiceResponse> + 'static, + P: 'static, + B: 'static, +{ + fn into_transform(self) -> CorsFactory { + let mut slf = if !self.methods { self.allowed_methods(vec![ Method::GET, Method::HEAD, @@ -810,14 +491,16 @@ impl CorsBuilder { Method::PUT, Method::PATCH, Method::DELETE, - ]); - } + ]) + } else { + self + }; - if let Some(e) = self.error.take() { + if let Some(e) = slf.error.take() { panic!("{}", e); } - let mut cors = self.cors.take().expect("cannot reuse CorsBuilder"); + let mut cors = slf.cors.take().expect("cannot reuse CorsBuilder"); if cors.supports_credentials && cors.send_wildcard && cors.origins.is_all() { panic!("Credentials are allowed, but the Origin is set to \"*\""); @@ -830,152 +513,383 @@ impl CorsBuilder { cors.origins_str = Some(HeaderValue::try_from(&s[2..]).unwrap()); } - if !self.expose_hdrs.is_empty() { + if !slf.expose_hdrs.is_empty() { cors.expose_hdrs = Some( - self.expose_hdrs + slf.expose_hdrs .iter() .fold(String::new(), |s, v| format!("{}, {}", s, v.as_str()))[2..] .to_owned(), ); } - Cors { + + CorsFactory { inner: Rc::new(cors), } } +} - /// Finishes building and returns the built `Cors` instance. - /// - /// This method panics in case of any configuration error. - pub fn finish(&mut self) -> Cors { - if !self.resources.is_empty() { - panic!( - "CorsBuilder::resource() was used, - to construct CORS `.register(app)` method should be used" - ); +/// `Middleware` for Cross-origin resource sharing support +/// +/// The Cors struct contains the settings for CORS requests to be validated and +/// for responses to be generated. +pub struct CorsFactory { + inner: Rc, +} + +impl Transform for CorsFactory +where + S: Service, Response = ServiceResponse>, + S::Future: 'static, + S::Error: 'static, + P: 'static, + B: 'static, +{ + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = S::Error; + type InitError = (); + type Transform = CorsMiddleware; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(CorsMiddleware { + service, + inner: self.inner.clone(), + }) + } +} + +/// `Middleware` for Cross-origin resource sharing support +/// +/// The Cors struct contains the settings for CORS requests to be validated and +/// for responses to be generated. +#[derive(Clone)] +pub struct CorsMiddleware { + service: S, + inner: Rc, +} + +struct Inner { + methods: HashSet, + origins: AllOrSome>, + origins_str: Option, + headers: AllOrSome>, + expose_hdrs: Option, + max_age: Option, + preflight: bool, + send_wildcard: bool, + supports_credentials: bool, + vary_header: bool, +} + +impl Inner { + fn validate_origin(&self, req: &RequestHead) -> Result<(), CorsError> { + if let Some(hdr) = req.headers().get(header::ORIGIN) { + if let Ok(origin) = hdr.to_str() { + return match self.origins { + AllOrSome::All => Ok(()), + AllOrSome::Some(ref allowed_origins) => allowed_origins + .get(origin) + .and_then(|_| Some(())) + .ok_or_else(|| CorsError::OriginNotAllowed), + }; + } + Err(CorsError::BadOrigin) + } else { + return match self.origins { + AllOrSome::All => Ok(()), + _ => Err(CorsError::MissingOrigin), + }; } - self.construct() } - /// Finishes building Cors middleware and register middleware for - /// application - /// - /// This method panics in case of any configuration error or if non of - /// resources are registered. - pub fn register(&mut self) -> App { - if self.resources.is_empty() { - panic!("No resources are registered."); + fn access_control_allow_origin(&self, req: &RequestHead) -> Option { + match self.origins { + AllOrSome::All => { + if self.send_wildcard { + Some(HeaderValue::from_static("*")) + } else if let Some(origin) = req.headers().get(header::ORIGIN) { + Some(origin.clone()) + } else { + None + } + } + AllOrSome::Some(ref origins) => { + if let Some(origin) = + req.headers() + .get(header::ORIGIN) + .filter(|o| match o.to_str() { + Ok(os) => origins.contains(os), + _ => false, + }) + { + Some(origin.clone()) + } else { + Some(self.origins_str.as_ref().unwrap().clone()) + } + } } + } - let cors = self.construct(); - let mut app = self - .app - .take() - .expect("CorsBuilder has to be constructed with Cors::for_app(app)"); - - // register resources - for mut resource in self.resources.drain(..) { - cors.clone().register(&mut resource); - app.register_resource(resource); + fn validate_allowed_method(&self, req: &RequestHead) -> Result<(), CorsError> { + if let Some(hdr) = req.headers().get(header::ACCESS_CONTROL_REQUEST_METHOD) { + if let Ok(meth) = hdr.to_str() { + if let Ok(method) = Method::try_from(meth) { + return self + .methods + .get(&method) + .and_then(|_| Some(())) + .ok_or_else(|| CorsError::MethodNotAllowed); + } + } + Err(CorsError::BadRequestMethod) + } else { + Err(CorsError::MissingRequestMethod) } + } - app + fn validate_allowed_headers(&self, req: &RequestHead) -> Result<(), CorsError> { + match self.headers { + AllOrSome::All => Ok(()), + AllOrSome::Some(ref allowed_headers) => { + if let Some(hdr) = + req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) + { + if let Ok(headers) = hdr.to_str() { + let mut hdrs = HashSet::new(); + for hdr in headers.split(',') { + match HeaderName::try_from(hdr.trim()) { + Ok(hdr) => hdrs.insert(hdr), + Err(_) => return Err(CorsError::BadRequestHeaders), + }; + } + + if !hdrs.is_empty() && !hdrs.is_subset(allowed_headers) { + return Err(CorsError::HeadersNotAllowed); + } + return Ok(()); + } + Err(CorsError::BadRequestHeaders) + } else { + Err(CorsError::MissingRequestHeaders) + } + } + } + } +} + +impl Service for CorsMiddleware +where + S: Service, Response = ServiceResponse>, + S::Future: 'static, + S::Error: 'static, + P: 'static, + B: 'static, +{ + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = S::Error; + type Future = Either< + FutureResult, + Either>>, + >; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready() + } + + fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { + if self.inner.preflight && Method::OPTIONS == *req.method() { + if let Err(e) = self + .inner + .validate_origin(&req) + .and_then(|_| self.inner.validate_allowed_method(&req)) + .and_then(|_| self.inner.validate_allowed_headers(&req)) + { + return Either::A(ok(req.error_response(e))); + } + + // allowed headers + let headers = if let Some(headers) = self.inner.headers.as_ref() { + Some( + HeaderValue::try_from( + &headers + .iter() + .fold(String::new(), |s, v| s + "," + v.as_str()) + .as_str()[1..], + ) + .unwrap(), + ) + } else if let Some(hdr) = + req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) + { + Some(hdr.clone()) + } else { + None + }; + + let res = HttpResponse::Ok() + .if_some(self.inner.max_age.as_ref(), |max_age, resp| { + let _ = resp.header( + header::ACCESS_CONTROL_MAX_AGE, + format!("{}", max_age).as_str(), + ); + }) + .if_some(headers, |headers, resp| { + let _ = resp.header(header::ACCESS_CONTROL_ALLOW_HEADERS, headers); + }) + .if_some( + self.inner.access_control_allow_origin(&req), + |origin, resp| { + let _ = resp.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin); + }, + ) + .if_true(self.inner.supports_credentials, |resp| { + resp.header(header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + }) + .header( + header::ACCESS_CONTROL_ALLOW_METHODS, + &self + .inner + .methods + .iter() + .fold(String::new(), |s, v| s + "," + v.as_str()) + .as_str()[1..], + ) + .finish() + .into_body(); + + Either::A(ok(req.into_response(res))) + } else if req.headers().contains_key(header::ORIGIN) { + // Only check requests with a origin header. + if let Err(e) = self.inner.validate_origin(&req) { + return Either::A(ok(req.error_response(e))); + } + + let inner = self.inner.clone(); + + Either::B(Either::B(Box::new(self.service.call(req).and_then( + move |mut res| { + if let Some(origin) = + inner.access_control_allow_origin(&res.request()) + { + res.headers_mut() + .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone()); + }; + + if let Some(ref expose) = inner.expose_hdrs { + res.headers_mut().insert( + header::ACCESS_CONTROL_EXPOSE_HEADERS, + HeaderValue::try_from(expose.as_str()).unwrap(), + ); + } + if inner.supports_credentials { + res.headers_mut().insert( + header::ACCESS_CONTROL_ALLOW_CREDENTIALS, + HeaderValue::from_static("true"), + ); + } + if inner.vary_header { + let value = + if let Some(hdr) = res.headers_mut().get(header::VARY) { + let mut val: Vec = + Vec::with_capacity(hdr.as_bytes().len() + 8); + val.extend(hdr.as_bytes()); + val.extend(b", Origin"); + HeaderValue::try_from(&val[..]).unwrap() + } else { + HeaderValue::from_static("Origin") + }; + res.headers_mut().insert(header::VARY, value); + } + Ok(res) + }, + )))) + } else { + Either::B(Either::A(self.service.call(req))) + } } } #[cfg(test)] mod tests { - use super::*; - use test::{self, TestRequest}; + use actix_service::{FnService, Transform}; - impl Started { - fn is_done(&self) -> bool { - match *self { - Started::Done => true, - _ => false, - } - } - fn response(self) -> HttpResponse { - match self { - Started::Response(resp) => resp, - _ => panic!(), - } - } - } - impl Response { - fn response(self) -> HttpResponse { - match self { - Response::Done(resp) => resp, - _ => panic!(), - } + use super::*; + use crate::dev::PayloadStream; + use crate::test::{self, block_on, TestRequest}; + + impl Cors { + fn finish(self, srv: S) -> CorsMiddleware + where + S: Service, Response = ServiceResponse> + + 'static, + S::Future: 'static, + S::Error: 'static, + P: 'static, + B: 'static, + { + block_on( + IntoTransform::::into_transform(self).new_transform(srv), + ) + .unwrap() } } #[test] #[should_panic(expected = "Credentials are allowed, but the Origin is set to")] fn cors_validates_illegal_allow_credentials() { - Cors::build() + let _cors = Cors::new() .supports_credentials() .send_wildcard() - .finish(); - } - - #[test] - #[should_panic(expected = "No resources are registered")] - fn no_resource() { - Cors::build() - .supports_credentials() - .send_wildcard() - .register(); - } - - #[test] - #[should_panic(expected = "Cors::for_app(app)")] - fn no_resource2() { - Cors::build() - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .register(); + .finish(test::ok_service()); } #[test] fn validate_origin_allows_all_origins() { - let cors = Cors::default(); - let req = TestRequest::with_header("Origin", "https://www.example.com").finish(); + let mut cors = Cors::new().finish(test::ok_service()); + let req = + TestRequest::with_header("Origin", "https://www.example.com").to_service(); - assert!(cors.start(&req).ok().unwrap().is_done()) + let resp = test::call_success(&mut cors, req); + assert_eq!(resp.status(), StatusCode::OK); } #[test] fn test_preflight() { - let mut cors = Cors::build() + let mut cors = Cors::new() .send_wildcard() .max_age(3600) .allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST]) .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) .allowed_header(header::CONTENT_TYPE) - .finish(); + .finish(test::ok_service()); let req = TestRequest::with_header("Origin", "https://www.example.com") .method(Method::OPTIONS) - .finish(); + .to_service(); - assert!(cors.start(&req).is_err()); + assert!(cors.inner.validate_allowed_method(&req).is_err()); + assert!(cors.inner.validate_allowed_headers(&req).is_err()); let req = TestRequest::with_header("Origin", "https://www.example.com") .header(header::ACCESS_CONTROL_REQUEST_METHOD, "put") .method(Method::OPTIONS) - .finish(); + .to_service(); - assert!(cors.start(&req).is_err()); + assert!(cors.inner.validate_allowed_method(&req).is_err()); + assert!(cors.inner.validate_allowed_headers(&req).is_err()); let req = TestRequest::with_header("Origin", "https://www.example.com") .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST") .header( header::ACCESS_CONTROL_REQUEST_HEADERS, "AUTHORIZATION,ACCEPT", - ).method(Method::OPTIONS) - .finish(); + ) + .method(Method::OPTIONS) + .to_service(); - let resp = cors.start(&req).unwrap().response(); + let resp = test::call_success(&mut cors, req); assert_eq!( &b"*"[..], resp.headers() @@ -990,16 +904,39 @@ mod tests { .unwrap() .as_bytes() ); - //assert_eq!( - // &b"authorization,accept,content-type"[..], - // resp.headers().get(header::ACCESS_CONTROL_ALLOW_HEADERS).unwrap(). - // as_bytes()); assert_eq!( - // &b"POST,GET,OPTIONS"[..], - // resp.headers().get(header::ACCESS_CONTROL_ALLOW_METHODS).unwrap(). - // as_bytes()); + let hdr = resp + .headers() + .get(header::ACCESS_CONTROL_ALLOW_HEADERS) + .unwrap() + .to_str() + .unwrap(); + assert!(hdr.contains("authorization")); + assert!(hdr.contains("accept")); + assert!(hdr.contains("content-type")); + + let methods = resp + .headers() + .get(header::ACCESS_CONTROL_ALLOW_METHODS) + .unwrap() + .to_str() + .unwrap(); + assert!(methods.contains("POST")); + assert!(methods.contains("GET")); + assert!(methods.contains("OPTIONS")); Rc::get_mut(&mut cors.inner).unwrap().preflight = false; - assert!(cors.start(&req).unwrap().is_done()); + + let req = TestRequest::with_header("Origin", "https://www.example.com") + .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST") + .header( + header::ACCESS_CONTROL_REQUEST_HEADERS, + "AUTHORIZATION,ACCEPT", + ) + .method(Method::OPTIONS) + .to_service(); + + let resp = test::call_success(&mut cors, req); + assert_eq!(resp.status(), StatusCode::OK); } // #[test] @@ -1015,46 +952,47 @@ mod tests { #[test] #[should_panic(expected = "OriginNotAllowed")] fn test_validate_not_allowed_origin() { - let cors = Cors::build() + let cors = Cors::new() .allowed_origin("https://www.example.com") - .finish(); + .finish(test::ok_service()); let req = TestRequest::with_header("Origin", "https://www.unknown.com") .method(Method::GET) - .finish(); - cors.start(&req).unwrap(); + .to_service(); + cors.inner.validate_origin(&req).unwrap(); + cors.inner.validate_allowed_method(&req).unwrap(); + cors.inner.validate_allowed_headers(&req).unwrap(); } #[test] fn test_validate_origin() { - let cors = Cors::build() + let mut cors = Cors::new() .allowed_origin("https://www.example.com") - .finish(); + .finish(test::ok_service()); let req = TestRequest::with_header("Origin", "https://www.example.com") .method(Method::GET) - .finish(); + .to_service(); - assert!(cors.start(&req).unwrap().is_done()); + let resp = test::call_success(&mut cors, req); + assert_eq!(resp.status(), StatusCode::OK); } #[test] fn test_no_origin_response() { - let cors = Cors::build().finish(); + let mut cors = Cors::new().disable_preflight().finish(test::ok_service()); - let req = TestRequest::default().method(Method::GET).finish(); - let resp: HttpResponse = HttpResponse::Ok().into(); - let resp = cors.response(&req, resp).unwrap().response(); - assert!( - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .is_none() - ); + let req = TestRequest::default().method(Method::GET).to_service(); + let resp = test::call_success(&mut cors, req); + assert!(resp + .headers() + .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) + .is_none()); let req = TestRequest::with_header("Origin", "https://www.example.com") .method(Method::OPTIONS) - .finish(); - let resp = cors.response(&req, resp).unwrap().response(); + .to_service(); + let resp = test::call_success(&mut cors, req); assert_eq!( &b"https://www.example.com"[..], resp.headers() @@ -1067,7 +1005,7 @@ mod tests { #[test] fn test_response() { let exposed_headers = vec![header::AUTHORIZATION, header::ACCEPT]; - let cors = Cors::build() + let mut cors = Cors::new() .send_wildcard() .disable_preflight() .max_age(3600) @@ -1075,14 +1013,13 @@ mod tests { .allowed_headers(exposed_headers.clone()) .expose_headers(exposed_headers.clone()) .allowed_header(header::CONTENT_TYPE) - .finish(); + .finish(test::ok_service()); let req = TestRequest::with_header("Origin", "https://www.example.com") .method(Method::OPTIONS) - .finish(); + .to_service(); - let resp: HttpResponse = HttpResponse::Ok().into(); - let resp = cors.response(&req, resp).unwrap().response(); + let resp = test::call_success(&mut cors, req); assert_eq!( &b"*"[..], resp.headers() @@ -1111,21 +1048,40 @@ mod tests { } } - let resp: HttpResponse = - HttpResponse::Ok().header(header::VARY, "Accept").finish(); - let resp = cors.response(&req, resp).unwrap().response(); + let exposed_headers = vec![header::AUTHORIZATION, header::ACCEPT]; + let mut cors = Cors::new() + .send_wildcard() + .disable_preflight() + .max_age(3600) + .allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST]) + .allowed_headers(exposed_headers.clone()) + .expose_headers(exposed_headers.clone()) + .allowed_header(header::CONTENT_TYPE) + .finish(FnService::new(move |req: ServiceRequest| { + req.into_response( + HttpResponse::Ok().header(header::VARY, "Accept").finish(), + ) + })); + let req = TestRequest::with_header("Origin", "https://www.example.com") + .method(Method::OPTIONS) + .to_service(); + let resp = test::call_success(&mut cors, req); assert_eq!( &b"Accept, Origin"[..], resp.headers().get(header::VARY).unwrap().as_bytes() ); - let cors = Cors::build() + let mut cors = Cors::new() .disable_vary_header() .allowed_origin("https://www.example.com") .allowed_origin("https://www.google.com") - .finish(); - let resp: HttpResponse = HttpResponse::Ok().into(); - let resp = cors.response(&req, resp).unwrap().response(); + .finish(test::ok_service()); + + let req = TestRequest::with_header("Origin", "https://www.example.com") + .method(Method::OPTIONS) + .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST") + .to_service(); + let resp = test::call_success(&mut cors, req); let origins_str = resp .headers() @@ -1134,61 +1090,22 @@ mod tests { .to_str() .unwrap(); - assert_eq!( - "https://www.example.com", - origins_str - ); - } - - #[test] - fn cors_resource() { - let mut srv = test::TestServer::with_factory(|| { - App::new().configure(|app| { - Cors::for_app(app) - .allowed_origin("https://www.example.com") - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .register() - }) - }); - - let request = srv - .get() - .uri(srv.url("/test")) - .header("ORIGIN", "https://www.example2.com") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - let request = srv - .get() - .uri(srv.url("/test")) - .header("ORIGIN", "https://www.example.com") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); + assert_eq!("https://www.example.com", origins_str); } #[test] fn test_multiple_origins() { - let cors = Cors::build() + let mut cors = Cors::new() .allowed_origin("https://example.com") .allowed_origin("https://example.org") .allowed_methods(vec![Method::GET]) - .finish(); - + .finish(test::ok_service()); let req = TestRequest::with_header("Origin", "https://example.com") .method(Method::GET) - .finish(); - let resp: HttpResponse = HttpResponse::Ok().into(); + .to_service(); - let resp = cors.response(&req, resp).unwrap().response(); + let resp = test::call_success(&mut cors, req); assert_eq!( &b"https://example.com"[..], resp.headers() @@ -1199,10 +1116,9 @@ mod tests { let req = TestRequest::with_header("Origin", "https://example.org") .method(Method::GET) - .finish(); - let resp: HttpResponse = HttpResponse::Ok().into(); + .to_service(); - let resp = cors.response(&req, resp).unwrap().response(); + let resp = test::call_success(&mut cors, req); assert_eq!( &b"https://example.org"[..], resp.headers() @@ -1214,19 +1130,18 @@ mod tests { #[test] fn test_multiple_origins_preflight() { - let cors = Cors::build() + let mut cors = Cors::new() .allowed_origin("https://example.com") .allowed_origin("https://example.org") .allowed_methods(vec![Method::GET]) - .finish(); - + .finish(test::ok_service()); let req = TestRequest::with_header("Origin", "https://example.com") .header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET") .method(Method::OPTIONS) - .finish(); + .to_service(); - let resp = cors.start(&req).ok().unwrap().response(); + let resp = test::call_success(&mut cors, req); assert_eq!( &b"https://example.com"[..], resp.headers() @@ -1238,9 +1153,9 @@ mod tests { let req = TestRequest::with_header("Origin", "https://example.org") .header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET") .method(Method::OPTIONS) - .finish(); + .to_service(); - let resp = cors.start(&req).ok().unwrap().response(); + let resp = test::call_success(&mut cors, req); assert_eq!( &b"https://example.org"[..], resp.headers() diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index bca2cf6e0..4d879cda7 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -154,18 +154,15 @@ mod tests { use super::*; use crate::dev::ServiceRequest; use crate::http::header::CONTENT_TYPE; - use crate::test::{block_on, TestRequest}; + use crate::test::{block_on, ok_service, TestRequest}; use crate::HttpResponse; #[test] fn test_default_headers() { - let srv = FnService::new(|req: ServiceRequest<_>| { - req.into_response(HttpResponse::Ok().finish()) - }); let mut mw = block_on( DefaultHeaders::new() .header(CONTENT_TYPE, "0001") - .new_transform(srv), + .new_transform(ok_service()), ) .unwrap(); diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 9b13a20ab..b997cca28 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -3,6 +3,7 @@ mod compress; #[cfg(any(feature = "brotli", feature = "flate2"))] pub use self::compress::Compress; +pub mod cors; mod defaultheaders; mod errhandlers; mod logger; diff --git a/src/test.rs b/src/test.rs index ed9cf27cc..7e79659af 100644 --- a/src/test.rs +++ b/src/test.rs @@ -3,13 +3,13 @@ use std::cell::RefCell; use std::rc::Rc; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; -use actix_http::http::{HttpTryFrom, Method, Version}; +use actix_http::http::{HttpTryFrom, Method, StatusCode, Version}; use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{Extensions, PayloadStream, Request}; use actix_router::{Path, ResourceDef, Url}; use actix_rt::Runtime; use actix_server_config::ServerConfig; -use actix_service::{IntoNewService, NewService, Service}; +use actix_service::{FnService, IntoNewService, NewService, Service}; use bytes::Bytes; #[cfg(feature = "cookies")] use cookie::Cookie; @@ -17,9 +17,10 @@ use futures::future::{lazy, Future}; use crate::config::{AppConfig, AppConfigInner}; use crate::data::RouteData; +use crate::dev::Body; use crate::rmap::ResourceMap; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; -use crate::{HttpRequest, HttpResponse}; +use crate::{Error, HttpRequest, HttpResponse}; thread_local! { static RT: RefCell = { @@ -55,6 +56,26 @@ where RT.with(move |rt| rt.borrow_mut().block_on(lazy(f))) } +pub fn ok_service() -> impl Service< + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, +> { + default_service(StatusCode::OK) +} + +pub fn default_service( + status_code: StatusCode, +) -> impl Service< + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, +> { + FnService::new(move |req: ServiceRequest| { + req.into_response(HttpResponse::build(status_code).finish()) + }) +} + /// This method accepts application builder instance, and constructs /// service. /// From 548f6f89bf95bc2e2475074c33221768a1d1515e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 23 Mar 2019 21:39:02 -0700 Subject: [PATCH 323/427] allow to get app data via HttpRequest --- src/request.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/request.rs b/src/request.rs index 1722925f8..33b63acd7 100644 --- a/src/request.rs +++ b/src/request.rs @@ -8,6 +8,7 @@ use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; use actix_router::{Path, Url}; use crate::config::AppConfig; +use crate::data::Data; use crate::error::UrlGenerationError; use crate::extract::FromRequest; use crate::info::ConnectionInfo; @@ -100,6 +101,16 @@ impl HttpRequest { &self.config } + /// Get an application data stored with `App::data()` method during + /// application configuration. + pub fn app_data(&self) -> Option> { + if let Some(st) = self.config.extensions().get::>() { + Some(st.clone()) + } else { + None + } + } + /// Generate url for named resource /// /// ```rust @@ -239,8 +250,9 @@ impl fmt::Debug for HttpRequest { mod tests { use super::*; use crate::dev::{ResourceDef, ResourceMap}; - use crate::http::header; - use crate::test::TestRequest; + use crate::http::{header, StatusCode}; + use crate::test::{call_success, init_service, TestRequest}; + use crate::{web, App, HttpResponse}; #[test] fn test_debug() { @@ -356,4 +368,35 @@ mod tests { "https://youtube.com/watch/oHg5SJYRHA0" ); } + + #[test] + fn test_app_data() { + let mut srv = init_service(App::new().data(10usize).service( + web::resource("/").to(|req: HttpRequest| { + if req.app_data::().is_some() { + HttpResponse::Ok() + } else { + HttpResponse::BadRequest() + } + }), + )); + + let req = TestRequest::default().to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + + let mut srv = init_service(App::new().data(10u32).service( + web::resource("/").to(|req: HttpRequest| { + if req.app_data::().is_some() { + HttpResponse::Ok() + } else { + HttpResponse::BadRequest() + } + }), + )); + + let req = TestRequest::default().to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } } From bc01d39d4d5485e62edbf2cb94570cde63b7ed54 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 23 Mar 2019 22:03:40 -0700 Subject: [PATCH 324/427] add error response test for cors --- src/middleware/cors.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/middleware/cors.rs b/src/middleware/cors.rs index 8f33d69b0..9ba09256c 100644 --- a/src/middleware/cors.rs +++ b/src/middleware/cors.rs @@ -871,6 +871,8 @@ mod tests { assert!(cors.inner.validate_allowed_method(&req).is_err()); assert!(cors.inner.validate_allowed_headers(&req).is_err()); + let resp = test::call_success(&mut cors, req); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_header("Origin", "https://www.example.com") .header(header::ACCESS_CONTROL_REQUEST_METHOD, "put") From 307b2e5b0e2732a600ca6d34df18a2559ded0c03 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 24 Mar 2019 11:29:35 -0700 Subject: [PATCH 325/427] fix compress features --- .travis.yml | 6 +++--- src/app.rs | 2 +- src/middleware/compress.rs | 29 ++++++++++++++++------------- src/middleware/mod.rs | 1 + 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9caaac1b5..061c8b8e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,13 +35,13 @@ before_script: script: - cargo clean - - cargo test --all -- --nocapture + - cargo test --all-features --all -- --nocapture # Upload docs after_success: - | if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then - cargo doc --no-deps && + cargo doc --all-features --no-deps && echo "" > target/doc/index.html && git clone https://github.com/davisp/ghp-import.git && ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc && @@ -49,7 +49,7 @@ after_success: fi - | if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then - taskset -c 0 cargo tarpaulin --out Xml --all + taskset -c 0 cargo tarpaulin --out Xml --all --all-features bash <(curl -s https://codecov.io/bash) echo "Uploaded code coverage" fi diff --git a/src/app.rs b/src/app.rs index c4f2e33bd..991288dd7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -361,7 +361,7 @@ where } } - /// Default resource to be used if no matching route could be found. + /// Default resource to be used if no matching resource could be found. pub fn default_resource(mut self, f: F) -> Self where F: FnOnce(Resource

    ) -> Resource, diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index b3880a539..3c4718fed 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -1,3 +1,4 @@ +/// `Middleware` for compressing response body. use std::io::Write; use std::marker::PhantomData; use std::str::FromStr; @@ -17,15 +18,17 @@ use log::trace; #[cfg(feature = "brotli")] use brotli2::write::BrotliEncoder; -#[cfg(feature = "flate2")] +#[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] use flate2::write::{GzEncoder, ZlibEncoder}; use crate::service::{ServiceRequest, ServiceResponse}; #[derive(Debug, Clone)] +/// `Middleware` for compressing response body. pub struct Compress(ContentEncoding); impl Compress { + /// Create new `Compress` middleware with default encoding. pub fn new(encoding: ContentEncoding) -> Self { Compress(encoding) } @@ -283,9 +286,9 @@ impl io::Write for Writer { } pub(crate) enum ContentEncoder { - #[cfg(feature = "flate2")] + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] Deflate(ZlibEncoder), - #[cfg(feature = "flate2")] + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] Gzip(GzEncoder), #[cfg(feature = "brotli")] Br(BrotliEncoder), @@ -296,9 +299,9 @@ impl fmt::Debug for ContentEncoder { match *self { #[cfg(feature = "brotli")] ContentEncoder::Br(_) => writeln!(f, "ContentEncoder(Brotli)"), - #[cfg(feature = "flate2")] + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] ContentEncoder::Deflate(_) => writeln!(f, "ContentEncoder(Deflate)"), - #[cfg(feature = "flate2")] + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] ContentEncoder::Gzip(_) => writeln!(f, "ContentEncoder(Gzip)"), } } @@ -307,12 +310,12 @@ impl fmt::Debug for ContentEncoder { impl ContentEncoder { fn encoder(encoding: ContentEncoding) -> Option { match encoding { - #[cfg(feature = "flate2")] + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new( Writer::new(), flate2::Compression::fast(), ))), - #[cfg(feature = "flate2")] + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( Writer::new(), flate2::Compression::fast(), @@ -330,9 +333,9 @@ impl ContentEncoder { match *self { #[cfg(feature = "brotli")] ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), - #[cfg(feature = "flate2")] + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), - #[cfg(feature = "flate2")] + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), } } @@ -344,12 +347,12 @@ impl ContentEncoder { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, - #[cfg(feature = "flate2")] + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] ContentEncoder::Gzip(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, - #[cfg(feature = "flate2")] + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] ContentEncoder::Deflate(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), @@ -367,7 +370,7 @@ impl ContentEncoder { Err(err) } }, - #[cfg(feature = "flate2")] + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), Err(err) => { @@ -375,7 +378,7 @@ impl ContentEncoder { Err(err) } }, - #[cfg(feature = "flate2")] + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), Err(err) => { diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index b997cca28..e984b1d81 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1,3 +1,4 @@ +//! Middlewares #[cfg(any(feature = "brotli", feature = "flate2"))] mod compress; #[cfg(any(feature = "brotli", feature = "flate2"))] From ede32c8b3fbb29c5ea096c6f5d36ec1d0b46f2e8 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 24 Mar 2019 11:32:30 -0700 Subject: [PATCH 326/427] export errhandlers module --- src/middleware/errhandlers.rs | 2 ++ src/middleware/mod.rs | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/middleware/errhandlers.rs b/src/middleware/errhandlers.rs index 7a79aae16..41bdb384a 100644 --- a/src/middleware/errhandlers.rs +++ b/src/middleware/errhandlers.rs @@ -1,3 +1,4 @@ +/// Custom handlers service for responses. use std::rc::Rc; use actix_service::{Service, Transform}; @@ -106,6 +107,7 @@ where } } +#[doc(hidden)] pub struct ErrorHandlersMiddleware { service: S, handlers: Rc>>>, diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index e984b1d81..998b59052 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -6,11 +6,10 @@ pub use self::compress::Compress; pub mod cors; mod defaultheaders; -mod errhandlers; +pub mod errhandlers; mod logger; pub use self::defaultheaders::DefaultHeaders; -pub use self::errhandlers::{ErrorHandlerResponse, ErrorHandlers}; pub use self::logger::Logger; #[cfg(feature = "cookies")] From 913155d34ccb88f558dd933098bf3c6f7daef21a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 24 Mar 2019 11:47:23 -0700 Subject: [PATCH 327/427] update doc strings --- src/lib.rs | 77 +++++++++++++++++++++++++++++++++++ src/middleware/errhandlers.rs | 2 +- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index f59cbc265..926ca6d80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,80 @@ +//! Actix web is a small, pragmatic, and extremely fast web framework +//! for Rust. +//! +//! ```rust +//! use actix_web::{web, App, Responder, HttpServer}; +//! # use std::thread; +//! +//! fn index(info: web::Path<(String, u32)>) -> impl Responder { +//! format!("Hello {}! id:{}", info.0, info.1) +//! } +//! +//! fn main() -> std::io::Result<()> { +//! # thread::spawn(|| { +//! HttpServer::new(|| App::new().service( +//! web::resource("/{name}/{id}/index.html").to(index)) +//! ) +//! .bind("127.0.0.1:8080")? +//! .run() +//! # }); +//! # Ok(()) +//! } +//! ``` +//! +//! ## Documentation & community resources +//! +//! Besides the API documentation (which you are currently looking +//! at!), several other resources are available: +//! +//! * [User Guide](https://actix.rs/docs/) +//! * [Chat on gitter](https://gitter.im/actix/actix) +//! * [GitHub repository](https://github.com/actix/actix-web) +//! * [Cargo package](https://crates.io/crates/actix-web) +//! +//! To get started navigating the API documentation you may want to +//! consider looking at the following pages: +//! +//! * [App](struct.App.html): This struct represents an actix-web +//! application and is used to configure routes and other common +//! settings. +//! +//! * [HttpServer](struct.HttpServer.html): This struct +//! represents an HTTP server instance and is used to instantiate and +//! configure servers. +//! +//! * [HttpRequest](struct.HttpRequest.html) and +//! [HttpResponse](struct.HttpResponse.html): These structs +//! represent HTTP requests and responses and expose various methods +//! for inspecting, creating and otherwise utilizing them. +//! +//! ## Features +//! +//! * Supported *HTTP/1.x* and *HTTP/2.0* protocols +//! * Streaming and pipelining +//! * Keep-alive and slow requests handling +//! * `WebSockets` server/client +//! * Transparent content compression/decompression (br, gzip, deflate) +//! * Configurable request routing +//! * Multipart streams +//! * SSL support with OpenSSL or `native-tls` +//! * Middlewares (`Logger`, `Session`, `CORS`, `CSRF`, `DefaultHeaders`) +//! * Supports [Actix actor framework](https://github.com/actix/actix) +//! * Supported Rust version: 1.32 or later +//! +//! ## Package feature +//! +//! * `tls` - enables ssl support via `native-tls` crate +//! * `ssl` - enables ssl support via `openssl` crate, supports `http/2` +//! * `rust-tls` - enables ssl support via `rustls` crate, supports `http/2` +//! * `cookies` - enables cookies support, includes `ring` crate as +//! dependency +//! * `brotli` - enables `brotli` compression support, requires `c` +//! compiler +//! * `flate2-c` - enables `gzip`, `deflate` compression support, requires +//! `c` compiler +//! * `flate2-rust` - experimental rust based implementation for +//! `gzip`, `deflate` compression. +//! #![allow(clippy::type_complexity, clippy::new_without_default)] mod app; diff --git a/src/middleware/errhandlers.rs b/src/middleware/errhandlers.rs index 41bdb384a..78a09bc00 100644 --- a/src/middleware/errhandlers.rs +++ b/src/middleware/errhandlers.rs @@ -29,7 +29,7 @@ type ErrorHandler = Fn(ServiceResponse) -> Result> /// ## Example /// /// ```rust -/// use actix_web::middleware::{ErrorHandlers, ErrorHandlerResponse}; +/// use actix_web::middleware::errhandlers::{ErrorHandlers, ErrorHandlerResponse}; /// use actix_web::{web, http, dev, App, HttpRequest, HttpResponse, Result}; /// /// fn render_500(mut res: dev::ServiceResponse) -> Result> { From c1e8d8363c4453fd771855373d3877ec8d8b978b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 24 Mar 2019 11:49:26 -0700 Subject: [PATCH 328/427] fix errhandlers doc string --- src/middleware/errhandlers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/errhandlers.rs b/src/middleware/errhandlers.rs index 78a09bc00..ca9b1cd59 100644 --- a/src/middleware/errhandlers.rs +++ b/src/middleware/errhandlers.rs @@ -1,4 +1,4 @@ -/// Custom handlers service for responses. +//! Custom handlers service for responses. use std::rc::Rc; use actix_service::{Service, Transform}; From 9932a342ef9f6fcde4fffb6c9264ce01e9fb289e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 24 Mar 2019 11:59:35 -0700 Subject: [PATCH 329/427] export Scope --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 926ca6d80..79e1ba34a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,6 +115,7 @@ pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::{Either, Responder}; pub use crate::route::Route; +pub use crate::scope::Scope; pub use crate::server::HttpServer; pub mod dev { From ffb3324129fa84595e37fb11a5162116498e88c0 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 24 Mar 2019 16:15:34 -0700 Subject: [PATCH 330/427] do not use default resource from app, return 405 if no matching route found --- src/resource.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/src/resource.rs b/src/resource.rs index 46b3e2a8f..29ff07857 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -39,6 +39,10 @@ type HttpNewService

    = /// web::resource("/") /// .route(web::get().to(|| HttpResponse::Ok()))); /// } +/// ``` +/// +/// If no matching route could be found, *405* response code get returned. +/// Default behavior could be overriden with `default_resource()` method. pub struct Resource> { endpoint: T, rdef: String, @@ -261,6 +265,8 @@ where } /// Default resource to be used if no matching route could be found. + /// By default *405* response get returned. Resource does not use + /// default handler from `App` or `Scope`. pub fn default_resource(mut self, f: F) -> Self where F: FnOnce(Resource

    ) -> R, @@ -291,9 +297,6 @@ where > + 'static, { fn register(mut self, config: &mut ServiceConfig

    ) { - if self.default.borrow().is_none() { - *self.default.borrow_mut() = Some(config.default_service()); - } let guards = if self.guards.is_empty() { None } else { @@ -454,7 +457,7 @@ impl

    Service for ResourceService

    { let req = req.into_request(); Either::B(Either::B(ok(ServiceResponse::new( req, - Response::NotFound().finish(), + Response::MethodNotAllowed().finish(), )))) } } @@ -483,3 +486,48 @@ impl NewService for ResourceEndpoint

    { self.factory.borrow_mut().as_mut().unwrap().new_service(&()) } } + +#[cfg(test)] +mod tests { + use crate::http::{Method, StatusCode}; + use crate::test::{call_success, init_service, TestRequest}; + use crate::{web, App, HttpResponse}; + + #[test] + fn test_default_resource() { + let mut srv = init_service( + App::new() + .service( + web::resource("/test").route(web::get().to(|| HttpResponse::Ok())), + ) + .default_resource(|r| r.to(|| HttpResponse::BadRequest())), + ); + let req = TestRequest::with_uri("/test").to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/test") + .method(Method::POST) + .to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + + let mut srv = init_service( + App::new().service( + web::resource("/test") + .route(web::get().to(|| HttpResponse::Ok())) + .default_resource(|r| r.to(|| HttpResponse::BadRequest())), + ), + ); + + let req = TestRequest::with_uri("/test").to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/test") + .method(Method::POST) + .to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } +} From b95e99a09e5318651a3f8588a8f8090a760f1261 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 24 Mar 2019 16:17:59 -0700 Subject: [PATCH 331/427] update changes --- CHANGES.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index ccbb8f070..e44f5dc3c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ # Changes -## [1.0.0] - 2019-10-x +## [1.0.0-alpha.1] - 2019-03-x + +### Changed + +* Return 405 response if no matching route found within resource #538 From ed322c175ee5d245b5b5e88e03d2bf97a8ab2a6d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 24 Mar 2019 16:28:16 -0700 Subject: [PATCH 332/427] update tests --- src/route.rs | 2 +- src/scope.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/route.rs b/src/route.rs index 626b09514..1f1aed471 100644 --- a/src/route.rs +++ b/src/route.rs @@ -442,6 +442,6 @@ mod tests { .method(Method::HEAD) .to_request(); let resp = call_success(&mut srv, req); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } } diff --git a/src/scope.rs b/src/scope.rs index bf3261f2f..4a894450c 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -598,7 +598,7 @@ mod tests { .method(Method::POST) .to_request(); let resp = block_on(srv.call(req)).unwrap(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } #[test] From e37e81af0b80e9a90ae49950b6e45cac998340dd Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 24 Mar 2019 17:00:59 -0700 Subject: [PATCH 333/427] simplify Payload extractor --- actix-files/Cargo.toml | 8 -------- actix-files/src/config.rs | 31 +++++++++++++---------------- actix-files/src/named.rs | 29 +++++++++++---------------- actix-session/Cargo.toml | 10 +--------- actix-web-actors/tests/test_ws.rs | 11 ++++++----- src/types/payload.rs | 33 +++++++++++++++++-------------- 6 files changed, 51 insertions(+), 71 deletions(-) diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index c0f38b9a6..aa93ac225 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -32,12 +32,4 @@ percent-encoding = "1.0" v_htmlescape = "0.4" [dev-dependencies] -actix-rt = "0.1.0" -#actix-server = { version="0.2", features=["ssl"] } actix-web = { path="..", features=["ssl"] } -actix-server = { git = "https://github.com/actix/actix-net.git", features=["ssl"] } -actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } -actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } -rand = "0.6" -env_logger = "0.6" -serde_derive = "1.0" diff --git a/actix-files/src/config.rs b/actix-files/src/config.rs index da72da201..7ad65ae79 100644 --- a/actix-files/src/config.rs +++ b/actix-files/src/config.rs @@ -1,5 +1,4 @@ -use actix_http::http::header::DispositionType; -use actix_web::http::Method; +use actix_web::http::{header::DispositionType, Method}; use mime; /// Describes `StaticFiles` configiration @@ -11,11 +10,9 @@ use mime; /// /// ## Example /// -/// ```rust,ignore -/// extern crate mime; -/// extern crate actix_web; +/// ```rust /// use actix_web::http::header::DispositionType; -/// use actix_web::fs::{StaticFileConfig, NamedFile}; +/// use actix_files::{StaticFileConfig, NamedFile}; /// /// #[derive(Default)] /// struct MyConfig; @@ -29,10 +26,10 @@ use mime; /// let file = NamedFile::open_with_config("foo.txt", MyConfig); /// ``` pub trait StaticFileConfig: Default { - ///Describes mapping for mime type to content disposition header + /// Describes mapping for mime type to content disposition header /// - ///By default `IMAGE`, `TEXT` and `VIDEO` are mapped to Inline. - ///Others are mapped to Attachment + /// By default `IMAGE`, `TEXT` and `VIDEO` are mapped to Inline. + /// Others are mapped to Attachment fn content_disposition_map(typ: mime::Name) -> DispositionType { match typ { mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, @@ -40,30 +37,30 @@ pub trait StaticFileConfig: Default { } } - ///Describes whether Actix should attempt to calculate `ETag` + /// Describes whether Actix should attempt to calculate `ETag` /// - ///Defaults to `true` + /// Defaults to `true` fn is_use_etag() -> bool { true } - ///Describes whether Actix should use last modified date of file. + /// Describes whether Actix should use last modified date of file. /// - ///Defaults to `true` + /// Defaults to `true` fn is_use_last_modifier() -> bool { true } - ///Describes allowed methods to access static resources. + /// Describes allowed methods to access static resources. /// - ///By default all methods are allowed + /// By default all methods are allowed fn is_method_allowed(_method: &Method) -> bool { true } } -///Default content disposition as described in -///[StaticFileConfig](trait.StaticFileConfig.html) +/// Default content disposition as described in +/// [StaticFileConfig](trait.StaticFileConfig.html) #[derive(Default)] pub struct DefaultConfig; diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 6372a183c..2bfa30675 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -11,10 +11,9 @@ use std::os::unix::fs::MetadataExt; use mime; use mime_guess::guess_mime_type; -use actix_http::error::Error; -use actix_http::http::header::{self, ContentDisposition, DispositionParam}; +use actix_web::http::header::{self, ContentDisposition, DispositionParam}; use actix_web::http::{ContentEncoding, Method, StatusCode}; -use actix_web::{HttpMessage, HttpRequest, HttpResponse, Responder}; +use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder}; use crate::config::{DefaultConfig, StaticFileConfig}; use crate::range::HttpRange; @@ -42,10 +41,8 @@ impl NamedFile { /// /// # Examples /// - /// ```rust,ignore - /// extern crate actix_web; - /// - /// use actix_web::fs::NamedFile; + /// ```rust + /// use actix_files::NamedFile; /// use std::io::{self, Write}; /// use std::env; /// use std::fs::File; @@ -65,8 +62,8 @@ impl NamedFile { /// /// # Examples /// - /// ```rust,ignore - /// use actix_web::fs::NamedFile; + /// ```rust + /// use actix_files::NamedFile; /// /// let file = NamedFile::open("foo.txt"); /// ``` @@ -83,10 +80,8 @@ impl NamedFile { /// /// # Examples /// - /// ```rust,ignore - /// extern crate actix_web; - /// - /// use actix_web::fs::{DefaultConfig, NamedFile}; + /// ```rust + /// use actix_files::{DefaultConfig, NamedFile}; /// use std::io::{self, Write}; /// use std::env; /// use std::fs::File; @@ -147,8 +142,8 @@ impl NamedFile { /// /// # Examples /// - /// ```rust,ignore - /// use actix_web::fs::{DefaultConfig, NamedFile}; + /// ```rust + /// use actix_files::{DefaultConfig, NamedFile}; /// /// let file = NamedFile::open_with_config("foo.txt", DefaultConfig); /// ``` @@ -169,9 +164,9 @@ impl NamedFile { /// /// # Examples /// - /// ```rust,ignore + /// ```rust /// # use std::io; - /// use actix_web::fs::NamedFile; + /// use actix_files::NamedFile; /// /// # fn path() -> io::Result<()> { /// let file = NamedFile::open("test.txt")?; diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml index 421c6fc42..554f3d7fc 100644 --- a/actix-session/Cargo.toml +++ b/actix-session/Cargo.toml @@ -25,23 +25,15 @@ cookie-session = ["cookie/secure"] [dependencies] actix-web = { path=".." } -actix-codec = "0.1.1" actix-service = "0.3.3" -actix-utils = "0.3.3" -actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-router = { git = "https://github.com/actix/actix-net.git" } -actix-server = { git = "https://github.com/actix/actix-net.git" } - bytes = "0.4" cookie = { version="0.11", features=["percent-encode"], optional=true } derive_more = "0.14" -encoding = "0.2" futures = "0.1" hashbrown = "0.1.8" -log = "0.4" serde = "1.0" serde_json = "1.0" time = "0.1" [dev-dependencies] -actix-rt = "0.2.0" +actix-rt = "0.2.1" diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs index 202d562ca..687cf4314 100644 --- a/actix-web-actors/tests/test_ws.rs +++ b/actix-web-actors/tests/test_ws.rs @@ -26,11 +26,12 @@ impl StreamHandler for Ws { #[test] fn test_simple() { - let mut srv = TestServer::new(|| { - HttpService::new(App::new().service(web::resource("/").to( - |req: HttpRequest, stream: web::Payload<_>| ws::start(Ws, &req, stream), - ))) - }); + let mut srv = + TestServer::new(|| { + HttpService::new(App::new().service(web::resource("/").to( + |req: HttpRequest, stream: web::Payload| ws::start(Ws, &req, stream), + ))) + }); // client service let framed = srv.ws().unwrap(); diff --git a/src/types/payload.rs b/src/types/payload.rs index 402486b66..170b9c627 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -23,9 +23,7 @@ use crate::service::ServiceFromRequest; /// use actix_web::{web, error, App, Error, HttpResponse}; /// /// /// extract binary data from request -/// fn index

    (body: web::Payload

    ) -> impl Future -/// where -/// P: Stream +/// fn index(body: web::Payload) -> impl Future /// { /// body.map_err(Error::from) /// .fold(web::BytesMut::new(), move |mut body, chunk| { @@ -45,12 +43,9 @@ use crate::service::ServiceFromRequest; /// ); /// } /// ``` -pub struct Payload(crate::dev::Payload); +pub struct Payload(crate::dev::Payload>>); -impl Stream for Payload -where - T: Stream, -{ +impl Stream for Payload { type Item = Bytes; type Error = PayloadError; @@ -69,9 +64,7 @@ where /// use actix_web::{web, error, App, Error, HttpResponse}; /// /// /// extract binary data from request -/// fn index

    (body: web::Payload

    ) -> impl Future -/// where -/// P: Stream +/// fn index(body: web::Payload) -> impl Future /// { /// body.map_err(Error::from) /// .fold(web::BytesMut::new(), move |mut body, chunk| { @@ -91,16 +84,26 @@ where /// ); /// } /// ``` -impl

    FromRequest

    for Payload

    +impl

    FromRequest

    for Payload where - P: Stream, + P: Stream + 'static, { type Error = Error; - type Future = Result, Error>; + type Future = Result; #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - Ok(Payload(req.take_payload())) + let pl = match req.take_payload() { + crate::dev::Payload::Stream(s) => { + let pl: Box> = + Box::new(s); + crate::dev::Payload::Stream(pl) + } + crate::dev::Payload::None => crate::dev::Payload::None, + crate::dev::Payload::H1(pl) => crate::dev::Payload::H1(pl), + crate::dev::Payload::H2(pl) => crate::dev::Payload::H2(pl), + }; + Ok(Payload(pl)) } } From 51e4dcf3b36b1399b72174c82101b8ad4492d185 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 24 Mar 2019 17:13:17 -0700 Subject: [PATCH 334/427] update test doc string --- src/test.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test.rs b/src/test.rs index 7e79659af..c5936ea35 100644 --- a/src/test.rs +++ b/src/test.rs @@ -80,13 +80,13 @@ pub fn default_service( /// service. /// /// ```rust,ignore -/// use actix_web::{test, App, HttpResponse, http::StatusCode}; /// use actix_service::Service; +/// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; /// /// fn main() { /// let mut app = test::init_service( /// App::new() -/// .resource("/test", |r| r.to(|| HttpResponse::Ok())) +/// .service(web::resource("/test").to(|| HttpResponse::Ok())) /// ); /// /// // Create request object @@ -123,7 +123,7 @@ where /// fn main() { /// let mut app = test::init_service( /// App::new() -/// .resource("/test", |r| r.to(|| HttpResponse::Ok())) +/// .service(web::resource("/test").to(|| HttpResponse::Ok())) /// ); /// /// // Create request object From 1970c99522ef37d4a5fbed404b9b100912fad69a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 24 Mar 2019 20:21:20 -0700 Subject: [PATCH 335/427] add session test --- actix-session/src/lib.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/actix-session/src/lib.rs b/actix-session/src/lib.rs index 1dd367ba7..ae0fd23cd 100644 --- a/actix-session/src/lib.rs +++ b/actix-session/src/lib.rs @@ -181,3 +181,30 @@ impl

    FromRequest

    for Session { Ok(Session::get_session(req)) } } + +#[cfg(test)] +mod tests { + use actix_web::{test, HttpResponse}; + + use super::*; + + #[test] + fn session() { + let mut req = test::TestRequest::default().to_service(); + + Session::set_session( + vec![("key".to_string(), "\"value\"".to_string())].into_iter(), + &mut req, + ); + let session = Session::get_session(&mut req); + let res = session.get::("key").unwrap(); + assert_eq!(res, Some("value".to_string())); + + session.set("key2", "value2".to_string()).unwrap(); + session.remove("key"); + + let mut res = req.into_response(HttpResponse::Ok().finish()); + let changes: Vec<_> = Session::get_changes(&mut res).unwrap().collect(); + assert_eq!(changes, [("key2".to_string(), "\"value2\"".to_string())]); + } +} From 939d2e745c9ae3e5ba1f240f0613efa8db66390c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 25 Mar 2019 12:47:58 -0700 Subject: [PATCH 336/427] rename Resource::middleware to Resource::wrap and add wrap_fn for fn middlewares --- examples/basic.rs | 2 +- src/lib.rs | 26 ++++++++++ src/resource.rs | 125 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 149 insertions(+), 4 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index 756f1b796..ee7e4c967 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -33,7 +33,7 @@ fn main() -> std::io::Result<()> { .service(no_params) .service( web::resource("/resource2/index.html") - .middleware( + .wrap( middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), ) .default_resource(|r| { diff --git a/src/lib.rs b/src/lib.rs index 79e1ba34a..8ae7156c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -340,4 +340,30 @@ pub mod web { { blocking::run(f).from_err() } + + use actix_service::{fn_transform, Service, Transform}; + + use crate::service::{ServiceRequest, ServiceResponse}; + + /// Create middleare + pub fn md( + f: F, + ) -> impl Transform< + S, + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = Error, + InitError = (), + > + where + S: Service< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = Error, + >, + F: FnMut(ServiceRequest

    , &mut S) -> R + Clone, + R: IntoFuture, Error = Error>, + { + fn_transform(f) + } } diff --git a/src/resource.rs b/src/resource.rs index 29ff07857..5d5671310 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -230,7 +230,9 @@ where /// This is similar to `App's` middlewares, but middleware get invoked on resource level. /// Resource level middlewares are not allowed to change response /// type (i.e modify response's body). - pub fn middleware( + /// + /// **Note**: middlewares get called in opposite order of middlewares registration. + pub fn wrap( self, mw: F, ) -> Resource< @@ -264,6 +266,57 @@ where } } + /// Register a resource middleware function. + /// + /// This function accepts instance of `ServiceRequest` type and + /// mutable reference to the next middleware in chain. + /// + /// This is similar to `App's` middlewares, but middleware get invoked on resource level. + /// Resource level middlewares are not allowed to change response + /// type (i.e modify response's body). + /// + /// ```rust + /// use actix_service::Service; + /// # use futures::Future; + /// use actix_web::{web, App}; + /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; + /// + /// fn index() -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new().service( + /// web::resource("/index.html") + /// .wrap_fn(|req, srv| + /// srv.call(req).map(|mut res| { + /// res.headers_mut().insert( + /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), + /// ); + /// res + /// })) + /// .route(web::get().to(index))); + /// } + /// ``` + pub fn wrap_fn( + self, + mw: F, + ) -> Resource< + P, + impl NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = Error, + InitError = (), + >, + > + where + F: FnMut(ServiceRequest

    , &mut T::Service) -> R + Clone, + R: IntoFuture, + { + self.wrap(mw) + } + /// Default resource to be used if no matching route could be found. /// By default *405* response get returned. Resource does not use /// default handler from `App` or `Scope`. @@ -489,9 +542,75 @@ impl NewService for ResourceEndpoint

    { #[cfg(test)] mod tests { - use crate::http::{Method, StatusCode}; + use actix_service::Service; + use futures::{Future, IntoFuture}; + + use crate::http::{header, HeaderValue, Method, StatusCode}; + use crate::service::{ServiceRequest, ServiceResponse}; use crate::test::{call_success, init_service, TestRequest}; - use crate::{web, App, HttpResponse}; + use crate::{web, App, Error, HttpResponse}; + + fn md1( + req: ServiceRequest

    , + srv: &mut S, + ) -> impl IntoFuture, Error = Error> + where + S: Service< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = Error, + >, + { + srv.call(req).map(|mut res| { + res.headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001")); + res + }) + } + + #[test] + fn test_middleware() { + let mut srv = init_service( + App::new().service( + web::resource("/test") + .wrap(md1) + .route(web::get().to(|| HttpResponse::Ok())), + ), + ); + let req = TestRequest::with_uri("/test").to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + HeaderValue::from_static("0001") + ); + } + + #[test] + fn test_middleware_fn() { + let mut srv = init_service( + App::new().service( + web::resource("/test") + .wrap_fn(|req, srv| { + srv.call(req).map(|mut res| { + res.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static("0001"), + ); + res + }) + }) + .route(web::get().to(|| HttpResponse::Ok())), + ), + ); + let req = TestRequest::with_uri("/test").to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + HeaderValue::from_static("0001") + ); + } #[test] fn test_default_resource() { From 86a21c956c247ab098a7631bc18ae60032ef5eac Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 25 Mar 2019 13:02:10 -0700 Subject: [PATCH 337/427] rename .middleware to .wrap --- actix-session/src/cookie.rs | 6 ++--- actix-session/src/lib.rs | 2 +- examples/basic.rs | 6 ++--- src/app.rs | 4 +-- src/middleware/cors.rs | 6 ++--- src/middleware/defaultheaders.rs | 2 +- src/middleware/errhandlers.rs | 2 +- src/middleware/identity.rs | 8 +++--- src/middleware/logger.rs | 4 +-- src/resource.rs | 4 +-- src/scope.rs | 43 +++++++++++++++++++++++++++++--- tests/test_server.rs | 14 +++++------ 12 files changed, 68 insertions(+), 33 deletions(-) diff --git a/actix-session/src/cookie.rs b/actix-session/src/cookie.rs index 37c552ea8..2d5bd7089 100644 --- a/actix-session/src/cookie.rs +++ b/actix-session/src/cookie.rs @@ -178,7 +178,7 @@ impl CookieSessionInner { /// use actix_web::{web, App, HttpResponse, HttpServer}; /// /// fn main() { -/// let app = App::new().middleware( +/// let app = App::new().wrap( /// CookieSession::signed(&[0; 32]) /// .domain("www.rust-lang.org") /// .name("actix_session") @@ -323,7 +323,7 @@ mod tests { fn cookie_session() { let mut app = test::init_service( App::new() - .middleware(CookieSession::signed(&[0; 32]).secure(false)) + .wrap(CookieSession::signed(&[0; 32]).secure(false)) .service(web::resource("/").to(|ses: Session| { let _ = ses.set("counter", 100); "test" @@ -342,7 +342,7 @@ mod tests { fn cookie_session_extractor() { let mut app = test::init_service( App::new() - .middleware(CookieSession::signed(&[0; 32]).secure(false)) + .wrap(CookieSession::signed(&[0; 32]).secure(false)) .service(web::resource("/").to(|ses: Session| { let _ = ses.set("counter", 100); "test" diff --git a/actix-session/src/lib.rs b/actix-session/src/lib.rs index ae0fd23cd..819773c6a 100644 --- a/actix-session/src/lib.rs +++ b/actix-session/src/lib.rs @@ -31,7 +31,7 @@ //! fn main() -> std::io::Result<()> { //! # std::thread::spawn(|| //! HttpServer::new( -//! || App::new().middleware( +//! || App::new().wrap( //! CookieSession::signed(&[0; 32]) // <- create cookie based session middleware //! .secure(false) //! ) diff --git a/examples/basic.rs b/examples/basic.rs index ee7e4c967..911196570 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -26,9 +26,9 @@ fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() - .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) - .middleware(middleware::Compress::default()) - .middleware(middleware::Logger::default()) + .wrap(middleware::DefaultHeaders::new().header("X-Version", "0.2")) + .wrap(middleware::Compress::default()) + .wrap(middleware::Logger::default()) .service(index) .service(no_params) .service( diff --git a/src/app.rs b/src/app.rs index 991288dd7..94a47afe6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -108,7 +108,7 @@ where } /// Register a middleware. - pub fn middleware( + pub fn wrap( self, mw: F, ) -> AppRouter< @@ -322,7 +322,7 @@ where } /// Register a middleware. - pub fn middleware( + pub fn wrap( self, mw: F, ) -> AppRouter< diff --git a/src/middleware/cors.rs b/src/middleware/cors.rs index 9ba09256c..b6acf4299 100644 --- a/src/middleware/cors.rs +++ b/src/middleware/cors.rs @@ -1,8 +1,8 @@ //! Cross-origin resource sharing (CORS) for Actix applications //! //! CORS middleware could be used with application and with resource. -//! Cors middleware could be used as parameter for `App::middleware()`, -//! `Resource::middleware()` or `Scope::middleware()` methods. +//! Cors middleware could be used as parameter for `App::wrap()`, +//! `Resource::wrap()` or `Scope::wrap()` methods. //! //! # Example //! @@ -16,7 +16,7 @@ //! //! fn main() -> std::io::Result<()> { //! HttpServer::new(|| App::new() -//! .middleware( +//! .wrap( //! Cors::new() // <- Construct CORS middleware builder //! .allowed_origin("https://www.rust-lang.org/") //! .allowed_methods(vec!["GET", "POST"]) diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index 4d879cda7..ca5b8f807 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -18,7 +18,7 @@ use crate::service::{ServiceRequest, ServiceResponse}; /// /// fn main() { /// let app = App::new() -/// .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) +/// .wrap(middleware::DefaultHeaders::new().header("X-Version", "0.2")) /// .service( /// web::resource("/test") /// .route(web::get().to(|| HttpResponse::Ok())) diff --git a/src/middleware/errhandlers.rs b/src/middleware/errhandlers.rs index ca9b1cd59..4f2537221 100644 --- a/src/middleware/errhandlers.rs +++ b/src/middleware/errhandlers.rs @@ -41,7 +41,7 @@ type ErrorHandler = Fn(ServiceResponse) -> Result> /// /// fn main() { /// let app = App::new() -/// .middleware( +/// .wrap( /// ErrorHandlers::new() /// .handler(http::StatusCode::INTERNAL_SERVER_ERROR, render_500), /// ) diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs index d0a4146ae..e94f99db1 100644 --- a/src/middleware/identity.rs +++ b/src/middleware/identity.rs @@ -37,7 +37,7 @@ //! } //! //! fn main() { -//! let app = App::new().middleware(IdentityService::new( +//! let app = App::new().wrap(IdentityService::new( //! // <- create identity middleware //! CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend //! .name("auth-cookie") @@ -179,7 +179,7 @@ pub trait IdentityPolicy: Sized + 'static { /// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; /// /// fn main() { -/// let app = App::new().middleware(IdentityService::new( +/// let app = App::new().wrap(IdentityService::new( /// // <- create identity middleware /// CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend /// .name("auth-cookie") @@ -381,7 +381,7 @@ impl CookieIdentityInner { /// use actix_web::App; /// /// fn main() { -/// let app = App::new().middleware(IdentityService::new( +/// let app = App::new().wrap(IdentityService::new( /// // <- create identity middleware /// CookieIdentityPolicy::new(&[0; 32]) // <- construct cookie policy /// .domain("www.rust-lang.org") @@ -473,7 +473,7 @@ mod tests { fn test_identity() { let mut srv = test::init_service( App::new() - .middleware(IdentityService::new( + .wrap(IdentityService::new( CookieIdentityPolicy::new(&[0; 32]) .domain("www.rust-lang.org") .name("actix_auth") diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 4af3e10d8..42f344f03 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -41,8 +41,8 @@ use crate::{HttpMessage, HttpResponse}; /// env_logger::init(); /// /// let app = App::new() -/// .middleware(Logger::default()) -/// .middleware(Logger::new("%a %{User-Agent}i")); +/// .wrap(Logger::default()) +/// .wrap(Logger::new("%a %{User-Agent}i")); /// } /// ``` /// diff --git a/src/resource.rs b/src/resource.rs index 5d5671310..632e9c332 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -550,7 +550,7 @@ mod tests { use crate::test::{call_success, init_service, TestRequest}; use crate::{web, App, Error, HttpResponse}; - fn md1( + fn md( req: ServiceRequest

    , srv: &mut S, ) -> impl IntoFuture, Error = Error> @@ -573,7 +573,7 @@ mod tests { let mut srv = init_service( App::new().service( web::resource("/test") - .wrap(md1) + .wrap(md) .route(web::get().to(|| HttpResponse::Ok())), ), ); diff --git a/src/scope.rs b/src/scope.rs index 4a894450c..1be594f1b 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -205,7 +205,7 @@ where /// This is similar to `App's` middlewares, but middleware get invoked on scope level. /// Scope level middlewares are not allowed to change response /// type (i.e modify response's body). - pub fn middleware( + pub fn wrap( self, mw: F, ) -> Scope< @@ -476,11 +476,13 @@ impl NewService for ScopeEndpoint

    { mod tests { use actix_service::Service; use bytes::Bytes; + use futures::{Future, IntoFuture}; use crate::dev::{Body, ResponseBody}; - use crate::http::{Method, StatusCode}; - use crate::test::{block_on, init_service, TestRequest}; - use crate::{guard, web, App, HttpRequest, HttpResponse}; + use crate::http::{header, HeaderValue, Method, StatusCode}; + use crate::service::{ServiceRequest, ServiceResponse}; + use crate::test::{block_on, call_success, init_service, TestRequest}; + use crate::{guard, web, App, Error, HttpRequest, HttpResponse}; #[test] fn test_scope() { @@ -827,4 +829,37 @@ mod tests { let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } + + fn md( + req: ServiceRequest

    , + srv: &mut S, + ) -> impl IntoFuture, Error = Error> + where + S: Service< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = Error, + >, + { + srv.call(req).map(|mut res| { + res.headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001")); + res + }) + } + + #[test] + fn test_middleware() { + let mut srv = + init_service(App::new().service(web::scope("app").wrap(md).service( + web::resource("/test").route(web::get().to(|| HttpResponse::Ok())), + ))); + let req = TestRequest::with_uri("/app/test").to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + HeaderValue::from_static("0001") + ); + } } diff --git a/tests/test_server.rs b/tests/test_server.rs index 965d444fc..2742164ab 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -59,7 +59,7 @@ fn test_body_gzip() { let mut srv = TestServer::new(|| { h1::H1Service::new( App::new() - .middleware(middleware::Compress::new(ContentEncoding::Gzip)) + .wrap(middleware::Compress::new(ContentEncoding::Gzip)) .service(web::resource("/").route(web::to(|| Response::Ok().body(STR)))), ) }); @@ -87,7 +87,7 @@ fn test_body_gzip_large() { let data = srv_data.clone(); h1::H1Service::new( App::new() - .middleware(middleware::Compress::new(ContentEncoding::Gzip)) + .wrap(middleware::Compress::new(ContentEncoding::Gzip)) .service( web::resource("/") .route(web::to(move || Response::Ok().body(data.clone()))), @@ -121,7 +121,7 @@ fn test_body_gzip_large_random() { let data = srv_data.clone(); h1::H1Service::new( App::new() - .middleware(middleware::Compress::new(ContentEncoding::Gzip)) + .wrap(middleware::Compress::new(ContentEncoding::Gzip)) .service( web::resource("/") .route(web::to(move || Response::Ok().body(data.clone()))), @@ -149,7 +149,7 @@ fn test_body_chunked_implicit() { let mut srv = TestServer::new(move || { h1::H1Service::new( App::new() - .middleware(middleware::Compress::new(ContentEncoding::Gzip)) + .wrap(middleware::Compress::new(ContentEncoding::Gzip)) .service(web::resource("/").route(web::get().to(move || { Response::Ok().streaming(once(Ok::<_, Error>(Bytes::from_static( STR.as_ref(), @@ -181,7 +181,7 @@ fn test_body_br_streaming() { let mut srv = TestServer::new(move || { h1::H1Service::new( App::new() - .middleware(middleware::Compress::new(ContentEncoding::Br)) + .wrap(middleware::Compress::new(ContentEncoding::Br)) .service(web::resource("/").route(web::to(move || { Response::Ok().streaming(once(Ok::<_, Error>(Bytes::from_static( STR.as_ref(), @@ -254,7 +254,7 @@ fn test_body_deflate() { let mut srv = TestServer::new(move || { h1::H1Service::new( App::new() - .middleware(middleware::Compress::new(ContentEncoding::Deflate)) + .wrap(middleware::Compress::new(ContentEncoding::Deflate)) .service( web::resource("/").route(web::to(move || Response::Ok().body(STR))), ), @@ -281,7 +281,7 @@ fn test_body_brotli() { let mut srv = TestServer::new(move || { h1::H1Service::new( App::new() - .middleware(middleware::Compress::new(ContentEncoding::Br)) + .wrap(middleware::Compress::new(ContentEncoding::Br)) .service( web::resource("/").route(web::to(move || Response::Ok().body(STR))), ), From d30027ac5b13d517645492055100ab76632a62f9 Mon Sep 17 00:00:00 2001 From: Douman Date: Mon, 25 Mar 2019 23:02:37 +0300 Subject: [PATCH 338/427] Remove StaticFilesConfig (#731) * Remove StaticFilesConfig * Applying comments * Impl Clone for Files --- actix-files/Cargo.toml | 1 + actix-files/src/config.rs | 67 ------------ actix-files/src/lib.rs | 221 ++++++++++++++++++-------------------- actix-files/src/named.rs | 126 ++++++++++------------ 4 files changed, 162 insertions(+), 253 deletions(-) delete mode 100644 actix-files/src/config.rs diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index aa93ac225..65faa5e8c 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -22,6 +22,7 @@ actix-web = { path=".." } actix-http = { git = "https://github.com/actix/actix-http.git" } actix-service = "0.3.3" +bitflags = "1" bytes = "0.4" futures = "0.1" derive_more = "0.14" diff --git a/actix-files/src/config.rs b/actix-files/src/config.rs deleted file mode 100644 index 7ad65ae79..000000000 --- a/actix-files/src/config.rs +++ /dev/null @@ -1,67 +0,0 @@ -use actix_web::http::{header::DispositionType, Method}; -use mime; - -/// Describes `StaticFiles` configiration -/// -/// To configure actix's static resources you need -/// to define own configiration type and implement any method -/// you wish to customize. -/// As trait implements reasonable defaults for Actix. -/// -/// ## Example -/// -/// ```rust -/// use actix_web::http::header::DispositionType; -/// use actix_files::{StaticFileConfig, NamedFile}; -/// -/// #[derive(Default)] -/// struct MyConfig; -/// -/// impl StaticFileConfig for MyConfig { -/// fn content_disposition_map(typ: mime::Name) -> DispositionType { -/// DispositionType::Attachment -/// } -/// } -/// -/// let file = NamedFile::open_with_config("foo.txt", MyConfig); -/// ``` -pub trait StaticFileConfig: Default { - /// Describes mapping for mime type to content disposition header - /// - /// By default `IMAGE`, `TEXT` and `VIDEO` are mapped to Inline. - /// Others are mapped to Attachment - fn content_disposition_map(typ: mime::Name) -> DispositionType { - match typ { - mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, - _ => DispositionType::Attachment, - } - } - - /// Describes whether Actix should attempt to calculate `ETag` - /// - /// Defaults to `true` - fn is_use_etag() -> bool { - true - } - - /// Describes whether Actix should use last modified date of file. - /// - /// Defaults to `true` - fn is_use_last_modifier() -> bool { - true - } - - /// Describes allowed methods to access static resources. - /// - /// By default all methods are allowed - fn is_method_allowed(_method: &Method) -> bool { - true - } -} - -/// Default content disposition as described in -/// [StaticFileConfig](trait.StaticFileConfig.html) -#[derive(Default)] -pub struct DefaultConfig; - -impl StaticFileConfig for DefaultConfig {} diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index b92400099..31ff4cdaf 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -3,7 +3,6 @@ use std::cell::RefCell; use std::fmt::Write; use std::fs::{DirEntry, File}; use std::io::{Read, Seek}; -use std::marker::PhantomData; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::{cmp, io}; @@ -22,15 +21,14 @@ use actix_web::dev::{ }; use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder}; +use actix_web::http::header::{DispositionType}; use futures::future::{ok, FutureResult}; -mod config; mod error; mod named; mod range; use self::error::{FilesError, UriSegmentError}; -pub use crate::config::{DefaultConfig, StaticFileConfig}; pub use crate::named::NamedFile; pub use crate::range::HttpRange; @@ -211,6 +209,8 @@ fn directory_listing( )) } +type MimeOverride = Fn(&mime::Name) -> DispositionType; + /// Static files handling /// /// `Files` service must be registered with `App::service()` method. @@ -224,16 +224,34 @@ fn directory_listing( /// .service(fs::Files::new("/static", ".")); /// } /// ``` -pub struct Files { +pub struct Files { path: String, directory: PathBuf, index: Option, show_index: bool, default: Rc>>>>, renderer: Rc, + mime_override: Option>, _chunk_size: usize, _follow_symlinks: bool, - _cd_map: PhantomData, + file_flags: named::Flags, +} + +impl Clone for Files { + fn clone(&self) -> Self { + Self { + directory: self.directory.clone(), + index: self.index.clone(), + show_index: self.show_index, + default: self.default.clone(), + renderer: self.renderer.clone(), + _chunk_size: self._chunk_size, + _follow_symlinks: self._follow_symlinks, + file_flags: self.file_flags, + path: self.path.clone(), + mime_override: self.mime_override.clone(), + } + } } impl Files { @@ -243,15 +261,6 @@ impl Files { /// By default pool with 5x threads of available cpus is used. /// Pool size can be changed by setting ACTIX_CPU_POOL environment variable. pub fn new>(path: &str, dir: T) -> Files { - Self::with_config(path, dir, DefaultConfig) - } -} - -impl Files { - /// Create new `Files` instance for specified base directory. - /// - /// Identical with `new` but allows to specify configiration to use. - pub fn with_config>(path: &str, dir: T, _: C) -> Files { let dir = dir.into().canonicalize().unwrap_or_else(|_| PathBuf::new()); if !dir.is_dir() { log::error!("Specified path is not a directory"); @@ -264,9 +273,10 @@ impl Files { show_index: false, default: Rc::new(RefCell::new(None)), renderer: Rc::new(directory_listing), + mime_override: None, _chunk_size: 0, _follow_symlinks: false, - _cd_map: PhantomData, + file_flags: named::Flags::default(), } } @@ -289,20 +299,44 @@ impl Files { self } + /// Specifies mime override callback + pub fn mime_override(mut self, f: F) -> Self where F: Fn(&mime::Name) -> DispositionType + 'static { + self.mime_override = Some(Rc::new(f)); + self + } + /// Set index file /// /// Shows specific index file for directory "/" instead of /// showing files listing. - pub fn index_file>(mut self, index: T) -> Files { + pub fn index_file>(mut self, index: T) -> Self { self.index = Some(index.into()); self } + + #[inline] + ///Specifies whether to use ETag or not. + /// + ///Default is true. + pub fn use_etag(mut self, value: bool) -> Self { + self.file_flags.set(named::Flags::ETAG, value); + self + } + + #[inline] + ///Specifies whether to use Last-Modified or not. + /// + ///Default is true. + pub fn use_last_modified(mut self, value: bool) -> Self { + self.file_flags.set(named::Flags::LAST_MD, value); + self + } + } -impl HttpServiceFactory

    for Files +impl

    HttpServiceFactory

    for Files

    where P: 'static, - C: StaticFileConfig + 'static, { fn register(self, config: &mut ServiceConfig

    ) { if self.default.borrow().is_none() { @@ -317,40 +351,20 @@ where } } -impl NewService for Files { +impl

    NewService for Files

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = Error; - type Service = FilesService; + type Service = Self; type InitError = (); type Future = FutureResult; fn new_service(&self, _: &()) -> Self::Future { - ok(FilesService { - directory: self.directory.clone(), - index: self.index.clone(), - show_index: self.show_index, - default: self.default.clone(), - renderer: self.renderer.clone(), - _chunk_size: self._chunk_size, - _follow_symlinks: self._follow_symlinks, - _cd_map: self._cd_map, - }) + ok(self.clone()) } } -pub struct FilesService { - directory: PathBuf, - index: Option, - show_index: bool, - default: Rc>>>>, - renderer: Rc, - _chunk_size: usize, - _follow_symlinks: bool, - _cd_map: PhantomData, -} - -impl Service for FilesService { +impl

    Service for Files

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = Error; @@ -378,10 +392,18 @@ impl Service for FilesService { if let Some(ref redir_index) = self.index { let path = path.join(redir_index); - match NamedFile::open_with_config(path, C::default()) { - Ok(named_file) => match named_file.respond_to(&req) { - Ok(item) => ok(ServiceResponse::new(req.clone(), item)), - Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + match NamedFile::open(path) { + Ok(mut named_file) => { + if let Some(ref mime_override) = self.mime_override { + let new_disposition = mime_override(&named_file.content_type.type_()); + named_file.content_disposition.disposition = new_disposition; + } + + named_file.flags = self.file_flags; + match named_file.respond_to(&req) { + Ok(item) => ok(ServiceResponse::new(req.clone(), item)), + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + } }, Err(e) => ok(ServiceResponse::from_err(e, req.clone())), } @@ -399,10 +421,18 @@ impl Service for FilesService { )) } } else { - match NamedFile::open_with_config(path, C::default()) { - Ok(named_file) => match named_file.respond_to(&req) { - Ok(item) => ok(ServiceResponse::new(req.clone(), item)), - Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + match NamedFile::open(path) { + Ok(mut named_file) => { + if let Some(ref mime_override) = self.mime_override { + let new_disposition = mime_override(&named_file.content_type.type_()); + named_file.content_disposition.disposition = new_disposition; + } + + named_file.flags = self.file_flags; + match named_file.respond_to(&req) { + Ok(item) => ok(ServiceResponse::new(req.clone(), item)), + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + } }, Err(e) => ok(ServiceResponse::from_err(e, req.clone())), } @@ -606,53 +636,6 @@ mod tests { ); } - #[derive(Default)] - pub struct AllAttachmentConfig; - impl StaticFileConfig for AllAttachmentConfig { - fn content_disposition_map(_typ: mime::Name) -> DispositionType { - DispositionType::Attachment - } - } - - #[derive(Default)] - pub struct AllInlineConfig; - impl StaticFileConfig for AllInlineConfig { - fn content_disposition_map(_typ: mime::Name) -> DispositionType { - DispositionType::Inline - } - } - - #[test] - fn test_named_file_image_attachment_and_custom_config() { - let file = - NamedFile::open_with_config("tests/test.png", AllAttachmentConfig).unwrap(); - - let req = TestRequest::default().to_http_request(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "image/png" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "attachment; filename=\"test.png\"" - ); - - let file = - NamedFile::open_with_config("tests/test.png", AllInlineConfig).unwrap(); - - let req = TestRequest::default().to_http_request(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "image/png" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"test.png\"" - ); - } - #[test] fn test_named_file_binary() { let mut file = NamedFile::open("tests/test.binary").unwrap(); @@ -702,6 +685,25 @@ mod tests { assert_eq!(resp.status(), StatusCode::NOT_FOUND); } + #[test] + fn test_mime_override() { + fn all_attachment(_: &mime::Name) -> DispositionType { + DispositionType::Attachment + } + + let mut srv = test::init_service( + App::new().service(Files::new("/", ".").mime_override(all_attachment).index_file("Cargo.toml")), + ); + + let request = TestRequest::get().uri("/").to_request(); + let response = test::call_success(&mut srv, request); + assert_eq!(response.status(), StatusCode::OK); + + let content_disposition = response.headers().get(header::CONTENT_DISPOSITION).expect("To have CONTENT_DISPOSITION"); + let content_disposition = content_disposition.to_str().expect("Convert CONTENT_DISPOSITION to str"); + assert_eq!(content_disposition, "attachment; filename=\"Cargo.toml\""); + } + #[test] fn test_named_file_ranges_status_code() { let mut srv = test::init_service( @@ -860,21 +862,10 @@ mod tests { assert_eq!(bytes.freeze(), data); } - #[derive(Default)] - pub struct OnlyMethodHeadConfig; - impl StaticFileConfig for OnlyMethodHeadConfig { - fn is_method_allowed(method: &Method) -> bool { - match *method { - Method::HEAD => true, - _ => false, - } - } - } - #[test] fn test_named_file_not_allowed() { let file = - NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); + NamedFile::open("Cargo.toml").unwrap(); let req = TestRequest::default() .method(Method::POST) .to_http_request(); @@ -882,16 +873,10 @@ mod tests { assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); let file = - NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); + NamedFile::open("Cargo.toml").unwrap(); let req = TestRequest::default().method(Method::PUT).to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - - let file = - NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); - let req = TestRequest::default().method(Method::GET).to_http_request(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } // #[test] @@ -910,9 +895,9 @@ mod tests { // } #[test] - fn test_named_file_any_method() { + fn test_named_file_allowed_method() { let req = TestRequest::default() - .method(Method::POST) + .method(Method::GET) .to_http_request(); let file = NamedFile::open("Cargo.toml").unwrap(); let resp = file.respond_to(&req).unwrap(); diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 2bfa30675..d2bf25691 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -1,6 +1,5 @@ use std::fs::{File, Metadata}; use std::io; -use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -8,20 +7,33 @@ use std::time::{SystemTime, UNIX_EPOCH}; #[cfg(unix)] use std::os::unix::fs::MetadataExt; +use bitflags::bitflags; use mime; use mime_guess::guess_mime_type; -use actix_web::http::header::{self, ContentDisposition, DispositionParam}; +use actix_web::http::header::{self, DispositionType, ContentDisposition, DispositionParam}; use actix_web::http::{ContentEncoding, Method, StatusCode}; use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder}; -use crate::config::{DefaultConfig, StaticFileConfig}; use crate::range::HttpRange; use crate::ChunkedReadFile; +bitflags! { + pub(crate) struct Flags: u32 { + const ETAG = 0b00000001; + const LAST_MD = 0b00000010; + } +} + +impl Default for Flags { + fn default() -> Self { + Flags::all() + } +} + /// A file with an associated name. #[derive(Debug)] -pub struct NamedFile { +pub struct NamedFile { path: PathBuf, file: File, pub(crate) content_type: mime::Mime, @@ -30,7 +42,7 @@ pub struct NamedFile { modified: Option, encoding: Option, pub(crate) status_code: StatusCode, - _cd_map: PhantomData, + pub(crate) flags: Flags, } impl NamedFile { @@ -55,49 +67,6 @@ impl NamedFile { /// } /// ``` pub fn from_file>(file: File, path: P) -> io::Result { - Self::from_file_with_config(file, path, DefaultConfig) - } - - /// Attempts to open a file in read-only mode. - /// - /// # Examples - /// - /// ```rust - /// use actix_files::NamedFile; - /// - /// let file = NamedFile::open("foo.txt"); - /// ``` - pub fn open>(path: P) -> io::Result { - Self::open_with_config(path, DefaultConfig) - } -} - -impl NamedFile { - /// Creates an instance from a previously opened file using the provided configuration. - /// - /// The given `path` need not exist and is only used to determine the `ContentType` and - /// `ContentDisposition` headers. - /// - /// # Examples - /// - /// ```rust - /// use actix_files::{DefaultConfig, NamedFile}; - /// use std::io::{self, Write}; - /// use std::env; - /// use std::fs::File; - /// - /// fn main() -> io::Result<()> { - /// let mut file = File::create("foo.txt")?; - /// file.write_all(b"Hello, world!")?; - /// let named_file = NamedFile::from_file_with_config(file, "bar.txt", DefaultConfig)?; - /// Ok(()) - /// } - /// ``` - pub fn from_file_with_config>( - file: File, - path: P, - _: C, - ) -> io::Result> { let path = path.as_ref().to_path_buf(); // Get the name of the file and use it to construct default Content-Type @@ -114,7 +83,10 @@ impl NamedFile { }; let ct = guess_mime_type(&path); - let disposition_type = C::content_disposition_map(ct.type_()); + let disposition_type = match ct.type_() { + mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, + _ => DispositionType::Attachment, + }; let cd = ContentDisposition { disposition: disposition_type, parameters: vec![DispositionParam::Filename(filename.into_owned())], @@ -134,24 +106,21 @@ impl NamedFile { modified, encoding, status_code: StatusCode::OK, - _cd_map: PhantomData, + flags: Flags::default(), }) } - /// Attempts to open a file in read-only mode using provided configuration. + /// Attempts to open a file in read-only mode. /// /// # Examples /// /// ```rust - /// use actix_files::{DefaultConfig, NamedFile}; + /// use actix_files::NamedFile; /// - /// let file = NamedFile::open_with_config("foo.txt", DefaultConfig); + /// let file = NamedFile::open("foo.txt"); /// ``` - pub fn open_with_config>( - path: P, - config: C, - ) -> io::Result> { - Self::from_file_with_config(File::open(&path)?, path, config) + pub fn open>(path: P) -> io::Result { + Self::from_file(File::open(&path)?, path) } /// Returns reference to the underlying `File` object. @@ -213,6 +182,24 @@ impl NamedFile { self } + #[inline] + ///Specifies whether to use ETag or not. + /// + ///Default is true. + pub fn use_etag(mut self, value: bool) -> Self { + self.flags.set(Flags::ETAG, value); + self + } + + #[inline] + ///Specifies whether to use Last-Modified or not. + /// + ///Default is true. + pub fn use_last_modified(mut self, value: bool) -> Self { + self.flags.set(Flags::LAST_MD, value); + self + } + pub(crate) fn etag(&self) -> Option { // This etag format is similar to Apache's. self.modified.as_ref().map(|mtime| { @@ -245,7 +232,7 @@ impl NamedFile { } } -impl Deref for NamedFile { +impl Deref for NamedFile { type Target = File; fn deref(&self) -> &File { @@ -253,7 +240,7 @@ impl Deref for NamedFile { } } -impl DerefMut for NamedFile { +impl DerefMut for NamedFile { fn deref_mut(&mut self) -> &mut File { &mut self.file } @@ -294,7 +281,7 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { } } -impl Responder for NamedFile { +impl Responder for NamedFile { type Error = Error; type Future = Result; @@ -320,15 +307,18 @@ impl Responder for NamedFile { return Ok(resp.streaming(reader)); } - if !C::is_method_allowed(req.method()) { - return Ok(HttpResponse::MethodNotAllowed() - .header(header::CONTENT_TYPE, "text/plain") - .header(header::ALLOW, "GET, HEAD") - .body("This resource only supports GET and HEAD.")); + match req.method() { + &Method::HEAD | &Method::GET => (), + _ => { + return Ok(HttpResponse::MethodNotAllowed() + .header(header::CONTENT_TYPE, "text/plain") + .header(header::ALLOW, "GET, HEAD") + .body("This resource only supports GET and HEAD.")); + } } - let etag = if C::is_use_etag() { self.etag() } else { None }; - let last_modified = if C::is_use_last_modifier() { + let etag = if self.flags.contains(Flags::ETAG) { self.etag() } else { None }; + let last_modified = if self.flags.contains(Flags::LAST_MD) { self.last_modified() } else { None From e18227cc3d97369e90159fafcfc5b80b5d73b917 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 25 Mar 2019 13:43:02 -0700 Subject: [PATCH 339/427] add wrap_fn to App and Scope --- actix-files/src/lib.rs | 43 ++++++---- actix-files/src/named.rs | 16 ++-- src/app.rs | 174 ++++++++++++++++++++++++++++++++++++++- src/scope.rs | 75 ++++++++++++++++- 4 files changed, 282 insertions(+), 26 deletions(-) diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 31ff4cdaf..8254c5fe5 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -20,8 +20,8 @@ use actix_web::dev::{ ServiceResponse, }; use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; +use actix_web::http::header::DispositionType; use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder}; -use actix_web::http::header::{DispositionType}; use futures::future::{ok, FutureResult}; mod error; @@ -300,7 +300,10 @@ impl Files { } /// Specifies mime override callback - pub fn mime_override(mut self, f: F) -> Self where F: Fn(&mime::Name) -> DispositionType + 'static { + pub fn mime_override(mut self, f: F) -> Self + where + F: Fn(&mime::Name) -> DispositionType + 'static, + { self.mime_override = Some(Rc::new(f)); self } @@ -331,7 +334,6 @@ impl Files { self.file_flags.set(named::Flags::LAST_MD, value); self } - } impl

    HttpServiceFactory

    for Files

    @@ -395,7 +397,8 @@ impl

    Service for Files

    { match NamedFile::open(path) { Ok(mut named_file) => { if let Some(ref mime_override) = self.mime_override { - let new_disposition = mime_override(&named_file.content_type.type_()); + let new_disposition = + mime_override(&named_file.content_type.type_()); named_file.content_disposition.disposition = new_disposition; } @@ -404,7 +407,7 @@ impl

    Service for Files

    { Ok(item) => ok(ServiceResponse::new(req.clone(), item)), Err(e) => ok(ServiceResponse::from_err(e, req.clone())), } - }, + } Err(e) => ok(ServiceResponse::from_err(e, req.clone())), } } else if self.show_index { @@ -424,7 +427,8 @@ impl

    Service for Files

    { match NamedFile::open(path) { Ok(mut named_file) => { if let Some(ref mime_override) = self.mime_override { - let new_disposition = mime_override(&named_file.content_type.type_()); + let new_disposition = + mime_override(&named_file.content_type.type_()); named_file.content_disposition.disposition = new_disposition; } @@ -433,7 +437,7 @@ impl

    Service for Files

    { Ok(item) => ok(ServiceResponse::new(req.clone(), item)), Err(e) => ok(ServiceResponse::from_err(e, req.clone())), } - }, + } Err(e) => ok(ServiceResponse::from_err(e, req.clone())), } } @@ -692,15 +696,24 @@ mod tests { } let mut srv = test::init_service( - App::new().service(Files::new("/", ".").mime_override(all_attachment).index_file("Cargo.toml")), + App::new().service( + Files::new("/", ".") + .mime_override(all_attachment) + .index_file("Cargo.toml"), + ), ); let request = TestRequest::get().uri("/").to_request(); let response = test::call_success(&mut srv, request); assert_eq!(response.status(), StatusCode::OK); - let content_disposition = response.headers().get(header::CONTENT_DISPOSITION).expect("To have CONTENT_DISPOSITION"); - let content_disposition = content_disposition.to_str().expect("Convert CONTENT_DISPOSITION to str"); + let content_disposition = response + .headers() + .get(header::CONTENT_DISPOSITION) + .expect("To have CONTENT_DISPOSITION"); + let content_disposition = content_disposition + .to_str() + .expect("Convert CONTENT_DISPOSITION to str"); assert_eq!(content_disposition, "attachment; filename=\"Cargo.toml\""); } @@ -864,16 +877,14 @@ mod tests { #[test] fn test_named_file_not_allowed() { - let file = - NamedFile::open("Cargo.toml").unwrap(); + let file = NamedFile::open("Cargo.toml").unwrap(); let req = TestRequest::default() .method(Method::POST) .to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - let file = - NamedFile::open("Cargo.toml").unwrap(); + let file = NamedFile::open("Cargo.toml").unwrap(); let req = TestRequest::default().method(Method::PUT).to_http_request(); let resp = file.respond_to(&req).unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); @@ -896,9 +907,7 @@ mod tests { #[test] fn test_named_file_allowed_method() { - let req = TestRequest::default() - .method(Method::GET) - .to_http_request(); + let req = TestRequest::default().method(Method::GET).to_http_request(); let file = NamedFile::open("Cargo.toml").unwrap(); let resp = file.respond_to(&req).unwrap(); assert_eq!(resp.status(), StatusCode::OK); diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index d2bf25691..7bc37054a 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -11,7 +11,9 @@ use bitflags::bitflags; use mime; use mime_guess::guess_mime_type; -use actix_web::http::header::{self, DispositionType, ContentDisposition, DispositionParam}; +use actix_web::http::header::{ + self, ContentDisposition, DispositionParam, DispositionType, +}; use actix_web::http::{ContentEncoding, Method, StatusCode}; use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder}; @@ -311,13 +313,17 @@ impl Responder for NamedFile { &Method::HEAD | &Method::GET => (), _ => { return Ok(HttpResponse::MethodNotAllowed() - .header(header::CONTENT_TYPE, "text/plain") - .header(header::ALLOW, "GET, HEAD") - .body("This resource only supports GET and HEAD.")); + .header(header::CONTENT_TYPE, "text/plain") + .header(header::ALLOW, "GET, HEAD") + .body("This resource only supports GET and HEAD.")); } } - let etag = if self.flags.contains(Flags::ETAG) { self.etag() } else { None }; + let etag = if self.flags.contains(Flags::ETAG) { + self.etag() + } else { + None + }; let last_modified = if self.flags.contains(Flags::LAST_MD) { self.last_modified() } else { diff --git a/src/app.rs b/src/app.rs index 94a47afe6..f46f5252f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -147,6 +147,51 @@ where } } + /// Register a middleware function. + /// + /// ```rust + /// use actix_service::Service; + /// # use futures::Future; + /// use actix_web::{web, App}; + /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; + /// + /// fn index() -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new() + /// .wrap_fn(|req, srv| + /// srv.call(req).map(|mut res| { + /// res.headers_mut().insert( + /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), + /// ); + /// res + /// })) + /// .route("/index.html", web::get().to(index)); + /// } + /// ``` + pub fn wrap_fn( + self, + mw: F, + ) -> AppRouter< + T, + P, + B, + impl NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = Error, + InitError = (), + >, + > + where + F: FnMut(ServiceRequest

    , &mut AppRouting

    ) -> R + Clone, + R: IntoFuture, Error = Error>, + { + self.wrap(mw) + } + /// Register a request modifier. It can modify any request parameters /// including payload stream type. pub fn chain( @@ -361,6 +406,29 @@ where } } + /// Register a middleware function. + pub fn wrap_fn( + self, + mw: F, + ) -> AppRouter< + C, + P, + B1, + impl NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = Error, + InitError = (), + >, + > + where + B1: MessageBody, + F: FnMut(ServiceRequest

    , &mut T::Service) -> R + Clone, + R: IntoFuture, Error = Error>, + { + self.wrap(mw) + } + /// Default resource to be used if no matching resource could be found. pub fn default_resource(mut self, f: F) -> Self where @@ -447,11 +515,13 @@ where #[cfg(test)] mod tests { use actix_service::Service; + use futures::{Future, IntoFuture}; use super::*; - use crate::http::{Method, StatusCode}; - use crate::test::{block_on, init_service, TestRequest}; - use crate::{web, HttpResponse}; + use crate::http::{header, HeaderValue, Method, StatusCode}; + use crate::service::{ServiceRequest, ServiceResponse}; + use crate::test::{block_on, call_success, init_service, TestRequest}; + use crate::{web, Error, HttpResponse}; #[test] fn test_default_resource() { @@ -510,4 +580,102 @@ mod tests { let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + + fn md( + req: ServiceRequest

    , + srv: &mut S, + ) -> impl IntoFuture, Error = Error> + where + S: Service< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = Error, + >, + { + srv.call(req).map(|mut res| { + res.headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001")); + res + }) + } + + #[test] + fn test_wrap() { + let mut srv = init_service( + App::new() + .wrap(md) + .route("/test", web::get().to(|| HttpResponse::Ok())), + ); + let req = TestRequest::with_uri("/test").to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + HeaderValue::from_static("0001") + ); + } + + #[test] + fn test_router_wrap() { + let mut srv = init_service( + App::new() + .route("/test", web::get().to(|| HttpResponse::Ok())) + .wrap(md), + ); + let req = TestRequest::with_uri("/test").to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + HeaderValue::from_static("0001") + ); + } + + #[test] + fn test_wrap_fn() { + let mut srv = init_service( + App::new() + .wrap_fn(|req, srv| { + srv.call(req).map(|mut res| { + res.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static("0001"), + ); + res + }) + }) + .service(web::resource("/test").to(|| HttpResponse::Ok())), + ); + let req = TestRequest::with_uri("/test").to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + HeaderValue::from_static("0001") + ); + } + + #[test] + fn test_router_wrap_fn() { + let mut srv = init_service( + App::new() + .route("/test", web::get().to(|| HttpResponse::Ok())) + .wrap_fn(|req, srv| { + srv.call(req).map(|mut res| { + res.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static("0001"), + ); + res + }) + }), + ); + let req = TestRequest::with_uri("/test").to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + HeaderValue::from_static("0001") + ); + } } diff --git a/src/scope.rs b/src/scope.rs index 1be594f1b..8c72824f4 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -8,7 +8,7 @@ use actix_service::{ ApplyTransform, IntoNewService, IntoTransform, NewService, Service, Transform, }; use futures::future::{ok, Either, Future, FutureResult}; -use futures::{Async, Poll}; +use futures::{Async, IntoFuture, Poll}; use crate::dev::{HttpServiceFactory, ServiceConfig}; use crate::error::Error; @@ -237,6 +237,53 @@ where factory_ref: self.factory_ref, } } + + /// Register a scope level middleware function. + /// + /// This function accepts instance of `ServiceRequest` type and + /// mutable reference to the next middleware in chain. + /// + /// ```rust + /// use actix_service::Service; + /// # use futures::Future; + /// use actix_web::{web, App}; + /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; + /// + /// fn index() -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new().service( + /// web::scope("/app") + /// .wrap_fn(|req, srv| + /// srv.call(req).map(|mut res| { + /// res.headers_mut().insert( + /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), + /// ); + /// res + /// })) + /// .route("/index.html", web::get().to(index))); + /// } + /// ``` + pub fn wrap_fn( + self, + mw: F, + ) -> Scope< + P, + impl NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = Error, + InitError = (), + >, + > + where + F: FnMut(ServiceRequest

    , &mut T::Service) -> R + Clone, + R: IntoFuture, + { + self.wrap(mw) + } } impl HttpServiceFactory

    for Scope @@ -862,4 +909,30 @@ mod tests { HeaderValue::from_static("0001") ); } + + #[test] + fn test_middleware_fn() { + let mut srv = init_service( + App::new().service( + web::scope("app") + .wrap_fn(|req, srv| { + srv.call(req).map(|mut res| { + res.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static("0001"), + ); + res + }) + }) + .route("/test", web::get().to(|| HttpResponse::Ok())), + ), + ); + let req = TestRequest::with_uri("/app/test").to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + HeaderValue::from_static("0001") + ); + } } From 8d1195d8acc8c0f56ac1555e6515d67c8eab0501 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 25 Mar 2019 14:33:34 -0700 Subject: [PATCH 340/427] add async handler tests --- Cargo.toml | 1 + src/resource.rs | 15 +++++++++++++++ src/route.rs | 47 +++++++++++++++++++++++++++++++++++++---------- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6920bc09f..185f3fc3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,7 @@ actix-http-test = { git = "https://github.com/actix/actix-http.git", features=[" rand = "0.6" env_logger = "0.6" serde_derive = "1.0" +tokio-timer = "0.2.8" [profile.release] lto = true diff --git a/src/resource.rs b/src/resource.rs index 632e9c332..55237157f 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -542,8 +542,11 @@ impl NewService for ResourceEndpoint

    { #[cfg(test)] mod tests { + use std::time::Duration; + use actix_service::Service; use futures::{Future, IntoFuture}; + use tokio_timer::sleep; use crate::http::{header, HeaderValue, Method, StatusCode}; use crate::service::{ServiceRequest, ServiceResponse}; @@ -573,6 +576,7 @@ mod tests { let mut srv = init_service( App::new().service( web::resource("/test") + .name("test") .wrap(md) .route(web::get().to(|| HttpResponse::Ok())), ), @@ -612,6 +616,17 @@ mod tests { ); } + #[test] + fn test_to_async() { + let mut srv = + init_service(App::new().service(web::resource("/test").to_async(|| { + sleep(Duration::from_millis(100)).then(|_| HttpResponse::Ok()) + }))); + let req = TestRequest::with_uri("/test").to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + } + #[test] fn test_default_resource() { let mut srv = init_service( diff --git a/src/route.rs b/src/route.rs index 1f1aed471..7f1cee3d4 100644 --- a/src/route.rs +++ b/src/route.rs @@ -410,21 +410,36 @@ where #[cfg(test)] mod tests { + use std::time::Duration; + + use futures::Future; + use tokio_timer::sleep; + use crate::http::{Method, StatusCode}; use crate::test::{call_success, init_service, TestRequest}; - use crate::{web, App, Error, HttpResponse}; + use crate::{error, web, App, HttpResponse}; #[test] fn test_route() { - let mut srv = init_service( - App::new().service( - web::resource("/test") - .route(web::get().to(|| HttpResponse::Ok())) - .route( - web::post().to_async(|| Ok::<_, Error>(HttpResponse::Created())), - ), - ), - ); + let mut srv = + init_service( + App::new().service( + web::resource("/test") + .route(web::get().to(|| HttpResponse::Ok())) + .route(web::put().to(|| { + Err::(error::ErrorBadRequest("err")) + })) + .route(web::post().to_async(|| { + sleep(Duration::from_millis(100)) + .then(|_| HttpResponse::Created()) + })) + .route(web::delete().to_async(|| { + sleep(Duration::from_millis(100)).then(|_| { + Err::(error::ErrorBadRequest("err")) + }) + })), + ), + ); let req = TestRequest::with_uri("/test") .method(Method::GET) @@ -438,6 +453,18 @@ mod tests { let resp = call_success(&mut srv, req); assert_eq!(resp.status(), StatusCode::CREATED); + let req = TestRequest::with_uri("/test") + .method(Method::PUT) + .to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/test") + .method(Method::DELETE) + .to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + let req = TestRequest::with_uri("/test") .method(Method::HEAD) .to_request(); From 9037473e0fc60b668cfb2b68beeed43d1a8891a0 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 25 Mar 2019 21:52:45 -0700 Subject: [PATCH 341/427] update client error --- Cargo.toml | 2 +- src/body.rs | 10 ++++++++++ src/client/connection.rs | 2 +- src/client/error.rs | 4 ++++ src/client/request.rs | 11 ++++++++--- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 84b974be5..da11a5853 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,7 @@ failure = { version = "0.1.5", optional = true } openssl = { version="0.10", optional = true } [dev-dependencies] -actix-rt = "0.2.0" +actix-rt = "0.2.1" actix-server = { version = "0.4.0", features=["ssl"] } actix-connect = { version = "0.1.0", features=["ssl"] } actix-http-test = { path="test-server", features=["ssl"] } diff --git a/src/body.rs b/src/body.rs index b7e8ec98a..e1399e6b4 100644 --- a/src/body.rs +++ b/src/body.rs @@ -45,6 +45,16 @@ impl MessageBody for () { } } +impl MessageBody for Box { + fn length(&self) -> BodyLength { + self.as_ref().length() + } + + fn poll_next(&mut self) -> Poll, Error> { + self.as_mut().poll_next() + } +} + pub enum ResponseBody { Body(B), Other(Body), diff --git a/src/client/connection.rs b/src/client/connection.rs index 683738e28..8de23bd24 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -21,7 +21,7 @@ pub(crate) enum ConnectionType { pub trait Connection { type Future: Future; - /// Close connection + /// Send request and body fn send_request( self, head: RequestHead, diff --git a/src/client/error.rs b/src/client/error.rs index 4fce904f1..69ec49585 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -7,6 +7,7 @@ use trust_dns_resolver::error::ResolveError; use openssl::ssl::{Error as SslError, HandshakeError}; use crate::error::{Error, ParseError, ResponseError}; +use crate::http::Error as HttpError; use crate::response::Response; /// A set of errors that can occur while connecting to an HTTP host @@ -98,6 +99,9 @@ pub enum SendRequestError { Send(io::Error), /// Error parsing response Response(ParseError), + /// Http error + #[display(fmt = "{}", _0)] + Http(HttpError), /// Http2 error #[display(fmt = "{}", _0)] H2(h2::Error), diff --git a/src/client/request.rs b/src/client/request.rs index 26713aa46..134a42640 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -118,6 +118,11 @@ impl ClientRequest where B: MessageBody, { + /// Create new client request + pub fn new(head: RequestHead, body: B) -> Self { + ClientRequest { head, body } + } + /// Get the request URI #[inline] pub fn uri(&self) -> &Uri { @@ -174,14 +179,14 @@ where // Send request /// /// This method returns a future that resolves to a ClientResponse - pub fn send( + pub fn send( self, connector: &mut T, ) -> impl Future where B: 'static, - T: Service, - I: Connection, + T: Service, + T::Response: Connection, { let Self { head, body } = self; From 83d44473496ba0abd4d530cef3334cdf82c95ede Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 25 Mar 2019 21:58:01 -0700 Subject: [PATCH 342/427] add http client --- Cargo.toml | 7 +- awc/Cargo.toml | 49 +++++ awc/src/builder.rs | 85 ++++++++ awc/src/connect.rs | 38 ++++ awc/src/lib.rs | 120 ++++++++++++ awc/src/request.rs | 471 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 + 7 files changed, 773 insertions(+), 1 deletion(-) create mode 100644 awc/Cargo.toml create mode 100644 awc/src/builder.rs create mode 100644 awc/src/connect.rs create mode 100644 awc/src/lib.rs create mode 100644 awc/src/request.rs diff --git a/Cargo.toml b/Cargo.toml index 185f3fc3f..2a9883ead 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ path = "src/lib.rs" [workspace] members = [ ".", + "awc", "actix-files", "actix-session", "actix-web-actors", @@ -37,7 +38,10 @@ members = [ features = ["ssl", "tls", "rust-tls", "brotli", "flate2-c", "cookies"] [features] -default = ["brotli", "flate2-c", "cookies"] +default = ["brotli", "flate2-c", "cookies", "client"] + +# http client +client = ["awc"] # brotli encoding, requires c compiler brotli = ["brotli2"] @@ -70,6 +74,7 @@ actix-web-codegen = { path="actix-web-codegen" } actix-http = { git = "https://github.com/actix/actix-http.git", features=["fail"] } actix-server = "0.4.1" actix-server-config = "0.1.0" +awc = { path = "awc", optional = true } bytes = "0.4" derive_more = "0.14" diff --git a/awc/Cargo.toml b/awc/Cargo.toml new file mode 100644 index 000000000..f316d0620 --- /dev/null +++ b/awc/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "awc" +version = "0.1.0" +authors = ["Nikolay Kim "] +description = "Actix web client." +readme = "README.md" +keywords = ["http", "web", "framework", "async", "futures"] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web.git" +documentation = "https://docs.rs/awc/" +license = "MIT/Apache-2.0" +exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] +workspace = ".." +edition = "2018" + +[lib] +name = "awc" +path = "src/lib.rs" + +[features] +default = ["cookies"] + +# openssl +ssl = ["openssl", "actix-http/ssl"] + +# cookies integration +cookies = ["cookie", "actix-http/cookies"] + +[dependencies] +actix-service = "0.3.4" +actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-codec = "0.1.1" +bytes = "0.4" +futures = "0.1" +log =" 0.4" +percent-encoding = "1.0" +serde = "1.0" +serde_json = "1.0" +serde_urlencoded = "0.5.3" + +cookie = { version="0.11", features=["percent-encode"], optional = true } +openssl = { version="0.10", optional = true } + +[dev-dependencies] +env_logger = "0.6" +mime = "0.3" +actix-rt = "0.2.1" +actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } diff --git a/awc/src/builder.rs b/awc/src/builder.rs new file mode 100644 index 000000000..3104e5244 --- /dev/null +++ b/awc/src/builder.rs @@ -0,0 +1,85 @@ +use std::cell::RefCell; +use std::fmt; +use std::rc::Rc; + +use actix_http::client::Connector; +use actix_http::http::{header::IntoHeaderValue, HeaderMap, HeaderName, HttpTryFrom}; + +use crate::connect::{Connect, ConnectorWrapper}; +use crate::Client; + +/// An HTTP Client builder +/// +/// This type can be used to construct an instance of `Client` through a +/// builder-like pattern. +pub struct ClientBuilder { + connector: Rc>, + default_headers: bool, + allow_redirects: bool, + max_redirects: usize, + headers: HeaderMap, +} + +impl ClientBuilder { + pub fn new() -> Self { + ClientBuilder { + default_headers: true, + allow_redirects: true, + max_redirects: 10, + headers: HeaderMap::new(), + connector: Rc::new(RefCell::new(ConnectorWrapper( + Connector::new().service(), + ))), + } + } + + /// Do not follow redirects. + /// + /// Redirects are allowed by default. + pub fn disable_redirects(mut self) -> Self { + self.allow_redirects = false; + self + } + + /// Set max number of redirects. + /// + /// Max redirects is set to 10 by default. + pub fn max_redirects(mut self, num: usize) -> Self { + self.max_redirects = num; + self + } + + /// Do not add default request headers. + /// By default `Accept-Encoding` and `User-Agent` headers are set. + pub fn skip_default_headers(mut self) -> Self { + self.default_headers = false; + self + } + + /// Add default header. This header adds to every request. + pub fn header(mut self, key: K, value: V) -> Self + where + HeaderName: HttpTryFrom, + >::Error: fmt::Debug, + V: IntoHeaderValue, + V::Error: fmt::Debug, + { + match HeaderName::try_from(key) { + Ok(key) => match value.try_into() { + Ok(value) => { + self.headers.append(key, value); + } + Err(e) => log::error!("Header value error: {:?}", e), + }, + Err(e) => log::error!("Header name error: {:?}", e), + } + self + } + + /// Finish build process and create `Client`. + pub fn finish(self) -> Client { + Client { + connector: self.connector, + } + } +} diff --git a/awc/src/connect.rs b/awc/src/connect.rs new file mode 100644 index 000000000..c52bc34cb --- /dev/null +++ b/awc/src/connect.rs @@ -0,0 +1,38 @@ +use actix_http::body::Body; +use actix_http::client::{ClientResponse, ConnectError, Connection, SendRequestError}; +use actix_http::{http, RequestHead}; +use actix_service::Service; +use futures::Future; + +pub(crate) struct ConnectorWrapper(pub T); + +pub(crate) trait Connect { + fn send_request( + &mut self, + head: RequestHead, + body: Body, + ) -> Box>; +} + +impl Connect for ConnectorWrapper +where + T: Service, + T::Response: Connection, + ::Future: 'static, + T::Future: 'static, +{ + fn send_request( + &mut self, + head: RequestHead, + body: Body, + ) -> Box> { + Box::new( + self.0 + // connect to the host + .call(head.uri.clone()) + .from_err() + // send request + .and_then(move |connection| connection.send_request(head, body)), + ) + } +} diff --git a/awc/src/lib.rs b/awc/src/lib.rs new file mode 100644 index 000000000..b1a309c4c --- /dev/null +++ b/awc/src/lib.rs @@ -0,0 +1,120 @@ +use std::cell::RefCell; +use std::rc::Rc; + +pub use actix_http::client::{ + ClientResponse, ConnectError, InvalidUrl, SendRequestError, +}; +pub use actix_http::http; + +use actix_http::client::Connector; +use actix_http::http::{HttpTryFrom, Method, Uri}; + +mod builder; +mod connect; +mod request; + +pub use self::builder::ClientBuilder; +pub use self::request::ClientRequest; + +use self::connect::{Connect, ConnectorWrapper}; + +/// An HTTP Client Request +/// +/// ```rust +/// # use futures::future::{Future, lazy}; +/// use actix_rt::System; +/// use awc::Client; +/// +/// fn main() { +/// System::new("test").block_on(lazy(|| { +/// let mut client = Client::default(); +/// +/// client.get("http://www.rust-lang.org") // <- Create request builder +/// .header("User-Agent", "Actix-web") +/// .send() // <- Send http request +/// .map_err(|_| ()) +/// .and_then(|response| { // <- server http response +/// println!("Response: {:?}", response); +/// Ok(()) +/// }) +/// })); +/// } +/// ``` +#[derive(Clone)] +pub struct Client { + pub(crate) connector: Rc>, +} + +impl Default for Client { + fn default() -> Self { + Client { + connector: Rc::new(RefCell::new(ConnectorWrapper( + Connector::new().service(), + ))), + } + } +} + +impl Client { + /// Build client instance. + pub fn build() -> ClientBuilder { + ClientBuilder::new() + } + + /// Construct HTTP request. + pub fn request(&self, method: Method, url: U) -> ClientRequest + where + Uri: HttpTryFrom, + { + ClientRequest::new(method, url, self.connector.clone()) + } + + pub fn get(&self, url: U) -> ClientRequest + where + Uri: HttpTryFrom, + { + ClientRequest::new(Method::GET, url, self.connector.clone()) + } + + pub fn head(&self, url: U) -> ClientRequest + where + Uri: HttpTryFrom, + { + ClientRequest::new(Method::HEAD, url, self.connector.clone()) + } + + pub fn put(&self, url: U) -> ClientRequest + where + Uri: HttpTryFrom, + { + ClientRequest::new(Method::PUT, url, self.connector.clone()) + } + + pub fn post(&self, url: U) -> ClientRequest + where + Uri: HttpTryFrom, + { + ClientRequest::new(Method::POST, url, self.connector.clone()) + } + + pub fn patch(&self, url: U) -> ClientRequest + where + Uri: HttpTryFrom, + { + ClientRequest::new(Method::PATCH, url, self.connector.clone()) + } + + pub fn delete(&self, url: U) -> ClientRequest + where + Uri: HttpTryFrom, + { + ClientRequest::new(Method::DELETE, url, self.connector.clone()) + } + + pub fn options(&self, url: U) -> ClientRequest + where + Uri: HttpTryFrom, + { + ClientRequest::new(Method::OPTIONS, url, self.connector.clone()) + } +} diff --git a/awc/src/request.rs b/awc/src/request.rs new file mode 100644 index 000000000..90dfebcca --- /dev/null +++ b/awc/src/request.rs @@ -0,0 +1,471 @@ +use std::cell::RefCell; +use std::fmt; +use std::io::Write; +use std::rc::Rc; + +use bytes::{BufMut, Bytes, BytesMut}; +#[cfg(feature = "cookies")] +use cookie::{Cookie, CookieJar}; +use futures::future::{err, Either}; +use futures::{Future, Stream}; +use serde::Serialize; +use serde_json; + +use actix_http::body::{Body, BodyStream}; +use actix_http::client::{ClientResponse, InvalidUrl, SendRequestError}; +use actix_http::http::header::{self, Header, IntoHeaderValue}; +use actix_http::http::{ + uri, ConnectionType, Error as HttpError, HeaderName, HeaderValue, HttpTryFrom, + Method, Uri, Version, +}; +use actix_http::{Error, Head, RequestHead}; + +use crate::Connect; + +/// An HTTP Client request builder +/// +/// This type can be used to construct an instance of `ClientRequest` through a +/// builder-like pattern. +/// +/// ```rust +/// use futures::future::{Future, lazy}; +/// use actix_rt::System; +/// use actix_http::client; +/// +/// fn main() { +/// System::new("test").block_on(lazy(|| { +/// let mut connector = client::Connector::new().service(); +/// +/// client::ClientRequest::get("http://www.rust-lang.org") // <- Create request builder +/// .header("User-Agent", "Actix-web") +/// .finish().unwrap() +/// .send(&mut connector) // <- Send http request +/// .map_err(|_| ()) +/// .and_then(|response| { // <- server http response +/// println!("Response: {:?}", response); +/// Ok(()) +/// }) +/// })); +/// } +/// ``` +pub struct ClientRequest { + head: RequestHead, + err: Option, + #[cfg(feature = "cookies")] + cookies: Option, + default_headers: bool, + connector: Rc>, +} + +impl ClientRequest { + /// Create new client request builder. + pub(crate) fn new( + method: Method, + uri: U, + connector: Rc>, + ) -> Self + where + Uri: HttpTryFrom, + { + let mut err = None; + let mut head = RequestHead::default(); + head.method = method; + + match Uri::try_from(uri) { + Ok(uri) => head.uri = uri, + Err(e) => err = Some(e.into()), + } + + ClientRequest { + head, + err, + connector, + #[cfg(feature = "cookies")] + cookies: None, + default_headers: true, + } + } + + /// Set HTTP method of this request. + #[inline] + pub fn method(mut self, method: Method) -> Self { + self.head.method = method; + self + } + + #[doc(hidden)] + /// Set HTTP version of this request. + /// + /// By default requests's HTTP version depends on network stream + #[inline] + pub fn version(mut self, version: Version) -> Self { + self.head.version = version; + self + } + + /// Set a header. + /// + /// ```rust + /// fn main() { + /// # actix_rt::System::new("test").block_on(futures::future::lazy(|| { + /// let req = awc::Client::new() + /// .get("http://www.rust-lang.org") + /// .set(awc::http::header::Date::now()) + /// .set(awc::http::header::ContentType(mime::TEXT_HTML)); + /// # Ok::<_, ()>(()) + /// # })); + /// } + /// ``` + pub fn set(mut self, hdr: H) -> Self { + match hdr.try_into() { + Ok(value) => { + self.head.headers.insert(H::name(), value); + } + Err(e) => self.err = Some(e.into()), + } + self + } + + /// Append a header. + /// + /// Header gets appended to existing header. + /// To override header use `set_header()` method. + /// + /// ```rust + /// # extern crate actix_http; + /// # + /// use actix_http::{client, http}; + /// + /// fn main() { + /// let req = client::ClientRequest::build() + /// .header("X-TEST", "value") + /// .header(http::header::CONTENT_TYPE, "application/json") + /// .finish() + /// .unwrap(); + /// } + /// ``` + pub fn header(mut self, key: K, value: V) -> Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + match HeaderName::try_from(key) { + Ok(key) => match value.try_into() { + Ok(value) => { + self.head.headers.append(key, value); + } + Err(e) => self.err = Some(e.into()), + }, + Err(e) => self.err = Some(e.into()), + } + self + } + + /// Insert a header, replaces existing header. + pub fn set_header(mut self, key: K, value: V) -> Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + match HeaderName::try_from(key) { + Ok(key) => match value.try_into() { + Ok(value) => { + self.head.headers.insert(key, value); + } + Err(e) => self.err = Some(e.into()), + }, + Err(e) => self.err = Some(e.into()), + } + self + } + + /// Insert a header only if it is not yet set. + pub fn set_header_if_none(mut self, key: K, value: V) -> Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + match HeaderName::try_from(key) { + Ok(key) => { + if !self.head.headers.contains_key(&key) { + match value.try_into() { + Ok(value) => { + self.head.headers.insert(key, value); + } + Err(e) => self.err = Some(e.into()), + } + } + } + Err(e) => self.err = Some(e.into()), + } + self + } + + /// Close connection + #[inline] + pub fn close_connection(mut self) -> Self { + self.head.set_connection_type(ConnectionType::Close); + self + } + + /// Set request's content type + #[inline] + pub fn content_type(mut self, value: V) -> Self + where + HeaderValue: HttpTryFrom, + { + match HeaderValue::try_from(value) { + Ok(value) => { + let _ = self.head.headers.insert(header::CONTENT_TYPE, value); + } + Err(e) => self.err = Some(e.into()), + } + self + } + + /// Set content length + #[inline] + pub fn content_length(self, len: u64) -> Self { + let mut wrt = BytesMut::new().writer(); + let _ = write!(wrt, "{}", len); + self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze()) + } + + #[cfg(feature = "cookies")] + /// Set a cookie + /// + /// ```rust + /// # use actix_rt::System; + /// # use futures::future::{lazy, Future}; + /// fn main() { + /// System::new("test").block_on(lazy(|| { + /// awc::Client::new().get("https://www.rust-lang.org") + /// .cookie( + /// awc::http::Cookie::build("name", "value") + /// .domain("www.rust-lang.org") + /// .path("/") + /// .secure(true) + /// .http_only(true) + /// .finish(), + /// ) + /// .send() + /// .map_err(|_| ()) + /// .and_then(|response| { + /// println!("Response: {:?}", response); + /// Ok(()) + /// }) + /// })); + /// } + /// ``` + pub fn cookie<'c>(mut self, cookie: Cookie<'c>) -> Self { + if self.cookies.is_none() { + let mut jar = CookieJar::new(); + jar.add(cookie.into_owned()); + self.cookies = Some(jar) + } else { + self.cookies.as_mut().unwrap().add(cookie.into_owned()); + } + self + } + + /// Do not add default request headers. + /// By default `Accept-Encoding` and `User-Agent` headers are set. + pub fn no_default_headers(mut self) -> Self { + self.default_headers = false; + self + } + + /// This method calls provided closure with builder reference if + /// value is `true`. + pub fn if_true(mut self, value: bool, f: F) -> Self + where + F: FnOnce(&mut ClientRequest), + { + if value { + f(&mut self); + } + self + } + + /// This method calls provided closure with builder reference if + /// value is `Some`. + pub fn if_some(mut self, value: Option, f: F) -> Self + where + F: FnOnce(T, &mut ClientRequest), + { + if let Some(val) = value { + f(val, &mut self); + } + self + } + + /// Complete request construction and send body. + pub fn send_body( + mut self, + body: B, + ) -> impl Future + where + B: Into, + { + if let Some(e) = self.err.take() { + return Either::A(err(e.into())); + } + + let mut slf = if self.default_headers { + // enable br only for https + let https = self + .head + .uri + .scheme_part() + .map(|s| s == &uri::Scheme::HTTPS) + .unwrap_or(true); + + let mut slf = if https { + self.set_header_if_none(header::ACCEPT_ENCODING, "br, gzip, deflate") + } else { + self.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate") + }; + + // set request host header + if let Some(host) = slf.head.uri.host() { + if !slf.head.headers.contains_key(header::HOST) { + let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); + + let _ = match slf.head.uri.port_u16() { + None | Some(80) | Some(443) => write!(wrt, "{}", host), + Some(port) => write!(wrt, "{}:{}", host, port), + }; + + match wrt.get_mut().take().freeze().try_into() { + Ok(value) => { + slf.head.headers.insert(header::HOST, value); + } + Err(e) => slf.err = Some(e.into()), + } + } + } + + // user agent + slf.set_header_if_none( + header::USER_AGENT, + concat!("actix-http/", env!("CARGO_PKG_VERSION")), + ) + } else { + self + }; + + #[allow(unused_mut)] + let mut head = slf.head; + + #[cfg(feature = "cookies")] + { + use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; + use std::fmt::Write; + + // set cookies + if let Some(ref mut jar) = slf.cookies { + let mut cookie = String::new(); + for c in jar.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = + percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); + } + head.headers.insert( + header::COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); + } + } + + let uri = head.uri.clone(); + + // validate uri + if uri.host().is_none() { + Either::A(err(InvalidUrl::MissingHost.into())) + } else if uri.scheme_part().is_none() { + Either::A(err(InvalidUrl::MissingScheme.into())) + } else if let Some(scheme) = uri.scheme_part() { + match scheme.as_str() { + "http" | "ws" | "https" | "wss" => { + Either::B(slf.connector.borrow_mut().send_request(head, body.into())) + } + _ => Either::A(err(InvalidUrl::UnknownScheme.into())), + } + } else { + Either::A(err(InvalidUrl::UnknownScheme.into())) + } + } + + /// Set a JSON body and generate `ClientRequest` + pub fn send_json( + self, + value: T, + ) -> impl Future { + let body = match serde_json::to_string(&value) { + Ok(body) => body, + Err(e) => return Either::A(err(Error::from(e).into())), + }; + // set content-type + let slf = if !self.head.headers.contains_key(header::CONTENT_TYPE) { + self.header(header::CONTENT_TYPE, "application/json") + } else { + self + }; + + Either::B(slf.send_body(Body::Bytes(Bytes::from(body)))) + } + + /// Set a urlencoded body and generate `ClientRequest` + /// + /// `ClientRequestBuilder` can not be used after this call. + pub fn send_form( + self, + value: T, + ) -> impl Future { + let body = match serde_urlencoded::to_string(&value) { + Ok(body) => body, + Err(e) => return Either::A(err(Error::from(e).into())), + }; + + let slf = if !self.head.headers.contains_key(header::CONTENT_TYPE) { + self.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") + } else { + self + }; + + Either::B(slf.send_body(Body::Bytes(Bytes::from(body)))) + } + + /// Set an streaming body and generate `ClientRequest`. + pub fn send_stream( + self, + stream: S, + ) -> impl Future + where + S: Stream + 'static, + E: Into + 'static, + { + self.send_body(Body::from_message(BodyStream::new(stream))) + } + + /// Set an empty body and generate `ClientRequest`. + pub fn send(self) -> impl Future { + self.send_body(Body::Empty) + } +} + +impl fmt::Debug for ClientRequest { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!( + f, + "\nClientRequest {:?} {}:{}", + self.head.version, self.head.method, self.head.uri + )?; + writeln!(f, " headers:")?; + for (key, val) in self.head.headers.iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 8ae7156c3..1bf29213c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,6 +63,7 @@ //! //! ## Package feature //! +//! * `client` - enables http client //! * `tls` - enables ssl support via `native-tls` crate //! * `ssl` - enables ssl support via `openssl` crate, supports `http/2` //! * `rust-tls` - enables ssl support via `rustls` crate, supports `http/2` @@ -105,6 +106,9 @@ extern crate actix_web_codegen; #[doc(hidden)] pub use actix_web_codegen::*; +#[cfg(feature = "client")] +pub use awc as client; + // re-export for convenience pub use actix_http::Response as HttpResponse; pub use actix_http::{http, Error, HttpMessage, ResponseError, Result}; From 254b61e8002fc45446c24662f47e49749b1157d6 Mon Sep 17 00:00:00 2001 From: Max Frai Date: Tue, 26 Mar 2019 18:07:19 +0200 Subject: [PATCH 343/427] Fix copy/paste mistake in error message (#733) --- actix-web-codegen/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 13d1b97f5..16123930a 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -48,7 +48,7 @@ pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); if args.is_empty() { - panic!("invalid server definition, expected: #[get(\"some path\")]"); + panic!("invalid server definition, expected: #[post(\"some path\")]"); } // path @@ -85,7 +85,7 @@ pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); if args.is_empty() { - panic!("invalid server definition, expected: #[get(\"some path\")]"); + panic!("invalid server definition, expected: #[put(\"some path\")]"); } // path From cc24c77acc992c06a57dd13f3316020f7a90cf6d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Mar 2019 09:11:27 -0700 Subject: [PATCH 344/427] add Client::new() --- awc/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/awc/src/lib.rs b/awc/src/lib.rs index b1a309c4c..885af48e9 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -56,6 +56,11 @@ impl Default for Client { } impl Client { + /// Create new client instance with default settings. + pub fn new() -> Client { + Client::default() + } + /// Build client instance. pub fn build() -> ClientBuilder { ClientBuilder::new() From b254113d9fcdf0a9c156f5d8868051e9088aa8c1 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Mar 2019 11:41:38 -0700 Subject: [PATCH 345/427] move high level client code from actix-http --- awc/Cargo.toml | 1 - awc/src/builder.rs | 19 ++++++++- awc/src/connect.rs | 7 ++- awc/src/lib.rs | 6 +-- awc/src/request.rs | 24 +++++------ awc/src/response.rs | 102 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 awc/src/response.rs diff --git a/awc/Cargo.toml b/awc/Cargo.toml index f316d0620..b43a8d47f 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -29,7 +29,6 @@ cookies = ["cookie", "actix-http/cookies"] [dependencies] actix-service = "0.3.4" actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-codec = "0.1.1" bytes = "0.4" futures = "0.1" log =" 0.4" diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 3104e5244..686948682 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -2,8 +2,11 @@ use std::cell::RefCell; use std::fmt; use std::rc::Rc; -use actix_http::client::Connector; -use actix_http::http::{header::IntoHeaderValue, HeaderMap, HeaderName, HttpTryFrom}; +use actix_http::client::{ConnectError, Connection, Connector}; +use actix_http::http::{ + header::IntoHeaderValue, HeaderMap, HeaderName, HttpTryFrom, Uri, +}; +use actix_service::Service; use crate::connect::{Connect, ConnectorWrapper}; use crate::Client; @@ -33,6 +36,18 @@ impl ClientBuilder { } } + /// Use custom connector service. + pub fn connector(mut self, connector: T) -> Self + where + T: Service + 'static, + T::Response: Connection, + ::Future: 'static, + T::Future: 'static, + { + self.connector = Rc::new(RefCell::new(ConnectorWrapper(connector))); + self + } + /// Do not follow redirects. /// /// Redirects are allowed by default. diff --git a/awc/src/connect.rs b/awc/src/connect.rs index c52bc34cb..a07662791 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -1,9 +1,11 @@ use actix_http::body::Body; -use actix_http::client::{ClientResponse, ConnectError, Connection, SendRequestError}; +use actix_http::client::{ConnectError, Connection, SendRequestError}; use actix_http::{http, RequestHead}; use actix_service::Service; use futures::Future; +use crate::response::ClientResponse; + pub(crate) struct ConnectorWrapper(pub T); pub(crate) trait Connect { @@ -32,7 +34,8 @@ where .call(head.uri.clone()) .from_err() // send request - .and_then(move |connection| connection.send_request(head, body)), + .and_then(move |connection| connection.send_request(head, body)) + .map(|(head, payload)| ClientResponse::new(head, payload)), ) } } diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 885af48e9..89acf7d58 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -1,9 +1,7 @@ use std::cell::RefCell; use std::rc::Rc; -pub use actix_http::client::{ - ClientResponse, ConnectError, InvalidUrl, SendRequestError, -}; +pub use actix_http::client::{ConnectError, InvalidUrl, SendRequestError}; pub use actix_http::http; use actix_http::client::Connector; @@ -12,9 +10,11 @@ use actix_http::http::{HttpTryFrom, Method, Uri}; mod builder; mod connect; mod request; +mod response; pub use self::builder::ClientBuilder; pub use self::request::ClientRequest; +pub use self::response::ClientResponse; use self::connect::{Connect, ConnectorWrapper}; diff --git a/awc/src/request.rs b/awc/src/request.rs index 90dfebcca..f23aa7ef9 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -12,7 +12,7 @@ use serde::Serialize; use serde_json; use actix_http::body::{Body, BodyStream}; -use actix_http::client::{ClientResponse, InvalidUrl, SendRequestError}; +use actix_http::client::{InvalidUrl, SendRequestError}; use actix_http::http::header::{self, Header, IntoHeaderValue}; use actix_http::http::{ uri, ConnectionType, Error as HttpError, HeaderName, HeaderValue, HttpTryFrom, @@ -20,6 +20,7 @@ use actix_http::http::{ }; use actix_http::{Error, Head, RequestHead}; +use crate::response::ClientResponse; use crate::Connect; /// An HTTP Client request builder @@ -30,18 +31,15 @@ use crate::Connect; /// ```rust /// use futures::future::{Future, lazy}; /// use actix_rt::System; -/// use actix_http::client; /// /// fn main() { /// System::new("test").block_on(lazy(|| { -/// let mut connector = client::Connector::new().service(); -/// -/// client::ClientRequest::get("http://www.rust-lang.org") // <- Create request builder +/// awc::Client::new() +/// .get("http://www.rust-lang.org") // <- Create request builder /// .header("User-Agent", "Actix-web") -/// .finish().unwrap() -/// .send(&mut connector) // <- Send http request +/// .send() // <- Send http request /// .map_err(|_| ()) -/// .and_then(|response| { // <- server http response +/// .and_then(|response| { // <- server http response /// println!("Response: {:?}", response); /// Ok(()) /// }) @@ -137,11 +135,13 @@ impl ClientRequest { /// use actix_http::{client, http}; /// /// fn main() { - /// let req = client::ClientRequest::build() + /// # actix_rt::System::new("test").block_on(futures::future::lazy(|| { + /// let req = awc::Client::new() + /// .get("http://www.rust-lang.org") /// .header("X-TEST", "value") - /// .header(http::header::CONTENT_TYPE, "application/json") - /// .finish() - /// .unwrap(); + /// .header(http::header::CONTENT_TYPE, "application/json"); + /// # Ok::<_, ()>(()) + /// # })); /// } /// ``` pub fn header(mut self, key: K, value: V) -> Self diff --git a/awc/src/response.rs b/awc/src/response.rs new file mode 100644 index 000000000..0ae66df0f --- /dev/null +++ b/awc/src/response.rs @@ -0,0 +1,102 @@ +use std::cell::{Ref, RefMut}; +use std::fmt; + +use bytes::Bytes; +use futures::{Poll, Stream}; + +use actix_http::error::PayloadError; +use actix_http::http::{HeaderMap, StatusCode, Version}; +use actix_http::{Extensions, Head, HttpMessage, Payload, PayloadStream, ResponseHead}; + +/// Client Response +pub struct ClientResponse { + pub(crate) head: ResponseHead, + pub(crate) payload: Payload, +} + +impl HttpMessage for ClientResponse { + type Stream = PayloadStream; + + fn headers(&self) -> &HeaderMap { + &self.head.headers + } + + fn extensions(&self) -> Ref { + self.head.extensions() + } + + fn extensions_mut(&self) -> RefMut { + self.head.extensions_mut() + } + + fn take_payload(&mut self) -> Payload { + std::mem::replace(&mut self.payload, Payload::None) + } +} + +impl ClientResponse { + /// Create new Request instance + pub(crate) fn new(head: ResponseHead, payload: Payload) -> ClientResponse { + ClientResponse { head, payload } + } + + #[inline] + pub(crate) fn head(&self) -> &ResponseHead { + &self.head + } + + #[inline] + pub(crate) fn head_mut(&mut self) -> &mut ResponseHead { + &mut self.head + } + + /// Read the Request Version. + #[inline] + pub fn version(&self) -> Version { + self.head().version + } + + /// Get the status from the server. + #[inline] + pub fn status(&self) -> StatusCode { + self.head().status + } + + #[inline] + /// Returns Request's headers. + pub fn headers(&self) -> &HeaderMap { + &self.head().headers + } + + #[inline] + /// Returns mutable Request's headers. + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.head_mut().headers + } + + /// Checks if a connection should be kept alive. + #[inline] + pub fn keep_alive(&self) -> bool { + self.head().keep_alive() + } +} + +impl Stream for ClientResponse { + type Item = Bytes; + type Error = PayloadError; + + fn poll(&mut self) -> Poll, Self::Error> { + self.payload.poll() + } +} + +impl fmt::Debug for ClientResponse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status(),)?; + writeln!(f, " headers:")?; + for (key, val) in self.headers().iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } +} From 2c7da28ef9fab2265c8149daedfdfc60b7ba3641 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Mar 2019 11:43:22 -0700 Subject: [PATCH 346/427] move high level client code to awc crate --- src/client/connection.rs | 10 +- src/client/h1proto.rs | 38 +-- src/client/h2proto.rs | 9 +- src/client/mod.rs | 4 - src/client/request.rs | 699 --------------------------------------- src/client/response.rs | 116 ------- src/h1/client.rs | 9 +- src/h1/decoder.rs | 15 +- src/h1/dispatcher.rs | 2 +- src/lib.rs | 62 +--- src/response.rs | 2 - src/ws/client/connect.rs | 37 ++- src/ws/client/service.rs | 50 +-- test-server/Cargo.toml | 3 +- test-server/src/lib.rs | 121 ++++--- tests/test_client.rs | 31 +- tests/test_server.rs | 129 +++----- 17 files changed, 211 insertions(+), 1126 deletions(-) delete mode 100644 src/client/request.rs delete mode 100644 src/client/response.rs diff --git a/src/client/connection.rs b/src/client/connection.rs index 8de23bd24..e8c1201aa 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -6,11 +6,11 @@ use futures::Future; use h2::client::SendRequest; use crate::body::MessageBody; -use crate::message::RequestHead; +use crate::message::{RequestHead, ResponseHead}; +use crate::payload::Payload; use super::error::SendRequestError; use super::pool::Acquired; -use super::response::ClientResponse; use super::{h1proto, h2proto}; pub(crate) enum ConnectionType { @@ -19,7 +19,7 @@ pub(crate) enum ConnectionType { } pub trait Connection { - type Future: Future; + type Future: Future; /// Send request and body fn send_request( @@ -80,7 +80,7 @@ impl Connection for IoConnection where T: AsyncRead + AsyncWrite + 'static, { - type Future = Box>; + type Future = Box>; fn send_request( mut self, @@ -117,7 +117,7 @@ where A: AsyncRead + AsyncWrite + 'static, B: AsyncRead + AsyncWrite + 'static, { - type Future = Box>; + type Future = Box>; fn send_request( self, diff --git a/src/client/h1proto.rs b/src/client/h1proto.rs index 34521cc2f..2e29484ff 100644 --- a/src/client/h1proto.rs +++ b/src/client/h1proto.rs @@ -2,17 +2,18 @@ use std::{io, time}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use bytes::Bytes; -use futures::future::{err, ok, Either}; +use futures::future::{ok, Either}; use futures::{Async, Future, Poll, Sink, Stream}; +use crate::error::PayloadError; +use crate::h1; +use crate::message::{RequestHead, ResponseHead}; +use crate::payload::{Payload, PayloadStream}; + use super::connection::{ConnectionLifetime, ConnectionType, IoConnection}; use super::error::{ConnectError, SendRequestError}; use super::pool::Acquired; -use super::response::ClientResponse; use crate::body::{BodyLength, MessageBody}; -use crate::error::PayloadError; -use crate::h1; -use crate::message::RequestHead; pub(crate) fn send_request( io: T, @@ -20,7 +21,7 @@ pub(crate) fn send_request( body: B, created: time::Instant, pool: Option>, -) -> impl Future +) -> impl Future where T: AsyncRead + AsyncWrite + 'static, B: MessageBody, @@ -50,19 +51,20 @@ where .into_future() .map_err(|(e, _)| SendRequestError::from(e)) .and_then(|(item, framed)| { - if let Some(mut res) = item { + if let Some(res) = item { match framed.get_codec().message_type() { h1::MessageType::None => { let force_close = !framed.get_codec().keepalive(); - release_connection(framed, force_close) + release_connection(framed, force_close); + Ok((res, Payload::None)) } _ => { - res.set_payload(Payload::stream(framed).into()); + let pl: PayloadStream = Box::new(PlStream::new(framed)); + Ok((res, pl.into())) } } - ok(res) } else { - err(ConnectError::Disconnected.into()) + Err(ConnectError::Disconnected.into()) } }) }) @@ -199,21 +201,19 @@ where } } -pub(crate) struct Payload { +pub(crate) struct PlStream { framed: Option>, } -impl Payload { - pub fn stream( - framed: Framed, - ) -> Box> { - Box::new(Payload { +impl PlStream { + fn new(framed: Framed) -> Self { + PlStream { framed: Some(framed.map_codec(|codec| codec.into_payload_codec())), - }) + } } } -impl Stream for Payload { +impl Stream for PlStream { type Item = Bytes; type Error = PayloadError; diff --git a/src/client/h2proto.rs b/src/client/h2proto.rs index bf2d3e1b2..9ad722627 100644 --- a/src/client/h2proto.rs +++ b/src/client/h2proto.rs @@ -9,13 +9,12 @@ use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING}; use http::{request::Request, HttpTryFrom, Method, Version}; use crate::body::{BodyLength, MessageBody}; -use crate::message::{Message, RequestHead, ResponseHead}; +use crate::message::{RequestHead, ResponseHead}; use crate::payload::Payload; use super::connection::{ConnectionType, IoConnection}; use super::error::SendRequestError; use super::pool::Acquired; -use super::response::ClientResponse; pub(crate) fn send_request( io: SendRequest, @@ -23,7 +22,7 @@ pub(crate) fn send_request( body: B, created: time::Instant, pool: Option>, -) -> impl Future +) -> impl Future where T: AsyncRead + AsyncWrite + 'static, B: MessageBody, @@ -105,12 +104,12 @@ where let (parts, body) = resp.into_parts(); let payload = if head_req { Payload::None } else { body.into() }; - let mut head: Message = Message::new(); + let mut head = ResponseHead::default(); head.version = parts.version; head.status = parts.status; head.headers = parts.headers; - Ok(ClientResponse { head, payload }) + Ok((head, payload)) }) .from_err() } diff --git a/src/client/mod.rs b/src/client/mod.rs index 86b1a0cc0..87c374742 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -5,11 +5,7 @@ mod error; mod h1proto; mod h2proto; mod pool; -mod request; -mod response; pub use self::connection::Connection; pub use self::connector::Connector; pub use self::error::{ConnectError, InvalidUrl, SendRequestError}; -pub use self::request::{ClientRequest, ClientRequestBuilder}; -pub use self::response::ClientResponse; diff --git a/src/client/request.rs b/src/client/request.rs deleted file mode 100644 index 134a42640..000000000 --- a/src/client/request.rs +++ /dev/null @@ -1,699 +0,0 @@ -use std::fmt; -use std::io::Write; - -use actix_service::Service; -use bytes::{BufMut, Bytes, BytesMut}; -#[cfg(feature = "cookies")] -use cookie::{Cookie, CookieJar}; -use futures::future::{err, Either}; -use futures::{Future, Stream}; -use serde::Serialize; -use serde_json; - -use crate::body::{BodyStream, MessageBody}; -use crate::error::Error; -use crate::header::{self, Header, IntoHeaderValue}; -use crate::http::{ - uri, Error as HttpError, HeaderMap, HeaderName, HeaderValue, HttpTryFrom, Method, - Uri, Version, -}; -use crate::message::{ConnectionType, Head, RequestHead}; - -use super::connection::Connection; -use super::error::{ConnectError, InvalidUrl, SendRequestError}; -use super::response::ClientResponse; - -/// An HTTP Client Request -/// -/// ```rust -/// use futures::future::{Future, lazy}; -/// use actix_rt::System; -/// use actix_http::client; -/// -/// fn main() { -/// System::new("test").block_on(lazy(|| { -/// let mut connector = client::Connector::new().service(); -/// -/// client::ClientRequest::get("http://www.rust-lang.org") // <- Create request builder -/// .header("User-Agent", "Actix-web") -/// .finish().unwrap() -/// .send(&mut connector) // <- Send http request -/// .map_err(|_| ()) -/// .and_then(|response| { // <- server http response -/// println!("Response: {:?}", response); -/// Ok(()) -/// }) -/// })); -/// } -/// ``` -pub struct ClientRequest { - head: RequestHead, - body: B, -} - -impl ClientRequest<()> { - /// Create client request builder - pub fn build() -> ClientRequestBuilder { - ClientRequestBuilder { - head: Some(RequestHead::default()), - err: None, - #[cfg(feature = "cookies")] - cookies: None, - default_headers: true, - } - } - - /// Create request builder for `GET` request - pub fn get(uri: U) -> ClientRequestBuilder - where - Uri: HttpTryFrom, - { - let mut builder = ClientRequest::build(); - builder.method(Method::GET).uri(uri); - builder - } - - /// Create request builder for `HEAD` request - pub fn head(uri: U) -> ClientRequestBuilder - where - Uri: HttpTryFrom, - { - let mut builder = ClientRequest::build(); - builder.method(Method::HEAD).uri(uri); - builder - } - - /// Create request builder for `POST` request - pub fn post(uri: U) -> ClientRequestBuilder - where - Uri: HttpTryFrom, - { - let mut builder = ClientRequest::build(); - builder.method(Method::POST).uri(uri); - builder - } - - /// Create request builder for `PUT` request - pub fn put(uri: U) -> ClientRequestBuilder - where - Uri: HttpTryFrom, - { - let mut builder = ClientRequest::build(); - builder.method(Method::PUT).uri(uri); - builder - } - - /// Create request builder for `DELETE` request - pub fn delete(uri: U) -> ClientRequestBuilder - where - Uri: HttpTryFrom, - { - let mut builder = ClientRequest::build(); - builder.method(Method::DELETE).uri(uri); - builder - } -} - -impl ClientRequest -where - B: MessageBody, -{ - /// Create new client request - pub fn new(head: RequestHead, body: B) -> Self { - ClientRequest { head, body } - } - - /// Get the request URI - #[inline] - pub fn uri(&self) -> &Uri { - &self.head.uri - } - - /// Set client request URI - #[inline] - pub fn set_uri(&mut self, uri: Uri) { - self.head.uri = uri - } - - /// Get the request method - #[inline] - pub fn method(&self) -> &Method { - &self.head.method - } - - /// Set HTTP `Method` for the request - #[inline] - pub fn set_method(&mut self, method: Method) { - self.head.method = method - } - - /// Get HTTP version for the request - #[inline] - pub fn version(&self) -> Version { - self.head.version - } - - /// Set http `Version` for the request - #[inline] - pub fn set_version(&mut self, version: Version) { - self.head.version = version - } - - /// Get the headers from the request - #[inline] - pub fn headers(&self) -> &HeaderMap { - &self.head.headers - } - - /// Get a mutable reference to the headers - #[inline] - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.head.headers - } - - /// Deconstruct ClientRequest to a RequestHead and body tuple - pub fn into_parts(self) -> (RequestHead, B) { - (self.head, self.body) - } - - // Send request - /// - /// This method returns a future that resolves to a ClientResponse - pub fn send( - self, - connector: &mut T, - ) -> impl Future - where - B: 'static, - T: Service, - T::Response: Connection, - { - let Self { head, body } = self; - - let uri = head.uri.clone(); - - // validate uri - if uri.host().is_none() { - Either::A(err(InvalidUrl::MissingHost.into())) - } else if uri.scheme_part().is_none() { - Either::A(err(InvalidUrl::MissingScheme.into())) - } else if let Some(scheme) = uri.scheme_part() { - match scheme.as_str() { - "http" | "ws" | "https" | "wss" => Either::B( - connector - // connect to the host - .call(uri) - .from_err() - // send request - .and_then(move |connection| connection.send_request(head, body)), - ), - _ => Either::A(err(InvalidUrl::UnknownScheme.into())), - } - } else { - Either::A(err(InvalidUrl::UnknownScheme.into())) - } - } -} - -impl fmt::Debug for ClientRequest -where - B: MessageBody, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!( - f, - "\nClientRequest {:?} {}:{}", - self.head.version, self.head.method, self.head.uri - )?; - writeln!(f, " headers:")?; - for (key, val) in self.head.headers.iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -/// An HTTP Client request builder -/// -/// This type can be used to construct an instance of `ClientRequest` through a -/// builder-like pattern. -pub struct ClientRequestBuilder { - head: Option, - err: Option, - #[cfg(feature = "cookies")] - cookies: Option, - default_headers: bool, -} - -impl ClientRequestBuilder { - /// Set HTTP URI of request. - #[inline] - pub fn uri(&mut self, uri: U) -> &mut Self - where - Uri: HttpTryFrom, - { - match Uri::try_from(uri) { - Ok(uri) => { - if let Some(parts) = parts(&mut self.head, &self.err) { - parts.uri = uri; - } - } - Err(e) => self.err = Some(e.into()), - } - self - } - - /// Set HTTP method of this request. - #[inline] - pub fn method(&mut self, method: Method) -> &mut Self { - if let Some(parts) = parts(&mut self.head, &self.err) { - parts.method = method; - } - self - } - - /// Set HTTP method of this request. - #[inline] - pub fn get_method(&mut self) -> &Method { - let parts = self.head.as_ref().expect("cannot reuse request builder"); - &parts.method - } - - /// Set HTTP version of this request. - /// - /// By default requests's HTTP version depends on network stream - #[inline] - pub fn version(&mut self, version: Version) -> &mut Self { - if let Some(parts) = parts(&mut self.head, &self.err) { - parts.version = version; - } - self - } - - /// Set a header. - /// - /// ```rust - /// # extern crate mime; - /// # extern crate actix_http; - /// # - /// use actix_http::{client, http}; - /// - /// fn main() { - /// let req = client::ClientRequest::build() - /// .set(http::header::Date::now()) - /// .set(http::header::ContentType(mime::TEXT_HTML)) - /// .finish() - /// .unwrap(); - /// } - /// ``` - pub fn set(&mut self, hdr: H) -> &mut Self { - if let Some(parts) = parts(&mut self.head, &self.err) { - match hdr.try_into() { - Ok(value) => { - parts.headers.insert(H::name(), value); - } - Err(e) => self.err = Some(e.into()), - } - } - self - } - - /// Append a header. - /// - /// Header gets appended to existing header. - /// To override header use `set_header()` method. - /// - /// ```rust - /// # extern crate actix_http; - /// # - /// use actix_http::{client, http}; - /// - /// fn main() { - /// let req = client::ClientRequest::build() - /// .header("X-TEST", "value") - /// .header(http::header::CONTENT_TYPE, "application/json") - /// .finish() - /// .unwrap(); - /// } - /// ``` - pub fn header(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.head, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - parts.headers.append(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set a header. - pub fn set_header(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.head, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - parts.headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set a header only if it is not yet set. - pub fn set_header_if_none(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.head, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => { - if !parts.headers.contains_key(&key) { - match value.try_into() { - Ok(value) => { - parts.headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - } - } - } - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Enable connection upgrade - #[inline] - pub fn upgrade(&mut self, value: V) -> &mut Self - where - V: IntoHeaderValue, - { - { - if let Some(parts) = parts(&mut self.head, &self.err) { - parts.set_connection_type(ConnectionType::Upgrade); - } - } - self.set_header(header::UPGRADE, value) - } - - /// Close connection - #[inline] - pub fn close(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.head, &self.err) { - parts.set_connection_type(ConnectionType::Close); - } - self - } - - /// Set request's content type - #[inline] - pub fn content_type(&mut self, value: V) -> &mut Self - where - HeaderValue: HttpTryFrom, - { - if let Some(parts) = parts(&mut self.head, &self.err) { - match HeaderValue::try_from(value) { - Ok(value) => { - parts.headers.insert(header::CONTENT_TYPE, value); - } - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set content length - #[inline] - pub fn content_length(&mut self, len: u64) -> &mut Self { - let mut wrt = BytesMut::new().writer(); - let _ = write!(wrt, "{}", len); - self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze()) - } - - #[cfg(feature = "cookies")] - /// Set a cookie - /// - /// ```rust - /// # extern crate actix_http; - /// use actix_http::{client, http}; - /// - /// fn main() { - /// let req = client::ClientRequest::build() - /// .cookie( - /// http::Cookie::build("name", "value") - /// .domain("www.rust-lang.org") - /// .path("/") - /// .secure(true) - /// .http_only(true) - /// .finish(), - /// ) - /// .finish() - /// .unwrap(); - /// } - /// ``` - pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { - if self.cookies.is_none() { - let mut jar = CookieJar::new(); - jar.add(cookie.into_owned()); - self.cookies = Some(jar) - } else { - self.cookies.as_mut().unwrap().add(cookie.into_owned()); - } - self - } - - /// Do not add default request headers. - /// By default `Accept-Encoding` and `User-Agent` headers are set. - pub fn no_default_headers(&mut self) -> &mut Self { - self.default_headers = false; - self - } - - /// This method calls provided closure with builder reference if - /// value is `true`. - pub fn if_true(&mut self, value: bool, f: F) -> &mut Self - where - F: FnOnce(&mut ClientRequestBuilder), - { - if value { - f(self); - } - self - } - - /// This method calls provided closure with builder reference if - /// value is `Some`. - pub fn if_some(&mut self, value: Option, f: F) -> &mut Self - where - F: FnOnce(T, &mut ClientRequestBuilder), - { - if let Some(val) = value { - f(val, self); - } - self - } - - /// Set a body and generate `ClientRequest`. - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn body( - &mut self, - body: B, - ) -> Result, HttpError> { - if let Some(e) = self.err.take() { - return Err(e); - } - - if self.default_headers { - // enable br only for https - let https = if let Some(parts) = parts(&mut self.head, &self.err) { - parts - .uri - .scheme_part() - .map(|s| s == &uri::Scheme::HTTPS) - .unwrap_or(true) - } else { - true - }; - - if https { - self.set_header_if_none(header::ACCEPT_ENCODING, "br, gzip, deflate"); - } else { - self.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate"); - } - - // set request host header - if let Some(parts) = parts(&mut self.head, &self.err) { - if let Some(host) = parts.uri.host() { - if !parts.headers.contains_key(header::HOST) { - let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); - - let _ = match parts.uri.port_u16() { - None | Some(80) | Some(443) => write!(wrt, "{}", host), - Some(port) => write!(wrt, "{}:{}", host, port), - }; - - match wrt.get_mut().take().freeze().try_into() { - Ok(value) => { - parts.headers.insert(header::HOST, value); - } - Err(e) => self.err = Some(e.into()), - } - } - } - } - - // user agent - self.set_header_if_none( - header::USER_AGENT, - concat!("actix-http/", env!("CARGO_PKG_VERSION")), - ); - } - - #[allow(unused_mut)] - let mut head = self.head.take().expect("cannot reuse request builder"); - - #[cfg(feature = "cookies")] - { - use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; - use std::fmt::Write; - - // set cookies - if let Some(ref mut jar) = self.cookies { - let mut cookie = String::new(); - for c in jar.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); - let value = - percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); - } - } - Ok(ClientRequest { head, body }) - } - - /// Set a JSON body and generate `ClientRequest` - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn json( - &mut self, - value: T, - ) -> Result, Error> { - let body = serde_json::to_string(&value)?; - - let contains = if let Some(head) = parts(&mut self.head, &self.err) { - head.headers.contains_key(header::CONTENT_TYPE) - } else { - true - }; - if !contains { - self.header(header::CONTENT_TYPE, "application/json"); - } - - Ok(self.body(body)?) - } - - /// Set a urlencoded body and generate `ClientRequest` - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn form( - &mut self, - value: T, - ) -> Result, Error> { - let body = serde_urlencoded::to_string(&value)?; - - let contains = if let Some(head) = parts(&mut self.head, &self.err) { - head.headers.contains_key(header::CONTENT_TYPE) - } else { - true - }; - if !contains { - self.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded"); - } - - Ok(self.body(body)?) - } - - /// Set an streaming body and generate `ClientRequest`. - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn stream( - &mut self, - stream: S, - ) -> Result, HttpError> - where - S: Stream, - E: Into + 'static, - { - self.body(BodyStream::new(stream)) - } - - /// Set an empty body and generate `ClientRequest`. - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn finish(&mut self) -> Result, HttpError> { - self.body(()) - } - - /// This method construct new `ClientRequestBuilder` - pub fn take(&mut self) -> ClientRequestBuilder { - ClientRequestBuilder { - head: self.head.take(), - err: self.err.take(), - #[cfg(feature = "cookies")] - cookies: self.cookies.take(), - default_headers: self.default_headers, - } - } -} - -#[inline] -fn parts<'a>( - parts: &'a mut Option, - err: &Option, -) -> Option<&'a mut RequestHead> { - if err.is_some() { - return None; - } - parts.as_mut() -} - -impl fmt::Debug for ClientRequestBuilder { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(ref parts) = self.head { - writeln!( - f, - "\nClientRequestBuilder {:?} {}:{}", - parts.version, parts.method, parts.uri - )?; - writeln!(f, " headers:")?; - for (key, val) in parts.headers.iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } else { - write!(f, "ClientRequestBuilder(Consumed)") - } - } -} diff --git a/src/client/response.rs b/src/client/response.rs deleted file mode 100644 index 7c6cdf643..000000000 --- a/src/client/response.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::cell::{Ref, RefMut}; -use std::fmt; - -use bytes::Bytes; -use futures::{Poll, Stream}; -use http::{HeaderMap, StatusCode, Version}; - -use crate::error::PayloadError; -use crate::extensions::Extensions; -use crate::httpmessage::HttpMessage; -use crate::message::{Head, Message, ResponseHead}; -use crate::payload::{Payload, PayloadStream}; - -/// Client Response -pub struct ClientResponse { - pub(crate) head: Message, - pub(crate) payload: Payload, -} - -impl HttpMessage for ClientResponse { - type Stream = PayloadStream; - - fn headers(&self) -> &HeaderMap { - &self.head.headers - } - - fn extensions(&self) -> Ref { - self.head.extensions() - } - - fn extensions_mut(&self) -> RefMut { - self.head.extensions_mut() - } - - fn take_payload(&mut self) -> Payload { - std::mem::replace(&mut self.payload, Payload::None) - } -} - -impl ClientResponse { - /// Create new Request instance - pub fn new() -> ClientResponse { - let head: Message = Message::new(); - head.extensions_mut().clear(); - - ClientResponse { - head, - payload: Payload::None, - } - } - - #[inline] - pub(crate) fn head(&self) -> &ResponseHead { - &self.head - } - - #[inline] - pub(crate) fn head_mut(&mut self) -> &mut ResponseHead { - &mut self.head - } - - /// Read the Request Version. - #[inline] - pub fn version(&self) -> Version { - self.head().version - } - - /// Get the status from the server. - #[inline] - pub fn status(&self) -> StatusCode { - self.head().status - } - - #[inline] - /// Returns Request's headers. - pub fn headers(&self) -> &HeaderMap { - &self.head().headers - } - - #[inline] - /// Returns mutable Request's headers. - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.head_mut().headers - } - - /// Checks if a connection should be kept alive. - #[inline] - pub fn keep_alive(&self) -> bool { - self.head().keep_alive() - } - - /// Set response payload - pub fn set_payload(&mut self, payload: Payload) { - self.payload = payload; - } -} - -impl Stream for ClientResponse { - type Item = Bytes; - type Error = PayloadError; - - fn poll(&mut self) -> Poll, Self::Error> { - self.payload.poll() - } -} - -impl fmt::Debug for ClientResponse { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status(),)?; - writeln!(f, " headers:")?; - for (key, val) in self.headers().iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} diff --git a/src/h1/client.rs b/src/h1/client.rs index 851851266..b3a5a5d9f 100644 --- a/src/h1/client.rs +++ b/src/h1/client.rs @@ -13,11 +13,10 @@ use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; use super::{decoder, encoder}; use super::{Message, MessageType}; use crate::body::BodyLength; -use crate::client::ClientResponse; use crate::config::ServiceConfig; use crate::error::{ParseError, PayloadError}; use crate::helpers; -use crate::message::{ConnectionType, Head, MessagePool, RequestHead}; +use crate::message::{ConnectionType, Head, MessagePool, RequestHead, ResponseHead}; bitflags! { struct Flags: u8 { @@ -41,7 +40,7 @@ pub struct ClientPayloadCodec { struct ClientCodecInner { config: ServiceConfig, - decoder: decoder::MessageDecoder, + decoder: decoder::MessageDecoder, payload: Option, version: Version, ctype: ConnectionType, @@ -123,14 +122,14 @@ impl ClientPayloadCodec { } impl Decoder for ClientCodec { - type Item = ClientResponse; + type Item = ResponseHead; type Error = ParseError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set"); if let Some((req, payload)) = self.inner.decoder.decode(src)? { - if let Some(ctype) = req.head().ctype { + if let Some(ctype) = req.ctype { // do not use peer's keep-alive self.inner.ctype = if ctype == ConnectionType::KeepAlive { self.inner.ctype diff --git a/src/h1/decoder.rs b/src/h1/decoder.rs index 77b76c242..a1b221c06 100644 --- a/src/h1/decoder.rs +++ b/src/h1/decoder.rs @@ -9,9 +9,8 @@ use http::{header, HeaderMap, HttpTryFrom, Method, StatusCode, Uri, Version}; use httparse; use log::{debug, error, trace}; -use crate::client::ClientResponse; use crate::error::ParseError; -use crate::message::ConnectionType; +use crate::message::{ConnectionType, ResponseHead}; use crate::request::Request; const MAX_BUFFER_SIZE: usize = 131_072; @@ -227,13 +226,13 @@ impl MessageType for Request { } } -impl MessageType for ClientResponse { +impl MessageType for ResponseHead { fn set_connection_type(&mut self, ctype: Option) { - self.head.ctype = ctype; + self.ctype = ctype; } fn headers_mut(&mut self) -> &mut HeaderMap { - self.headers_mut() + &mut self.headers } fn decode(src: &mut BytesMut) -> Result, ParseError> { @@ -263,7 +262,7 @@ impl MessageType for ClientResponse { } }; - let mut msg = ClientResponse::new(); + let mut msg = ResponseHead::default(); // convert headers let len = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; @@ -281,8 +280,8 @@ impl MessageType for ClientResponse { PayloadType::None }; - msg.head.status = status; - msg.head.version = ver; + msg.status = status; + msg.version = ver; Ok(Some((msg, decoder))) } diff --git a/src/h1/dispatcher.rs b/src/h1/dispatcher.rs index afeabc825..09a17fcf7 100644 --- a/src/h1/dispatcher.rs +++ b/src/h1/dispatcher.rs @@ -553,7 +553,7 @@ mod tests { use actix_codec::{AsyncRead, AsyncWrite}; use actix_service::IntoService; - use bytes::{Buf, Bytes, BytesMut}; + use bytes::{Buf, Bytes}; use futures::future::{lazy, ok}; use super::*; diff --git a/src/lib.rs b/src/lib.rs index 85efe5e44..b41ce7ae8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,64 +1,4 @@ -//! Actix web is a small, pragmatic, and extremely fast web framework -//! for Rust. -//! -//! ```rust,ignore -//! use actix_web::{server, App, Path, Responder}; -//! # use std::thread; -//! -//! fn index(info: Path<(String, u32)>) -> impl Responder { -//! format!("Hello {}! id:{}", info.0, info.1) -//! } -//! -//! fn main() { -//! # thread::spawn(|| { -//! server::new(|| { -//! App::new().resource("/{name}/{id}/index.html", |r| r.with(index)) -//! }).bind("127.0.0.1:8080") -//! .unwrap() -//! .run(); -//! # }); -//! } -//! ``` -//! -//! ## Documentation & community resources -//! -//! Besides the API documentation (which you are currently looking -//! at!), several other resources are available: -//! -//! * [User Guide](https://actix.rs/docs/) -//! * [Chat on gitter](https://gitter.im/actix/actix) -//! * [GitHub repository](https://github.com/actix/actix-web) -//! * [Cargo package](https://crates.io/crates/actix-web) -//! -//! To get started navigating the API documentation you may want to -//! consider looking at the following pages: -//! -//! * [App](struct.App.html): This struct represents an actix-web -//! application and is used to configure routes and other common -//! settings. -//! -//! * [HttpServer](server/struct.HttpServer.html): This struct -//! represents an HTTP server instance and is used to instantiate and -//! configure servers. -//! -//! * [Request](struct.Request.html) and -//! [Response](struct.Response.html): These structs -//! represent HTTP requests and responses and expose various methods -//! for inspecting, creating and otherwise utilizing them. -//! -//! ## Features -//! -//! * Supported *HTTP/1.x* protocol -//! * Streaming and pipelining -//! * Keep-alive and slow requests handling -//! * `WebSockets` server/client -//! * Supported Rust version: 1.26 or later -//! -//! ## Package feature -//! -//! * `session` - enables session support, includes `ring` crate as -//! dependency -//! +//! Basic http primitives for actix-net framework. #![allow( clippy::type_complexity, clippy::new_without_default, diff --git a/src/response.rs b/src/response.rs index 5281c2d95..31c0010ab 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1042,8 +1042,6 @@ mod tests { #[test] #[cfg(feature = "cookies")] fn test_into_builder() { - use crate::httpmessage::HttpMessage; - let mut resp: Response = "test".into(); assert_eq!(resp.status(), StatusCode::OK); diff --git a/src/ws/client/connect.rs b/src/ws/client/connect.rs index 5e877a645..2760967e0 100644 --- a/src/ws/client/connect.rs +++ b/src/ws/client/connect.rs @@ -4,15 +4,15 @@ use std::str; #[cfg(feature = "cookies")] use cookie::Cookie; use http::header::{HeaderName, HeaderValue}; -use http::{Error as HttpError, HttpTryFrom}; +use http::{Error as HttpError, HttpTryFrom, Uri}; use super::ClientError; -use crate::client::{ClientRequest, ClientRequestBuilder}; use crate::header::IntoHeaderValue; +use crate::message::RequestHead; /// `WebSocket` connection pub struct Connect { - pub(super) request: ClientRequestBuilder, + pub(super) head: RequestHead, pub(super) err: Option, pub(super) http_err: Option, pub(super) origin: Option, @@ -25,7 +25,7 @@ impl Connect { /// Create new websocket connection pub fn new>(uri: S) -> Connect { let mut cl = Connect { - request: ClientRequest::build(), + head: RequestHead::default(), err: None, http_err: None, origin: None, @@ -33,7 +33,12 @@ impl Connect { max_size: 65_536, server_mode: false, }; - cl.request.uri(uri.as_ref()); + + match Uri::try_from(uri.as_ref()) { + Ok(uri) => cl.head.uri = uri, + Err(e) => cl.http_err = Some(e.into()), + } + cl } @@ -51,12 +56,12 @@ impl Connect { self } - #[cfg(feature = "cookies")] - /// Set cookie for handshake request - pub fn cookie(mut self, cookie: Cookie) -> Self { - self.request.cookie(cookie); - self - } + // #[cfg(feature = "cookies")] + // /// Set cookie for handshake request + // pub fn cookie(mut self, cookie: Cookie) -> Self { + // self.request.cookie(cookie); + // self + // } /// Set request Origin pub fn origin(mut self, origin: V) -> Self @@ -90,7 +95,15 @@ impl Connect { HeaderName: HttpTryFrom, V: IntoHeaderValue, { - self.request.header(key, value); + match HeaderName::try_from(key) { + Ok(key) => match value.try_into() { + Ok(value) => { + self.head.headers.append(key, value); + } + Err(e) => self.http_err = Some(e.into()), + }, + Err(e) => self.http_err = Some(e.into()), + } self } } diff --git a/src/ws/client/service.rs b/src/ws/client/service.rs index a0a9b2030..8a0840f90 100644 --- a/src/ws/client/service.rs +++ b/src/ws/client/service.rs @@ -14,8 +14,8 @@ use rand; use sha1::Sha1; use crate::body::BodyLength; -use crate::client::ClientResponse; use crate::h1; +use crate::message::{ConnectionType, Head, ResponseHead}; use crate::ws::Codec; use super::{ClientError, Connect, Protocol}; @@ -89,27 +89,35 @@ where } else { // origin if let Some(origin) = req.origin.take() { - req.request.set_header(header::ORIGIN, origin); + req.head.headers.insert(header::ORIGIN, origin); } - req.request.upgrade("websocket"); - req.request.set_header(header::SEC_WEBSOCKET_VERSION, "13"); + req.head.set_connection_type(ConnectionType::Upgrade); + req.head + .headers + .insert(header::UPGRADE, HeaderValue::from_static("websocket")); + req.head.headers.insert( + header::SEC_WEBSOCKET_VERSION, + HeaderValue::from_static("13"), + ); if let Some(protocols) = req.protocols.take() { - req.request - .set_header(header::SEC_WEBSOCKET_PROTOCOL, protocols.as_str()); + req.head.headers.insert( + header::SEC_WEBSOCKET_PROTOCOL, + HeaderValue::try_from(protocols.as_str()).unwrap(), + ); } - let mut request = match req.request.finish() { - Ok(req) => req, - Err(e) => return Either::A(err(e.into())), + if let Some(e) = req.http_err { + return Either::A(err(e.into())); }; - if request.uri().host().is_none() { + let mut request = req.head; + if request.uri.host().is_none() { return Either::A(err(ClientError::InvalidUrl)); } // supported protocols - let proto = if let Some(scheme) = request.uri().scheme_part() { + let proto = if let Some(scheme) = request.uri.scheme_part() { match Protocol::from(scheme.as_str()) { Some(proto) => proto, None => return Either::A(err(ClientError::InvalidUrl)), @@ -124,14 +132,14 @@ where let sec_key: [u8; 16] = rand::random(); let key = base64::encode(&sec_key); - request.headers_mut().insert( + request.headers.insert( header::SEC_WEBSOCKET_KEY, HeaderValue::try_from(key.as_str()).unwrap(), ); // prep connection - let connect = TcpConnect::new(request.uri().host().unwrap().to_string()) - .set_port(request.uri().port_u16().unwrap_or_else(|| proto.port())); + let connect = TcpConnect::new(request.uri.host().unwrap().to_string()) + .set_port(request.uri.port_u16().unwrap_or_else(|| proto.port())); let fut = Box::new( self.connector @@ -141,7 +149,7 @@ where // h1 protocol let framed = Framed::new(io, h1::ClientCodec::default()); framed - .send((request.into_parts().0, BodyLength::None).into()) + .send((request, BodyLength::None).into()) .map_err(ClientError::from) .and_then(|framed| { framed @@ -172,7 +180,7 @@ where { fut: Box< Future< - Item = (Option, Framed), + Item = (Option, Framed), Error = ClientError, >, >, @@ -198,11 +206,11 @@ where }; // verify response - if res.status() != StatusCode::SWITCHING_PROTOCOLS { - return Err(ClientError::InvalidResponseStatus(res.status())); + if res.status != StatusCode::SWITCHING_PROTOCOLS { + return Err(ClientError::InvalidResponseStatus(res.status)); } // Check for "UPGRADE" to websocket header - let has_hdr = if let Some(hdr) = res.headers().get(header::UPGRADE) { + let has_hdr = if let Some(hdr) = res.headers.get(header::UPGRADE) { if let Ok(s) = hdr.to_str() { s.to_lowercase().contains("websocket") } else { @@ -216,7 +224,7 @@ where return Err(ClientError::InvalidUpgradeHeader); } // Check for "CONNECTION" header - if let Some(conn) = res.headers().get(header::CONNECTION) { + if let Some(conn) = res.headers.get(header::CONNECTION) { if let Ok(s) = conn.to_str() { if !s.to_lowercase().contains("upgrade") { trace!("Invalid connection header: {}", s); @@ -231,7 +239,7 @@ where return Err(ClientError::MissingConnectionHeader); } - if let Some(key) = res.headers().get(header::SEC_WEBSOCKET_ACCEPT) { + if let Some(key) = res.headers.get(header::SEC_WEBSOCKET_ACCEPT) { // field is constructed by concatenating /key/ // with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (RFC 6455) const WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index b7535f99c..313bc55cf 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -29,7 +29,7 @@ default = ["session"] session = ["cookie/secure"] # openssl -ssl = ["openssl", "actix-http/ssl", "actix-server/ssl"] +ssl = ["openssl", "actix-http/ssl", "actix-server/ssl", "awc/ssl"] [dependencies] actix-codec = "0.1.1" @@ -38,6 +38,7 @@ actix-http = { path=".." } actix-service = "0.3.4" actix-server = "0.4.0" actix-utils = "0.3.4" +awc = { git = "https://github.com/actix/actix-web.git" } base64 = "0.10" bytes = "0.4" diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 26bca787e..77329e700 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -3,15 +3,12 @@ use std::sync::mpsc; use std::{net, thread, time}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_http::body::MessageBody; -use actix_http::client::{ - ClientRequest, ClientRequestBuilder, ClientResponse, ConnectError, Connection, - Connector, SendRequestError, -}; -use actix_http::{http::Uri, ws}; +use actix_http::client::Connector; +use actix_http::ws; use actix_rt::{Runtime, System}; use actix_server::{Server, StreamServiceFactory}; use actix_service::Service; +use awc::{Client, ClientRequest}; use futures::future::{lazy, Future}; use http::Method; use net2::TcpBuilder; @@ -47,6 +44,7 @@ pub struct TestServer; pub struct TestServerRuntime { addr: net::SocketAddr, rt: Runtime, + client: Client, } impl TestServer { @@ -71,11 +69,39 @@ impl TestServer { }); let (system, addr) = rx.recv().unwrap(); + let mut rt = Runtime::new().unwrap(); + + let client = rt + .block_on(lazy(move || { + let connector = { + #[cfg(feature = "ssl")] + { + use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; + + let mut builder = + SslConnector::builder(SslMethod::tls()).unwrap(); + builder.set_verify(SslVerifyMode::NONE); + let _ = builder.set_alpn_protos(b"\x02h2\x08http/1.1").map_err( + |e| log::error!("Can not set alpn protocol: {:?}", e), + ); + Connector::new() + .timeout(time::Duration::from_millis(500)) + .ssl(builder.build()) + .service() + } + #[cfg(not(feature = "ssl"))] + { + Connector::new() + .timeout(time::Duration::from_millis(500)) + .service() + } + }; + + Ok::(Client::build().connector(connector).finish()) + })) + .unwrap(); System::set_current(system); - TestServerRuntime { - addr, - rt: Runtime::new().unwrap(), - } + TestServerRuntime { addr, rt, client } } /// Get firat available unused address @@ -130,64 +156,38 @@ impl TestServerRuntime { } /// Create `GET` request - pub fn get(&self) -> ClientRequestBuilder { - ClientRequest::get(self.url("/").as_str()) + pub fn get(&self) -> ClientRequest { + self.client.get(self.url("/").as_str()) } /// Create https `GET` request - pub fn sget(&self) -> ClientRequestBuilder { - ClientRequest::get(self.surl("/").as_str()) + pub fn sget(&self) -> ClientRequest { + self.client.get(self.surl("/").as_str()) } /// Create `POST` request - pub fn post(&self) -> ClientRequestBuilder { - ClientRequest::post(self.url("/").as_str()) + pub fn post(&self) -> ClientRequest { + self.client.post(self.url("/").as_str()) + } + + /// Create https `POST` request + pub fn spost(&self) -> ClientRequest { + self.client.post(self.surl("/").as_str()) } /// Create `HEAD` request - pub fn head(&self) -> ClientRequestBuilder { - ClientRequest::head(self.url("/").as_str()) + pub fn head(&self) -> ClientRequest { + self.client.head(self.url("/").as_str()) + } + + /// Create https `HEAD` request + pub fn shead(&self) -> ClientRequest { + self.client.head(self.surl("/").as_str()) } /// Connect to test http server - pub fn client(&self, meth: Method, path: &str) -> ClientRequestBuilder { - ClientRequest::build() - .method(meth) - .uri(self.url(path).as_str()) - .take() - } - - fn new_connector( - ) -> impl Service + Clone - { - #[cfg(feature = "ssl")] - { - use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; - - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - let _ = builder - .set_alpn_protos(b"\x02h2\x08http/1.1") - .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); - Connector::new() - .timeout(time::Duration::from_millis(500)) - .ssl(builder.build()) - .service() - } - #[cfg(not(feature = "ssl"))] - { - Connector::new() - .timeout(time::Duration::from_millis(500)) - .service() - } - } - - /// Http connector - pub fn connector( - &mut self, - ) -> impl Service + Clone - { - self.execute(|| TestServerRuntime::new_connector()) + pub fn request>(&self, method: Method, path: S) -> ClientRequest { + self.client.request(method, path.as_ref()) } /// Stop http server @@ -213,15 +213,6 @@ impl TestServerRuntime { ) -> Result, ws::ClientError> { self.ws_at("/") } - - /// Send request and read response message - pub fn send_request( - &mut self, - req: ClientRequest, - ) -> Result { - let mut conn = self.connector(); - self.rt.block_on(req.send(&mut conn)) - } } impl Drop for TestServerRuntime { diff --git a/tests/test_client.rs b/tests/test_client.rs index 2832b1b70..1ca7437d6 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -4,7 +4,7 @@ use futures::future::{self, ok}; use futures::{Future, Stream}; use actix_http::{ - client, error::PayloadError, HttpMessage, HttpService, Request, Response, + error::PayloadError, http, HttpMessage, HttpService, Request, Response, }; use actix_http_test::TestServer; @@ -48,26 +48,18 @@ fn test_h1_v2() { .finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) .map(|_| ()) }); - let mut connector = srv.connector(); - - let request = srv.get().finish().unwrap(); - let response = srv.block_on(request.send(&mut connector)).unwrap(); + let response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); - let request = srv.get().header("x-test", "111").finish().unwrap(); - let repr = format!("{:?}", request); - assert!(repr.contains("ClientRequest")); - assert!(repr.contains("x-test")); - - let mut response = srv.block_on(request.send(&mut connector)).unwrap(); + let request = srv.get().header("x-test", "111").send(); + let mut response = srv.block_on(request).unwrap(); assert!(response.status().is_success()); // read response let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); - let request = srv.post().finish().unwrap(); - let mut response = srv.block_on(request.send(&mut connector)).unwrap(); + let mut response = srv.block_on(srv.post().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -82,10 +74,7 @@ fn test_connection_close() { .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .map(|_| ()) }); - let mut connector = srv.connector(); - - let request = srv.get().close().finish().unwrap(); - let response = srv.block_on(request.send(&mut connector)).unwrap(); + let response = srv.block_on(srv.get().close_connection().send()).unwrap(); assert!(response.status().is_success()); } @@ -102,12 +91,8 @@ fn test_with_query_parameter() { }) .map(|_| ()) }); - let mut connector = srv.connector(); - let request = client::ClientRequest::get(srv.url("/?qp=5")) - .finish() - .unwrap(); - - let response = srv.block_on(request.send(&mut connector)).unwrap(); + let request = srv.request(http::Method::GET, srv.url("/?qp=5")).send(); + let response = srv.block_on(request).unwrap(); assert!(response.status().is_success()); } diff --git a/tests/test_server.rs b/tests/test_server.rs index 8a7316cdf..5777c5691 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -13,8 +13,8 @@ use futures::stream::{once, Stream}; use actix_http::body::Body; use actix_http::error::PayloadError; use actix_http::{ - body, client, error, http, http::header, Error, HttpMessage as HttpMessage2, - HttpService, KeepAlive, Request, Response, + body, error, http, http::header, Error, HttpMessage as HttpMessage2, HttpService, + KeepAlive, Request, Response, }; fn load_body(stream: S) -> impl Future @@ -37,8 +37,7 @@ fn test_h1() { .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) }); - let req = client::ClientRequest::get(srv.url("/")).finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); } @@ -56,8 +55,7 @@ fn test_h1_2() { .map(|_| ()) }); - let req = client::ClientRequest::get(srv.url("/")).finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); } @@ -100,8 +98,7 @@ fn test_h2() -> std::io::Result<()> { ) }); - let req = client::ClientRequest::get(srv.surl("/")).finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let response = srv.block_on(srv.sget().send()).unwrap(); assert!(response.status().is_success()); Ok(()) } @@ -124,8 +121,7 @@ fn test_h2_1() -> std::io::Result<()> { ) }); - let req = client::ClientRequest::get(srv.surl("/")).finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let response = srv.block_on(srv.sget().send()).unwrap(); assert!(response.status().is_success()); Ok(()) } @@ -149,10 +145,7 @@ fn test_h2_body() -> std::io::Result<()> { ) }); - let req = client::ClientRequest::get(srv.surl("/")) - .body(data.clone()) - .unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.sget().send_body(data.clone())).unwrap(); assert!(response.status().is_success()); let body = srv.block_on(load_body(response.take_payload())).unwrap(); @@ -331,24 +324,24 @@ fn test_content_length() { { for i in 0..4 { - let req = client::ClientRequest::get(srv.url(&format!("/{}", i))) - .finish() - .unwrap(); - let response = srv.send_request(req).unwrap(); + let req = srv + .request(http::Method::GET, srv.url(&format!("/{}", i))) + .send(); + let response = srv.block_on(req).unwrap(); assert_eq!(response.headers().get(&header), None); - let req = client::ClientRequest::head(srv.url(&format!("/{}", i))) - .finish() - .unwrap(); - let response = srv.send_request(req).unwrap(); + let req = srv + .request(http::Method::HEAD, srv.url(&format!("/{}", i))) + .send(); + let response = srv.block_on(req).unwrap(); assert_eq!(response.headers().get(&header), None); } for i in 4..6 { - let req = client::ClientRequest::get(srv.url(&format!("/{}", i))) - .finish() - .unwrap(); - let response = srv.send_request(req).unwrap(); + let req = srv + .request(http::Method::GET, srv.url(&format!("/{}", i))) + .send(); + let response = srv.block_on(req).unwrap(); assert_eq!(response.headers().get(&header), Some(&value)); } } @@ -389,24 +382,24 @@ fn test_h2_content_length() { { for i in 0..4 { - let req = client::ClientRequest::get(srv.surl(&format!("/{}", i))) - .finish() - .unwrap(); - let response = srv.send_request(req).unwrap(); + let req = srv + .request(http::Method::GET, srv.surl(&format!("/{}", i))) + .send(); + let response = srv.block_on(req).unwrap(); assert_eq!(response.headers().get(&header), None); - let req = client::ClientRequest::head(srv.surl(&format!("/{}", i))) - .finish() - .unwrap(); - let response = srv.send_request(req).unwrap(); + let req = srv + .request(http::Method::HEAD, srv.surl(&format!("/{}", i))) + .send(); + let response = srv.block_on(req).unwrap(); assert_eq!(response.headers().get(&header), None); } for i in 4..6 { - let req = client::ClientRequest::get(srv.surl(&format!("/{}", i))) - .finish() - .unwrap(); - let response = srv.send_request(req).unwrap(); + let req = srv + .request(http::Method::GET, srv.surl(&format!("/{}", i))) + .send(); + let response = srv.block_on(req).unwrap(); assert_eq!(response.headers().get(&header), Some(&value)); } } @@ -442,11 +435,8 @@ fn test_h1_headers() { future::ok::<_, ()>(builder.body(data.clone())) }) }); - let mut connector = srv.connector(); - let req = srv.get().finish().unwrap(); - - let mut response = srv.block_on(req.send(&mut connector)).unwrap(); + let mut response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -489,10 +479,8 @@ fn test_h2_headers() { future::ok::<_, ()>(builder.body(data.clone())) }).map_err(|_| ())) }); - let mut connector = srv.connector(); - let req = client::ClientRequest::get(srv.surl("/")).finish().unwrap(); - let mut response = srv.block_on(req.send(&mut connector)).unwrap(); + let mut response = srv.block_on(srv.sget().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -528,8 +516,7 @@ fn test_h1_body() { HttpService::build().h1(|_| future::ok::<_, ()>(Response::Ok().body(STR))) }); - let req = srv.get().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -551,8 +538,7 @@ fn test_h2_body2() { ) }); - let req = srv.sget().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.sget().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -566,8 +552,7 @@ fn test_h1_head_empty() { HttpService::build().h1(|_| ok::<_, ()>(Response::Ok().body(STR))) }); - let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.head().send()).unwrap(); assert!(response.status().is_success()); { @@ -597,8 +582,7 @@ fn test_h2_head_empty() { ) }); - let req = client::ClientRequest::head(srv.surl("/")).finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.shead().send()).unwrap(); assert!(response.status().is_success()); assert_eq!(response.version(), http::Version::HTTP_2); @@ -623,8 +607,7 @@ fn test_h1_head_binary() { }) }); - let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.head().send()).unwrap(); assert!(response.status().is_success()); { @@ -658,8 +641,7 @@ fn test_h2_head_binary() { ) }); - let req = client::ClientRequest::head(srv.surl("/")).finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.shead().send()).unwrap(); assert!(response.status().is_success()); { @@ -681,8 +663,7 @@ fn test_h1_head_binary2() { HttpService::build().h1(|_| ok::<_, ()>(Response::Ok().body(STR))) }); - let req = client::ClientRequest::head(srv.url("/")).finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let response = srv.block_on(srv.head().send()).unwrap(); assert!(response.status().is_success()); { @@ -708,8 +689,7 @@ fn test_h2_head_binary2() { ) }); - let req = client::ClientRequest::head(srv.surl("/")).finish().unwrap(); - let response = srv.send_request(req).unwrap(); + let response = srv.block_on(srv.shead().send()).unwrap(); assert!(response.status().is_success()); { @@ -733,8 +713,7 @@ fn test_h1_body_length() { }) }); - let req = srv.get().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -761,8 +740,7 @@ fn test_h2_body_length() { ) }); - let req = srv.sget().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.sget().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -783,8 +761,7 @@ fn test_h1_body_chunked_explicit() { }) }); - let req = srv.get().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); assert_eq!( response @@ -825,8 +802,7 @@ fn test_h2_body_chunked_explicit() { ) }); - let req = srv.sget().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.sget().send()).unwrap(); assert!(response.status().is_success()); assert!(!response.headers().contains_key(header::TRANSFER_ENCODING)); @@ -846,8 +822,7 @@ fn test_h1_body_chunked_implicit() { }) }); - let req = srv.get().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); assert_eq!( response @@ -879,8 +854,7 @@ fn test_h1_response_http_error_handling() { })) }); - let req = srv.get().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.get().send()).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response @@ -912,8 +886,7 @@ fn test_h2_response_http_error_handling() { ) }); - let req = srv.sget().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.sget().send()).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response @@ -928,8 +901,7 @@ fn test_h1_service_error() { .h1(|_| Err::(error::ErrorBadRequest("error"))) }); - let req = srv.get().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.get().send()).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response @@ -952,8 +924,7 @@ fn test_h2_service_error() { ) }); - let req = srv.sget().finish().unwrap(); - let mut response = srv.send_request(req).unwrap(); + let mut response = srv.block_on(srv.sget().send()).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response From 50c0ddb3cdac458cc39282830e8815156774d10d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Mar 2019 12:31:51 -0700 Subject: [PATCH 347/427] update tests --- Cargo.toml | 7 +++-- actix-http/Cargo.toml | 1 + actix-http/test-server/Cargo.toml | 2 +- awc/Cargo.toml | 6 ++-- {actix-http/examples => examples}/client.rs | 11 +++---- tests/test_httpserver.rs | 20 ++++++------ tests/test_server.rs | 34 +++++++++------------ 7 files changed, 36 insertions(+), 45 deletions(-) rename {actix-http/examples => examples}/client.rs (74%) diff --git a/Cargo.toml b/Cargo.toml index 2a9883ead..44262e849 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ path = "src/lib.rs" members = [ ".", "awc", + "actix-http", "actix-files", "actix-session", "actix-web-actors", @@ -71,7 +72,7 @@ actix-utils = "0.3.4" actix-router = "0.1.0" actix-rt = "0.2.1" actix-web-codegen = { path="actix-web-codegen" } -actix-http = { git = "https://github.com/actix/actix-http.git", features=["fail"] } +actix-http = { path = "actix-http", features=["fail"] } actix-server = "0.4.1" actix-server-config = "0.1.0" awc = { path = "awc", optional = true } @@ -105,8 +106,8 @@ openssl = { version="0.10", optional = true } # rustls = { version = "^0.15", optional = true } [dev-dependencies] -actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } -actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +actix-http = { path = "actix-http", features=["ssl"] } +actix-http-test = { path = "actix-http/test-server", features=["ssl"] } rand = "0.6" env_logger = "0.6" serde_derive = "1.0" diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index da11a5853..798508a94 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -14,6 +14,7 @@ categories = ["network-programming", "asynchronous", license = "MIT/Apache-2.0" exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] edition = "2018" +workspace = ".." [package.metadata.docs.rs] features = ["ssl", "fail", "cookie"] diff --git a/actix-http/test-server/Cargo.toml b/actix-http/test-server/Cargo.toml index 313bc55cf..316f3e36d 100644 --- a/actix-http/test-server/Cargo.toml +++ b/actix-http/test-server/Cargo.toml @@ -38,7 +38,7 @@ actix-http = { path=".." } actix-service = "0.3.4" actix-server = "0.4.0" actix-utils = "0.3.4" -awc = { git = "https://github.com/actix/actix-web.git" } +awc = { path = "../../awc" } base64 = "0.10" bytes = "0.4" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index b43a8d47f..233fe59db 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -28,7 +28,7 @@ cookies = ["cookie", "actix-http/cookies"] [dependencies] actix-service = "0.3.4" -actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-http = { path = "../actix-http/" } bytes = "0.4" futures = "0.1" log =" 0.4" @@ -44,5 +44,5 @@ openssl = { version="0.10", optional = true } env_logger = "0.6" mime = "0.3" actix-rt = "0.2.1" -actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } -actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +actix-http = { path = "../actix-http/", features=["ssl"] } +actix-http-test = { path = "../actix-http/test-server/", features=["ssl"] } diff --git a/actix-http/examples/client.rs b/examples/client.rs similarity index 74% rename from actix-http/examples/client.rs rename to examples/client.rs index 7f5f8c91a..c4df6f7d6 100644 --- a/actix-http/examples/client.rs +++ b/examples/client.rs @@ -1,4 +1,4 @@ -use actix_http::{client, Error}; +use actix_http::Error; use actix_rt::System; use bytes::BytesMut; use futures::{future::lazy, Future, Stream}; @@ -8,13 +8,10 @@ fn main() -> Result<(), Error> { env_logger::init(); System::new("test").block_on(lazy(|| { - let mut connector = client::Connector::new().service(); - - client::ClientRequest::get("https://www.rust-lang.org/") // <- Create request builder + awc::Client::new() + .get("https://www.rust-lang.org/") // <- Create request builder .header("User-Agent", "Actix-web") - .finish() - .unwrap() - .send(&mut connector) // <- Send http request + .send() // <- Send http request .from_err() .and_then(|response| { // <- server http response diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index 764d50ca2..4a3850f13 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -48,23 +48,21 @@ fn test_start() { }); let (srv, sys) = rx.recv().unwrap(); - let mut connector = test::run_on(|| { + let client = test::run_on(|| { Ok::<_, ()>( - client::Connector::new() - .timeout(Duration::from_millis(100)) - .service(), + awc::Client::build() + .connector( + client::Connector::new() + .timeout(Duration::from_millis(100)) + .service(), + ) + .finish(), ) }) .unwrap(); let host = format!("http://{}", addr); - let response = test::block_on( - client::ClientRequest::get(host.clone()) - .finish() - .unwrap() - .send(&mut connector), - ) - .unwrap(); + let response = test::block_on(client.get(host.clone()).send()).unwrap(); assert!(response.status().is_success()); // stop diff --git a/tests/test_server.rs b/tests/test_server.rs index 2742164ab..9c0f1f655 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -45,8 +45,7 @@ fn test_body() { ) }); - let request = srv.get().finish().unwrap(); - let mut response = srv.send_request(request).unwrap(); + let mut response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -64,8 +63,7 @@ fn test_body_gzip() { ) }); - let request = srv.get().finish().unwrap(); - let mut response = srv.send_request(request).unwrap(); + let mut response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -95,8 +93,7 @@ fn test_body_gzip_large() { ) }); - let request = srv.get().finish().unwrap(); - let mut response = srv.send_request(request).unwrap(); + let mut response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -129,8 +126,7 @@ fn test_body_gzip_large_random() { ) }); - let request = srv.get().finish().unwrap(); - let mut response = srv.send_request(request).unwrap(); + let mut response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -158,8 +154,7 @@ fn test_body_chunked_implicit() { ) }); - let request = srv.get().finish().unwrap(); - let mut response = srv.send_request(request).unwrap(); + let mut response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); assert_eq!( response.headers().get(TRANSFER_ENCODING).unwrap(), @@ -190,8 +185,9 @@ fn test_body_br_streaming() { ) }); - let request = srv.get().header(ACCEPT_ENCODING, "br").finish().unwrap(); - let mut response = srv.send_request(request).unwrap(); + let mut response = srv + .block_on(srv.get().header(ACCEPT_ENCODING, "br").send()) + .unwrap(); assert!(response.status().is_success()); // read response @@ -212,8 +208,7 @@ fn test_head_binary() { ))) }); - let request = srv.head().finish().unwrap(); - let mut response = srv.send_request(request).unwrap(); + let mut response = srv.block_on(srv.head().send()).unwrap(); assert!(response.status().is_success()); { @@ -239,8 +234,7 @@ fn test_no_chunking() { )))) }); - let request = srv.get().finish().unwrap(); - let mut response = srv.send_request(request).unwrap(); + let mut response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); assert!(!response.headers().contains_key(TRANSFER_ENCODING)); @@ -262,8 +256,7 @@ fn test_body_deflate() { }); // client request - let request = srv.get().finish().unwrap(); - let mut response = srv.send_request(request).unwrap(); + let mut response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -289,8 +282,9 @@ fn test_body_brotli() { }); // client request - let request = srv.get().header(ACCEPT_ENCODING, "br").finish().unwrap(); - let mut response = srv.send_request(request).unwrap(); + let mut response = srv + .block_on(srv.get().header(ACCEPT_ENCODING, "br").send()) + .unwrap(); assert!(response.status().is_success()); // read response From 9451ba71f4386ddcbd4c338efbfe673d71cf1802 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Mar 2019 12:50:51 -0700 Subject: [PATCH 348/427] update cargo files --- Cargo.toml | 2 +- actix-files/Cargo.toml | 6 ++-- actix-http/Cargo.toml | 9 +++--- actix-session/Cargo.toml | 4 +-- actix-web-actors/Cargo.toml | 8 ++--- actix-web-codegen/Cargo.toml | 6 ++-- actix-web-codegen/tests/test_macro.rs | 6 ++-- awc/Cargo.toml | 4 +-- .../test-server => test-server}/Cargo.toml | 14 +++++---- .../test-server => test-server}/src/lib.rs | 30 +++++++++++-------- 10 files changed, 48 insertions(+), 41 deletions(-) rename {actix-http/test-server => test-server}/Cargo.toml (84%) rename {actix-http/test-server => test-server}/src/lib.rs (91%) diff --git a/Cargo.toml b/Cargo.toml index 44262e849..40f8c7c4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,7 +107,7 @@ openssl = { version="0.10", optional = true } [dev-dependencies] actix-http = { path = "actix-http", features=["ssl"] } -actix-http-test = { path = "actix-http/test-server", features=["ssl"] } +actix-http-test = { path = "test-server", features=["ssl"] } rand = "0.6" env_logger = "0.6" serde_derive = "1.0" diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 65faa5e8c..ba8fbb6c2 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "actix-files" -version = "0.1.0" +version = "0.1.0-alpha.1" authors = ["Nikolay Kim "] description = "Static files support for Actix web." readme = "README.md" keywords = ["actix", "http", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" -documentation = "https://actix.rs/api/actix-web/stable/actix_web/" +documentation = "https://docs.rs/actix-files/" categories = ["asynchronous", "web-programming::http-server"] license = "MIT/Apache-2.0" edition = "2018" @@ -19,7 +19,7 @@ path = "src/lib.rs" [dependencies] actix-web = { path=".." } -actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-http = { path="../actix-http" } actix-service = "0.3.3" bitflags = "1" diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 798508a94..3b403ac2a 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "0.1.0" +version = "0.1.0-alpha.1" authors = ["Nikolay Kim "] description = "Actix http" readme = "README.md" @@ -20,9 +20,8 @@ workspace = ".." features = ["ssl", "fail", "cookie"] [badges] -travis-ci = { repository = "actix/actix-http", branch = "master" } -# appveyor = { repository = "fafhrd91/actix-http-b1qsn" } -codecov = { repository = "actix/actix-http", branch = "master", service = "github" } +travis-ci = { repository = "actix/actix-web", branch = "master" } +codecov = { repository = "actix/actix-web", branch = "master", service = "github" } [lib] name = "actix_http" @@ -87,7 +86,7 @@ openssl = { version="0.10", optional = true } actix-rt = "0.2.1" actix-server = { version = "0.4.0", features=["ssl"] } actix-connect = { version = "0.1.0", features=["ssl"] } -actix-http-test = { path="test-server", features=["ssl"] } +actix-http-test = { path="../test-server", features=["ssl"] } env_logger = "0.6" serde_derive = "1.0" diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml index 554f3d7fc..3adcc8f53 100644 --- a/actix-session/Cargo.toml +++ b/actix-session/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "actix-session" -version = "0.1.0" +version = "0.1.0-alpha.1" authors = ["Nikolay Kim "] description = "Session for actix web framework." readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-web/" +documentation = "https://docs.rs/actix-session/" license = "MIT/Apache-2.0" exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] workspace = ".." diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index db42a1a23..95b726619 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "0.1.0" +version = "0.1.0-alpha.1" authors = ["Nikolay Kim "] description = "Actix actors support for actix web framework." readme = "README.md" @@ -20,12 +20,12 @@ path = "src/lib.rs" [dependencies] actix-web = { path=".." } actix = { git = "https://github.com/actix/actix.git" } -actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-http = { path = "../actix-http/" } actix-codec = "0.1.1" bytes = "0.4" futures = "0.1" [dev-dependencies] env_logger = "0.6" -actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } -actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +actix-http = { path = "../actix-http/", features=["ssl"] } +actix-http-test = { path = "../test-server/", features=["ssl"] } diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index d87b71ba9..3785acb32 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "actix-web-codegen" description = "Actix web codegen macros" -version = "0.1.0" +version = "0.1.0-alpha.1" authors = ["Nikolay Kim "] license = "MIT/Apache-2.0" edition = "2018" @@ -16,5 +16,5 @@ syn = { version = "0.15", features = ["full", "parsing"] } [dev-dependencies] actix-web = { path = ".." } -actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } -actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +actix-http = { path = "../actix-http/", features=["ssl"] } +actix-http-test = { path = "../test-server/", features=["ssl"] } diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 62b5d618f..8bf2c88be 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -1,6 +1,6 @@ use actix_http::HttpService; use actix_http_test::TestServer; -use actix_web::{get, App, HttpResponse, Responder}; +use actix_web::{get, http, App, HttpResponse, Responder}; #[get("/test")] fn test() -> impl Responder { @@ -11,7 +11,7 @@ fn test() -> impl Responder { fn test_body() { let mut srv = TestServer::new(|| HttpService::new(App::new().service(test))); - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.send_request(request).unwrap(); + let request = srv.request(http::Method::GET, srv.url("/test")); + let response = srv.block_on(request.send()).unwrap(); assert!(response.status().is_success()); } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 233fe59db..023bd088b 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "0.1.0" +version = "0.1.0-alpha.1" authors = ["Nikolay Kim "] description = "Actix web client." readme = "README.md" @@ -45,4 +45,4 @@ env_logger = "0.6" mime = "0.3" actix-rt = "0.2.1" actix-http = { path = "../actix-http/", features=["ssl"] } -actix-http-test = { path = "../actix-http/test-server/", features=["ssl"] } +actix-http-test = { path = "../test-server/", features=["ssl"] } diff --git a/actix-http/test-server/Cargo.toml b/test-server/Cargo.toml similarity index 84% rename from actix-http/test-server/Cargo.toml rename to test-server/Cargo.toml index 316f3e36d..6959adbe5 100644 --- a/actix-http/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -1,19 +1,20 @@ [package] name = "actix-http-test" -version = "0.1.0" +version = "0.1.0-alpha.1" authors = ["Nikolay Kim "] -description = "Actix http" +description = "Actix http test server" readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" -documentation = "https://actix.rs/api/actix-web/stable/actix_web/" +documentation = "https://docs.rs/actix-http-test/" categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] license = "MIT/Apache-2.0" exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] edition = "2018" +workspace = ".." [package.metadata.docs.rs] features = ["session"] @@ -34,11 +35,11 @@ ssl = ["openssl", "actix-http/ssl", "actix-server/ssl", "awc/ssl"] [dependencies] actix-codec = "0.1.1" actix-rt = "0.2.1" -actix-http = { path=".." } +actix-http = { path="../actix-http" } actix-service = "0.3.4" actix-server = "0.4.0" actix-utils = "0.3.4" -awc = { path = "../../awc" } +awc = { path = "../awc" } base64 = "0.10" bytes = "0.4" @@ -58,3 +59,6 @@ tokio-tcp = "0.1" tokio-timer = "0.2" openssl = { version="0.10", optional = true } + +[dev-dependencies] +actix-web = { path=".." } diff --git a/actix-http/test-server/src/lib.rs b/test-server/src/lib.rs similarity index 91% rename from actix-http/test-server/src/lib.rs rename to test-server/src/lib.rs index 77329e700..7cd94d4d2 100644 --- a/actix-http/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -21,22 +21,26 @@ use net2::TcpBuilder; /// # Examples /// /// ```rust -/// # extern crate actix_web; -/// # use actix_web::*; +/// use actix_http::HttpService; +/// use actix_http_test::TestServer; +/// use actix_web::{web, App, HttpResponse}; /// # -/// # fn my_handler(req: &HttpRequest) -> HttpResponse { -/// # HttpResponse::Ok().into() -/// # } -/// # -/// # fn main() { -/// use actix_web::test::TestServer; +/// fn my_handler() -> HttpResponse { +/// HttpResponse::Ok().into() +/// } /// -/// let mut srv = TestServer::new(|app| app.handler(my_handler)); +/// fn main() { +/// let mut srv = TestServer::new( +/// || HttpService::new( +/// App::new().service( +/// web::resource("/").to(my_handler)) +/// ) +/// ); /// -/// let req = srv.get().finish().unwrap(); -/// let response = srv.execute(req.send()).unwrap(); -/// assert!(response.status().is_success()); -/// # } +/// let req = srv.get(); +/// let response = srv.block_on(req.send()).unwrap(); +/// assert!(response.status().is_success()); +/// } /// ``` pub struct TestServer; From 1904b01fc0586bcefed1df9c3c04d34c65f6a995 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Mar 2019 15:14:32 -0700 Subject: [PATCH 349/427] add content-encoding decompression --- Cargo.toml | 16 +- actix-http/Cargo.toml | 13 + actix-http/src/encoding/decoder.rs | 191 ++++++++++++ actix-http/src/encoding/encoder.rs | 234 +++++++++++++++ actix-http/src/encoding/mod.rs | 35 +++ actix-http/src/lib.rs | 1 + src/app.rs | 4 +- src/app_service.rs | 2 +- src/middleware/compress.rs | 278 +----------------- src/middleware/decompress.rs | 60 ++++ src/middleware/mod.rs | 5 + src/resource.rs | 2 +- src/scope.rs | 2 +- src/service.rs | 16 +- src/test.rs | 3 +- tests/test_server.rs | 456 ++++++++++++++--------------- 16 files changed, 780 insertions(+), 538 deletions(-) create mode 100644 actix-http/src/encoding/decoder.rs create mode 100644 actix-http/src/encoding/encoder.rs create mode 100644 actix-http/src/encoding/mod.rs create mode 100644 src/middleware/decompress.rs diff --git a/Cargo.toml b/Cargo.toml index 40f8c7c4a..22c2efe99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ members = [ ] [package.metadata.docs.rs] -features = ["ssl", "tls", "rust-tls", "brotli", "flate2-c", "cookies"] +features = ["ssl", "tls", "rust-tls", "brotli", "flate2-c", "cookies", "client"] [features] default = ["brotli", "flate2-c", "cookies", "client"] @@ -45,13 +45,13 @@ default = ["brotli", "flate2-c", "cookies", "client"] client = ["awc"] # brotli encoding, requires c compiler -brotli = ["brotli2"] +brotli = ["actix-http/brotli2"] # miniz-sys backend for flate2 crate -flate2-c = ["flate2/miniz-sys"] +flate2-c = ["actix-http/flate2-c"] # rust backend for flate2 crate -flate2-rust = ["flate2/rust_backend"] +flate2-rust = ["actix-http/flate2-rust"] # sessions feature, session require "ring" crate and c compiler cookies = ["cookie", "actix-http/cookies"] @@ -96,22 +96,20 @@ url = { version="1.7", features=["query_encoding"] } # cookies support cookie = { version="0.11", features=["secure", "percent-encode"], optional = true } -# compression -brotli2 = { version="^0.3.2", optional = true } -flate2 = { version="^1.0.2", optional = true, default-features = false } - # ssl support native-tls = { version="0.2", optional = true } openssl = { version="0.10", optional = true } # rustls = { version = "^0.15", optional = true } [dev-dependencies] -actix-http = { path = "actix-http", features=["ssl"] } +actix-http = { path = "actix-http", features=["ssl", "brotli", "flate2-c"] } actix-http-test = { path = "test-server", features=["ssl"] } rand = "0.6" env_logger = "0.6" serde_derive = "1.0" tokio-timer = "0.2.8" +brotli2 = { version="^0.3.2" } +flate2 = { version="^1.0.2" } [profile.release] lto = true diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 3b403ac2a..7b73e7e26 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -36,6 +36,15 @@ ssl = ["openssl", "actix-connect/ssl"] # cookies integration cookies = ["cookie"] +# brotli encoding, requires c compiler +brotli = ["brotli2"] + +# miniz-sys backend for flate2 crate +flate2-c = ["flate2/miniz-sys"] + +# rust backend for flate2 crate +flate2-rust = ["flate2/rust_backend"] + # failure integration. actix does not use failure anymore fail = ["failure"] @@ -77,6 +86,10 @@ tokio-timer = "0.2" tokio-current-thread = "0.1" trust-dns-resolver = { version="0.11.0-alpha.2", default-features = false } +# compression +brotli2 = { version="^0.3.2", optional = true } +flate2 = { version="^1.0.2", optional = true, default-features = false } + # optional deps cookie = { version="0.11", features=["percent-encode"], optional = true } failure = { version = "0.1.5", optional = true } diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs new file mode 100644 index 000000000..a922d1738 --- /dev/null +++ b/actix-http/src/encoding/decoder.rs @@ -0,0 +1,191 @@ +use std::io::{self, Write}; + +use bytes::Bytes; +use futures::{Async, Poll, Stream}; + +#[cfg(feature = "brotli")] +use brotli2::write::BrotliDecoder; +#[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] +use flate2::write::{GzDecoder, ZlibDecoder}; + +use super::Writer; +use crate::error::PayloadError; +use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING}; + +pub struct Decoder { + stream: T, + decoder: Option, +} + +impl Decoder +where + T: Stream, +{ + pub fn new(stream: T, encoding: ContentEncoding) -> Self { + let decoder = match encoding { + #[cfg(feature = "brotli")] + ContentEncoding::Br => Some(ContentDecoder::Br(Box::new( + BrotliDecoder::new(Writer::new()), + ))), + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( + ZlibDecoder::new(Writer::new()), + ))), + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new( + GzDecoder::new(Writer::new()), + ))), + _ => None, + }; + Decoder { stream, decoder } + } + + pub fn from_headers(headers: &HeaderMap, stream: T) -> Self { + // check content-encoding + let encoding = if let Some(enc) = headers.get(CONTENT_ENCODING) { + if let Ok(enc) = enc.to_str() { + ContentEncoding::from(enc) + } else { + ContentEncoding::Identity + } + } else { + ContentEncoding::Identity + }; + + Self::new(stream, encoding) + } +} + +impl Stream for Decoder +where + T: Stream, +{ + type Item = Bytes; + type Error = PayloadError; + + fn poll(&mut self) -> Poll, Self::Error> { + loop { + match self.stream.poll()? { + Async::Ready(Some(chunk)) => { + if let Some(ref mut decoder) = self.decoder { + match decoder.feed_data(chunk) { + Ok(Some(chunk)) => return Ok(Async::Ready(Some(chunk))), + Ok(None) => continue, + Err(e) => return Err(e.into()), + } + } else { + break; + } + } + Async::Ready(None) => { + return if let Some(mut decoder) = self.decoder.take() { + match decoder.feed_eof() { + Ok(chunk) => Ok(Async::Ready(chunk)), + Err(e) => Err(e.into()), + } + } else { + Ok(Async::Ready(None)) + }; + } + Async::NotReady => break, + } + } + Ok(Async::NotReady) + } +} + +enum ContentDecoder { + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + Deflate(Box>), + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + Gzip(Box>), + #[cfg(feature = "brotli")] + Br(Box>), +} + +impl ContentDecoder { + fn feed_eof(&mut self) -> io::Result> { + match self { + #[cfg(feature = "brotli")] + ContentDecoder::Br(ref mut decoder) => match decoder.finish() { + Ok(mut writer) => { + let b = writer.take(); + if !b.is_empty() { + Ok(Some(b)) + } else { + Ok(None) + } + } + Err(e) => Err(e), + }, + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() { + Ok(_) => { + let b = decoder.get_mut().take(); + if !b.is_empty() { + Ok(Some(b)) + } else { + Ok(None) + } + } + Err(e) => Err(e), + }, + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() { + Ok(_) => { + let b = decoder.get_mut().take(); + if !b.is_empty() { + Ok(Some(b)) + } else { + Ok(None) + } + } + Err(e) => Err(e), + }, + } + } + + fn feed_data(&mut self, data: Bytes) -> io::Result> { + match self { + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) { + Ok(_) => { + decoder.flush()?; + let b = decoder.get_mut().take(); + if !b.is_empty() { + Ok(Some(b)) + } else { + Ok(None) + } + } + Err(e) => Err(e), + }, + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) { + Ok(_) => { + decoder.flush()?; + let b = decoder.get_mut().take(); + if !b.is_empty() { + Ok(Some(b)) + } else { + Ok(None) + } + } + Err(e) => Err(e), + }, + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) { + Ok(_) => { + decoder.flush()?; + let b = decoder.get_mut().take(); + if !b.is_empty() { + Ok(Some(b)) + } else { + Ok(None) + } + } + Err(e) => Err(e), + }, + } + } +} diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs new file mode 100644 index 000000000..1985dcdf2 --- /dev/null +++ b/actix-http/src/encoding/encoder.rs @@ -0,0 +1,234 @@ +//! Stream encoder +use std::io::{self, Write}; + +use bytes::Bytes; +use futures::{Async, Poll}; + +#[cfg(feature = "brotli")] +use brotli2::write::BrotliEncoder; +#[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] +use flate2::write::{GzEncoder, ZlibEncoder}; + +use crate::body::{Body, BodyLength, MessageBody, ResponseBody}; +use crate::http::header::{ContentEncoding, CONTENT_ENCODING}; +use crate::http::{HeaderValue, HttpTryFrom, StatusCode}; +use crate::{Error, Head, ResponseHead}; + +use super::Writer; + +pub struct Encoder { + body: EncoderBody, + encoder: Option, +} + +impl Encoder { + pub fn response( + encoding: ContentEncoding, + head: &mut ResponseHead, + body: ResponseBody, + ) -> ResponseBody> { + let has_ce = head.headers().contains_key(CONTENT_ENCODING); + match body { + ResponseBody::Other(b) => match b { + Body::None => ResponseBody::Other(Body::None), + Body::Empty => ResponseBody::Other(Body::Empty), + Body::Bytes(buf) => { + if !(has_ce + || encoding == ContentEncoding::Identity + || encoding == ContentEncoding::Auto) + { + let mut enc = ContentEncoder::encoder(encoding).unwrap(); + + // TODO return error! + let _ = enc.write(buf.as_ref()); + let body = enc.finish().unwrap(); + update_head(encoding, head); + ResponseBody::Other(Body::Bytes(body)) + } else { + ResponseBody::Other(Body::Bytes(buf)) + } + } + Body::Message(stream) => { + if has_ce || head.status == StatusCode::SWITCHING_PROTOCOLS { + ResponseBody::Body(Encoder { + body: EncoderBody::Other(stream), + encoder: None, + }) + } else { + update_head(encoding, head); + head.no_chunking = false; + ResponseBody::Body(Encoder { + body: EncoderBody::Other(stream), + encoder: ContentEncoder::encoder(encoding), + }) + } + } + }, + ResponseBody::Body(stream) => { + if has_ce || head.status == StatusCode::SWITCHING_PROTOCOLS { + ResponseBody::Body(Encoder { + body: EncoderBody::Body(stream), + encoder: None, + }) + } else { + update_head(encoding, head); + head.no_chunking = false; + ResponseBody::Body(Encoder { + body: EncoderBody::Body(stream), + encoder: ContentEncoder::encoder(encoding), + }) + } + } + } + } +} + +enum EncoderBody { + Body(B), + Other(Box), +} + +impl MessageBody for Encoder { + fn length(&self) -> BodyLength { + if self.encoder.is_none() { + match self.body { + EncoderBody::Body(ref b) => b.length(), + EncoderBody::Other(ref b) => b.length(), + } + } else { + BodyLength::Stream + } + } + + fn poll_next(&mut self) -> Poll, Error> { + loop { + let result = match self.body { + EncoderBody::Body(ref mut b) => b.poll_next()?, + EncoderBody::Other(ref mut b) => b.poll_next()?, + }; + match result { + Async::NotReady => return Ok(Async::NotReady), + Async::Ready(Some(chunk)) => { + if let Some(ref mut encoder) = self.encoder { + if encoder.write(&chunk)? { + return Ok(Async::Ready(Some(encoder.take()))); + } + } else { + return Ok(Async::Ready(Some(chunk))); + } + } + Async::Ready(None) => { + if let Some(encoder) = self.encoder.take() { + let chunk = encoder.finish()?; + if chunk.is_empty() { + return Ok(Async::Ready(None)); + } else { + return Ok(Async::Ready(Some(chunk))); + } + } else { + return Ok(Async::Ready(None)); + } + } + } + } + } +} + +fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { + head.headers_mut().insert( + CONTENT_ENCODING, + HeaderValue::try_from(Bytes::from_static(encoding.as_str().as_bytes())).unwrap(), + ); +} + +enum ContentEncoder { + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + Deflate(ZlibEncoder), + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + Gzip(GzEncoder), + #[cfg(feature = "brotli")] + Br(BrotliEncoder), +} + +impl ContentEncoder { + fn encoder(encoding: ContentEncoding) -> Option { + match encoding { + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new( + Writer::new(), + flate2::Compression::fast(), + ))), + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( + Writer::new(), + flate2::Compression::fast(), + ))), + #[cfg(feature = "brotli")] + ContentEncoding::Br => { + Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) + } + _ => None, + } + } + + #[inline] + pub(crate) fn take(&mut self) -> Bytes { + match *self { + #[cfg(feature = "brotli")] + ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), + } + } + + fn finish(self) -> Result { + match self { + #[cfg(feature = "brotli")] + ContentEncoder::Br(encoder) => match encoder.finish() { + Ok(writer) => Ok(writer.buf.freeze()), + Err(err) => Err(err), + }, + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentEncoder::Gzip(encoder) => match encoder.finish() { + Ok(writer) => Ok(writer.buf.freeze()), + Err(err) => Err(err), + }, + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentEncoder::Deflate(encoder) => match encoder.finish() { + Ok(writer) => Ok(writer.buf.freeze()), + Err(err) => Err(err), + }, + } + } + + fn write(&mut self, data: &[u8]) -> Result { + match *self { + #[cfg(feature = "brotli")] + ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { + Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), + Err(err) => { + trace!("Error decoding br encoding: {}", err); + Err(err) + } + }, + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { + Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), + Err(err) => { + trace!("Error decoding gzip encoding: {}", err); + Err(err) + } + }, + #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { + Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), + Err(err) => { + trace!("Error decoding deflate encoding: {}", err); + Err(err) + } + }, + } + } +} diff --git a/actix-http/src/encoding/mod.rs b/actix-http/src/encoding/mod.rs new file mode 100644 index 000000000..b55a43a7c --- /dev/null +++ b/actix-http/src/encoding/mod.rs @@ -0,0 +1,35 @@ +//! Content-Encoding support +use std::io; + +use bytes::{Bytes, BytesMut}; + +mod decoder; +mod encoder; + +pub use self::decoder::Decoder; +pub use self::encoder::Encoder; + +pub(self) struct Writer { + buf: BytesMut, +} + +impl Writer { + fn new() -> Writer { + Writer { + buf: BytesMut::with_capacity(8192), + } + } + fn take(&mut self) -> Bytes { + self.buf.take().freeze() + } +} + +impl io::Write for Writer { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.buf.extend_from_slice(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index b41ce7ae8..edc06c2a6 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -12,6 +12,7 @@ pub mod body; mod builder; pub mod client; mod config; +pub mod encoding; mod extensions; mod header; mod helpers; diff --git a/src/app.rs b/src/app.rs index f46f5252f..b8efdd38b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -193,10 +193,10 @@ where } /// Register a request modifier. It can modify any request parameters - /// including payload stream type. + /// including request payload type. pub fn chain( self, - chain: C, + chain: F, ) -> App< P1, impl NewService< diff --git a/src/app_service.rs b/src/app_service.rs index 0bf3d3095..236eed9f9 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -380,7 +380,7 @@ impl

    Service for AppRouting

    { } else if let Some(ref mut default) = self.default { Either::A(default.call(req)) } else { - let req = req.into_request(); + let req = req.into_parts().0; Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) } } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 3c4718fed..5ffe9afb1 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -1,25 +1,14 @@ -/// `Middleware` for compressing response body. -use std::io::Write; +//! `Middleware` for compressing response body. +use std::cmp; use std::marker::PhantomData; use std::str::FromStr; -use std::{cmp, fmt, io}; -use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; -use actix_http::http::header::{ - ContentEncoding, HeaderValue, ACCEPT_ENCODING, CONTENT_ENCODING, -}; -use actix_http::http::{HttpTryFrom, StatusCode}; -use actix_http::{Error, Head, ResponseHead}; +use actix_http::body::MessageBody; +use actix_http::encoding::Encoder; +use actix_http::http::header::{ContentEncoding, ACCEPT_ENCODING}; use actix_service::{Service, Transform}; -use bytes::{Bytes, BytesMut}; use futures::future::{ok, FutureResult}; use futures::{Async, Future, Poll}; -use log::trace; - -#[cfg(feature = "brotli")] -use brotli2::write::BrotliEncoder; -#[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] -use flate2::write::{GzEncoder, ZlibEncoder}; use crate::service::{ServiceRequest, ServiceResponse}; @@ -130,266 +119,11 @@ where let resp = futures::try_ready!(self.fut.poll()); Ok(Async::Ready(resp.map_body(move |head, body| { - Encoder::body(self.encoding, head, body) + Encoder::response(self.encoding, head, body) }))) } } -enum EncoderBody { - Body(B), - Other(Box), -} - -pub struct Encoder { - body: EncoderBody, - encoder: Option, -} - -impl MessageBody for Encoder { - fn length(&self) -> BodyLength { - if self.encoder.is_none() { - match self.body { - EncoderBody::Body(ref b) => b.length(), - EncoderBody::Other(ref b) => b.length(), - } - } else { - BodyLength::Stream - } - } - - fn poll_next(&mut self) -> Poll, Error> { - loop { - let result = match self.body { - EncoderBody::Body(ref mut b) => b.poll_next()?, - EncoderBody::Other(ref mut b) => b.poll_next()?, - }; - match result { - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(Some(chunk)) => { - if let Some(ref mut encoder) = self.encoder { - if encoder.write(&chunk)? { - return Ok(Async::Ready(Some(encoder.take()))); - } - } else { - return Ok(Async::Ready(Some(chunk))); - } - } - Async::Ready(None) => { - if let Some(encoder) = self.encoder.take() { - let chunk = encoder.finish()?; - if chunk.is_empty() { - return Ok(Async::Ready(None)); - } else { - return Ok(Async::Ready(Some(chunk))); - } - } else { - return Ok(Async::Ready(None)); - } - } - } - } - } -} - -fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { - head.headers_mut().insert( - CONTENT_ENCODING, - HeaderValue::try_from(Bytes::from_static(encoding.as_str().as_bytes())).unwrap(), - ); -} - -impl Encoder { - fn body( - encoding: ContentEncoding, - head: &mut ResponseHead, - body: ResponseBody, - ) -> ResponseBody> { - let has_ce = head.headers().contains_key(CONTENT_ENCODING); - match body { - ResponseBody::Other(b) => match b { - Body::None => ResponseBody::Other(Body::None), - Body::Empty => ResponseBody::Other(Body::Empty), - Body::Bytes(buf) => { - if !(has_ce - || encoding == ContentEncoding::Identity - || encoding == ContentEncoding::Auto) - { - let mut enc = ContentEncoder::encoder(encoding).unwrap(); - - // TODO return error! - let _ = enc.write(buf.as_ref()); - let body = enc.finish().unwrap(); - update_head(encoding, head); - ResponseBody::Other(Body::Bytes(body)) - } else { - ResponseBody::Other(Body::Bytes(buf)) - } - } - Body::Message(stream) => { - if has_ce || head.status == StatusCode::SWITCHING_PROTOCOLS { - ResponseBody::Body(Encoder { - body: EncoderBody::Other(stream), - encoder: None, - }) - } else { - update_head(encoding, head); - head.no_chunking = false; - ResponseBody::Body(Encoder { - body: EncoderBody::Other(stream), - encoder: ContentEncoder::encoder(encoding), - }) - } - } - }, - ResponseBody::Body(stream) => { - if has_ce || head.status == StatusCode::SWITCHING_PROTOCOLS { - ResponseBody::Body(Encoder { - body: EncoderBody::Body(stream), - encoder: None, - }) - } else { - update_head(encoding, head); - head.no_chunking = false; - ResponseBody::Body(Encoder { - body: EncoderBody::Body(stream), - encoder: ContentEncoder::encoder(encoding), - }) - } - } - } - } -} - -pub(crate) struct Writer { - buf: BytesMut, -} - -impl Writer { - fn new() -> Writer { - Writer { - buf: BytesMut::with_capacity(8192), - } - } - fn take(&mut self) -> Bytes { - self.buf.take().freeze() - } -} - -impl io::Write for Writer { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.buf.extend_from_slice(buf); - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -pub(crate) enum ContentEncoder { - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] - Deflate(ZlibEncoder), - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] - Gzip(GzEncoder), - #[cfg(feature = "brotli")] - Br(BrotliEncoder), -} - -impl fmt::Debug for ContentEncoder { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(_) => writeln!(f, "ContentEncoder(Brotli)"), - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] - ContentEncoder::Deflate(_) => writeln!(f, "ContentEncoder(Deflate)"), - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] - ContentEncoder::Gzip(_) => writeln!(f, "ContentEncoder(Gzip)"), - } - } -} - -impl ContentEncoder { - fn encoder(encoding: ContentEncoding) -> Option { - match encoding { - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] - ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new( - Writer::new(), - flate2::Compression::fast(), - ))), - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] - ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( - Writer::new(), - flate2::Compression::fast(), - ))), - #[cfg(feature = "brotli")] - ContentEncoding::Br => { - Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) - } - _ => None, - } - } - - #[inline] - pub(crate) fn take(&mut self) -> Bytes { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] - ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] - ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), - } - } - - fn finish(self) -> Result { - match self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(encoder) => match encoder.finish() { - Ok(writer) => Ok(writer.buf.freeze()), - Err(err) => Err(err), - }, - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] - ContentEncoder::Gzip(encoder) => match encoder.finish() { - Ok(writer) => Ok(writer.buf.freeze()), - Err(err) => Err(err), - }, - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] - ContentEncoder::Deflate(encoder) => match encoder.finish() { - Ok(writer) => Ok(writer.buf.freeze()), - Err(err) => Err(err), - }, - } - } - - fn write(&mut self, data: &[u8]) -> Result { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { - Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), - Err(err) => { - trace!("Error decoding br encoding: {}", err); - Err(err) - } - }, - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] - ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { - Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), - Err(err) => { - trace!("Error decoding gzip encoding: {}", err); - Err(err) - } - }, - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] - ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { - Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), - Err(err) => { - trace!("Error decoding deflate encoding: {}", err); - Err(err) - } - }, - } - } -} - struct AcceptEncoding { encoding: ContentEncoding, quality: f64, diff --git a/src/middleware/decompress.rs b/src/middleware/decompress.rs new file mode 100644 index 000000000..d0a9bfd24 --- /dev/null +++ b/src/middleware/decompress.rs @@ -0,0 +1,60 @@ +//! Chain service for decompressing request payload. +use std::marker::PhantomData; + +use actix_http::encoding::Decoder; +use actix_service::{NewService, Service}; +use bytes::Bytes; +use futures::future::{ok, FutureResult}; +use futures::{Async, Poll, Stream}; + +use crate::dev::Payload; +use crate::error::{Error, PayloadError}; +use crate::service::ServiceRequest; +use crate::HttpMessage; + +pub struct Decompress

    (PhantomData

    ); + +impl

    Decompress

    +where + P: Stream, +{ + pub fn new() -> Self { + Decompress(PhantomData) + } +} + +impl

    NewService for Decompress

    +where + P: Stream, +{ + type Request = ServiceRequest

    ; + type Response = ServiceRequest>>; + type Error = Error; + type InitError = (); + type Service = Decompress

    ; + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(Decompress(PhantomData)) + } +} + +impl

    Service for Decompress

    +where + P: Stream, +{ + type Request = ServiceRequest

    ; + type Response = ServiceRequest>>; + type Error = Error; + type Future = FutureResult; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { + let (req, payload) = req.into_parts(); + let payload = Decoder::from_headers(req.headers(), payload); + ok(ServiceRequest::from_parts(req, Payload::Stream(payload))) + } +} diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 998b59052..764cd9a3d 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -4,6 +4,11 @@ mod compress; #[cfg(any(feature = "brotli", feature = "flate2"))] pub use self::compress::Compress; +#[cfg(any(feature = "brotli", feature = "flate2"))] +mod decompress; +#[cfg(any(feature = "brotli", feature = "flate2"))] +pub use self::decompress::Decompress; + pub mod cors; mod defaultheaders; pub mod errhandlers; diff --git a/src/resource.rs b/src/resource.rs index 55237157f..b24e8dd51 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -507,7 +507,7 @@ impl

    Service for ResourceService

    { if let Some(ref mut default) = self.default { Either::B(Either::A(default.call(req))) } else { - let req = req.into_request(); + let req = req.into_parts().0; Either::B(Either::B(ok(ServiceResponse::new( req, Response::MethodNotAllowed().finish(), diff --git a/src/scope.rs b/src/scope.rs index 8c72824f4..d45609c5e 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -489,7 +489,7 @@ impl

    Service for ScopeService

    { } else if let Some(ref mut default) = self.default { Either::A(default.call(req)) } else { - let req = req.into_request(); + let req = req.into_parts().0; Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) } } diff --git a/src/service.rs b/src/service.rs index b8c3a1584..5a0422089 100644 --- a/src/service.rs +++ b/src/service.rs @@ -69,9 +69,14 @@ impl

    ServiceRequest

    { } } - #[inline] - pub fn into_request(self) -> HttpRequest { - self.req + /// Construct service request from parts + pub fn from_parts(req: HttpRequest, payload: Payload

    ) -> Self { + ServiceRequest { req, payload } + } + + /// Deconstruct request into parts + pub fn into_parts(self) -> (HttpRequest, Payload

    ) { + (self.req, self.payload) } /// Create service response @@ -162,11 +167,6 @@ impl

    ServiceRequest

    { pub fn app_config(&self) -> &AppConfig { self.req.config() } - - /// Deconstruct request into parts - pub fn into_parts(self) -> (HttpRequest, Payload

    ) { - (self.req, self.payload) - } } impl

    Resource for ServiceRequest

    { diff --git a/src/test.rs b/src/test.rs index c5936ea35..9e1f01f90 100644 --- a/src/test.rs +++ b/src/test.rs @@ -350,7 +350,8 @@ impl TestRequest { Rc::new(self.rmap), AppConfig::new(self.config), ) - .into_request() + .into_parts() + .0 } /// Complete request creation and generate `ServiceFromRequest` instance diff --git a/tests/test_server.rs b/tests/test_server.rs index 9c0f1f655..acea029c6 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1,14 +1,16 @@ use std::io::{Read, Write}; use actix_http::http::header::{ - ContentEncoding, ACCEPT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING, + ContentEncoding, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, + TRANSFER_ENCODING, }; -use actix_http::{h1, Error, Response}; +use actix_http::{h1, Error, HttpService, Response}; use actix_http_test::TestServer; -use brotli2::write::BrotliDecoder; +use brotli2::write::{BrotliDecoder, BrotliEncoder}; use bytes::Bytes; use flate2::read::GzDecoder; -use flate2::write::ZlibDecoder; +use flate2::write::{GzEncoder, ZlibDecoder, ZlibEncoder}; +use flate2::Compression; use futures::stream::once; //Future, Stream use rand::{distributions::Alphanumeric, Rng}; @@ -297,278 +299,246 @@ fn test_body_brotli() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } -// #[test] -// fn test_gzip_encoding() { -// let mut srv = test::TestServer::new(|app| { -// app.handler(|req: &HttpRequest| { -// req.body() -// .and_then(|bytes: Bytes| { -// Ok(HttpResponse::Ok() -// .content_encoding(http::ContentEncoding::Identity) -// .body(bytes)) -// }) -// .responder() -// }) -// }); +#[test] +fn test_gzip_encoding() { + let mut srv = TestServer::new(move || { + HttpService::new( + App::new().chain(middleware::Decompress::new()).service( + web::resource("/") + .route(web::to(move |body: Bytes| Response::Ok().body(body))), + ), + ) + }); -// // client request -// let mut e = GzEncoder::new(Vec::new(), Compression::default()); -// e.write_all(STR.as_ref()).unwrap(); -// let enc = e.finish().unwrap(); + // client request + let mut e = GzEncoder::new(Vec::new(), Compression::default()); + e.write_all(STR.as_ref()).unwrap(); + let enc = e.finish().unwrap(); -// let request = srv -// .post() -// .header(http::header::CONTENT_ENCODING, "gzip") -// .body(enc.clone()) -// .unwrap(); -// let response = srv.block_on(request.send()).unwrap(); -// assert!(response.status().is_success()); + let request = srv + .post() + .header(CONTENT_ENCODING, "gzip") + .send_body(enc.clone()); + let mut response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); -// // read response -// let bytes = srv.block_on(response.body()).unwrap(); -// assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -// } + // read response + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} -// #[test] -// fn test_gzip_encoding_large() { -// let data = STR.repeat(10); -// let mut srv = test::TestServer::new(|app| { -// app.handler(|req: &HttpRequest| { -// req.body() -// .and_then(|bytes: Bytes| { -// Ok(HttpResponse::Ok() -// .content_encoding(http::ContentEncoding::Identity) -// .body(bytes)) -// }) -// .responder() -// }) -// }); +#[test] +fn test_gzip_encoding_large() { + let data = STR.repeat(10); + let mut srv = TestServer::new(move || { + h1::H1Service::new( + App::new().chain(middleware::Decompress::new()).service( + web::resource("/") + .route(web::to(move |body: Bytes| Response::Ok().body(body))), + ), + ) + }); -// // client request -// let mut e = GzEncoder::new(Vec::new(), Compression::default()); -// e.write_all(data.as_ref()).unwrap(); -// let enc = e.finish().unwrap(); + // client request + let mut e = GzEncoder::new(Vec::new(), Compression::default()); + e.write_all(data.as_ref()).unwrap(); + let enc = e.finish().unwrap(); -// let request = srv -// .post() -// .header(http::header::CONTENT_ENCODING, "gzip") -// .body(enc.clone()) -// .unwrap(); -// let response = srv.block_on(request.send()).unwrap(); -// assert!(response.status().is_success()); + let request = srv + .post() + .header(CONTENT_ENCODING, "gzip") + .send_body(enc.clone()); + let mut response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); -// // read response -// let bytes = srv.block_on(response.body()).unwrap(); -// assert_eq!(bytes, Bytes::from(data)); -// } + // read response + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + assert_eq!(bytes, Bytes::from(data)); +} -// #[test] -// fn test_reading_gzip_encoding_large_random() { -// let data = rand::thread_rng() -// .sample_iter(&Alphanumeric) -// .take(60_000) -// .collect::(); +#[test] +fn test_reading_gzip_encoding_large_random() { + let data = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(60_000) + .collect::(); -// let mut srv = test::TestServer::new(|app| { -// app.handler(|req: &HttpRequest| { -// req.body() -// .and_then(|bytes: Bytes| { -// Ok(HttpResponse::Ok() -// .content_encoding(http::ContentEncoding::Identity) -// .body(bytes)) -// }) -// .responder() -// }) -// }); + let mut srv = TestServer::new(move || { + HttpService::new( + App::new().chain(middleware::Decompress::new()).service( + web::resource("/") + .route(web::to(move |body: Bytes| Response::Ok().body(body))), + ), + ) + }); -// // client request -// let mut e = GzEncoder::new(Vec::new(), Compression::default()); -// e.write_all(data.as_ref()).unwrap(); -// let enc = e.finish().unwrap(); + // client request + let mut e = GzEncoder::new(Vec::new(), Compression::default()); + e.write_all(data.as_ref()).unwrap(); + let enc = e.finish().unwrap(); -// let request = srv -// .post() -// .header(http::header::CONTENT_ENCODING, "gzip") -// .body(enc.clone()) -// .unwrap(); -// let response = srv.block_on(request.send()).unwrap(); -// assert!(response.status().is_success()); + let request = srv + .post() + .header(CONTENT_ENCODING, "gzip") + .send_body(enc.clone()); + let mut response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); -// // read response -// let bytes = srv.block_on(response.body()).unwrap(); -// assert_eq!(bytes.len(), data.len()); -// assert_eq!(bytes, Bytes::from(data)); -// } + // read response + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + assert_eq!(bytes.len(), data.len()); + assert_eq!(bytes, Bytes::from(data)); +} -// #[test] -// fn test_reading_deflate_encoding() { -// let mut srv = test::TestServer::new(|app| { -// app.handler(|req: &HttpRequest| { -// req.body() -// .and_then(|bytes: Bytes| { -// Ok(HttpResponse::Ok() -// .content_encoding(http::ContentEncoding::Identity) -// .body(bytes)) -// }) -// .responder() -// }) -// }); +#[test] +fn test_reading_deflate_encoding() { + let mut srv = TestServer::new(move || { + h1::H1Service::new( + App::new().chain(middleware::Decompress::new()).service( + web::resource("/") + .route(web::to(move |body: Bytes| Response::Ok().body(body))), + ), + ) + }); -// let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); -// e.write_all(STR.as_ref()).unwrap(); -// let enc = e.finish().unwrap(); + let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); + e.write_all(STR.as_ref()).unwrap(); + let enc = e.finish().unwrap(); -// // client request -// let request = srv -// .post() -// .header(http::header::CONTENT_ENCODING, "deflate") -// .body(enc) -// .unwrap(); -// let response = srv.block_on(request.send()).unwrap(); -// assert!(response.status().is_success()); + // client request + let request = srv + .post() + .header(CONTENT_ENCODING, "deflate") + .send_body(enc.clone()); + let mut response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); -// // read response -// let bytes = srv.block_on(response.body()).unwrap(); -// assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -// } + // read response + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} -// #[test] -// fn test_reading_deflate_encoding_large() { -// let data = STR.repeat(10); -// let mut srv = test::TestServer::new(|app| { -// app.handler(|req: &HttpRequest| { -// req.body() -// .and_then(|bytes: Bytes| { -// Ok(HttpResponse::Ok() -// .content_encoding(http::ContentEncoding::Identity) -// .body(bytes)) -// }) -// .responder() -// }) -// }); +#[test] +fn test_reading_deflate_encoding_large() { + let data = STR.repeat(10); + let mut srv = TestServer::new(move || { + h1::H1Service::new( + App::new().chain(middleware::Decompress::new()).service( + web::resource("/") + .route(web::to(move |body: Bytes| Response::Ok().body(body))), + ), + ) + }); -// let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); -// e.write_all(data.as_ref()).unwrap(); -// let enc = e.finish().unwrap(); + let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); + e.write_all(data.as_ref()).unwrap(); + let enc = e.finish().unwrap(); -// // client request -// let request = srv -// .post() -// .header(http::header::CONTENT_ENCODING, "deflate") -// .body(enc) -// .unwrap(); -// let response = srv.block_on(request.send()).unwrap(); -// assert!(response.status().is_success()); + // client request + let request = srv + .post() + .header(CONTENT_ENCODING, "deflate") + .send_body(enc.clone()); + let mut response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); -// // read response -// let bytes = srv.block_on(response.body()).unwrap(); -// assert_eq!(bytes, Bytes::from(data)); -// } + // read response + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + assert_eq!(bytes, Bytes::from(data)); +} -// #[test] -// fn test_reading_deflate_encoding_large_random() { -// let data = rand::thread_rng() -// .sample_iter(&Alphanumeric) -// .take(160_000) -// .collect::(); +#[test] +fn test_reading_deflate_encoding_large_random() { + let data = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(160_000) + .collect::(); -// let mut srv = test::TestServer::new(|app| { -// app.handler(|req: &HttpRequest| { -// req.body() -// .and_then(|bytes: Bytes| { -// Ok(HttpResponse::Ok() -// .content_encoding(http::ContentEncoding::Identity) -// .body(bytes)) -// }) -// .responder() -// }) -// }); + let mut srv = TestServer::new(move || { + h1::H1Service::new( + App::new().chain(middleware::Decompress::new()).service( + web::resource("/") + .route(web::to(move |body: Bytes| Response::Ok().body(body))), + ), + ) + }); -// let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); -// e.write_all(data.as_ref()).unwrap(); -// let enc = e.finish().unwrap(); + let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); + e.write_all(data.as_ref()).unwrap(); + let enc = e.finish().unwrap(); -// // client request -// let request = srv -// .post() -// .header(http::header::CONTENT_ENCODING, "deflate") -// .body(enc) -// .unwrap(); -// let response = srv.block_on(request.send()).unwrap(); -// assert!(response.status().is_success()); + // client request + let request = srv + .post() + .header(CONTENT_ENCODING, "deflate") + .send_body(enc.clone()); + let mut response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); -// // read response -// let bytes = srv.execute(response.body()).unwrap(); -// assert_eq!(bytes.len(), data.len()); -// assert_eq!(bytes, Bytes::from(data)); -// } + // read response + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + assert_eq!(bytes.len(), data.len()); + assert_eq!(bytes, Bytes::from(data)); +} -// #[cfg(feature = "brotli")] -// #[test] -// fn test_brotli_encoding() { -// let mut srv = test::TestServer::new(|app| { -// app.handler(|req: &HttpRequest| { -// req.body() -// .and_then(|bytes: Bytes| { -// Ok(HttpResponse::Ok() -// .content_encoding(http::ContentEncoding::Identity) -// .body(bytes)) -// }) -// .responder() -// }) -// }); +#[cfg(feature = "brotli")] +#[test] +fn test_brotli_encoding() { + let mut srv = TestServer::new(move || { + h1::H1Service::new( + App::new().chain(middleware::Decompress::new()).service( + web::resource("/") + .route(web::to(move |body: Bytes| Response::Ok().body(body))), + ), + ) + }); -// let mut e = BrotliEncoder::new(Vec::new(), 5); -// e.write_all(STR.as_ref()).unwrap(); -// let enc = e.finish().unwrap(); + let mut e = BrotliEncoder::new(Vec::new(), 5); + e.write_all(STR.as_ref()).unwrap(); + let enc = e.finish().unwrap(); -// // client request -// let request = srv -// .post() -// .header(http::header::CONTENT_ENCODING, "br") -// .body(enc) -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert!(response.status().is_success()); + // client request + let request = srv + .post() + .header(CONTENT_ENCODING, "br") + .send_body(enc.clone()); + let mut response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); -// // read response -// let bytes = srv.execute(response.body()).unwrap(); -// assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -// } + // read response + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} -// #[cfg(feature = "brotli")] -// #[test] -// fn test_brotli_encoding_large() { -// let data = STR.repeat(10); -// let mut srv = test::TestServer::new(|app| { -// app.handler(|req: &HttpRequest| { -// req.body() -// .and_then(|bytes: Bytes| { -// Ok(HttpResponse::Ok() -// .content_encoding(http::ContentEncoding::Identity) -// .body(bytes)) -// }) -// .responder() -// }) -// }); +#[cfg(feature = "brotli")] +#[test] +fn test_brotli_encoding_large() { + let data = STR.repeat(10); + let mut srv = TestServer::new(move || { + h1::H1Service::new( + App::new().chain(middleware::Decompress::new()).service( + web::resource("/") + .route(web::to(move |body: Bytes| Response::Ok().body(body))), + ), + ) + }); -// let mut e = BrotliEncoder::new(Vec::new(), 5); -// e.write_all(data.as_ref()).unwrap(); -// let enc = e.finish().unwrap(); + let mut e = BrotliEncoder::new(Vec::new(), 5); + e.write_all(data.as_ref()).unwrap(); + let enc = e.finish().unwrap(); -// // client request -// let request = srv -// .post() -// .header(http::header::CONTENT_ENCODING, "br") -// .body(enc) -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert!(response.status().is_success()); + // client request + let request = srv + .post() + .header(CONTENT_ENCODING, "br") + .send_body(enc.clone()); + let mut response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); -// // read response -// let bytes = srv.execute(response.body()).unwrap(); -// assert_eq!(bytes, Bytes::from(data)); -// } + // read response + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + assert_eq!(bytes, Bytes::from(data)); +} // #[cfg(all(feature = "brotli", feature = "ssl"))] // #[test] From 2629699b6206997f57872553354c0020adc768c9 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Mar 2019 18:46:06 -0700 Subject: [PATCH 350/427] rename flate2-c feature to flate2-zlib --- Cargo.toml | 8 ++++---- actix-http/Cargo.toml | 2 +- actix-http/src/encoding/decoder.rs | 24 ++++++++++++++---------- actix-http/src/encoding/encoder.rs | 22 +++++++++++----------- awc/Cargo.toml | 14 +++++++++++++- src/lib.rs | 2 +- src/middleware/mod.rs | 8 ++++---- 7 files changed, 48 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 22c2efe99..363989bf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,10 +36,10 @@ members = [ ] [package.metadata.docs.rs] -features = ["ssl", "tls", "rust-tls", "brotli", "flate2-c", "cookies", "client"] +features = ["ssl", "tls", "rust-tls", "brotli", "flate2-zlib", "cookies", "client"] [features] -default = ["brotli", "flate2-c", "cookies", "client"] +default = ["brotli", "flate2-zlib", "cookies", "client"] # http client client = ["awc"] @@ -48,7 +48,7 @@ client = ["awc"] brotli = ["actix-http/brotli2"] # miniz-sys backend for flate2 crate -flate2-c = ["actix-http/flate2-c"] +flate2-zlib = ["actix-http/flate2-zlib"] # rust backend for flate2 crate flate2-rust = ["actix-http/flate2-rust"] @@ -102,7 +102,7 @@ openssl = { version="0.10", optional = true } # rustls = { version = "^0.15", optional = true } [dev-dependencies] -actix-http = { path = "actix-http", features=["ssl", "brotli", "flate2-c"] } +actix-http = { path = "actix-http", features=["ssl", "brotli", "flate2-zlib"] } actix-http-test = { path = "test-server", features=["ssl"] } rand = "0.6" env_logger = "0.6" diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 7b73e7e26..427024e24 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -40,7 +40,7 @@ cookies = ["cookie"] brotli = ["brotli2"] # miniz-sys backend for flate2 crate -flate2-c = ["flate2/miniz-sys"] +flate2-zlib = ["flate2/miniz-sys"] # rust backend for flate2 crate flate2-rust = ["flate2/rust_backend"] diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index a922d1738..b4246c64d 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -5,7 +5,7 @@ use futures::{Async, Poll, Stream}; #[cfg(feature = "brotli")] use brotli2::write::BrotliDecoder; -#[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] use flate2::write::{GzDecoder, ZlibDecoder}; use super::Writer; @@ -27,11 +27,11 @@ where ContentEncoding::Br => Some(ContentDecoder::Br(Box::new( BrotliDecoder::new(Writer::new()), ))), - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ZlibDecoder::new(Writer::new()), ))), - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new( GzDecoder::new(Writer::new()), ))), @@ -95,15 +95,16 @@ where } enum ContentDecoder { - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] Deflate(Box>), - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] Gzip(Box>), #[cfg(feature = "brotli")] Br(Box>), } impl ContentDecoder { + #[allow(unreachable_patterns)] fn feed_eof(&mut self) -> io::Result> { match self { #[cfg(feature = "brotli")] @@ -118,7 +119,7 @@ impl ContentDecoder { } Err(e) => Err(e), }, - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() { Ok(_) => { let b = decoder.get_mut().take(); @@ -130,7 +131,7 @@ impl ContentDecoder { } Err(e) => Err(e), }, - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() { Ok(_) => { let b = decoder.get_mut().take(); @@ -142,12 +143,14 @@ impl ContentDecoder { } Err(e) => Err(e), }, + _ => Ok(None), } } + #[allow(unreachable_patterns)] fn feed_data(&mut self, data: Bytes) -> io::Result> { match self { - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; @@ -160,7 +163,7 @@ impl ContentDecoder { } Err(e) => Err(e), }, - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; @@ -173,7 +176,7 @@ impl ContentDecoder { } Err(e) => Err(e), }, - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; @@ -186,6 +189,7 @@ impl ContentDecoder { } Err(e) => Err(e), }, + _ => Ok(Some(data)), } } } diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 1985dcdf2..0778cc262 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -6,7 +6,7 @@ use futures::{Async, Poll}; #[cfg(feature = "brotli")] use brotli2::write::BrotliEncoder; -#[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] use flate2::write::{GzEncoder, ZlibEncoder}; use crate::body::{Body, BodyLength, MessageBody, ResponseBody}; @@ -142,9 +142,9 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { } enum ContentEncoder { - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] Deflate(ZlibEncoder), - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] Gzip(GzEncoder), #[cfg(feature = "brotli")] Br(BrotliEncoder), @@ -153,12 +153,12 @@ enum ContentEncoder { impl ContentEncoder { fn encoder(encoding: ContentEncoding) -> Option { match encoding { - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new( Writer::new(), flate2::Compression::fast(), ))), - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( Writer::new(), flate2::Compression::fast(), @@ -176,9 +176,9 @@ impl ContentEncoder { match *self { #[cfg(feature = "brotli")] ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), } } @@ -190,12 +190,12 @@ impl ContentEncoder { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentEncoder::Gzip(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentEncoder::Deflate(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), @@ -213,7 +213,7 @@ impl ContentEncoder { Err(err) } }, - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), Err(err) => { @@ -221,7 +221,7 @@ impl ContentEncoder { Err(err) } }, - #[cfg(any(feature = "flate2-c", feature = "flate2-rust"))] + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), Err(err) => { diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 023bd088b..72b72d369 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -17,8 +17,11 @@ edition = "2018" name = "awc" path = "src/lib.rs" +[package.metadata.docs.rs] +features = ["ssl", "brotli", "flate2-zlib", "cookies"] + [features] -default = ["cookies"] +default = ["cookies", "brotli", "flate2-zlib"] # openssl ssl = ["openssl", "actix-http/ssl"] @@ -26,6 +29,15 @@ ssl = ["openssl", "actix-http/ssl"] # cookies integration cookies = ["cookie", "actix-http/cookies"] +# brotli encoding, requires c compiler +brotli = ["actix-http/brotli2"] + +# miniz-sys backend for flate2 crate +flate2-zlib = ["actix-http/flate2-zlib"] + +# rust backend for flate2 crate +flate2-rust = ["actix-http/flate2-rust"] + [dependencies] actix-service = "0.3.4" actix-http = { path = "../actix-http/" } diff --git a/src/lib.rs b/src/lib.rs index 1bf29213c..a21032db6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,7 +71,7 @@ //! dependency //! * `brotli` - enables `brotli` compression support, requires `c` //! compiler -//! * `flate2-c` - enables `gzip`, `deflate` compression support, requires +//! * `flate2-zlib` - enables `gzip`, `deflate` compression support, requires //! `c` compiler //! * `flate2-rust` - experimental rust based implementation for //! `gzip`, `deflate` compression. diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 764cd9a3d..aee0ae3df 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1,12 +1,12 @@ //! Middlewares -#[cfg(any(feature = "brotli", feature = "flate2"))] +#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] mod compress; -#[cfg(any(feature = "brotli", feature = "flate2"))] +#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] pub use self::compress::Compress; -#[cfg(any(feature = "brotli", feature = "flate2"))] +#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] mod decompress; -#[cfg(any(feature = "brotli", feature = "flate2"))] +#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] pub use self::decompress::Decompress; pub mod cors; From 1cca25c27631f023d1fc844c83250321e7f470ae Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Mar 2019 20:45:00 -0700 Subject: [PATCH 351/427] add client decompression support --- actix-http/src/encoding/decoder.rs | 4 +- awc/Cargo.toml | 10 +- awc/src/lib.rs | 1 + awc/src/request.rs | 149 ++++++--- awc/src/response.rs | 146 ++++++++- awc/tests/test_client.rs | 508 +++++++++++++++++++++++++++++ tests/test_server.rs | 24 +- 7 files changed, 775 insertions(+), 67 deletions(-) create mode 100644 awc/tests/test_client.rs diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index b4246c64d..8be6702fc 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -74,7 +74,7 @@ where Err(e) => return Err(e.into()), } } else { - break; + return Ok(Async::Ready(Some(chunk))); } } Async::Ready(None) => { @@ -150,7 +150,7 @@ impl ContentDecoder { #[allow(unreachable_patterns)] fn feed_data(&mut self, data: Bytes) -> io::Result> { match self { - #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] + #[cfg(feature = "brotli")] ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 72b72d369..88c3be421 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -30,7 +30,7 @@ ssl = ["openssl", "actix-http/ssl"] cookies = ["cookie", "actix-http/cookies"] # brotli encoding, requires c compiler -brotli = ["actix-http/brotli2"] +brotli = ["actix-http/brotli"] # miniz-sys backend for flate2 crate flate2-zlib = ["actix-http/flate2-zlib"] @@ -53,8 +53,12 @@ cookie = { version="0.11", features=["percent-encode"], optional = true } openssl = { version="0.10", optional = true } [dev-dependencies] -env_logger = "0.6" -mime = "0.3" actix-rt = "0.2.1" +actix-web = { path = "..", features=["ssl"] } actix-http = { path = "../actix-http/", features=["ssl"] } actix-http-test = { path = "../test-server/", features=["ssl"] } +brotli2 = { version="^0.3.2" } +flate2 = { version="^1.0.2" } +env_logger = "0.6" +mime = "0.3" +rand = "0.6" diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 89acf7d58..4898a0627 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -2,6 +2,7 @@ use std::cell::RefCell; use std::rc::Rc; pub use actix_http::client::{ConnectError, InvalidUrl, SendRequestError}; +pub use actix_http::error::PayloadError; pub use actix_http::http; use actix_http::client::Connector; diff --git a/awc/src/request.rs b/awc/src/request.rs index f23aa7ef9..90f9a1ab9 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -13,15 +13,24 @@ use serde_json; use actix_http::body::{Body, BodyStream}; use actix_http::client::{InvalidUrl, SendRequestError}; -use actix_http::http::header::{self, Header, IntoHeaderValue}; +use actix_http::encoding::Decoder; +use actix_http::http::header::{self, ContentEncoding, Header, IntoHeaderValue}; use actix_http::http::{ uri, ConnectionType, Error as HttpError, HeaderName, HeaderValue, HttpTryFrom, Method, Uri, Version, }; -use actix_http::{Error, Head, RequestHead}; +use actix_http::{Error, Head, Payload, RequestHead}; use crate::response::ClientResponse; -use crate::Connect; +use crate::{Connect, PayloadError}; + +#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] +const HTTPS_ENCODING: &str = "br, gzip, deflate"; +#[cfg(all( + any(feature = "flate2-zlib", feature = "flate2-rust"), + not(feature = "brotli") +))] +const HTTPS_ENCODING: &str = "gzip, deflate"; /// An HTTP Client request builder /// @@ -52,6 +61,7 @@ pub struct ClientRequest { #[cfg(feature = "cookies")] cookies: Option, default_headers: bool, + response_decompress: bool, connector: Rc>, } @@ -81,6 +91,7 @@ impl ClientRequest { #[cfg(feature = "cookies")] cookies: None, default_headers: true, + response_decompress: true, } } @@ -275,6 +286,12 @@ impl ClientRequest { self } + /// Disable automatic decompress of response's body + pub fn no_decompress(mut self) -> Self { + self.response_decompress = false; + self + } + /// This method calls provided closure with builder reference if /// value is `true`. pub fn if_true(mut self, value: bool, f: F) -> Self @@ -303,7 +320,10 @@ impl ClientRequest { pub fn send_body( mut self, body: B, - ) -> impl Future + ) -> impl Future< + Item = ClientResponse>, + Error = SendRequestError, + > where B: Into, { @@ -311,42 +331,44 @@ impl ClientRequest { return Either::A(err(e.into())); } - let mut slf = if self.default_headers { - // enable br only for https - let https = self - .head - .uri - .scheme_part() - .map(|s| s == &uri::Scheme::HTTPS) - .unwrap_or(true); - - let mut slf = if https { - self.set_header_if_none(header::ACCEPT_ENCODING, "br, gzip, deflate") - } else { - self.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate") - }; + // validate uri + let uri = &self.head.uri; + if uri.host().is_none() { + return Either::A(err(InvalidUrl::MissingHost.into())); + } else if uri.scheme_part().is_none() { + return Either::A(err(InvalidUrl::MissingScheme.into())); + } else if let Some(scheme) = uri.scheme_part() { + match scheme.as_str() { + "http" | "ws" | "https" | "wss" => (), + _ => return Either::A(err(InvalidUrl::UnknownScheme.into())), + } + } else { + return Either::A(err(InvalidUrl::UnknownScheme.into())); + } + // set default headers + let slf = if self.default_headers { // set request host header - if let Some(host) = slf.head.uri.host() { - if !slf.head.headers.contains_key(header::HOST) { + if let Some(host) = self.head.uri.host() { + if !self.head.headers.contains_key(header::HOST) { let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); - let _ = match slf.head.uri.port_u16() { + let _ = match self.head.uri.port_u16() { None | Some(80) | Some(443) => write!(wrt, "{}", host), Some(port) => write!(wrt, "{}:{}", host, port), }; match wrt.get_mut().take().freeze().try_into() { Ok(value) => { - slf.head.headers.insert(header::HOST, value); + self.head.headers.insert(header::HOST, value); } - Err(e) => slf.err = Some(e.into()), + Err(e) => return Either::A(err(HttpError::from(e).into())), } } } // user agent - slf.set_header_if_none( + self.set_header_if_none( header::USER_AGENT, concat!("actix-http/", env!("CARGO_PKG_VERSION")), ) @@ -354,6 +376,32 @@ impl ClientRequest { self }; + // enable br only for https + let https = slf + .head + .uri + .scheme_part() + .map(|s| s == &uri::Scheme::HTTPS) + .unwrap_or(true); + + #[cfg(any( + feature = "brotli", + feature = "flate2-zlib", + feature = "flate2-rust" + ))] + let mut slf = { + if https { + slf.set_header_if_none(header::ACCEPT_ENCODING, HTTPS_ENCODING) + } else { + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] + { + slf.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate") + } + #[cfg(not(any(feature = "flate2-zlib", feature = "flate2-rust")))] + slf + } + }; + #[allow(unused_mut)] let mut head = slf.head; @@ -378,30 +426,32 @@ impl ClientRequest { } } - let uri = head.uri.clone(); + let response_decompress = slf.response_decompress; - // validate uri - if uri.host().is_none() { - Either::A(err(InvalidUrl::MissingHost.into())) - } else if uri.scheme_part().is_none() { - Either::A(err(InvalidUrl::MissingScheme.into())) - } else if let Some(scheme) = uri.scheme_part() { - match scheme.as_str() { - "http" | "ws" | "https" | "wss" => { - Either::B(slf.connector.borrow_mut().send_request(head, body.into())) - } - _ => Either::A(err(InvalidUrl::UnknownScheme.into())), - } - } else { - Either::A(err(InvalidUrl::UnknownScheme.into())) - } + let fut = slf + .connector + .borrow_mut() + .send_request(head, body.into()) + .map(move |res| { + res.map_body(|head, payload| { + if response_decompress { + Payload::Stream(Decoder::from_headers(&head.headers, payload)) + } else { + Payload::Stream(Decoder::new(payload, ContentEncoding::Identity)) + } + }) + }); + Either::B(fut) } /// Set a JSON body and generate `ClientRequest` pub fn send_json( self, value: T, - ) -> impl Future { + ) -> impl Future< + Item = ClientResponse>, + Error = SendRequestError, + > { let body = match serde_json::to_string(&value) { Ok(body) => body, Err(e) => return Either::A(err(Error::from(e).into())), @@ -422,7 +472,10 @@ impl ClientRequest { pub fn send_form( self, value: T, - ) -> impl Future { + ) -> impl Future< + Item = ClientResponse>, + Error = SendRequestError, + > { let body = match serde_urlencoded::to_string(&value) { Ok(body) => body, Err(e) => return Either::A(err(Error::from(e).into())), @@ -441,7 +494,10 @@ impl ClientRequest { pub fn send_stream( self, stream: S, - ) -> impl Future + ) -> impl Future< + Item = ClientResponse>, + Error = SendRequestError, + > where S: Stream + 'static, E: Into + 'static, @@ -450,7 +506,12 @@ impl ClientRequest { } /// Set an empty body and generate `ClientRequest`. - pub fn send(self) -> impl Future { + pub fn send( + self, + ) -> impl Future< + Item = ClientResponse>, + Error = SendRequestError, + > { self.send_body(Body::Empty) } } diff --git a/awc/src/response.rs b/awc/src/response.rs index 0ae66df0f..4525bbc1a 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -1,21 +1,22 @@ use std::cell::{Ref, RefMut}; use std::fmt; -use bytes::Bytes; -use futures::{Poll, Stream}; +use bytes::{Bytes, BytesMut}; +use futures::{Future, Poll, Stream}; use actix_http::error::PayloadError; +use actix_http::http::header::CONTENT_LENGTH; use actix_http::http::{HeaderMap, StatusCode, Version}; use actix_http::{Extensions, Head, HttpMessage, Payload, PayloadStream, ResponseHead}; /// Client Response -pub struct ClientResponse { +pub struct ClientResponse { pub(crate) head: ResponseHead, - pub(crate) payload: Payload, + pub(crate) payload: Payload, } -impl HttpMessage for ClientResponse { - type Stream = PayloadStream; +impl HttpMessage for ClientResponse { + type Stream = S; fn headers(&self) -> &HeaderMap { &self.head.headers @@ -29,14 +30,14 @@ impl HttpMessage for ClientResponse { self.head.extensions_mut() } - fn take_payload(&mut self) -> Payload { + fn take_payload(&mut self) -> Payload { std::mem::replace(&mut self.payload, Payload::None) } } -impl ClientResponse { +impl ClientResponse { /// Create new Request instance - pub(crate) fn new(head: ResponseHead, payload: Payload) -> ClientResponse { + pub(crate) fn new(head: ResponseHead, payload: Payload) -> Self { ClientResponse { head, payload } } @@ -79,9 +80,35 @@ impl ClientResponse { pub fn keep_alive(&self) -> bool { self.head().keep_alive() } + + /// Set a body and return previous body value + pub fn map_body(mut self, f: F) -> ClientResponse + where + F: FnOnce(&mut ResponseHead, Payload) -> Payload, + { + let payload = f(&mut self.head, self.payload); + + ClientResponse { + payload, + head: self.head, + } + } } -impl Stream for ClientResponse { +impl ClientResponse +where + S: Stream + 'static, +{ + /// Load http response's body. + pub fn body(self) -> MessageBody { + MessageBody::new(self) + } +} + +impl Stream for ClientResponse +where + S: Stream, +{ type Item = Bytes; type Error = PayloadError; @@ -90,7 +117,7 @@ impl Stream for ClientResponse { } } -impl fmt::Debug for ClientResponse { +impl fmt::Debug for ClientResponse { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status(),)?; writeln!(f, " headers:")?; @@ -100,3 +127,100 @@ impl fmt::Debug for ClientResponse { Ok(()) } } + +/// Future that resolves to a complete http message body. +pub struct MessageBody { + limit: usize, + length: Option, + stream: Option>, + err: Option, + fut: Option>>, +} + +impl MessageBody +where + S: Stream + 'static, +{ + /// Create `MessageBody` for request. + pub fn new(res: ClientResponse) -> MessageBody { + let mut len = None; + if let Some(l) = res.headers().get(CONTENT_LENGTH) { + if let Ok(s) = l.to_str() { + if let Ok(l) = s.parse::() { + len = Some(l) + } else { + return Self::err(PayloadError::UnknownLength); + } + } else { + return Self::err(PayloadError::UnknownLength); + } + } + + MessageBody { + limit: 262_144, + length: len, + stream: Some(res), + fut: None, + err: None, + } + } + + /// Change max size of payload. By default max size is 256Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + fn err(e: PayloadError) -> Self { + MessageBody { + stream: None, + limit: 262_144, + fut: None, + err: Some(e), + length: None, + } + } +} + +impl Future for MessageBody +where + S: Stream + 'static, +{ + type Item = Bytes; + type Error = PayloadError; + + fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.fut { + return fut.poll(); + } + + if let Some(err) = self.err.take() { + return Err(err); + } + + if let Some(len) = self.length.take() { + if len > self.limit { + return Err(PayloadError::Overflow); + } + } + + // future + let limit = self.limit; + self.fut = Some(Box::new( + self.stream + .take() + .expect("Can not be used second time") + .from_err() + .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { + if (body.len() + chunk.len()) > limit { + Err(PayloadError::Overflow) + } else { + body.extend_from_slice(&chunk); + Ok(body) + } + }) + .map(|body| body.freeze()), + )); + self.poll() + } +} diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs new file mode 100644 index 000000000..f7605b59c --- /dev/null +++ b/awc/tests/test_client.rs @@ -0,0 +1,508 @@ +use std::io::{Read, Write}; +use std::{net, thread}; + +use brotli2::write::BrotliEncoder; +use bytes::Bytes; +use flate2::write::{GzEncoder, ZlibEncoder}; +use flate2::Compression; +use futures::stream::once; +use futures::Future; +use rand::Rng; + +use actix_http::HttpService; +use actix_http_test::TestServer; +use actix_web::{middleware, web, App, HttpRequest, HttpResponse}; + +const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World \ + Hello World Hello World Hello World Hello World Hello World"; + +#[test] +fn test_simple() { + let mut srv = + TestServer::new(|| { + HttpService::new(App::new().service( + web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))), + )) + }); + + let request = srv.get().header("x-test", "111").send(); + let response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + let response = srv.block_on(srv.post().send()).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} + +// #[test] +// fn test_connection_close() { +// let mut srv = +// test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); + +// let request = srv.get().header("Connection", "close").finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); +// } + +// #[test] +// fn test_with_query_parameter() { +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| match req.query().get("qp") { +// Some(_) => HttpResponse::Ok().finish(), +// None => HttpResponse::BadRequest().finish(), +// }) +// }); + +// let request = srv.get().uri(srv.url("/?qp=5").as_str()).finish().unwrap(); + +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); +// } + +// #[test] +// fn test_no_decompress() { +// let mut srv = +// test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); + +// let request = srv.get().disable_decompress().finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); + +// let mut e = GzDecoder::new(&bytes[..]); +// let mut dec = Vec::new(); +// e.read_to_end(&mut dec).unwrap(); +// assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + +// // POST +// let request = srv.post().disable_decompress().finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); + +// let bytes = srv.execute(response.body()).unwrap(); +// let mut e = GzDecoder::new(&bytes[..]); +// let mut dec = Vec::new(); +// e.read_to_end(&mut dec).unwrap(); +// assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); +// } + +#[test] +fn test_client_gzip_encoding() { + let mut srv = TestServer::new(|| { + HttpService::new(App::new().service(web::resource("/").route(web::to(|| { + let mut e = GzEncoder::new(Vec::new(), Compression::default()); + e.write_all(STR.as_ref()).unwrap(); + let data = e.finish().unwrap(); + + HttpResponse::Ok() + .header("content-encoding", "gzip") + .body(data) + })))) + }); + + // client request + let response = srv.block_on(srv.post().send()).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} + +#[test] +fn test_client_gzip_encoding_large() { + let mut srv = TestServer::new(|| { + HttpService::new(App::new().service(web::resource("/").route(web::to(|| { + let mut e = GzEncoder::new(Vec::new(), Compression::default()); + e.write_all(STR.repeat(10).as_ref()).unwrap(); + let data = e.finish().unwrap(); + + HttpResponse::Ok() + .header("content-encoding", "gzip") + .body(data) + })))) + }); + + // client request + let response = srv.block_on(srv.post().send()).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from(STR.repeat(10))); +} + +#[test] +fn test_client_gzip_encoding_large_random() { + let data = rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(100_000) + .collect::(); + + let mut srv = TestServer::new(|| { + HttpService::new(App::new().service(web::resource("/").route(web::to( + |data: Bytes| { + let mut e = GzEncoder::new(Vec::new(), Compression::default()); + e.write_all(&data).unwrap(); + let data = e.finish().unwrap(); + HttpResponse::Ok() + .header("content-encoding", "gzip") + .body(data) + }, + )))) + }); + + // client request + let response = srv.block_on(srv.post().send_body(data.clone())).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from(data)); +} + +#[test] +fn test_client_brotli_encoding() { + let mut srv = TestServer::new(|| { + HttpService::new(App::new().service(web::resource("/").route(web::to( + |data: Bytes| { + let mut e = BrotliEncoder::new(Vec::new(), 5); + e.write_all(&data).unwrap(); + let data = e.finish().unwrap(); + HttpResponse::Ok() + .header("content-encoding", "br") + .body(data) + }, + )))) + }); + + // client request + let response = srv.block_on(srv.post().send_body(STR)).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} + +// #[test] +// fn test_client_brotli_encoding_large_random() { +// let data = rand::thread_rng() +// .sample_iter(&rand::distributions::Alphanumeric) +// .take(70_000) +// .collect::(); + +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(move |bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Gzip) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// // client request +// let request = srv +// .client(http::Method::POST, "/") +// .content_encoding(http::ContentEncoding::Br) +// .body(data.clone()) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes.len(), data.len()); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[cfg(feature = "brotli")] +// #[test] +// fn test_client_deflate_encoding() { +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Br) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// // client request +// let request = srv +// .post() +// .content_encoding(http::ContentEncoding::Deflate) +// .body(STR) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +// } + +// #[test] +// fn test_client_deflate_encoding_large_random() { +// let data = rand::thread_rng() +// .sample_iter(&rand::distributions::Alphanumeric) +// .take(70_000) +// .collect::(); + +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Br) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// // client request +// let request = srv +// .post() +// .content_encoding(http::ContentEncoding::Deflate) +// .body(data.clone()) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[test] +// fn test_client_streaming_explicit() { +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .map_err(Error::from) +// .and_then(|body| { +// Ok(HttpResponse::Ok() +// .chunked() +// .content_encoding(http::ContentEncoding::Identity) +// .body(body)) +// }) +// .responder() +// }) +// }); + +// let body = once(Ok(Bytes::from_static(STR.as_ref()))); + +// let request = srv.get().body(Body::Streaming(Box::new(body))).unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +// } + +// #[test] +// fn test_body_streaming_implicit() { +// let mut srv = test::TestServer::new(|app| { +// app.handler(|_| { +// let body = once(Ok(Bytes::from_static(STR.as_ref()))); +// HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Gzip) +// .body(Body::Streaming(Box::new(body))) +// }) +// }); + +// let request = srv.get().finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +// } + +// #[test] +// fn test_client_cookie_handling() { +// use actix_web::http::Cookie; +// fn err() -> Error { +// use std::io::{Error as IoError, ErrorKind}; +// // stub some generic error +// Error::from(IoError::from(ErrorKind::NotFound)) +// } +// let cookie1 = Cookie::build("cookie1", "value1").finish(); +// let cookie2 = Cookie::build("cookie2", "value2") +// .domain("www.example.org") +// .path("/") +// .secure(true) +// .http_only(true) +// .finish(); +// // Q: are all these clones really necessary? A: Yes, possibly +// let cookie1b = cookie1.clone(); +// let cookie2b = cookie2.clone(); +// let mut srv = test::TestServer::new(move |app| { +// let cookie1 = cookie1b.clone(); +// let cookie2 = cookie2b.clone(); +// app.handler(move |req: &HttpRequest| { +// // Check cookies were sent correctly +// req.cookie("cookie1") +// .ok_or_else(err) +// .and_then(|c1| { +// if c1.value() == "value1" { +// Ok(()) +// } else { +// Err(err()) +// } +// }) +// .and_then(|()| req.cookie("cookie2").ok_or_else(err)) +// .and_then(|c2| { +// if c2.value() == "value2" { +// Ok(()) +// } else { +// Err(err()) +// } +// }) +// // Send some cookies back +// .map(|_| { +// HttpResponse::Ok() +// .cookie(cookie1.clone()) +// .cookie(cookie2.clone()) +// .finish() +// }) +// }) +// }); + +// let request = srv +// .get() +// .cookie(cookie1.clone()) +// .cookie(cookie2.clone()) +// .finish() +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); +// let c1 = response.cookie("cookie1").expect("Missing cookie1"); +// assert_eq!(c1, cookie1); +// let c2 = response.cookie("cookie2").expect("Missing cookie2"); +// assert_eq!(c2, cookie2); +// } + +// #[test] +// fn test_default_headers() { +// let srv = test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); + +// let request = srv.get().finish().unwrap(); +// let repr = format!("{:?}", request); +// assert!(repr.contains("\"accept-encoding\": \"gzip, deflate\"")); +// assert!(repr.contains(concat!( +// "\"user-agent\": \"actix-web/", +// env!("CARGO_PKG_VERSION"), +// "\"" +// ))); + +// let request_override = srv +// .get() +// .header("User-Agent", "test") +// .header("Accept-Encoding", "over_test") +// .finish() +// .unwrap(); +// let repr_override = format!("{:?}", request_override); +// assert!(repr_override.contains("\"user-agent\": \"test\"")); +// assert!(repr_override.contains("\"accept-encoding\": \"over_test\"")); +// assert!(!repr_override.contains("\"accept-encoding\": \"gzip, deflate\"")); +// assert!(!repr_override.contains(concat!( +// "\"user-agent\": \"Actix-web/", +// env!("CARGO_PKG_VERSION"), +// "\"" +// ))); +// } + +// #[test] +// fn client_read_until_eof() { +// let addr = test::TestServer::unused_addr(); + +// thread::spawn(move || { +// let lst = net::TcpListener::bind(addr).unwrap(); + +// for stream in lst.incoming() { +// let mut stream = stream.unwrap(); +// let mut b = [0; 1000]; +// let _ = stream.read(&mut b).unwrap(); +// let _ = stream +// .write_all(b"HTTP/1.1 200 OK\r\nconnection: close\r\n\r\nwelcome!"); +// } +// }); + +// let mut sys = actix::System::new("test"); + +// // client request +// let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) +// .finish() +// .unwrap(); +// let response = sys.block_on(req.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = sys.block_on(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from_static(b"welcome!")); +// } + +// #[test] +// fn client_basic_auth() { +// let mut srv = +// test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); +// /// set authorization header to Basic +// let request = srv +// .get() +// .basic_auth("username", Some("password")) +// .finish() +// .unwrap(); +// let repr = format!("{:?}", request); +// assert!(repr.contains("Basic dXNlcm5hbWU6cGFzc3dvcmQ=")); +// } + +// #[test] +// fn client_bearer_auth() { +// let mut srv = +// test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); +// /// set authorization header to Bearer +// let request = srv +// .get() +// .bearer_auth("someS3cr3tAutht0k3n") +// .finish() +// .unwrap(); +// let repr = format!("{:?}", request); +// assert!(repr.contains("Bearer someS3cr3tAutht0k3n")); +// } diff --git a/tests/test_server.rs b/tests/test_server.rs index acea029c6..29998bc06 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -65,7 +65,7 @@ fn test_body_gzip() { ) }); - let mut response = srv.block_on(srv.get().send()).unwrap(); + let mut response = srv.block_on(srv.get().no_decompress().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -95,7 +95,7 @@ fn test_body_gzip_large() { ) }); - let mut response = srv.block_on(srv.get().send()).unwrap(); + let mut response = srv.block_on(srv.get().no_decompress().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -128,7 +128,7 @@ fn test_body_gzip_large_random() { ) }); - let mut response = srv.block_on(srv.get().send()).unwrap(); + let mut response = srv.block_on(srv.get().no_decompress().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -156,7 +156,7 @@ fn test_body_chunked_implicit() { ) }); - let mut response = srv.block_on(srv.get().send()).unwrap(); + let mut response = srv.block_on(srv.get().no_decompress().send()).unwrap(); assert!(response.status().is_success()); assert_eq!( response.headers().get(TRANSFER_ENCODING).unwrap(), @@ -188,7 +188,12 @@ fn test_body_br_streaming() { }); let mut response = srv - .block_on(srv.get().header(ACCEPT_ENCODING, "br").send()) + .block_on( + srv.get() + .header(ACCEPT_ENCODING, "br") + .no_decompress() + .send(), + ) .unwrap(); assert!(response.status().is_success()); @@ -258,7 +263,7 @@ fn test_body_deflate() { }); // client request - let mut response = srv.block_on(srv.get().send()).unwrap(); + let mut response = srv.block_on(srv.get().no_decompress().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -285,7 +290,12 @@ fn test_body_brotli() { // client request let mut response = srv - .block_on(srv.get().header(ACCEPT_ENCODING, "br").send()) + .block_on( + srv.get() + .header(ACCEPT_ENCODING, "br") + .no_decompress() + .send(), + ) .unwrap(); assert!(response.status().is_success()); From ab597dd98a4bd2f768e2ee419fa752e97ddcec2e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Mar 2019 20:57:06 -0700 Subject: [PATCH 352/427] Added HTTP Authentication for Client #540 --- awc/Cargo.toml | 1 + awc/src/request.rs | 24 +++++++++++ awc/tests/test_client.rs | 89 +++++++++++++++++++++++++--------------- 3 files changed, 82 insertions(+), 32 deletions(-) diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 88c3be421..e08169c96 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -41,6 +41,7 @@ flate2-rust = ["actix-http/flate2-rust"] [dependencies] actix-service = "0.3.4" actix-http = { path = "../actix-http/" } +base64 = "0.10.1" bytes = "0.4" futures = "0.1" log =" 0.4" diff --git a/awc/src/request.rs b/awc/src/request.rs index 90f9a1ab9..649797df8 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -242,6 +242,30 @@ impl ClientRequest { self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze()) } + /// Set HTTP basic authorization + pub fn basic_auth(self, username: U, password: Option

    ) -> Self + where + U: fmt::Display, + P: fmt::Display, + { + let auth = match password { + Some(password) => format!("{}:{}", username, password), + None => format!("{}", username), + }; + self.header( + header::AUTHORIZATION, + format!("Basic {}", base64::encode(&auth)), + ) + } + + /// Set HTTP bearer authentication + pub fn bearer_auth(self, token: T) -> Self + where + T: fmt::Display, + { + self.header(header::AUTHORIZATION, format!("Bearer {}", token)) + } + #[cfg(feature = "cookies")] /// Set a cookie /// diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index f7605b59c..ac07eb6d0 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -1,17 +1,14 @@ -use std::io::{Read, Write}; -use std::{net, thread}; +use std::io::Write; use brotli2::write::BrotliEncoder; use bytes::Bytes; -use flate2::write::{GzEncoder, ZlibEncoder}; +use flate2::write::GzEncoder; use flate2::Compression; -use futures::stream::once; -use futures::Future; use rand::Rng; use actix_http::HttpService; use actix_http_test::TestServer; -use actix_web::{middleware, web, App, HttpRequest, HttpResponse}; +use actix_web::{http::header, web, App, HttpMessage, HttpRequest, HttpResponse}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -479,30 +476,58 @@ fn test_client_brotli_encoding() { // assert_eq!(bytes, Bytes::from_static(b"welcome!")); // } -// #[test] -// fn client_basic_auth() { -// let mut srv = -// test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); -// /// set authorization header to Basic -// let request = srv -// .get() -// .basic_auth("username", Some("password")) -// .finish() -// .unwrap(); -// let repr = format!("{:?}", request); -// assert!(repr.contains("Basic dXNlcm5hbWU6cGFzc3dvcmQ=")); -// } +#[test] +fn client_basic_auth() { + let mut srv = TestServer::new(|| { + HttpService::new(App::new().route( + "/", + web::to(|req: HttpRequest| { + if req + .headers() + .get(header::AUTHORIZATION) + .unwrap() + .to_str() + .unwrap() + == "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + { + HttpResponse::Ok() + } else { + HttpResponse::BadRequest() + } + }), + )) + }); -// #[test] -// fn client_bearer_auth() { -// let mut srv = -// test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); -// /// set authorization header to Bearer -// let request = srv -// .get() -// .bearer_auth("someS3cr3tAutht0k3n") -// .finish() -// .unwrap(); -// let repr = format!("{:?}", request); -// assert!(repr.contains("Bearer someS3cr3tAutht0k3n")); -// } + // set authorization header to Basic + let request = srv.get().basic_auth("username", Some("password")); + let response = srv.block_on(request.send()).unwrap(); + assert!(response.status().is_success()); +} + +#[test] +fn client_bearer_auth() { + let mut srv = TestServer::new(|| { + HttpService::new(App::new().route( + "/", + web::to(|req: HttpRequest| { + if req + .headers() + .get(header::AUTHORIZATION) + .unwrap() + .to_str() + .unwrap() + == "Bearer someS3cr3tAutht0k3n" + { + HttpResponse::Ok() + } else { + HttpResponse::BadRequest() + } + }), + )) + }); + + // set authorization header to Bearer + let request = srv.get().bearer_auth("someS3cr3tAutht0k3n"); + let response = srv.block_on(request.send()).unwrap(); + assert!(response.status().is_success()); +} From 5703bd8160e7f0788af70740f84f9973272b09eb Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Mar 2019 21:31:18 -0700 Subject: [PATCH 353/427] fix client cookies parsing --- awc/src/request.rs | 4 +- awc/src/response.rs | 27 ++++++++- awc/tests/test_client.rs | 126 +++++++++++++++++++-------------------- 3 files changed, 91 insertions(+), 66 deletions(-) diff --git a/awc/src/request.rs b/awc/src/request.rs index 649797df8..16e42939c 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -242,7 +242,7 @@ impl ClientRequest { self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze()) } - /// Set HTTP basic authorization + /// Set HTTP basic authorization header pub fn basic_auth(self, username: U, password: Option

    ) -> Self where U: fmt::Display, @@ -258,7 +258,7 @@ impl ClientRequest { ) } - /// Set HTTP bearer authentication + /// Set HTTP bearer authentication header pub fn bearer_auth(self, token: T) -> Self where T: fmt::Display, diff --git a/awc/src/response.rs b/awc/src/response.rs index 4525bbc1a..5806dc91d 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -5,10 +5,15 @@ use bytes::{Bytes, BytesMut}; use futures::{Future, Poll, Stream}; use actix_http::error::PayloadError; -use actix_http::http::header::CONTENT_LENGTH; +use actix_http::http::header::{CONTENT_LENGTH, SET_COOKIE}; use actix_http::http::{HeaderMap, StatusCode, Version}; use actix_http::{Extensions, Head, HttpMessage, Payload, PayloadStream, ResponseHead}; +#[cfg(feature = "cookies")] +use actix_http::error::CookieParseError; +#[cfg(feature = "cookies")] +use cookie::Cookie; + /// Client Response pub struct ClientResponse { pub(crate) head: ResponseHead, @@ -33,6 +38,26 @@ impl HttpMessage for ClientResponse { fn take_payload(&mut self) -> Payload { std::mem::replace(&mut self.payload, Payload::None) } + + /// Load request cookies. + #[inline] + #[cfg(feature = "cookies")] + fn cookies(&self) -> Result>>, CookieParseError> { + struct Cookies(Vec>); + + if self.extensions().get::().is_none() { + let mut cookies = Vec::new(); + for hdr in self.headers().get_all(SET_COOKIE) { + let s = std::str::from_utf8(hdr.as_bytes()) + .map_err(CookieParseError::from)?; + cookies.push(Cookie::parse_encoded(s)?.into_owned()); + } + self.extensions_mut().insert(Cookies(cookies)); + } + Ok(Ref::map(self.extensions(), |ext| { + &ext.get::().unwrap().0 + })) + } } impl ClientResponse { diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index ac07eb6d0..698b5ab7d 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -8,7 +8,7 @@ use rand::Rng; use actix_http::HttpService; use actix_http_test::TestServer; -use actix_web::{http::header, web, App, HttpMessage, HttpRequest, HttpResponse}; +use actix_web::{http::header, web, App, Error, HttpMessage, HttpRequest, HttpResponse}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -352,69 +352,69 @@ fn test_client_brotli_encoding() { // assert_eq!(bytes, Bytes::from_static(STR.as_ref())); // } -// #[test] -// fn test_client_cookie_handling() { -// use actix_web::http::Cookie; -// fn err() -> Error { -// use std::io::{Error as IoError, ErrorKind}; -// // stub some generic error -// Error::from(IoError::from(ErrorKind::NotFound)) -// } -// let cookie1 = Cookie::build("cookie1", "value1").finish(); -// let cookie2 = Cookie::build("cookie2", "value2") -// .domain("www.example.org") -// .path("/") -// .secure(true) -// .http_only(true) -// .finish(); -// // Q: are all these clones really necessary? A: Yes, possibly -// let cookie1b = cookie1.clone(); -// let cookie2b = cookie2.clone(); -// let mut srv = test::TestServer::new(move |app| { -// let cookie1 = cookie1b.clone(); -// let cookie2 = cookie2b.clone(); -// app.handler(move |req: &HttpRequest| { -// // Check cookies were sent correctly -// req.cookie("cookie1") -// .ok_or_else(err) -// .and_then(|c1| { -// if c1.value() == "value1" { -// Ok(()) -// } else { -// Err(err()) -// } -// }) -// .and_then(|()| req.cookie("cookie2").ok_or_else(err)) -// .and_then(|c2| { -// if c2.value() == "value2" { -// Ok(()) -// } else { -// Err(err()) -// } -// }) -// // Send some cookies back -// .map(|_| { -// HttpResponse::Ok() -// .cookie(cookie1.clone()) -// .cookie(cookie2.clone()) -// .finish() -// }) -// }) -// }); +#[test] +fn test_client_cookie_handling() { + use actix_web::http::Cookie; + fn err() -> Error { + use std::io::{Error as IoError, ErrorKind}; + // stub some generic error + Error::from(IoError::from(ErrorKind::NotFound)) + } + let cookie1 = Cookie::build("cookie1", "value1").finish(); + let cookie2 = Cookie::build("cookie2", "value2") + .domain("www.example.org") + .path("/") + .secure(true) + .http_only(true) + .finish(); + // Q: are all these clones really necessary? A: Yes, possibly + let cookie1b = cookie1.clone(); + let cookie2b = cookie2.clone(); -// let request = srv -// .get() -// .cookie(cookie1.clone()) -// .cookie(cookie2.clone()) -// .finish() -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert!(response.status().is_success()); -// let c1 = response.cookie("cookie1").expect("Missing cookie1"); -// assert_eq!(c1, cookie1); -// let c2 = response.cookie("cookie2").expect("Missing cookie2"); -// assert_eq!(c2, cookie2); -// } + let mut srv = TestServer::new(move || { + let cookie1 = cookie1b.clone(); + let cookie2 = cookie2b.clone(); + + HttpService::new(App::new().route( + "/", + web::to(move |req: HttpRequest| { + // Check cookies were sent correctly + req.cookie("cookie1") + .ok_or_else(err) + .and_then(|c1| { + if c1.value() == "value1" { + Ok(()) + } else { + Err(err()) + } + }) + .and_then(|()| req.cookie("cookie2").ok_or_else(err)) + .and_then(|c2| { + if c2.value() == "value2" { + Ok(()) + } else { + Err(err()) + } + }) + // Send some cookies back + .map(|_| { + HttpResponse::Ok() + .cookie(cookie1.clone()) + .cookie(cookie2.clone()) + .finish() + }) + }), + )) + }); + + let request = srv.get().cookie(cookie1.clone()).cookie(cookie2.clone()); + let response = srv.block_on(request.send()).unwrap(); + assert!(response.status().is_success()); + let c1 = response.cookie("cookie1").expect("Missing cookie1"); + assert_eq!(c1, cookie1); + let c2 = response.cookie("cookie2").expect("Missing cookie2"); + assert_eq!(c2, cookie2); +} // #[test] // fn test_default_headers() { From d49a8ba53bb9f9d6997a7c9d470c18a59c149527 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Mar 2019 21:54:57 -0700 Subject: [PATCH 354/427] add client TestResponse --- actix-http/src/h1/payload.rs | 46 +++-------- awc/src/lib.rs | 1 + awc/src/response.rs | 39 +++++++++ awc/src/test.rs | 155 +++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+), 35 deletions(-) create mode 100644 awc/src/test.rs diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index 6665a0e41..979dd015c 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -1,14 +1,14 @@ //! Payload stream -use bytes::{Bytes, BytesMut}; -#[cfg(not(test))] -use futures::task::current as current_task; -use futures::task::Task; -use futures::{Async, Poll, Stream}; use std::cell::RefCell; use std::cmp; use std::collections::VecDeque; use std::rc::{Rc, Weak}; +use bytes::{Bytes, BytesMut}; +use futures::task::current as current_task; +use futures::task::Task; +use futures::{Async, Poll, Stream}; + use crate::error::PayloadError; /// max buffer size 32k @@ -79,11 +79,6 @@ impl Payload { self.inner.borrow_mut().unread_data(data); } - #[cfg(test)] - pub(crate) fn readall(&self) -> Option { - self.inner.borrow_mut().readall() - } - #[inline] /// Set read buffer capacity /// @@ -226,35 +221,16 @@ impl Inner { self.len } - #[cfg(test)] - pub(crate) fn readall(&mut self) -> Option { - let len = self.items.iter().map(|b| b.len()).sum(); - if len > 0 { - let mut buf = BytesMut::with_capacity(len); - for item in &self.items { - buf.extend_from_slice(item); - } - self.items = VecDeque::new(); - self.len = 0; - Some(buf.take().freeze()) - } else { - self.need_read = true; - None - } - } - fn readany(&mut self) -> Poll, PayloadError> { if let Some(data) = self.items.pop_front() { self.len -= data.len(); self.need_read = self.len < self.capacity; - #[cfg(not(test))] - { - if self.need_read && self.task.is_none() { - self.task = Some(current_task()); - } - if let Some(task) = self.io_task.take() { - task.notify() - } + + if self.need_read && self.task.is_none() && !self.eof { + self.task = Some(current_task()); + } + if let Some(task) = self.io_task.take() { + task.notify() } Ok(Async::Ready(Some(data))) } else if let Some(err) = self.err.take() { diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 4898a0627..8ce5b35f7 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -12,6 +12,7 @@ mod builder; mod connect; mod request; mod response; +pub mod test; pub use self::builder::ClientBuilder; pub use self::request::ClientRequest; diff --git a/awc/src/response.rs b/awc/src/response.rs index 5806dc91d..abff771ce 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -249,3 +249,42 @@ where self.poll() } } + +#[cfg(test)] +mod tests { + use super::*; + use futures::Async; + + use crate::{http::header, test::TestResponse}; + + #[test] + fn test_body() { + let req = TestResponse::with_header(header::CONTENT_LENGTH, "xxxx").finish(); + match req.body().poll().err().unwrap() { + PayloadError::UnknownLength => (), + _ => unreachable!("error"), + } + + let req = TestResponse::with_header(header::CONTENT_LENGTH, "1000000").finish(); + match req.body().poll().err().unwrap() { + PayloadError::Overflow => (), + _ => unreachable!("error"), + } + + let req = TestResponse::default() + .set_payload(Bytes::from_static(b"test")) + .finish(); + match req.body().poll().ok().unwrap() { + Async::Ready(bytes) => assert_eq!(bytes, Bytes::from_static(b"test")), + _ => unreachable!("error"), + } + + let req = TestResponse::default() + .set_payload(Bytes::from_static(b"11111111111111")) + .finish(); + match req.body().limit(5).poll().err().unwrap() { + PayloadError::Overflow => (), + _ => unreachable!("error"), + } + } +} diff --git a/awc/src/test.rs b/awc/src/test.rs new file mode 100644 index 000000000..464abdd55 --- /dev/null +++ b/awc/src/test.rs @@ -0,0 +1,155 @@ +//! Test Various helpers for Actix applications to use during testing. + +use actix_http::http::header::{Header, IntoHeaderValue}; +use actix_http::http::{HeaderName, HttpTryFrom, Version}; +use actix_http::{h1, Payload, ResponseHead}; +use bytes::Bytes; +#[cfg(feature = "cookies")] +use cookie::{Cookie, CookieJar}; + +use crate::ClientResponse; + +/// Test `ClientResponse` builder +/// +/// ```rust,ignore +/// # extern crate http; +/// # extern crate actix_web; +/// # use http::{header, StatusCode}; +/// # use actix_web::*; +/// use actix_web::test::TestRequest; +/// +/// fn index(req: &HttpRequest) -> Response { +/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { +/// Response::Ok().into() +/// } else { +/// Response::BadRequest().into() +/// } +/// } +/// +/// fn main() { +/// let resp = TestRequest::with_header("content-type", "text/plain") +/// .run(&index) +/// .unwrap(); +/// assert_eq!(resp.status(), StatusCode::OK); +/// +/// let resp = TestRequest::default().run(&index).unwrap(); +/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); +/// } +/// ``` +pub struct TestResponse(Option); + +struct Inner { + head: ResponseHead, + #[cfg(feature = "cookies")] + cookies: CookieJar, + payload: Option, +} + +impl Default for TestResponse { + fn default() -> TestResponse { + TestResponse(Some(Inner { + head: ResponseHead::default(), + #[cfg(feature = "cookies")] + cookies: CookieJar::new(), + payload: None, + })) + } +} + +impl TestResponse { + /// Create TestRequest and set header + pub fn with_header(key: K, value: V) -> Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + Self::default().header(key, value).take() + } + + /// Set HTTP version of this request + pub fn version(&mut self, ver: Version) -> &mut Self { + parts(&mut self.0).head.version = ver; + self + } + + /// Set a header + pub fn set(&mut self, hdr: H) -> &mut Self { + if let Ok(value) = hdr.try_into() { + parts(&mut self.0).head.headers.append(H::name(), value); + return self; + } + panic!("Can not set header"); + } + + /// Set a header + pub fn header(&mut self, key: K, value: V) -> &mut Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + if let Ok(key) = HeaderName::try_from(key) { + if let Ok(value) = value.try_into() { + parts(&mut self.0).head.headers.append(key, value); + return self; + } + } + panic!("Can not create header"); + } + + /// Set cookie for this request + #[cfg(feature = "cookies")] + pub fn cookie<'a>(&mut self, cookie: Cookie<'a>) -> &mut Self { + parts(&mut self.0).cookies.add(cookie.into_owned()); + self + } + + /// Set request payload + pub fn set_payload>(&mut self, data: B) -> &mut Self { + let mut payload = h1::Payload::empty(); + payload.unread_data(data.into()); + parts(&mut self.0).payload = Some(payload.into()); + self + } + + pub fn take(&mut self) -> Self { + Self(self.0.take()) + } + + /// Complete request creation and generate `Request` instance + pub fn finish(&mut self) -> ClientResponse { + let inner = self.0.take().expect("cannot reuse test request builder");; + let mut head = inner.head; + + #[cfg(feature = "cookies")] + { + use std::fmt::Write as FmtWrite; + + use actix_http::http::header::{self, HeaderValue}; + use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; + + let mut cookie = String::new(); + for c in inner.cookies.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); + } + if !cookie.is_empty() { + head.headers.insert( + header::SET_COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); + } + } + + if let Some(pl) = inner.payload { + ClientResponse::new(head, pl) + } else { + ClientResponse::new(head, h1::Payload::empty().into()) + } + } +} + +#[inline] +fn parts<'a>(parts: &'a mut Option) -> &'a mut Inner { + parts.as_mut().expect("cannot reuse test request builder") +} From 959aebb24f006faa53511813e65f3baa2c98960a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Mar 2019 22:03:00 -0700 Subject: [PATCH 355/427] simplify TestResponse builder --- awc/src/test.rs | 86 ++++++++++++++----------------------------------- 1 file changed, 24 insertions(+), 62 deletions(-) diff --git a/awc/src/test.rs b/awc/src/test.rs index 464abdd55..165694d83 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -10,35 +10,7 @@ use cookie::{Cookie, CookieJar}; use crate::ClientResponse; /// Test `ClientResponse` builder -/// -/// ```rust,ignore -/// # extern crate http; -/// # extern crate actix_web; -/// # use http::{header, StatusCode}; -/// # use actix_web::*; -/// use actix_web::test::TestRequest; -/// -/// fn index(req: &HttpRequest) -> Response { -/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { -/// Response::Ok().into() -/// } else { -/// Response::BadRequest().into() -/// } -/// } -/// -/// fn main() { -/// let resp = TestRequest::with_header("content-type", "text/plain") -/// .run(&index) -/// .unwrap(); -/// assert_eq!(resp.status(), StatusCode::OK); -/// -/// let resp = TestRequest::default().run(&index).unwrap(); -/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); -/// } -/// ``` -pub struct TestResponse(Option); - -struct Inner { +pub struct TestResponse { head: ResponseHead, #[cfg(feature = "cookies")] cookies: CookieJar, @@ -47,78 +19,73 @@ struct Inner { impl Default for TestResponse { fn default() -> TestResponse { - TestResponse(Some(Inner { + TestResponse { head: ResponseHead::default(), #[cfg(feature = "cookies")] cookies: CookieJar::new(), payload: None, - })) + } } } impl TestResponse { - /// Create TestRequest and set header + /// Create TestResponse and set header pub fn with_header(key: K, value: V) -> Self where HeaderName: HttpTryFrom, V: IntoHeaderValue, { - Self::default().header(key, value).take() + Self::default().header(key, value) } - /// Set HTTP version of this request - pub fn version(&mut self, ver: Version) -> &mut Self { - parts(&mut self.0).head.version = ver; + /// Set HTTP version of this response + pub fn version(mut self, ver: Version) -> Self { + self.head.version = ver; self } /// Set a header - pub fn set(&mut self, hdr: H) -> &mut Self { + pub fn set(mut self, hdr: H) -> Self { if let Ok(value) = hdr.try_into() { - parts(&mut self.0).head.headers.append(H::name(), value); + self.head.headers.append(H::name(), value); return self; } panic!("Can not set header"); } - /// Set a header - pub fn header(&mut self, key: K, value: V) -> &mut Self + /// Append a header + pub fn header(mut self, key: K, value: V) -> Self where HeaderName: HttpTryFrom, V: IntoHeaderValue, { if let Ok(key) = HeaderName::try_from(key) { if let Ok(value) = value.try_into() { - parts(&mut self.0).head.headers.append(key, value); + self.head.headers.append(key, value); return self; } } panic!("Can not create header"); } - /// Set cookie for this request + /// Set cookie for this response #[cfg(feature = "cookies")] - pub fn cookie<'a>(&mut self, cookie: Cookie<'a>) -> &mut Self { - parts(&mut self.0).cookies.add(cookie.into_owned()); + pub fn cookie<'a>(mut self, cookie: Cookie<'a>) -> Self { + self.cookies.add(cookie.into_owned()); self } - /// Set request payload - pub fn set_payload>(&mut self, data: B) -> &mut Self { + /// Set response's payload + pub fn set_payload>(mut self, data: B) -> Self { let mut payload = h1::Payload::empty(); payload.unread_data(data.into()); - parts(&mut self.0).payload = Some(payload.into()); + self.payload = Some(payload.into()); self } - pub fn take(&mut self) -> Self { - Self(self.0.take()) - } - - /// Complete request creation and generate `Request` instance - pub fn finish(&mut self) -> ClientResponse { - let inner = self.0.take().expect("cannot reuse test request builder");; - let mut head = inner.head; + /// Complete response creation and generate `ClientResponse` instance + pub fn finish(self) -> ClientResponse { + let mut head = self.head; #[cfg(feature = "cookies")] { @@ -128,7 +95,7 @@ impl TestResponse { use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; let mut cookie = String::new(); - for c in inner.cookies.delta() { + for c in self.cookies.delta() { let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); let _ = write!(&mut cookie, "; {}={}", name, value); @@ -141,15 +108,10 @@ impl TestResponse { } } - if let Some(pl) = inner.payload { + if let Some(pl) = self.payload { ClientResponse::new(head, pl) } else { ClientResponse::new(head, h1::Payload::empty().into()) } } } - -#[inline] -fn parts<'a>(parts: &'a mut Option) -> &'a mut Inner { - parts.as_mut().expect("cannot reuse test request builder") -} From b7570b2476ff605ef407b411c71790a79b0d4bdb Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Mar 2019 22:33:01 -0700 Subject: [PATCH 356/427] remove unused code --- awc/src/builder.rs | 9 +++++---- awc/src/request.rs | 10 ++++------ awc/src/response.rs | 17 ----------------- 3 files changed, 9 insertions(+), 27 deletions(-) diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 686948682..562ff2419 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -65,13 +65,14 @@ impl ClientBuilder { } /// Do not add default request headers. - /// By default `Accept-Encoding` and `User-Agent` headers are set. - pub fn skip_default_headers(mut self) -> Self { + /// By default `Date` and `User-Agent` headers are set. + pub fn no_default_headers(mut self) -> Self { self.default_headers = false; self } - /// Add default header. This header adds to every request. + /// Add default header. Headers adds byt this method + /// get added to every request. pub fn header(mut self, key: K, value: V) -> Self where HeaderName: HttpTryFrom, @@ -91,7 +92,7 @@ impl ClientBuilder { self } - /// Finish build process and create `Client`. + /// Finish build process and create `Client` instance. pub fn finish(self) -> Client { Client { connector: self.connector, diff --git a/awc/src/request.rs b/awc/src/request.rs index 16e42939c..c944c6ca3 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -141,13 +141,11 @@ impl ClientRequest { /// To override header use `set_header()` method. /// /// ```rust - /// # extern crate actix_http; - /// # - /// use actix_http::{client, http}; + /// use awc::{http, Client}; /// /// fn main() { /// # actix_rt::System::new("test").block_on(futures::future::lazy(|| { - /// let req = awc::Client::new() + /// let req = Client::new() /// .get("http://www.rust-lang.org") /// .header("X-TEST", "value") /// .header(http::header::CONTENT_TYPE, "application/json"); @@ -304,7 +302,7 @@ impl ClientRequest { } /// Do not add default request headers. - /// By default `Accept-Encoding` and `User-Agent` headers are set. + /// By default `Date` and `User-Agent` headers are set. pub fn no_default_headers(mut self) -> Self { self.default_headers = false; self @@ -394,7 +392,7 @@ impl ClientRequest { // user agent self.set_header_if_none( header::USER_AGENT, - concat!("actix-http/", env!("CARGO_PKG_VERSION")), + concat!("awc/", env!("CARGO_PKG_VERSION")), ) } else { self diff --git a/awc/src/response.rs b/awc/src/response.rs index abff771ce..03606a768 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -71,11 +71,6 @@ impl ClientResponse { &self.head } - #[inline] - pub(crate) fn head_mut(&mut self) -> &mut ResponseHead { - &mut self.head - } - /// Read the Request Version. #[inline] pub fn version(&self) -> Version { @@ -94,18 +89,6 @@ impl ClientResponse { &self.head().headers } - #[inline] - /// Returns mutable Request's headers. - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.head_mut().headers - } - - /// Checks if a connection should be kept alive. - #[inline] - pub fn keep_alive(&self) -> bool { - self.head().keep_alive() - } - /// Set a body and return previous body value pub fn map_body(mut self, f: F) -> ClientResponse where From b6b37d3ea3702eaa3a3384b37af8ddfe0b630150 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 26 Mar 2019 23:25:24 -0700 Subject: [PATCH 357/427] Add Client::request_from --- awc/src/lib.rs | 20 +++++++++++++++++++- awc/src/request.rs | 2 +- awc/src/response.rs | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 8ce5b35f7..ac7dcf2f1 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -3,7 +3,7 @@ use std::rc::Rc; pub use actix_http::client::{ConnectError, InvalidUrl, SendRequestError}; pub use actix_http::error::PayloadError; -pub use actix_http::http; +pub use actix_http::{http, RequestHead}; use actix_http::client::Connector; use actix_http::http::{HttpTryFrom, Method, Uri}; @@ -76,6 +76,24 @@ impl Client { ClientRequest::new(method, url, self.connector.clone()) } + /// Create `ClientRequest` from `RequestHead` + /// + /// It is useful for proxy requests. This implementation + /// copies all headers and the method. + pub fn request_from(&self, url: U, head: &RequestHead) -> ClientRequest + where + Uri: HttpTryFrom, + { + let mut req = + ClientRequest::new(head.method.clone(), url, self.connector.clone()); + + for (key, value) in &head.headers { + req.head.headers.insert(key.clone(), value.clone()); + } + + req + } + pub fn get(&self, url: U) -> ClientRequest where Uri: HttpTryFrom, diff --git a/awc/src/request.rs b/awc/src/request.rs index c944c6ca3..d25ecc421 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -56,7 +56,7 @@ const HTTPS_ENCODING: &str = "gzip, deflate"; /// } /// ``` pub struct ClientRequest { - head: RequestHead, + pub(crate) head: RequestHead, err: Option, #[cfg(feature = "cookies")] cookies: Option, diff --git a/awc/src/response.rs b/awc/src/response.rs index 03606a768..3b77eaa60 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -7,7 +7,7 @@ use futures::{Future, Poll, Stream}; use actix_http::error::PayloadError; use actix_http::http::header::{CONTENT_LENGTH, SET_COOKIE}; use actix_http::http::{HeaderMap, StatusCode, Version}; -use actix_http::{Extensions, Head, HttpMessage, Payload, PayloadStream, ResponseHead}; +use actix_http::{Extensions, HttpMessage, Payload, PayloadStream, ResponseHead}; #[cfg(feature = "cookies")] use actix_http::error::CookieParseError; From faa3ea8e5bb0a4561b78288a6ee4b80e0f066517 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 27 Mar 2019 09:24:55 -0700 Subject: [PATCH 358/427] rename BodyLength to BodySize --- actix-http/.gitignore | 14 ----- actix-http/.travis.yml | 52 ------------------ actix-http/Cargo.toml | 1 - actix-http/src/body.rs | 85 ++++++++++++++--------------- actix-http/src/client/h1proto.rs | 4 +- actix-http/src/client/h2proto.rs | 14 ++--- actix-http/src/encoding/encoder.rs | 6 +- actix-http/src/h1/client.rs | 4 +- actix-http/src/h1/codec.rs | 4 +- actix-http/src/h1/dispatcher.rs | 4 +- actix-http/src/h1/encoder.rs | 36 ++++++------ actix-http/src/h2/dispatcher.rs | 18 +++--- actix-http/src/service/senderror.rs | 8 +-- actix-http/src/ws/client/service.rs | 4 +- awc/src/lib.rs | 27 ++++++++- awc/src/test.rs | 3 +- src/lib.rs | 35 ++++++++++-- src/middleware/logger.rs | 4 +- 18 files changed, 151 insertions(+), 172 deletions(-) delete mode 100644 actix-http/.gitignore delete mode 100644 actix-http/.travis.yml diff --git a/actix-http/.gitignore b/actix-http/.gitignore deleted file mode 100644 index 42d0755dd..000000000 --- a/actix-http/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -Cargo.lock -target/ -guide/build/ -/gh-pages - -*.so -*.out -*.pyc -*.pid -*.sock -*~ - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/actix-http/.travis.yml b/actix-http/.travis.yml deleted file mode 100644 index 02fbd42c7..000000000 --- a/actix-http/.travis.yml +++ /dev/null @@ -1,52 +0,0 @@ -language: rust -sudo: required -dist: trusty - -cache: - cargo: true - apt: true - -matrix: - include: - - rust: stable - - rust: beta - - rust: nightly-2019-03-02 - allow_failures: - - rust: nightly-2019-03-02 - -env: - global: - - RUSTFLAGS="-C link-dead-code" - - OPENSSL_VERSION=openssl-1.0.2 - -before_install: - - sudo add-apt-repository -y ppa:0k53d-karl-f830m/openssl - - sudo apt-get update -qq - - sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev - -before_cache: | - if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then - RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin - fi - -script: -- cargo clean -- cargo build --all-features -- cargo test --all-features - -# Upload docs -after_success: - - | - if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then - cargo doc --no-deps && - echo "" > target/doc/index.html && - git clone https://github.com/davisp/ghp-import.git && - ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc && - echo "Uploaded documentation" - fi - - | - if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then - taskset -c 0 cargo tarpaulin --features="ssl" --out Xml - bash <(curl -s https://codecov.io/bash) - echo "Uploaded code coverage" - fi diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 427024e24..99d80b0be 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -12,7 +12,6 @@ categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] license = "MIT/Apache-2.0" -exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] edition = "2018" workspace = ".." diff --git a/actix-http/src/body.rs b/actix-http/src/body.rs index e1399e6b4..85717ba85 100644 --- a/actix-http/src/body.rs +++ b/actix-http/src/body.rs @@ -7,8 +7,8 @@ use futures::{Async, Poll, Stream}; use crate::error::Error; #[derive(Debug, PartialEq, Copy, Clone)] -/// Different type of body -pub enum BodyLength { +/// Body size hint +pub enum BodySize { None, Empty, Sized(usize), @@ -16,13 +16,13 @@ pub enum BodyLength { Stream, } -impl BodyLength { +impl BodySize { pub fn is_eof(&self) -> bool { match self { - BodyLength::None - | BodyLength::Empty - | BodyLength::Sized(0) - | BodyLength::Sized64(0) => true, + BodySize::None + | BodySize::Empty + | BodySize::Sized(0) + | BodySize::Sized64(0) => true, _ => false, } } @@ -30,14 +30,14 @@ impl BodyLength { /// Type that provides this trait can be streamed to a peer. pub trait MessageBody { - fn length(&self) -> BodyLength; + fn length(&self) -> BodySize; fn poll_next(&mut self) -> Poll, Error>; } impl MessageBody for () { - fn length(&self) -> BodyLength { - BodyLength::Empty + fn length(&self) -> BodySize { + BodySize::Empty } fn poll_next(&mut self) -> Poll, Error> { @@ -46,7 +46,7 @@ impl MessageBody for () { } impl MessageBody for Box { - fn length(&self) -> BodyLength { + fn length(&self) -> BodySize { self.as_ref().length() } @@ -86,7 +86,7 @@ impl ResponseBody { } impl MessageBody for ResponseBody { - fn length(&self) -> BodyLength { + fn length(&self) -> BodySize { match self { ResponseBody::Body(ref body) => body.length(), ResponseBody::Other(ref body) => body.length(), @@ -135,11 +135,11 @@ impl Body { } impl MessageBody for Body { - fn length(&self) -> BodyLength { + fn length(&self) -> BodySize { match self { - Body::None => BodyLength::None, - Body::Empty => BodyLength::Empty, - Body::Bytes(ref bin) => BodyLength::Sized(bin.len()), + Body::None => BodySize::None, + Body::Empty => BodySize::Empty, + Body::Bytes(ref bin) => BodySize::Sized(bin.len()), Body::Message(ref body) => body.length(), } } @@ -235,8 +235,8 @@ impl From for Body { } impl MessageBody for Bytes { - fn length(&self) -> BodyLength { - BodyLength::Sized(self.len()) + fn length(&self) -> BodySize { + BodySize::Sized(self.len()) } fn poll_next(&mut self) -> Poll, Error> { @@ -249,8 +249,8 @@ impl MessageBody for Bytes { } impl MessageBody for BytesMut { - fn length(&self) -> BodyLength { - BodyLength::Sized(self.len()) + fn length(&self) -> BodySize { + BodySize::Sized(self.len()) } fn poll_next(&mut self) -> Poll, Error> { @@ -265,8 +265,8 @@ impl MessageBody for BytesMut { } impl MessageBody for &'static str { - fn length(&self) -> BodyLength { - BodyLength::Sized(self.len()) + fn length(&self) -> BodySize { + BodySize::Sized(self.len()) } fn poll_next(&mut self) -> Poll, Error> { @@ -281,8 +281,8 @@ impl MessageBody for &'static str { } impl MessageBody for &'static [u8] { - fn length(&self) -> BodyLength { - BodyLength::Sized(self.len()) + fn length(&self) -> BodySize { + BodySize::Sized(self.len()) } fn poll_next(&mut self) -> Poll, Error> { @@ -297,8 +297,8 @@ impl MessageBody for &'static [u8] { } impl MessageBody for Vec { - fn length(&self) -> BodyLength { - BodyLength::Sized(self.len()) + fn length(&self) -> BodySize { + BodySize::Sized(self.len()) } fn poll_next(&mut self) -> Poll, Error> { @@ -314,8 +314,8 @@ impl MessageBody for Vec { } impl MessageBody for String { - fn length(&self) -> BodyLength { - BodyLength::Sized(self.len()) + fn length(&self) -> BodySize { + BodySize::Sized(self.len()) } fn poll_next(&mut self) -> Poll, Error> { @@ -354,8 +354,8 @@ where S: Stream, E: Into, { - fn length(&self) -> BodyLength { - BodyLength::Stream + fn length(&self) -> BodySize { + BodySize::Stream } fn poll_next(&mut self) -> Poll, Error> { @@ -383,8 +383,8 @@ impl MessageBody for SizedStream where S: Stream, { - fn length(&self) -> BodyLength { - BodyLength::Sized(self.size) + fn length(&self) -> BodySize { + BodySize::Sized(self.size) } fn poll_next(&mut self) -> Poll, Error> { @@ -416,50 +416,47 @@ mod tests { #[test] fn test_static_str() { - assert_eq!(Body::from("").length(), BodyLength::Sized(0)); - assert_eq!(Body::from("test").length(), BodyLength::Sized(4)); + assert_eq!(Body::from("").length(), BodySize::Sized(0)); + assert_eq!(Body::from("test").length(), BodySize::Sized(4)); assert_eq!(Body::from("test").get_ref(), b"test"); } #[test] fn test_static_bytes() { - assert_eq!(Body::from(b"test".as_ref()).length(), BodyLength::Sized(4)); + assert_eq!(Body::from(b"test".as_ref()).length(), BodySize::Sized(4)); assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test"); assert_eq!( Body::from_slice(b"test".as_ref()).length(), - BodyLength::Sized(4) + BodySize::Sized(4) ); assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test"); } #[test] fn test_vec() { - assert_eq!(Body::from(Vec::from("test")).length(), BodyLength::Sized(4)); + assert_eq!(Body::from(Vec::from("test")).length(), BodySize::Sized(4)); assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test"); } #[test] fn test_bytes() { - assert_eq!( - Body::from(Bytes::from("test")).length(), - BodyLength::Sized(4) - ); + assert_eq!(Body::from(Bytes::from("test")).length(), BodySize::Sized(4)); assert_eq!(Body::from(Bytes::from("test")).get_ref(), b"test"); } #[test] fn test_string() { let b = "test".to_owned(); - assert_eq!(Body::from(b.clone()).length(), BodyLength::Sized(4)); + assert_eq!(Body::from(b.clone()).length(), BodySize::Sized(4)); assert_eq!(Body::from(b.clone()).get_ref(), b"test"); - assert_eq!(Body::from(&b).length(), BodyLength::Sized(4)); + assert_eq!(Body::from(&b).length(), BodySize::Sized(4)); assert_eq!(Body::from(&b).get_ref(), b"test"); } #[test] fn test_bytes_mut() { let b = BytesMut::from("test"); - assert_eq!(Body::from(b.clone()).length(), BodyLength::Sized(4)); + assert_eq!(Body::from(b.clone()).length(), BodySize::Sized(4)); assert_eq!(Body::from(b).get_ref(), b"test"); } } diff --git a/actix-http/src/client/h1proto.rs b/actix-http/src/client/h1proto.rs index 2e29484ff..b7b8d4a0a 100644 --- a/actix-http/src/client/h1proto.rs +++ b/actix-http/src/client/h1proto.rs @@ -13,7 +13,7 @@ use crate::payload::{Payload, PayloadStream}; use super::connection::{ConnectionLifetime, ConnectionType, IoConnection}; use super::error::{ConnectError, SendRequestError}; use super::pool::Acquired; -use crate::body::{BodyLength, MessageBody}; +use crate::body::{BodySize, MessageBody}; pub(crate) fn send_request( io: T, @@ -40,7 +40,7 @@ where .from_err() // send request body .and_then(move |framed| match body.length() { - BodyLength::None | BodyLength::Empty | BodyLength::Sized(0) => { + BodySize::None | BodySize::Empty | BodySize::Sized(0) => { Either::A(ok(framed)) } _ => Either::B(SendBody::new(body, framed)), diff --git a/actix-http/src/client/h2proto.rs b/actix-http/src/client/h2proto.rs index 9ad722627..d45716ab8 100644 --- a/actix-http/src/client/h2proto.rs +++ b/actix-http/src/client/h2proto.rs @@ -8,7 +8,7 @@ use h2::{client::SendRequest, SendStream}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING}; use http::{request::Request, HttpTryFrom, Method, Version}; -use crate::body::{BodyLength, MessageBody}; +use crate::body::{BodySize, MessageBody}; use crate::message::{RequestHead, ResponseHead}; use crate::payload::Payload; @@ -31,7 +31,7 @@ where let head_req = head.method == Method::HEAD; let length = body.length(); let eof = match length { - BodyLength::None | BodyLength::Empty | BodyLength::Sized(0) => true, + BodySize::None | BodySize::Empty | BodySize::Sized(0) => true, _ => false, }; @@ -48,19 +48,19 @@ where // Content length let _ = match length { - BodyLength::None => None, - BodyLength::Stream => { + BodySize::None => None, + BodySize::Stream => { skip_len = false; None } - BodyLength::Empty => req + BodySize::Empty => req .headers_mut() .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), - BodyLength::Sized(len) => req.headers_mut().insert( + BodySize::Sized(len) => req.headers_mut().insert( CONTENT_LENGTH, HeaderValue::try_from(format!("{}", len)).unwrap(), ), - BodyLength::Sized64(len) => req.headers_mut().insert( + BodySize::Sized64(len) => req.headers_mut().insert( CONTENT_LENGTH, HeaderValue::try_from(format!("{}", len)).unwrap(), ), diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 0778cc262..af861b9d7 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -9,7 +9,7 @@ use brotli2::write::BrotliEncoder; #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] use flate2::write::{GzEncoder, ZlibEncoder}; -use crate::body::{Body, BodyLength, MessageBody, ResponseBody}; +use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::http::header::{ContentEncoding, CONTENT_ENCODING}; use crate::http::{HeaderValue, HttpTryFrom, StatusCode}; use crate::{Error, Head, ResponseHead}; @@ -89,14 +89,14 @@ enum EncoderBody { } impl MessageBody for Encoder { - fn length(&self) -> BodyLength { + fn length(&self) -> BodySize { if self.encoder.is_none() { match self.body { EncoderBody::Body(ref b) => b.length(), EncoderBody::Other(ref b) => b.length(), } } else { - BodyLength::Stream + BodySize::Stream } } diff --git a/actix-http/src/h1/client.rs b/actix-http/src/h1/client.rs index b3a5a5d9f..fbdc8bde1 100644 --- a/actix-http/src/h1/client.rs +++ b/actix-http/src/h1/client.rs @@ -12,7 +12,7 @@ use http::{Method, Version}; use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; use super::{decoder, encoder}; use super::{Message, MessageType}; -use crate::body::BodyLength; +use crate::body::BodySize; use crate::config::ServiceConfig; use crate::error::{ParseError, PayloadError}; use crate::helpers; @@ -179,7 +179,7 @@ impl Decoder for ClientPayloadCodec { } impl Encoder for ClientCodec { - type Item = Message<(RequestHead, BodyLength)>; + type Item = Message<(RequestHead, BodySize)>; type Error = io::Error; fn encode( diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index c66364c02..9bb417091 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -11,7 +11,7 @@ use http::{Method, StatusCode, Version}; use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; use super::{decoder, encoder}; use super::{Message, MessageType}; -use crate::body::BodyLength; +use crate::body::BodySize; use crate::config::ServiceConfig; use crate::error::ParseError; use crate::helpers; @@ -140,7 +140,7 @@ impl Decoder for Codec { } impl Encoder for Codec { - type Item = Message<(Response<()>, BodyLength)>; + type Item = Message<(Response<()>, BodySize)>; type Error = io::Error; fn encode( diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 09a17fcf7..34204bf5a 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -11,7 +11,7 @@ use futures::{Async, Future, Poll, Sink, Stream}; use log::{debug, error, trace}; use tokio_timer::Delay; -use crate::body::{Body, BodyLength, MessageBody, ResponseBody}; +use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::config::ServiceConfig; use crate::error::DispatchError; use crate::error::{ParseError, PayloadError}; @@ -208,7 +208,7 @@ where self.flags .set(Flags::KEEPALIVE, self.framed.get_codec().keepalive()); match body.length() { - BodyLength::None | BodyLength::Empty => Ok(State::None), + BodySize::None | BodySize::Empty => Ok(State::None), _ => Ok(State::SendPayload(body)), } } diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index 712d123eb..dbf6d440f 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -11,7 +11,7 @@ use http::header::{ }; use http::{HeaderMap, Method, StatusCode, Version}; -use crate::body::BodyLength; +use crate::body::BodySize; use crate::config::ServiceConfig; use crate::header::ContentEncoding; use crate::helpers; @@ -23,7 +23,7 @@ const AVERAGE_HEADER_SIZE: usize = 30; #[derive(Debug)] pub(crate) struct MessageEncoder { - pub length: BodyLength, + pub length: BodySize, pub te: TransferEncoding, _t: PhantomData, } @@ -31,7 +31,7 @@ pub(crate) struct MessageEncoder { impl Default for MessageEncoder { fn default() -> Self { MessageEncoder { - length: BodyLength::None, + length: BodySize::None, te: TransferEncoding::empty(), _t: PhantomData, } @@ -53,28 +53,28 @@ pub(crate) trait MessageType: Sized { &mut self, dst: &mut BytesMut, version: Version, - mut length: BodyLength, + mut length: BodySize, ctype: ConnectionType, config: &ServiceConfig, ) -> io::Result<()> { let chunked = self.chunked(); - let mut skip_len = length != BodyLength::Stream; + let mut skip_len = length != BodySize::Stream; // Content length if let Some(status) = self.status() { match status { StatusCode::NO_CONTENT | StatusCode::CONTINUE - | StatusCode::PROCESSING => length = BodyLength::None, + | StatusCode::PROCESSING => length = BodySize::None, StatusCode::SWITCHING_PROTOCOLS => { skip_len = true; - length = BodyLength::Stream; + length = BodySize::Stream; } _ => (), } } match length { - BodyLength::Stream => { + BodySize::Stream => { if chunked { dst.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") } else { @@ -82,16 +82,16 @@ pub(crate) trait MessageType: Sized { dst.extend_from_slice(b"\r\n"); } } - BodyLength::Empty => { + BodySize::Empty => { dst.extend_from_slice(b"\r\ncontent-length: 0\r\n"); } - BodyLength::Sized(len) => helpers::write_content_length(len, dst), - BodyLength::Sized64(len) => { + BodySize::Sized(len) => helpers::write_content_length(len, dst), + BodySize::Sized64(len) => { dst.extend_from_slice(b"\r\ncontent-length: "); write!(dst.writer(), "{}", len)?; dst.extend_from_slice(b"\r\n"); } - BodyLength::None => dst.extend_from_slice(b"\r\n"), + BodySize::None => dst.extend_from_slice(b"\r\n"), } // Connection @@ -243,24 +243,24 @@ impl MessageEncoder { head: bool, stream: bool, version: Version, - length: BodyLength, + length: BodySize, ctype: ConnectionType, config: &ServiceConfig, ) -> io::Result<()> { // transfer encoding if !head { self.te = match length { - BodyLength::Empty => TransferEncoding::empty(), - BodyLength::Sized(len) => TransferEncoding::length(len as u64), - BodyLength::Sized64(len) => TransferEncoding::length(len), - BodyLength::Stream => { + BodySize::Empty => TransferEncoding::empty(), + BodySize::Sized(len) => TransferEncoding::length(len as u64), + BodySize::Sized64(len) => TransferEncoding::length(len), + BodySize::Stream => { if message.chunked() && !stream { TransferEncoding::chunked() } else { TransferEncoding::eof() } } - BodyLength::None => TransferEncoding::empty(), + BodySize::None => TransferEncoding::empty(), }; } else { self.te = TransferEncoding::empty(); diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index ea63dc2bc..9b43be669 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -18,7 +18,7 @@ use http::HttpTryFrom; use log::{debug, error, trace}; use tokio_timer::Delay; -use crate::body::{Body, BodyLength, MessageBody, ResponseBody}; +use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::config::ServiceConfig; use crate::error::{DispatchError, Error, ParseError, PayloadError, ResponseError}; use crate::message::ResponseHead; @@ -151,10 +151,10 @@ where fn prepare_response( &self, head: &ResponseHead, - length: &mut BodyLength, + length: &mut BodySize, ) -> http::Response<()> { let mut has_date = false; - let mut skip_len = length != &BodyLength::Stream; + let mut skip_len = length != &BodySize::Stream; let mut res = http::Response::new(()); *res.status_mut() = head.status; @@ -164,23 +164,23 @@ where match head.status { http::StatusCode::NO_CONTENT | http::StatusCode::CONTINUE - | http::StatusCode::PROCESSING => *length = BodyLength::None, + | http::StatusCode::PROCESSING => *length = BodySize::None, http::StatusCode::SWITCHING_PROTOCOLS => { skip_len = true; - *length = BodyLength::Stream; + *length = BodySize::Stream; } _ => (), } let _ = match length { - BodyLength::None | BodyLength::Stream => None, - BodyLength::Empty => res + BodySize::None | BodySize::Stream => None, + BodySize::Empty => res .headers_mut() .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), - BodyLength::Sized(len) => res.headers_mut().insert( + BodySize::Sized(len) => res.headers_mut().insert( CONTENT_LENGTH, HeaderValue::try_from(format!("{}", len)).unwrap(), ), - BodyLength::Sized64(len) => res.headers_mut().insert( + BodySize::Sized64(len) => res.headers_mut().insert( CONTENT_LENGTH, HeaderValue::try_from(format!("{}", len)).unwrap(), ), diff --git a/actix-http/src/service/senderror.rs b/actix-http/src/service/senderror.rs index 44d362593..03fe5976a 100644 --- a/actix-http/src/service/senderror.rs +++ b/actix-http/src/service/senderror.rs @@ -5,7 +5,7 @@ use actix_service::{NewService, Service}; use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, Poll, Sink}; -use crate::body::{BodyLength, MessageBody, ResponseBody}; +use crate::body::{BodySize, MessageBody, ResponseBody}; use crate::error::{Error, ResponseError}; use crate::h1::{Codec, Message}; use crate::response::Response; @@ -61,7 +61,7 @@ where let (res, _body) = res.replace_body(()); Either::B(SendErrorFut { framed: Some(framed), - res: Some((res, BodyLength::Empty).into()), + res: Some((res, BodySize::Empty).into()), err: Some(e), _t: PhantomData, }) @@ -71,7 +71,7 @@ where } pub struct SendErrorFut { - res: Option, BodyLength)>>, + res: Option, BodySize)>>, framed: Option>, err: Option, _t: PhantomData, @@ -172,7 +172,7 @@ where } pub struct SendResponseFut { - res: Option, BodyLength)>>, + res: Option, BodySize)>>, body: Option>, framed: Option>, } diff --git a/actix-http/src/ws/client/service.rs b/actix-http/src/ws/client/service.rs index 8a0840f90..cb3fb6f39 100644 --- a/actix-http/src/ws/client/service.rs +++ b/actix-http/src/ws/client/service.rs @@ -13,7 +13,7 @@ use log::trace; use rand; use sha1::Sha1; -use crate::body::BodyLength; +use crate::body::BodySize; use crate::h1; use crate::message::{ConnectionType, Head, ResponseHead}; use crate::ws::Codec; @@ -149,7 +149,7 @@ where // h1 protocol let framed = Framed::new(io, h1::ClientCodec::default()); framed - .send((request, BodyLength::None).into()) + .send((request, BodySize::None).into()) .map_err(ClientError::from) .and_then(|framed| { framed diff --git a/awc/src/lib.rs b/awc/src/lib.rs index ac7dcf2f1..3bad8caa3 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -1,12 +1,35 @@ +//! An HTTP Client +//! +//! ```rust +//! # use futures::future::{Future, lazy}; +//! use actix_rt::System; +//! use awc::Client; +//! +//! fn main() { +//! System::new("test").block_on(lazy(|| { +//! let mut client = Client::default(); +//! +//! client.get("http://www.rust-lang.org") // <- Create request builder +//! .header("User-Agent", "Actix-web") +//! .send() // <- Send http request +//! .map_err(|_| ()) +//! .and_then(|response| { // <- server http response +//! println!("Response: {:?}", response); +//! Ok(()) +//! }) +//! })); +//! } +//! ``` use std::cell::RefCell; use std::rc::Rc; pub use actix_http::client::{ConnectError, InvalidUrl, SendRequestError}; pub use actix_http::error::PayloadError; -pub use actix_http::{http, RequestHead}; +pub use actix_http::http; use actix_http::client::Connector; use actix_http::http::{HttpTryFrom, Method, Uri}; +use actix_http::RequestHead; mod builder; mod connect; @@ -20,7 +43,7 @@ pub use self::response::ClientResponse; use self::connect::{Connect, ConnectorWrapper}; -/// An HTTP Client Request +/// An HTTP Client /// /// ```rust /// # use futures::future::{Future, lazy}; diff --git a/awc/src/test.rs b/awc/src/test.rs index 165694d83..395e62904 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -1,5 +1,4 @@ -//! Test Various helpers for Actix applications to use during testing. - +//! Test helpers for actix http client to use during testing. use actix_http::http::header::{Header, IntoHeaderValue}; use actix_http::http::{HeaderName, HttpTryFrom, Version}; use actix_http::{h1, Payload, ResponseHead}; diff --git a/src/lib.rs b/src/lib.rs index a21032db6..54709b47b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,9 +106,6 @@ extern crate actix_web_codegen; #[doc(hidden)] pub use actix_web_codegen::*; -#[cfg(feature = "client")] -pub use awc as client; - // re-export for convenience pub use actix_http::Response as HttpResponse; pub use actix_http::{http, Error, HttpMessage, ResponseError, Result}; @@ -145,7 +142,7 @@ pub mod dev { pub use crate::types::payload::HttpMessageBody; pub use crate::types::readlines::Readlines; - pub use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; + pub use actix_http::body::{Body, BodySize, MessageBody, ResponseBody}; pub use actix_http::ResponseBuilder as HttpResponseBuilder; pub use actix_http::{ Extensions, Head, Payload, PayloadStream, RequestHead, ResponseHead, @@ -371,3 +368,33 @@ pub mod web { fn_transform(f) } } + +#[cfg(feature = "client")] +pub mod client { + //! An HTTP Client + //! + //! ```rust + //! # use futures::future::{Future, lazy}; + //! use actix_rt::System; + //! use actix_web::client::Client; + //! + //! fn main() { + //! System::new("test").block_on(lazy(|| { + //! let mut client = Client::default(); + //! + //! client.get("http://www.rust-lang.org") // <- Create request builder + //! .header("User-Agent", "Actix-web") + //! .send() // <- Send http request + //! .map_err(|_| ()) + //! .and_then(|response| { // <- server http response + //! println!("Response: {:?}", response); + //! Ok(()) + //! }) + //! })); + //! } + //! ``` + pub use awc::{ + test, Client, ClientBuilder, ClientRequest, ClientResponse, ConnectError, + InvalidUrl, PayloadError, SendRequestError, + }; +} diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 42f344f03..cd52048f7 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -12,7 +12,7 @@ use futures::{Async, Future, Poll}; use regex::Regex; use time; -use crate::dev::{BodyLength, MessageBody, ResponseBody}; +use crate::dev::{BodySize, MessageBody, ResponseBody}; use crate::error::{Error, Result}; use crate::service::{ServiceRequest, ServiceResponse}; use crate::{HttpMessage, HttpResponse}; @@ -238,7 +238,7 @@ impl Drop for StreamLog { } impl MessageBody for StreamLog { - fn length(&self) -> BodyLength { + fn length(&self) -> BodySize { self.body.length() } From fb9c94c3e0b490a20f90d4893a53369cb1989971 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 27 Mar 2019 09:31:07 -0700 Subject: [PATCH 359/427] remove Backtrace from error --- actix-http/Cargo.toml | 1 - actix-http/src/error.rs | 49 +---------------------------------------- 2 files changed, 1 insertion(+), 49 deletions(-) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 99d80b0be..809d4d677 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -55,7 +55,6 @@ actix-utils = "0.3.4" actix-server-config = "0.1.0" base64 = "0.10" -backtrace = "0.3" bitflags = "1.0" bytes = "0.4" byteorder = "1.2" diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 820071b1e..23e6728d8 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -7,7 +7,6 @@ use std::{fmt, io, result}; // use actix::MailboxError; use actix_utils::timeout::TimeoutError; -use backtrace::Backtrace; #[cfg(feature = "cookies")] use cookie; use derive_more::{Display, From}; @@ -47,7 +46,6 @@ pub type Result = result::Result; /// `ResponseError` reference from it. pub struct Error { cause: Box, - backtrace: Option, } impl Error { @@ -56,18 +54,6 @@ impl Error { self.cause.as_ref() } - /// Returns a reference to the Backtrace carried by this error, if it - /// carries one. - /// - /// This uses the same `Backtrace` type that `failure` uses. - pub fn backtrace(&self) -> &Backtrace { - if let Some(bt) = self.cause.backtrace() { - bt - } else { - self.backtrace.as_ref().unwrap() - } - } - /// Converts error to a response instance and set error message as response body pub fn response_with_message(self) -> Response { let message = format!("{}", self); @@ -84,11 +70,6 @@ pub trait ResponseError: fmt::Debug + fmt::Display { fn error_response(&self) -> Response { Response::new(StatusCode::INTERNAL_SERVER_ERROR) } - - /// Response - fn backtrace(&self) -> Option<&Backtrace> { - None - } } impl fmt::Display for Error { @@ -99,16 +80,7 @@ impl fmt::Display for Error { impl fmt::Debug for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(bt) = self.cause.backtrace() { - write!(f, "{:?}\n\n{:?}", &self.cause, bt) - } else { - write!( - f, - "{:?}\n\n{:?}", - &self.cause, - self.backtrace.as_ref().unwrap() - ) - } + write!(f, "{:?}\n", &self.cause) } } @@ -122,14 +94,8 @@ impl From for Response { /// `Error` for any error that implements `ResponseError` impl From for Error { fn from(err: T) -> Error { - let backtrace = if err.backtrace().is_none() { - Some(Backtrace::new()) - } else { - None - }; Error { cause: Box::new(err), - backtrace, } } } @@ -412,7 +378,6 @@ impl ResponseError for ContentTypeError { pub struct InternalError { cause: T, status: InternalErrorType, - backtrace: Backtrace, } enum InternalErrorType { @@ -426,7 +391,6 @@ impl InternalError { InternalError { cause, status: InternalErrorType::Status(status), - backtrace: Backtrace::new(), } } @@ -435,7 +399,6 @@ impl InternalError { InternalError { cause, status: InternalErrorType::Response(RefCell::new(Some(response))), - backtrace: Backtrace::new(), } } } @@ -462,10 +425,6 @@ impl ResponseError for InternalError where T: fmt::Debug + fmt::Display + 'static, { - fn backtrace(&self) -> Option<&Backtrace> { - Some(&self.backtrace) - } - fn error_response(&self) -> Response { match self.status { InternalErrorType::Status(st) => Response::new(st), @@ -922,12 +881,6 @@ mod tests { assert_eq!(format!("{}", e.as_response_error()), "IO error: other"); } - #[test] - fn test_backtrace() { - let e = ErrorBadRequest("err"); - let _ = e.backtrace(); - } - #[test] fn test_error_cause() { let orig = io::Error::new(io::ErrorKind::Other, "other"); From 3edc515bacccb95ce47ea46ab8ad265d248b60c4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 27 Mar 2019 10:38:01 -0700 Subject: [PATCH 360/427] refactor RequestHead/ResponseHead --- actix-http/src/encoding/encoder.rs | 6 +- actix-http/src/h1/client.rs | 2 +- actix-http/src/h1/codec.rs | 2 +- actix-http/src/h1/decoder.rs | 27 +-- actix-http/src/h1/encoder.rs | 18 +- actix-http/src/lib.rs | 2 +- actix-http/src/message.rs | 250 +++++++++++++++++----------- actix-http/src/response.rs | 6 +- actix-http/src/ws/client/service.rs | 2 +- actix-web-actors/src/ws.rs | 2 +- awc/src/request.rs | 2 +- src/lib.rs | 2 +- src/middleware/cors.rs | 2 +- 13 files changed, 191 insertions(+), 132 deletions(-) diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index af861b9d7..fcac3a427 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -12,7 +12,7 @@ use flate2::write::{GzEncoder, ZlibEncoder}; use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::http::header::{ContentEncoding, CONTENT_ENCODING}; use crate::http::{HeaderValue, HttpTryFrom, StatusCode}; -use crate::{Error, Head, ResponseHead}; +use crate::{Error, ResponseHead}; use super::Writer; @@ -56,7 +56,7 @@ impl Encoder { }) } else { update_head(encoding, head); - head.no_chunking = false; + head.no_chunking(false); ResponseBody::Body(Encoder { body: EncoderBody::Other(stream), encoder: ContentEncoder::encoder(encoding), @@ -72,7 +72,7 @@ impl Encoder { }) } else { update_head(encoding, head); - head.no_chunking = false; + head.no_chunking(false); ResponseBody::Body(Encoder { body: EncoderBody::Body(stream), encoder: ContentEncoder::encoder(encoding), diff --git a/actix-http/src/h1/client.rs b/actix-http/src/h1/client.rs index fbdc8bde1..6a50c0271 100644 --- a/actix-http/src/h1/client.rs +++ b/actix-http/src/h1/client.rs @@ -129,7 +129,7 @@ impl Decoder for ClientCodec { debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set"); if let Some((req, payload)) = self.inner.decoder.decode(src)? { - if let Some(ctype) = req.ctype { + if let Some(ctype) = req.ctype() { // do not use peer's keep-alive self.inner.ctype = if ctype == ConnectionType::KeepAlive { self.inner.ctype diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index 9bb417091..ceb1027e5 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -154,7 +154,7 @@ impl Encoder for Codec { res.head_mut().version = self.version; // connection status - self.ctype = if let Some(ct) = res.head().ctype { + self.ctype = if let Some(ct) = res.head().ctype() { if ct == ConnectionType::KeepAlive { self.ctype } else { diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index a1b221c06..9b97713fb 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -158,7 +158,9 @@ pub(crate) trait MessageType: Sized { impl MessageType for Request { fn set_connection_type(&mut self, ctype: Option) { - self.head_mut().ctype = ctype; + if let Some(ctype) = ctype { + self.head_mut().set_connection_type(ctype); + } } fn headers_mut(&mut self) -> &mut HeaderMap { @@ -228,7 +230,9 @@ impl MessageType for Request { impl MessageType for ResponseHead { fn set_connection_type(&mut self, ctype: Option) { - self.ctype = ctype; + if let Some(ctype) = ctype { + ResponseHead::set_connection_type(self, ctype); + } } fn headers_mut(&mut self) -> &mut HeaderMap { @@ -814,7 +818,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert_eq!(req.head().ctype, Some(ConnectionType::Close)); + assert_eq!(req.head().connection_type(), ConnectionType::Close); let mut buf = BytesMut::from( "GET /test HTTP/1.1\r\n\ @@ -822,7 +826,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert_eq!(req.head().ctype, Some(ConnectionType::Close)); + assert_eq!(req.head().connection_type(), ConnectionType::Close); } #[test] @@ -834,7 +838,7 @@ mod tests { let req = parse_ready!(&mut buf); - assert_eq!(req.head().ctype, Some(ConnectionType::Close)); + assert_eq!(req.head().connection_type(), ConnectionType::Close); } #[test] @@ -845,7 +849,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert_eq!(req.head().ctype, Some(ConnectionType::KeepAlive)); + assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); let mut buf = BytesMut::from( "GET /test HTTP/1.0\r\n\ @@ -853,7 +857,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert_eq!(req.head().ctype, Some(ConnectionType::KeepAlive)); + assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); } #[test] @@ -864,7 +868,7 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert_eq!(req.head().ctype, Some(ConnectionType::KeepAlive)); + assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); } #[test] @@ -886,7 +890,6 @@ mod tests { ); let req = parse_ready!(&mut buf); - assert_eq!(req.head().ctype, None); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); } @@ -900,7 +903,7 @@ mod tests { let req = parse_ready!(&mut buf); assert!(req.upgrade()); - assert_eq!(req.head().ctype, Some(ConnectionType::Upgrade)); + assert_eq!(req.head().connection_type(), ConnectionType::Upgrade); let mut buf = BytesMut::from( "GET /test HTTP/1.1\r\n\ @@ -910,7 +913,7 @@ mod tests { let req = parse_ready!(&mut buf); assert!(req.upgrade()); - assert_eq!(req.head().ctype, Some(ConnectionType::Upgrade)); + assert_eq!(req.head().connection_type(), ConnectionType::Upgrade); } #[test] @@ -1008,7 +1011,7 @@ mod tests { ); let mut reader = MessageDecoder::::default(); let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); - assert_eq!(req.head().ctype, Some(ConnectionType::Upgrade)); + assert_eq!(req.head().connection_type(), ConnectionType::Upgrade); assert!(req.upgrade()); assert!(pl.is_unhandled()); } diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index dbf6d440f..382ebe5f1 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -15,7 +15,7 @@ use crate::body::BodySize; use crate::config::ServiceConfig; use crate::header::ContentEncoding; use crate::helpers; -use crate::message::{ConnectionType, RequestHead, ResponseHead}; +use crate::message::{ConnectionType, Head, RequestHead, ResponseHead}; use crate::request::Request; use crate::response::Response; @@ -41,7 +41,7 @@ impl Default for MessageEncoder { pub(crate) trait MessageType: Sized { fn status(&self) -> Option; - fn connection_type(&self) -> Option; + // fn connection_type(&self) -> Option; fn headers(&self) -> &HeaderMap; @@ -168,12 +168,12 @@ impl MessageType for Response<()> { } fn chunked(&self) -> bool { - !self.head().no_chunking + self.head().chunked() } - fn connection_type(&self) -> Option { - self.head().ctype - } + //fn connection_type(&self) -> Option { + // self.head().ctype + //} fn headers(&self) -> &HeaderMap { &self.head().headers @@ -196,12 +196,8 @@ impl MessageType for RequestHead { None } - fn connection_type(&self) -> Option { - self.ctype - } - fn chunked(&self) -> bool { - !self.no_chunking + self.chunked() } fn headers(&self) -> &HeaderMap { diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index edc06c2a6..565fe4455 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -35,7 +35,7 @@ pub use self::config::{KeepAlive, ServiceConfig}; pub use self::error::{Error, ResponseError, Result}; pub use self::extensions::Extensions; pub use self::httpmessage::HttpMessage; -pub use self::message::{Head, Message, RequestHead, ResponseHead}; +pub use self::message::{Message, RequestHead, ResponseHead}; pub use self::payload::{Payload, PayloadStream}; pub use self::request::Request; pub use self::response::{Response, ResponseBuilder}; diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index 4e46093f6..a1e9e3c60 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -2,6 +2,8 @@ use std::cell::{Ref, RefCell, RefMut}; use std::collections::VecDeque; use std::rc::Rc; +use bitflags::bitflags; + use crate::extensions::Extensions; use crate::http::{header, HeaderMap, Method, StatusCode, Uri, Version}; @@ -16,39 +18,22 @@ pub enum ConnectionType { Upgrade, } +bitflags! { + pub(crate) struct Flags: u8 { + const CLOSE = 0b0000_0001; + const KEEP_ALIVE = 0b0000_0010; + const UPGRADE = 0b0000_0100; + const NO_CHUNKING = 0b0000_1000; + const ENC_BR = 0b0001_0000; + const ENC_DEFLATE = 0b0010_0000; + const ENC_GZIP = 0b0100_0000; + } +} + #[doc(hidden)] pub trait Head: Default + 'static { fn clear(&mut self); - /// Read the message headers. - fn headers(&self) -> &HeaderMap; - - /// Mutable reference to the message headers. - fn headers_mut(&mut self) -> &mut HeaderMap; - - /// Connection type - fn connection_type(&self) -> ConnectionType; - - /// Set connection type of the message - fn set_connection_type(&mut self, ctype: ConnectionType); - - fn upgrade(&self) -> bool { - if let Some(hdr) = self.headers().get(header::CONNECTION) { - if let Ok(s) = hdr.to_str() { - s.to_ascii_lowercase().contains("upgrade") - } else { - false - } - } else { - false - } - } - - /// Check if keep-alive is enabled - fn keep_alive(&self) -> bool { - self.connection_type() == ConnectionType::KeepAlive - } - fn pool() -> &'static MessagePool; } @@ -58,9 +43,8 @@ pub struct RequestHead { pub method: Method, pub version: Version, pub headers: HeaderMap, - pub ctype: Option, - pub no_chunking: bool, pub extensions: RefCell, + flags: Flags, } impl Default for RequestHead { @@ -70,8 +54,7 @@ impl Default for RequestHead { method: Method::default(), version: Version::HTTP_11, headers: HeaderMap::with_capacity(16), - ctype: None, - no_chunking: false, + flags: Flags::empty(), extensions: RefCell::new(Extensions::new()), } } @@ -79,45 +62,11 @@ impl Default for RequestHead { impl Head for RequestHead { fn clear(&mut self) { - self.ctype = None; + self.flags = Flags::empty(); self.headers.clear(); self.extensions.borrow_mut().clear(); } - fn headers(&self) -> &HeaderMap { - &self.headers - } - - fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.headers - } - - fn set_connection_type(&mut self, ctype: ConnectionType) { - self.ctype = Some(ctype) - } - - fn connection_type(&self) -> ConnectionType { - if let Some(ct) = self.ctype { - ct - } else if self.version < Version::HTTP_11 { - ConnectionType::Close - } else { - ConnectionType::KeepAlive - } - } - - fn upgrade(&self) -> bool { - if let Some(hdr) = self.headers().get(header::CONNECTION) { - if let Ok(s) = hdr.to_str() { - s.to_ascii_lowercase().contains("upgrade") - } else { - false - } - } else { - false - } - } - fn pool() -> &'static MessagePool { REQUEST_POOL.with(|p| *p) } @@ -135,6 +84,70 @@ impl RequestHead { pub fn extensions_mut(&self) -> RefMut { self.extensions.borrow_mut() } + + /// Read the message headers. + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + /// Mutable reference to the message headers. + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.headers + } + + #[inline] + /// Set connection type of the message + pub fn set_connection_type(&mut self, ctype: ConnectionType) { + match ctype { + ConnectionType::Close => self.flags.insert(Flags::CLOSE), + ConnectionType::KeepAlive => self.flags.insert(Flags::KEEP_ALIVE), + ConnectionType::Upgrade => self.flags.insert(Flags::UPGRADE), + } + } + + #[inline] + /// Connection type + pub fn connection_type(&self) -> ConnectionType { + if self.flags.contains(Flags::CLOSE) { + ConnectionType::Close + } else if self.flags.contains(Flags::KEEP_ALIVE) { + ConnectionType::KeepAlive + } else if self.flags.contains(Flags::UPGRADE) { + ConnectionType::Upgrade + } else if self.version < Version::HTTP_11 { + ConnectionType::Close + } else { + ConnectionType::KeepAlive + } + } + + /// Connection upgrade status + pub fn upgrade(&self) -> bool { + if let Some(hdr) = self.headers().get(header::CONNECTION) { + if let Ok(s) = hdr.to_str() { + s.to_ascii_lowercase().contains("upgrade") + } else { + false + } + } else { + false + } + } + + #[inline] + /// Get response body chunking state + pub fn chunked(&self) -> bool { + !self.flags.contains(Flags::NO_CHUNKING) + } + + #[inline] + pub fn no_chunking(&mut self, val: bool) { + if val { + self.flags.insert(Flags::NO_CHUNKING); + } else { + self.flags.remove(Flags::NO_CHUNKING); + } + } } #[derive(Debug)] @@ -143,9 +156,8 @@ pub struct ResponseHead { pub status: StatusCode, pub headers: HeaderMap, pub reason: Option<&'static str>, - pub no_chunking: bool, - pub(crate) ctype: Option, pub(crate) extensions: RefCell, + flags: Flags, } impl Default for ResponseHead { @@ -155,13 +167,24 @@ impl Default for ResponseHead { status: StatusCode::OK, headers: HeaderMap::with_capacity(16), reason: None, - no_chunking: false, - ctype: None, + flags: Flags::empty(), extensions: RefCell::new(Extensions::new()), } } } +impl Head for ResponseHead { + fn clear(&mut self) { + self.reason = None; + self.flags = Flags::empty(); + self.headers.clear(); + } + + fn pool() -> &'static MessagePool { + RESPONSE_POOL.with(|p| *p) + } +} + impl ResponseHead { /// Message extensions #[inline] @@ -174,31 +197,37 @@ impl ResponseHead { pub fn extensions_mut(&self) -> RefMut { self.extensions.borrow_mut() } -} -impl Head for ResponseHead { - fn clear(&mut self) { - self.ctype = None; - self.reason = None; - self.no_chunking = false; - self.headers.clear(); - } - - fn headers(&self) -> &HeaderMap { + #[inline] + /// Read the message headers. + pub fn headers(&self) -> &HeaderMap { &self.headers } - fn headers_mut(&mut self) -> &mut HeaderMap { + #[inline] + /// Mutable reference to the message headers. + pub fn headers_mut(&mut self) -> &mut HeaderMap { &mut self.headers } - fn set_connection_type(&mut self, ctype: ConnectionType) { - self.ctype = Some(ctype) + #[inline] + /// Set connection type of the message + pub fn set_connection_type(&mut self, ctype: ConnectionType) { + match ctype { + ConnectionType::Close => self.flags.insert(Flags::CLOSE), + ConnectionType::KeepAlive => self.flags.insert(Flags::KEEP_ALIVE), + ConnectionType::Upgrade => self.flags.insert(Flags::UPGRADE), + } } - fn connection_type(&self) -> ConnectionType { - if let Some(ct) = self.ctype { - ct + #[inline] + pub fn connection_type(&self) -> ConnectionType { + if self.flags.contains(Flags::CLOSE) { + ConnectionType::Close + } else if self.flags.contains(Flags::KEEP_ALIVE) { + ConnectionType::KeepAlive + } else if self.flags.contains(Flags::UPGRADE) { + ConnectionType::Upgrade } else if self.version < Version::HTTP_11 { ConnectionType::Close } else { @@ -206,16 +235,18 @@ impl Head for ResponseHead { } } - fn upgrade(&self) -> bool { + #[inline] + /// Check if keep-alive is enabled + pub fn keep_alive(&self) -> bool { + self.connection_type() == ConnectionType::KeepAlive + } + + #[inline] + /// Check upgrade status of this message + pub fn upgrade(&self) -> bool { self.connection_type() == ConnectionType::Upgrade } - fn pool() -> &'static MessagePool { - RESPONSE_POOL.with(|p| *p) - } -} - -impl ResponseHead { /// Get custom reason for the response #[inline] pub fn reason(&self) -> &str { @@ -227,6 +258,35 @@ impl ResponseHead { .unwrap_or("") } } + + #[inline] + pub(crate) fn ctype(&self) -> Option { + if self.flags.contains(Flags::CLOSE) { + Some(ConnectionType::Close) + } else if self.flags.contains(Flags::KEEP_ALIVE) { + Some(ConnectionType::KeepAlive) + } else if self.flags.contains(Flags::UPGRADE) { + Some(ConnectionType::Upgrade) + } else { + None + } + } + + #[inline] + /// Get response body chunking state + pub fn chunked(&self) -> bool { + !self.flags.contains(Flags::NO_CHUNKING) + } + + #[inline] + /// Set no chunking for payload + pub fn no_chunking(&mut self, val: bool) { + if val { + self.flags.insert(Flags::NO_CHUNKING); + } else { + self.flags.remove(Flags::NO_CHUNKING); + } + } } pub struct Message { diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 31c0010ab..3b33e1f91 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -15,7 +15,7 @@ use serde_json; use crate::body::{Body, BodyStream, MessageBody, ResponseBody}; use crate::error::Error; use crate::header::{Header, IntoHeaderValue}; -use crate::message::{ConnectionType, Head, Message, ResponseHead}; +use crate::message::{ConnectionType, Message, ResponseHead}; /// An HTTP Response pub struct Response { @@ -462,7 +462,7 @@ impl ResponseBuilder { #[inline] pub fn no_chunking(&mut self) -> &mut Self { if let Some(parts) = parts(&mut self.head, &self.err) { - parts.no_chunking = true; + parts.no_chunking(true); } self } @@ -740,7 +740,7 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder { msg.status = head.status; msg.reason = head.reason; msg.headers = head.headers.clone(); - msg.no_chunking = head.no_chunking; + msg.no_chunking(!head.chunked()); ResponseBuilder { head: Some(msg), diff --git a/actix-http/src/ws/client/service.rs b/actix-http/src/ws/client/service.rs index cb3fb6f39..bc86e516a 100644 --- a/actix-http/src/ws/client/service.rs +++ b/actix-http/src/ws/client/service.rs @@ -15,7 +15,7 @@ use sha1::Sha1; use crate::body::BodySize; use crate::h1; -use crate::message::{ConnectionType, Head, ResponseHead}; +use crate::message::{ConnectionType, ResponseHead}; use crate::ws::Codec; use super::{ClientError, Connect, Protocol}; diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index cef5080c6..436011888 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -17,7 +17,7 @@ pub use actix_http::ws::{ CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError, }; -use actix_web::dev::{Head, HttpResponseBuilder}; +use actix_web::dev::HttpResponseBuilder; use actix_web::error::{Error, ErrorInternalServerError, PayloadError}; use actix_web::http::{header, Method, StatusCode}; use actix_web::{HttpMessage, HttpRequest, HttpResponse}; diff --git a/awc/src/request.rs b/awc/src/request.rs index d25ecc421..7beb737e1 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -19,7 +19,7 @@ use actix_http::http::{ uri, ConnectionType, Error as HttpError, HeaderName, HeaderValue, HttpTryFrom, Method, Uri, Version, }; -use actix_http::{Error, Head, Payload, RequestHead}; +use actix_http::{Error, Payload, RequestHead}; use crate::response::ClientResponse; use crate::{Connect, PayloadError}; diff --git a/src/lib.rs b/src/lib.rs index 54709b47b..5b0ce7841 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,7 +145,7 @@ pub mod dev { pub use actix_http::body::{Body, BodySize, MessageBody, ResponseBody}; pub use actix_http::ResponseBuilder as HttpResponseBuilder; pub use actix_http::{ - Extensions, Head, Payload, PayloadStream, RequestHead, ResponseHead, + Extensions, Payload, PayloadStream, RequestHead, ResponseHead, }; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::Server; diff --git a/src/middleware/cors.rs b/src/middleware/cors.rs index b6acf4299..2ece543d2 100644 --- a/src/middleware/cors.rs +++ b/src/middleware/cors.rs @@ -46,7 +46,7 @@ use derive_more::Display; use futures::future::{ok, Either, Future, FutureResult}; use futures::Poll; -use crate::dev::{Head, RequestHead}; +use crate::dev::RequestHead; use crate::error::{ResponseError, Result}; use crate::http::header::{self, HeaderName, HeaderValue}; use crate::http::{self, HttpTryFrom, Method, StatusCode, Uri}; From e254fe4f9c37da954c4bf544ca30207415dbd426 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 27 Mar 2019 11:29:31 -0700 Subject: [PATCH 361/427] allow to override response body encoding --- actix-files/src/named.rs | 12 ++++---- actix-http/src/message.rs | 3 -- actix-http/src/response.rs | 16 ++++++++++ examples/basic.rs | 2 +- src/middleware/compress.rs | 40 ++++++++++++++++++++++++- src/middleware/decompress.rs | 16 ++++++++++ src/middleware/mod.rs | 9 +++--- tests/test_server.rs | 58 ++++++++++++++++++++++++++---------- 8 files changed, 125 insertions(+), 31 deletions(-) diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 7bc37054a..842a0e5e0 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -15,6 +15,7 @@ use actix_web::http::header::{ self, ContentDisposition, DispositionParam, DispositionType, }; use actix_web::http::{ContentEncoding, Method, StatusCode}; +use actix_web::middleware::encoding::BodyEncoding; use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder}; use crate::range::HttpRange; @@ -360,10 +361,10 @@ impl Responder for NamedFile { header::CONTENT_DISPOSITION, self.content_disposition.to_string(), ); - // TODO blocking by compressing - // if let Some(current_encoding) = self.encoding { - // resp.content_encoding(current_encoding); - // } + // default compressing + if let Some(current_encoding) = self.encoding { + resp.encoding(current_encoding); + } resp.if_some(last_modified, |lm, resp| { resp.set(header::LastModified(lm)); @@ -383,8 +384,7 @@ impl Responder for NamedFile { if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) { length = rangesvec[0].length; offset = rangesvec[0].start; - // TODO blocking by compressing - // resp.content_encoding(ContentEncoding::Identity); + resp.encoding(ContentEncoding::Identity); resp.header( header::CONTENT_RANGE, format!( diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index a1e9e3c60..3466f66df 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -24,9 +24,6 @@ bitflags! { const KEEP_ALIVE = 0b0000_0010; const UPGRADE = 0b0000_0100; const NO_CHUNKING = 0b0000_1000; - const ENC_BR = 0b0001_0000; - const ENC_DEFLATE = 0b0010_0000; - const ENC_GZIP = 0b0100_0000; } } diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 3b33e1f91..29a850fae 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -1,4 +1,5 @@ //! Http response +use std::cell::{Ref, RefMut}; use std::io::Write; use std::{fmt, str}; @@ -14,6 +15,7 @@ use serde_json; use crate::body::{Body, BodyStream, MessageBody, ResponseBody}; use crate::error::Error; +use crate::extensions::Extensions; use crate::header::{Header, IntoHeaderValue}; use crate::message::{ConnectionType, Message, ResponseHead}; @@ -577,6 +579,20 @@ impl ResponseBuilder { self } + /// Responses extensions + #[inline] + pub fn extensions(&self) -> Ref { + let head = self.head.as_ref().expect("cannot reuse response builder"); + head.extensions.borrow() + } + + /// Mutable reference to a the response's extensions + #[inline] + pub fn extensions_mut(&mut self) -> RefMut { + let head = self.head.as_ref().expect("cannot reuse response builder"); + head.extensions.borrow_mut() + } + /// Set a body and generate `Response`. /// /// `ResponseBuilder` can not be used after this call. diff --git a/examples/basic.rs b/examples/basic.rs index 911196570..1191b371c 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -27,7 +27,7 @@ fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .wrap(middleware::DefaultHeaders::new().header("X-Version", "0.2")) - .wrap(middleware::Compress::default()) + .wrap(middleware::encoding::Compress::default()) .wrap(middleware::Logger::default()) .service(index) .service(no_params) diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 5ffe9afb1..5c6bad874 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -6,14 +6,46 @@ use std::str::FromStr; use actix_http::body::MessageBody; use actix_http::encoding::Encoder; use actix_http::http::header::{ContentEncoding, ACCEPT_ENCODING}; +use actix_http::ResponseBuilder; use actix_service::{Service, Transform}; use futures::future::{ok, FutureResult}; use futures::{Async, Future, Poll}; use crate::service::{ServiceRequest, ServiceResponse}; +struct Enc(ContentEncoding); + +/// Helper trait that allows to set specific encoding for response. +pub trait BodyEncoding { + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self; +} + +impl BodyEncoding for ResponseBuilder { + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(Enc(encoding)); + self + } +} + #[derive(Debug, Clone)] /// `Middleware` for compressing response body. +/// +/// Use `BodyEncoding` trait for overriding response compression. +/// To disable compression set encoding to `ContentEncoding::Identity` value. +/// +/// ```rust +/// use actix_web::{web, middleware::encoding, App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new() +/// .wrap(encoding::Compress::default()) +/// .service( +/// web::resource("/test") +/// .route(web::get().to(|| HttpResponse::Ok())) +/// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) +/// ); +/// } +/// ``` pub struct Compress(ContentEncoding); impl Compress { @@ -118,8 +150,14 @@ where fn poll(&mut self) -> Poll { let resp = futures::try_ready!(self.fut.poll()); + let enc = if let Some(enc) = resp.head().extensions().get::() { + enc.0 + } else { + self.encoding + }; + Ok(Async::Ready(resp.map_body(move |head, body| { - Encoder::response(self.encoding, head, body) + Encoder::response(enc, head, body) }))) } } diff --git a/src/middleware/decompress.rs b/src/middleware/decompress.rs index d0a9bfd24..eaffbbdb4 100644 --- a/src/middleware/decompress.rs +++ b/src/middleware/decompress.rs @@ -12,6 +12,22 @@ use crate::error::{Error, PayloadError}; use crate::service::ServiceRequest; use crate::HttpMessage; +/// `Middleware` for decompressing request's payload. +/// `Decompress` middleware must be added with `App::chain()` method. +/// +/// ```rust +/// use actix_web::{web, middleware::encoding, App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new() +/// .chain(encoding::Decompress::new()) +/// .service( +/// web::resource("/test") +/// .route(web::get().to(|| HttpResponse::Ok())) +/// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) +/// ); +/// } +/// ``` pub struct Decompress

    (PhantomData

    ); impl

    Decompress

    diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index aee0ae3df..037d00067 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1,13 +1,14 @@ //! Middlewares #[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] mod compress; -#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] -pub use self::compress::Compress; - #[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] mod decompress; #[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] -pub use self::decompress::Decompress; +pub mod encoding { + //! Middlewares for compressing/decompressing payloads. + pub use super::compress::{BodyEncoding, Compress}; + pub use super::decompress::Decompress; +} pub mod cors; mod defaultheaders; diff --git a/tests/test_server.rs b/tests/test_server.rs index 29998bc06..364f92626 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -14,7 +14,7 @@ use flate2::Compression; use futures::stream::once; //Future, Stream use rand::{distributions::Alphanumeric, Rng}; -use actix_web::{dev::HttpMessageBody, middleware, web, App}; +use actix_web::{dev::HttpMessageBody, middleware::encoding, web, App}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -60,7 +60,7 @@ fn test_body_gzip() { let mut srv = TestServer::new(|| { h1::H1Service::new( App::new() - .wrap(middleware::Compress::new(ContentEncoding::Gzip)) + .wrap(encoding::Compress::new(ContentEncoding::Gzip)) .service(web::resource("/").route(web::to(|| Response::Ok().body(STR)))), ) }); @@ -78,6 +78,32 @@ fn test_body_gzip() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } +#[test] +fn test_body_encoding_override() { + let mut srv = TestServer::new(|| { + h1::H1Service::new( + App::new() + .wrap(encoding::Compress::new(ContentEncoding::Gzip)) + .service(web::resource("/").route(web::to(|| { + use actix_web::middleware::encoding::BodyEncoding; + Response::Ok().encoding(ContentEncoding::Deflate).body(STR) + }))), + ) + }); + + let mut response = srv.block_on(srv.get().no_decompress().send()).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + + // decode + let mut e = ZlibDecoder::new(Vec::new()); + e.write_all(bytes.as_ref()).unwrap(); + let dec = e.finish().unwrap(); + assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); +} + #[test] fn test_body_gzip_large() { let data = STR.repeat(10); @@ -87,7 +113,7 @@ fn test_body_gzip_large() { let data = srv_data.clone(); h1::H1Service::new( App::new() - .wrap(middleware::Compress::new(ContentEncoding::Gzip)) + .wrap(encoding::Compress::new(ContentEncoding::Gzip)) .service( web::resource("/") .route(web::to(move || Response::Ok().body(data.clone()))), @@ -120,7 +146,7 @@ fn test_body_gzip_large_random() { let data = srv_data.clone(); h1::H1Service::new( App::new() - .wrap(middleware::Compress::new(ContentEncoding::Gzip)) + .wrap(encoding::Compress::new(ContentEncoding::Gzip)) .service( web::resource("/") .route(web::to(move || Response::Ok().body(data.clone()))), @@ -147,7 +173,7 @@ fn test_body_chunked_implicit() { let mut srv = TestServer::new(move || { h1::H1Service::new( App::new() - .wrap(middleware::Compress::new(ContentEncoding::Gzip)) + .wrap(encoding::Compress::new(ContentEncoding::Gzip)) .service(web::resource("/").route(web::get().to(move || { Response::Ok().streaming(once(Ok::<_, Error>(Bytes::from_static( STR.as_ref(), @@ -178,7 +204,7 @@ fn test_body_br_streaming() { let mut srv = TestServer::new(move || { h1::H1Service::new( App::new() - .wrap(middleware::Compress::new(ContentEncoding::Br)) + .wrap(encoding::Compress::new(ContentEncoding::Br)) .service(web::resource("/").route(web::to(move || { Response::Ok().streaming(once(Ok::<_, Error>(Bytes::from_static( STR.as_ref(), @@ -255,7 +281,7 @@ fn test_body_deflate() { let mut srv = TestServer::new(move || { h1::H1Service::new( App::new() - .wrap(middleware::Compress::new(ContentEncoding::Deflate)) + .wrap(encoding::Compress::new(ContentEncoding::Deflate)) .service( web::resource("/").route(web::to(move || Response::Ok().body(STR))), ), @@ -281,7 +307,7 @@ fn test_body_brotli() { let mut srv = TestServer::new(move || { h1::H1Service::new( App::new() - .wrap(middleware::Compress::new(ContentEncoding::Br)) + .wrap(encoding::Compress::new(ContentEncoding::Br)) .service( web::resource("/").route(web::to(move || Response::Ok().body(STR))), ), @@ -313,7 +339,7 @@ fn test_body_brotli() { fn test_gzip_encoding() { let mut srv = TestServer::new(move || { HttpService::new( - App::new().chain(middleware::Decompress::new()).service( + App::new().chain(encoding::Decompress::new()).service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -342,7 +368,7 @@ fn test_gzip_encoding_large() { let data = STR.repeat(10); let mut srv = TestServer::new(move || { h1::H1Service::new( - App::new().chain(middleware::Decompress::new()).service( + App::new().chain(encoding::Decompress::new()).service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -375,7 +401,7 @@ fn test_reading_gzip_encoding_large_random() { let mut srv = TestServer::new(move || { HttpService::new( - App::new().chain(middleware::Decompress::new()).service( + App::new().chain(encoding::Decompress::new()).service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -404,7 +430,7 @@ fn test_reading_gzip_encoding_large_random() { fn test_reading_deflate_encoding() { let mut srv = TestServer::new(move || { h1::H1Service::new( - App::new().chain(middleware::Decompress::new()).service( + App::new().chain(encoding::Decompress::new()).service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -433,7 +459,7 @@ fn test_reading_deflate_encoding_large() { let data = STR.repeat(10); let mut srv = TestServer::new(move || { h1::H1Service::new( - App::new().chain(middleware::Decompress::new()).service( + App::new().chain(encoding::Decompress::new()).service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -466,7 +492,7 @@ fn test_reading_deflate_encoding_large_random() { let mut srv = TestServer::new(move || { h1::H1Service::new( - App::new().chain(middleware::Decompress::new()).service( + App::new().chain(encoding::Decompress::new()).service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -496,7 +522,7 @@ fn test_reading_deflate_encoding_large_random() { fn test_brotli_encoding() { let mut srv = TestServer::new(move || { h1::H1Service::new( - App::new().chain(middleware::Decompress::new()).service( + App::new().chain(encoding::Decompress::new()).service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -526,7 +552,7 @@ fn test_brotli_encoding_large() { let data = STR.repeat(10); let mut srv = TestServer::new(move || { h1::H1Service::new( - App::new().chain(middleware::Decompress::new()).service( + App::new().chain(encoding::Decompress::new()).service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), From c59937784e1dee1383f6e70f8b7eac5ca268a903 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 27 Mar 2019 18:53:19 -0700 Subject: [PATCH 362/427] add client websockets support --- actix-http/Cargo.toml | 2 +- actix-http/src/client/connection.rs | 145 ++++++- actix-http/src/client/error.rs | 3 + actix-http/src/client/h1proto.rs | 26 ++ actix-http/src/client/pool.rs | 60 --- actix-http/src/h1/decoder.rs | 1 - actix-http/src/ws/client/connect.rs | 109 ----- actix-http/src/ws/client/mod.rs | 48 --- actix-http/src/ws/client/service.rs | 272 ------------ actix-http/src/ws/mod.rs | 2 - awc/Cargo.toml | 6 + awc/src/connect.rs | 94 ++++- .../src/ws/client => awc/src}/error.rs | 47 +-- awc/src/lib.rs | 12 +- awc/src/request.rs | 4 +- awc/src/ws.rs | 398 ++++++++++++++++++ {actix-http => awc}/tests/test_ws.rs | 0 src/lib.rs | 6 +- test-server/src/lib.rs | 10 +- 19 files changed, 709 insertions(+), 536 deletions(-) delete mode 100644 actix-http/src/ws/client/connect.rs delete mode 100644 actix-http/src/ws/client/mod.rs delete mode 100644 actix-http/src/ws/client/service.rs rename {actix-http/src/ws/client => awc/src}/error.rs (55%) create mode 100644 awc/src/ws.rs rename {actix-http => awc}/tests/test_ws.rs (100%) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 809d4d677..fefe05c4c 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -49,7 +49,7 @@ fail = ["failure"] [dependencies] actix-service = "0.3.4" -actix-codec = "0.1.1" +actix-codec = "0.1.2" actix-connect = "0.1.0" actix-utils = "0.3.4" actix-server-config = "0.1.0" diff --git a/actix-http/src/client/connection.rs b/actix-http/src/client/connection.rs index e8c1201aa..267c85d31 100644 --- a/actix-http/src/client/connection.rs +++ b/actix-http/src/client/connection.rs @@ -1,11 +1,13 @@ -use std::{fmt, time}; +use std::{fmt, io, time}; -use actix_codec::{AsyncRead, AsyncWrite}; -use bytes::Bytes; -use futures::Future; +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use bytes::{Buf, Bytes}; +use futures::future::{err, Either, Future, FutureResult}; +use futures::Poll; use h2::client::SendRequest; use crate::body::MessageBody; +use crate::h1::ClientCodec; use crate::message::{RequestHead, ResponseHead}; use crate::payload::Payload; @@ -19,6 +21,7 @@ pub(crate) enum ConnectionType { } pub trait Connection { + type Io: AsyncRead + AsyncWrite; type Future: Future; /// Send request and body @@ -27,6 +30,14 @@ pub trait Connection { head: RequestHead, body: B, ) -> Self::Future; + + type TunnelFuture: Future< + Item = (ResponseHead, Framed), + Error = SendRequestError, + >; + + /// Send request, returns Response and Framed + fn open_tunnel(self, head: RequestHead) -> Self::TunnelFuture; } pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static { @@ -80,6 +91,7 @@ impl Connection for IoConnection where T: AsyncRead + AsyncWrite + 'static, { + type Io = T; type Future = Box>; fn send_request( @@ -104,6 +116,35 @@ where )), } } + + type TunnelFuture = Either< + Box< + Future< + Item = (ResponseHead, Framed), + Error = SendRequestError, + >, + >, + FutureResult<(ResponseHead, Framed), SendRequestError>, + >; + + /// Send request, returns Response and Framed + fn open_tunnel(mut self, head: RequestHead) -> Self::TunnelFuture { + match self.io.take().unwrap() { + ConnectionType::H1(io) => { + Either::A(Box::new(h1proto::open_tunnel(io, head))) + } + ConnectionType::H2(io) => { + if let Some(mut pool) = self.pool.take() { + pool.release(IoConnection::new( + ConnectionType::H2(io), + self.created, + None, + )); + } + Either::B(err(SendRequestError::TunnelNotSupported)) + } + } + } } #[allow(dead_code)] @@ -117,6 +158,7 @@ where A: AsyncRead + AsyncWrite + 'static, B: AsyncRead + AsyncWrite + 'static, { + type Io = EitherIo; type Future = Box>; fn send_request( @@ -129,4 +171,99 @@ where EitherConnection::B(con) => con.send_request(head, body), } } + + type TunnelFuture = Box< + Future< + Item = (ResponseHead, Framed), + Error = SendRequestError, + >, + >; + + /// Send request, returns Response and Framed + fn open_tunnel(self, head: RequestHead) -> Self::TunnelFuture { + match self { + EitherConnection::A(con) => Box::new( + con.open_tunnel(head) + .map(|(head, framed)| (head, framed.map_io(|io| EitherIo::A(io)))), + ), + EitherConnection::B(con) => Box::new( + con.open_tunnel(head) + .map(|(head, framed)| (head, framed.map_io(|io| EitherIo::B(io)))), + ), + } + } +} + +pub enum EitherIo { + A(A), + B(B), +} + +impl io::Read for EitherIo +where + A: io::Read, + B: io::Read, +{ + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match self { + EitherIo::A(ref mut val) => val.read(buf), + EitherIo::B(ref mut val) => val.read(buf), + } + } +} + +impl AsyncRead for EitherIo +where + A: AsyncRead, + B: AsyncRead, +{ + unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { + match self { + EitherIo::A(ref val) => val.prepare_uninitialized_buffer(buf), + EitherIo::B(ref val) => val.prepare_uninitialized_buffer(buf), + } + } +} + +impl io::Write for EitherIo +where + A: io::Write, + B: io::Write, +{ + fn write(&mut self, buf: &[u8]) -> io::Result { + match self { + EitherIo::A(ref mut val) => val.write(buf), + EitherIo::B(ref mut val) => val.write(buf), + } + } + + fn flush(&mut self) -> io::Result<()> { + match self { + EitherIo::A(ref mut val) => val.flush(), + EitherIo::B(ref mut val) => val.flush(), + } + } +} + +impl AsyncWrite for EitherIo +where + A: AsyncWrite, + B: AsyncWrite, +{ + fn shutdown(&mut self) -> Poll<(), io::Error> { + match self { + EitherIo::A(ref mut val) => val.shutdown(), + EitherIo::B(ref mut val) => val.shutdown(), + } + } + + fn write_buf(&mut self, buf: &mut U) -> Poll + where + Self: Sized, + { + match self { + EitherIo::A(ref mut val) => val.write_buf(buf), + EitherIo::B(ref mut val) => val.write_buf(buf), + } + } } diff --git a/actix-http/src/client/error.rs b/actix-http/src/client/error.rs index 69ec49585..e67db5462 100644 --- a/actix-http/src/client/error.rs +++ b/actix-http/src/client/error.rs @@ -105,6 +105,9 @@ pub enum SendRequestError { /// Http2 error #[display(fmt = "{}", _0)] H2(h2::Error), + /// Tunnels are not supported for http2 connection + #[display(fmt = "Tunnels are not supported for http2 connection")] + TunnelNotSupported, /// Error sending request body Body(Error), } diff --git a/actix-http/src/client/h1proto.rs b/actix-http/src/client/h1proto.rs index b7b8d4a0a..5fec9c4f1 100644 --- a/actix-http/src/client/h1proto.rs +++ b/actix-http/src/client/h1proto.rs @@ -70,6 +70,32 @@ where }) } +pub(crate) fn open_tunnel( + io: T, + head: RequestHead, +) -> impl Future), Error = SendRequestError> +where + T: AsyncRead + AsyncWrite + 'static, +{ + // create Framed and send reqest + Framed::new(io, h1::ClientCodec::default()) + .send((head, BodySize::None).into()) + .from_err() + // read response + .and_then(|framed| { + framed + .into_future() + .map_err(|(e, _)| SendRequestError::from(e)) + .and_then(|(head, framed)| { + if let Some(head) = head { + Ok((head, framed)) + } else { + Err(SendRequestError::from(ConnectError::Disconnected)) + } + }) + }) +} + #[doc(hidden)] /// HTTP client connection pub struct H1Connection { diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs index a94b1e52a..aff11181b 100644 --- a/actix-http/src/client/pool.rs +++ b/actix-http/src/client/pool.rs @@ -411,66 +411,6 @@ where } } -// struct ConnectorPoolSupport -// where -// Io: AsyncRead + AsyncWrite + 'static, -// { -// connector: T, -// inner: Rc>>, -// } - -// impl Future for ConnectorPoolSupport -// where -// Io: AsyncRead + AsyncWrite + 'static, -// T: Service, -// T::Future: 'static, -// { -// type Item = (); -// type Error = (); - -// fn poll(&mut self) -> Poll { -// let mut inner = self.inner.as_ref().borrow_mut(); -// inner.task.register(); - -// // check waiters -// loop { -// let (key, token) = { -// if let Some((key, token)) = inner.waiters_queue.get_index(0) { -// (key.clone(), *token) -// } else { -// break; -// } -// }; -// match inner.acquire(&key) { -// Acquire::NotAvailable => break, -// Acquire::Acquired(io, created) => { -// let (_, tx) = inner.waiters.remove(token); -// if let Err(conn) = tx.send(Ok(IoConnection::new( -// io, -// created, -// Some(Acquired(key.clone(), Some(self.inner.clone()))), -// ))) { -// let (io, created) = conn.unwrap().into_inner(); -// inner.release_conn(&key, io, created); -// } -// } -// Acquire::Available => { -// let (connect, tx) = inner.waiters.remove(token); -// OpenWaitingConnection::spawn( -// key.clone(), -// tx, -// self.inner.clone(), -// self.connector.call(connect), -// ); -// } -// } -// let _ = inner.waiters_queue.swap_remove_index(0); -// } - -// Ok(Async::NotReady) -// } -// } - struct CloseConnection { io: T, timeout: Delay, diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index 9b97713fb..dfd9fe25c 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -611,7 +611,6 @@ mod tests { use super::*; use crate::error::ParseError; use crate::httpmessage::HttpMessage; - use crate::message::Head; impl PayloadType { fn unwrap(self) -> PayloadDecoder { diff --git a/actix-http/src/ws/client/connect.rs b/actix-http/src/ws/client/connect.rs deleted file mode 100644 index 2760967e0..000000000 --- a/actix-http/src/ws/client/connect.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Http client request -use std::str; - -#[cfg(feature = "cookies")] -use cookie::Cookie; -use http::header::{HeaderName, HeaderValue}; -use http::{Error as HttpError, HttpTryFrom, Uri}; - -use super::ClientError; -use crate::header::IntoHeaderValue; -use crate::message::RequestHead; - -/// `WebSocket` connection -pub struct Connect { - pub(super) head: RequestHead, - pub(super) err: Option, - pub(super) http_err: Option, - pub(super) origin: Option, - pub(super) protocols: Option, - pub(super) max_size: usize, - pub(super) server_mode: bool, -} - -impl Connect { - /// Create new websocket connection - pub fn new>(uri: S) -> Connect { - let mut cl = Connect { - head: RequestHead::default(), - err: None, - http_err: None, - origin: None, - protocols: None, - max_size: 65_536, - server_mode: false, - }; - - match Uri::try_from(uri.as_ref()) { - Ok(uri) => cl.head.uri = uri, - Err(e) => cl.http_err = Some(e.into()), - } - - cl - } - - /// Set supported websocket protocols - pub fn protocols(mut self, protos: U) -> Self - where - U: IntoIterator + 'static, - V: AsRef, - { - let mut protos = protos - .into_iter() - .fold(String::new(), |acc, s| acc + s.as_ref() + ","); - protos.pop(); - self.protocols = Some(protos); - self - } - - // #[cfg(feature = "cookies")] - // /// Set cookie for handshake request - // pub fn cookie(mut self, cookie: Cookie) -> Self { - // self.request.cookie(cookie); - // self - // } - - /// Set request Origin - pub fn origin(mut self, origin: V) -> Self - where - HeaderValue: HttpTryFrom, - { - match HeaderValue::try_from(origin) { - Ok(value) => self.origin = Some(value), - Err(e) => self.http_err = Some(e.into()), - } - self - } - - /// Set max frame size - /// - /// By default max size is set to 64kb - pub fn max_frame_size(mut self, size: usize) -> Self { - self.max_size = size; - self - } - - /// Disable payload masking. By default ws client masks frame payload. - pub fn server_mode(mut self) -> Self { - self.server_mode = true; - self - } - - /// Set request header - pub fn header(mut self, key: K, value: V) -> Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - self.head.headers.append(key, value); - } - Err(e) => self.http_err = Some(e.into()), - }, - Err(e) => self.http_err = Some(e.into()), - } - self - } -} diff --git a/actix-http/src/ws/client/mod.rs b/actix-http/src/ws/client/mod.rs deleted file mode 100644 index a5c221967..000000000 --- a/actix-http/src/ws/client/mod.rs +++ /dev/null @@ -1,48 +0,0 @@ -mod connect; -mod error; -mod service; - -pub use self::connect::Connect; -pub use self::error::ClientError; -pub use self::service::Client; - -#[derive(PartialEq, Hash, Debug, Clone, Copy)] -pub(crate) enum Protocol { - Http, - Https, - Ws, - Wss, -} - -impl Protocol { - fn from(s: &str) -> Option { - match s { - "http" => Some(Protocol::Http), - "https" => Some(Protocol::Https), - "ws" => Some(Protocol::Ws), - "wss" => Some(Protocol::Wss), - _ => None, - } - } - - // fn is_http(self) -> bool { - // match self { - // Protocol::Https | Protocol::Http => true, - // _ => false, - // } - // } - - // fn is_secure(self) -> bool { - // match self { - // Protocol::Https | Protocol::Wss => true, - // _ => false, - // } - // } - - fn port(self) -> u16 { - match self { - Protocol::Http | Protocol::Ws => 80, - Protocol::Https | Protocol::Wss => 443, - } - } -} diff --git a/actix-http/src/ws/client/service.rs b/actix-http/src/ws/client/service.rs deleted file mode 100644 index bc86e516a..000000000 --- a/actix-http/src/ws/client/service.rs +++ /dev/null @@ -1,272 +0,0 @@ -//! websockets client -use std::marker::PhantomData; - -use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_connect::{default_connector, Connect as TcpConnect, ConnectError}; -use actix_service::{apply_fn, Service}; -use base64; -use futures::future::{err, Either, FutureResult}; -use futures::{try_ready, Async, Future, Poll, Sink, Stream}; -use http::header::{self, HeaderValue}; -use http::{HttpTryFrom, StatusCode}; -use log::trace; -use rand; -use sha1::Sha1; - -use crate::body::BodySize; -use crate::h1; -use crate::message::{ConnectionType, ResponseHead}; -use crate::ws::Codec; - -use super::{ClientError, Connect, Protocol}; - -/// WebSocket's client -pub struct Client { - connector: T, -} - -impl Client<()> { - /// Create client with default connector. - pub fn default() -> Client< - impl Service< - Request = TcpConnect, - Response = impl AsyncRead + AsyncWrite, - Error = ConnectError, - > + Clone, - > { - Client::new(apply_fn(default_connector(), |msg: TcpConnect<_>, srv| { - srv.call(msg).map(|stream| stream.into_parts().0) - })) - } -} - -impl Client -where - T: Service, Error = ConnectError>, - T::Response: AsyncRead + AsyncWrite, -{ - /// Create new websocket's client factory - pub fn new(connector: T) -> Self { - Client { connector } - } -} - -impl Clone for Client -where - T: Service, Error = ConnectError> + Clone, - T::Response: AsyncRead + AsyncWrite, -{ - fn clone(&self) -> Self { - Client { - connector: self.connector.clone(), - } - } -} - -impl Service for Client -where - T: Service, Error = ConnectError>, - T::Response: AsyncRead + AsyncWrite + 'static, - T::Future: 'static, -{ - type Request = Connect; - type Response = Framed; - type Error = ClientError; - type Future = Either< - FutureResult, - ClientResponseFut, - >; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.connector.poll_ready().map_err(ClientError::from) - } - - fn call(&mut self, mut req: Connect) -> Self::Future { - if let Some(e) = req.err.take() { - Either::A(err(e)) - } else if let Some(e) = req.http_err.take() { - Either::A(err(e.into())) - } else { - // origin - if let Some(origin) = req.origin.take() { - req.head.headers.insert(header::ORIGIN, origin); - } - - req.head.set_connection_type(ConnectionType::Upgrade); - req.head - .headers - .insert(header::UPGRADE, HeaderValue::from_static("websocket")); - req.head.headers.insert( - header::SEC_WEBSOCKET_VERSION, - HeaderValue::from_static("13"), - ); - - if let Some(protocols) = req.protocols.take() { - req.head.headers.insert( - header::SEC_WEBSOCKET_PROTOCOL, - HeaderValue::try_from(protocols.as_str()).unwrap(), - ); - } - if let Some(e) = req.http_err { - return Either::A(err(e.into())); - }; - - let mut request = req.head; - if request.uri.host().is_none() { - return Either::A(err(ClientError::InvalidUrl)); - } - - // supported protocols - let proto = if let Some(scheme) = request.uri.scheme_part() { - match Protocol::from(scheme.as_str()) { - Some(proto) => proto, - None => return Either::A(err(ClientError::InvalidUrl)), - } - } else { - return Either::A(err(ClientError::InvalidUrl)); - }; - - // Generate a random key for the `Sec-WebSocket-Key` header. - // a base64-encoded (see Section 4 of [RFC4648]) value that, - // when decoded, is 16 bytes in length (RFC 6455) - let sec_key: [u8; 16] = rand::random(); - let key = base64::encode(&sec_key); - - request.headers.insert( - header::SEC_WEBSOCKET_KEY, - HeaderValue::try_from(key.as_str()).unwrap(), - ); - - // prep connection - let connect = TcpConnect::new(request.uri.host().unwrap().to_string()) - .set_port(request.uri.port_u16().unwrap_or_else(|| proto.port())); - - let fut = Box::new( - self.connector - .call(connect) - .map_err(ClientError::from) - .and_then(move |io| { - // h1 protocol - let framed = Framed::new(io, h1::ClientCodec::default()); - framed - .send((request, BodySize::None).into()) - .map_err(ClientError::from) - .and_then(|framed| { - framed - .into_future() - .map_err(|(e, _)| ClientError::from(e)) - }) - }), - ); - - // start handshake - Either::B(ClientResponseFut { - key, - fut, - max_size: req.max_size, - server_mode: req.server_mode, - _t: PhantomData, - }) - } - } -} - -/// Future that implementes client websocket handshake process. -/// -/// It resolves to a `Framed` instance. -pub struct ClientResponseFut -where - T: AsyncRead + AsyncWrite, -{ - fut: Box< - Future< - Item = (Option, Framed), - Error = ClientError, - >, - >, - key: String, - max_size: usize, - server_mode: bool, - _t: PhantomData, -} - -impl Future for ClientResponseFut -where - T: AsyncRead + AsyncWrite, -{ - type Item = Framed; - type Error = ClientError; - - fn poll(&mut self) -> Poll { - let (item, framed) = try_ready!(self.fut.poll()); - - let res = match item { - Some(res) => res, - None => return Err(ClientError::Disconnected), - }; - - // verify response - if res.status != StatusCode::SWITCHING_PROTOCOLS { - return Err(ClientError::InvalidResponseStatus(res.status)); - } - // Check for "UPGRADE" to websocket header - let has_hdr = if let Some(hdr) = res.headers.get(header::UPGRADE) { - if let Ok(s) = hdr.to_str() { - s.to_lowercase().contains("websocket") - } else { - false - } - } else { - false - }; - if !has_hdr { - trace!("Invalid upgrade header"); - return Err(ClientError::InvalidUpgradeHeader); - } - // Check for "CONNECTION" header - if let Some(conn) = res.headers.get(header::CONNECTION) { - if let Ok(s) = conn.to_str() { - if !s.to_lowercase().contains("upgrade") { - trace!("Invalid connection header: {}", s); - return Err(ClientError::InvalidConnectionHeader(conn.clone())); - } - } else { - trace!("Invalid connection header: {:?}", conn); - return Err(ClientError::InvalidConnectionHeader(conn.clone())); - } - } else { - trace!("Missing connection header"); - return Err(ClientError::MissingConnectionHeader); - } - - if let Some(key) = res.headers.get(header::SEC_WEBSOCKET_ACCEPT) { - // field is constructed by concatenating /key/ - // with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (RFC 6455) - const WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - let mut sha1 = Sha1::new(); - sha1.update(self.key.as_ref()); - sha1.update(WS_GUID); - let encoded = base64::encode(&sha1.digest().bytes()); - if key.as_bytes() != encoded.as_bytes() { - trace!( - "Invalid challenge response: expected: {} received: {:?}", - encoded, - key - ); - return Err(ClientError::InvalidChallengeResponse(encoded, key.clone())); - } - } else { - trace!("Missing SEC-WEBSOCKET-ACCEPT header"); - return Err(ClientError::MissingWebSocketAcceptHeader); - }; - - // websockets codec - let codec = if self.server_mode { - Codec::new().max_size(self.max_size) - } else { - Codec::new().max_size(self.max_size).client_mode() - }; - - Ok(Async::Ready(framed.into_framed(codec))) - } -} diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index 88fabde94..065c34d93 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -13,7 +13,6 @@ use crate::httpmessage::HttpMessage; use crate::request::Request; use crate::response::{Response, ResponseBuilder}; -mod client; mod codec; mod frame; mod mask; @@ -21,7 +20,6 @@ mod proto; mod service; mod transport; -pub use self::client::{Client, ClientError, Connect}; pub use self::codec::{Codec, Frame, Message}; pub use self::frame::Parser; pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode}; diff --git a/awc/Cargo.toml b/awc/Cargo.toml index e08169c96..c915475f8 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -39,13 +39,16 @@ flate2-zlib = ["actix-http/flate2-zlib"] flate2-rust = ["actix-http/flate2-rust"] [dependencies] +actix-codec = "0.1.1" actix-service = "0.3.4" actix-http = { path = "../actix-http/" } base64 = "0.10.1" bytes = "0.4" +derive_more = "0.14" futures = "0.1" log =" 0.4" percent-encoding = "1.0" +rand = "0.6" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.5.3" @@ -58,8 +61,11 @@ actix-rt = "0.2.1" actix-web = { path = "..", features=["ssl"] } actix-http = { path = "../actix-http/", features=["ssl"] } actix-http-test = { path = "../test-server/", features=["ssl"] } +actix-utils = "0.3.4" +actix-server = { version = "0.4.0", features=["ssl"] } brotli2 = { version="^0.3.2" } flate2 = { version="^1.0.2" } env_logger = "0.6" mime = "0.3" rand = "0.6" +tokio-tcp = "0.1" \ No newline at end of file diff --git a/awc/src/connect.rs b/awc/src/connect.rs index a07662791..77cd1fbff 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -1,8 +1,12 @@ +use std::io; + +use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_http::body::Body; use actix_http::client::{ConnectError, Connection, SendRequestError}; -use actix_http::{http, RequestHead}; +use actix_http::h1::ClientCodec; +use actix_http::{http, RequestHead, ResponseHead}; use actix_service::Service; -use futures::Future; +use futures::{Future, Poll}; use crate::response::ClientResponse; @@ -14,13 +18,26 @@ pub(crate) trait Connect { head: RequestHead, body: Body, ) -> Box>; + + /// Send request, returns Response and Framed + fn open_tunnel( + &mut self, + head: RequestHead, + ) -> Box< + Future< + Item = (ResponseHead, Framed), + Error = SendRequestError, + >, + >; } impl Connect for ConnectorWrapper where T: Service, T::Response: Connection, + ::Io: 'static, ::Future: 'static, + ::TunnelFuture: 'static, T::Future: 'static, { fn send_request( @@ -38,4 +55,77 @@ where .map(|(head, payload)| ClientResponse::new(head, payload)), ) } + + fn open_tunnel( + &mut self, + head: RequestHead, + ) -> Box< + Future< + Item = (ResponseHead, Framed), + Error = SendRequestError, + >, + > { + Box::new( + self.0 + // connect to the host + .call(head.uri.clone()) + .from_err() + // send request + .and_then(move |connection| connection.open_tunnel(head)) + .map(|(head, framed)| { + let framed = framed.map_io(|io| BoxedSocket(Box::new(Socket(io)))); + (head, framed) + }), + ) + } +} + +trait AsyncSocket { + fn as_read(&self) -> &AsyncRead; + fn as_read_mut(&mut self) -> &mut AsyncRead; + fn as_write(&mut self) -> &mut AsyncWrite; +} + +struct Socket(T); + +impl AsyncSocket for Socket { + fn as_read(&self) -> &AsyncRead { + &self.0 + } + fn as_read_mut(&mut self) -> &mut AsyncRead { + &mut self.0 + } + fn as_write(&mut self) -> &mut AsyncWrite { + &mut self.0 + } +} + +pub struct BoxedSocket(Box); + +impl io::Read for BoxedSocket { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.0.as_read_mut().read(buf) + } +} + +impl AsyncRead for BoxedSocket { + unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { + self.0.as_read().prepare_uninitialized_buffer(buf) + } +} + +impl io::Write for BoxedSocket { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.as_write().write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.0.as_write().flush() + } +} + +impl AsyncWrite for BoxedSocket { + fn shutdown(&mut self) -> Poll<(), io::Error> { + self.0.as_write().shutdown() + } } diff --git a/actix-http/src/ws/client/error.rs b/awc/src/error.rs similarity index 55% rename from actix-http/src/ws/client/error.rs rename to awc/src/error.rs index ae1e39967..d3f1c1a17 100644 --- a/actix-http/src/ws/client/error.rs +++ b/awc/src/error.rs @@ -1,19 +1,14 @@ -//! Http client request -use std::io; +//! Http client errors +pub use actix_http::client::{ConnectError, InvalidUrl, SendRequestError}; +pub use actix_http::error::PayloadError; +pub use actix_http::ws::ProtocolError as WsProtocolError; -use actix_connect::ConnectError; +use actix_http::http::{header::HeaderValue, Error as HttpError, StatusCode}; use derive_more::{Display, From}; -use http::{header::HeaderValue, Error as HttpError, StatusCode}; - -use crate::error::ParseError; -use crate::ws::ProtocolError; /// Websocket client error #[derive(Debug, Display, From)] -pub enum ClientError { - /// Invalid url - #[display(fmt = "Invalid url")] - InvalidUrl, +pub enum WsClientError { /// Invalid response status #[display(fmt = "Invalid response status")] InvalidResponseStatus(StatusCode), @@ -32,22 +27,22 @@ pub enum ClientError { /// Invalid challenge response #[display(fmt = "Invalid challenge response")] InvalidChallengeResponse(String, HeaderValue), - /// Http parsing error - #[display(fmt = "Http parsing error")] - Http(HttpError), - /// Response parsing error - #[display(fmt = "Response parsing error: {}", _0)] - ParseError(ParseError), /// Protocol error #[display(fmt = "{}", _0)] - Protocol(ProtocolError), - /// Connect error - #[display(fmt = "Connector error: {:?}", _0)] - Connect(ConnectError), - /// IO Error + Protocol(WsProtocolError), + /// Send request error #[display(fmt = "{}", _0)] - Io(io::Error), - /// "Disconnected" - #[display(fmt = "Disconnected")] - Disconnected, + SendRequest(SendRequestError), +} + +impl From for WsClientError { + fn from(err: InvalidUrl) -> Self { + WsClientError::SendRequest(err.into()) + } +} + +impl From for WsClientError { + fn from(err: HttpError) -> Self { + WsClientError::SendRequest(err.into()) + } } diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 3bad8caa3..9f5ca1f28 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -23,8 +23,6 @@ use std::cell::RefCell; use std::rc::Rc; -pub use actix_http::client::{ConnectError, InvalidUrl, SendRequestError}; -pub use actix_http::error::PayloadError; pub use actix_http::http; use actix_http::client::Connector; @@ -33,13 +31,16 @@ use actix_http::RequestHead; mod builder; mod connect; +pub mod error; mod request; mod response; pub mod test; +mod ws; pub use self::builder::ClientBuilder; pub use self::request::ClientRequest; pub use self::response::ClientResponse; +pub use self::ws::WebsocketsRequest; use self::connect::{Connect, ConnectorWrapper}; @@ -165,4 +166,11 @@ impl Client { { ClientRequest::new(Method::OPTIONS, url, self.connector.clone()) } + + pub fn ws(&self, url: U) -> WebsocketsRequest + where + Uri: HttpTryFrom, + { + WebsocketsRequest::new(url, self.connector.clone()) + } } diff --git a/awc/src/request.rs b/awc/src/request.rs index 7beb737e1..c0962ebf1 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -12,7 +12,6 @@ use serde::Serialize; use serde_json; use actix_http::body::{Body, BodyStream}; -use actix_http::client::{InvalidUrl, SendRequestError}; use actix_http::encoding::Decoder; use actix_http::http::header::{self, ContentEncoding, Header, IntoHeaderValue}; use actix_http::http::{ @@ -21,8 +20,9 @@ use actix_http::http::{ }; use actix_http::{Error, Payload, RequestHead}; +use crate::connect::Connect; +use crate::error::{InvalidUrl, PayloadError, SendRequestError}; use crate::response::ClientResponse; -use crate::{Connect, PayloadError}; #[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] const HTTPS_ENCODING: &str = "br, gzip, deflate"; diff --git a/awc/src/ws.rs b/awc/src/ws.rs new file mode 100644 index 000000000..f959e62c5 --- /dev/null +++ b/awc/src/ws.rs @@ -0,0 +1,398 @@ +//! Websockets client +use std::cell::RefCell; +use std::io::Write; +use std::rc::Rc; +use std::{fmt, str}; + +use actix_codec::Framed; +use actix_http::{ws, Payload, RequestHead}; +use bytes::{BufMut, BytesMut}; +#[cfg(feature = "cookies")] +use cookie::{Cookie, CookieJar}; +use futures::future::{err, Either, Future}; + +use crate::connect::{BoxedSocket, Connect}; +use crate::error::{InvalidUrl, WsClientError}; +use crate::http::header::{ + self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION, +}; +use crate::http::{ + ConnectionType, Error as HttpError, HttpTryFrom, Method, StatusCode, Uri, Version, +}; +use crate::response::ClientResponse; + +/// `WebSocket` connection +pub struct WebsocketsRequest { + head: RequestHead, + err: Option, + origin: Option, + protocols: Option, + max_size: usize, + server_mode: bool, + default_headers: bool, + #[cfg(feature = "cookies")] + cookies: Option, + connector: Rc>, +} + +impl WebsocketsRequest { + /// Create new websocket connection + pub(crate) fn new(uri: U, connector: Rc>) -> Self + where + Uri: HttpTryFrom, + { + let mut err = None; + let mut head = RequestHead::default(); + head.method = Method::GET; + head.version = Version::HTTP_11; + + match Uri::try_from(uri) { + Ok(uri) => head.uri = uri, + Err(e) => err = Some(e.into()), + } + + WebsocketsRequest { + head, + err, + connector, + origin: None, + protocols: None, + max_size: 65_536, + server_mode: false, + #[cfg(feature = "cookies")] + cookies: None, + default_headers: true, + } + } + + /// Set supported websocket protocols + pub fn protocols(mut self, protos: U) -> Self + where + U: IntoIterator + 'static, + V: AsRef, + { + let mut protos = protos + .into_iter() + .fold(String::new(), |acc, s| acc + s.as_ref() + ","); + protos.pop(); + self.protocols = Some(protos); + self + } + + #[cfg(feature = "cookies")] + /// Set a cookie + pub fn cookie<'c>(mut self, cookie: Cookie<'c>) -> Self { + if self.cookies.is_none() { + let mut jar = CookieJar::new(); + jar.add(cookie.into_owned()); + self.cookies = Some(jar) + } else { + self.cookies.as_mut().unwrap().add(cookie.into_owned()); + } + self + } + + /// Set request Origin + pub fn origin(mut self, origin: V) -> Self + where + HeaderValue: HttpTryFrom, + { + match HeaderValue::try_from(origin) { + Ok(value) => self.origin = Some(value), + Err(e) => self.err = Some(e.into()), + } + self + } + + /// Set max frame size + /// + /// By default max size is set to 64kb + pub fn max_frame_size(mut self, size: usize) -> Self { + self.max_size = size; + self + } + + /// Disable payload masking. By default ws client masks frame payload. + pub fn server_mode(mut self) -> Self { + self.server_mode = true; + self + } + + /// Do not add default request headers. + /// By default `Date` and `User-Agent` headers are set. + pub fn no_default_headers(mut self) -> Self { + self.default_headers = false; + self + } + + /// Append a header. + /// + /// Header gets appended to existing header. + /// To override header use `set_header()` method. + pub fn header(mut self, key: K, value: V) -> Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + match HeaderName::try_from(key) { + Ok(key) => match value.try_into() { + Ok(value) => { + self.head.headers.append(key, value); + } + Err(e) => self.err = Some(e.into()), + }, + Err(e) => self.err = Some(e.into()), + } + self + } + + /// Insert a header, replaces existing header. + pub fn set_header(mut self, key: K, value: V) -> Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + match HeaderName::try_from(key) { + Ok(key) => match value.try_into() { + Ok(value) => { + self.head.headers.insert(key, value); + } + Err(e) => self.err = Some(e.into()), + }, + Err(e) => self.err = Some(e.into()), + } + self + } + + /// Insert a header only if it is not yet set. + pub fn set_header_if_none(mut self, key: K, value: V) -> Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + match HeaderName::try_from(key) { + Ok(key) => { + if !self.head.headers.contains_key(&key) { + match value.try_into() { + Ok(value) => { + self.head.headers.insert(key, value); + } + Err(e) => self.err = Some(e.into()), + } + } + } + Err(e) => self.err = Some(e.into()), + } + self + } + + /// Set HTTP basic authorization header + pub fn basic_auth(self, username: U, password: Option

    ) -> Self + where + U: fmt::Display, + P: fmt::Display, + { + let auth = match password { + Some(password) => format!("{}:{}", username, password), + None => format!("{}", username), + }; + self.header(AUTHORIZATION, format!("Basic {}", base64::encode(&auth))) + } + + /// Set HTTP bearer authentication header + pub fn bearer_auth(self, token: T) -> Self + where + T: fmt::Display, + { + self.header(AUTHORIZATION, format!("Bearer {}", token)) + } + + /// Complete request construction and connect. + pub fn connect( + mut self, + ) -> impl Future< + Item = (ClientResponse, Framed), + Error = WsClientError, + > { + if let Some(e) = self.err.take() { + return Either::A(err(e.into())); + } + + // validate uri + let uri = &self.head.uri; + if uri.host().is_none() { + return Either::A(err(InvalidUrl::MissingHost.into())); + } else if uri.scheme_part().is_none() { + return Either::A(err(InvalidUrl::MissingScheme.into())); + } else if let Some(scheme) = uri.scheme_part() { + match scheme.as_str() { + "http" | "ws" | "https" | "wss" => (), + _ => return Either::A(err(InvalidUrl::UnknownScheme.into())), + } + } else { + return Either::A(err(InvalidUrl::UnknownScheme.into())); + } + + // set default headers + let mut slf = if self.default_headers { + // set request host header + if let Some(host) = self.head.uri.host() { + if !self.head.headers.contains_key(header::HOST) { + let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); + + let _ = match self.head.uri.port_u16() { + None | Some(80) | Some(443) => write!(wrt, "{}", host), + Some(port) => write!(wrt, "{}:{}", host, port), + }; + + match wrt.get_mut().take().freeze().try_into() { + Ok(value) => { + self.head.headers.insert(header::HOST, value); + } + Err(e) => return Either::A(err(HttpError::from(e).into())), + } + } + } + + // user agent + self.set_header_if_none( + header::USER_AGENT, + concat!("awc/", env!("CARGO_PKG_VERSION")), + ) + } else { + self + }; + + #[allow(unused_mut)] + let mut head = slf.head; + + #[cfg(feature = "cookies")] + { + use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; + use std::fmt::Write; + + // set cookies + if let Some(ref mut jar) = slf.cookies { + let mut cookie = String::new(); + for c in jar.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = + percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); + } + head.headers.insert( + header::COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); + } + } + + // origin + if let Some(origin) = slf.origin.take() { + head.headers.insert(header::ORIGIN, origin); + } + + head.set_connection_type(ConnectionType::Upgrade); + head.headers + .insert(header::UPGRADE, HeaderValue::from_static("websocket")); + head.headers.insert( + header::SEC_WEBSOCKET_VERSION, + HeaderValue::from_static("13"), + ); + + if let Some(protocols) = slf.protocols.take() { + head.headers.insert( + header::SEC_WEBSOCKET_PROTOCOL, + HeaderValue::try_from(protocols.as_str()).unwrap(), + ); + } + + // Generate a random key for the `Sec-WebSocket-Key` header. + // a base64-encoded (see Section 4 of [RFC4648]) value that, + // when decoded, is 16 bytes in length (RFC 6455) + let sec_key: [u8; 16] = rand::random(); + let key = base64::encode(&sec_key); + + head.headers.insert( + header::SEC_WEBSOCKET_KEY, + HeaderValue::try_from(key.as_str()).unwrap(), + ); + + let max_size = slf.max_size; + let server_mode = slf.server_mode; + + let fut = slf + .connector + .borrow_mut() + .open_tunnel(head) + .from_err() + .and_then(move |(head, framed)| { + // verify response + if head.status != StatusCode::SWITCHING_PROTOCOLS { + return Err(WsClientError::InvalidResponseStatus(head.status)); + } + // Check for "UPGRADE" to websocket header + let has_hdr = if let Some(hdr) = head.headers.get(header::UPGRADE) { + if let Ok(s) = hdr.to_str() { + s.to_ascii_lowercase().contains("websocket") + } else { + false + } + } else { + false + }; + if !has_hdr { + log::trace!("Invalid upgrade header"); + return Err(WsClientError::InvalidUpgradeHeader); + } + // Check for "CONNECTION" header + if let Some(conn) = head.headers.get(header::CONNECTION) { + if let Ok(s) = conn.to_str() { + if !s.to_ascii_lowercase().contains("upgrade") { + log::trace!("Invalid connection header: {}", s); + return Err(WsClientError::InvalidConnectionHeader( + conn.clone(), + )); + } + } else { + log::trace!("Invalid connection header: {:?}", conn); + return Err(WsClientError::InvalidConnectionHeader(conn.clone())); + } + } else { + log::trace!("Missing connection header"); + return Err(WsClientError::MissingConnectionHeader); + } + + if let Some(hdr_key) = head.headers.get(header::SEC_WEBSOCKET_ACCEPT) { + let encoded = ws::hash_key(key.as_ref()); + if hdr_key.as_bytes() != encoded.as_bytes() { + log::trace!( + "Invalid challenge response: expected: {} received: {:?}", + encoded, + key + ); + return Err(WsClientError::InvalidChallengeResponse( + encoded, + hdr_key.clone(), + )); + } + } else { + log::trace!("Missing SEC-WEBSOCKET-ACCEPT header"); + return Err(WsClientError::MissingWebSocketAcceptHeader); + }; + + // response and ws framed + Ok(( + ClientResponse::new(head, Payload::None), + framed.map_codec(|_| { + if server_mode { + ws::Codec::new().max_size(max_size) + } else { + ws::Codec::new().max_size(max_size).client_mode() + } + }), + )) + }); + Either::B(fut) + } +} diff --git a/actix-http/tests/test_ws.rs b/awc/tests/test_ws.rs similarity index 100% rename from actix-http/tests/test_ws.rs rename to awc/tests/test_ws.rs diff --git a/src/lib.rs b/src/lib.rs index 5b0ce7841..d3d66c616 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -393,8 +393,8 @@ pub mod client { //! })); //! } //! ``` - pub use awc::{ - test, Client, ClientBuilder, ClientRequest, ClientResponse, ConnectError, - InvalidUrl, PayloadError, SendRequestError, + pub use awc::error::{ + ConnectError, InvalidUrl, PayloadError, SendRequestError, WsClientError, }; + pub use awc::{test, Client, ClientBuilder, ClientRequest, ClientResponse}; } diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 7cd94d4d2..07a0e0b4c 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -7,7 +7,6 @@ use actix_http::client::Connector; use actix_http::ws; use actix_rt::{Runtime, System}; use actix_server::{Server, StreamServiceFactory}; -use actix_service::Service; use awc::{Client, ClientRequest}; use futures::future::{lazy, Future}; use http::Method; @@ -205,16 +204,19 @@ impl TestServerRuntime { pub fn ws_at( &mut self, path: &str, - ) -> Result, ws::ClientError> { + ) -> Result, awc::error::WsClientError> + { let url = self.url(path); + let connect = self.client.ws(url).connect(); self.rt - .block_on(lazy(|| ws::Client::default().call(ws::Connect::new(url)))) + .block_on(lazy(move || connect.map(|(_, framed)| framed))) } /// Connect to a websocket server pub fn ws( &mut self, - ) -> Result, ws::ClientError> { + ) -> Result, awc::error::WsClientError> + { self.ws_at("/") } } From 4309d9b88c90fb2448af127384d9b3cb8b11b932 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 05:04:39 -0700 Subject: [PATCH 363/427] port multipart support --- Cargo.toml | 1 + actix-http/src/error.rs | 14 + src/error.rs | 36 ++ src/test.rs | 7 +- src/types/mod.rs | 2 + src/types/multipart.rs | 1137 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 1194 insertions(+), 3 deletions(-) create mode 100644 src/types/multipart.rs diff --git a/Cargo.toml b/Cargo.toml index 363989bf7..63a607e2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,7 @@ derive_more = "0.14" encoding = "0.2" futures = "0.1" hashbrown = "0.1.8" +httparse = "1.3" log = "0.4" mime = "0.3" net2 = "0.2.33" diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 23e6728d8..a026fe9db 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -905,6 +905,20 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + #[test] + fn test_payload_error() { + let err: PayloadError = + io::Error::new(io::ErrorKind::Other, "ParseError").into(); + assert_eq!(format!("{}", err), "ParseError"); + assert_eq!(format!("{}", err.cause().unwrap()), "ParseError"); + + let err = PayloadError::Incomplete; + assert_eq!( + format!("{}", err), + "A payload reached EOF, but is not complete." + ); + } + macro_rules! from { ($from:expr => $error:pat) => { match ParseError::from($from) { diff --git a/src/error.rs b/src/error.rs index fc0f9fdf3..f7610d509 100644 --- a/src/error.rs +++ b/src/error.rs @@ -143,6 +143,36 @@ impl ResponseError for ReadlinesError { } } +/// A set of errors that can occur during parsing multipart streams +#[derive(Debug, Display, From)] +pub enum MultipartError { + /// Content-Type header is not found + #[display(fmt = "No Content-type header found")] + NoContentType, + /// Can not parse Content-Type header + #[display(fmt = "Can not parse Content-Type header")] + ParseContentType, + /// Multipart boundary is not found + #[display(fmt = "Multipart boundary is not found")] + Boundary, + /// Multipart stream is incomplete + #[display(fmt = "Multipart stream is incomplete")] + Incomplete, + /// Error during field parsing + #[display(fmt = "{}", _0)] + Parse(ParseError), + /// Payload error + #[display(fmt = "{}", _0)] + Payload(PayloadError), +} + +/// Return `BadRequest` for `MultipartError` +impl ResponseError for MultipartError { + fn error_response(&self) -> HttpResponse { + HttpResponse::new(StatusCode::BAD_REQUEST) + } +} + #[cfg(test)] mod tests { use super::*; @@ -172,4 +202,10 @@ mod tests { let resp: HttpResponse = ReadlinesError::EncodingError.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } + + #[test] + fn test_multipart_error() { + let resp: HttpResponse = MultipartError::Boundary.error_response(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } } diff --git a/src/test.rs b/src/test.rs index 9e1f01f90..4fdb6ea38 100644 --- a/src/test.rs +++ b/src/test.rs @@ -49,11 +49,12 @@ where /// /// Note that this function is intended to be used only for testing purpose. /// This function panics on nested call. -pub fn run_on(f: F) -> Result +pub fn run_on(f: F) -> R where - F: Fn() -> Result, + F: Fn() -> R, { - RT.with(move |rt| rt.borrow_mut().block_on(lazy(f))) + RT.with(move |rt| rt.borrow_mut().block_on(lazy(|| Ok::<_, ()>(f())))) + .unwrap() } pub fn ok_service() -> impl Service< diff --git a/src/types/mod.rs b/src/types/mod.rs index 30ee73091..c9aed94f9 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod form; pub(crate) mod json; +mod multipart; mod path; pub(crate) mod payload; mod query; @@ -9,6 +10,7 @@ pub(crate) mod readlines; pub use self::form::{Form, FormConfig}; pub use self::json::{Json, JsonConfig}; +pub use self::multipart::{Multipart, MultipartItem}; pub use self::path::Path; pub use self::payload::{Payload, PayloadConfig}; pub use self::query::Query; diff --git a/src/types/multipart.rs b/src/types/multipart.rs new file mode 100644 index 000000000..d66053ff5 --- /dev/null +++ b/src/types/multipart.rs @@ -0,0 +1,1137 @@ +//! Multipart payload support +use std::cell::{RefCell, UnsafeCell}; +use std::collections::VecDeque; +use std::marker::PhantomData; +use std::rc::Rc; +use std::{cmp, fmt}; + +use bytes::{Bytes, BytesMut}; +use futures::task::{current as current_task, Task}; +use futures::{Async, Poll, Stream}; +use httparse; +use mime; + +use crate::error::{Error, MultipartError, ParseError, PayloadError}; +use crate::extract::FromRequest; +use crate::http::header::{ + self, ContentDisposition, HeaderMap, HeaderName, HeaderValue, +}; +use crate::http::HttpTryFrom; +use crate::service::ServiceFromRequest; +use crate::HttpMessage; + +const MAX_HEADERS: usize = 32; + +/// The server-side implementation of `multipart/form-data` requests. +/// +/// This will parse the incoming stream into `MultipartItem` instances via its +/// Stream implementation. +/// `MultipartItem::Field` contains multipart field. `MultipartItem::Multipart` +/// is used for nested multipart streams. +pub struct Multipart { + safety: Safety, + error: Option, + inner: Option>>, +} + +/// Multipart item +pub enum MultipartItem { + /// Multipart field + Field(Field), + /// Nested multipart stream + Nested(Multipart), +} + +/// Get request's payload as multipart stream +/// +/// Content-type: multipart/form-data; +/// +/// ## Server example +/// +/// ```rust +/// # use futures::{Future, Stream}; +/// # use futures::future::{ok, result, Either}; +/// use actix_web::{web, HttpResponse, Error}; +/// +/// fn index(payload: web::Multipart) -> impl Future { +/// payload.from_err() // <- get multipart stream for current request +/// .and_then(|item| match item { // <- iterate over multipart items +/// web::MultipartItem::Field(field) => { +/// // Field in turn is stream of *Bytes* object +/// Either::A(field.from_err() +/// .fold((), |_, chunk| { +/// println!("-- CHUNK: \n{:?}", std::str::from_utf8(&chunk)); +/// Ok::<_, Error>(()) +/// })) +/// }, +/// web::MultipartItem::Nested(mp) => { +/// // Or item could be nested Multipart stream +/// Either::B(ok(())) +/// } +/// }) +/// .fold((), |_, _| Ok::<_, Error>(())) +/// .map(|_| HttpResponse::Ok().into()) +/// } +/// # fn main() {} +/// ``` +impl

    FromRequest

    for Multipart +where + P: Stream + 'static, +{ + type Error = Error; + type Future = Result; + + #[inline] + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + let pl = req.take_payload(); + Ok(Multipart::new(req.headers(), pl)) + } +} + +enum InnerMultipartItem { + None, + Field(Rc>), + Multipart(Rc>), +} + +#[derive(PartialEq, Debug)] +enum InnerState { + /// Stream eof + Eof, + /// Skip data until first boundary + FirstBoundary, + /// Reading boundary + Boundary, + /// Reading Headers, + Headers, +} + +struct InnerMultipart { + payload: PayloadRef, + boundary: String, + state: InnerState, + item: InnerMultipartItem, +} + +impl Multipart { + /// Create multipart instance for boundary. + pub fn new(headers: &HeaderMap, stream: S) -> Multipart + where + S: Stream + 'static, + { + match Self::boundary(headers) { + Ok(boundary) => Multipart { + error: None, + safety: Safety::new(), + inner: Some(Rc::new(RefCell::new(InnerMultipart { + boundary, + payload: PayloadRef::new(PayloadBuffer::new(stream)), + state: InnerState::FirstBoundary, + item: InnerMultipartItem::None, + }))), + }, + Err(err) => Multipart { + error: Some(err), + safety: Safety::new(), + inner: None, + }, + } + } + + /// Extract boundary info from headers. + fn boundary(headers: &HeaderMap) -> Result { + if let Some(content_type) = headers.get(header::CONTENT_TYPE) { + if let Ok(content_type) = content_type.to_str() { + if let Ok(ct) = content_type.parse::() { + if let Some(boundary) = ct.get_param(mime::BOUNDARY) { + Ok(boundary.as_str().to_owned()) + } else { + Err(MultipartError::Boundary) + } + } else { + Err(MultipartError::ParseContentType) + } + } else { + Err(MultipartError::ParseContentType) + } + } else { + Err(MultipartError::NoContentType) + } + } +} + +impl Stream for Multipart { + type Item = MultipartItem; + type Error = MultipartError; + + fn poll(&mut self) -> Poll, Self::Error> { + if let Some(err) = self.error.take() { + Err(err) + } else if self.safety.current() { + self.inner.as_mut().unwrap().borrow_mut().poll(&self.safety) + } else { + Ok(Async::NotReady) + } + } +} + +impl InnerMultipart { + fn read_headers(payload: &mut PayloadBuffer) -> Poll { + match payload.read_until(b"\r\n\r\n")? { + Async::NotReady => Ok(Async::NotReady), + Async::Ready(None) => Err(MultipartError::Incomplete), + Async::Ready(Some(bytes)) => { + let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS]; + match httparse::parse_headers(&bytes, &mut hdrs) { + Ok(httparse::Status::Complete((_, hdrs))) => { + // convert headers + let mut headers = HeaderMap::with_capacity(hdrs.len()); + for h in hdrs { + if let Ok(name) = HeaderName::try_from(h.name) { + if let Ok(value) = HeaderValue::try_from(h.value) { + headers.append(name, value); + } else { + return Err(ParseError::Header.into()); + } + } else { + return Err(ParseError::Header.into()); + } + } + Ok(Async::Ready(headers)) + } + Ok(httparse::Status::Partial) => Err(ParseError::Header.into()), + Err(err) => Err(ParseError::from(err).into()), + } + } + } + } + + fn read_boundary( + payload: &mut PayloadBuffer, + boundary: &str, + ) -> Poll { + // TODO: need to read epilogue + match payload.readline()? { + Async::NotReady => Ok(Async::NotReady), + Async::Ready(None) => Err(MultipartError::Incomplete), + Async::Ready(Some(chunk)) => { + if chunk.len() == boundary.len() + 4 + && &chunk[..2] == b"--" + && &chunk[2..boundary.len() + 2] == boundary.as_bytes() + { + Ok(Async::Ready(false)) + } else if chunk.len() == boundary.len() + 6 + && &chunk[..2] == b"--" + && &chunk[2..boundary.len() + 2] == boundary.as_bytes() + && &chunk[boundary.len() + 2..boundary.len() + 4] == b"--" + { + Ok(Async::Ready(true)) + } else { + Err(MultipartError::Boundary) + } + } + } + } + + fn skip_until_boundary( + payload: &mut PayloadBuffer, + boundary: &str, + ) -> Poll { + let mut eof = false; + loop { + match payload.readline()? { + Async::Ready(Some(chunk)) => { + if chunk.is_empty() { + //ValueError("Could not find starting boundary %r" + //% (self._boundary)) + } + if chunk.len() < boundary.len() { + continue; + } + if &chunk[..2] == b"--" + && &chunk[2..chunk.len() - 2] == boundary.as_bytes() + { + break; + } else { + if chunk.len() < boundary.len() + 2 { + continue; + } + let b: &[u8] = boundary.as_ref(); + if &chunk[..boundary.len()] == b + && &chunk[boundary.len()..boundary.len() + 2] == b"--" + { + eof = true; + break; + } + } + } + Async::NotReady => return Ok(Async::NotReady), + Async::Ready(None) => return Err(MultipartError::Incomplete), + } + } + Ok(Async::Ready(eof)) + } + + fn poll(&mut self, safety: &Safety) -> Poll, MultipartError> { + if self.state == InnerState::Eof { + Ok(Async::Ready(None)) + } else { + // release field + loop { + // Nested multipart streams of fields has to be consumed + // before switching to next + if safety.current() { + let stop = match self.item { + InnerMultipartItem::Field(ref mut field) => { + match field.borrow_mut().poll(safety)? { + Async::NotReady => return Ok(Async::NotReady), + Async::Ready(Some(_)) => continue, + Async::Ready(None) => true, + } + } + InnerMultipartItem::Multipart(ref mut multipart) => { + match multipart.borrow_mut().poll(safety)? { + Async::NotReady => return Ok(Async::NotReady), + Async::Ready(Some(_)) => continue, + Async::Ready(None) => true, + } + } + _ => false, + }; + if stop { + self.item = InnerMultipartItem::None; + } + if let InnerMultipartItem::None = self.item { + break; + } + } + } + + let headers = if let Some(payload) = self.payload.get_mut(safety) { + match self.state { + // read until first boundary + InnerState::FirstBoundary => { + match InnerMultipart::skip_until_boundary( + payload, + &self.boundary, + )? { + Async::Ready(eof) => { + if eof { + self.state = InnerState::Eof; + return Ok(Async::Ready(None)); + } else { + self.state = InnerState::Headers; + } + } + Async::NotReady => return Ok(Async::NotReady), + } + } + // read boundary + InnerState::Boundary => { + match InnerMultipart::read_boundary(payload, &self.boundary)? { + Async::NotReady => return Ok(Async::NotReady), + Async::Ready(eof) => { + if eof { + self.state = InnerState::Eof; + return Ok(Async::Ready(None)); + } else { + self.state = InnerState::Headers; + } + } + } + } + _ => (), + } + + // read field headers for next field + if self.state == InnerState::Headers { + if let Async::Ready(headers) = InnerMultipart::read_headers(payload)? + { + self.state = InnerState::Boundary; + headers + } else { + return Ok(Async::NotReady); + } + } else { + unreachable!() + } + } else { + log::debug!("NotReady: field is in flight"); + return Ok(Async::NotReady); + }; + + // content type + let mut mt = mime::APPLICATION_OCTET_STREAM; + if let Some(content_type) = headers.get(header::CONTENT_TYPE) { + if let Ok(content_type) = content_type.to_str() { + if let Ok(ct) = content_type.parse::() { + mt = ct; + } + } + } + + self.state = InnerState::Boundary; + + // nested multipart stream + if mt.type_() == mime::MULTIPART { + let inner = if let Some(boundary) = mt.get_param(mime::BOUNDARY) { + Rc::new(RefCell::new(InnerMultipart { + payload: self.payload.clone(), + boundary: boundary.as_str().to_owned(), + state: InnerState::FirstBoundary, + item: InnerMultipartItem::None, + })) + } else { + return Err(MultipartError::Boundary); + }; + + self.item = InnerMultipartItem::Multipart(Rc::clone(&inner)); + + Ok(Async::Ready(Some(MultipartItem::Nested(Multipart { + safety: safety.clone(), + error: None, + inner: Some(inner), + })))) + } else { + let field = Rc::new(RefCell::new(InnerField::new( + self.payload.clone(), + self.boundary.clone(), + &headers, + )?)); + self.item = InnerMultipartItem::Field(Rc::clone(&field)); + + Ok(Async::Ready(Some(MultipartItem::Field(Field::new( + safety.clone(), + headers, + mt, + field, + ))))) + } + } + } +} + +impl Drop for InnerMultipart { + fn drop(&mut self) { + // InnerMultipartItem::Field has to be dropped first because of Safety. + self.item = InnerMultipartItem::None; + } +} + +/// A single field in a multipart stream +pub struct Field { + ct: mime::Mime, + headers: HeaderMap, + inner: Rc>, + safety: Safety, +} + +impl Field { + fn new( + safety: Safety, + headers: HeaderMap, + ct: mime::Mime, + inner: Rc>, + ) -> Self { + Field { + ct, + headers, + inner, + safety, + } + } + + /// Get a map of headers + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + /// Get the content type of the field + pub fn content_type(&self) -> &mime::Mime { + &self.ct + } + + /// Get the content disposition of the field, if it exists + pub fn content_disposition(&self) -> Option { + // RFC 7578: 'Each part MUST contain a Content-Disposition header field + // where the disposition type is "form-data".' + if let Some(content_disposition) = self.headers.get(header::CONTENT_DISPOSITION) + { + ContentDisposition::from_raw(content_disposition).ok() + } else { + None + } + } +} + +impl Stream for Field { + type Item = Bytes; + type Error = MultipartError; + + fn poll(&mut self) -> Poll, Self::Error> { + if self.safety.current() { + self.inner.borrow_mut().poll(&self.safety) + } else { + Ok(Async::NotReady) + } + } +} + +impl fmt::Debug for Field { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "\nMultipartField: {}", self.ct)?; + writeln!(f, " boundary: {}", self.inner.borrow().boundary)?; + writeln!(f, " headers:")?; + for (key, val) in self.headers.iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } +} + +struct InnerField { + payload: Option, + boundary: String, + eof: bool, + length: Option, +} + +impl InnerField { + fn new( + payload: PayloadRef, + boundary: String, + headers: &HeaderMap, + ) -> Result { + let len = if let Some(len) = headers.get(header::CONTENT_LENGTH) { + if let Ok(s) = len.to_str() { + if let Ok(len) = s.parse::() { + Some(len) + } else { + return Err(PayloadError::Incomplete(None)); + } + } else { + return Err(PayloadError::Incomplete(None)); + } + } else { + None + }; + + Ok(InnerField { + boundary, + payload: Some(payload), + eof: false, + length: len, + }) + } + + /// Reads body part content chunk of the specified size. + /// The body part must has `Content-Length` header with proper value. + fn read_len( + payload: &mut PayloadBuffer, + size: &mut u64, + ) -> Poll, MultipartError> { + if *size == 0 { + Ok(Async::Ready(None)) + } else { + match payload.readany() { + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(None)) => Err(MultipartError::Incomplete), + Ok(Async::Ready(Some(mut chunk))) => { + let len = cmp::min(chunk.len() as u64, *size); + *size -= len; + let ch = chunk.split_to(len as usize); + if !chunk.is_empty() { + payload.unprocessed(chunk); + } + Ok(Async::Ready(Some(ch))) + } + Err(err) => Err(err.into()), + } + } + } + + /// Reads content chunk of body part with unknown length. + /// The `Content-Length` header for body part is not necessary. + fn read_stream( + payload: &mut PayloadBuffer, + boundary: &str, + ) -> Poll, MultipartError> { + match payload.read_until(b"\r")? { + Async::NotReady => Ok(Async::NotReady), + Async::Ready(None) => Err(MultipartError::Incomplete), + Async::Ready(Some(mut chunk)) => { + if chunk.len() == 1 { + payload.unprocessed(chunk); + match payload.read_exact(boundary.len() + 4)? { + Async::NotReady => Ok(Async::NotReady), + Async::Ready(None) => Err(MultipartError::Incomplete), + Async::Ready(Some(mut chunk)) => { + if &chunk[..2] == b"\r\n" + && &chunk[2..4] == b"--" + && &chunk[4..] == boundary.as_bytes() + { + payload.unprocessed(chunk); + Ok(Async::Ready(None)) + } else { + // \r might be part of data stream + let ch = chunk.split_to(1); + payload.unprocessed(chunk); + Ok(Async::Ready(Some(ch))) + } + } + } + } else { + let to = chunk.len() - 1; + let ch = chunk.split_to(to); + payload.unprocessed(chunk); + Ok(Async::Ready(Some(ch))) + } + } + } + } + + fn poll(&mut self, s: &Safety) -> Poll, MultipartError> { + if self.payload.is_none() { + return Ok(Async::Ready(None)); + } + + let result = if let Some(payload) = self.payload.as_ref().unwrap().get_mut(s) { + let res = if let Some(ref mut len) = self.length { + InnerField::read_len(payload, len)? + } else { + InnerField::read_stream(payload, &self.boundary)? + }; + + match res { + Async::NotReady => Async::NotReady, + Async::Ready(Some(bytes)) => Async::Ready(Some(bytes)), + Async::Ready(None) => { + self.eof = true; + match payload.readline()? { + Async::NotReady => Async::NotReady, + Async::Ready(None) => Async::Ready(None), + Async::Ready(Some(line)) => { + if line.as_ref() != b"\r\n" { + log::warn!("multipart field did not read all the data or it is malformed"); + } + Async::Ready(None) + } + } + } + } + } else { + Async::NotReady + }; + + if Async::Ready(None) == result { + self.payload.take(); + } + Ok(result) + } +} + +struct PayloadRef { + payload: Rc>, +} + +impl PayloadRef { + fn new(payload: PayloadBuffer) -> PayloadRef { + PayloadRef { + payload: Rc::new(payload.into()), + } + } + + fn get_mut<'a, 'b>(&'a self, s: &'b Safety) -> Option<&'a mut PayloadBuffer> + where + 'a: 'b, + { + // Unsafe: Invariant is inforced by Safety Safety is used as ref counter, + // only top most ref can have mutable access to payload. + if s.current() { + let payload: &mut PayloadBuffer = unsafe { &mut *self.payload.get() }; + Some(payload) + } else { + None + } + } +} + +impl Clone for PayloadRef { + fn clone(&self) -> PayloadRef { + PayloadRef { + payload: Rc::clone(&self.payload), + } + } +} + +/// Counter. It tracks of number of clones of payloads and give access to +/// payload only to top most task panics if Safety get destroyed and it not top +/// most task. +#[derive(Debug)] +struct Safety { + task: Option, + level: usize, + payload: Rc>, +} + +impl Safety { + fn new() -> Safety { + let payload = Rc::new(PhantomData); + Safety { + task: None, + level: Rc::strong_count(&payload), + payload, + } + } + + fn current(&self) -> bool { + Rc::strong_count(&self.payload) == self.level + } +} + +impl Clone for Safety { + fn clone(&self) -> Safety { + let payload = Rc::clone(&self.payload); + Safety { + task: Some(current_task()), + level: Rc::strong_count(&payload), + payload, + } + } +} + +impl Drop for Safety { + fn drop(&mut self) { + // parent task is dead + if Rc::strong_count(&self.payload) != self.level { + panic!("Safety get dropped but it is not from top-most task"); + } + if let Some(task) = self.task.take() { + task.notify() + } + } +} + +/// Payload buffer +pub struct PayloadBuffer { + len: usize, + items: VecDeque, + stream: Box>, +} + +impl PayloadBuffer { + /// Create new `PayloadBuffer` instance + pub fn new(stream: S) -> Self + where + S: Stream + 'static, + { + PayloadBuffer { + len: 0, + items: VecDeque::new(), + stream: Box::new(stream), + } + } + + #[inline] + fn poll_stream(&mut self) -> Poll { + self.stream.poll().map(|res| match res { + Async::Ready(Some(data)) => { + self.len += data.len(); + self.items.push_back(data); + Async::Ready(true) + } + Async::Ready(None) => Async::Ready(false), + Async::NotReady => Async::NotReady, + }) + } + + /// Read first available chunk of bytes + #[inline] + pub fn readany(&mut self) -> Poll, PayloadError> { + if let Some(data) = self.items.pop_front() { + self.len -= data.len(); + Ok(Async::Ready(Some(data))) + } else { + match self.poll_stream()? { + Async::Ready(true) => self.readany(), + Async::Ready(false) => Ok(Async::Ready(None)), + Async::NotReady => Ok(Async::NotReady), + } + } + } + + /// Read exact number of bytes + #[inline] + pub fn read_exact(&mut self, size: usize) -> Poll, PayloadError> { + if size <= self.len { + self.len -= size; + let mut chunk = self.items.pop_front().unwrap(); + if size < chunk.len() { + let buf = chunk.split_to(size); + self.items.push_front(chunk); + Ok(Async::Ready(Some(buf))) + } else if size == chunk.len() { + Ok(Async::Ready(Some(chunk))) + } else { + let mut buf = BytesMut::with_capacity(size); + buf.extend_from_slice(&chunk); + + while buf.len() < size { + let mut chunk = self.items.pop_front().unwrap(); + let rem = cmp::min(size - buf.len(), chunk.len()); + buf.extend_from_slice(&chunk.split_to(rem)); + if !chunk.is_empty() { + self.items.push_front(chunk); + } + } + Ok(Async::Ready(Some(buf.freeze()))) + } + } else { + match self.poll_stream()? { + Async::Ready(true) => self.read_exact(size), + Async::Ready(false) => Ok(Async::Ready(None)), + Async::NotReady => Ok(Async::NotReady), + } + } + } + + /// Read until specified ending + pub fn read_until(&mut self, line: &[u8]) -> Poll, PayloadError> { + let mut idx = 0; + let mut num = 0; + let mut offset = 0; + let mut found = false; + let mut length = 0; + + for no in 0..self.items.len() { + { + let chunk = &self.items[no]; + for (pos, ch) in chunk.iter().enumerate() { + if *ch == line[idx] { + idx += 1; + if idx == line.len() { + num = no; + offset = pos + 1; + length += pos + 1; + found = true; + break; + } + } else { + idx = 0 + } + } + if !found { + length += chunk.len() + } + } + + if found { + let mut buf = BytesMut::with_capacity(length); + if num > 0 { + for _ in 0..num { + buf.extend_from_slice(&self.items.pop_front().unwrap()); + } + } + if offset > 0 { + let mut chunk = self.items.pop_front().unwrap(); + buf.extend_from_slice(&chunk.split_to(offset)); + if !chunk.is_empty() { + self.items.push_front(chunk) + } + } + self.len -= length; + return Ok(Async::Ready(Some(buf.freeze()))); + } + } + + match self.poll_stream()? { + Async::Ready(true) => self.read_until(line), + Async::Ready(false) => Ok(Async::Ready(None)), + Async::NotReady => Ok(Async::NotReady), + } + } + + /// Read bytes until new line delimiter + pub fn readline(&mut self) -> Poll, PayloadError> { + self.read_until(b"\n") + } + + /// Put unprocessed data back to the buffer + pub fn unprocessed(&mut self, data: Bytes) { + self.len += data.len(); + self.items.push_front(data); + } +} + +#[cfg(test)] +mod tests { + use bytes::Bytes; + use futures::unsync::mpsc; + + use super::*; + use crate::http::header::{DispositionParam, DispositionType}; + use crate::test::run_on; + + #[test] + fn test_boundary() { + let headers = HeaderMap::new(); + match Multipart::boundary(&headers) { + Err(MultipartError::NoContentType) => (), + _ => unreachable!("should not happen"), + } + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("test"), + ); + + match Multipart::boundary(&headers) { + Err(MultipartError::ParseContentType) => (), + _ => unreachable!("should not happen"), + } + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("multipart/mixed"), + ); + match Multipart::boundary(&headers) { + Err(MultipartError::Boundary) => (), + _ => unreachable!("should not happen"), + } + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/mixed; boundary=\"5c02368e880e436dab70ed54e1c58209\"", + ), + ); + + assert_eq!( + Multipart::boundary(&headers).unwrap(), + "5c02368e880e436dab70ed54e1c58209" + ); + } + + fn create_stream() -> ( + mpsc::UnboundedSender>, + impl Stream, + ) { + let (tx, rx) = mpsc::unbounded(); + + (tx, rx.map_err(|_| panic!()).and_then(|res| res)) + } + + #[test] + fn test_multipart() { + run_on(|| { + let (sender, payload) = create_stream(); + + let bytes = Bytes::from( + "testasdadsad\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ + Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ + test\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ + data\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n", + ); + sender.unbounded_send(Ok(bytes)).unwrap(); + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", + ), + ); + + let mut multipart = Multipart::new(&headers, payload); + match multipart.poll() { + Ok(Async::Ready(Some(item))) => match item { + MultipartItem::Field(mut field) => { + { + let cd = field.content_disposition().unwrap(); + assert_eq!(cd.disposition, DispositionType::FormData); + assert_eq!( + cd.parameters[0], + DispositionParam::Name("file".into()) + ); + } + assert_eq!(field.content_type().type_(), mime::TEXT); + assert_eq!(field.content_type().subtype(), mime::PLAIN); + + match field.poll() { + Ok(Async::Ready(Some(chunk))) => assert_eq!(chunk, "test"), + _ => unreachable!(), + } + match field.poll() { + Ok(Async::Ready(None)) => (), + _ => unreachable!(), + } + } + _ => unreachable!(), + }, + _ => unreachable!(), + } + + match multipart.poll() { + Ok(Async::Ready(Some(item))) => match item { + MultipartItem::Field(mut field) => { + assert_eq!(field.content_type().type_(), mime::TEXT); + assert_eq!(field.content_type().subtype(), mime::PLAIN); + + match field.poll() { + Ok(Async::Ready(Some(chunk))) => assert_eq!(chunk, "data"), + _ => unreachable!(), + } + match field.poll() { + Ok(Async::Ready(None)) => (), + _ => unreachable!(), + } + } + _ => unreachable!(), + }, + _ => unreachable!(), + } + + match multipart.poll() { + Ok(Async::Ready(None)) => (), + _ => unreachable!(), + } + }); + } + + #[test] + fn test_basic() { + run_on(|| { + let (_sender, payload) = create_stream(); + { + let mut payload = PayloadBuffer::new(payload); + assert_eq!(payload.len, 0); + assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); + } + }); + } + + #[test] + fn test_eof() { + run_on(|| { + let (sender, payload) = create_stream(); + let mut payload = PayloadBuffer::new(payload); + + assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); + sender.unbounded_send(Ok(Bytes::from("data"))).unwrap(); + drop(sender); + + assert_eq!( + Async::Ready(Some(Bytes::from("data"))), + payload.readany().ok().unwrap() + ); + assert_eq!(payload.len, 0); + assert_eq!(Async::Ready(None), payload.readany().ok().unwrap()); + }); + } + + #[test] + fn test_err() { + run_on(|| { + let (sender, payload) = create_stream(); + let mut payload = PayloadBuffer::new(payload); + + assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); + + sender + .unbounded_send(Err(PayloadError::Incomplete(None))) + .unwrap(); + payload.readany().err().unwrap(); + }); + } + + #[test] + fn test_readany() { + run_on(|| { + let (sender, payload) = create_stream(); + let mut payload = PayloadBuffer::new(payload); + + sender.unbounded_send(Ok(Bytes::from("line1"))).unwrap(); + sender.unbounded_send(Ok(Bytes::from("line2"))).unwrap(); + + assert_eq!( + Async::Ready(Some(Bytes::from("line1"))), + payload.readany().ok().unwrap() + ); + assert_eq!(payload.len, 0); + + assert_eq!( + Async::Ready(Some(Bytes::from("line2"))), + payload.readany().ok().unwrap() + ); + assert_eq!(payload.len, 0); + }); + } + + #[test] + fn test_readexactly() { + run_on(|| { + let (sender, payload) = create_stream(); + let mut payload = PayloadBuffer::new(payload); + + assert_eq!(Async::NotReady, payload.read_exact(2).ok().unwrap()); + + sender.unbounded_send(Ok(Bytes::from("line1"))).unwrap(); + sender.unbounded_send(Ok(Bytes::from("line2"))).unwrap(); + + assert_eq!( + Async::Ready(Some(Bytes::from_static(b"li"))), + payload.read_exact(2).ok().unwrap() + ); + assert_eq!(payload.len, 3); + + assert_eq!( + Async::Ready(Some(Bytes::from_static(b"ne1l"))), + payload.read_exact(4).ok().unwrap() + ); + assert_eq!(payload.len, 4); + + sender + .unbounded_send(Err(PayloadError::Incomplete(None))) + .unwrap(); + payload.read_exact(10).err().unwrap(); + }); + } + + #[test] + fn test_readuntil() { + run_on(|| { + let (sender, payload) = create_stream(); + let mut payload = PayloadBuffer::new(payload); + + assert_eq!(Async::NotReady, payload.read_until(b"ne").ok().unwrap()); + + sender.unbounded_send(Ok(Bytes::from("line1"))).unwrap(); + sender.unbounded_send(Ok(Bytes::from("line2"))).unwrap(); + + assert_eq!( + Async::Ready(Some(Bytes::from("line"))), + payload.read_until(b"ne").ok().unwrap() + ); + assert_eq!(payload.len, 1); + + assert_eq!( + Async::Ready(Some(Bytes::from("1line2"))), + payload.read_until(b"2").ok().unwrap() + ); + assert_eq!(payload.len, 0); + + sender + .unbounded_send(Err(PayloadError::Incomplete(None))) + .unwrap(); + payload.read_until(b"b").err().unwrap(); + }); + } +} From 6e0fe7db2dcd737582b05673028d098ef9a0c58a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 05:16:43 -0700 Subject: [PATCH 364/427] use actix-threadpool for blocking calls --- Cargo.toml | 3 ++- src/error.rs | 8 ++++---- src/lib.rs | 3 +-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 63a607e2d..5a75e9ac6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,11 +70,12 @@ actix-codec = "0.1.1" actix-service = "0.3.4" actix-utils = "0.3.4" actix-router = "0.1.0" -actix-rt = "0.2.1" +actix-rt = "0.2.2" actix-web-codegen = { path="actix-web-codegen" } actix-http = { path = "actix-http", features=["fail"] } actix-server = "0.4.1" actix-server-config = "0.1.0" +actix-threadpool = "0.1.0" awc = { path = "awc", optional = true } bytes = "0.4" diff --git a/src/error.rs b/src/error.rs index f7610d509..984b46e08 100644 --- a/src/error.rs +++ b/src/error.rs @@ -37,11 +37,11 @@ pub enum BlockingError { impl ResponseError for BlockingError {} -impl From> for BlockingError { - fn from(err: actix_rt::blocking::BlockingError) -> Self { +impl From> for BlockingError { + fn from(err: actix_threadpool::BlockingError) -> Self { match err { - actix_rt::blocking::BlockingError::Error(e) => BlockingError::Error(e), - actix_rt::blocking::BlockingError::Canceled => BlockingError::Canceled, + actix_threadpool::BlockingError::Error(e) => BlockingError::Error(e), + actix_threadpool::BlockingError::Canceled => BlockingError::Canceled, } } } diff --git a/src/lib.rs b/src/lib.rs index d3d66c616..ec5a9e6a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -162,7 +162,6 @@ pub mod dev { pub mod web { //! Various types use actix_http::{http::Method, Response}; - use actix_rt::blocking; use futures::{Future, IntoFuture}; pub use actix_http::Response as HttpResponse; @@ -339,7 +338,7 @@ pub mod web { I: Send + 'static, E: Send + std::fmt::Debug + 'static, { - blocking::run(f).from_err() + actix_threadpool::run(f).from_err() } use actix_service::{fn_transform, Service, Transform}; From e84c95968fe89a47475309e3fad5e7f9c7b1a1cd Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 05:34:33 -0700 Subject: [PATCH 365/427] reuse PayloadBuffer from actix-http --- actix-http/src/error.rs | 12 +- actix-http/src/h1/payload.rs | 9 -- src/types/multipart.rs | 288 +---------------------------------- 3 files changed, 12 insertions(+), 297 deletions(-) diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index a026fe9db..4329970d4 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -254,7 +254,10 @@ impl From for ParseError { /// A set of errors that can occur during payload parsing pub enum PayloadError { /// A payload reached EOF, but is not complete. - #[display(fmt = "A payload reached EOF, but is not complete.")] + #[display( + fmt = "A payload reached EOF, but is not complete. With error: {:?}", + _0 + )] Incomplete(Option), /// Content encoding stream corruption #[display(fmt = "Can not decode content-encoding.")] @@ -909,13 +912,12 @@ mod tests { fn test_payload_error() { let err: PayloadError = io::Error::new(io::ErrorKind::Other, "ParseError").into(); - assert_eq!(format!("{}", err), "ParseError"); - assert_eq!(format!("{}", err.cause().unwrap()), "ParseError"); + assert!(format!("{}", err).contains("ParseError")); - let err = PayloadError::Incomplete; + let err = PayloadError::Incomplete(None); assert_eq!( format!("{}", err), - "A payload reached EOF, but is not complete." + "A payload reached EOF, but is not complete. With error: None" ); } diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index 979dd015c..73d05c4bb 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -502,15 +502,6 @@ mod tests { use actix_rt::Runtime; use futures::future::{lazy, result}; - #[test] - fn test_error() { - let err = PayloadError::Incomplete(None); - assert_eq!( - format!("{}", err), - "A payload reached EOF, but is not complete." - ); - } - #[test] fn test_basic() { Runtime::new() diff --git a/src/types/multipart.rs b/src/types/multipart.rs index d66053ff5..50ef38135 100644 --- a/src/types/multipart.rs +++ b/src/types/multipart.rs @@ -1,11 +1,10 @@ //! Multipart payload support use std::cell::{RefCell, UnsafeCell}; -use std::collections::VecDeque; use std::marker::PhantomData; use std::rc::Rc; use std::{cmp, fmt}; -use bytes::{Bytes, BytesMut}; +use bytes::Bytes; use futures::task::{current as current_task, Task}; use futures::{Async, Poll, Stream}; use httparse; @@ -22,6 +21,9 @@ use crate::HttpMessage; const MAX_HEADERS: usize = 32; +type PayloadBuffer = + actix_http::h1::PayloadBuffer>>; + /// The server-side implementation of `multipart/form-data` requests. /// /// This will parse the incoming stream into `MultipartItem` instances via its @@ -125,7 +127,7 @@ impl Multipart { safety: Safety::new(), inner: Some(Rc::new(RefCell::new(InnerMultipart { boundary, - payload: PayloadRef::new(PayloadBuffer::new(stream)), + payload: PayloadRef::new(PayloadBuffer::new(Box::new(stream))), state: InnerState::FirstBoundary, item: InnerMultipartItem::None, }))), @@ -712,157 +714,6 @@ impl Drop for Safety { } } -/// Payload buffer -pub struct PayloadBuffer { - len: usize, - items: VecDeque, - stream: Box>, -} - -impl PayloadBuffer { - /// Create new `PayloadBuffer` instance - pub fn new(stream: S) -> Self - where - S: Stream + 'static, - { - PayloadBuffer { - len: 0, - items: VecDeque::new(), - stream: Box::new(stream), - } - } - - #[inline] - fn poll_stream(&mut self) -> Poll { - self.stream.poll().map(|res| match res { - Async::Ready(Some(data)) => { - self.len += data.len(); - self.items.push_back(data); - Async::Ready(true) - } - Async::Ready(None) => Async::Ready(false), - Async::NotReady => Async::NotReady, - }) - } - - /// Read first available chunk of bytes - #[inline] - pub fn readany(&mut self) -> Poll, PayloadError> { - if let Some(data) = self.items.pop_front() { - self.len -= data.len(); - Ok(Async::Ready(Some(data))) - } else { - match self.poll_stream()? { - Async::Ready(true) => self.readany(), - Async::Ready(false) => Ok(Async::Ready(None)), - Async::NotReady => Ok(Async::NotReady), - } - } - } - - /// Read exact number of bytes - #[inline] - pub fn read_exact(&mut self, size: usize) -> Poll, PayloadError> { - if size <= self.len { - self.len -= size; - let mut chunk = self.items.pop_front().unwrap(); - if size < chunk.len() { - let buf = chunk.split_to(size); - self.items.push_front(chunk); - Ok(Async::Ready(Some(buf))) - } else if size == chunk.len() { - Ok(Async::Ready(Some(chunk))) - } else { - let mut buf = BytesMut::with_capacity(size); - buf.extend_from_slice(&chunk); - - while buf.len() < size { - let mut chunk = self.items.pop_front().unwrap(); - let rem = cmp::min(size - buf.len(), chunk.len()); - buf.extend_from_slice(&chunk.split_to(rem)); - if !chunk.is_empty() { - self.items.push_front(chunk); - } - } - Ok(Async::Ready(Some(buf.freeze()))) - } - } else { - match self.poll_stream()? { - Async::Ready(true) => self.read_exact(size), - Async::Ready(false) => Ok(Async::Ready(None)), - Async::NotReady => Ok(Async::NotReady), - } - } - } - - /// Read until specified ending - pub fn read_until(&mut self, line: &[u8]) -> Poll, PayloadError> { - let mut idx = 0; - let mut num = 0; - let mut offset = 0; - let mut found = false; - let mut length = 0; - - for no in 0..self.items.len() { - { - let chunk = &self.items[no]; - for (pos, ch) in chunk.iter().enumerate() { - if *ch == line[idx] { - idx += 1; - if idx == line.len() { - num = no; - offset = pos + 1; - length += pos + 1; - found = true; - break; - } - } else { - idx = 0 - } - } - if !found { - length += chunk.len() - } - } - - if found { - let mut buf = BytesMut::with_capacity(length); - if num > 0 { - for _ in 0..num { - buf.extend_from_slice(&self.items.pop_front().unwrap()); - } - } - if offset > 0 { - let mut chunk = self.items.pop_front().unwrap(); - buf.extend_from_slice(&chunk.split_to(offset)); - if !chunk.is_empty() { - self.items.push_front(chunk) - } - } - self.len -= length; - return Ok(Async::Ready(Some(buf.freeze()))); - } - } - - match self.poll_stream()? { - Async::Ready(true) => self.read_until(line), - Async::Ready(false) => Ok(Async::Ready(None)), - Async::NotReady => Ok(Async::NotReady), - } - } - - /// Read bytes until new line delimiter - pub fn readline(&mut self) -> Poll, PayloadError> { - self.read_until(b"\n") - } - - /// Put unprocessed data back to the buffer - pub fn unprocessed(&mut self, data: Bytes) { - self.len += data.len(); - self.items.push_front(data); - } -} - #[cfg(test)] mod tests { use bytes::Bytes; @@ -1005,133 +856,4 @@ mod tests { } }); } - - #[test] - fn test_basic() { - run_on(|| { - let (_sender, payload) = create_stream(); - { - let mut payload = PayloadBuffer::new(payload); - assert_eq!(payload.len, 0); - assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); - } - }); - } - - #[test] - fn test_eof() { - run_on(|| { - let (sender, payload) = create_stream(); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); - sender.unbounded_send(Ok(Bytes::from("data"))).unwrap(); - drop(sender); - - assert_eq!( - Async::Ready(Some(Bytes::from("data"))), - payload.readany().ok().unwrap() - ); - assert_eq!(payload.len, 0); - assert_eq!(Async::Ready(None), payload.readany().ok().unwrap()); - }); - } - - #[test] - fn test_err() { - run_on(|| { - let (sender, payload) = create_stream(); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); - - sender - .unbounded_send(Err(PayloadError::Incomplete(None))) - .unwrap(); - payload.readany().err().unwrap(); - }); - } - - #[test] - fn test_readany() { - run_on(|| { - let (sender, payload) = create_stream(); - let mut payload = PayloadBuffer::new(payload); - - sender.unbounded_send(Ok(Bytes::from("line1"))).unwrap(); - sender.unbounded_send(Ok(Bytes::from("line2"))).unwrap(); - - assert_eq!( - Async::Ready(Some(Bytes::from("line1"))), - payload.readany().ok().unwrap() - ); - assert_eq!(payload.len, 0); - - assert_eq!( - Async::Ready(Some(Bytes::from("line2"))), - payload.readany().ok().unwrap() - ); - assert_eq!(payload.len, 0); - }); - } - - #[test] - fn test_readexactly() { - run_on(|| { - let (sender, payload) = create_stream(); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(Async::NotReady, payload.read_exact(2).ok().unwrap()); - - sender.unbounded_send(Ok(Bytes::from("line1"))).unwrap(); - sender.unbounded_send(Ok(Bytes::from("line2"))).unwrap(); - - assert_eq!( - Async::Ready(Some(Bytes::from_static(b"li"))), - payload.read_exact(2).ok().unwrap() - ); - assert_eq!(payload.len, 3); - - assert_eq!( - Async::Ready(Some(Bytes::from_static(b"ne1l"))), - payload.read_exact(4).ok().unwrap() - ); - assert_eq!(payload.len, 4); - - sender - .unbounded_send(Err(PayloadError::Incomplete(None))) - .unwrap(); - payload.read_exact(10).err().unwrap(); - }); - } - - #[test] - fn test_readuntil() { - run_on(|| { - let (sender, payload) = create_stream(); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(Async::NotReady, payload.read_until(b"ne").ok().unwrap()); - - sender.unbounded_send(Ok(Bytes::from("line1"))).unwrap(); - sender.unbounded_send(Ok(Bytes::from("line2"))).unwrap(); - - assert_eq!( - Async::Ready(Some(Bytes::from("line"))), - payload.read_until(b"ne").ok().unwrap() - ); - assert_eq!(payload.len, 1); - - assert_eq!( - Async::Ready(Some(Bytes::from("1line2"))), - payload.read_until(b"2").ok().unwrap() - ); - assert_eq!(payload.len, 0); - - sender - .unbounded_send(Err(PayloadError::Incomplete(None))) - .unwrap(); - payload.read_until(b"b").err().unwrap(); - }); - } } From 5795850bbb6dce3fd6ac0b8a7b10819a1e2500b9 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 11:08:24 -0700 Subject: [PATCH 366/427] decompress payload in cpu threadpool --- actix-http/Cargo.toml | 3 +- actix-http/src/encoding/decoder.rs | 74 ++++++++++++++++++++---------- actix-http/src/error.rs | 40 ++++++++++++++-- awc/src/request.rs | 2 +- src/error.rs | 22 --------- src/lib.rs | 6 +-- src/middleware/decompress.rs | 2 +- 7 files changed, 91 insertions(+), 58 deletions(-) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index fefe05c4c..cdaeb1fc5 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -53,6 +53,7 @@ actix-codec = "0.1.2" actix-connect = "0.1.0" actix-utils = "0.3.4" actix-server-config = "0.1.0" +actix-threadpool = "0.1.0" base64 = "0.10" bitflags = "1.0" @@ -94,7 +95,7 @@ failure = { version = "0.1.5", optional = true } openssl = { version="0.10", optional = true } [dev-dependencies] -actix-rt = "0.2.1" +actix-rt = "0.2.2" actix-server = { version = "0.4.0", features=["ssl"] } actix-connect = { version = "0.1.0", features=["ssl"] } actix-http-test = { path="../test-server", features=["ssl"] } diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index 8be6702fc..ae2b4ae6b 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -1,27 +1,31 @@ use std::io::{self, Write}; -use bytes::Bytes; -use futures::{Async, Poll, Stream}; - +use actix_threadpool::{run, CpuFuture}; #[cfg(feature = "brotli")] use brotli2::write::BrotliDecoder; +use bytes::Bytes; #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] use flate2::write::{GzDecoder, ZlibDecoder}; +use futures::{try_ready, Async, Future, Poll, Stream}; use super::Writer; use crate::error::PayloadError; use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING}; -pub struct Decoder { - stream: T, +pub struct Decoder { decoder: Option, + stream: S, + eof: bool, + fut: Option, ContentDecoder), io::Error>>, } -impl Decoder +impl Decoder where - T: Stream, + S: Stream, { - pub fn new(stream: T, encoding: ContentEncoding) -> Self { + /// Construct a decoder. + #[inline] + pub fn new(stream: S, encoding: ContentEncoding) -> Decoder { let decoder = match encoding { #[cfg(feature = "brotli")] ContentEncoding::Br => Some(ContentDecoder::Br(Box::new( @@ -37,10 +41,17 @@ where ))), _ => None, }; - Decoder { stream, decoder } + Decoder { + decoder, + stream, + fut: None, + eof: false, + } } - pub fn from_headers(headers: &HeaderMap, stream: T) -> Self { + /// Construct decoder based on headers. + #[inline] + pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder { // check content-encoding let encoding = if let Some(enc) = headers.get(CONTENT_ENCODING) { if let Ok(enc) = enc.to_str() { @@ -56,35 +67,50 @@ where } } -impl Stream for Decoder +impl Stream for Decoder where - T: Stream, + S: Stream, { type Item = Bytes; type Error = PayloadError; fn poll(&mut self) -> Poll, Self::Error> { loop { + if let Some(ref mut fut) = self.fut { + let (chunk, decoder) = try_ready!(fut.poll()); + self.decoder = Some(decoder); + self.fut.take(); + if let Some(chunk) = chunk { + return Ok(Async::Ready(Some(chunk))); + } + } + + if self.eof { + return Ok(Async::Ready(None)); + } + match self.stream.poll()? { Async::Ready(Some(chunk)) => { - if let Some(ref mut decoder) = self.decoder { - match decoder.feed_data(chunk) { - Ok(Some(chunk)) => return Ok(Async::Ready(Some(chunk))), - Ok(None) => continue, - Err(e) => return Err(e.into()), - } + if let Some(mut decoder) = self.decoder.take() { + self.fut = Some(run(move || { + let chunk = decoder.feed_data(chunk)?; + Ok((chunk, decoder)) + })); + continue; } else { return Ok(Async::Ready(Some(chunk))); } } Async::Ready(None) => { - return if let Some(mut decoder) = self.decoder.take() { - match decoder.feed_eof() { - Ok(chunk) => Ok(Async::Ready(chunk)), - Err(e) => Err(e.into()), - } + self.eof = true; + if let Some(mut decoder) = self.decoder.take() { + self.fut = Some(run(move || { + let chunk = decoder.feed_eof()?; + Ok((chunk, decoder)) + })); + continue; } else { - Ok(Async::Ready(None)) + return Ok(Async::Ready(None)); }; } Async::NotReady => break, diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 4329970d4..e6cc0e07f 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -1,11 +1,11 @@ //! Error and Result module use std::cell::RefCell; -use std::io::Error as IoError; use std::str::Utf8Error; use std::string::FromUtf8Error; use std::{fmt, io, result}; // use actix::MailboxError; +pub use actix_threadpool::BlockingError; use actix_utils::timeout::TimeoutError; #[cfg(feature = "cookies")] use cookie; @@ -126,6 +126,9 @@ impl ResponseError for DeError { } } +/// `InternalServerError` for `BlockingError` +impl ResponseError for BlockingError {} + /// Return `BAD_REQUEST` for `Utf8Error` impl ResponseError for Utf8Error { fn error_response(&self) -> Response { @@ -199,7 +202,7 @@ pub enum ParseError { /// An `io::Error` that occurred while trying to read or write to a network /// stream. #[display(fmt = "IO error: {}", _0)] - Io(IoError), + Io(io::Error), /// Parsing a field as string failed #[display(fmt = "UTF8 error: {}", _0)] Utf8(Utf8Error), @@ -212,8 +215,8 @@ impl ResponseError for ParseError { } } -impl From for ParseError { - fn from(err: IoError) -> ParseError { +impl From for ParseError { + fn from(err: io::Error) -> ParseError { ParseError::Io(err) } } @@ -250,7 +253,7 @@ impl From for ParseError { } } -#[derive(Display, Debug, From)] +#[derive(Display, Debug)] /// A set of errors that can occur during payload parsing pub enum PayloadError { /// A payload reached EOF, but is not complete. @@ -271,6 +274,21 @@ pub enum PayloadError { /// Http2 payload error #[display(fmt = "{}", _0)] Http2Payload(h2::Error), + /// Io error + #[display(fmt = "{}", _0)] + Io(io::Error), +} + +impl From for PayloadError { + fn from(err: h2::Error) -> Self { + PayloadError::Http2Payload(err) + } +} + +impl From> for PayloadError { + fn from(err: Option) -> Self { + PayloadError::Incomplete(err) + } } impl From for PayloadError { @@ -279,6 +297,18 @@ impl From for PayloadError { } } +impl From> for PayloadError { + fn from(err: BlockingError) -> Self { + match err { + BlockingError::Error(e) => PayloadError::Io(e), + BlockingError::Canceled => PayloadError::Io(io::Error::new( + io::ErrorKind::Other, + "Thread pool is gone", + )), + } + } +} + /// `PayloadError` returns two possible results: /// /// - `Overflow` returns `PayloadTooLarge` diff --git a/awc/src/request.rs b/awc/src/request.rs index c0962ebf1..dde51a8f5 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -457,7 +457,7 @@ impl ClientRequest { .map(move |res| { res.map_body(|head, payload| { if response_decompress { - Payload::Stream(Decoder::from_headers(&head.headers, payload)) + Payload::Stream(Decoder::from_headers(payload, &head.headers)) } else { Payload::Stream(Decoder::new(payload, ContentEncoding::Identity)) } diff --git a/src/error.rs b/src/error.rs index 984b46e08..02e17241f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,4 @@ //! Error and Result module -use std::fmt; - pub use actix_http::error::*; use derive_more::{Display, From}; use serde_json::error::Error as JsonError; @@ -26,26 +24,6 @@ pub enum UrlGenerationError { /// `InternalServerError` for `UrlGeneratorError` impl ResponseError for UrlGenerationError {} -/// Blocking operation execution error -#[derive(Debug, Display)] -pub enum BlockingError { - #[display(fmt = "{:?}", _0)] - Error(E), - #[display(fmt = "Thread pool is gone")] - Canceled, -} - -impl ResponseError for BlockingError {} - -impl From> for BlockingError { - fn from(err: actix_threadpool::BlockingError) -> Self { - match err { - actix_threadpool::BlockingError::Error(e) => BlockingError::Error(e), - actix_threadpool::BlockingError::Canceled => BlockingError::Canceled, - } - } -} - /// A set of errors that can occur during parsing urlencoded payloads #[derive(Debug, Display, From)] pub enum UrlencodedError { diff --git a/src/lib.rs b/src/lib.rs index ec5a9e6a9..7a4f4bfbb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -162,6 +162,7 @@ pub mod dev { pub mod web { //! Various types use actix_http::{http::Method, Response}; + use actix_service::{fn_transform, Service, Transform}; use futures::{Future, IntoFuture}; pub use actix_http::Response as HttpResponse; @@ -174,6 +175,7 @@ pub mod web { use crate::responder::Responder; use crate::route::Route; use crate::scope::Scope; + use crate::service::{ServiceRequest, ServiceResponse}; pub use crate::data::{Data, RouteData}; pub use crate::request::HttpRequest; @@ -341,10 +343,6 @@ pub mod web { actix_threadpool::run(f).from_err() } - use actix_service::{fn_transform, Service, Transform}; - - use crate::service::{ServiceRequest, ServiceResponse}; - /// Create middleare pub fn md( f: F, diff --git a/src/middleware/decompress.rs b/src/middleware/decompress.rs index eaffbbdb4..84d357375 100644 --- a/src/middleware/decompress.rs +++ b/src/middleware/decompress.rs @@ -70,7 +70,7 @@ where fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { let (req, payload) = req.into_parts(); - let payload = Decoder::from_headers(req.headers(), payload); + let payload = Decoder::from_headers(payload, req.headers()); ok(ServiceRequest::from_parts(req, Payload::Stream(payload))) } } From 605ce051274b0432e06067e2d78e3d8649078791 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 12:32:59 -0700 Subject: [PATCH 367/427] App::enable_encoding() allows to enable compression and decompression --- src/app.rs | 99 +++++++++++++++++++++++++++++--------------- tests/test_server.rs | 28 +++++++++++++ 2 files changed, 94 insertions(+), 33 deletions(-) diff --git a/src/app.rs b/src/app.rs index b8efdd38b..0daa54b66 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,18 +3,21 @@ use std::marker::PhantomData; use std::rc::Rc; use actix_http::body::{Body, MessageBody}; +#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] +use actix_http::encoding::{Decoder, Encoder}; use actix_server_config::ServerConfig; use actix_service::boxed::{self, BoxedNewService}; use actix_service::{ ApplyTransform, IntoNewService, IntoTransform, NewService, Transform, }; -use futures::IntoFuture; +use bytes::Bytes; +use futures::{IntoFuture, Stream}; use crate::app_service::{AppChain, AppEntry, AppInit, AppRouting, AppRoutingFactory}; use crate::config::{AppConfig, AppConfigInner}; use crate::data::{Data, DataFactory}; -use crate::dev::{PayloadStream, ResourceDef}; -use crate::error::Error; +use crate::dev::{Payload, PayloadStream, ResourceDef}; +use crate::error::{Error, PayloadError}; use crate::resource::Resource; use crate::route::Route; use crate::service::{ @@ -27,17 +30,17 @@ type HttpNewService

    = /// Application builder - structure that follows the builder pattern /// for building application instances. -pub struct App +pub struct App where - T: NewService>, + T: NewService, Response = ServiceRequest>, { chain: T, data: Vec>, config: AppConfigInner, - _t: PhantomData<(P,)>, + _t: PhantomData<(In, Out)>, } -impl App { +impl App { /// Create application builder. Application can be configured with a builder-like pattern. pub fn new() -> Self { App { @@ -49,12 +52,13 @@ impl App { } } -impl App +impl App where - P: 'static, + In: 'static, + Out: 'static, T: NewService< - Request = ServiceRequest, - Response = ServiceRequest

    , + Request = ServiceRequest, + Response = ServiceRequest, Error = Error, InitError = (), >, @@ -97,11 +101,11 @@ where /// Set application data factory. This function is /// similar to `.data()` but it accepts data factory. Data object get /// constructed asynchronously during application initialization. - pub fn data_factory(mut self, data: F) -> Self + pub fn data_factory(mut self, data: F) -> Self where - F: Fn() -> Out + 'static, - Out: IntoFuture + 'static, - Out::Error: std::fmt::Debug, + F: Fn() -> R + 'static, + R: IntoFuture + 'static, + R::Error: std::fmt::Debug, { self.data.push(Box::new(data)); self @@ -113,10 +117,10 @@ where mw: F, ) -> AppRouter< T, - P, + Out, B, impl NewService< - Request = ServiceRequest

    , + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), @@ -124,13 +128,13 @@ where > where M: Transform< - AppRouting

    , - Request = ServiceRequest

    , + AppRouting, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), >, - F: IntoTransform>, + F: IntoTransform>, { let fref = Rc::new(RefCell::new(None)); let endpoint = ApplyTransform::new(mw, AppEntry::new(fref.clone())); @@ -176,17 +180,17 @@ where mw: F, ) -> AppRouter< T, - P, + Out, B, impl NewService< - Request = ServiceRequest

    , + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), >, > where - F: FnMut(ServiceRequest

    , &mut AppRouting

    ) -> R + Clone, + F: FnMut(ServiceRequest, &mut AppRouting) -> R + Clone, R: IntoFuture, Error = Error>, { self.wrap(mw) @@ -194,22 +198,23 @@ where /// Register a request modifier. It can modify any request parameters /// including request payload type. - pub fn chain( + pub fn chain( self, chain: F, ) -> App< - P1, + In, + P, impl NewService< - Request = ServiceRequest, - Response = ServiceRequest, + Request = ServiceRequest, + Response = ServiceRequest

    , Error = Error, InitError = (), >, > where C: NewService< - Request = ServiceRequest

    , - Response = ServiceRequest, + Request = ServiceRequest, + Response = ServiceRequest

    , Error = Error, InitError = (), >, @@ -246,8 +251,8 @@ where pub fn route( self, path: &str, - mut route: Route

    , - ) -> AppRouter> { + mut route: Route, + ) -> AppRouter> { self.service( Resource::new(path) .add_guards(route.take_guards()) @@ -264,9 +269,9 @@ where /// * *Resource* is an entry in resource table which corresponds to requested URL. /// * *Scope* is a set of resources with common root path. /// * "StaticFiles" is a service for static files support - pub fn service(self, service: F) -> AppRouter> + pub fn service(self, service: F) -> AppRouter> where - F: HttpServiceFactory

    + 'static, + F: HttpServiceFactory + 'static, { let fref = Rc::new(RefCell::new(None)); @@ -294,6 +299,34 @@ where self.config.host = val.to_owned(); self } + + #[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] + /// Enable content compression and decompression. + pub fn enable_encoding( + self, + ) -> AppRouter< + impl NewService< + Request = ServiceRequest, + Response = ServiceRequest>>, + Error = Error, + InitError = (), + >, + Decoder>, + Encoder, + impl NewService< + Request = ServiceRequest>>, + Response = ServiceResponse>, + Error = Error, + InitError = (), + >, + > + where + Out: Stream, + { + use crate::middleware::encoding::{Compress, Decompress}; + + self.chain(Decompress::new()).wrap(Compress::default()) + } } /// Application router builder - Structure that follows the builder pattern diff --git a/tests/test_server.rs b/tests/test_server.rs index 364f92626..c30cf67f8 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -335,6 +335,34 @@ fn test_body_brotli() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } +#[test] +fn test_encoding() { + let mut srv = TestServer::new(move || { + HttpService::new( + App::new().enable_encoding().service( + web::resource("/") + .route(web::to(move |body: Bytes| Response::Ok().body(body))), + ), + ) + }); + + // client request + let mut e = GzEncoder::new(Vec::new(), Compression::default()); + e.write_all(STR.as_ref()).unwrap(); + let enc = e.finish().unwrap(); + + let request = srv + .post() + .header(CONTENT_ENCODING, "gzip") + .send_body(enc.clone()); + let mut response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +} + #[test] fn test_gzip_encoding() { let mut srv = TestServer::new(move || { From 9cca86e60d8d3b5ebb77ddff7c00a621206a8572 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 12:45:41 -0700 Subject: [PATCH 368/427] prepear actix-http release --- actix-http/CHANGES.md | 2 +- actix-http/Cargo.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 74fa4a22e..95ec1c35f 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,5 +1,5 @@ # Changes -## [0.1.0] - 2019-01-x +## [0.1.0-alpha.1] - 2019-03-28 * Initial impl diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index cdaeb1fc5..5862ac846 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -2,9 +2,9 @@ name = "actix-http" version = "0.1.0-alpha.1" authors = ["Nikolay Kim "] -description = "Actix http" +description = "Actix http primitives" readme = "README.md" -keywords = ["http", "web", "framework", "async", "futures"] +keywords = ["actix", "http", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-http.git" documentation = "https://docs.rs/actix-http/" @@ -16,7 +16,7 @@ edition = "2018" workspace = ".." [package.metadata.docs.rs] -features = ["ssl", "fail", "cookie"] +features = ["ssl", "fail", "cookie", "brotli", "flate2-zlib"] [badges] travis-ci = { repository = "actix/actix-web", branch = "master" } From 9c198a0d29f7827616380a9db84830643f2755ce Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 13:46:26 -0700 Subject: [PATCH 369/427] alpha.1 release --- CHANGES.md | 4 +- Cargo.toml | 17 ++++---- README.md | 9 ++-- actix-files/CHANGES.md | 2 +- actix-files/Cargo.toml | 10 ++--- actix-files/README.md | 83 +----------------------------------- actix-http/Cargo.toml | 2 +- actix-session/CHANGES.md | 5 +++ actix-session/Cargo.toml | 6 +-- actix-session/README.md | 1 + actix-web-actors/CHANGES.md | 5 +++ actix-web-actors/Cargo.toml | 18 ++++---- actix-web-actors/README.md | 1 + actix-web-codegen/CHANGES.md | 5 +++ actix-web-codegen/Cargo.toml | 7 +-- actix-web-codegen/README.md | 1 + awc/CHANGES.md | 5 +++ awc/Cargo.toml | 20 ++++----- awc/README.md | 1 + test-server/CHANGES.md | 5 +++ test-server/Cargo.toml | 6 +-- test-server/README.md | 1 + 22 files changed, 82 insertions(+), 132 deletions(-) create mode 100644 actix-session/CHANGES.md create mode 100644 actix-session/README.md create mode 100644 actix-web-actors/CHANGES.md create mode 100644 actix-web-actors/README.md create mode 100644 actix-web-codegen/CHANGES.md create mode 100644 actix-web-codegen/README.md create mode 100644 awc/CHANGES.md create mode 100644 awc/README.md create mode 100644 test-server/CHANGES.md create mode 100644 test-server/README.md diff --git a/CHANGES.md b/CHANGES.md index e44f5dc3c..13eeb67d7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,9 @@ # Changes -## [1.0.0-alpha.1] - 2019-03-x +## [1.0.0-alpha.1] - 2019-03-28 ### Changed +* Complete architecture re-design. + * Return 405 response if no matching route found within resource #538 diff --git a/Cargo.toml b/Cargo.toml index 5a75e9ac6..f0bcabee0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "1.0.0-alpha.1" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" -keywords = ["http", "web", "framework", "async", "futures"] +keywords = ["actix", "http", "web", "framework", "async"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" documentation = "https://docs.rs/actix-web/" @@ -17,7 +17,6 @@ edition = "2018" [badges] travis-ci = { repository = "actix/actix-web", branch = "master" } -appveyor = { repository = "fafhrd91/actix-web-hdy9d" } codecov = { repository = "actix/actix-web", branch = "master", service = "github" } [lib] @@ -71,12 +70,12 @@ actix-service = "0.3.4" actix-utils = "0.3.4" actix-router = "0.1.0" actix-rt = "0.2.2" -actix-web-codegen = { path="actix-web-codegen" } -actix-http = { path = "actix-http", features=["fail"] } +actix-web-codegen = "0.1.0-alpha.1" +actix-http = { version = "0.1.0-alpha.1", features=["fail"] } actix-server = "0.4.1" actix-server-config = "0.1.0" actix-threadpool = "0.1.0" -awc = { path = "awc", optional = true } +awc = { version = "0.1.0-alpha.1", optional = true } bytes = "0.4" derive_more = "0.14" @@ -104,14 +103,14 @@ openssl = { version="0.10", optional = true } # rustls = { version = "^0.15", optional = true } [dev-dependencies] -actix-http = { path = "actix-http", features=["ssl", "brotli", "flate2-zlib"] } -actix-http-test = { path = "test-server", features=["ssl"] } +actix-http = { version = "0.1.0-alpha.1", features=["ssl", "brotli", "flate2-zlib"] } +actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } rand = "0.6" env_logger = "0.6" serde_derive = "1.0" tokio-timer = "0.2.8" -brotli2 = { version="^0.3.2" } -flate2 = { version="^1.0.2" } +brotli2 = "^0.3.2" +flate2 = "^1.0.2" [profile.release] lto = true diff --git a/README.md b/README.md index ce9efbb71..35b886adf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# Actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Actix web is a simple, pragmatic and extremely fast web framework for Rust. @@ -10,11 +10,10 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust. * Configurable [request routing](https://actix.rs/docs/url-dispatch/) * Multipart streams * Static assets -* SSL support with OpenSSL or `native-tls` +* SSL support with OpenSSL or native-tls * Middlewares ([Logger, Session, CORS, CSRF, etc](https://actix.rs/docs/middleware/)) * Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) * Supports [Actix actor framework](https://github.com/actix/actix) -* Experimental [Async/Await](https://github.com/mehcode/actix-web-async-await) support. ## Documentation & community resources @@ -36,8 +35,8 @@ fn index(info: web::Path<(u32, String)>) -> impl Responder { fn main() -> std::io::Result<()> { HttpServer::new( - || App::new() - .service(web::resource("/{id}/{name}/index.html") + || App::new().service( + web::resource("/{id}/{name}/index.html") .route(web::get().to(index))) .bind("127.0.0.1:8080")? .run(); diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index b93e282ac..95ec1c35f 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,5 +1,5 @@ # Changes -## [0.1.0] - 2018-10-x +## [0.1.0-alpha.1] - 2019-03-28 * Initial impl diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index ba8fbb6c2..d6ae67540 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -2,7 +2,7 @@ name = "actix-files" version = "0.1.0-alpha.1" authors = ["Nikolay Kim "] -description = "Static files support for Actix web." +description = "Static files support for actix web." readme = "README.md" keywords = ["actix", "http", "async", "futures"] homepage = "https://actix.rs" @@ -18,13 +18,13 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { path=".." } -actix-http = { path="../actix-http" } +actix-web = "1.0.0-alpha.1" +actix-http = "0.1.0-alpha.1" actix-service = "0.3.3" bitflags = "1" bytes = "0.4" -futures = "0.1" +futures = "0.1.25" derive_more = "0.14" log = "0.4" mime = "0.3" @@ -33,4 +33,4 @@ percent-encoding = "1.0" v_htmlescape = "0.4" [dev-dependencies] -actix-web = { path="..", features=["ssl"] } +actix-web = { version = "1.0.0-alpha.1", features=["ssl"] } diff --git a/actix-files/README.md b/actix-files/README.md index c7e195de9..5b133f57e 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -1,82 +1 @@ -# Actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -Actix web is a simple, pragmatic and extremely fast web framework for Rust. - -* Supported *HTTP/1.x* and [*HTTP/2.0*](https://actix.rs/docs/http2/) protocols -* Streaming and pipelining -* Keep-alive and slow requests handling -* Client/server [WebSockets](https://actix.rs/docs/websockets/) support -* Transparent content compression/decompression (br, gzip, deflate) -* Configurable [request routing](https://actix.rs/docs/url-dispatch/) -* Multipart streams -* Static assets -* SSL support with OpenSSL or `native-tls` -* Middlewares ([Logger, Session, CORS, CSRF, etc](https://actix.rs/docs/middleware/)) -* Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) -* Built on top of [Actix actor framework](https://github.com/actix/actix) -* Experimental [Async/Await](https://github.com/mehcode/actix-web-async-await) support. - -## Documentation & community resources - -* [User Guide](https://actix.rs/docs/) -* [API Documentation (Development)](https://actix.rs/actix-web/actix_web/) -* [API Documentation (Releases)](https://actix.rs/api/actix-web/stable/actix_web/) -* [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-web](https://crates.io/crates/actix-web) -* Minimum supported Rust version: 1.31 or later - -## Example - -```rust -extern crate actix_web; -use actix_web::{http, server, App, Path, Responder}; - -fn index(info: Path<(u32, String)>) -> impl Responder { - format!("Hello {}! id:{}", info.1, info.0) -} - -fn main() { - server::new( - || App::new() - .route("/{id}/{name}/index.html", http::Method::GET, index)) - .bind("127.0.0.1:8080").unwrap() - .run(); -} -``` - -### More examples - -* [Basics](https://github.com/actix/examples/tree/master/basics/) -* [Stateful](https://github.com/actix/examples/tree/master/state/) -* [Protobuf support](https://github.com/actix/examples/tree/master/protobuf/) -* [Multipart streams](https://github.com/actix/examples/tree/master/multipart/) -* [Simple websocket](https://github.com/actix/examples/tree/master/websocket/) -* [Tera](https://github.com/actix/examples/tree/master/template_tera/) / - [Askama](https://github.com/actix/examples/tree/master/template_askama/) templates -* [Diesel integration](https://github.com/actix/examples/tree/master/diesel/) -* [r2d2](https://github.com/actix/examples/tree/master/r2d2/) -* [SSL / HTTP/2.0](https://github.com/actix/examples/tree/master/tls/) -* [Tcp/Websocket chat](https://github.com/actix/examples/tree/master/websocket-chat/) -* [Json](https://github.com/actix/examples/tree/master/json/) - -You may consider checking out -[this directory](https://github.com/actix/examples/tree/master/) for more examples. - -## Benchmarks - -* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r16&hw=ph&test=plaintext) - -## License - -This project is licensed under either of - -* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) -* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) - -at your option. - -## Code of Conduct - -Contribution to the actix-web crate is organized under the terms of the -Contributor Covenant, the maintainer of actix-web, @fafhrd91, promises to -intervene to uphold that code of conduct. +# Static files support for actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-files)](https://crates.io/crates/actix-files) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 5862ac846..07f712863 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -98,7 +98,7 @@ openssl = { version="0.10", optional = true } actix-rt = "0.2.2" actix-server = { version = "0.4.0", features=["ssl"] } actix-connect = { version = "0.1.0", features=["ssl"] } -actix-http-test = { path="../test-server", features=["ssl"] } +actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } env_logger = "0.6" serde_derive = "1.0" diff --git a/actix-session/CHANGES.md b/actix-session/CHANGES.md new file mode 100644 index 000000000..95ec1c35f --- /dev/null +++ b/actix-session/CHANGES.md @@ -0,0 +1,5 @@ +# Changes + +## [0.1.0-alpha.1] - 2019-03-28 + +* Initial impl diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml index 3adcc8f53..a2b8e0e15 100644 --- a/actix-session/Cargo.toml +++ b/actix-session/Cargo.toml @@ -24,16 +24,16 @@ default = ["cookie-session"] cookie-session = ["cookie/secure"] [dependencies] -actix-web = { path=".." } +actix-web = "1.0.0-alpha.1" actix-service = "0.3.3" bytes = "0.4" cookie = { version="0.11", features=["percent-encode"], optional=true } derive_more = "0.14" -futures = "0.1" +futures = "0.1.25" hashbrown = "0.1.8" serde = "1.0" serde_json = "1.0" time = "0.1" [dev-dependencies] -actix-rt = "0.2.1" +actix-rt = "0.2.2" diff --git a/actix-session/README.md b/actix-session/README.md new file mode 100644 index 000000000..504fe150c --- /dev/null +++ b/actix-session/README.md @@ -0,0 +1 @@ +# Session for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-session)](https://crates.io/crates/actix-session) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md new file mode 100644 index 000000000..95ec1c35f --- /dev/null +++ b/actix-web-actors/CHANGES.md @@ -0,0 +1,5 @@ +# Changes + +## [0.1.0-alpha.1] - 2019-03-28 + +* Initial impl diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 95b726619..c0ef89fac 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "actix-web-actors" -version = "0.1.0-alpha.1" +version = "1.0.0-alpha.1" authors = ["Nikolay Kim "] description = "Actix actors support for actix web framework." readme = "README.md" -keywords = ["http", "web", "framework", "async", "futures"] +keywords = ["actix", "http", "web", "framework", "async"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" documentation = "https://docs.rs/actix-web-actors/" @@ -18,14 +18,14 @@ name = "actix_web_actors" path = "src/lib.rs" [dependencies] -actix-web = { path=".." } -actix = { git = "https://github.com/actix/actix.git" } -actix-http = { path = "../actix-http/" } -actix-codec = "0.1.1" +actix = "0.8.0-alpha.1" +actix-web = "1.0.0-alpha.1" +actix-http = "0.1.0-alpha.1" +actix-codec = "0.1.2" bytes = "0.4" -futures = "0.1" +futures = "0.1.25" [dev-dependencies] env_logger = "0.6" -actix-http = { path = "../actix-http/", features=["ssl"] } -actix-http-test = { path = "../test-server/", features=["ssl"] } +actix-http = { version = "0.1.0-alpha.1", features=["ssl"] } +actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md new file mode 100644 index 000000000..c70990385 --- /dev/null +++ b/actix-web-actors/README.md @@ -0,0 +1 @@ +Actix actors support for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web-actors)](https://crates.io/crates/actix-web-actors) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md new file mode 100644 index 000000000..95ec1c35f --- /dev/null +++ b/actix-web-codegen/CHANGES.md @@ -0,0 +1,5 @@ +# Changes + +## [0.1.0-alpha.1] - 2019-03-28 + +* Initial impl diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 3785acb32..8c9ce00c8 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "actix-web-codegen" -description = "Actix web codegen macros" version = "0.1.0-alpha.1" +description = "Actix web proc macros" +readme = "README.md" authors = ["Nikolay Kim "] license = "MIT/Apache-2.0" edition = "2018" @@ -16,5 +17,5 @@ syn = { version = "0.15", features = ["full", "parsing"] } [dev-dependencies] actix-web = { path = ".." } -actix-http = { path = "../actix-http/", features=["ssl"] } -actix-http-test = { path = "../test-server/", features=["ssl"] } +actix-http = { version = "0.1.0-alpha.1", features=["ssl"] } +actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md new file mode 100644 index 000000000..c44a5fc7f --- /dev/null +++ b/actix-web-codegen/README.md @@ -0,0 +1 @@ +# Macros for actix-web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web-codegen)](https://crates.io/crates/actix-web-codegen) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) diff --git a/awc/CHANGES.md b/awc/CHANGES.md new file mode 100644 index 000000000..95ec1c35f --- /dev/null +++ b/awc/CHANGES.md @@ -0,0 +1,5 @@ +# Changes + +## [0.1.0-alpha.1] - 2019-03-28 + +* Initial impl diff --git a/awc/Cargo.toml b/awc/Cargo.toml index c915475f8..65724dd77 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -2,9 +2,9 @@ name = "awc" version = "0.1.0-alpha.1" authors = ["Nikolay Kim "] -description = "Actix web client." +description = "Actix http client." readme = "README.md" -keywords = ["http", "web", "framework", "async", "futures"] +keywords = ["actix", "http", "framework", "async", "web"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" documentation = "https://docs.rs/awc/" @@ -41,11 +41,11 @@ flate2-rust = ["actix-http/flate2-rust"] [dependencies] actix-codec = "0.1.1" actix-service = "0.3.4" -actix-http = { path = "../actix-http/" } +actix-http = "0.1.0-alpha.1" base64 = "0.10.1" bytes = "0.4" derive_more = "0.14" -futures = "0.1" +futures = "0.1.25" log =" 0.4" percent-encoding = "1.0" rand = "0.6" @@ -57,14 +57,14 @@ cookie = { version="0.11", features=["percent-encode"], optional = true } openssl = { version="0.10", optional = true } [dev-dependencies] -actix-rt = "0.2.1" -actix-web = { path = "..", features=["ssl"] } -actix-http = { path = "../actix-http/", features=["ssl"] } -actix-http-test = { path = "../test-server/", features=["ssl"] } +actix-rt = "0.2.2" +actix-web = { version = "1.0.0-alpha.1", features=["ssl"] } +actix-http = { version = "0.1.0-alpha.1", features=["ssl"] } +actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } actix-utils = "0.3.4" actix-server = { version = "0.4.0", features=["ssl"] } -brotli2 = { version="^0.3.2" } -flate2 = { version="^1.0.2" } +brotli2 = { version="0.3.2" } +flate2 = { version="1.0.2" } env_logger = "0.6" mime = "0.3" rand = "0.6" diff --git a/awc/README.md b/awc/README.md new file mode 100644 index 000000000..bb64559c1 --- /dev/null +++ b/awc/README.md @@ -0,0 +1 @@ +# Actix http client [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/awc)](https://crates.io/crates/awc) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) diff --git a/test-server/CHANGES.md b/test-server/CHANGES.md new file mode 100644 index 000000000..95ec1c35f --- /dev/null +++ b/test-server/CHANGES.md @@ -0,0 +1,5 @@ +# Changes + +## [0.1.0-alpha.1] - 2019-03-28 + +* Initial impl diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 6959adbe5..b6bbcf16b 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -35,11 +35,11 @@ ssl = ["openssl", "actix-http/ssl", "actix-server/ssl", "awc/ssl"] [dependencies] actix-codec = "0.1.1" actix-rt = "0.2.1" -actix-http = { path="../actix-http" } +actix-http = "0.1.0-alpha.1" actix-service = "0.3.4" actix-server = "0.4.0" actix-utils = "0.3.4" -awc = { path = "../awc" } +awc = "0.1.0-alpha.1" base64 = "0.10" bytes = "0.4" @@ -61,4 +61,4 @@ tokio-timer = "0.2" openssl = { version="0.10", optional = true } [dev-dependencies] -actix-web = { path=".." } +actix-web = "1.0.0-alpha.1" diff --git a/test-server/README.md b/test-server/README.md new file mode 100644 index 000000000..596dddf87 --- /dev/null +++ b/test-server/README.md @@ -0,0 +1 @@ +# Actix http test server [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-http-test)](https://crates.io/crates/actix-http-test) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) From a2c9ff3a33830fc5d3d2f9b6a04cb7e4945ab7ab Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 14:10:03 -0700 Subject: [PATCH 370/427] back to development --- Cargo.toml | 12 ++++++------ actix-http/Cargo.toml | 4 ++-- actix-http/tests/test_server.rs | 4 ++-- actix-web-codegen/Cargo.toml | 2 +- awc/Cargo.toml | 8 ++++---- test-server/Cargo.toml | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f0bcabee0..25d950a98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,11 +71,11 @@ actix-utils = "0.3.4" actix-router = "0.1.0" actix-rt = "0.2.2" actix-web-codegen = "0.1.0-alpha.1" -actix-http = { version = "0.1.0-alpha.1", features=["fail"] } +actix-http = { path = "actix-http", features=["fail"] } actix-server = "0.4.1" actix-server-config = "0.1.0" actix-threadpool = "0.1.0" -awc = { version = "0.1.0-alpha.1", optional = true } +awc = { path = "awc", optional = true } bytes = "0.4" derive_more = "0.14" @@ -103,14 +103,14 @@ openssl = { version="0.10", optional = true } # rustls = { version = "^0.15", optional = true } [dev-dependencies] -actix-http = { version = "0.1.0-alpha.1", features=["ssl", "brotli", "flate2-zlib"] } -actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } +actix-http = { path = "actix-http", features=["ssl", "brotli", "flate2-zlib"] } +actix-http-test = { path = "test-server", features=["ssl"] } rand = "0.6" env_logger = "0.6" serde_derive = "1.0" tokio-timer = "0.2.8" -brotli2 = "^0.3.2" -flate2 = "^1.0.2" +brotli2 = "0.3.2" +flate2 = "1.0.2" [profile.release] lto = true diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 07f712863..9734bd1a6 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -98,9 +98,9 @@ openssl = { version="0.10", optional = true } actix-rt = "0.2.2" actix-server = { version = "0.4.0", features=["ssl"] } actix-connect = { version = "0.1.0", features=["ssl"] } -actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } +actix-http-test = { path = "../test-server", features=["ssl"] } env_logger = "0.6" serde_derive = "1.0" openssl = { version="0.10" } -tokio-tcp = "0.1" \ No newline at end of file +tokio-tcp = "0.1" diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index 5777c5691..455edfec1 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -13,8 +13,8 @@ use futures::stream::{once, Stream}; use actix_http::body::Body; use actix_http::error::PayloadError; use actix_http::{ - body, error, http, http::header, Error, HttpMessage as HttpMessage2, HttpService, - KeepAlive, Request, Response, + body, error, http, http::header, Error, HttpMessage, HttpService, KeepAlive, + Request, Response, }; fn load_body(stream: S) -> impl Future diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 8c9ce00c8..1f04cfd57 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -16,6 +16,6 @@ quote = "0.6" syn = { version = "0.15", features = ["full", "parsing"] } [dev-dependencies] -actix-web = { path = ".." } +actix-web = { version = "1.0.0-alpha.1" } actix-http = { version = "0.1.0-alpha.1", features=["ssl"] } actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 65724dd77..de5142995 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -41,7 +41,7 @@ flate2-rust = ["actix-http/flate2-rust"] [dependencies] actix-codec = "0.1.1" actix-service = "0.3.4" -actix-http = "0.1.0-alpha.1" +actix-http = { path = "../actix-http" } base64 = "0.10.1" bytes = "0.4" derive_more = "0.14" @@ -58,9 +58,9 @@ openssl = { version="0.10", optional = true } [dev-dependencies] actix-rt = "0.2.2" -actix-web = { version = "1.0.0-alpha.1", features=["ssl"] } -actix-http = { version = "0.1.0-alpha.1", features=["ssl"] } -actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } +actix-web = { path = "..", features=["ssl"] } +actix-http = { path = "../actix-http", features=["ssl"] } +actix-http-test = { path = "../test-server", features=["ssl"] } actix-utils = "0.3.4" actix-server = { version = "0.4.0", features=["ssl"] } brotli2 = { version="0.3.2" } diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index b6bbcf16b..582d96ed3 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -35,11 +35,11 @@ ssl = ["openssl", "actix-http/ssl", "actix-server/ssl", "awc/ssl"] [dependencies] actix-codec = "0.1.1" actix-rt = "0.2.1" -actix-http = "0.1.0-alpha.1" +actix-http = { path = "../actix-http" } actix-service = "0.3.4" actix-server = "0.4.0" actix-utils = "0.3.4" -awc = "0.1.0-alpha.1" +awc = { path = "../awc" } base64 = "0.10" bytes = "0.4" @@ -61,4 +61,4 @@ tokio-timer = "0.2" openssl = { version="0.10", optional = true } [dev-dependencies] -actix-web = "1.0.0-alpha.1" +actix-web = { path = ".." } From 878f32c4950247d49b9ffeb63385e4a133ee750c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 14:27:07 -0700 Subject: [PATCH 371/427] fix tests for no-default-features --- .travis.yml | 3 ++- src/app.rs | 1 + tests/test_httpserver.rs | 41 ++++++++++++++++++++++------------------ tests/test_server.rs | 24 ++++++++++++++++++++--- 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index 061c8b8e7..7f61201f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,7 +34,8 @@ before_script: - export PATH=$PATH:~/.cargo/bin script: - - cargo clean + - cargo update + - cargo check --all --no-default-features - cargo test --all-features --all -- --nocapture # Upload docs diff --git a/src/app.rs b/src/app.rs index 0daa54b66..a6dfdd554 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,6 +10,7 @@ use actix_service::boxed::{self, BoxedNewService}; use actix_service::{ ApplyTransform, IntoNewService, IntoTransform, NewService, Transform, }; +#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] use bytes::Bytes; use futures::{IntoFuture, Stream}; diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index 4a3850f13..bafe578e9 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -2,9 +2,8 @@ use net2::TcpBuilder; use std::sync::mpsc; use std::{net, thread, time::Duration}; -use actix_http::{client, Response}; - -use actix_web::{test, web, App, HttpServer}; +use actix_http::Response; +use actix_web::{web, App, HttpServer}; fn unused_addr() -> net::SocketAddr { let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); @@ -48,22 +47,28 @@ fn test_start() { }); let (srv, sys) = rx.recv().unwrap(); - let client = test::run_on(|| { - Ok::<_, ()>( - awc::Client::build() - .connector( - client::Connector::new() - .timeout(Duration::from_millis(100)) - .service(), - ) - .finish(), - ) - }) - .unwrap(); - let host = format!("http://{}", addr); + #[cfg(feature = "client")] + { + use actix_http::client; + use actix_web::test; - let response = test::block_on(client.get(host.clone()).send()).unwrap(); - assert!(response.status().is_success()); + let client = test::run_on(|| { + Ok::<_, ()>( + awc::Client::build() + .connector( + client::Connector::new() + .timeout(Duration::from_millis(100)) + .service(), + ) + .finish(), + ) + }) + .unwrap(); + let host = format!("http://{}", addr); + + let response = test::block_on(client.get(host.clone()).send()).unwrap(); + assert!(response.status().is_success()); + } // stop let _ = srv.stop(false); diff --git a/tests/test_server.rs b/tests/test_server.rs index c30cf67f8..cd5e95d20 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -11,10 +11,13 @@ use bytes::Bytes; use flate2::read::GzDecoder; use flate2::write::{GzEncoder, ZlibDecoder, ZlibEncoder}; use flate2::Compression; -use futures::stream::once; //Future, Stream +use futures::stream::once; use rand::{distributions::Alphanumeric, Rng}; -use actix_web::{dev::HttpMessageBody, middleware::encoding, web, App}; +use actix_web::{dev::HttpMessageBody, web, App}; + +#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] +use actix_web::middleware::encoding; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -55,6 +58,7 @@ fn test_body() { assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] #[test] fn test_body_gzip() { let mut srv = TestServer::new(|| { @@ -78,6 +82,7 @@ fn test_body_gzip() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] #[test] fn test_body_encoding_override() { let mut srv = TestServer::new(|| { @@ -104,6 +109,7 @@ fn test_body_encoding_override() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] #[test] fn test_body_gzip_large() { let data = STR.repeat(10); @@ -134,6 +140,7 @@ fn test_body_gzip_large() { assert_eq!(Bytes::from(dec), Bytes::from(data)); } +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] #[test] fn test_body_gzip_large_random() { let data = rand::thread_rng() @@ -168,6 +175,7 @@ fn test_body_gzip_large_random() { assert_eq!(Bytes::from(dec), Bytes::from(data)); } +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] #[test] fn test_body_chunked_implicit() { let mut srv = TestServer::new(move || { @@ -200,6 +208,7 @@ fn test_body_chunked_implicit() { } #[test] +#[cfg(feature = "brotli")] fn test_body_br_streaming() { let mut srv = TestServer::new(move || { h1::H1Service::new( @@ -277,6 +286,7 @@ fn test_no_chunking() { } #[test] +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] fn test_body_deflate() { let mut srv = TestServer::new(move || { h1::H1Service::new( @@ -303,6 +313,7 @@ fn test_body_deflate() { } #[test] +#[cfg(any(feature = "brotli"))] fn test_body_brotli() { let mut srv = TestServer::new(move || { h1::H1Service::new( @@ -336,6 +347,7 @@ fn test_body_brotli() { } #[test] +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] fn test_encoding() { let mut srv = TestServer::new(move || { HttpService::new( @@ -364,6 +376,7 @@ fn test_encoding() { } #[test] +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] fn test_gzip_encoding() { let mut srv = TestServer::new(move || { HttpService::new( @@ -392,6 +405,7 @@ fn test_gzip_encoding() { } #[test] +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] fn test_gzip_encoding_large() { let data = STR.repeat(10); let mut srv = TestServer::new(move || { @@ -421,6 +435,7 @@ fn test_gzip_encoding_large() { } #[test] +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] fn test_reading_gzip_encoding_large_random() { let data = rand::thread_rng() .sample_iter(&Alphanumeric) @@ -455,6 +470,7 @@ fn test_reading_gzip_encoding_large_random() { } #[test] +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] fn test_reading_deflate_encoding() { let mut srv = TestServer::new(move || { h1::H1Service::new( @@ -483,6 +499,7 @@ fn test_reading_deflate_encoding() { } #[test] +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] fn test_reading_deflate_encoding_large() { let data = STR.repeat(10); let mut srv = TestServer::new(move || { @@ -512,6 +529,7 @@ fn test_reading_deflate_encoding_large() { } #[test] +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] fn test_reading_deflate_encoding_large_random() { let data = rand::thread_rng() .sample_iter(&Alphanumeric) @@ -545,8 +563,8 @@ fn test_reading_deflate_encoding_large_random() { assert_eq!(bytes, Bytes::from(data)); } -#[cfg(feature = "brotli")] #[test] +#[cfg(feature = "brotli")] fn test_brotli_encoding() { let mut srv = TestServer::new(move || { h1::H1Service::new( From 670a457013d6797d03f32f3b2dc0742192d5deb6 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 14:28:59 -0700 Subject: [PATCH 372/427] fix docs.rs feature list --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 25d950a98..46ece2c28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ members = [ ] [package.metadata.docs.rs] -features = ["ssl", "tls", "rust-tls", "brotli", "flate2-zlib", "cookies", "client"] +features = ["ssl", "tls", "brotli", "flate2-zlib", "cookies", "client"] [features] default = ["brotli", "flate2-zlib", "cookies", "client"] From 1d79f1652935430cd4b552a94cd2fb1ea1195ff4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 14:30:38 -0700 Subject: [PATCH 373/427] update release api docs link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 35b886adf..876d95f6f 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust. * [User Guide](https://actix.rs/docs/) * [API Documentation (Development)](https://actix.rs/actix-web/actix_web/) -* [API Documentation (Releases)](https://docs.rs/actix-web/) +* [API Documentation (Releases)](https://docs.rs/actix-web/0.7.18/actix_web/) * [Chat on gitter](https://gitter.im/actix/actix) * Cargo package: [actix-web](https://crates.io/crates/actix-web) * Minimum supported Rust version: 1.32 or later From 9710e9b01f66eb6d26384e29de8d3c34cb65e94b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 14:46:33 -0700 Subject: [PATCH 374/427] Re-export actix_http::client::Connector --- Cargo.toml | 2 +- awc/CHANGES.md | 7 +++++++ awc/Cargo.toml | 2 +- awc/src/lib.rs | 3 +-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 46ece2c28..19fb0e477 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ default = ["brotli", "flate2-zlib", "cookies", "client"] client = ["awc"] # brotli encoding, requires c compiler -brotli = ["actix-http/brotli2"] +brotli = ["actix-http/brotli"] # miniz-sys backend for flate2 crate flate2-zlib = ["actix-http/flate2-zlib"] diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 95ec1c35f..4656df391 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,5 +1,12 @@ # Changes +## [0.1.0-alpha.2] - 2019-04-xx + +### Added + +* Re-export `actix_http::client::Connector` + + ## [0.1.0-alpha.1] - 2019-03-28 * Initial impl diff --git a/awc/Cargo.toml b/awc/Cargo.toml index de5142995..69c9e9831 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "0.1.0-alpha.1" +version = "0.1.0-alpha.2" authors = ["Nikolay Kim "] description = "Actix http client." readme = "README.md" diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 9f5ca1f28..ca237798c 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -23,9 +23,8 @@ use std::cell::RefCell; use std::rc::Rc; -pub use actix_http::http; +pub use actix_http::{client::Connector, http}; -use actix_http::client::Connector; use actix_http::http::{HttpTryFrom, Method, Uri}; use actix_http::RequestHead; From c4a8bbe47b760f239a7dca04a5cca4e732e8a190 Mon Sep 17 00:00:00 2001 From: joekyo Date: Fri, 29 Mar 2019 11:03:17 +0800 Subject: [PATCH 375/427] fix the example in README.md (#739) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 876d95f6f..3cd0f3659 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,9 @@ fn main() -> std::io::Result<()> { HttpServer::new( || App::new().service( web::resource("/{id}/{name}/index.html") - .route(web::get().to(index))) + .route(web::get().to(index)))) .bind("127.0.0.1:8080")? - .run(); + .run() } ``` From 80ff7d40a10ad76faa968d37ea837242937b5e58 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 20:27:47 -0700 Subject: [PATCH 376/427] enable awc/ssl if ssl features is enabled --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 19fb0e477..be5b6c902 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ cookies = ["cookie", "actix-http/cookies"] tls = ["native-tls", "actix-server/ssl"] # openssl -ssl = ["openssl", "actix-server/ssl"] +ssl = ["openssl", "actix-server/ssl", "awc/ssl"] # rustls # rust-tls = ["rustls", "actix-server/rustls"] From 3b897da8e25f7b09b8e56eb1c022df1e16279478 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 21:15:26 -0700 Subject: [PATCH 377/427] Do not use thread pool for decomression if chunk size is smaller than 2048 --- actix-http/CHANGES.md | 7 +++++++ actix-http/Cargo.toml | 2 +- actix-http/src/encoding/decoder.rs | 26 ++++++++++++++++---------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 95ec1c35f..e95963496 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,5 +1,12 @@ # Changes +## [0.1.0-alpha.2] - 2019-xx-xx + +### Changed + +* Do not use thread pool for decomression if chunk size is smaller than 2048. + + ## [0.1.0-alpha.1] - 2019-03-28 * Initial impl diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 9734bd1a6..180cda787 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "0.1.0-alpha.1" +version = "0.1.0-alpha.2" authors = ["Nikolay Kim "] description = "Actix http primitives" readme = "README.md" diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index ae2b4ae6b..16d15e905 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -12,6 +12,8 @@ use super::Writer; use crate::error::PayloadError; use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING}; +const INPLACE: usize = 2049; + pub struct Decoder { decoder: Option, stream: S, @@ -92,10 +94,18 @@ where match self.stream.poll()? { Async::Ready(Some(chunk)) => { if let Some(mut decoder) = self.decoder.take() { - self.fut = Some(run(move || { + if chunk.len() < INPLACE { let chunk = decoder.feed_data(chunk)?; - Ok((chunk, decoder)) - })); + self.decoder = Some(decoder); + if let Some(chunk) = chunk { + return Ok(Async::Ready(Some(chunk))); + } + } else { + self.fut = Some(run(move || { + let chunk = decoder.feed_data(chunk)?; + Ok((chunk, decoder)) + })); + } continue; } else { return Ok(Async::Ready(Some(chunk))); @@ -103,14 +113,10 @@ where } Async::Ready(None) => { self.eof = true; - if let Some(mut decoder) = self.decoder.take() { - self.fut = Some(run(move || { - let chunk = decoder.feed_eof()?; - Ok((chunk, decoder)) - })); - continue; + return if let Some(mut decoder) = self.decoder.take() { + Ok(Async::Ready(decoder.feed_eof()?)) } else { - return Ok(Async::Ready(None)); + Ok(Async::Ready(None)) }; } Async::NotReady => break, From ea4d98d669616e42cd1d05e6b74b0a341db72686 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 21:48:35 -0700 Subject: [PATCH 378/427] Session wide headers, basic and bearer auth --- awc/CHANGES.md | 4 ++ awc/src/builder.rs | 83 +++++++++++++++++++++++++++++++--- awc/src/lib.rs | 43 ++++++++++++------ awc/src/request.rs | 108 ++++++++++++++++++++++++++++++++++++++++++++- awc/src/test.rs | 19 ++++++++ awc/src/ws.rs | 2 +- 6 files changed, 237 insertions(+), 22 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 4656df391..b24ae50b2 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -6,6 +6,10 @@ * Re-export `actix_http::client::Connector` +* Session wide headers + +* Session wide basic and bearer auth + ## [0.1.0-alpha.1] - 2019-03-28 diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 562ff2419..d53d0d442 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -3,13 +3,11 @@ use std::fmt; use std::rc::Rc; use actix_http::client::{ConnectError, Connection, Connector}; -use actix_http::http::{ - header::IntoHeaderValue, HeaderMap, HeaderName, HttpTryFrom, Uri, -}; +use actix_http::http::{header, HeaderMap, HeaderName, HttpTryFrom, Uri}; use actix_service::Service; use crate::connect::{Connect, ConnectorWrapper}; -use crate::Client; +use crate::{Client, ClientConfig}; /// An HTTP Client builder /// @@ -77,7 +75,7 @@ impl ClientBuilder { where HeaderName: HttpTryFrom, >::Error: fmt::Debug, - V: IntoHeaderValue, + V: header::IntoHeaderValue, V::Error: fmt::Debug, { match HeaderName::try_from(key) { @@ -92,10 +90,85 @@ impl ClientBuilder { self } + /// Set client wide HTTP basic authorization header + pub fn basic_auth(self, username: U, password: Option<&str>) -> Self + where + U: fmt::Display, + { + let auth = match password { + Some(password) => format!("{}:{}", username, password), + None => format!("{}", username), + }; + self.header( + header::AUTHORIZATION, + format!("Basic {}", base64::encode(&auth)), + ) + } + + /// Set client wide HTTP bearer authentication header + pub fn bearer_auth(self, token: T) -> Self + where + T: fmt::Display, + { + self.header(header::AUTHORIZATION, format!("Bearer {}", token)) + } + /// Finish build process and create `Client` instance. pub fn finish(self) -> Client { Client { connector: self.connector, + config: Rc::new(ClientConfig { + headers: self.headers, + }), } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test; + + #[test] + fn client_basic_auth() { + test::run_on(|| { + let client = ClientBuilder::new().basic_auth("username", Some("password")); + assert_eq!( + client + .headers + .get(header::AUTHORIZATION) + .unwrap() + .to_str() + .unwrap(), + "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + ); + + let client = ClientBuilder::new().basic_auth("username", None); + assert_eq!( + client + .headers + .get(header::AUTHORIZATION) + .unwrap() + .to_str() + .unwrap(), + "Basic dXNlcm5hbWU=" + ); + }); + } + + #[test] + fn client_bearer_auth() { + test::run_on(|| { + let client = ClientBuilder::new().bearer_auth("someS3cr3tAutht0k3n"); + assert_eq!( + client + .headers + .get(header::AUTHORIZATION) + .unwrap() + .to_str() + .unwrap(), + "Bearer someS3cr3tAutht0k3n" + ); + }) + } +} diff --git a/awc/src/lib.rs b/awc/src/lib.rs index ca237798c..3518bf8b4 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -25,7 +25,7 @@ use std::rc::Rc; pub use actix_http::{client::Connector, http}; -use actix_http::http::{HttpTryFrom, Method, Uri}; +use actix_http::http::{HeaderMap, HttpTryFrom, Method, Uri}; use actix_http::RequestHead; mod builder; @@ -68,6 +68,11 @@ use self::connect::{Connect, ConnectorWrapper}; #[derive(Clone)] pub struct Client { pub(crate) connector: Rc>, + pub(crate) config: Rc, +} + +pub(crate) struct ClientConfig { + pub(crate) headers: HeaderMap, } impl Default for Client { @@ -76,6 +81,9 @@ impl Default for Client { connector: Rc::new(RefCell::new(ConnectorWrapper( Connector::new().service(), ))), + config: Rc::new(ClientConfig { + headers: HeaderMap::new(), + }), } } } @@ -96,7 +104,12 @@ impl Client { where Uri: HttpTryFrom, { - ClientRequest::new(method, url, self.connector.clone()) + let mut req = ClientRequest::new(method, url, self.connector.clone()); + + for (key, value) in &self.config.headers { + req.head.headers.insert(key.clone(), value.clone()); + } + req } /// Create `ClientRequest` from `RequestHead` @@ -107,13 +120,10 @@ impl Client { where Uri: HttpTryFrom, { - let mut req = - ClientRequest::new(head.method.clone(), url, self.connector.clone()); - + let mut req = self.request(head.method.clone(), url); for (key, value) in &head.headers { req.head.headers.insert(key.clone(), value.clone()); } - req } @@ -121,55 +131,60 @@ impl Client { where Uri: HttpTryFrom, { - ClientRequest::new(Method::GET, url, self.connector.clone()) + self.request(Method::GET, url) } pub fn head(&self, url: U) -> ClientRequest where Uri: HttpTryFrom, { - ClientRequest::new(Method::HEAD, url, self.connector.clone()) + self.request(Method::HEAD, url) } pub fn put(&self, url: U) -> ClientRequest where Uri: HttpTryFrom, { - ClientRequest::new(Method::PUT, url, self.connector.clone()) + self.request(Method::PUT, url) } pub fn post(&self, url: U) -> ClientRequest where Uri: HttpTryFrom, { - ClientRequest::new(Method::POST, url, self.connector.clone()) + self.request(Method::POST, url) } pub fn patch(&self, url: U) -> ClientRequest where Uri: HttpTryFrom, { - ClientRequest::new(Method::PATCH, url, self.connector.clone()) + self.request(Method::PATCH, url) } pub fn delete(&self, url: U) -> ClientRequest where Uri: HttpTryFrom, { - ClientRequest::new(Method::DELETE, url, self.connector.clone()) + self.request(Method::DELETE, url) } pub fn options(&self, url: U) -> ClientRequest where Uri: HttpTryFrom, { - ClientRequest::new(Method::OPTIONS, url, self.connector.clone()) + self.request(Method::OPTIONS, url) } pub fn ws(&self, url: U) -> WebsocketsRequest where Uri: HttpTryFrom, { - WebsocketsRequest::new(url, self.connector.clone()) + let mut req = WebsocketsRequest::new(url, self.connector.clone()); + + for (key, value) in &self.config.headers { + req.head.headers.insert(key.clone(), value.clone()); + } + req } } diff --git a/awc/src/request.rs b/awc/src/request.rs index dde51a8f5..2e778cfcf 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -241,10 +241,9 @@ impl ClientRequest { } /// Set HTTP basic authorization header - pub fn basic_auth(self, username: U, password: Option

    ) -> Self + pub fn basic_auth(self, username: U, password: Option<&str>) -> Self where U: fmt::Display, - P: fmt::Display, { let auth = match password { Some(password) => format!("{}:{}", username, password), @@ -552,3 +551,108 @@ impl fmt::Debug for ClientRequest { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test, Client}; + + #[test] + fn test_debug() { + test::run_on(|| { + let request = Client::new().get("/").header("x-test", "111"); + let repr = format!("{:?}", request); + assert!(repr.contains("ClientRequest")); + assert!(repr.contains("x-test")); + }) + } + + #[test] + fn test_client_header() { + test::run_on(|| { + let req = Client::build() + .header(header::CONTENT_TYPE, "111") + .finish() + .get("/"); + + assert_eq!( + req.head + .headers + .get(header::CONTENT_TYPE) + .unwrap() + .to_str() + .unwrap(), + "111" + ); + }) + } + + #[test] + fn test_client_header_override() { + test::run_on(|| { + let req = Client::build() + .header(header::CONTENT_TYPE, "111") + .finish() + .get("/") + .set_header(header::CONTENT_TYPE, "222"); + + assert_eq!( + req.head + .headers + .get(header::CONTENT_TYPE) + .unwrap() + .to_str() + .unwrap(), + "222" + ); + }) + } + + #[test] + fn client_basic_auth() { + test::run_on(|| { + let client = Client::new() + .get("/") + .basic_auth("username", Some("password")); + assert_eq!( + client + .head + .headers + .get(header::AUTHORIZATION) + .unwrap() + .to_str() + .unwrap(), + "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + ); + + let client = Client::new().get("/").basic_auth("username", None); + assert_eq!( + client + .head + .headers + .get(header::AUTHORIZATION) + .unwrap() + .to_str() + .unwrap(), + "Basic dXNlcm5hbWU=" + ); + }); + } + + #[test] + fn client_bearer_auth() { + test::run_on(|| { + let client = Client::new().get("/").bearer_auth("someS3cr3tAutht0k3n"); + assert_eq!( + client + .head + .headers + .get(header::AUTHORIZATION) + .unwrap() + .to_str() + .unwrap(), + "Bearer someS3cr3tAutht0k3n" + ); + }) + } +} diff --git a/awc/src/test.rs b/awc/src/test.rs index 395e62904..7723b9d2c 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -8,6 +8,25 @@ use cookie::{Cookie, CookieJar}; use crate::ClientResponse; +#[cfg(test)] +thread_local! { + static RT: std::cell::RefCell = { + std::cell::RefCell::new(actix_rt::Runtime::new().unwrap()) + }; +} + +#[cfg(test)] +pub fn run_on(f: F) -> R +where + F: Fn() -> R, +{ + RT.with(move |rt| { + rt.borrow_mut() + .block_on(futures::future::lazy(|| Ok::<_, ()>(f()))) + }) + .unwrap() +} + /// Test `ClientResponse` builder pub struct TestResponse { head: ResponseHead, diff --git a/awc/src/ws.rs b/awc/src/ws.rs index f959e62c5..ec7fc0da9 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -23,7 +23,7 @@ use crate::response::ClientResponse; /// `WebSocket` connection pub struct WebsocketsRequest { - head: RequestHead, + pub(crate) head: RequestHead, err: Option, origin: Option, protocols: Option, From 1e7096a63a5f07d41fdb10dacca7376d8833b03a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 28 Mar 2019 22:33:41 -0700 Subject: [PATCH 379/427] add request timeout --- actix-http/src/client/error.rs | 3 +++ awc/CHANGES.md | 8 +++--- awc/Cargo.toml | 1 + awc/src/builder.rs | 46 +++++++++++++++++++++++----------- awc/src/lib.rs | 28 ++++++++++----------- awc/src/request.rs | 29 +++++++++++++-------- awc/src/ws.rs | 28 +++++++++++++++------ awc/tests/test_client.rs | 26 +++++++++++++++++++ 8 files changed, 119 insertions(+), 50 deletions(-) diff --git a/actix-http/src/client/error.rs b/actix-http/src/client/error.rs index e67db5462..fc4b5b72b 100644 --- a/actix-http/src/client/error.rs +++ b/actix-http/src/client/error.rs @@ -105,6 +105,9 @@ pub enum SendRequestError { /// Http2 error #[display(fmt = "{}", _0)] H2(h2::Error), + /// Response took too long + #[display(fmt = "Timeout out while waiting for response")] + Timeout, /// Tunnels are not supported for http2 connection #[display(fmt = "Tunnels are not supported for http2 connection")] TunnelNotSupported, diff --git a/awc/CHANGES.md b/awc/CHANGES.md index b24ae50b2..9192e2db4 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -4,11 +4,13 @@ ### Added -* Re-export `actix_http::client::Connector` +* Request timeout. -* Session wide headers +* Re-export `actix_http::client::Connector`. -* Session wide basic and bearer auth +* Session wide headers. + +* Session wide basic and bearer auth. ## [0.1.0-alpha.1] - 2019-03-28 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 69c9e9831..cd94057fb 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -52,6 +52,7 @@ rand = "0.6" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.5.3" +tokio-timer = "0.2.8" cookie = { version="0.11", features=["percent-encode"], optional = true } openssl = { version="0.10", optional = true } diff --git a/awc/src/builder.rs b/awc/src/builder.rs index d53d0d442..6ef145bf8 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -1,12 +1,13 @@ use std::cell::RefCell; use std::fmt; use std::rc::Rc; +use std::time::Duration; use actix_http::client::{ConnectError, Connection, Connector}; use actix_http::http::{header, HeaderMap, HeaderName, HttpTryFrom, Uri}; use actix_service::Service; -use crate::connect::{Connect, ConnectorWrapper}; +use crate::connect::ConnectorWrapper; use crate::{Client, ClientConfig}; /// An HTTP Client builder @@ -14,11 +15,10 @@ use crate::{Client, ClientConfig}; /// This type can be used to construct an instance of `Client` through a /// builder-like pattern. pub struct ClientBuilder { - connector: Rc>, + config: ClientConfig, default_headers: bool, allow_redirects: bool, max_redirects: usize, - headers: HeaderMap, } impl ClientBuilder { @@ -27,10 +27,13 @@ impl ClientBuilder { default_headers: true, allow_redirects: true, max_redirects: 10, - headers: HeaderMap::new(), - connector: Rc::new(RefCell::new(ConnectorWrapper( - Connector::new().service(), - ))), + config: ClientConfig { + headers: HeaderMap::new(), + timeout: Some(Duration::from_secs(5)), + connector: RefCell::new(Box::new(ConnectorWrapper( + Connector::new().service(), + ))), + }, } } @@ -42,7 +45,22 @@ impl ClientBuilder { ::Future: 'static, T::Future: 'static, { - self.connector = Rc::new(RefCell::new(ConnectorWrapper(connector))); + self.config.connector = RefCell::new(Box::new(ConnectorWrapper(connector))); + self + } + + /// Set request timeout + /// + /// Request timeout is the total time before a response must be received. + /// Default value is 5 seconds. + pub fn timeout(mut self, timeout: Duration) -> Self { + self.config.timeout = Some(timeout); + self + } + + /// Disable request timeout. + pub fn disable_timeout(mut self) -> Self { + self.config.timeout = None; self } @@ -81,7 +99,7 @@ impl ClientBuilder { match HeaderName::try_from(key) { Ok(key) => match value.try_into() { Ok(value) => { - self.headers.append(key, value); + self.config.headers.append(key, value); } Err(e) => log::error!("Header value error: {:?}", e), }, @@ -115,12 +133,7 @@ impl ClientBuilder { /// Finish build process and create `Client` instance. pub fn finish(self) -> Client { - Client { - connector: self.connector, - config: Rc::new(ClientConfig { - headers: self.headers, - }), - } + Client(Rc::new(self.config)) } } @@ -135,6 +148,7 @@ mod tests { let client = ClientBuilder::new().basic_auth("username", Some("password")); assert_eq!( client + .config .headers .get(header::AUTHORIZATION) .unwrap() @@ -146,6 +160,7 @@ mod tests { let client = ClientBuilder::new().basic_auth("username", None); assert_eq!( client + .config .headers .get(header::AUTHORIZATION) .unwrap() @@ -162,6 +177,7 @@ mod tests { let client = ClientBuilder::new().bearer_auth("someS3cr3tAutht0k3n"); assert_eq!( client + .config .headers .get(header::AUTHORIZATION) .unwrap() diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 3518bf8b4..9a8daeb43 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -22,6 +22,7 @@ //! ``` use std::cell::RefCell; use std::rc::Rc; +use std::time::Duration; pub use actix_http::{client::Connector, http}; @@ -66,25 +67,23 @@ use self::connect::{Connect, ConnectorWrapper}; /// } /// ``` #[derive(Clone)] -pub struct Client { - pub(crate) connector: Rc>, - pub(crate) config: Rc, -} +pub struct Client(Rc); pub(crate) struct ClientConfig { + pub(crate) connector: RefCell>, pub(crate) headers: HeaderMap, + pub(crate) timeout: Option, } impl Default for Client { fn default() -> Self { - Client { - connector: Rc::new(RefCell::new(ConnectorWrapper( + Client(Rc::new(ClientConfig { + connector: RefCell::new(Box::new(ConnectorWrapper( Connector::new().service(), ))), - config: Rc::new(ClientConfig { - headers: HeaderMap::new(), - }), - } + headers: HeaderMap::new(), + timeout: Some(Duration::from_secs(5)), + })) } } @@ -104,9 +103,9 @@ impl Client { where Uri: HttpTryFrom, { - let mut req = ClientRequest::new(method, url, self.connector.clone()); + let mut req = ClientRequest::new(method, url, self.0.clone()); - for (key, value) in &self.config.headers { + for (key, value) in &self.0.headers { req.head.headers.insert(key.clone(), value.clone()); } req @@ -180,9 +179,8 @@ impl Client { where Uri: HttpTryFrom, { - let mut req = WebsocketsRequest::new(url, self.connector.clone()); - - for (key, value) in &self.config.headers { + let mut req = WebsocketsRequest::new(url, self.0.clone()); + for (key, value) in &self.0.headers { req.head.headers.insert(key.clone(), value.clone()); } req diff --git a/awc/src/request.rs b/awc/src/request.rs index 2e778cfcf..170be75f7 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -1,4 +1,3 @@ -use std::cell::RefCell; use std::fmt; use std::io::Write; use std::rc::Rc; @@ -10,6 +9,7 @@ use futures::future::{err, Either}; use futures::{Future, Stream}; use serde::Serialize; use serde_json; +use tokio_timer::Timeout; use actix_http::body::{Body, BodyStream}; use actix_http::encoding::Decoder; @@ -20,9 +20,9 @@ use actix_http::http::{ }; use actix_http::{Error, Payload, RequestHead}; -use crate::connect::Connect; use crate::error::{InvalidUrl, PayloadError, SendRequestError}; use crate::response::ClientResponse; +use crate::ClientConfig; #[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] const HTTPS_ENCODING: &str = "br, gzip, deflate"; @@ -62,16 +62,12 @@ pub struct ClientRequest { cookies: Option, default_headers: bool, response_decompress: bool, - connector: Rc>, + config: Rc, } impl ClientRequest { /// Create new client request builder. - pub(crate) fn new( - method: Method, - uri: U, - connector: Rc>, - ) -> Self + pub(crate) fn new(method: Method, uri: U, config: Rc) -> Self where Uri: HttpTryFrom, { @@ -87,7 +83,7 @@ impl ClientRequest { ClientRequest { head, err, - connector, + config, #[cfg(feature = "cookies")] cookies: None, default_headers: true, @@ -450,6 +446,7 @@ impl ClientRequest { let response_decompress = slf.response_decompress; let fut = slf + .config .connector .borrow_mut() .send_request(head, body.into()) @@ -462,7 +459,19 @@ impl ClientRequest { } }) }); - Either::B(fut) + + // set request timeout + if let Some(timeout) = slf.config.timeout { + Either::B(Either::A(Timeout::new(fut, timeout).map_err(|e| { + if let Some(e) = e.into_inner() { + e + } else { + SendRequestError::Timeout + } + }))) + } else { + Either::B(Either::B(fut)) + } } /// Set a JSON body and generate `ClientRequest` diff --git a/awc/src/ws.rs b/awc/src/ws.rs index ec7fc0da9..26594531d 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -1,5 +1,4 @@ //! Websockets client -use std::cell::RefCell; use std::io::Write; use std::rc::Rc; use std::{fmt, str}; @@ -10,9 +9,10 @@ use bytes::{BufMut, BytesMut}; #[cfg(feature = "cookies")] use cookie::{Cookie, CookieJar}; use futures::future::{err, Either, Future}; +use tokio_timer::Timeout; -use crate::connect::{BoxedSocket, Connect}; -use crate::error::{InvalidUrl, WsClientError}; +use crate::connect::BoxedSocket; +use crate::error::{InvalidUrl, SendRequestError, WsClientError}; use crate::http::header::{ self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION, }; @@ -20,6 +20,7 @@ use crate::http::{ ConnectionType, Error as HttpError, HttpTryFrom, Method, StatusCode, Uri, Version, }; use crate::response::ClientResponse; +use crate::ClientConfig; /// `WebSocket` connection pub struct WebsocketsRequest { @@ -32,12 +33,12 @@ pub struct WebsocketsRequest { default_headers: bool, #[cfg(feature = "cookies")] cookies: Option, - connector: Rc>, + config: Rc, } impl WebsocketsRequest { /// Create new websocket connection - pub(crate) fn new(uri: U, connector: Rc>) -> Self + pub(crate) fn new(uri: U, config: Rc) -> Self where Uri: HttpTryFrom, { @@ -54,7 +55,7 @@ impl WebsocketsRequest { WebsocketsRequest { head, err, - connector, + config, origin: None, protocols: None, max_size: 65_536, @@ -322,6 +323,7 @@ impl WebsocketsRequest { let server_mode = slf.server_mode; let fut = slf + .config .connector .borrow_mut() .open_tunnel(head) @@ -393,6 +395,18 @@ impl WebsocketsRequest { }), )) }); - Either::B(fut) + + // set request timeout + if let Some(timeout) = slf.config.timeout { + Either::B(Either::A(Timeout::new(fut, timeout).map_err(|e| { + if let Some(e) = e.into_inner() { + e + } else { + SendRequestError::Timeout.into() + } + }))) + } else { + Either::B(Either::B(fut)) + } } } diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 698b5ab7d..b2d6f8e90 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -1,14 +1,17 @@ use std::io::Write; +use std::time::Duration; use brotli2::write::BrotliEncoder; use bytes::Bytes; use flate2::write::GzEncoder; use flate2::Compression; +use futures::future::Future; use rand::Rng; use actix_http::HttpService; use actix_http_test::TestServer; use actix_web::{http::header, web, App, Error, HttpMessage, HttpRequest, HttpResponse}; +use awc::error::SendRequestError; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -57,6 +60,29 @@ fn test_simple() { assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } +#[test] +fn test_timeout() { + let mut srv = TestServer::new(|| { + HttpService::new(App::new().service(web::resource("/").route(web::to_async( + || { + tokio_timer::sleep(Duration::from_millis(200)) + .then(|_| Ok::<_, Error>(HttpResponse::Ok().body(STR))) + }, + )))) + }); + + let client = srv.execute(|| { + awc::Client::build() + .timeout(Duration::from_millis(50)) + .finish() + }); + let request = client.get(srv.url("/")).send(); + match srv.block_on(request) { + Err(SendRequestError::Timeout) => (), + _ => panic!(), + } +} + // #[test] // fn test_connection_close() { // let mut srv = From 19a0b8046bb62612970041d02a68357040f50e1a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 29 Mar 2019 11:13:36 -0700 Subject: [PATCH 380/427] remove actix reference --- actix-http/src/error.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index e6cc0e07f..b062fdf42 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -4,7 +4,6 @@ use std::str::Utf8Error; use std::string::FromUtf8Error; use std::{fmt, io, result}; -// use actix::MailboxError; pub use actix_threadpool::BlockingError; use actix_utils::timeout::TimeoutError; #[cfg(feature = "cookies")] @@ -168,9 +167,6 @@ impl ResponseError for header::InvalidHeaderValueBytes { /// `InternalServerError` for `futures::Canceled` impl ResponseError for Canceled {} -// /// `InternalServerError` for `actix::MailboxError` -// impl ResponseError for MailboxError {} - /// A set of errors that can occur during parsing HTTP streams #[derive(Debug, Display)] pub enum ParseError { From 709475b2bbd72b01bba7cd23412e601dd0351df1 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 29 Mar 2019 11:59:38 -0700 Subject: [PATCH 381/427] multipart::Field renamed to MultipartField --- CHANGES.md | 6 ++++++ src/types/mod.rs | 2 +- src/types/multipart.rs | 21 +++++++++------------ 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 13eeb67d7..47b0f9875 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Changes +## [1.0.0-alpha.2] - 2019-03-29 + +### Changed + +* multipart::Field renamed to MultipartField + ## [1.0.0-alpha.1] - 2019-03-28 ### Changed diff --git a/src/types/mod.rs b/src/types/mod.rs index c9aed94f9..9a0a08801 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -10,7 +10,7 @@ pub(crate) mod readlines; pub use self::form::{Form, FormConfig}; pub use self::json::{Json, JsonConfig}; -pub use self::multipart::{Multipart, MultipartItem}; +pub use self::multipart::{Multipart, MultipartField, MultipartItem}; pub use self::path::Path; pub use self::payload::{Payload, PayloadConfig}; pub use self::query::Query; diff --git a/src/types/multipart.rs b/src/types/multipart.rs index 50ef38135..65a64d5e1 100644 --- a/src/types/multipart.rs +++ b/src/types/multipart.rs @@ -39,7 +39,7 @@ pub struct Multipart { /// Multipart item pub enum MultipartItem { /// Multipart field - Field(Field), + Field(MultipartField), /// Nested multipart stream Nested(Multipart), } @@ -402,12 +402,9 @@ impl InnerMultipart { )?)); self.item = InnerMultipartItem::Field(Rc::clone(&field)); - Ok(Async::Ready(Some(MultipartItem::Field(Field::new( - safety.clone(), - headers, - mt, - field, - ))))) + Ok(Async::Ready(Some(MultipartItem::Field( + MultipartField::new(safety.clone(), headers, mt, field), + )))) } } } @@ -421,21 +418,21 @@ impl Drop for InnerMultipart { } /// A single field in a multipart stream -pub struct Field { +pub struct MultipartField { ct: mime::Mime, headers: HeaderMap, inner: Rc>, safety: Safety, } -impl Field { +impl MultipartField { fn new( safety: Safety, headers: HeaderMap, ct: mime::Mime, inner: Rc>, ) -> Self { - Field { + MultipartField { ct, headers, inner, @@ -466,7 +463,7 @@ impl Field { } } -impl Stream for Field { +impl Stream for MultipartField { type Item = Bytes; type Error = MultipartError; @@ -479,7 +476,7 @@ impl Stream for Field { } } -impl fmt::Debug for Field { +impl fmt::Debug for MultipartField { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "\nMultipartField: {}", self.ct)?; writeln!(f, " boundary: {}", self.inner.borrow().boundary)?; From 058b1d56e6b3c5efe48a28a25d9308915d435d20 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 29 Mar 2019 13:49:21 -0700 Subject: [PATCH 382/427] Export ws sub-module with websockets related types --- awc/CHANGES.md | 6 +++++- awc/src/error.rs | 1 + awc/src/lib.rs | 15 +++++++++++---- awc/src/ws.rs | 4 +++- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 9192e2db4..dcc38c317 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,6 @@ # Changes -## [0.1.0-alpha.2] - 2019-04-xx +## [0.1.0-alpha.2] - 2019-03-xx ### Added @@ -12,6 +12,10 @@ * Session wide basic and bearer auth. +### Changed + +* Export `ws` sub-module with websockets related types + ## [0.1.0-alpha.1] - 2019-03-28 diff --git a/awc/src/error.rs b/awc/src/error.rs index d3f1c1a17..8f51fd7db 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -1,6 +1,7 @@ //! Http client errors pub use actix_http::client::{ConnectError, InvalidUrl, SendRequestError}; pub use actix_http::error::PayloadError; +pub use actix_http::ws::HandshakeError as WsHandshakeError; pub use actix_http::ws::ProtocolError as WsProtocolError; use actix_http::http::{header::HeaderValue, Error as HttpError, StatusCode}; diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 9a8daeb43..92f749d03 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -35,12 +35,11 @@ pub mod error; mod request; mod response; pub mod test; -mod ws; +pub mod ws; pub use self::builder::ClientBuilder; pub use self::request::ClientRequest; pub use self::response::ClientResponse; -pub use self::ws::WebsocketsRequest; use self::connect::{Connect, ConnectorWrapper}; @@ -126,6 +125,7 @@ impl Client { req } + /// Construct HTTP *GET* request. pub fn get(&self, url: U) -> ClientRequest where Uri: HttpTryFrom, @@ -133,6 +133,7 @@ impl Client { self.request(Method::GET, url) } + /// Construct HTTP *HEAD* request. pub fn head(&self, url: U) -> ClientRequest where Uri: HttpTryFrom, @@ -140,6 +141,7 @@ impl Client { self.request(Method::HEAD, url) } + /// Construct HTTP *PUT* request. pub fn put(&self, url: U) -> ClientRequest where Uri: HttpTryFrom, @@ -147,6 +149,7 @@ impl Client { self.request(Method::PUT, url) } + /// Construct HTTP *POST* request. pub fn post(&self, url: U) -> ClientRequest where Uri: HttpTryFrom, @@ -154,6 +157,7 @@ impl Client { self.request(Method::POST, url) } + /// Construct HTTP *PATCH* request. pub fn patch(&self, url: U) -> ClientRequest where Uri: HttpTryFrom, @@ -161,6 +165,7 @@ impl Client { self.request(Method::PATCH, url) } + /// Construct HTTP *DELETE* request. pub fn delete(&self, url: U) -> ClientRequest where Uri: HttpTryFrom, @@ -168,6 +173,7 @@ impl Client { self.request(Method::DELETE, url) } + /// Construct HTTP *OPTIONS* request. pub fn options(&self, url: U) -> ClientRequest where Uri: HttpTryFrom, @@ -175,11 +181,12 @@ impl Client { self.request(Method::OPTIONS, url) } - pub fn ws(&self, url: U) -> WebsocketsRequest + /// Construct WebSockets request. + pub fn ws(&self, url: U) -> ws::WebsocketsRequest where Uri: HttpTryFrom, { - let mut req = WebsocketsRequest::new(url, self.0.clone()); + let mut req = ws::WebsocketsRequest::new(url, self.0.clone()); for (key, value) in &self.0.headers { req.head.headers.insert(key.clone(), value.clone()); } diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 26594531d..9697210de 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -11,6 +11,8 @@ use cookie::{Cookie, CookieJar}; use futures::future::{err, Either, Future}; use tokio_timer::Timeout; +pub use actix_http::ws::{CloseCode, CloseReason, Frame, Message}; + use crate::connect::BoxedSocket; use crate::error::{InvalidUrl, SendRequestError, WsClientError}; use crate::http::header::{ @@ -208,7 +210,7 @@ impl WebsocketsRequest { self.header(AUTHORIZATION, format!("Bearer {}", token)) } - /// Complete request construction and connect. + /// Complete request construction and connect to a websockets server. pub fn connect( mut self, ) -> impl Future< From 744d82431da34da01ddabe063579581c5a96a48d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 29 Mar 2019 14:07:37 -0700 Subject: [PATCH 383/427] add per request timeout --- awc/CHANGES.md | 9 +++++---- awc/src/builder.rs | 2 +- awc/src/request.rs | 18 +++++++++++++++--- awc/tests/test_client.rs | 26 ++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index dcc38c317..cd97ed86a 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,17 +1,18 @@ # Changes -## [0.1.0-alpha.2] - 2019-03-xx +## [0.1.0-alpha.2] - 2019-03-29 ### Added -* Request timeout. - -* Re-export `actix_http::client::Connector`. +* Per request and session wide request timeout. * Session wide headers. * Session wide basic and bearer auth. +* Re-export `actix_http::client::Connector`. + + ### Changed * Export `ws` sub-module with websockets related types diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 6ef145bf8..dcea55952 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -87,7 +87,7 @@ impl ClientBuilder { self } - /// Add default header. Headers adds byt this method + /// Add default header. Headers added by this method /// get added to every request. pub fn header(mut self, key: K, value: V) -> Self where diff --git a/awc/src/request.rs b/awc/src/request.rs index 170be75f7..a29c3e607 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -1,6 +1,7 @@ use std::fmt; use std::io::Write; use std::rc::Rc; +use std::time::Duration; use bytes::{BufMut, Bytes, BytesMut}; #[cfg(feature = "cookies")] @@ -62,6 +63,7 @@ pub struct ClientRequest { cookies: Option, default_headers: bool, response_decompress: bool, + timeout: Option, config: Rc, } @@ -86,6 +88,7 @@ impl ClientRequest { config, #[cfg(feature = "cookies")] cookies: None, + timeout: None, default_headers: true, response_decompress: true, } @@ -309,6 +312,15 @@ impl ClientRequest { self } + /// Set request timeout. Overrides client wide timeout setting. + /// + /// Request timeout is the total time before a response must be received. + /// Default value is 5 seconds. + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } + /// This method calls provided closure with builder reference if /// value is `true`. pub fn if_true(mut self, value: bool, f: F) -> Self @@ -443,10 +455,10 @@ impl ClientRequest { } } + let config = slf.config; let response_decompress = slf.response_decompress; - let fut = slf - .config + let fut = config .connector .borrow_mut() .send_request(head, body.into()) @@ -461,7 +473,7 @@ impl ClientRequest { }); // set request timeout - if let Some(timeout) = slf.config.timeout { + if let Some(timeout) = slf.timeout.or_else(|| config.timeout.clone()) { Either::B(Either::A(Timeout::new(fut, timeout).map_err(|e| { if let Some(e) = e.into_inner() { e diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index b2d6f8e90..51791d67a 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -83,6 +83,32 @@ fn test_timeout() { } } +#[test] +fn test_timeout_override() { + let mut srv = TestServer::new(|| { + HttpService::new(App::new().service(web::resource("/").route(web::to_async( + || { + tokio_timer::sleep(Duration::from_millis(200)) + .then(|_| Ok::<_, Error>(HttpResponse::Ok().body(STR))) + }, + )))) + }); + + let client = srv.execute(|| { + awc::Client::build() + .timeout(Duration::from_millis(50000)) + .finish() + }); + let request = client + .get(srv.url("/")) + .timeout(Duration::from_millis(50)) + .send(); + match srv.block_on(request) { + Err(SendRequestError::Timeout) => (), + _ => panic!(), + } +} + // #[test] // fn test_connection_close() { // let mut srv = From aebeb511cd3a1c0e1ae7217214c13ce42ec983f5 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 29 Mar 2019 14:26:11 -0700 Subject: [PATCH 384/427] explicit impl traits for ws connect --- awc/src/ws.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 9697210de..ae281737b 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -3,7 +3,7 @@ use std::io::Write; use std::rc::Rc; use std::{fmt, str}; -use actix_codec::Framed; +use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_http::{ws, Payload, RequestHead}; use bytes::{BufMut, BytesMut}; #[cfg(feature = "cookies")] @@ -13,7 +13,6 @@ use tokio_timer::Timeout; pub use actix_http::ws::{CloseCode, CloseReason, Frame, Message}; -use crate::connect::BoxedSocket; use crate::error::{InvalidUrl, SendRequestError, WsClientError}; use crate::http::header::{ self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION, @@ -214,7 +213,10 @@ impl WebsocketsRequest { pub fn connect( mut self, ) -> impl Future< - Item = (ClientResponse, Framed), + Item = ( + ClientResponse, + Framed, + ), Error = WsClientError, > { if let Some(e) = self.err.take() { From 5eb3f1154ec085ad1a94c809efc8955cdbe3b173 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 29 Mar 2019 14:27:22 -0700 Subject: [PATCH 385/427] revert --- awc/src/ws.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/awc/src/ws.rs b/awc/src/ws.rs index ae281737b..9697210de 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -3,7 +3,7 @@ use std::io::Write; use std::rc::Rc; use std::{fmt, str}; -use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_codec::Framed; use actix_http::{ws, Payload, RequestHead}; use bytes::{BufMut, BytesMut}; #[cfg(feature = "cookies")] @@ -13,6 +13,7 @@ use tokio_timer::Timeout; pub use actix_http::ws::{CloseCode, CloseReason, Frame, Message}; +use crate::connect::BoxedSocket; use crate::error::{InvalidUrl, SendRequestError, WsClientError}; use crate::http::header::{ self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION, @@ -213,10 +214,7 @@ impl WebsocketsRequest { pub fn connect( mut self, ) -> impl Future< - Item = ( - ClientResponse, - Framed, - ), + Item = (ClientResponse, Framed), Error = WsClientError, > { if let Some(e) = self.err.take() { From e9bbde6832722cff9bac0069cd33bb7e801f2005 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 29 Mar 2019 16:27:18 -0700 Subject: [PATCH 386/427] allow to override request's uri --- awc/CHANGES.md | 2 ++ awc/src/request.rs | 28 +++++++++++++++++----------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index cd97ed86a..e0e832144 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -15,6 +15,8 @@ ### Changed +* Allow to override request's uri + * Export `ws` sub-module with websockets related types diff --git a/awc/src/request.rs b/awc/src/request.rs index a29c3e607..bdde6faf5 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -73,25 +73,31 @@ impl ClientRequest { where Uri: HttpTryFrom, { - let mut err = None; - let mut head = RequestHead::default(); - head.method = method; - - match Uri::try_from(uri) { - Ok(uri) => head.uri = uri, - Err(e) => err = Some(e.into()), - } - ClientRequest { - head, - err, config, + head: RequestHead::default(), + err: None, #[cfg(feature = "cookies")] cookies: None, timeout: None, default_headers: true, response_decompress: true, } + .method(method) + .uri(uri) + } + + /// Set HTTP URI of request. + #[inline] + pub fn uri(mut self, uri: U) -> Self + where + Uri: HttpTryFrom, + { + match Uri::try_from(uri) { + Ok(uri) => self.head.uri = uri, + Err(e) => self.err = Some(e.into()), + } + self } /// Set HTTP method of this request. From c126713f4058995d198761d32a83b12030f49d81 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 29 Mar 2019 16:28:19 -0700 Subject: [PATCH 387/427] add rustls support to HttpServer --- Cargo.toml | 9 ++- README.md | 2 +- actix-http/tests/cert.pem | 32 ++++---- actix-http/tests/identity.pfx | Bin 5549 -> 0 bytes actix-http/tests/key.pem | 55 +++++++------ src/server.rs | 101 +++++++++++++----------- tests/cert.pem | 47 +++++------- tests/key.pem | 74 ++++++------------ tests/test_server.rs | 140 ++++++++++++++++++++-------------- 9 files changed, 234 insertions(+), 226 deletions(-) delete mode 100644 actix-http/tests/identity.pfx diff --git a/Cargo.toml b/Cargo.toml index be5b6c902..06c88c57e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ members = [ ] [package.metadata.docs.rs] -features = ["ssl", "tls", "brotli", "flate2-zlib", "cookies", "client"] +features = ["ssl", "tls", "brotli", "flate2-zlib", "cookies", "client", "rust-tls"] [features] default = ["brotli", "flate2-zlib", "cookies", "client"] @@ -62,7 +62,7 @@ tls = ["native-tls", "actix-server/ssl"] ssl = ["openssl", "actix-server/ssl", "awc/ssl"] # rustls -# rust-tls = ["rustls", "actix-server/rustls"] +rust-tls = ["rustls", "actix-server/rust-tls"] [dependencies] actix-codec = "0.1.1" @@ -100,7 +100,7 @@ cookie = { version="0.11", features=["secure", "percent-encode"], optional = tru # ssl support native-tls = { version="0.2", optional = true } openssl = { version="0.10", optional = true } -# rustls = { version = "^0.15", optional = true } +rustls = { version = "^0.15", optional = true } [dev-dependencies] actix-http = { path = "actix-http", features=["ssl", "brotli", "flate2-zlib"] } @@ -112,6 +112,9 @@ tokio-timer = "0.2.8" brotli2 = "0.3.2" flate2 = "1.0.2" +[replace] +"cookie:0.11.0" = { git = 'https://github.com/alexcrichton/cookie-rs.git' } + [profile.release] lto = true opt-level = 3 diff --git a/README.md b/README.md index 3cd0f3659..9829ab864 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust. * Configurable [request routing](https://actix.rs/docs/url-dispatch/) * Multipart streams * Static assets -* SSL support with OpenSSL or native-tls +* SSL support with OpenSSL or Rustls * Middlewares ([Logger, Session, CORS, CSRF, etc](https://actix.rs/docs/middleware/)) * Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) * Supports [Actix actor framework](https://github.com/actix/actix) diff --git a/actix-http/tests/cert.pem b/actix-http/tests/cert.pem index 5e195d98d..eafad5245 100644 --- a/actix-http/tests/cert.pem +++ b/actix-http/tests/cert.pem @@ -1,16 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICljCCAX4CCQDFdWu66640QjANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJ1 -czAeFw0xOTAyMDQyMzEyNTBaFw0yMDAyMDQyMzEyNTBaMA0xCzAJBgNVBAYTAnVz -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzZUXMnS5X8HWxTvHAc82 -Q2d32fiPQGtD+fp3OV90l6RC9jgMdH4yTVUgX5mYYcW0k89RaP8g61H6b76F9gcd -yZ1idqKI1AU9aeBUPV8wkrouhR/6Omv8fA7yr9tVmNo53jPN7WyKoBoU0r7Yj9Ez -g3qjv/808Jlgby3EhduruyyfdvSt5ZFXnOz2D3SF9DS4yrM2jSw4ZTuoVMfZ8vZe -FVzLo/+sV8qokU6wBTEOAmZQ7e/zZV4qAoH2Z3Vj/uD1Zr/MXYyh81RdXpDqIXwV -Z29LEOa2eTGFEdvfG+tdvvuIvSdF3+WbLrwn2ECfwJ8zmKyTauPRV4pj7ks+wkBI -EQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB6dmuWBOpFfDdu0mdsDb8XnJY1svjH -4kbztXhjQJ/WuhCUIwvXFyz9dqQCq+TbJUbUEzZJEfaq1uaI3iB5wd35ArSoAGJA -k0lonzyeSM+cmNOe/5BPqWhd1qPwbsfgMoCCkZUoTT5Rvw6yt00XIqZzMqrsvRBX -hAcUW3zBtFQNP6aQqsMdn4ClZE0WHf+LzWy2NQh+Sf46tSYBHELfdUawgR789PB4 -/gNjAeklq06JmE/3gELijwaijVIuUsMC9ua//ITk4YIFpqanPtka+7BpfTegPGNs -HCj1g7Jot97oQMuvDOJeso91aiSA+gutepCClZICT8LxNRkY3ZlXYp92 +MIIDPjCCAiYCCQCmkoCBehOyYTANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAlNGMRAwDgYDVQQKDAdDb21wYW55MQww +CgYDVQQLDANPcmcxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xOTAzMjky +MzE5MDlaFw0yMDAzMjgyMzE5MDlaMGExCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJD +QTELMAkGA1UEBwwCU0YxEDAOBgNVBAoMB0NvbXBhbnkxDDAKBgNVBAsMA09yZzEY +MBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA2uFoWm74qumqIIsBBf/rgP3ZtZw6dRQhVoYjIwYk00T1RLmmbt8r +YNh3lehmnrQlM/YC3dzcspucGqIfvs5FEReh/vgvsqY3lfy47Q1zzdtBrKq2ZBro +AuJUe4ayMYz/L/2jAtPtGDQqWyzhKv6x/oz6N/tKqlzoGbjSGSJUqKAV+Tuo4YI4 +xw3r/RJg3I3+ruXOgM65GBdja7usI/BhseEOp9VXotoTEItGmvG2RFZ4A7cN124x +giFl2IeYuC60jteZ+bnhPiqxcdzf3K4dnZlzrYma+FxwWbaow4wlpQcZVFdZ+K/Y +p/Bbm/FDKoUHnEdn/QAanTruRxSGdai0owIDAQABMA0GCSqGSIb3DQEBCwUAA4IB +AQAEWn3WAwAbd64f5jo2w4076s2qFiCJjPWoxO6bO75FgFFtw/NNev8pxGVw1ehg +HiTO6VRYolL5S/RKOchjA83AcDEBjgf8fKtvTmE9kxZSUIo4kIvv8V9ZM72gJhDN +8D/lXduTZ9JMwLOa1NUB8/I6CbaU3VzWkfodArKKpQF3M+LLgK03i12PD0KPQ5zv +bwaNoQo6cTmPNIdsVZETRvPqONiCUaQV57G74dGtjeirCh/DO5EYRtb1thgS7TGm ++Xg8OC5vZ6g0+xsrSqDBmWNtlI7S3bsL5C3LIEOOAL1ZJHRy2KvIGQ9ipb3XjnKS +N7/wlQduRyPH7oaD/o4xf5Gt -----END CERTIFICATE----- diff --git a/actix-http/tests/identity.pfx b/actix-http/tests/identity.pfx deleted file mode 100644 index 946e3b8b8ae10e19a11e7ac6eead66b12fff0014..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5549 zcmY+GRZtv&vTbn*Fa-DD5S(Fv;O=h0gF_fx!{8nycyM=jcXvr}cXtMNxH(n#z4P8j zS68jQyT2EE0A2|kEIfMvo;?yO<4>8N_ZYCqu-O54MhF3T`v0&tdjMMWe%lL7KUFAFV5{u;owkU`~uKqm}O*fBSo5RVYIG` zPAKF6ePO1*(FhW)-QpFAvtO?cVWGD0hs0WO4>qy>9T48E19Q`LuD0r#V<$Vj_c2yp z)4}gHs(AF<^Bo{R_`YId+=%k!c;why`_I0GXxv)FEmD)RG7Vs?g`z@xC^k*0+`Ps8 zZQH$QkvVVeOE(P8=g*0kvj|2!A! zCaUuJ^XO0NPlE$=oQRK9ti`Ic7JJnq;osnZ4OA`G7m{`HKP{cH&`YLS z&7cXaq1TKaOG&uTJqqY25!-%6M82yrFSuJv{`JhJUOE4ZMT1J%h8u3pSuQ$qle0}C z2}0Lk)3OpsFm1yeJ1|XW9gt0oX1PIiJ+HG_ccQbIH}C6hb|aPpRO(@Rt|u~DJIX%l zC+^x;>&_>s!^v1hwDp-biu2rspT|oc!3;MUJ>ui2i(Lamzq&B9OCcN`j&N`^wTcd1 z`DFH$T83h&1Ws&{N(khR##{-N3RSG@_ZPyM3SQ_TAXRRqyV#LUkE0&kEvE8Y61{fKJ+Y= z$eQ*1oOaeF^Z9A#-r@E7LmI)arBxsyT>V+(Ruq)&tBqzOi1sjBwNjPe_jraq(=2r^ zB|%@2bxrAnZr&d!&MNXZn&!iHPAs)aINufHy*7>&8Ap}7vY+Ji590{9Gq+KBpjkN0 zeCWDYHbCrNc%062ljm(+!6eM>ku}@T8$$TYjIT%~Y3z>YH2FxhEJ6lUL_#W3!=?ks zFo969*bgk}a!GRqXMSjjgd&?6Y%hBVuhL~74jyLlXNnJzvi?lWEo_HD7ZkXXB+vYf z**da2rqVV>3tDz^j`grt{G5Ye8`Q@KgQelPxGF%zLkZ=m7N>eor9pQmK6B$F&s9TaFeP0w?b5Q2p`A;be~m;x!mwj;I4ye%g!Op2Z^tb4S}i z#o}zOP0;F#mw3YEF_5DmxuBJQ{MZN!`^i6PwkgfA(n$bs9PQx6aZB5p$2&d$+`VjO zsH)tO=SExUR825&T#?}~JqRDb1{y6YsTMn zYINH#Ru0n1kxJc|=rVjd@`uh*nzIv3n@lt8^$P6GC5xIW1?7XLu5&GtX$Ho%rp5@a z<;*{8D{Rd@ocNX}STxWU(2moCw*4dI^{&HccPIwaXF^%V^P@gTFF*ow(`z_5$lNR7B+?<1E> zULlb}pDx~mAAb5{pg@(IH|PW09Pz6r7!}<9>(fAn8=3nRL|EV^f5o-!1D@`uY_E0= zXWciaPcKF60&?K+MAnU0gz}SuZb>wVAeRY>Lz9(&Zu_rB_JiKSV8HOqO3;vSD41G~ zIYpO4JcJ5K0+;O6x0VlNvNY%;v*XCt2V>-|%pxixP0jfH+xE^8^D3ZXs zS4AQ;$|oz)t4F{7(j7F7l{R}lJhwq)ekCh$|7`g4CXbIghAm6OSZ1L&K5-Y1w$mQa z_MBdP-vu45dbf@y=ls^2$D{4YS(f+0N}W49&$hLSRWl_WZB;uN`i7GQSU2{> z%tc5FX16D4Hc~M^2Q-}r|Th>@2!HT<`AbzLpW6XWl;-{}i)bscjNo9~Z zt>K*BFR$SlC#sBVi?$v)Y?HUyl)E2s`8W6ZXD9n+OhlM3@csT_+o|7G5+{-kChNBE zGoihxT`ld2`0D_n8Kx;B37>D}D7${tb4|NGV4*rXJY< zrP!d-L?ATkL~#^D#VB&5mZjsJsQ(E>4kqY74x%)7DdB0tEho2OB+t;4_6jhwgA|cO znS?z?-;5Y{h$UXY8kALQOM~}sKX{KB4C#6j4aK&Qk9w{6xr6GoWSrQBZ-V^FUe|IuEu|!Ho zP~_-_Q3K`aH^E3h7qd#rjmRUmA*ZPMaLSD!4zIf>se1cE%=z*bSCvx#B3|QUR$0#m9GAZ3L&y-$F1DIC& zT8gc<4SckFy#eY%&9U^Kz5>cD`L{z)t6x+lxMS@K!T}DzX*QtVExm3-I%(wjkMP#> z1(Cx)JgRslsAW|p`13(`X}#T<>eipuNW4PA4R2-@N+Vc&(y87pk_BSeS+d6)P$^TP z`?$x%WPFPWh9eb*vW#gb#gty{p~gAkXo)>T5Uf}c#r*~bw?jGhA9M7i8I5Xoby}f^ z^p4Qzi_ghn7*)HR6CmO5t+f(DYnCA3GX_!BFzYWFViu9Jn~Rh{W4lS>Ien~yruGbu z>(;O4jayyFSMEP1yQ9A{eQ^Hh(X@0+auu!ON=}-efn0d?U5LMh$zI#vo9nqvQ$058 z0%`dSn#OpIe|FctVo1fv^b^C(=MqC(usOYfc9zoBNAJEY_F!S;aKK8MEaXZ2?J0*q z%9PBkwW53|md4e|UjFDs!BwW$KTztoZy9^)QA!(U)itkV!h$GWP*eYJRt+x@x5V%i z1J7`#SEWSE_@>C3i32lz2g3U@e}8X+mAz+L`pS5%*YB7Dxr&GE4t*>KB6I zE-@2=x9^2U&Ux`Z5<+-#U+{a@#CzMU?Leqv9AhmP6iaWp>Oq!NsxHbS=EW1!zJLz{ zMUGYEB7n6q3Er#o<|Z`u0MwrUN4&EGP-_taP%Ho8(tlHkg!X?l`~xi9ztHXDeK*m&V~nDC|pLVujbH(b=fk1EfnQhXPjX1C48B*(7^BO>cFO~ zAH*SPpf#PD###N!E9oD!h87(U5h5>lHJLd?LymUDW$S z^HqD#EbaAcgn)aq3MVH(f*KmTMjHO5dW|_+katLspc=S!v^H3wli*n6ojf^qf3K=y zKr%Yay%^*Xvh0;}J@-TW@bcRzax<}Q{(@FjI+hfy@)|L*hscS(TttTvz_%Nv+(z8GGr?Ic>si}%oD zjS`B@EE+;f!(VuH>B7DBvpXijW%<{YjhQlo9fhwvuO{nH%@)BNiq<1`YY&2FCcm1v z)V&H!Kcj^3Bt0y%<I%;IH=P zJ41oky-lS{I9?XOY~Id^5UX<7RGwt!+GLyLENxHkj-snrrD9wg(faIi6I{jBh1go{ zM*ViN9ui7wip-=5;2)#r3!%>8z?BacE<1_@ALhkFNVIX%-ABNFWSr=jiLQ6%-{KRS zWwq<$wd1t!5~>t;g)EsTUt{vq;Fq-VE-~ml6ZXxgTNfUz_@@T7 zjsLv05k4~6I(7sOoX;Me9Mx*!2n`*&G62*4lL#eRXMj)}# zKr96@p5g@T(q@%oV#NRfPt?05WUo1wQk&aO{Pmwj7rdpbgcv6`Z78Sujv^Cw!}t9r`^Lc8t$WvLza7iBfkmc zDDY9Tit~6VzqS0L=Ayqpm%buj8Ag(=8<}4__o+jk*7sRDS~t>ecLxvu9W`08b;!5* zVykt!s+BvMXq)=q=y6Z4d=}r*-a;}j-*Qyy?4M4cOLy-!>N4}f4H%^$+ju=$d;iJ7 zPw8!Js)`{-TTglxv%Sm^K45gm#!5u2ni5W%dv9_+kEJG*<)*>LnTIO3 zP6HOD&&FrUjtF+byLCKHD-ACb%_vi{l0xPj|E3=MNnc)wn90{jb#I)vL$jsxtHlPu zCN(WOd)y*If;(Pc2D_T*H8TGC6#FjyA&L=1eB$U3^IK?5o$tR7-J`4e7hlpb7w~C+Agyessj{qzudSzQHf)p zE3bG;_o5JE@|R;^XOzYSMRqL`ey& z7cxd^&=J@~sxSdNJ~4`WKG3Pf5(-q%1&@Bp@h^T2p}7IVJL}{Ir#}ZYrY(`qOdx+V z!4-keXG9fPwZ72Z>0InsQ!U51)YwT!#5(o7Q73y)O7Vj^xant+Mw#45BH$9?B1vV- zK-S5Bmx_T9S^3JTc)fx@Q&R72RSU{u74roB{&3hlzknbj{dy%m?DxD5GH*40Tk$q; zDxcEj{-~XXePdPnzs!=S2lkV$IweaW8lBM@z7iZlAu5N%pYus=+D*DQyFs=GYfXl#{yYa^Ur z^wdzws}mB6Y~FeN1FIrbx)Pe=LH~}x^wgbl7Mnn)HlS|~MKWejJ*=#vF+_%p7!)FC z*>GliQit=X=~)<^UO-kl>$hKAhy@PbN%T6E8AFDRIPqP?3nKbu9SC1*x-_q%oa>un z)jigLjSg;Ie=Sf4&{X0(O6asc*}f}KP)&dmbcEmjIu<#h|51e3-$TXpM$ACpy4wx^ z7~&b%^6mSNlP@DXnZzWuf4O5+N;qfkl$8#UJw?Z-lL=nl=k*Qj5K!X_jNe5*ceN|1 zJO(G;HL??jza#IvW7CL&O%M}9wD`Q<7M?twO`H?+^WCB~JG8Y5`NHaesh58pW(2{G0QEPc8si#*pBZg zbmb#%Lx#-6A2aE}_`5AxI8xhZ#FL@NXPYMwK!#-*abrgDL>W=V)NBx_S5!i=ATh~B zoT!rujGkse$2dK9!A$E8ILE})=TNqjus$Y5VZgwNlvHx9Q@B&7X378MKMHcNc9XmD z{1dA1N8T7+{i%3V@mO!?q$Lgg=zTVwG9vk)dN9s4cwe diff --git a/actix-http/tests/key.pem b/actix-http/tests/key.pem index 50ded0ce0..2afbf5497 100644 --- a/actix-http/tests/key.pem +++ b/actix-http/tests/key.pem @@ -1,28 +1,27 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDNlRcydLlfwdbF -O8cBzzZDZ3fZ+I9Aa0P5+nc5X3SXpEL2OAx0fjJNVSBfmZhhxbSTz1Fo/yDrUfpv -voX2Bx3JnWJ2oojUBT1p4FQ9XzCSui6FH/o6a/x8DvKv21WY2jneM83tbIqgGhTS -vtiP0TODeqO//zTwmWBvLcSF26u7LJ929K3lkVec7PYPdIX0NLjKszaNLDhlO6hU -x9ny9l4VXMuj/6xXyqiRTrAFMQ4CZlDt7/NlXioCgfZndWP+4PVmv8xdjKHzVF1e -kOohfBVnb0sQ5rZ5MYUR298b612++4i9J0Xf5ZsuvCfYQJ/AnzOYrJNq49FXimPu -Sz7CQEgRAgMBAAECggEBALC547EaKmko5wmyM4dYq9sRzTPxuqO0EkGIkIkfh8j8 -ChxDXmGeQnu8HBJSpW4XWP5fkCpkd9YTKOh6rgorX+37f7NgUaOBxaOIlqITfFwF -9Qu3y5IBVpEHAJUwRcsaffiILBRX5GtxQElSijRHsLLr8GySZN4X25B3laNEjcJe -NWJrDaxOn0m0MMGRvBpM8PaZu1Mn9NWxt04b/fteVLdN4TAcuY9TgvVZBq92S2FM -qvZcnJCQckNOuMOptVdP45qPkerKUohpOcqBfIiWFaalC378jE3Dm68p7slt3R6y -I1wVqCI4+MZfM3CtKcYJV0fdqklJCvXORvRiT8OZKakCgYEA5YnhgXOu4CO4DR1T -Lacv716DPyHeKVa6TbHhUhWw4bLwNLUsEL98jeU9SZ6VH8enBlDm5pCsp2i//t9n -8hoykN4L0rS4EyAGENouTRkLhtHfjTAKTKDK8cNvEaS8NOBJWrI0DTiHtFbCRBvI -zRx5VhrB5H4DDbqn7QV9g+GBKvMCgYEA5Ug3bN0RNUi3KDoIRcnWc06HsX307su7 -tB4cGqXJqVOJCrkk5sefGF502+W8m3Ldjaakr+Q9BoOdZX6boZnFtVetT8Hyzk1C -Rkiyz3GcwovOkQK//UmljsuRjgHF+PuQGX5ol4YlJtXU21k5bCsi1Tmyp7IufiGV -AQRMVZVbeesCgYA/QBZGwKTgmJcf7gO8ocRAto999wwr4f0maazIHLgICXHNZFsH -JmzhANk5jxxSjIaG5AYsZJNe8ittxQv0l6l1Z+pkHm5Wvs1NGYIGtq8JcI2kbyd3 -ZBtoMU1K1FUUUPWFq3NSbVBfrkSL1ggoFP+ObYMePmcDAntBgfDLRXl9ZwKBgQCt -/dh5l2UIn27Gawt+EkXX6L8WVTQ6xoZhj/vZyPe4tDip14gGTXQQ5RUfDj7LZCZ2 -6P/OrpAU0mnt7F8kCfI7xBY0EUU1gvGJLn/q5heElt2hs4mIJ4woSZjiP7xBTn2y -qveqDNVCnEBUWGg4Cp/7WTaXBaM8ejV9uQpIY/gwEwKBgQCCYnd9fD8L4nGyOLoD -eUzMV7G8TZfinlxCNMVXfpn4Z8OaYHOk5NiujHK55w4ghx06NQw038qhnX0ogbjU -caWOwCIbrYgx2fwYuOZbJFXdjWlpjIK3RFOcbNCNgCRLT6Lgz4uZYZ9RVftADvMi -zR1QsLWnIvARbTtOPfZqizT2gQ== ------END PRIVATE KEY----- +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA2uFoWm74qumqIIsBBf/rgP3ZtZw6dRQhVoYjIwYk00T1RLmm +bt8rYNh3lehmnrQlM/YC3dzcspucGqIfvs5FEReh/vgvsqY3lfy47Q1zzdtBrKq2 +ZBroAuJUe4ayMYz/L/2jAtPtGDQqWyzhKv6x/oz6N/tKqlzoGbjSGSJUqKAV+Tuo +4YI4xw3r/RJg3I3+ruXOgM65GBdja7usI/BhseEOp9VXotoTEItGmvG2RFZ4A7cN +124xgiFl2IeYuC60jteZ+bnhPiqxcdzf3K4dnZlzrYma+FxwWbaow4wlpQcZVFdZ ++K/Yp/Bbm/FDKoUHnEdn/QAanTruRxSGdai0owIDAQABAoIBAQC4lzyQd+ITEbi+ +dTxJuQj94hgHB1htgKqU888SLI5F9nP6n67y9hb5N9WygSp6UWbGqYTFYwxlPMKr +22p2WjL5NTsTcm+XdIKQZW/3y06Mn4qFefsT9XURaZriCjihfU2BRaCCNARSUzwd +ZH4I6n9mM7KaH71aa7v6ZVoahE9tXPR6hM+SHQEySW4pWkEu98VpNNeIt6vP7WF9 +ONGbRa+0En4xgkuaxem2ZYa/GZFFtdQRkroNMhIRlfcPpkjy8DCc8E5RAkOzKC3O +lnxQwt+tdNNkGZz02ed2hx/YHPwFYy76y6hK5dxq74iKIaOc8U5t0HjB1zVfwiR0 +5mcxMncxAoGBAP+RivwXZ4FcxDY1uyziF+rwlC/1RujQFEWXIxsXCnba5DH3yKul +iKEIZPZtGhpsnQe367lcXcn7tztuoVjpAnk5L+hQY64tLwYbHeRcOMJ75C2y8FFC +NeG5sQsrk3IU1+jhGvrbE7UgOeAuWJmv0M1vPNB/+hGoZBW5W5uU1x89AoGBANtA +AhLtAcqQ/Qh2SpVhLljh7U85Be9tbCGua09clkYFzh3bcoBolXKH18Veww0TP0yF +0748CKw1A+ITbTVFV+vKvi4jzIxS7mr4wYtVCMssbttQN7y3l30IDxJwa9j3zTJx +IUn5OMMLv1JyitLId8HdOy1AdU3MkpJzdLyi1mFfAoGBAL3kL4fGABM/kU7SN6RO +zgS0AvdrYOeljBp1BRGg2hab58g02vamxVEZgqMTR7zwjPDqOIz+03U7wda4Ccyd +PUhDNJSB/r6xNepshZZi642eLlnCRguqjYyNw72QADtYv2B6uehAlXEUY8xtw0lW +OGgcSeyF2pH6M3tswWNlgT3lAoGAQ/BttBelOnP7NKgTLH7Usc4wjyAIaszpePZn +Ykw6dLBP0oixzoCZ7seRYSOgJWkVcEz39Db+KP60mVWTvbIjMHm+vOVy+Pip0JQM +xXQwKWU3ZNZSrzPkyWW55ejYQn9nIn5T5mxH3ojBXHcJ9Y8RLQ20zKzwrI77zE3i +mqGK9NkCgYEAq3dzHI0DGAJrR19sWl2LcqI19sj5a91tHx4cl1dJXS/iApOLLieU +zyUGkwfsqjHPAZ7GacICeBojIn/7KdPdlSKAbGVAU3d4qzvFS0qmWzObplBz3niT +Xnep2XLaVXqwlFJZZ6AHeKzYmMH0d0raiou2bpEUBqYizy2fi3NI4mA= +-----END RSA PRIVATE KEY----- diff --git a/src/server.rs b/src/server.rs index 9055be308..2817f549c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -11,11 +11,10 @@ use parking_lot::Mutex; use net2::TcpBuilder; -// #[cfg(feature = "tls")] -// use native_tls::TlsAcceptor; - #[cfg(feature = "ssl")] use openssl::ssl::{SslAcceptor, SslAcceptorBuilder}; +#[cfg(feature = "rust-tls")] +use rustls::ServerConfig as RustlsServerConfig; struct Socket { scheme: &'static str, @@ -254,19 +253,6 @@ where Ok(self) } - // #[cfg(feature = "tls")] - // /// Use listener for accepting incoming tls connection requests - // /// - // /// HttpServer does not change any configuration for TcpListener, - // /// it needs to be configured before passing it to listen() method. - // pub fn listen_nativetls(self, lst: net::TcpListener, acceptor: TlsAcceptor) -> Self { - // use actix_server::ssl; - - // self.listen_with(lst, move || { - // ssl::NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) - // }) - // } - #[cfg(feature = "ssl")] /// Use listener for accepting incoming tls connection requests /// @@ -294,7 +280,7 @@ where let addr = lst.local_addr().unwrap(); self.sockets.push(Socket { addr, - scheme: "http", + scheme: "https", }); self.builder = Some(self.builder.take().unwrap().listen( @@ -320,12 +306,52 @@ where /// Use listener for accepting incoming tls connection requests /// /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn listen_rustls(self, lst: net::TcpListener, config: ServerConfig) -> Self { - use super::{RustlsAcceptor, ServerFlags}; + pub fn listen_rustls( + mut self, + lst: net::TcpListener, + config: RustlsServerConfig, + ) -> io::Result { + self.listen_rustls_inner(lst, config)?; + Ok(self) + } - self.listen_with(lst, move || { - RustlsAcceptor::with_flags(config.clone(), flags).map_err(|_| ()) - }) + #[cfg(feature = "rust-tls")] + fn listen_rustls_inner( + &mut self, + lst: net::TcpListener, + mut config: RustlsServerConfig, + ) -> io::Result<()> { + use actix_server::ssl::{RustlsAcceptor, SslError}; + + let protos = vec!["h2".to_string().into(), "http/1.1".to_string().into()]; + config.set_protocols(&protos); + + let acceptor = RustlsAcceptor::new(config); + let factory = self.factory.clone(); + let cfg = self.config.clone(); + let addr = lst.local_addr().unwrap(); + self.sockets.push(Socket { + addr, + scheme: "https", + }); + + self.builder = Some(self.builder.take().unwrap().listen( + format!("actix-web-service-{}", addr), + lst, + move || { + let c = cfg.lock(); + acceptor.clone().map_err(|e| SslError::Ssl(e)).and_then( + HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .client_disconnect(c.client_shutdown) + .finish(factory()) + .map_err(|e| SslError::Service(e)) + .map_init_err(|_| ()), + ) + }, + )?); + Ok(()) } /// The socket address to bind @@ -372,22 +398,6 @@ where } } - // #[cfg(feature = "tls")] - // /// The ssl socket address to bind - // /// - // /// To bind multiple addresses this method can be called multiple times. - // pub fn bind_nativetls( - // self, - // addr: A, - // acceptor: TlsAcceptor, - // ) -> io::Result { - // use actix_server::ssl::NativeTlsAcceptor; - - // self.bind_with(addr, move || { - // NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) - // }) - // } - #[cfg(feature = "ssl")] /// Start listening for incoming tls connections. /// @@ -415,16 +425,15 @@ where /// /// This method sets alpn protocols to "h2" and "http/1.1" pub fn bind_rustls( - self, + mut self, addr: A, - builder: ServerConfig, + config: RustlsServerConfig, ) -> io::Result { - use super::{RustlsAcceptor, ServerFlags}; - use actix_service::NewServiceExt; - - self.bind_with(addr, move || { - RustlsAcceptor::with_flags(builder.clone(), flags).map_err(|_| ()) - }) + let sockets = self.bind2(addr)?; + for lst in sockets { + self.listen_rustls_inner(lst, config.clone())?; + } + Ok(self) } } diff --git a/tests/cert.pem b/tests/cert.pem index db04fbfae..eafad5245 100644 --- a/tests/cert.pem +++ b/tests/cert.pem @@ -1,31 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIFXTCCA0WgAwIBAgIJAJ3tqfd0MLLNMA0GCSqGSIb3DQEBCwUAMGExCzAJBgNV -BAYTAlVTMQswCQYDVQQIDAJDRjELMAkGA1UEBwwCU0YxEDAOBgNVBAoMB0NvbXBh -bnkxDDAKBgNVBAsMA09yZzEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tMB4XDTE4 -MDcyOTE4MDgzNFoXDTE5MDcyOTE4MDgzNFowYTELMAkGA1UEBhMCVVMxCzAJBgNV -BAgMAkNGMQswCQYDVQQHDAJTRjEQMA4GA1UECgwHQ29tcGFueTEMMAoGA1UECwwD -T3JnMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUA -A4ICDwAwggIKAoICAQDZbMgDYilVH1Nv0QWEhOXG6ETmtjZrdLqrNg3NBWBIWCDF -cQ+fyTWxARx6vkF8A/3zpJyTcfQW8HgG38jw/A61QKaHBxzwq0HlNwY9Hh+Neeuk -L4wgrlQ0uTC7IEMrOJjNN0GPyRQVfVbGa8QcSCpOg85l8GCxLvVwkBH/M5atoMtJ -EzniNfK+gtk3hOL2tBqBCu9NDjhXPnJwNDLtTG1tQaHUJW/r281Wvv9I46H83DkU -05lYtauh0bKh5znCH2KpFmBGqJNRzou3tXZFZzZfaCPBJPZR8j5TjoinehpDtkPh -4CSio0PF2eIFkDKRUbdz/327HgEARJMXx+w1yHpS2JwHFgy5O76i68/Smx8j3DDA -2WIkOYAJFRMH0CBHKdsvUDOGpCgN+xv3whl+N806nCfC4vCkwA+FuB3ko11logng -dvr+y0jIUSU4THF3dMDEXYayF3+WrUlw0cBnUNJdXky85ZP81aBfBsjNSBDx4iL4 -e4NhfZRS5oHpHy1t3nYfuttS/oet+Ke5KUpaqNJguSIoeTBSmgzDzL1TJxFLOzUT -2c/A9M69FdvSY0JB4EJX0W9K01Vd0JRNPwsY+/zvFIPama3suKOUTqYcsbwxx9xa -TMDr26cIQcgUAUOKZO43sQGWNzXX3FYVNwczKhkB8UX6hOrBJsEYiau4LGdokQID -AQABoxgwFjAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggIB -AIX+Qb4QRBxHl5X2UjRyLfWVkimtGlwI8P+eJZL3DrHBH/TpqAaCvTf0EbRC32nm -ASDMwIghaMvyrW40QN6V/CWRRi25cXUfsIZr1iHAHK0eZJV8SWooYtt4iNrcUs3g -4OTvDxhNmDyNwV9AXhJsBKf80dCW6/84jItqVAj20/OO4Rkd2tEeI8NomiYBc6a1 -hgwvv02myYF5hG/xZ9YSqeroBCZHwGYoJJnSpMPqJsxbCVnx2/U9FzGwcRmNHFCe -0g7EJZd3//8Plza6nkTBjJ/V7JnLqMU+ltx4mAgZO8rfzIr84qZdt0YN33VJQhYq -seuMySxrsuaAoxAmm8IoK9cW4IPzx1JveBQiroNlq5YJGf2UW7BTc3gz6c2tINZi -7ailBVdhlMnDXAf3/9xiiVlRAHOxgZh/7sRrKU7kDEHM4fGoc0YyZBTQKndPYMwO -3Bd82rlQ4sd46XYutTrB+mBYClVrJs+OzbNedTsR61DVNKKsRG4mNPyKSAIgOfM5 -XmSvCMPN5JK9U0DsNIV2/SnVsmcklQczT35FLTxl9ntx8ys7ZYK+SppD7XuLfWMq -GT9YMWhlpw0aRDg/aayeeOcnsNBhzAFMcOpQj1t6Fgv4+zbS9BM2bT0hbX86xjkr -E6wWgkuCslMgQlEJ+TM5RhYrI5/rVZQhvmgcob/9gPZv +MIIDPjCCAiYCCQCmkoCBehOyYTANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAlNGMRAwDgYDVQQKDAdDb21wYW55MQww +CgYDVQQLDANPcmcxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xOTAzMjky +MzE5MDlaFw0yMDAzMjgyMzE5MDlaMGExCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJD +QTELMAkGA1UEBwwCU0YxEDAOBgNVBAoMB0NvbXBhbnkxDDAKBgNVBAsMA09yZzEY +MBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA2uFoWm74qumqIIsBBf/rgP3ZtZw6dRQhVoYjIwYk00T1RLmmbt8r +YNh3lehmnrQlM/YC3dzcspucGqIfvs5FEReh/vgvsqY3lfy47Q1zzdtBrKq2ZBro +AuJUe4ayMYz/L/2jAtPtGDQqWyzhKv6x/oz6N/tKqlzoGbjSGSJUqKAV+Tuo4YI4 +xw3r/RJg3I3+ruXOgM65GBdja7usI/BhseEOp9VXotoTEItGmvG2RFZ4A7cN124x +giFl2IeYuC60jteZ+bnhPiqxcdzf3K4dnZlzrYma+FxwWbaow4wlpQcZVFdZ+K/Y +p/Bbm/FDKoUHnEdn/QAanTruRxSGdai0owIDAQABMA0GCSqGSIb3DQEBCwUAA4IB +AQAEWn3WAwAbd64f5jo2w4076s2qFiCJjPWoxO6bO75FgFFtw/NNev8pxGVw1ehg +HiTO6VRYolL5S/RKOchjA83AcDEBjgf8fKtvTmE9kxZSUIo4kIvv8V9ZM72gJhDN +8D/lXduTZ9JMwLOa1NUB8/I6CbaU3VzWkfodArKKpQF3M+LLgK03i12PD0KPQ5zv +bwaNoQo6cTmPNIdsVZETRvPqONiCUaQV57G74dGtjeirCh/DO5EYRtb1thgS7TGm ++Xg8OC5vZ6g0+xsrSqDBmWNtlI7S3bsL5C3LIEOOAL1ZJHRy2KvIGQ9ipb3XjnKS +N7/wlQduRyPH7oaD/o4xf5Gt -----END CERTIFICATE----- diff --git a/tests/key.pem b/tests/key.pem index aac387c64..2afbf5497 100644 --- a/tests/key.pem +++ b/tests/key.pem @@ -1,51 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIJKAIBAAKCAgEA2WzIA2IpVR9Tb9EFhITlxuhE5rY2a3S6qzYNzQVgSFggxXEP -n8k1sQEcer5BfAP986Sck3H0FvB4Bt/I8PwOtUCmhwcc8KtB5TcGPR4fjXnrpC+M -IK5UNLkwuyBDKziYzTdBj8kUFX1WxmvEHEgqToPOZfBgsS71cJAR/zOWraDLSRM5 -4jXyvoLZN4Ti9rQagQrvTQ44Vz5ycDQy7UxtbUGh1CVv69vNVr7/SOOh/Nw5FNOZ -WLWrodGyoec5wh9iqRZgRqiTUc6Lt7V2RWc2X2gjwST2UfI+U46Ip3oaQ7ZD4eAk -oqNDxdniBZAykVG3c/99ux4BAESTF8fsNch6UticBxYMuTu+ouvP0psfI9wwwNli -JDmACRUTB9AgRynbL1AzhqQoDfsb98IZfjfNOpwnwuLwpMAPhbgd5KNdZaIJ4Hb6 -/stIyFElOExxd3TAxF2Gshd/lq1JcNHAZ1DSXV5MvOWT/NWgXwbIzUgQ8eIi+HuD -YX2UUuaB6R8tbd52H7rbUv6HrfinuSlKWqjSYLkiKHkwUpoMw8y9UycRSzs1E9nP -wPTOvRXb0mNCQeBCV9FvStNVXdCUTT8LGPv87xSD2pmt7LijlE6mHLG8McfcWkzA -69unCEHIFAFDimTuN7EBljc119xWFTcHMyoZAfFF+oTqwSbBGImruCxnaJECAwEA -AQKCAgAME3aoeXNCPxMrSri7u4Xnnk71YXl0Tm9vwvjRQlMusXZggP8VKN/KjP0/ -9AE/GhmoxqPLrLCZ9ZE1EIjgmZ9Xgde9+C8rTtfCG2RFUL7/5J2p6NonlocmxoJm -YkxYwjP6ce86RTjQWL3RF3s09u0inz9/efJk5O7M6bOWMQ9VZXDlBiRY5BYvbqUR -6FeSzD4MnMbdyMRoVBeXE88gTvZk8xhB6DJnLzYgc0tKiRoeKT0iYv5JZw25VyRM -ycLzfTrFmXCPfB1ylb483d9Ly4fBlM8nkx37PzEnAuukIawDxsPOb9yZC+hfvNJI -7NFiMN+3maEqG2iC00w4Lep4skHY7eHUEUMl+Wjr+koAy2YGLWAwHZQTm7iXn9Ab -L6adL53zyCKelRuEQOzbeosJAqS+5fpMK0ekXyoFIuskj7bWuIoCX7K/kg6q5IW+ -vC2FrlsrbQ79GztWLVmHFO1I4J9M5r666YS0qdh8c+2yyRl4FmSiHfGxb3eOKpxQ -b6uI97iZlkxPF9LYUCSc7wq0V2gGz+6LnGvTHlHrOfVXqw/5pLAKhXqxvnroDTwz -0Ay/xFF6ei/NSxBY5t8ztGCBm45wCU3l8pW0X6dXqwUipw5b4MRy1VFRu6rqlmbL -OPSCuLxqyqsigiEYsBgS/icvXz9DWmCQMPd2XM9YhsHvUq+R4QKCAQEA98EuMMXI -6UKIt1kK2t/3OeJRyDd4iv/fCMUAnuPjLBvFE4cXD/SbqCxcQYqb+pue3PYkiTIC -71rN8OQAc5yKhzmmnCE5N26br/0pG4pwEjIr6mt8kZHmemOCNEzvhhT83nfKmV0g -9lNtuGEQMiwmZrpUOF51JOMC39bzcVjYX2Cmvb7cFbIq3lR0zwM+aZpQ4P8LHCIu -bgHmwbdlkLyIULJcQmHIbo6nPFB3ZZE4mqmjwY+rA6Fh9rgBa8OFCfTtrgeYXrNb -IgZQ5U8GoYRPNC2ot0vpTinraboa/cgm6oG4M7FW1POCJTl+/ktHEnKuO5oroSga -/BSg7hCNFVaOhwKCAQEA4Kkys0HtwEbV5mY/NnvUD5KwfXX7BxoXc9lZ6seVoLEc -KjgPYxqYRVrC7dB2YDwwp3qcRTi/uBAgFNm3iYlDzI4xS5SeaudUWjglj7BSgXE2 -iOEa7EwcvVPluLaTgiWjlzUKeUCNNHWSeQOt+paBOT+IgwRVemGVpAgkqQzNh/nP -tl3p9aNtgzEm1qVlPclY/XUCtf3bcOR+z1f1b4jBdn0leu5OhnxkC+Htik+2fTXD -jt6JGrMkanN25YzsjnD3Sn+v6SO26H99wnYx5oMSdmb8SlWRrKtfJHnihphjG/YY -l1cyorV6M/asSgXNQfGJm4OuJi0I4/FL2wLUHnU+JwKCAQEAzh4WipcRthYXXcoj -gMKRkMOb3GFh1OpYqJgVExtudNTJmZxq8GhFU51MR27Eo7LycMwKy2UjEfTOnplh -Us2qZiPtW7k8O8S2m6yXlYUQBeNdq9IuuYDTaYD94vsazscJNSAeGodjE+uGvb1q -1wLqE87yoE7dUInYa1cOA3+xy2/CaNuviBFJHtzOrSb6tqqenQEyQf6h9/12+DTW -t5pSIiixHrzxHiFqOoCLRKGToQB+71rSINwTf0nITNpGBWmSj5VcC3VV3TG5/XxI -fPlxV2yhD5WFDPVNGBGvwPDSh4jSMZdZMSNBZCy4XWFNSKjGEWoK4DFYed3DoSt9 -5IG1YwKCAQA63ntHl64KJUWlkwNbboU583FF3uWBjee5VqoGKHhf3CkKMxhtGqnt -+oN7t5VdUEhbinhqdx1dyPPvIsHCS3K1pkjqii4cyzNCVNYa2dQ00Qq+QWZBpwwc -3GAkz8rFXsGIPMDa1vxpU6mnBjzPniKMcsZ9tmQDppCEpBGfLpio2eAA5IkK8eEf -cIDB3CM0Vo94EvI76CJZabaE9IJ+0HIJb2+jz9BJ00yQBIqvJIYoNy9gP5Xjpi+T -qV/tdMkD5jwWjHD3AYHLWKUGkNwwkAYFeqT/gX6jpWBP+ZRPOp011X3KInJFSpKU -DT5GQ1Dux7EMTCwVGtXqjO8Ym5wjwwsfAoIBAEcxlhIW1G6BiNfnWbNPWBdh3v/K -5Ln98Rcrz8UIbWyl7qNPjYb13C1KmifVG1Rym9vWMO3KuG5atK3Mz2yLVRtmWAVc -fxzR57zz9MZFDun66xo+Z1wN3fVxQB4CYpOEI4Lb9ioX4v85hm3D6RpFukNtRQEc -Gfr4scTjJX4jFWDp0h6ffMb8mY+quvZoJ0TJqV9L9Yj6Ksdvqez/bdSraev97bHQ -4gbQxaTZ6WjaD4HjpPQefMdWp97Metg0ZQSS8b8EzmNFgyJ3XcjirzwliKTAQtn6 -I2sd0NCIooelrKRD8EJoDUwxoOctY7R97wpZ7/wEHU45cBCbRV3H4JILS5c= +MIIEpQIBAAKCAQEA2uFoWm74qumqIIsBBf/rgP3ZtZw6dRQhVoYjIwYk00T1RLmm +bt8rYNh3lehmnrQlM/YC3dzcspucGqIfvs5FEReh/vgvsqY3lfy47Q1zzdtBrKq2 +ZBroAuJUe4ayMYz/L/2jAtPtGDQqWyzhKv6x/oz6N/tKqlzoGbjSGSJUqKAV+Tuo +4YI4xw3r/RJg3I3+ruXOgM65GBdja7usI/BhseEOp9VXotoTEItGmvG2RFZ4A7cN +124xgiFl2IeYuC60jteZ+bnhPiqxcdzf3K4dnZlzrYma+FxwWbaow4wlpQcZVFdZ ++K/Yp/Bbm/FDKoUHnEdn/QAanTruRxSGdai0owIDAQABAoIBAQC4lzyQd+ITEbi+ +dTxJuQj94hgHB1htgKqU888SLI5F9nP6n67y9hb5N9WygSp6UWbGqYTFYwxlPMKr +22p2WjL5NTsTcm+XdIKQZW/3y06Mn4qFefsT9XURaZriCjihfU2BRaCCNARSUzwd +ZH4I6n9mM7KaH71aa7v6ZVoahE9tXPR6hM+SHQEySW4pWkEu98VpNNeIt6vP7WF9 +ONGbRa+0En4xgkuaxem2ZYa/GZFFtdQRkroNMhIRlfcPpkjy8DCc8E5RAkOzKC3O +lnxQwt+tdNNkGZz02ed2hx/YHPwFYy76y6hK5dxq74iKIaOc8U5t0HjB1zVfwiR0 +5mcxMncxAoGBAP+RivwXZ4FcxDY1uyziF+rwlC/1RujQFEWXIxsXCnba5DH3yKul +iKEIZPZtGhpsnQe367lcXcn7tztuoVjpAnk5L+hQY64tLwYbHeRcOMJ75C2y8FFC +NeG5sQsrk3IU1+jhGvrbE7UgOeAuWJmv0M1vPNB/+hGoZBW5W5uU1x89AoGBANtA +AhLtAcqQ/Qh2SpVhLljh7U85Be9tbCGua09clkYFzh3bcoBolXKH18Veww0TP0yF +0748CKw1A+ITbTVFV+vKvi4jzIxS7mr4wYtVCMssbttQN7y3l30IDxJwa9j3zTJx +IUn5OMMLv1JyitLId8HdOy1AdU3MkpJzdLyi1mFfAoGBAL3kL4fGABM/kU7SN6RO +zgS0AvdrYOeljBp1BRGg2hab58g02vamxVEZgqMTR7zwjPDqOIz+03U7wda4Ccyd +PUhDNJSB/r6xNepshZZi642eLlnCRguqjYyNw72QADtYv2B6uehAlXEUY8xtw0lW +OGgcSeyF2pH6M3tswWNlgT3lAoGAQ/BttBelOnP7NKgTLH7Usc4wjyAIaszpePZn +Ykw6dLBP0oixzoCZ7seRYSOgJWkVcEz39Db+KP60mVWTvbIjMHm+vOVy+Pip0JQM +xXQwKWU3ZNZSrzPkyWW55ejYQn9nIn5T5mxH3ojBXHcJ9Y8RLQ20zKzwrI77zE3i +mqGK9NkCgYEAq3dzHI0DGAJrR19sWl2LcqI19sj5a91tHx4cl1dJXS/iApOLLieU +zyUGkwfsqjHPAZ7GacICeBojIn/7KdPdlSKAbGVAU3d4qzvFS0qmWzObplBz3niT +Xnep2XLaVXqwlFJZZ6AHeKzYmMH0d0raiou2bpEUBqYizy2fi3NI4mA= -----END RSA PRIVATE KEY----- diff --git a/tests/test_server.rs b/tests/test_server.rs index cd5e95d20..717df2337 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1,4 +1,6 @@ use std::io::{Read, Write}; +use std::sync::mpsc; +use std::thread; use actix_http::http::header::{ ContentEncoding, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, @@ -14,10 +16,12 @@ use flate2::Compression; use futures::stream::once; use rand::{distributions::Alphanumeric, Rng}; -use actix_web::{dev::HttpMessageBody, web, App}; +use actix_web::{dev::HttpMessageBody, http, test, web, App, HttpResponse, HttpServer}; #[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] use actix_web::middleware::encoding; +#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] +use actix_web::middleware::encoding::BodyEncoding; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -678,69 +682,93 @@ fn test_brotli_encoding_large() { // assert_eq!(bytes, Bytes::from(data)); // } -// #[cfg(all(feature = "rust-tls", feature = "ssl"))] -// #[test] -// fn test_reading_deflate_encoding_large_random_ssl() { -// use actix::{Actor, System}; -// use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; -// use rustls::internal::pemfile::{certs, rsa_private_keys}; -// use rustls::{NoClientAuth, ServerConfig}; -// use std::fs::File; -// use std::io::BufReader; +#[cfg(all( + feature = "rust-tls", + feature = "ssl", + any(feature = "flate2-zlib", feature = "flate2-rust") +))] +#[test] +fn test_reading_deflate_encoding_large_random_ssl() { + use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; + use rustls::internal::pemfile::{certs, rsa_private_keys}; + use rustls::{NoClientAuth, ServerConfig}; + use std::fs::File; + use std::io::BufReader; -// // load ssl keys -// let mut config = ServerConfig::new(NoClientAuth::new()); -// let cert_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); -// let key_file = &mut BufReader::new(File::open("tests/key.pem").unwrap()); -// let cert_chain = certs(cert_file).unwrap(); -// let mut keys = rsa_private_keys(key_file).unwrap(); -// config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); + let addr = TestServer::unused_addr(); + let (tx, rx) = mpsc::channel(); -// let data = rand::thread_rng() -// .sample_iter(&Alphanumeric) -// .take(160_000) -// .collect::(); + let data = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(160_000) + .collect::(); -// let srv = test::TestServer::build().rustls(config).start(|app| { -// app.handler(|req: &HttpRequest| { -// req.body() -// .and_then(|bytes: Bytes| { -// Ok(HttpResponse::Ok() -// .content_encoding(http::ContentEncoding::Identity) -// .body(bytes)) -// }) -// .responder() -// }) -// }); + thread::spawn(move || { + let sys = actix_rt::System::new("test"); -// let mut rt = System::new("test"); + // load ssl keys + let mut config = ServerConfig::new(NoClientAuth::new()); + let cert_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); + let key_file = &mut BufReader::new(File::open("tests/key.pem").unwrap()); + let cert_chain = certs(cert_file).unwrap(); + let mut keys = rsa_private_keys(key_file).unwrap(); + config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); -// // client connector -// let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); -// builder.set_verify(SslVerifyMode::NONE); -// let conn = client::ClientConnector::with_connector(builder.build()).start(); + let srv = HttpServer::new(|| { + App::new().service(web::resource("/").route(web::to(|bytes: Bytes| { + Ok::<_, Error>( + HttpResponse::Ok() + .encoding(http::ContentEncoding::Identity) + .body(bytes), + ) + }))) + }) + .bind_rustls(addr, config) + .unwrap() + .start(); -// // encode data -// let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); -// e.write_all(data.as_ref()).unwrap(); -// let enc = e.finish().unwrap(); + let _ = tx.send((srv, actix_rt::System::current())); + let _ = sys.run(); + }); + let (srv, _sys) = rx.recv().unwrap(); + let client = test::run_on(|| { + let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); + builder.set_verify(SslVerifyMode::NONE); + let _ = builder.set_alpn_protos(b"\x02h2\x08http/1.1").unwrap(); -// // client request -// let request = client::ClientRequest::build() -// .uri(srv.url("/")) -// .method(http::Method::POST) -// .header(http::header::CONTENT_ENCODING, "deflate") -// .with_connector(conn) -// .body(enc) -// .unwrap(); -// let response = rt.block_on(request.send()).unwrap(); -// assert!(response.status().is_success()); + awc::Client::build() + .connector( + awc::Connector::new() + .timeout(std::time::Duration::from_millis(500)) + .ssl(builder.build()) + .service(), + ) + .finish() + }); -// // read response -// let bytes = rt.block_on(response.body()).unwrap(); -// assert_eq!(bytes.len(), data.len()); -// assert_eq!(bytes, Bytes::from(data)); -// } + // encode data + let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); + e.write_all(data.as_ref()).unwrap(); + let enc = e.finish().unwrap(); + + // client request + let _req = client + .post(format!("https://{}/", addr)) + .header(http::header::CONTENT_ENCODING, "deflate") + .send_body(enc); + + // TODO: fix + // let response = test::block_on(req).unwrap(); + // assert!(response.status().is_success()); + + // read response + // let bytes = test::block_on(response.body()).unwrap(); + // assert_eq!(bytes.len(), data.len()); + // assert_eq!(bytes, Bytes::from(data)); + + // stop + let _ = srv.stop(false); +} // #[cfg(all(feature = "tls", feature = "ssl"))] // #[test] From 00526f60dc6834314caaab341359758939f94fa5 Mon Sep 17 00:00:00 2001 From: Douman Date: Sat, 30 Mar 2019 02:29:11 +0300 Subject: [PATCH 388/427] Impl BodyEncoding for Response (#740) --- actix-http/src/response.rs | 12 ++++++++++++ src/middleware/compress.rs | 9 ++++++++- tests/test_server.rs | 24 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 29a850fae..4da0f642c 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -189,6 +189,18 @@ impl Response { self.head.keep_alive() } + /// Responses extensions + #[inline] + pub fn extensions(&self) -> Ref { + self.head.extensions.borrow() + } + + /// Mutable reference to a the response's extensions + #[inline] + pub fn extensions_mut(&mut self) -> RefMut { + self.head.extensions.borrow_mut() + } + /// Get body os this response #[inline] pub fn body(&self) -> &ResponseBody { diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 5c6bad874..d797e1250 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use actix_http::body::MessageBody; use actix_http::encoding::Encoder; use actix_http::http::header::{ContentEncoding, ACCEPT_ENCODING}; -use actix_http::ResponseBuilder; +use actix_http::{Response, ResponseBuilder}; use actix_service::{Service, Transform}; use futures::future::{ok, FutureResult}; use futures::{Async, Future, Poll}; @@ -27,6 +27,13 @@ impl BodyEncoding for ResponseBuilder { } } +impl BodyEncoding for Response { + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(Enc(encoding)); + self + } +} + #[derive(Debug, Clone)] /// `Middleware` for compressing response body. /// diff --git a/tests/test_server.rs b/tests/test_server.rs index 717df2337..11ba1b6a9 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -96,10 +96,20 @@ fn test_body_encoding_override() { .service(web::resource("/").route(web::to(|| { use actix_web::middleware::encoding::BodyEncoding; Response::Ok().encoding(ContentEncoding::Deflate).body(STR) + }))) + .service(web::resource("/raw").route(web::to(|| { + use actix_web::middleware::encoding::BodyEncoding; + let body = actix_web::dev::Body::Bytes(STR.into()); + let mut response = Response::with_body(actix_web::http::StatusCode::OK, body); + + response.encoding(ContentEncoding::Deflate); + + response }))), ) }); + // Builder let mut response = srv.block_on(srv.get().no_decompress().send()).unwrap(); assert!(response.status().is_success()); @@ -111,6 +121,20 @@ fn test_body_encoding_override() { e.write_all(bytes.as_ref()).unwrap(); let dec = e.finish().unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + // Raw Response + let mut response = srv.block_on(srv.request(actix_web::http::Method::GET, srv.url("/raw")).no_decompress().send()).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + + // decode + let mut e = ZlibDecoder::new(Vec::new()); + e.write_all(bytes.as_ref()).unwrap(); + let dec = e.finish().unwrap(); + assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + } #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] From 3220777ff989b922c7e124f63d296221316d9e87 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 29 Mar 2019 18:22:49 -0700 Subject: [PATCH 389/427] Added ws::Message::Nop, no-op websockets message --- actix-http/CHANGES.md | 6 +++++- actix-http/src/ws/codec.rs | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index e95963496..5659597c6 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,10 @@ # Changes -## [0.1.0-alpha.2] - 2019-xx-xx +## [0.1.0-alpha.2] - 2019-03-29 + +### Added + +* Added ws::Message::Nop, no-op websockets message ### Changed diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index 286d15f8c..ad599ffa7 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -18,6 +18,8 @@ pub enum Message { Pong(String), /// Close message with optional reason Close(Option), + /// No-op. Useful for actix-net services + Nop, } /// `WebSocket` frame @@ -87,6 +89,7 @@ impl Encoder for Codec { Parser::write_message(dst, txt, OpCode::Pong, true, !self.server) } Message::Close(reason) => Parser::write_close(dst, reason, !self.server), + Message::Nop => (), } Ok(()) } From 193f8fb2d9b11995b1e1dc35041e2f4d10095299 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 29 Mar 2019 18:51:07 -0700 Subject: [PATCH 390/427] update tests --- actix-http/Cargo.toml | 3 +- actix-http/tests/test_client.rs | 8 ++-- actix-http/tests/test_server.rs | 75 ++++++++++++++++----------------- awc/src/ws.rs | 8 ++-- test-server/Cargo.toml | 5 +-- test-server/src/lib.rs | 26 ++++++++---- tests/test_server.rs | 12 ++++-- 7 files changed, 74 insertions(+), 63 deletions(-) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 180cda787..0c910cb1e 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -96,8 +96,9 @@ openssl = { version="0.10", optional = true } [dev-dependencies] actix-rt = "0.2.2" -actix-server = { version = "0.4.0", features=["ssl"] } +actix-server = { version = "0.4.1", features=["ssl"] } actix-connect = { version = "0.1.0", features=["ssl"] } +#actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } actix-http-test = { path = "../test-server", features=["ssl"] } env_logger = "0.6" diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index 1ca7437d6..78b999703 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -52,18 +52,18 @@ fn test_h1_v2() { assert!(response.status().is_success()); let request = srv.get().header("x-test", "111").send(); - let mut response = srv.block_on(request).unwrap(); + let response = srv.block_on(request).unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); - let mut response = srv.block_on(srv.post().send()).unwrap(); + let response = srv.block_on(srv.post().send()).unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index 455edfec1..f1f82b089 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -13,8 +13,7 @@ use futures::stream::{once, Stream}; use actix_http::body::Body; use actix_http::error::PayloadError; use actix_http::{ - body, error, http, http::header, Error, HttpMessage, HttpService, KeepAlive, - Request, Response, + body, error, http, http::header, Error, HttpService, KeepAlive, Request, Response, }; fn load_body(stream: S) -> impl Future @@ -145,10 +144,10 @@ fn test_h2_body() -> std::io::Result<()> { ) }); - let mut response = srv.block_on(srv.sget().send_body(data.clone())).unwrap(); + let response = srv.block_on(srv.sget().send_body(data.clone())).unwrap(); assert!(response.status().is_success()); - let body = srv.block_on(load_body(response.take_payload())).unwrap(); + let body = srv.load_body(response).unwrap(); assert_eq!(&body, data.as_bytes()); Ok(()) } @@ -436,11 +435,11 @@ fn test_h1_headers() { }) }); - let mut response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert_eq!(bytes, Bytes::from(data2)); } @@ -480,11 +479,11 @@ fn test_h2_headers() { }).map_err(|_| ())) }); - let mut response = srv.block_on(srv.sget().send()).unwrap(); + let response = srv.block_on(srv.sget().send()).unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert_eq!(bytes, Bytes::from(data2)); } @@ -516,11 +515,11 @@ fn test_h1_body() { HttpService::build().h1(|_| future::ok::<_, ()>(Response::Ok().body(STR))) }); - let mut response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -538,11 +537,11 @@ fn test_h2_body2() { ) }); - let mut response = srv.block_on(srv.sget().send()).unwrap(); + let response = srv.block_on(srv.sget().send()).unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -552,7 +551,7 @@ fn test_h1_head_empty() { HttpService::build().h1(|_| ok::<_, ()>(Response::Ok().body(STR))) }); - let mut response = srv.block_on(srv.head().send()).unwrap(); + let response = srv.block_on(srv.head().send()).unwrap(); assert!(response.status().is_success()); { @@ -564,7 +563,7 @@ fn test_h1_head_empty() { } // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert!(bytes.is_empty()); } @@ -582,7 +581,7 @@ fn test_h2_head_empty() { ) }); - let mut response = srv.block_on(srv.shead().send()).unwrap(); + let response = srv.block_on(srv.shead().send()).unwrap(); assert!(response.status().is_success()); assert_eq!(response.version(), http::Version::HTTP_2); @@ -595,7 +594,7 @@ fn test_h2_head_empty() { } // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert!(bytes.is_empty()); } @@ -607,7 +606,7 @@ fn test_h1_head_binary() { }) }); - let mut response = srv.block_on(srv.head().send()).unwrap(); + let response = srv.block_on(srv.head().send()).unwrap(); assert!(response.status().is_success()); { @@ -619,7 +618,7 @@ fn test_h1_head_binary() { } // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert!(bytes.is_empty()); } @@ -641,7 +640,7 @@ fn test_h2_head_binary() { ) }); - let mut response = srv.block_on(srv.shead().send()).unwrap(); + let response = srv.block_on(srv.shead().send()).unwrap(); assert!(response.status().is_success()); { @@ -653,7 +652,7 @@ fn test_h2_head_binary() { } // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert!(bytes.is_empty()); } @@ -713,11 +712,11 @@ fn test_h1_body_length() { }) }); - let mut response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -740,11 +739,11 @@ fn test_h2_body_length() { ) }); - let mut response = srv.block_on(srv.sget().send()).unwrap(); + let response = srv.block_on(srv.sget().send()).unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -761,7 +760,7 @@ fn test_h1_body_chunked_explicit() { }) }); - let mut response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); assert_eq!( response @@ -774,7 +773,7 @@ fn test_h1_body_chunked_explicit() { ); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); // decode assert_eq!(bytes, Bytes::from_static(STR.as_ref())); @@ -802,12 +801,12 @@ fn test_h2_body_chunked_explicit() { ) }); - let mut response = srv.block_on(srv.sget().send()).unwrap(); + let response = srv.block_on(srv.sget().send()).unwrap(); assert!(response.status().is_success()); assert!(!response.headers().contains_key(header::TRANSFER_ENCODING)); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); // decode assert_eq!(bytes, Bytes::from_static(STR.as_ref())); @@ -822,7 +821,7 @@ fn test_h1_body_chunked_implicit() { }) }); - let mut response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get().send()).unwrap(); assert!(response.status().is_success()); assert_eq!( response @@ -835,7 +834,7 @@ fn test_h1_body_chunked_implicit() { ); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -854,11 +853,11 @@ fn test_h1_response_http_error_handling() { })) }); - let mut response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get().send()).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert!(bytes.is_empty()); } @@ -886,11 +885,11 @@ fn test_h2_response_http_error_handling() { ) }); - let mut response = srv.block_on(srv.sget().send()).unwrap(); + let response = srv.block_on(srv.sget().send()).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert!(bytes.is_empty()); } @@ -901,11 +900,11 @@ fn test_h1_service_error() { .h1(|_| Err::(error::ErrorBadRequest("error"))) }); - let mut response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get().send()).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert!(bytes.is_empty()); } @@ -924,10 +923,10 @@ fn test_h2_service_error() { ) }); - let mut response = srv.block_on(srv.sget().send()).unwrap(); + let response = srv.block_on(srv.sget().send()).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response - let bytes = srv.block_on(load_body(response.take_payload())).unwrap(); + let bytes = srv.load_body(response).unwrap(); assert!(bytes.is_empty()); } diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 9697210de..bc023c069 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -11,7 +11,7 @@ use cookie::{Cookie, CookieJar}; use futures::future::{err, Either, Future}; use tokio_timer::Timeout; -pub use actix_http::ws::{CloseCode, CloseReason, Frame, Message}; +pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message}; use crate::connect::BoxedSocket; use crate::error::{InvalidUrl, SendRequestError, WsClientError}; @@ -213,10 +213,8 @@ impl WebsocketsRequest { /// Complete request construction and connect to a websockets server. pub fn connect( mut self, - ) -> impl Future< - Item = (ClientResponse, Framed), - Error = WsClientError, - > { + ) -> impl Future), Error = WsClientError> + { if let Some(e) = self.err.take() { return Either::A(err(e.into())); } diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 582d96ed3..a9b58483c 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -30,12 +30,11 @@ default = ["session"] session = ["cookie/secure"] # openssl -ssl = ["openssl", "actix-http/ssl", "actix-server/ssl", "awc/ssl"] +ssl = ["openssl", "actix-server/ssl", "awc/ssl"] [dependencies] actix-codec = "0.1.1" actix-rt = "0.2.1" -actix-http = { path = "../actix-http" } actix-service = "0.3.4" actix-server = "0.4.0" actix-utils = "0.3.4" @@ -61,4 +60,4 @@ tokio-timer = "0.2" openssl = { version="0.10", optional = true } [dev-dependencies] -actix-web = { path = ".." } +actix-web = "1.0.0-alpha.1" diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 07a0e0b4c..9eec065cd 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -3,12 +3,12 @@ use std::sync::mpsc; use std::{net, thread, time}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_http::client::Connector; -use actix_http::ws; use actix_rt::{Runtime, System}; use actix_server::{Server, StreamServiceFactory}; -use awc::{Client, ClientRequest}; -use futures::future::{lazy, Future}; +use awc::{error::PayloadError, ws, Client, ClientRequest, ClientResponse, Connector}; +use bytes::Bytes; +use futures::future::lazy; +use futures::{Future, Stream}; use http::Method; use net2::TcpBuilder; @@ -193,13 +193,16 @@ impl TestServerRuntime { self.client.request(method, path.as_ref()) } - /// Stop http server - fn stop(&mut self) { - System::current().stop(); + pub fn load_body( + &mut self, + response: ClientResponse, + ) -> Result + where + S: Stream + 'static, + { + self.block_on(response.body().limit(10_485_760)) } -} -impl TestServerRuntime { /// Connect to websocket server at a given path pub fn ws_at( &mut self, @@ -219,6 +222,11 @@ impl TestServerRuntime { { self.ws_at("/") } + + /// Stop http server + fn stop(&mut self) { + System::current().stop(); + } } impl Drop for TestServerRuntime { diff --git a/tests/test_server.rs b/tests/test_server.rs index 11ba1b6a9..fc590ff0b 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -100,7 +100,8 @@ fn test_body_encoding_override() { .service(web::resource("/raw").route(web::to(|| { use actix_web::middleware::encoding::BodyEncoding; let body = actix_web::dev::Body::Bytes(STR.into()); - let mut response = Response::with_body(actix_web::http::StatusCode::OK, body); + let mut response = + Response::with_body(actix_web::http::StatusCode::OK, body); response.encoding(ContentEncoding::Deflate); @@ -123,7 +124,13 @@ fn test_body_encoding_override() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); // Raw Response - let mut response = srv.block_on(srv.request(actix_web::http::Method::GET, srv.url("/raw")).no_decompress().send()).unwrap(); + let mut response = srv + .block_on( + srv.request(actix_web::http::Method::GET, srv.url("/raw")) + .no_decompress() + .send(), + ) + .unwrap(); assert!(response.status().is_success()); // read response @@ -134,7 +141,6 @@ fn test_body_encoding_override() { e.write_all(bytes.as_ref()).unwrap(); let dec = e.finish().unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); - } #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] From d846328f36573a4b28ce354db73618049c0658fd Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 29 Mar 2019 21:13:39 -0700 Subject: [PATCH 391/427] fork cookie crate --- Cargo.toml | 12 +- actix-files/Cargo.toml | 7 +- actix-http/Cargo.toml | 12 +- actix-http/src/client/connection.rs | 4 +- actix-http/src/cookie/builder.rs | 240 +++++ actix-http/src/cookie/delta.rs | 71 ++ actix-http/src/cookie/draft.rs | 98 ++ actix-http/src/cookie/jar.rs | 653 ++++++++++++++ actix-http/src/cookie/mod.rs | 1087 +++++++++++++++++++++++ actix-http/src/cookie/parse.rs | 426 +++++++++ actix-http/src/cookie/secure/key.rs | 180 ++++ actix-http/src/cookie/secure/macros.rs | 40 + actix-http/src/cookie/secure/mod.rs | 10 + actix-http/src/cookie/secure/private.rs | 226 +++++ actix-http/src/cookie/secure/signed.rs | 185 ++++ actix-http/src/error.rs | 12 +- actix-http/src/h1/dispatcher.rs | 6 +- actix-http/src/h2/service.rs | 2 +- actix-http/src/httpmessage.rs | 11 +- actix-http/src/lib.rs | 18 +- actix-http/src/response.rs | 66 +- actix-http/src/test.rs | 40 +- actix-http/tests/test_client.rs | 17 +- actix-session/Cargo.toml | 6 +- actix-session/src/cookie.rs | 2 +- actix-web-actors/Cargo.toml | 12 +- actix-web-codegen/Cargo.toml | 9 +- awc/Cargo.toml | 9 +- awc/src/lib.rs | 2 +- awc/src/request.rs | 38 +- awc/src/response.rs | 5 +- awc/src/test.rs | 41 +- awc/src/ws.rs | 38 +- src/lib.rs | 2 +- src/middleware/identity.rs | 2 +- src/middleware/mod.rs | 2 +- src/request.rs | 2 - src/test.rs | 4 +- test-server/Cargo.toml | 11 +- test-server/src/lib.rs | 2 +- 40 files changed, 3357 insertions(+), 253 deletions(-) create mode 100644 actix-http/src/cookie/builder.rs create mode 100644 actix-http/src/cookie/delta.rs create mode 100644 actix-http/src/cookie/draft.rs create mode 100644 actix-http/src/cookie/jar.rs create mode 100644 actix-http/src/cookie/mod.rs create mode 100644 actix-http/src/cookie/parse.rs create mode 100644 actix-http/src/cookie/secure/key.rs create mode 100644 actix-http/src/cookie/secure/macros.rs create mode 100644 actix-http/src/cookie/secure/mod.rs create mode 100644 actix-http/src/cookie/secure/private.rs create mode 100644 actix-http/src/cookie/secure/signed.rs diff --git a/Cargo.toml b/Cargo.toml index 06c88c57e..1bd089f02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,10 +35,10 @@ members = [ ] [package.metadata.docs.rs] -features = ["ssl", "tls", "brotli", "flate2-zlib", "cookies", "client", "rust-tls"] +features = ["ssl", "tls", "brotli", "flate2-zlib", "secure-cookies", "client", "rust-tls"] [features] -default = ["brotli", "flate2-zlib", "cookies", "client"] +default = ["brotli", "flate2-zlib", "secure-cookies", "client"] # http client client = ["awc"] @@ -53,7 +53,7 @@ flate2-zlib = ["actix-http/flate2-zlib"] flate2-rust = ["actix-http/flate2-rust"] # sessions feature, session require "ring" crate and c compiler -cookies = ["cookie", "actix-http/cookies"] +secure-cookies = ["actix-http/secure-cookies"] # tls tls = ["native-tls", "actix-server/ssl"] @@ -94,9 +94,6 @@ serde_urlencoded = "^0.5.3" time = "0.1" url = { version="1.7", features=["query_encoding"] } -# cookies support -cookie = { version="0.11", features=["secure", "percent-encode"], optional = true } - # ssl support native-tls = { version="0.2", optional = true } openssl = { version="0.10", optional = true } @@ -112,9 +109,6 @@ tokio-timer = "0.2.8" brotli2 = "0.3.2" flate2 = "1.0.2" -[replace] -"cookie:0.11.0" = { git = 'https://github.com/alexcrichton/cookie-rs.git' } - [profile.release] lto = true opt-level = 3 diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index d6ae67540..058a19987 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -18,8 +18,8 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = "1.0.0-alpha.1" -actix-http = "0.1.0-alpha.1" +#actix-web = "1.0.0-alpha.1" +actix-web = { path = ".." } actix-service = "0.3.3" bitflags = "1" @@ -33,4 +33,5 @@ percent-encoding = "1.0" v_htmlescape = "0.4" [dev-dependencies] -actix-web = { version = "1.0.0-alpha.1", features=["ssl"] } +#actix-web = { version = "1.0.0-alpha.1", features=["ssl"] } +actix-web = { path = "..", features=["ssl"] } diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 0c910cb1e..e9bb5d9bb 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -16,7 +16,7 @@ edition = "2018" workspace = ".." [package.metadata.docs.rs] -features = ["ssl", "fail", "cookie", "brotli", "flate2-zlib"] +features = ["ssl", "fail", "brotli", "flate2-zlib", "secure-cookies"] [badges] travis-ci = { repository = "actix/actix-web", branch = "master" } @@ -32,9 +32,6 @@ default = [] # openssl ssl = ["openssl", "actix-connect/ssl"] -# cookies integration -cookies = ["cookie"] - # brotli encoding, requires c compiler brotli = ["brotli2"] @@ -47,6 +44,9 @@ flate2-rust = ["flate2/rust_backend"] # failure integration. actix does not use failure anymore fail = ["failure"] +# support for secure cookies +secure-cookies = ["ring"] + [dependencies] actix-service = "0.3.4" actix-codec = "0.1.2" @@ -85,12 +85,14 @@ tokio-timer = "0.2" tokio-current-thread = "0.1" trust-dns-resolver = { version="0.11.0-alpha.2", default-features = false } +# for secure cookie +ring = { version = "0.14.6", optional = true } + # compression brotli2 = { version="^0.3.2", optional = true } flate2 = { version="^1.0.2", optional = true, default-features = false } # optional deps -cookie = { version="0.11", features=["percent-encode"], optional = true } failure = { version = "0.1.5", optional = true } openssl = { version="0.10", optional = true } diff --git a/actix-http/src/client/connection.rs b/actix-http/src/client/connection.rs index 267c85d31..4522dbbd3 100644 --- a/actix-http/src/client/connection.rs +++ b/actix-http/src/client/connection.rs @@ -184,11 +184,11 @@ where match self { EitherConnection::A(con) => Box::new( con.open_tunnel(head) - .map(|(head, framed)| (head, framed.map_io(|io| EitherIo::A(io)))), + .map(|(head, framed)| (head, framed.map_io(EitherIo::A))), ), EitherConnection::B(con) => Box::new( con.open_tunnel(head) - .map(|(head, framed)| (head, framed.map_io(|io| EitherIo::B(io)))), + .map(|(head, framed)| (head, framed.map_io(EitherIo::B))), ), } } diff --git a/actix-http/src/cookie/builder.rs b/actix-http/src/cookie/builder.rs new file mode 100644 index 000000000..2635572ab --- /dev/null +++ b/actix-http/src/cookie/builder.rs @@ -0,0 +1,240 @@ +use std::borrow::Cow; + +use time::{Duration, Tm}; + +use super::{Cookie, SameSite}; + +/// Structure that follows the builder pattern for building `Cookie` structs. +/// +/// To construct a cookie: +/// +/// 1. Call [`Cookie::build`](struct.Cookie.html#method.build) to start building. +/// 2. Use any of the builder methods to set fields in the cookie. +/// 3. Call [finish](#method.finish) to retrieve the built cookie. +/// +/// # Example +/// +/// ```rust +/// use actix_http::cookie::Cookie; +/// use time::Duration; +/// +/// # fn main() { +/// let cookie: Cookie = Cookie::build("name", "value") +/// .domain("www.rust-lang.org") +/// .path("/") +/// .secure(true) +/// .http_only(true) +/// .max_age(Duration::days(1)) +/// .finish(); +/// # } +/// ``` +#[derive(Debug, Clone)] +pub struct CookieBuilder { + /// The cookie being built. + cookie: Cookie<'static>, +} + +impl CookieBuilder { + /// Creates a new `CookieBuilder` instance from the given name and value. + /// + /// This method is typically called indirectly via + /// [Cookie::build](struct.Cookie.html#method.build). + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::build("foo", "bar").finish(); + /// assert_eq!(c.name_value(), ("foo", "bar")); + /// ``` + pub fn new(name: N, value: V) -> CookieBuilder + where + N: Into>, + V: Into>, + { + CookieBuilder { + cookie: Cookie::new(name, value), + } + } + + /// Sets the `expires` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// # fn main() { + /// let c = Cookie::build("foo", "bar") + /// .expires(time::now()) + /// .finish(); + /// + /// assert!(c.expires().is_some()); + /// # } + /// ``` + #[inline] + pub fn expires(mut self, when: Tm) -> CookieBuilder { + self.cookie.set_expires(when); + self + } + + /// Sets the `max_age` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// # fn main() { + /// let c = Cookie::build("foo", "bar") + /// .max_age(time::Duration::minutes(30)) + /// .finish(); + /// + /// assert_eq!(c.max_age(), Some(time::Duration::seconds(30 * 60))); + /// # } + /// ``` + #[inline] + pub fn max_age(mut self, value: Duration) -> CookieBuilder { + self.cookie.set_max_age(value); + self + } + + /// Sets the `domain` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::build("foo", "bar") + /// .domain("www.rust-lang.org") + /// .finish(); + /// + /// assert_eq!(c.domain(), Some("www.rust-lang.org")); + /// ``` + pub fn domain>>(mut self, value: D) -> CookieBuilder { + self.cookie.set_domain(value); + self + } + + /// Sets the `path` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::build("foo", "bar") + /// .path("/") + /// .finish(); + /// + /// assert_eq!(c.path(), Some("/")); + /// ``` + pub fn path>>(mut self, path: P) -> CookieBuilder { + self.cookie.set_path(path); + self + } + + /// Sets the `secure` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::build("foo", "bar") + /// .secure(true) + /// .finish(); + /// + /// assert_eq!(c.secure(), Some(true)); + /// ``` + #[inline] + pub fn secure(mut self, value: bool) -> CookieBuilder { + self.cookie.set_secure(value); + self + } + + /// Sets the `http_only` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::build("foo", "bar") + /// .http_only(true) + /// .finish(); + /// + /// assert_eq!(c.http_only(), Some(true)); + /// ``` + #[inline] + pub fn http_only(mut self, value: bool) -> CookieBuilder { + self.cookie.set_http_only(value); + self + } + + /// Sets the `same_site` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{Cookie, SameSite}; + /// + /// let c = Cookie::build("foo", "bar") + /// .same_site(SameSite::Strict) + /// .finish(); + /// + /// assert_eq!(c.same_site(), Some(SameSite::Strict)); + /// ``` + #[inline] + pub fn same_site(mut self, value: SameSite) -> CookieBuilder { + self.cookie.set_same_site(value); + self + } + + /// Makes the cookie being built 'permanent' by extending its expiration and + /// max age 20 years into the future. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// use time::Duration; + /// + /// # fn main() { + /// let c = Cookie::build("foo", "bar") + /// .permanent() + /// .finish(); + /// + /// assert_eq!(c.max_age(), Some(Duration::days(365 * 20))); + /// # assert!(c.expires().is_some()); + /// # } + /// ``` + #[inline] + pub fn permanent(mut self) -> CookieBuilder { + self.cookie.make_permanent(); + self + } + + /// Finishes building and returns the built `Cookie`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::build("foo", "bar") + /// .domain("crates.io") + /// .path("/") + /// .finish(); + /// + /// assert_eq!(c.name_value(), ("foo", "bar")); + /// assert_eq!(c.domain(), Some("crates.io")); + /// assert_eq!(c.path(), Some("/")); + /// ``` + #[inline] + pub fn finish(self) -> Cookie<'static> { + self.cookie + } +} diff --git a/actix-http/src/cookie/delta.rs b/actix-http/src/cookie/delta.rs new file mode 100644 index 000000000..a001a5bb8 --- /dev/null +++ b/actix-http/src/cookie/delta.rs @@ -0,0 +1,71 @@ +use std::borrow::Borrow; +use std::hash::{Hash, Hasher}; +use std::ops::{Deref, DerefMut}; + +use super::Cookie; + +/// A `DeltaCookie` is a helper structure used in a cookie jar. It wraps a +/// `Cookie` so that it can be hashed and compared purely by name. It further +/// records whether the wrapped cookie is a "removal" cookie, that is, a cookie +/// that when sent to the client removes the named cookie on the client's +/// machine. +#[derive(Clone, Debug)] +pub struct DeltaCookie { + pub cookie: Cookie<'static>, + pub removed: bool, +} + +impl DeltaCookie { + /// Create a new `DeltaCookie` that is being added to a jar. + #[inline] + pub fn added(cookie: Cookie<'static>) -> DeltaCookie { + DeltaCookie { + cookie, + removed: false, + } + } + + /// Create a new `DeltaCookie` that is being removed from a jar. The + /// `cookie` should be a "removal" cookie. + #[inline] + pub fn removed(cookie: Cookie<'static>) -> DeltaCookie { + DeltaCookie { + cookie, + removed: true, + } + } +} + +impl Deref for DeltaCookie { + type Target = Cookie<'static>; + + fn deref(&self) -> &Cookie<'static> { + &self.cookie + } +} + +impl DerefMut for DeltaCookie { + fn deref_mut(&mut self) -> &mut Cookie<'static> { + &mut self.cookie + } +} + +impl PartialEq for DeltaCookie { + fn eq(&self, other: &DeltaCookie) -> bool { + self.name() == other.name() + } +} + +impl Eq for DeltaCookie {} + +impl Hash for DeltaCookie { + fn hash(&self, state: &mut H) { + self.name().hash(state); + } +} + +impl Borrow for DeltaCookie { + fn borrow(&self) -> &str { + self.name() + } +} diff --git a/actix-http/src/cookie/draft.rs b/actix-http/src/cookie/draft.rs new file mode 100644 index 000000000..362133946 --- /dev/null +++ b/actix-http/src/cookie/draft.rs @@ -0,0 +1,98 @@ +//! This module contains types that represent cookie properties that are not yet +//! standardized. That is, _draft_ features. + +use std::fmt; + +/// The `SameSite` cookie attribute. +/// +/// A cookie with a `SameSite` attribute is imposed restrictions on when it is +/// sent to the origin server in a cross-site request. If the `SameSite` +/// attribute is "Strict", then the cookie is never sent in cross-site requests. +/// If the `SameSite` attribute is "Lax", the cookie is only sent in cross-site +/// requests with "safe" HTTP methods, i.e, `GET`, `HEAD`, `OPTIONS`, `TRACE`. +/// If the `SameSite` attribute is not present (made explicit via the +/// `SameSite::None` variant), then the cookie will be sent as normal. +/// +/// **Note:** This cookie attribute is an HTTP draft! Its meaning and definition +/// are subject to change. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SameSite { + /// The "Strict" `SameSite` attribute. + Strict, + /// The "Lax" `SameSite` attribute. + Lax, + /// No `SameSite` attribute. + None, +} + +impl SameSite { + /// Returns `true` if `self` is `SameSite::Strict` and `false` otherwise. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::SameSite; + /// + /// let strict = SameSite::Strict; + /// assert!(strict.is_strict()); + /// assert!(!strict.is_lax()); + /// assert!(!strict.is_none()); + /// ``` + #[inline] + pub fn is_strict(self) -> bool { + match self { + SameSite::Strict => true, + SameSite::Lax | SameSite::None => false, + } + } + + /// Returns `true` if `self` is `SameSite::Lax` and `false` otherwise. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::SameSite; + /// + /// let lax = SameSite::Lax; + /// assert!(lax.is_lax()); + /// assert!(!lax.is_strict()); + /// assert!(!lax.is_none()); + /// ``` + #[inline] + pub fn is_lax(self) -> bool { + match self { + SameSite::Lax => true, + SameSite::Strict | SameSite::None => false, + } + } + + /// Returns `true` if `self` is `SameSite::None` and `false` otherwise. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::SameSite; + /// + /// let none = SameSite::None; + /// assert!(none.is_none()); + /// assert!(!none.is_lax()); + /// assert!(!none.is_strict()); + /// ``` + #[inline] + pub fn is_none(self) -> bool { + match self { + SameSite::None => true, + SameSite::Lax | SameSite::Strict => false, + } + } +} + +impl fmt::Display for SameSite { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + SameSite::Strict => write!(f, "Strict"), + SameSite::Lax => write!(f, "Lax"), + SameSite::None => Ok(()), + } + } +} diff --git a/actix-http/src/cookie/jar.rs b/actix-http/src/cookie/jar.rs new file mode 100644 index 000000000..d9ab8f056 --- /dev/null +++ b/actix-http/src/cookie/jar.rs @@ -0,0 +1,653 @@ +use std::collections::HashSet; +use std::mem::replace; + +use time::{self, Duration}; + +use super::delta::DeltaCookie; +use super::Cookie; + +#[cfg(feature = "secure-cookies")] +use super::secure::{Key, PrivateJar, SignedJar}; + +/// A collection of cookies that tracks its modifications. +/// +/// A `CookieJar` provides storage for any number of cookies. Any changes made +/// to the jar are tracked; the changes can be retrieved via the +/// [delta](#method.delta) method which returns an interator over the changes. +/// +/// # Usage +/// +/// A jar's life begins via [new](#method.new) and calls to +/// [`add_original`](#method.add_original): +/// +/// ```rust +/// use actix_http::cookie::{Cookie, CookieJar}; +/// +/// let mut jar = CookieJar::new(); +/// jar.add_original(Cookie::new("name", "value")); +/// jar.add_original(Cookie::new("second", "another")); +/// ``` +/// +/// Cookies can be added via [add](#method.add) and removed via +/// [remove](#method.remove). Finally, cookies can be looked up via +/// [get](#method.get): +/// +/// ```rust +/// # use actix_http::cookie::{Cookie, CookieJar}; +/// let mut jar = CookieJar::new(); +/// jar.add(Cookie::new("a", "one")); +/// jar.add(Cookie::new("b", "two")); +/// +/// assert_eq!(jar.get("a").map(|c| c.value()), Some("one")); +/// assert_eq!(jar.get("b").map(|c| c.value()), Some("two")); +/// +/// jar.remove(Cookie::named("b")); +/// assert!(jar.get("b").is_none()); +/// ``` +/// +/// # Deltas +/// +/// A jar keeps track of any modifications made to it over time. The +/// modifications are recorded as cookies. The modifications can be retrieved +/// via [delta](#method.delta). Any new `Cookie` added to a jar via `add` +/// results in the same `Cookie` appearing in the `delta`; cookies added via +/// `add_original` do not count towards the delta. Any _original_ cookie that is +/// removed from a jar results in a "removal" cookie appearing in the delta. A +/// "removal" cookie is a cookie that a server sends so that the cookie is +/// removed from the client's machine. +/// +/// Deltas are typically used to create `Set-Cookie` headers corresponding to +/// the changes made to a cookie jar over a period of time. +/// +/// ```rust +/// # use actix_http::cookie::{Cookie, CookieJar}; +/// let mut jar = CookieJar::new(); +/// +/// // original cookies don't affect the delta +/// jar.add_original(Cookie::new("original", "value")); +/// assert_eq!(jar.delta().count(), 0); +/// +/// // new cookies result in an equivalent `Cookie` in the delta +/// jar.add(Cookie::new("a", "one")); +/// jar.add(Cookie::new("b", "two")); +/// assert_eq!(jar.delta().count(), 2); +/// +/// // removing an original cookie adds a "removal" cookie to the delta +/// jar.remove(Cookie::named("original")); +/// assert_eq!(jar.delta().count(), 3); +/// +/// // removing a new cookie that was added removes that `Cookie` from the delta +/// jar.remove(Cookie::named("a")); +/// assert_eq!(jar.delta().count(), 2); +/// ``` +#[derive(Default, Debug, Clone)] +pub struct CookieJar { + original_cookies: HashSet, + delta_cookies: HashSet, +} + +impl CookieJar { + /// Creates an empty cookie jar. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::CookieJar; + /// + /// let jar = CookieJar::new(); + /// assert_eq!(jar.iter().count(), 0); + /// ``` + pub fn new() -> CookieJar { + CookieJar::default() + } + + /// Returns a reference to the `Cookie` inside this jar with the name + /// `name`. If no such cookie exists, returns `None`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie}; + /// + /// let mut jar = CookieJar::new(); + /// assert!(jar.get("name").is_none()); + /// + /// jar.add(Cookie::new("name", "value")); + /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value")); + /// ``` + pub fn get(&self, name: &str) -> Option<&Cookie<'static>> { + self.delta_cookies + .get(name) + .or_else(|| self.original_cookies.get(name)) + .and_then(|c| if !c.removed { Some(&c.cookie) } else { None }) + } + + /// Adds an "original" `cookie` to this jar. If an original cookie with the + /// same name already exists, it is replaced with `cookie`. Cookies added + /// with `add` take precedence and are not replaced by this method. + /// + /// Adding an original cookie does not affect the [delta](#method.delta) + /// computation. This method is intended to be used to seed the cookie jar + /// with cookies received from a client's HTTP message. + /// + /// For accurate `delta` computations, this method should not be called + /// after calling `remove`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie}; + /// + /// let mut jar = CookieJar::new(); + /// jar.add_original(Cookie::new("name", "value")); + /// jar.add_original(Cookie::new("second", "two")); + /// + /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value")); + /// assert_eq!(jar.get("second").map(|c| c.value()), Some("two")); + /// assert_eq!(jar.iter().count(), 2); + /// assert_eq!(jar.delta().count(), 0); + /// ``` + pub fn add_original(&mut self, cookie: Cookie<'static>) { + self.original_cookies.replace(DeltaCookie::added(cookie)); + } + + /// Adds `cookie` to this jar. If a cookie with the same name already + /// exists, it is replaced with `cookie`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie}; + /// + /// let mut jar = CookieJar::new(); + /// jar.add(Cookie::new("name", "value")); + /// jar.add(Cookie::new("second", "two")); + /// + /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value")); + /// assert_eq!(jar.get("second").map(|c| c.value()), Some("two")); + /// assert_eq!(jar.iter().count(), 2); + /// assert_eq!(jar.delta().count(), 2); + /// ``` + pub fn add(&mut self, cookie: Cookie<'static>) { + self.delta_cookies.replace(DeltaCookie::added(cookie)); + } + + /// Removes `cookie` from this jar. If an _original_ cookie with the same + /// name as `cookie` is present in the jar, a _removal_ cookie will be + /// present in the `delta` computation. To properly generate the removal + /// cookie, `cookie` must contain the same `path` and `domain` as the cookie + /// that was initially set. + /// + /// A "removal" cookie is a cookie that has the same name as the original + /// cookie but has an empty value, a max-age of 0, and an expiration date + /// far in the past. + /// + /// # Example + /// + /// Removing an _original_ cookie results in a _removal_ cookie: + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie}; + /// use time::Duration; + /// + /// # fn main() { + /// let mut jar = CookieJar::new(); + /// + /// // Assume this cookie originally had a path of "/" and domain of "a.b". + /// jar.add_original(Cookie::new("name", "value")); + /// + /// // If the path and domain were set, they must be provided to `remove`. + /// jar.remove(Cookie::build("name", "").path("/").domain("a.b").finish()); + /// + /// // The delta will contain the removal cookie. + /// let delta: Vec<_> = jar.delta().collect(); + /// assert_eq!(delta.len(), 1); + /// assert_eq!(delta[0].name(), "name"); + /// assert_eq!(delta[0].max_age(), Some(Duration::seconds(0))); + /// # } + /// ``` + /// + /// Removing a new cookie does not result in a _removal_ cookie: + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie}; + /// + /// let mut jar = CookieJar::new(); + /// jar.add(Cookie::new("name", "value")); + /// assert_eq!(jar.delta().count(), 1); + /// + /// jar.remove(Cookie::named("name")); + /// assert_eq!(jar.delta().count(), 0); + /// ``` + pub fn remove(&mut self, mut cookie: Cookie<'static>) { + if self.original_cookies.contains(cookie.name()) { + cookie.set_value(""); + cookie.set_max_age(Duration::seconds(0)); + cookie.set_expires(time::now() - Duration::days(365)); + self.delta_cookies.replace(DeltaCookie::removed(cookie)); + } else { + self.delta_cookies.remove(cookie.name()); + } + } + + /// Removes `cookie` from this jar completely. This method differs from + /// `remove` in that no delta cookie is created under any condition. Neither + /// the `delta` nor `iter` methods will return a cookie that is removed + /// using this method. + /// + /// # Example + /// + /// Removing an _original_ cookie; no _removal_ cookie is generated: + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie}; + /// use time::Duration; + /// + /// # fn main() { + /// let mut jar = CookieJar::new(); + /// + /// // Add an original cookie and a new cookie. + /// jar.add_original(Cookie::new("name", "value")); + /// jar.add(Cookie::new("key", "value")); + /// assert_eq!(jar.delta().count(), 1); + /// assert_eq!(jar.iter().count(), 2); + /// + /// // Now force remove the original cookie. + /// jar.force_remove(Cookie::new("name", "value")); + /// assert_eq!(jar.delta().count(), 1); + /// assert_eq!(jar.iter().count(), 1); + /// + /// // Now force remove the new cookie. + /// jar.force_remove(Cookie::new("key", "value")); + /// assert_eq!(jar.delta().count(), 0); + /// assert_eq!(jar.iter().count(), 0); + /// # } + /// ``` + pub fn force_remove<'a>(&mut self, cookie: Cookie<'a>) { + self.original_cookies.remove(cookie.name()); + self.delta_cookies.remove(cookie.name()); + } + + /// Removes all cookies from this cookie jar. + #[deprecated( + since = "0.7.0", + note = "calling this method may not remove \ + all cookies since the path and domain are not specified; use \ + `remove` instead" + )] + pub fn clear(&mut self) { + self.delta_cookies.clear(); + for delta in replace(&mut self.original_cookies, HashSet::new()) { + self.remove(delta.cookie); + } + } + + /// Returns an iterator over cookies that represent the changes to this jar + /// over time. These cookies can be rendered directly as `Set-Cookie` header + /// values to affect the changes made to this jar on the client. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie}; + /// + /// let mut jar = CookieJar::new(); + /// jar.add_original(Cookie::new("name", "value")); + /// jar.add_original(Cookie::new("second", "two")); + /// + /// // Add new cookies. + /// jar.add(Cookie::new("new", "third")); + /// jar.add(Cookie::new("another", "fourth")); + /// jar.add(Cookie::new("yac", "fifth")); + /// + /// // Remove some cookies. + /// jar.remove(Cookie::named("name")); + /// jar.remove(Cookie::named("another")); + /// + /// // Delta contains two new cookies ("new", "yac") and a removal ("name"). + /// assert_eq!(jar.delta().count(), 3); + /// ``` + pub fn delta(&self) -> Delta { + Delta { + iter: self.delta_cookies.iter(), + } + } + + /// Returns an iterator over all of the cookies present in this jar. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie}; + /// + /// let mut jar = CookieJar::new(); + /// + /// jar.add_original(Cookie::new("name", "value")); + /// jar.add_original(Cookie::new("second", "two")); + /// + /// jar.add(Cookie::new("new", "third")); + /// jar.add(Cookie::new("another", "fourth")); + /// jar.add(Cookie::new("yac", "fifth")); + /// + /// jar.remove(Cookie::named("name")); + /// jar.remove(Cookie::named("another")); + /// + /// // There are three cookies in the jar: "second", "new", and "yac". + /// # assert_eq!(jar.iter().count(), 3); + /// for cookie in jar.iter() { + /// match cookie.name() { + /// "second" => assert_eq!(cookie.value(), "two"), + /// "new" => assert_eq!(cookie.value(), "third"), + /// "yac" => assert_eq!(cookie.value(), "fifth"), + /// _ => unreachable!("there are only three cookies in the jar") + /// } + /// } + /// ``` + pub fn iter(&self) -> Iter { + Iter { + delta_cookies: self + .delta_cookies + .iter() + .chain(self.original_cookies.difference(&self.delta_cookies)), + } + } + + /// Returns a `PrivateJar` with `self` as its parent jar using the key `key` + /// to sign/encrypt and verify/decrypt cookies added/retrieved from the + /// child jar. + /// + /// Any modifications to the child jar will be reflected on the parent jar, + /// and any retrievals from the child jar will be made from the parent jar. + /// + /// This method is only available when the `secure` feature is enabled. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{Cookie, CookieJar, Key}; + /// + /// // Generate a secure key. + /// let key = Key::generate(); + /// + /// // Add a private (signed + encrypted) cookie. + /// let mut jar = CookieJar::new(); + /// jar.private(&key).add(Cookie::new("private", "text")); + /// + /// // The cookie's contents are encrypted. + /// assert_ne!(jar.get("private").unwrap().value(), "text"); + /// + /// // They can be decrypted and verified through the child jar. + /// assert_eq!(jar.private(&key).get("private").unwrap().value(), "text"); + /// + /// // A tampered with cookie does not validate but still exists. + /// let mut cookie = jar.get("private").unwrap().clone(); + /// jar.add(Cookie::new("private", cookie.value().to_string() + "!")); + /// assert!(jar.private(&key).get("private").is_none()); + /// assert!(jar.get("private").is_some()); + /// ``` + #[cfg(feature = "secure-cookies")] + pub fn private(&mut self, key: &Key) -> PrivateJar { + PrivateJar::new(self, key) + } + + /// Returns a `SignedJar` with `self` as its parent jar using the key `key` + /// to sign/verify cookies added/retrieved from the child jar. + /// + /// Any modifications to the child jar will be reflected on the parent jar, + /// and any retrievals from the child jar will be made from the parent jar. + /// + /// This method is only available when the `secure` feature is enabled. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{Cookie, CookieJar, Key}; + /// + /// // Generate a secure key. + /// let key = Key::generate(); + /// + /// // Add a signed cookie. + /// let mut jar = CookieJar::new(); + /// jar.signed(&key).add(Cookie::new("signed", "text")); + /// + /// // The cookie's contents are signed but still in plaintext. + /// assert_ne!(jar.get("signed").unwrap().value(), "text"); + /// assert!(jar.get("signed").unwrap().value().contains("text")); + /// + /// // They can be verified through the child jar. + /// assert_eq!(jar.signed(&key).get("signed").unwrap().value(), "text"); + /// + /// // A tampered with cookie does not validate but still exists. + /// let mut cookie = jar.get("signed").unwrap().clone(); + /// jar.add(Cookie::new("signed", cookie.value().to_string() + "!")); + /// assert!(jar.signed(&key).get("signed").is_none()); + /// assert!(jar.get("signed").is_some()); + /// ``` + #[cfg(feature = "secure-cookies")] + pub fn signed(&mut self, key: &Key) -> SignedJar { + SignedJar::new(self, key) + } +} + +use std::collections::hash_set::Iter as HashSetIter; + +/// Iterator over the changes to a cookie jar. +pub struct Delta<'a> { + iter: HashSetIter<'a, DeltaCookie>, +} + +impl<'a> Iterator for Delta<'a> { + type Item = &'a Cookie<'static>; + + fn next(&mut self) -> Option<&'a Cookie<'static>> { + self.iter.next().map(|c| &c.cookie) + } +} + +use std::collections::hash_map::RandomState; +use std::collections::hash_set::Difference; +use std::iter::Chain; + +/// Iterator over all of the cookies in a jar. +pub struct Iter<'a> { + delta_cookies: + Chain, Difference<'a, DeltaCookie, RandomState>>, +} + +impl<'a> Iterator for Iter<'a> { + type Item = &'a Cookie<'static>; + + fn next(&mut self) -> Option<&'a Cookie<'static>> { + for cookie in self.delta_cookies.by_ref() { + if !cookie.removed { + return Some(&*cookie); + } + } + + None + } +} + +#[cfg(test)] +mod test { + use super::{Cookie, CookieJar, Key}; + + #[test] + #[allow(deprecated)] + fn simple() { + let mut c = CookieJar::new(); + + c.add(Cookie::new("test", "")); + c.add(Cookie::new("test2", "")); + c.remove(Cookie::named("test")); + + assert!(c.get("test").is_none()); + assert!(c.get("test2").is_some()); + + c.add(Cookie::new("test3", "")); + c.clear(); + + assert!(c.get("test").is_none()); + assert!(c.get("test2").is_none()); + assert!(c.get("test3").is_none()); + } + + #[test] + fn jar_is_send() { + fn is_send(_: T) -> bool { + true + } + + assert!(is_send(CookieJar::new())) + } + + #[test] + #[cfg(feature = "secure-cookies")] + fn iter() { + let key = Key::generate(); + let mut c = CookieJar::new(); + + c.add_original(Cookie::new("original", "original")); + + c.add(Cookie::new("test", "test")); + c.add(Cookie::new("test2", "test2")); + c.add(Cookie::new("test3", "test3")); + assert_eq!(c.iter().count(), 4); + + c.signed(&key).add(Cookie::new("signed", "signed")); + c.private(&key).add(Cookie::new("encrypted", "encrypted")); + assert_eq!(c.iter().count(), 6); + + c.remove(Cookie::named("test")); + assert_eq!(c.iter().count(), 5); + + c.remove(Cookie::named("signed")); + c.remove(Cookie::named("test2")); + assert_eq!(c.iter().count(), 3); + + c.add(Cookie::new("test2", "test2")); + assert_eq!(c.iter().count(), 4); + + c.remove(Cookie::named("test2")); + assert_eq!(c.iter().count(), 3); + } + + #[test] + #[cfg(feature = "secure-cookies")] + fn delta() { + use std::collections::HashMap; + use time::Duration; + + let mut c = CookieJar::new(); + + c.add_original(Cookie::new("original", "original")); + c.add_original(Cookie::new("original1", "original1")); + + c.add(Cookie::new("test", "test")); + c.add(Cookie::new("test2", "test2")); + c.add(Cookie::new("test3", "test3")); + c.add(Cookie::new("test4", "test4")); + + c.remove(Cookie::named("test")); + c.remove(Cookie::named("original")); + + assert_eq!(c.delta().count(), 4); + + let names: HashMap<_, _> = c.delta().map(|c| (c.name(), c.max_age())).collect(); + + assert!(names.get("test2").unwrap().is_none()); + assert!(names.get("test3").unwrap().is_none()); + assert!(names.get("test4").unwrap().is_none()); + assert_eq!(names.get("original").unwrap(), &Some(Duration::seconds(0))); + } + + #[test] + fn replace_original() { + let mut jar = CookieJar::new(); + jar.add_original(Cookie::new("original_a", "a")); + jar.add_original(Cookie::new("original_b", "b")); + assert_eq!(jar.get("original_a").unwrap().value(), "a"); + + jar.add(Cookie::new("original_a", "av2")); + assert_eq!(jar.get("original_a").unwrap().value(), "av2"); + } + + #[test] + fn empty_delta() { + let mut jar = CookieJar::new(); + jar.add(Cookie::new("name", "val")); + assert_eq!(jar.delta().count(), 1); + + jar.remove(Cookie::named("name")); + assert_eq!(jar.delta().count(), 0); + + jar.add_original(Cookie::new("name", "val")); + assert_eq!(jar.delta().count(), 0); + + jar.remove(Cookie::named("name")); + assert_eq!(jar.delta().count(), 1); + + jar.add(Cookie::new("name", "val")); + assert_eq!(jar.delta().count(), 1); + + jar.remove(Cookie::named("name")); + assert_eq!(jar.delta().count(), 1); + } + + #[test] + fn add_remove_add() { + let mut jar = CookieJar::new(); + jar.add_original(Cookie::new("name", "val")); + assert_eq!(jar.delta().count(), 0); + + jar.remove(Cookie::named("name")); + assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); + assert_eq!(jar.delta().count(), 1); + + // The cookie's been deleted. Another original doesn't change that. + jar.add_original(Cookie::new("name", "val")); + assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); + assert_eq!(jar.delta().count(), 1); + + jar.remove(Cookie::named("name")); + assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); + assert_eq!(jar.delta().count(), 1); + + jar.add(Cookie::new("name", "val")); + assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1); + assert_eq!(jar.delta().count(), 1); + + jar.remove(Cookie::named("name")); + assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); + assert_eq!(jar.delta().count(), 1); + } + + #[test] + fn replace_remove() { + let mut jar = CookieJar::new(); + jar.add_original(Cookie::new("name", "val")); + assert_eq!(jar.delta().count(), 0); + + jar.add(Cookie::new("name", "val")); + assert_eq!(jar.delta().count(), 1); + assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1); + + jar.remove(Cookie::named("name")); + assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); + } + + #[test] + fn remove_with_path() { + let mut jar = CookieJar::new(); + jar.add_original(Cookie::build("name", "val").finish()); + assert_eq!(jar.iter().count(), 1); + assert_eq!(jar.delta().count(), 0); + assert_eq!(jar.iter().filter(|c| c.path().is_none()).count(), 1); + + jar.remove(Cookie::build("name", "").path("/").finish()); + assert_eq!(jar.iter().count(), 0); + assert_eq!(jar.delta().count(), 1); + assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); + assert_eq!(jar.delta().filter(|c| c.path() == Some("/")).count(), 1); + } +} diff --git a/actix-http/src/cookie/mod.rs b/actix-http/src/cookie/mod.rs new file mode 100644 index 000000000..5545624a8 --- /dev/null +++ b/actix-http/src/cookie/mod.rs @@ -0,0 +1,1087 @@ +//! https://github.com/alexcrichton/cookie-rs fork +//! +//! HTTP cookie parsing and cookie jar management. +//! +//! This crates provides the [`Cookie`](struct.Cookie.html) type, which directly +//! maps to an HTTP cookie, and the [`CookieJar`](struct.CookieJar.html) type, +//! which allows for simple management of many cookies as well as encryption and +//! signing of cookies for session management. +//! +//! # Features +//! +//! This crates can be configured at compile-time through the following Cargo +//! features: +//! +//! +//! * **secure** (disabled by default) +//! +//! Enables signed and private (signed + encrypted) cookie jars. +//! +//! When this feature is enabled, the +//! [signed](struct.CookieJar.html#method.signed) and +//! [private](struct.CookieJar.html#method.private) method of `CookieJar` and +//! [`SignedJar`](struct.SignedJar.html) and +//! [`PrivateJar`](struct.PrivateJar.html) structures are available. The jars +//! act as "children jars", allowing for easy retrieval and addition of signed +//! and/or encrypted cookies to a cookie jar. When this feature is disabled, +//! none of the types are available. +//! +//! * **percent-encode** (disabled by default) +//! +//! Enables percent encoding and decoding of names and values in cookies. +//! +//! When this feature is enabled, the +//! [encoded](struct.Cookie.html#method.encoded) and +//! [`parse_encoded`](struct.Cookie.html#method.parse_encoded) methods of +//! `Cookie` become available. The `encoded` method returns a wrapper around a +//! `Cookie` whose `Display` implementation percent-encodes the name and value +//! of the cookie. The `parse_encoded` method percent-decodes the name and +//! value of a `Cookie` during parsing. When this feature is disabled, the +//! `encoded` and `parse_encoded` methods are not available. +//! +//! You can enable features via the `Cargo.toml` file: +//! +//! ```ignore +//! [dependencies.cookie] +//! features = ["secure", "percent-encode"] +//! ``` + +#![doc(html_root_url = "https://docs.rs/cookie/0.11")] +#![deny(missing_docs)] + +mod builder; +mod delta; +mod draft; +mod jar; +mod parse; + +#[cfg(feature = "secure-cookies")] +#[macro_use] +mod secure; +#[cfg(feature = "secure-cookies")] +pub use secure::*; + +use std::borrow::Cow; +use std::fmt; +use std::str::FromStr; + +use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; +use time::{Duration, Tm}; + +pub use builder::CookieBuilder; +pub use draft::*; +pub use jar::{CookieJar, Delta, Iter}; +use parse::parse_cookie; +pub use parse::ParseError; + +#[derive(Debug, Clone)] +enum CookieStr { + /// An string derived from indexes (start, end). + Indexed(usize, usize), + /// A string derived from a concrete string. + Concrete(Cow<'static, str>), +} + +impl CookieStr { + /// Retrieves the string `self` corresponds to. If `self` is derived from + /// indexes, the corresponding subslice of `string` is returned. Otherwise, + /// the concrete string is returned. + /// + /// # Panics + /// + /// Panics if `self` is an indexed string and `string` is None. + fn to_str<'s>(&'s self, string: Option<&'s Cow>) -> &'s str { + match *self { + CookieStr::Indexed(i, j) => { + let s = string.expect( + "`Some` base string must exist when \ + converting indexed str to str! (This is a module invariant.)", + ); + &s[i..j] + } + CookieStr::Concrete(ref cstr) => &*cstr, + } + } + + fn to_raw_str<'s, 'c: 's>(&'s self, string: &'s Cow<'c, str>) -> Option<&'c str> { + match *self { + CookieStr::Indexed(i, j) => match *string { + Cow::Borrowed(s) => Some(&s[i..j]), + Cow::Owned(_) => None, + }, + CookieStr::Concrete(_) => None, + } + } +} + +/// Representation of an HTTP cookie. +/// +/// # Constructing a `Cookie` +/// +/// To construct a cookie with only a name/value, use the [new](#method.new) +/// method: +/// +/// ```rust +/// use actix_http::cookie::Cookie; +/// +/// let cookie = Cookie::new("name", "value"); +/// assert_eq!(&cookie.to_string(), "name=value"); +/// ``` +/// +/// To construct more elaborate cookies, use the [build](#method.build) method +/// and [`CookieBuilder`](struct.CookieBuilder.html) methods: +/// +/// ```rust +/// use actix_http::cookie::Cookie; +/// +/// let cookie = Cookie::build("name", "value") +/// .domain("www.rust-lang.org") +/// .path("/") +/// .secure(true) +/// .http_only(true) +/// .finish(); +/// ``` +#[derive(Debug, Clone)] +pub struct Cookie<'c> { + /// Storage for the cookie string. Only used if this structure was derived + /// from a string that was subsequently parsed. + cookie_string: Option>, + /// The cookie's name. + name: CookieStr, + /// The cookie's value. + value: CookieStr, + /// The cookie's expiration, if any. + expires: Option, + /// The cookie's maximum age, if any. + max_age: Option, + /// The cookie's domain, if any. + domain: Option, + /// The cookie's path domain, if any. + path: Option, + /// Whether this cookie was marked Secure. + secure: Option, + /// Whether this cookie was marked HttpOnly. + http_only: Option, + /// The draft `SameSite` attribute. + same_site: Option, +} + +impl Cookie<'static> { + /// Creates a new `Cookie` with the given name and value. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let cookie = Cookie::new("name", "value"); + /// assert_eq!(cookie.name_value(), ("name", "value")); + /// ``` + pub fn new(name: N, value: V) -> Cookie<'static> + where + N: Into>, + V: Into>, + { + Cookie { + cookie_string: None, + name: CookieStr::Concrete(name.into()), + value: CookieStr::Concrete(value.into()), + expires: None, + max_age: None, + domain: None, + path: None, + secure: None, + http_only: None, + same_site: None, + } + } + + /// Creates a new `Cookie` with the given name and an empty value. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let cookie = Cookie::named("name"); + /// assert_eq!(cookie.name(), "name"); + /// assert!(cookie.value().is_empty()); + /// ``` + pub fn named(name: N) -> Cookie<'static> + where + N: Into>, + { + Cookie::new(name, "") + } + + /// Creates a new `CookieBuilder` instance from the given key and value + /// strings. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::build("foo", "bar").finish(); + /// assert_eq!(c.name_value(), ("foo", "bar")); + /// ``` + pub fn build(name: N, value: V) -> CookieBuilder + where + N: Into>, + V: Into>, + { + CookieBuilder::new(name, value) + } +} + +impl<'c> Cookie<'c> { + /// Parses a `Cookie` from the given HTTP cookie header value string. Does + /// not perform any percent-decoding. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::parse("foo=bar%20baz; HttpOnly").unwrap(); + /// assert_eq!(c.name_value(), ("foo", "bar%20baz")); + /// assert_eq!(c.http_only(), Some(true)); + /// ``` + pub fn parse(s: S) -> Result, ParseError> + where + S: Into>, + { + parse_cookie(s, false) + } + + /// Parses a `Cookie` from the given HTTP cookie header value string where + /// the name and value fields are percent-encoded. Percent-decodes the + /// name/value fields. + /// + /// This API requires the `percent-encode` feature to be enabled on this + /// crate. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::parse_encoded("foo=bar%20baz; HttpOnly").unwrap(); + /// assert_eq!(c.name_value(), ("foo", "bar baz")); + /// assert_eq!(c.http_only(), Some(true)); + /// ``` + pub fn parse_encoded(s: S) -> Result, ParseError> + where + S: Into>, + { + parse_cookie(s, true) + } + + /// Wraps `self` in an `EncodedCookie`: a cost-free wrapper around `Cookie` + /// whose `Display` implementation percent-encodes the name and value of the + /// wrapped `Cookie`. + /// + /// This method is only available when the `percent-encode` feature is + /// enabled. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let mut c = Cookie::new("my name", "this; value?"); + /// assert_eq!(&c.encoded().to_string(), "my%20name=this%3B%20value%3F"); + /// ``` + pub fn encoded<'a>(&'a self) -> EncodedCookie<'a, 'c> { + EncodedCookie(self) + } + + /// Converts `self` into a `Cookie` with a static lifetime. This method + /// results in at most one allocation. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::new("a", "b"); + /// let owned_cookie = c.into_owned(); + /// assert_eq!(owned_cookie.name_value(), ("a", "b")); + /// ``` + pub fn into_owned(self) -> Cookie<'static> { + Cookie { + cookie_string: self.cookie_string.map(|s| s.into_owned().into()), + name: self.name, + value: self.value, + expires: self.expires, + max_age: self.max_age, + domain: self.domain, + path: self.path, + secure: self.secure, + http_only: self.http_only, + same_site: self.same_site, + } + } + + /// Returns the name of `self`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::new("name", "value"); + /// assert_eq!(c.name(), "name"); + /// ``` + #[inline] + pub fn name(&self) -> &str { + self.name.to_str(self.cookie_string.as_ref()) + } + + /// Returns the value of `self`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::new("name", "value"); + /// assert_eq!(c.value(), "value"); + /// ``` + #[inline] + pub fn value(&self) -> &str { + self.value.to_str(self.cookie_string.as_ref()) + } + + /// Returns the name and value of `self` as a tuple of `(name, value)`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::new("name", "value"); + /// assert_eq!(c.name_value(), ("name", "value")); + /// ``` + #[inline] + pub fn name_value(&self) -> (&str, &str) { + (self.name(), self.value()) + } + + /// Returns whether this cookie was marked `HttpOnly` or not. Returns + /// `Some(true)` when the cookie was explicitly set (manually or parsed) as + /// `HttpOnly`, `Some(false)` when `http_only` was manually set to `false`, + /// and `None` otherwise. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::parse("name=value; httponly").unwrap(); + /// assert_eq!(c.http_only(), Some(true)); + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.http_only(), None); + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.http_only(), None); + /// + /// // An explicitly set "false" value. + /// c.set_http_only(false); + /// assert_eq!(c.http_only(), Some(false)); + /// + /// // An explicitly set "true" value. + /// c.set_http_only(true); + /// assert_eq!(c.http_only(), Some(true)); + /// ``` + #[inline] + pub fn http_only(&self) -> Option { + self.http_only + } + + /// Returns whether this cookie was marked `Secure` or not. Returns + /// `Some(true)` when the cookie was explicitly set (manually or parsed) as + /// `Secure`, `Some(false)` when `secure` was manually set to `false`, and + /// `None` otherwise. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::parse("name=value; Secure").unwrap(); + /// assert_eq!(c.secure(), Some(true)); + /// + /// let mut c = Cookie::parse("name=value").unwrap(); + /// assert_eq!(c.secure(), None); + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.secure(), None); + /// + /// // An explicitly set "false" value. + /// c.set_secure(false); + /// assert_eq!(c.secure(), Some(false)); + /// + /// // An explicitly set "true" value. + /// c.set_secure(true); + /// assert_eq!(c.secure(), Some(true)); + /// ``` + #[inline] + pub fn secure(&self) -> Option { + self.secure + } + + /// Returns the `SameSite` attribute of this cookie if one was specified. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{Cookie, SameSite}; + /// + /// let c = Cookie::parse("name=value; SameSite=Lax").unwrap(); + /// assert_eq!(c.same_site(), Some(SameSite::Lax)); + /// ``` + #[inline] + pub fn same_site(&self) -> Option { + self.same_site + } + + /// Returns the specified max-age of the cookie if one was specified. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::parse("name=value").unwrap(); + /// assert_eq!(c.max_age(), None); + /// + /// let c = Cookie::parse("name=value; Max-Age=3600").unwrap(); + /// assert_eq!(c.max_age().map(|age| age.num_hours()), Some(1)); + /// ``` + #[inline] + pub fn max_age(&self) -> Option { + self.max_age + } + + /// Returns the `Path` of the cookie if one was specified. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::parse("name=value").unwrap(); + /// assert_eq!(c.path(), None); + /// + /// let c = Cookie::parse("name=value; Path=/").unwrap(); + /// assert_eq!(c.path(), Some("/")); + /// + /// let c = Cookie::parse("name=value; path=/sub").unwrap(); + /// assert_eq!(c.path(), Some("/sub")); + /// ``` + #[inline] + pub fn path(&self) -> Option<&str> { + match self.path { + Some(ref c) => Some(c.to_str(self.cookie_string.as_ref())), + None => None, + } + } + + /// Returns the `Domain` of the cookie if one was specified. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::parse("name=value").unwrap(); + /// assert_eq!(c.domain(), None); + /// + /// let c = Cookie::parse("name=value; Domain=crates.io").unwrap(); + /// assert_eq!(c.domain(), Some("crates.io")); + /// ``` + #[inline] + pub fn domain(&self) -> Option<&str> { + match self.domain { + Some(ref c) => Some(c.to_str(self.cookie_string.as_ref())), + None => None, + } + } + + /// Returns the `Expires` time of the cookie if one was specified. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let c = Cookie::parse("name=value").unwrap(); + /// assert_eq!(c.expires(), None); + /// + /// let expire_time = "Wed, 21 Oct 2017 07:28:00 GMT"; + /// let cookie_str = format!("name=value; Expires={}", expire_time); + /// let c = Cookie::parse(cookie_str).unwrap(); + /// assert_eq!(c.expires().map(|t| t.tm_year), Some(117)); + /// ``` + #[inline] + pub fn expires(&self) -> Option { + self.expires + } + + /// Sets the name of `self` to `name`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.name(), "name"); + /// + /// c.set_name("foo"); + /// assert_eq!(c.name(), "foo"); + /// ``` + pub fn set_name>>(&mut self, name: N) { + self.name = CookieStr::Concrete(name.into()) + } + + /// Sets the value of `self` to `value`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.value(), "value"); + /// + /// c.set_value("bar"); + /// assert_eq!(c.value(), "bar"); + /// ``` + pub fn set_value>>(&mut self, value: V) { + self.value = CookieStr::Concrete(value.into()) + } + + /// Sets the value of `http_only` in `self` to `value`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.http_only(), None); + /// + /// c.set_http_only(true); + /// assert_eq!(c.http_only(), Some(true)); + /// ``` + #[inline] + pub fn set_http_only(&mut self, value: bool) { + self.http_only = Some(value); + } + + /// Sets the value of `secure` in `self` to `value`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.secure(), None); + /// + /// c.set_secure(true); + /// assert_eq!(c.secure(), Some(true)); + /// ``` + #[inline] + pub fn set_secure(&mut self, value: bool) { + self.secure = Some(value); + } + + /// Sets the value of `same_site` in `self` to `value`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{Cookie, SameSite}; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert!(c.same_site().is_none()); + /// + /// c.set_same_site(SameSite::Strict); + /// assert_eq!(c.same_site(), Some(SameSite::Strict)); + /// ``` + #[inline] + pub fn set_same_site(&mut self, value: SameSite) { + self.same_site = Some(value); + } + + /// Sets the value of `max_age` in `self` to `value`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// use time::Duration; + /// + /// # fn main() { + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.max_age(), None); + /// + /// c.set_max_age(Duration::hours(10)); + /// assert_eq!(c.max_age(), Some(Duration::hours(10))); + /// # } + /// ``` + #[inline] + pub fn set_max_age(&mut self, value: Duration) { + self.max_age = Some(value); + } + + /// Sets the `path` of `self` to `path`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.path(), None); + /// + /// c.set_path("/"); + /// assert_eq!(c.path(), Some("/")); + /// ``` + pub fn set_path>>(&mut self, path: P) { + self.path = Some(CookieStr::Concrete(path.into())); + } + + /// Sets the `domain` of `self` to `domain`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.domain(), None); + /// + /// c.set_domain("rust-lang.org"); + /// assert_eq!(c.domain(), Some("rust-lang.org")); + /// ``` + pub fn set_domain>>(&mut self, domain: D) { + self.domain = Some(CookieStr::Concrete(domain.into())); + } + + /// Sets the expires field of `self` to `time`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// # fn main() { + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.expires(), None); + /// + /// let mut now = time::now(); + /// now.tm_year += 1; + /// + /// c.set_expires(now); + /// assert!(c.expires().is_some()) + /// # } + /// ``` + #[inline] + pub fn set_expires(&mut self, time: Tm) { + self.expires = Some(time); + } + + /// Makes `self` a "permanent" cookie by extending its expiration and max + /// age 20 years into the future. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// use time::Duration; + /// + /// # fn main() { + /// let mut c = Cookie::new("foo", "bar"); + /// assert!(c.expires().is_none()); + /// assert!(c.max_age().is_none()); + /// + /// c.make_permanent(); + /// assert!(c.expires().is_some()); + /// assert_eq!(c.max_age(), Some(Duration::days(365 * 20))); + /// # } + /// ``` + pub fn make_permanent(&mut self) { + let twenty_years = Duration::days(365 * 20); + self.set_max_age(twenty_years); + self.set_expires(time::now() + twenty_years); + } + + fn fmt_parameters(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(true) = self.http_only() { + write!(f, "; HttpOnly")?; + } + + if let Some(true) = self.secure() { + write!(f, "; Secure")?; + } + + if let Some(same_site) = self.same_site() { + if !same_site.is_none() { + write!(f, "; SameSite={}", same_site)?; + } + } + + if let Some(path) = self.path() { + write!(f, "; Path={}", path)?; + } + + if let Some(domain) = self.domain() { + write!(f, "; Domain={}", domain)?; + } + + if let Some(max_age) = self.max_age() { + write!(f, "; Max-Age={}", max_age.num_seconds())?; + } + + if let Some(time) = self.expires() { + write!(f, "; Expires={}", time.rfc822())?; + } + + Ok(()) + } + + /// Returns the name of `self` as a string slice of the raw string `self` + /// was originally parsed from. If `self` was not originally parsed from a + /// raw string, returns `None`. + /// + /// This method differs from [name](#method.name) in that it returns a + /// string with the same lifetime as the originally parsed string. This + /// lifetime may outlive `self`. If a longer lifetime is not required, or + /// you're unsure if you need a longer lifetime, use [name](#method.name). + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let cookie_string = format!("{}={}", "foo", "bar"); + /// + /// // `c` will be dropped at the end of the scope, but `name` will live on + /// let name = { + /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); + /// c.name_raw() + /// }; + /// + /// assert_eq!(name, Some("foo")); + /// ``` + #[inline] + pub fn name_raw(&self) -> Option<&'c str> { + self.cookie_string + .as_ref() + .and_then(|s| self.name.to_raw_str(s)) + } + + /// Returns the value of `self` as a string slice of the raw string `self` + /// was originally parsed from. If `self` was not originally parsed from a + /// raw string, returns `None`. + /// + /// This method differs from [value](#method.value) in that it returns a + /// string with the same lifetime as the originally parsed string. This + /// lifetime may outlive `self`. If a longer lifetime is not required, or + /// you're unsure if you need a longer lifetime, use [value](#method.value). + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let cookie_string = format!("{}={}", "foo", "bar"); + /// + /// // `c` will be dropped at the end of the scope, but `value` will live on + /// let value = { + /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); + /// c.value_raw() + /// }; + /// + /// assert_eq!(value, Some("bar")); + /// ``` + #[inline] + pub fn value_raw(&self) -> Option<&'c str> { + self.cookie_string + .as_ref() + .and_then(|s| self.value.to_raw_str(s)) + } + + /// Returns the `Path` of `self` as a string slice of the raw string `self` + /// was originally parsed from. If `self` was not originally parsed from a + /// raw string, or if `self` doesn't contain a `Path`, or if the `Path` has + /// changed since parsing, returns `None`. + /// + /// This method differs from [path](#method.path) in that it returns a + /// string with the same lifetime as the originally parsed string. This + /// lifetime may outlive `self`. If a longer lifetime is not required, or + /// you're unsure if you need a longer lifetime, use [path](#method.path). + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let cookie_string = format!("{}={}; Path=/", "foo", "bar"); + /// + /// // `c` will be dropped at the end of the scope, but `path` will live on + /// let path = { + /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); + /// c.path_raw() + /// }; + /// + /// assert_eq!(path, Some("/")); + /// ``` + #[inline] + pub fn path_raw(&self) -> Option<&'c str> { + match (self.path.as_ref(), self.cookie_string.as_ref()) { + (Some(path), Some(string)) => path.to_raw_str(string), + _ => None, + } + } + + /// Returns the `Domain` of `self` as a string slice of the raw string + /// `self` was originally parsed from. If `self` was not originally parsed + /// from a raw string, or if `self` doesn't contain a `Domain`, or if the + /// `Domain` has changed since parsing, returns `None`. + /// + /// This method differs from [domain](#method.domain) in that it returns a + /// string with the same lifetime as the originally parsed string. This + /// lifetime may outlive `self` struct. If a longer lifetime is not + /// required, or you're unsure if you need a longer lifetime, use + /// [domain](#method.domain). + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let cookie_string = format!("{}={}; Domain=crates.io", "foo", "bar"); + /// + /// //`c` will be dropped at the end of the scope, but `domain` will live on + /// let domain = { + /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); + /// c.domain_raw() + /// }; + /// + /// assert_eq!(domain, Some("crates.io")); + /// ``` + #[inline] + pub fn domain_raw(&self) -> Option<&'c str> { + match (self.domain.as_ref(), self.cookie_string.as_ref()) { + (Some(domain), Some(string)) => domain.to_raw_str(string), + _ => None, + } + } +} + +/// Wrapper around `Cookie` whose `Display` implementation percent-encodes the +/// cookie's name and value. +/// +/// A value of this type can be obtained via the +/// [encoded](struct.Cookie.html#method.encoded) method on +/// [Cookie](struct.Cookie.html). This type should only be used for its +/// `Display` implementation. +/// +/// This type is only available when the `percent-encode` feature is enabled. +/// +/// # Example +/// +/// ```rust +/// use actix_http::cookie::Cookie; +/// +/// let mut c = Cookie::new("my name", "this; value?"); +/// assert_eq!(&c.encoded().to_string(), "my%20name=this%3B%20value%3F"); +/// ``` +pub struct EncodedCookie<'a, 'c: 'a>(&'a Cookie<'c>); + +impl<'a, 'c: 'a> fmt::Display for EncodedCookie<'a, 'c> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Percent-encode the name and value. + let name = percent_encode(self.0.name().as_bytes(), USERINFO_ENCODE_SET); + let value = percent_encode(self.0.value().as_bytes(), USERINFO_ENCODE_SET); + + // Write out the name/value pair and the cookie's parameters. + write!(f, "{}={}", name, value)?; + self.0.fmt_parameters(f) + } +} + +impl<'c> fmt::Display for Cookie<'c> { + /// Formats the cookie `self` as a `Set-Cookie` header value. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// let mut cookie = Cookie::build("foo", "bar") + /// .path("/") + /// .finish(); + /// + /// assert_eq!(&cookie.to_string(), "foo=bar; Path=/"); + /// ``` + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}={}", self.name(), self.value())?; + self.fmt_parameters(f) + } +} + +impl FromStr for Cookie<'static> { + type Err = ParseError; + + fn from_str(s: &str) -> Result, ParseError> { + Cookie::parse(s).map(|c| c.into_owned()) + } +} + +impl<'a, 'b> PartialEq> for Cookie<'a> { + fn eq(&self, other: &Cookie<'b>) -> bool { + let so_far_so_good = self.name() == other.name() + && self.value() == other.value() + && self.http_only() == other.http_only() + && self.secure() == other.secure() + && self.max_age() == other.max_age() + && self.expires() == other.expires(); + + if !so_far_so_good { + return false; + } + + match (self.path(), other.path()) { + (Some(a), Some(b)) if a.eq_ignore_ascii_case(b) => {} + (None, None) => {} + _ => return false, + }; + + match (self.domain(), other.domain()) { + (Some(a), Some(b)) if a.eq_ignore_ascii_case(b) => {} + (None, None) => {} + _ => return false, + }; + + true + } +} + +#[cfg(test)] +mod tests { + use super::{Cookie, SameSite}; + use time::{strptime, Duration}; + + #[test] + fn format() { + let cookie = Cookie::new("foo", "bar"); + assert_eq!(&cookie.to_string(), "foo=bar"); + + let cookie = Cookie::build("foo", "bar").http_only(true).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; HttpOnly"); + + let cookie = Cookie::build("foo", "bar") + .max_age(Duration::seconds(10)) + .finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Max-Age=10"); + + let cookie = Cookie::build("foo", "bar").secure(true).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Secure"); + + let cookie = Cookie::build("foo", "bar").path("/").finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Path=/"); + + let cookie = Cookie::build("foo", "bar") + .domain("www.rust-lang.org") + .finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Domain=www.rust-lang.org"); + + let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; + let expires = strptime(time_str, "%a, %d %b %Y %H:%M:%S %Z").unwrap(); + let cookie = Cookie::build("foo", "bar").expires(expires).finish(); + assert_eq!( + &cookie.to_string(), + "foo=bar; Expires=Wed, 21 Oct 2015 07:28:00 GMT" + ); + + let cookie = Cookie::build("foo", "bar") + .same_site(SameSite::Strict) + .finish(); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Strict"); + + let cookie = Cookie::build("foo", "bar") + .same_site(SameSite::Lax) + .finish(); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Lax"); + + let cookie = Cookie::build("foo", "bar") + .same_site(SameSite::None) + .finish(); + assert_eq!(&cookie.to_string(), "foo=bar"); + } + + #[test] + fn cookie_string_long_lifetimes() { + let cookie_string = + "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io".to_owned(); + let (name, value, path, domain) = { + // Create a cookie passing a slice + let c = Cookie::parse(cookie_string.as_str()).unwrap(); + (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) + }; + + assert_eq!(name, Some("bar")); + assert_eq!(value, Some("baz")); + assert_eq!(path, Some("/subdir")); + assert_eq!(domain, Some("crates.io")); + } + + #[test] + fn owned_cookie_string() { + let cookie_string = + "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io".to_owned(); + let (name, value, path, domain) = { + // Create a cookie passing an owned string + let c = Cookie::parse(cookie_string).unwrap(); + (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) + }; + + assert_eq!(name, None); + assert_eq!(value, None); + assert_eq!(path, None); + assert_eq!(domain, None); + } + + #[test] + fn owned_cookie_struct() { + let cookie_string = "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io"; + let (name, value, path, domain) = { + // Create an owned cookie + let c = Cookie::parse(cookie_string).unwrap().into_owned(); + + (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) + }; + + assert_eq!(name, None); + assert_eq!(value, None); + assert_eq!(path, None); + assert_eq!(domain, None); + } + + #[test] + fn format_encoded() { + let cookie = Cookie::build("foo !?=", "bar;; a").finish(); + let cookie_str = cookie.encoded().to_string(); + assert_eq!(&cookie_str, "foo%20!%3F%3D=bar%3B%3B%20a"); + + let cookie = Cookie::parse_encoded(cookie_str).unwrap(); + assert_eq!(cookie.name_value(), ("foo !?=", "bar;; a")); + } +} diff --git a/actix-http/src/cookie/parse.rs b/actix-http/src/cookie/parse.rs new file mode 100644 index 000000000..ebbd6ffc9 --- /dev/null +++ b/actix-http/src/cookie/parse.rs @@ -0,0 +1,426 @@ +use std::borrow::Cow; +use std::cmp; +use std::convert::From; +use std::error::Error; +use std::fmt; +use std::str::Utf8Error; + +use percent_encoding::percent_decode; +use time::{self, Duration}; + +use super::{Cookie, CookieStr, SameSite}; + +/// Enum corresponding to a parsing error. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum ParseError { + /// The cookie did not contain a name/value pair. + MissingPair, + /// The cookie's name was empty. + EmptyName, + /// Decoding the cookie's name or value resulted in invalid UTF-8. + Utf8Error(Utf8Error), + /// It is discouraged to exhaustively match on this enum as its variants may + /// grow without a breaking-change bump in version numbers. + #[doc(hidden)] + __Nonexhasutive, +} + +impl ParseError { + /// Returns a description of this error as a string + pub fn as_str(&self) -> &'static str { + match *self { + ParseError::MissingPair => "the cookie is missing a name/value pair", + ParseError::EmptyName => "the cookie's name is empty", + ParseError::Utf8Error(_) => { + "decoding the cookie's name or value resulted in invalid UTF-8" + } + ParseError::__Nonexhasutive => unreachable!("__Nonexhasutive ParseError"), + } + } +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl From for ParseError { + fn from(error: Utf8Error) -> ParseError { + ParseError::Utf8Error(error) + } +} + +impl Error for ParseError { + fn description(&self) -> &str { + self.as_str() + } +} + +fn indexes_of(needle: &str, haystack: &str) -> Option<(usize, usize)> { + let haystack_start = haystack.as_ptr() as usize; + let needle_start = needle.as_ptr() as usize; + + if needle_start < haystack_start { + return None; + } + + if (needle_start + needle.len()) > (haystack_start + haystack.len()) { + return None; + } + + let start = needle_start - haystack_start; + let end = start + needle.len(); + Some((start, end)) +} + +fn name_val_decoded( + name: &str, + val: &str, +) -> Result<(CookieStr, CookieStr), ParseError> { + let decoded_name = percent_decode(name.as_bytes()).decode_utf8()?; + let decoded_value = percent_decode(val.as_bytes()).decode_utf8()?; + let name = CookieStr::Concrete(Cow::Owned(decoded_name.into_owned())); + let val = CookieStr::Concrete(Cow::Owned(decoded_value.into_owned())); + + Ok((name, val)) +} + +// This function does the real parsing but _does not_ set the `cookie_string` in +// the returned cookie object. This only exists so that the borrow to `s` is +// returned at the end of the call, allowing the `cookie_string` field to be +// set in the outer `parse` function. +fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { + let mut attributes = s.split(';'); + let key_value = match attributes.next() { + Some(s) => s, + _ => panic!(), + }; + + // Determine the name = val. + let (name, value) = match key_value.find('=') { + Some(i) => (key_value[..i].trim(), key_value[(i + 1)..].trim()), + None => return Err(ParseError::MissingPair), + }; + + if name.is_empty() { + return Err(ParseError::EmptyName); + } + + // Create a cookie with all of the defaults. We'll fill things in while we + // iterate through the parameters below. + let (name, value) = if decode { + name_val_decoded(name, value)? + } else { + let name_indexes = indexes_of(name, s).expect("name sub"); + let value_indexes = indexes_of(value, s).expect("value sub"); + let name = CookieStr::Indexed(name_indexes.0, name_indexes.1); + let value = CookieStr::Indexed(value_indexes.0, value_indexes.1); + + (name, value) + }; + + let mut cookie = Cookie { + name, + value, + cookie_string: None, + expires: None, + max_age: None, + domain: None, + path: None, + secure: None, + http_only: None, + same_site: None, + }; + + for attr in attributes { + let (key, value) = match attr.find('=') { + Some(i) => (attr[..i].trim(), Some(attr[(i + 1)..].trim())), + None => (attr.trim(), None), + }; + + match (&*key.to_ascii_lowercase(), value) { + ("secure", _) => cookie.secure = Some(true), + ("httponly", _) => cookie.http_only = Some(true), + ("max-age", Some(v)) => { + // See RFC 6265 Section 5.2.2, negative values indicate that the + // earliest possible expiration time should be used, so set the + // max age as 0 seconds. + cookie.max_age = match v.parse() { + Ok(val) if val <= 0 => Some(Duration::zero()), + Ok(val) => { + // Don't panic if the max age seconds is greater than what's supported by + // `Duration`. + let val = cmp::min(val, Duration::max_value().num_seconds()); + Some(Duration::seconds(val)) + } + Err(_) => continue, + }; + } + ("domain", Some(mut domain)) if !domain.is_empty() => { + if domain.starts_with('.') { + domain = &domain[1..]; + } + + let (i, j) = indexes_of(domain, s).expect("domain sub"); + cookie.domain = Some(CookieStr::Indexed(i, j)); + } + ("path", Some(v)) => { + let (i, j) = indexes_of(v, s).expect("path sub"); + cookie.path = Some(CookieStr::Indexed(i, j)); + } + ("samesite", Some(v)) => { + if v.eq_ignore_ascii_case("strict") { + cookie.same_site = Some(SameSite::Strict); + } else if v.eq_ignore_ascii_case("lax") { + cookie.same_site = Some(SameSite::Lax); + } else { + // We do nothing here, for now. When/if the `SameSite` + // attribute becomes standard, the spec says that we should + // ignore this cookie, i.e, fail to parse it, when an + // invalid value is passed in. The draft is at + // http://httpwg.org/http-extensions/draft-ietf-httpbis-cookie-same-site.html. + } + } + ("expires", Some(v)) => { + // Try strptime with three date formats according to + // http://tools.ietf.org/html/rfc2616#section-3.3.1. Try + // additional ones as encountered in the real world. + let tm = time::strptime(v, "%a, %d %b %Y %H:%M:%S %Z") + .or_else(|_| time::strptime(v, "%A, %d-%b-%y %H:%M:%S %Z")) + .or_else(|_| time::strptime(v, "%a, %d-%b-%Y %H:%M:%S %Z")) + .or_else(|_| time::strptime(v, "%a %b %d %H:%M:%S %Y")); + + if let Ok(time) = tm { + cookie.expires = Some(time) + } + } + _ => { + // We're going to be permissive here. If we have no idea what + // this is, then it's something nonstandard. We're not going to + // store it (because it's not compliant), but we're also not + // going to emit an error. + } + } + } + + Ok(cookie) +} + +pub fn parse_cookie<'c, S>(cow: S, decode: bool) -> Result, ParseError> +where + S: Into>, +{ + let s = cow.into(); + let mut cookie = parse_inner(&s, decode)?; + cookie.cookie_string = Some(s); + Ok(cookie) +} + +#[cfg(test)] +mod tests { + use super::{Cookie, SameSite}; + use time::{strptime, Duration}; + + macro_rules! assert_eq_parse { + ($string:expr, $expected:expr) => { + let cookie = match Cookie::parse($string) { + Ok(cookie) => cookie, + Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e), + }; + + assert_eq!(cookie, $expected); + }; + } + + macro_rules! assert_ne_parse { + ($string:expr, $expected:expr) => { + let cookie = match Cookie::parse($string) { + Ok(cookie) => cookie, + Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e), + }; + + assert_ne!(cookie, $expected); + }; + } + + #[test] + fn parse_same_site() { + let expected = Cookie::build("foo", "bar") + .same_site(SameSite::Lax) + .finish(); + + assert_eq_parse!("foo=bar; SameSite=Lax", expected); + assert_eq_parse!("foo=bar; SameSite=lax", expected); + assert_eq_parse!("foo=bar; SameSite=LAX", expected); + assert_eq_parse!("foo=bar; samesite=Lax", expected); + assert_eq_parse!("foo=bar; SAMESITE=Lax", expected); + + let expected = Cookie::build("foo", "bar") + .same_site(SameSite::Strict) + .finish(); + + assert_eq_parse!("foo=bar; SameSite=Strict", expected); + assert_eq_parse!("foo=bar; SameSITE=Strict", expected); + assert_eq_parse!("foo=bar; SameSite=strict", expected); + assert_eq_parse!("foo=bar; SameSite=STrICT", expected); + assert_eq_parse!("foo=bar; SameSite=STRICT", expected); + } + + #[test] + fn parse() { + assert!(Cookie::parse("bar").is_err()); + assert!(Cookie::parse("=bar").is_err()); + assert!(Cookie::parse(" =bar").is_err()); + assert!(Cookie::parse("foo=").is_ok()); + + let expected = Cookie::build("foo", "bar=baz").finish(); + assert_eq_parse!("foo=bar=baz", expected); + + let mut expected = Cookie::build("foo", "bar").finish(); + assert_eq_parse!("foo=bar", expected); + assert_eq_parse!("foo = bar", expected); + assert_eq_parse!(" foo=bar ", expected); + assert_eq_parse!(" foo=bar ;Domain=", expected); + assert_eq_parse!(" foo=bar ;Domain= ", expected); + assert_eq_parse!(" foo=bar ;Ignored", expected); + + let mut unexpected = Cookie::build("foo", "bar").http_only(false).finish(); + assert_ne_parse!(" foo=bar ;HttpOnly", unexpected); + assert_ne_parse!(" foo=bar; httponly", unexpected); + + expected.set_http_only(true); + assert_eq_parse!(" foo=bar ;HttpOnly", expected); + assert_eq_parse!(" foo=bar ;httponly", expected); + assert_eq_parse!(" foo=bar ;HTTPONLY=whatever", expected); + assert_eq_parse!(" foo=bar ; sekure; HTTPONLY", expected); + + expected.set_secure(true); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure", expected); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure=aaaa", expected); + + unexpected.set_http_only(true); + unexpected.set_secure(true); + assert_ne_parse!(" foo=bar ;HttpOnly; skeure", unexpected); + assert_ne_parse!(" foo=bar ;HttpOnly; =secure", unexpected); + assert_ne_parse!(" foo=bar ;HttpOnly;", unexpected); + + unexpected.set_secure(false); + assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected); + assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected); + assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected); + + expected.set_max_age(Duration::zero()); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=0", expected); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0 ", expected); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=-1", expected); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", expected); + + expected.set_max_age(Duration::minutes(1)); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=60", expected); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 60 ", expected); + + expected.set_max_age(Duration::seconds(4)); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4", expected); + assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 4 ", expected); + + unexpected.set_secure(true); + unexpected.set_max_age(Duration::minutes(1)); + assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=122", unexpected); + assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 38 ", unexpected); + assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=51", unexpected); + assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", unexpected); + assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0", unexpected); + + expected.set_path("/"); + assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/", expected); + assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/", expected); + + expected.set_path("/foo"); + assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", expected); + assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/foo", expected); + assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path=/foo", expected); + assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path = /foo", expected); + + unexpected.set_max_age(Duration::seconds(4)); + unexpected.set_path("/bar"); + assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", unexpected); + assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/baz", unexpected); + + expected.set_domain("www.foo.com"); + assert_eq_parse!( + " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ + Domain=www.foo.com", + expected + ); + + expected.set_domain("foo.com"); + assert_eq_parse!( + " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ + Domain=foo.com", + expected + ); + assert_eq_parse!( + " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ + Domain=FOO.COM", + expected + ); + + unexpected.set_path("/foo"); + unexpected.set_domain("bar.com"); + assert_ne_parse!( + " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ + Domain=foo.com", + unexpected + ); + assert_ne_parse!( + " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ + Domain=FOO.COM", + unexpected + ); + + let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; + let expires = strptime(time_str, "%a, %d %b %Y %H:%M:%S %Z").unwrap(); + expected.set_expires(expires); + assert_eq_parse!( + " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ + Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", + expected + ); + + unexpected.set_domain("foo.com"); + let bad_expires = strptime(time_str, "%a, %d %b %Y %H:%S:%M %Z").unwrap(); + expected.set_expires(bad_expires); + assert_ne_parse!( + " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ + Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", + unexpected + ); + } + + #[test] + fn odd_characters() { + let expected = Cookie::new("foo", "b%2Fr"); + assert_eq_parse!("foo=b%2Fr", expected); + } + + #[test] + fn odd_characters_encoded() { + let expected = Cookie::new("foo", "b/r"); + let cookie = match Cookie::parse_encoded("foo=b%2Fr") { + Ok(cookie) => cookie, + Err(e) => panic!("Failed to parse: {:?}", e), + }; + + assert_eq!(cookie, expected); + } + + #[test] + fn do_not_panic_on_large_max_ages() { + let max_seconds = Duration::max_value().num_seconds(); + let expected = Cookie::build("foo", "bar") + .max_age(Duration::seconds(max_seconds)) + .finish(); + assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", max_seconds + 1), expected); + } +} diff --git a/actix-http/src/cookie/secure/key.rs b/actix-http/src/cookie/secure/key.rs new file mode 100644 index 000000000..3b8a0af78 --- /dev/null +++ b/actix-http/src/cookie/secure/key.rs @@ -0,0 +1,180 @@ +use ring::digest::{Algorithm, SHA256}; +use ring::hkdf::expand; +use ring::hmac::SigningKey; +use ring::rand::{SecureRandom, SystemRandom}; + +use super::private::KEY_LEN as PRIVATE_KEY_LEN; +use super::signed::KEY_LEN as SIGNED_KEY_LEN; + +static HKDF_DIGEST: &'static Algorithm = &SHA256; +const KEYS_INFO: &'static str = "COOKIE;SIGNED:HMAC-SHA256;PRIVATE:AEAD-AES-256-GCM"; + +/// A cryptographic master key for use with `Signed` and/or `Private` jars. +/// +/// This structure encapsulates secure, cryptographic keys for use with both +/// [PrivateJar](struct.PrivateJar.html) and [SignedJar](struct.SignedJar.html). +/// It can be derived from a single master key via +/// [from_master](#method.from_master) or generated from a secure random source +/// via [generate](#method.generate). A single instance of `Key` can be used for +/// both a `PrivateJar` and a `SignedJar`. +/// +/// This type is only available when the `secure` feature is enabled. +#[derive(Clone)] +pub struct Key { + signing_key: [u8; SIGNED_KEY_LEN], + encryption_key: [u8; PRIVATE_KEY_LEN], +} + +impl Key { + /// Derives new signing/encryption keys from a master key. + /// + /// The master key must be at least 256-bits (32 bytes). For security, the + /// master key _must_ be cryptographically random. The keys are derived + /// deterministically from the master key. + /// + /// # Panics + /// + /// Panics if `key` is less than 32 bytes in length. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Key; + /// + /// # /* + /// let master_key = { /* a cryptographically random key >= 32 bytes */ }; + /// # */ + /// # let master_key: &Vec = &(0..32).collect(); + /// + /// let key = Key::from_master(master_key); + /// ``` + pub fn from_master(key: &[u8]) -> Key { + if key.len() < 32 { + panic!( + "bad master key length: expected at least 32 bytes, found {}", + key.len() + ); + } + + // Expand the user's key into two. + let prk = SigningKey::new(HKDF_DIGEST, key); + let mut both_keys = [0; SIGNED_KEY_LEN + PRIVATE_KEY_LEN]; + expand(&prk, KEYS_INFO.as_bytes(), &mut both_keys); + + // Copy the keys into their respective arrays. + let mut signing_key = [0; SIGNED_KEY_LEN]; + let mut encryption_key = [0; PRIVATE_KEY_LEN]; + signing_key.copy_from_slice(&both_keys[..SIGNED_KEY_LEN]); + encryption_key.copy_from_slice(&both_keys[SIGNED_KEY_LEN..]); + + Key { + signing_key: signing_key, + encryption_key: encryption_key, + } + } + + /// Generates signing/encryption keys from a secure, random source. Keys are + /// generated nondeterministically. + /// + /// # Panics + /// + /// Panics if randomness cannot be retrieved from the operating system. See + /// [try_generate](#method.try_generate) for a non-panicking version. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Key; + /// + /// let key = Key::generate(); + /// ``` + pub fn generate() -> Key { + Self::try_generate().expect("failed to generate `Key` from randomness") + } + + /// Attempts to generate signing/encryption keys from a secure, random + /// source. Keys are generated nondeterministically. If randomness cannot be + /// retrieved from the underlying operating system, returns `None`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Key; + /// + /// let key = Key::try_generate(); + /// ``` + pub fn try_generate() -> Option { + let mut sign_key = [0; SIGNED_KEY_LEN]; + let mut enc_key = [0; PRIVATE_KEY_LEN]; + + let rng = SystemRandom::new(); + if rng.fill(&mut sign_key).is_err() || rng.fill(&mut enc_key).is_err() { + return None; + } + + Some(Key { + signing_key: sign_key, + encryption_key: enc_key, + }) + } + + /// Returns the raw bytes of a key suitable for signing cookies. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Key; + /// + /// let key = Key::generate(); + /// let signing_key = key.signing(); + /// ``` + pub fn signing(&self) -> &[u8] { + &self.signing_key[..] + } + + /// Returns the raw bytes of a key suitable for encrypting cookies. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Key; + /// + /// let key = Key::generate(); + /// let encryption_key = key.encryption(); + /// ``` + pub fn encryption(&self) -> &[u8] { + &self.encryption_key[..] + } +} + +#[cfg(test)] +mod test { + use super::Key; + + #[test] + fn deterministic_from_master() { + let master_key: Vec = (0..32).collect(); + + let key_a = Key::from_master(&master_key); + let key_b = Key::from_master(&master_key); + + assert_eq!(key_a.signing(), key_b.signing()); + assert_eq!(key_a.encryption(), key_b.encryption()); + assert_ne!(key_a.encryption(), key_a.signing()); + + let master_key_2: Vec = (32..64).collect(); + let key_2 = Key::from_master(&master_key_2); + + assert_ne!(key_2.signing(), key_a.signing()); + assert_ne!(key_2.encryption(), key_a.encryption()); + } + + #[test] + fn non_deterministic_generate() { + let key_a = Key::generate(); + let key_b = Key::generate(); + + assert_ne!(key_a.signing(), key_b.signing()); + assert_ne!(key_a.encryption(), key_b.encryption()); + } +} diff --git a/actix-http/src/cookie/secure/macros.rs b/actix-http/src/cookie/secure/macros.rs new file mode 100644 index 000000000..089047c4e --- /dev/null +++ b/actix-http/src/cookie/secure/macros.rs @@ -0,0 +1,40 @@ +#[cfg(test)] +macro_rules! assert_simple_behaviour { + ($clear:expr, $secure:expr) => {{ + assert_eq!($clear.iter().count(), 0); + + $secure.add(Cookie::new("name", "val")); + assert_eq!($clear.iter().count(), 1); + assert_eq!($secure.get("name").unwrap().value(), "val"); + assert_ne!($clear.get("name").unwrap().value(), "val"); + + $secure.add(Cookie::new("another", "two")); + assert_eq!($clear.iter().count(), 2); + + $clear.remove(Cookie::named("another")); + assert_eq!($clear.iter().count(), 1); + + $secure.remove(Cookie::named("name")); + assert_eq!($clear.iter().count(), 0); + }}; +} + +#[cfg(test)] +macro_rules! assert_secure_behaviour { + ($clear:expr, $secure:expr) => {{ + $secure.add(Cookie::new("secure", "secure")); + assert!($clear.get("secure").unwrap().value() != "secure"); + assert!($secure.get("secure").unwrap().value() == "secure"); + + let mut cookie = $clear.get("secure").unwrap().clone(); + let new_val = format!("{}l", cookie.value()); + cookie.set_value(new_val); + $clear.add(cookie); + assert!($secure.get("secure").is_none()); + + let mut cookie = $clear.get("secure").unwrap().clone(); + cookie.set_value("foobar"); + $clear.add(cookie); + assert!($secure.get("secure").is_none()); + }}; +} diff --git a/actix-http/src/cookie/secure/mod.rs b/actix-http/src/cookie/secure/mod.rs new file mode 100644 index 000000000..e0fba9733 --- /dev/null +++ b/actix-http/src/cookie/secure/mod.rs @@ -0,0 +1,10 @@ +//! Fork of https://github.com/alexcrichton/cookie-rs +#[macro_use] +mod macros; +mod key; +mod private; +mod signed; + +pub use self::key::*; +pub use self::private::*; +pub use self::signed::*; diff --git a/actix-http/src/cookie/secure/private.rs b/actix-http/src/cookie/secure/private.rs new file mode 100644 index 000000000..8b56991f1 --- /dev/null +++ b/actix-http/src/cookie/secure/private.rs @@ -0,0 +1,226 @@ +use ring::aead::{open_in_place, seal_in_place, Aad, Algorithm, Nonce, AES_256_GCM}; +use ring::aead::{OpeningKey, SealingKey}; +use ring::rand::{SecureRandom, SystemRandom}; + +use super::Key; +use crate::cookie::{Cookie, CookieJar}; + +// Keep these in sync, and keep the key len synced with the `private` docs as +// well as the `KEYS_INFO` const in secure::Key. +static ALGO: &'static Algorithm = &AES_256_GCM; +const NONCE_LEN: usize = 12; +pub const KEY_LEN: usize = 32; + +/// A child cookie jar that provides authenticated encryption for its cookies. +/// +/// A _private_ child jar signs and encrypts all the cookies added to it and +/// verifies and decrypts cookies retrieved from it. Any cookies stored in a +/// `PrivateJar` are simultaneously assured confidentiality, integrity, and +/// authenticity. In other words, clients cannot discover nor tamper with the +/// contents of a cookie, nor can they fabricate cookie data. +/// +/// This type is only available when the `secure` feature is enabled. +pub struct PrivateJar<'a> { + parent: &'a mut CookieJar, + key: [u8; KEY_LEN], +} + +impl<'a> PrivateJar<'a> { + /// Creates a new child `PrivateJar` with parent `parent` and key `key`. + /// This method is typically called indirectly via the `signed` method of + /// `CookieJar`. + #[doc(hidden)] + pub fn new(parent: &'a mut CookieJar, key: &Key) -> PrivateJar<'a> { + let mut key_array = [0u8; KEY_LEN]; + key_array.copy_from_slice(key.encryption()); + PrivateJar { + parent: parent, + key: key_array, + } + } + + /// Given a sealed value `str` and a key name `name`, where the nonce is + /// prepended to the original value and then both are Base64 encoded, + /// verifies and decrypts the sealed value and returns it. If there's a + /// problem, returns an `Err` with a string describing the issue. + fn unseal(&self, name: &str, value: &str) -> Result { + let mut data = base64::decode(value).map_err(|_| "bad base64 value")?; + if data.len() <= NONCE_LEN { + return Err("length of decoded data is <= NONCE_LEN"); + } + + let ad = Aad::from(name.as_bytes()); + let key = OpeningKey::new(ALGO, &self.key).expect("opening key"); + let (nonce, sealed) = data.split_at_mut(NONCE_LEN); + let nonce = + Nonce::try_assume_unique_for_key(nonce).expect("invalid length of `nonce`"); + let unsealed = open_in_place(&key, nonce, ad, 0, sealed) + .map_err(|_| "invalid key/nonce/value: bad seal")?; + + ::std::str::from_utf8(unsealed) + .map(|s| s.to_string()) + .map_err(|_| "bad unsealed utf8") + } + + /// Returns a reference to the `Cookie` inside this jar with the name `name` + /// and authenticates and decrypts the cookie's value, returning a `Cookie` + /// with the decrypted value. If the cookie cannot be found, or the cookie + /// fails to authenticate or decrypt, `None` is returned. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// let mut private_jar = jar.private(&key); + /// assert!(private_jar.get("name").is_none()); + /// + /// private_jar.add(Cookie::new("name", "value")); + /// assert_eq!(private_jar.get("name").unwrap().value(), "value"); + /// ``` + pub fn get(&self, name: &str) -> Option> { + if let Some(cookie_ref) = self.parent.get(name) { + let mut cookie = cookie_ref.clone(); + if let Ok(value) = self.unseal(name, cookie.value()) { + cookie.set_value(value); + return Some(cookie); + } + } + + None + } + + /// Adds `cookie` to the parent jar. The cookie's value is encrypted with + /// authenticated encryption assuring confidentiality, integrity, and + /// authenticity. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// jar.private(&key).add(Cookie::new("name", "value")); + /// + /// assert_ne!(jar.get("name").unwrap().value(), "value"); + /// assert_eq!(jar.private(&key).get("name").unwrap().value(), "value"); + /// ``` + pub fn add(&mut self, mut cookie: Cookie<'static>) { + self.encrypt_cookie(&mut cookie); + + // Add the sealed cookie to the parent. + self.parent.add(cookie); + } + + /// Adds an "original" `cookie` to parent jar. The cookie's value is + /// encrypted with authenticated encryption assuring confidentiality, + /// integrity, and authenticity. Adding an original cookie does not affect + /// the [`CookieJar::delta()`](struct.CookieJar.html#method.delta) + /// computation. This method is intended to be used to seed the cookie jar + /// with cookies received from a client's HTTP message. + /// + /// For accurate `delta` computations, this method should not be called + /// after calling `remove`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// jar.private(&key).add_original(Cookie::new("name", "value")); + /// + /// assert_eq!(jar.iter().count(), 1); + /// assert_eq!(jar.delta().count(), 0); + /// ``` + pub fn add_original(&mut self, mut cookie: Cookie<'static>) { + self.encrypt_cookie(&mut cookie); + + // Add the sealed cookie to the parent. + self.parent.add_original(cookie); + } + + /// Encrypts the cookie's value with + /// authenticated encryption assuring confidentiality, integrity, and authenticity. + fn encrypt_cookie(&self, cookie: &mut Cookie) { + let mut data; + let output_len = { + // Create the `SealingKey` structure. + let key = SealingKey::new(ALGO, &self.key).expect("sealing key creation"); + + // Create a vec to hold the [nonce | cookie value | overhead]. + let overhead = ALGO.tag_len(); + let cookie_val = cookie.value().as_bytes(); + data = vec![0; NONCE_LEN + cookie_val.len() + overhead]; + + // Randomly generate the nonce, then copy the cookie value as input. + let (nonce, in_out) = data.split_at_mut(NONCE_LEN); + SystemRandom::new() + .fill(nonce) + .expect("couldn't random fill nonce"); + in_out[..cookie_val.len()].copy_from_slice(cookie_val); + let nonce = Nonce::try_assume_unique_for_key(nonce) + .expect("invalid length of `nonce`"); + + // Use cookie's name as associated data to prevent value swapping. + let ad = Aad::from(cookie.name().as_bytes()); + + // Perform the actual sealing operation and get the output length. + seal_in_place(&key, nonce, ad, in_out, overhead).expect("in-place seal") + }; + + // Base64 encode the nonce and encrypted value. + let sealed_value = base64::encode(&data[..(NONCE_LEN + output_len)]); + cookie.set_value(sealed_value); + } + + /// Removes `cookie` from the parent jar. + /// + /// For correct removal, the passed in `cookie` must contain the same `path` + /// and `domain` as the cookie that was initially set. + /// + /// See [CookieJar::remove](struct.CookieJar.html#method.remove) for more + /// details. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// let mut private_jar = jar.private(&key); + /// + /// private_jar.add(Cookie::new("name", "value")); + /// assert!(private_jar.get("name").is_some()); + /// + /// private_jar.remove(Cookie::named("name")); + /// assert!(private_jar.get("name").is_none()); + /// ``` + pub fn remove(&mut self, cookie: Cookie<'static>) { + self.parent.remove(cookie); + } +} + +#[cfg(test)] +mod test { + use super::{Cookie, CookieJar, Key}; + + #[test] + fn simple() { + let key = Key::generate(); + let mut jar = CookieJar::new(); + assert_simple_behaviour!(jar, jar.private(&key)); + } + + #[test] + fn private() { + let key = Key::generate(); + let mut jar = CookieJar::new(); + assert_secure_behaviour!(jar, jar.private(&key)); + } +} diff --git a/actix-http/src/cookie/secure/signed.rs b/actix-http/src/cookie/secure/signed.rs new file mode 100644 index 000000000..5a4ffb76c --- /dev/null +++ b/actix-http/src/cookie/secure/signed.rs @@ -0,0 +1,185 @@ +use ring::digest::{Algorithm, SHA256}; +use ring::hmac::{sign, verify_with_own_key as verify, SigningKey}; + +use super::Key; +use crate::cookie::{Cookie, CookieJar}; + +// Keep these in sync, and keep the key len synced with the `signed` docs as +// well as the `KEYS_INFO` const in secure::Key. +static HMAC_DIGEST: &'static Algorithm = &SHA256; +const BASE64_DIGEST_LEN: usize = 44; +pub const KEY_LEN: usize = 32; + +/// A child cookie jar that authenticates its cookies. +/// +/// A _signed_ child jar signs all the cookies added to it and verifies cookies +/// retrieved from it. Any cookies stored in a `SignedJar` are assured integrity +/// and authenticity. In other words, clients cannot tamper with the contents of +/// a cookie nor can they fabricate cookie values, but the data is visible in +/// plaintext. +/// +/// This type is only available when the `secure` feature is enabled. +pub struct SignedJar<'a> { + parent: &'a mut CookieJar, + key: SigningKey, +} + +impl<'a> SignedJar<'a> { + /// Creates a new child `SignedJar` with parent `parent` and key `key`. This + /// method is typically called indirectly via the `signed` method of + /// `CookieJar`. + #[doc(hidden)] + pub fn new(parent: &'a mut CookieJar, key: &Key) -> SignedJar<'a> { + SignedJar { + parent: parent, + key: SigningKey::new(HMAC_DIGEST, key.signing()), + } + } + + /// Given a signed value `str` where the signature is prepended to `value`, + /// verifies the signed value and returns it. If there's a problem, returns + /// an `Err` with a string describing the issue. + fn verify(&self, cookie_value: &str) -> Result { + if cookie_value.len() < BASE64_DIGEST_LEN { + return Err("length of value is <= BASE64_DIGEST_LEN"); + } + + let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN); + let sig = base64::decode(digest_str).map_err(|_| "bad base64 digest")?; + + verify(&self.key, value.as_bytes(), &sig) + .map(|_| value.to_string()) + .map_err(|_| "value did not verify") + } + + /// Returns a reference to the `Cookie` inside this jar with the name `name` + /// and verifies the authenticity and integrity of the cookie's value, + /// returning a `Cookie` with the authenticated value. If the cookie cannot + /// be found, or the cookie fails to verify, `None` is returned. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// let mut signed_jar = jar.signed(&key); + /// assert!(signed_jar.get("name").is_none()); + /// + /// signed_jar.add(Cookie::new("name", "value")); + /// assert_eq!(signed_jar.get("name").unwrap().value(), "value"); + /// ``` + pub fn get(&self, name: &str) -> Option> { + if let Some(cookie_ref) = self.parent.get(name) { + let mut cookie = cookie_ref.clone(); + if let Ok(value) = self.verify(cookie.value()) { + cookie.set_value(value); + return Some(cookie); + } + } + + None + } + + /// Adds `cookie` to the parent jar. The cookie's value is signed assuring + /// integrity and authenticity. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// jar.signed(&key).add(Cookie::new("name", "value")); + /// + /// assert_ne!(jar.get("name").unwrap().value(), "value"); + /// assert!(jar.get("name").unwrap().value().contains("value")); + /// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value"); + /// ``` + pub fn add(&mut self, mut cookie: Cookie<'static>) { + self.sign_cookie(&mut cookie); + self.parent.add(cookie); + } + + /// Adds an "original" `cookie` to this jar. The cookie's value is signed + /// assuring integrity and authenticity. Adding an original cookie does not + /// affect the [`CookieJar::delta()`](struct.CookieJar.html#method.delta) + /// computation. This method is intended to be used to seed the cookie jar + /// with cookies received from a client's HTTP message. + /// + /// For accurate `delta` computations, this method should not be called + /// after calling `remove`. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// jar.signed(&key).add_original(Cookie::new("name", "value")); + /// + /// assert_eq!(jar.iter().count(), 1); + /// assert_eq!(jar.delta().count(), 0); + /// ``` + pub fn add_original(&mut self, mut cookie: Cookie<'static>) { + self.sign_cookie(&mut cookie); + self.parent.add_original(cookie); + } + + /// Signs the cookie's value assuring integrity and authenticity. + fn sign_cookie(&self, cookie: &mut Cookie) { + let digest = sign(&self.key, cookie.value().as_bytes()); + let mut new_value = base64::encode(digest.as_ref()); + new_value.push_str(cookie.value()); + cookie.set_value(new_value); + } + + /// Removes `cookie` from the parent jar. + /// + /// For correct removal, the passed in `cookie` must contain the same `path` + /// and `domain` as the cookie that was initially set. + /// + /// See [CookieJar::remove](struct.CookieJar.html#method.remove) for more + /// details. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// let mut signed_jar = jar.signed(&key); + /// + /// signed_jar.add(Cookie::new("name", "value")); + /// assert!(signed_jar.get("name").is_some()); + /// + /// signed_jar.remove(Cookie::named("name")); + /// assert!(signed_jar.get("name").is_none()); + /// ``` + pub fn remove(&mut self, cookie: Cookie<'static>) { + self.parent.remove(cookie); + } +} + +#[cfg(test)] +mod test { + use super::{Cookie, CookieJar, Key}; + + #[test] + fn simple() { + let key = Key::generate(); + let mut jar = CookieJar::new(); + assert_simple_behaviour!(jar, jar.signed(&key)); + } + + #[test] + fn private() { + let key = Key::generate(); + let mut jar = CookieJar::new(); + assert_secure_behaviour!(jar, jar.signed(&key)); + } +} diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index b062fdf42..45bf067dc 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -6,8 +6,6 @@ use std::{fmt, io, result}; pub use actix_threadpool::BlockingError; use actix_utils::timeout::TimeoutError; -#[cfg(feature = "cookies")] -use cookie; use derive_more::{Display, From}; use futures::Canceled; use http::uri::InvalidUri; @@ -19,8 +17,7 @@ use serde_urlencoded::ser::Error as FormError; use tokio_timer::Error as TimerError; // re-export for convinience -#[cfg(feature = "cookies")] -pub use cookie::ParseError as CookieParseError; +pub use crate::cookie::ParseError as CookieParseError; use crate::body::Body; use crate::response::Response; @@ -79,7 +76,7 @@ impl fmt::Display for Error { impl fmt::Debug for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}\n", &self.cause) + writeln!(f, "{:?}", &self.cause) } } @@ -319,8 +316,7 @@ impl ResponseError for PayloadError { } /// Return `BadRequest` for `cookie::ParseError` -#[cfg(feature = "cookies")] -impl ResponseError for cookie::ParseError { +impl ResponseError for crate::cookie::ParseError { fn error_response(&self) -> Response { Response::new(StatusCode::BAD_REQUEST) } @@ -895,10 +891,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } - #[cfg(feature = "cookies")] #[test] fn test_cookie_parse() { - use cookie::ParseError as CookieParseError; let resp: Response = CookieParseError::EmptyName.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 34204bf5a..96db08122 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -525,7 +525,7 @@ where // keep-alive and stream errors if inner.state.is_empty() && inner.framed.is_write_buf_empty() { if let Some(err) = inner.error.take() { - return Err(err); + Err(err) } // disconnect if keep-alive is not enabled else if inner.flags.contains(Flags::STARTED) @@ -538,10 +538,10 @@ where else if inner.flags.contains(Flags::SHUTDOWN) { self.poll() } else { - return Ok(Async::NotReady); + Ok(Async::NotReady) } } else { - return Ok(Async::NotReady); + Ok(Async::NotReady) } } } diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 6ab37919c..9d9a19e24 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -220,7 +220,7 @@ where Ok(Async::NotReady) => Ok(Async::NotReady), Err(err) => { trace!("H2 handshake error: {}", err); - return Err(err.into()); + Err(err.into()) } } } diff --git a/actix-http/src/httpmessage.rs b/actix-http/src/httpmessage.rs index 60821d300..7a2db52d6 100644 --- a/actix-http/src/httpmessage.rs +++ b/actix-http/src/httpmessage.rs @@ -7,17 +7,12 @@ use encoding::EncodingRef; use http::{header, HeaderMap}; use mime::Mime; -use crate::error::{ContentTypeError, ParseError}; +use crate::cookie::Cookie; +use crate::error::{ContentTypeError, CookieParseError, ParseError}; use crate::extensions::Extensions; use crate::header::Header; use crate::payload::Payload; -#[cfg(feature = "cookies")] -use crate::error::CookieParseError; -#[cfg(feature = "cookies")] -use cookie::Cookie; - -#[cfg(feature = "cookies")] struct Cookies(Vec>); /// Trait that implements general purpose operations on http messages @@ -110,7 +105,6 @@ pub trait HttpMessage: Sized { /// Load request cookies. #[inline] - #[cfg(feature = "cookies")] fn cookies(&self) -> Result>>, CookieParseError> { if self.extensions().get::().is_none() { let mut cookies = Vec::new(); @@ -131,7 +125,6 @@ pub trait HttpMessage: Sized { } /// Return request cookie. - #[cfg(feature = "cookies")] fn cookie(&self, name: &str) -> Option> { if let Ok(cookies) = self.cookies() { for cookie in cookies.iter() { diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 565fe4455..a8c44e833 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -1,9 +1,5 @@ //! Basic http primitives for actix-net framework. -#![allow( - clippy::type_complexity, - clippy::new_without_default, - clippy::new_without_default_derive -)] +#![allow(clippy::type_complexity, clippy::new_without_default)] #[macro_use] extern crate log; @@ -24,6 +20,7 @@ mod request; mod response; mod service; +pub mod cookie; pub mod error; pub mod h1; pub mod h2; @@ -46,16 +43,11 @@ pub mod http { // re-exports pub use http::header::{HeaderName, HeaderValue}; + pub use http::uri::PathAndQuery; + pub use http::{uri, Error, HeaderMap, HttpTryFrom, Uri}; pub use http::{Method, StatusCode, Version}; - #[doc(hidden)] - pub use http::{uri, Error, HeaderMap, HttpTryFrom, Uri}; - - #[doc(hidden)] - pub use http::uri::PathAndQuery; - - #[cfg(feature = "cookies")] - pub use cookie::{Cookie, CookieBuilder}; + pub use crate::cookie::{Cookie, CookieBuilder}; /// Various http headers pub mod header { diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 4da0f642c..a6e8f613c 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -4,8 +4,6 @@ use std::io::Write; use std::{fmt, str}; use bytes::{BufMut, Bytes, BytesMut}; -#[cfg(feature = "cookies")] -use cookie::{Cookie, CookieJar}; use futures::future::{ok, FutureResult, IntoFuture}; use futures::Stream; use http::header::{self, HeaderName, HeaderValue}; @@ -14,6 +12,7 @@ use serde::Serialize; use serde_json; use crate::body::{Body, BodyStream, MessageBody, ResponseBody}; +use crate::cookie::{Cookie, CookieJar}; use crate::error::Error; use crate::extensions::Extensions; use crate::header::{Header, IntoHeaderValue}; @@ -131,7 +130,6 @@ impl Response { /// Get an iterator for the cookies set by this response #[inline] - #[cfg(feature = "cookies")] pub fn cookies(&self) -> CookieIter { CookieIter { iter: self.head.headers.get_all(header::SET_COOKIE).iter(), @@ -140,7 +138,6 @@ impl Response { /// Add a cookie to this response #[inline] - #[cfg(feature = "cookies")] pub fn add_cookie(&mut self, cookie: &Cookie) -> Result<(), HttpError> { let h = &mut self.head.headers; HeaderValue::from_str(&cookie.to_string()) @@ -153,7 +150,6 @@ impl Response { /// Remove all cookies with the given name from this response. Returns /// the number of cookies removed. #[inline] - #[cfg(feature = "cookies")] pub fn del_cookie(&mut self, name: &str) -> usize { let h = &mut self.head.headers; let vals: Vec = h @@ -245,8 +241,8 @@ impl Response { let body = f(&mut self.head, self.body); Response { + body, head: self.head, - body: body, error: self.error, } } @@ -285,12 +281,10 @@ impl IntoFuture for Response { } } -#[cfg(feature = "cookies")] pub struct CookieIter<'a> { iter: header::ValueIter<'a, HeaderValue>, } -#[cfg(feature = "cookies")] impl<'a> Iterator for CookieIter<'a> { type Item = Cookie<'a>; @@ -312,7 +306,6 @@ impl<'a> Iterator for CookieIter<'a> { pub struct ResponseBuilder { head: Option>, err: Option, - #[cfg(feature = "cookies")] cookies: Option, } @@ -325,7 +318,6 @@ impl ResponseBuilder { ResponseBuilder { head: Some(head), err: None, - #[cfg(feature = "cookies")] cookies: None, } } @@ -525,7 +517,6 @@ impl ResponseBuilder { /// .finish() /// } /// ``` - #[cfg(feature = "cookies")] pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { if self.cookies.is_none() { let mut jar = CookieJar::new(); @@ -553,7 +544,6 @@ impl ResponseBuilder { /// builder.finish() /// } /// ``` - #[cfg(feature = "cookies")] pub fn del_cookie<'a>(&mut self, cookie: &Cookie<'a>) -> &mut Self { { if self.cookies.is_none() { @@ -620,20 +610,16 @@ impl ResponseBuilder { return Response::from(Error::from(e)).into_body(); } - #[allow(unused_mut)] let mut response = self.head.take().expect("cannot reuse response builder"); - #[cfg(feature = "cookies")] - { - if let Some(ref jar) = self.cookies { - for cookie in jar.delta() { - match HeaderValue::from_str(&cookie.to_string()) { - Ok(val) => { - let _ = response.headers.append(header::SET_COOKIE, val); - } - Err(e) => return Response::from(Error::from(e)).into_body(), - }; - } + if let Some(ref jar) = self.cookies { + for cookie in jar.delta() { + match HeaderValue::from_str(&cookie.to_string()) { + Ok(val) => { + let _ = response.headers.append(header::SET_COOKIE, val); + } + Err(e) => return Response::from(Error::from(e)).into_body(), + }; } } @@ -697,7 +683,6 @@ impl ResponseBuilder { ResponseBuilder { head: self.head.take(), err: self.err.take(), - #[cfg(feature = "cookies")] cookies: self.cookies.take(), } } @@ -718,9 +703,7 @@ fn parts<'a>( impl From> for ResponseBuilder { fn from(res: Response) -> ResponseBuilder { // If this response has cookies, load them into a jar - #[cfg(feature = "cookies")] let mut jar: Option = None; - #[cfg(feature = "cookies")] for c in res.cookies() { if let Some(ref mut j) = jar { j.add_original(c.into_owned()); @@ -734,7 +717,6 @@ impl From> for ResponseBuilder { ResponseBuilder { head: Some(res.head), err: None, - #[cfg(feature = "cookies")] cookies: jar, } } @@ -744,22 +726,18 @@ impl From> for ResponseBuilder { impl<'a> From<&'a ResponseHead> for ResponseBuilder { fn from(head: &'a ResponseHead) -> ResponseBuilder { // If this response has cookies, load them into a jar - #[cfg(feature = "cookies")] let mut jar: Option = None; - #[cfg(feature = "cookies")] - { - let cookies = CookieIter { - iter: head.headers.get_all(header::SET_COOKIE).iter(), - }; - for c in cookies { - if let Some(ref mut j) = jar { - j.add_original(c.into_owned()); - } else { - let mut j = CookieJar::new(); - j.add_original(c.into_owned()); - jar = Some(j); - } + let cookies = CookieIter { + iter: head.headers.get_all(header::SET_COOKIE).iter(), + }; + for c in cookies { + if let Some(ref mut j) = jar { + j.add_original(c.into_owned()); + } else { + let mut j = CookieJar::new(); + j.add_original(c.into_owned()); + jar = Some(j); } } @@ -773,7 +751,6 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder { ResponseBuilder { head: Some(msg), err: None, - #[cfg(feature = "cookies")] cookies: jar, } } @@ -870,7 +847,6 @@ mod tests { } #[test] - #[cfg(feature = "cookies")] fn test_response_cookies() { use crate::httpmessage::HttpMessage; @@ -907,7 +883,6 @@ mod tests { } #[test] - #[cfg(feature = "cookies")] fn test_update_response_cookies() { let mut r = Response::Ok() .cookie(crate::http::Cookie::new("original", "val100")) @@ -1068,7 +1043,6 @@ mod tests { } #[test] - #[cfg(feature = "cookies")] fn test_into_builder() { let mut resp: Response = "test".into(); assert_eq!(resp.status(), StatusCode::OK); diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs index 2d4b3d0f9..023161246 100644 --- a/actix-http/src/test.rs +++ b/actix-http/src/test.rs @@ -1,12 +1,13 @@ //! Test Various helpers for Actix applications to use during testing. +use std::fmt::Write as FmtWrite; use std::str::FromStr; use bytes::Bytes; -#[cfg(feature = "cookies")] -use cookie::{Cookie, CookieJar}; -use http::header::HeaderName; +use http::header::{self, HeaderName, HeaderValue}; use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; +use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; +use crate::cookie::{Cookie, CookieJar}; use crate::header::{Header, IntoHeaderValue}; use crate::payload::Payload; use crate::Request; @@ -45,7 +46,6 @@ struct Inner { method: Method, uri: Uri, headers: HeaderMap, - #[cfg(feature = "cookies")] cookies: CookieJar, payload: Option, } @@ -57,7 +57,6 @@ impl Default for TestRequest { uri: Uri::from_str("/").unwrap(), version: Version::HTTP_11, headers: HeaderMap::new(), - #[cfg(feature = "cookies")] cookies: CookieJar::new(), payload: None, })) @@ -127,7 +126,6 @@ impl TestRequest { } /// Set cookie for this request - #[cfg(feature = "cookies")] pub fn cookie<'a>(&mut self, cookie: Cookie<'a>) -> &mut Self { parts(&mut self.0).cookies.add(cookie.into_owned()); self @@ -161,25 +159,17 @@ impl TestRequest { head.version = inner.version; head.headers = inner.headers; - #[cfg(feature = "cookies")] - { - use std::fmt::Write as FmtWrite; - - use http::header::{self, HeaderValue}; - use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; - - let mut cookie = String::new(); - for c in inner.cookies.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); - let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - if !cookie.is_empty() { - head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); - } + let mut cookie = String::new(); + for c in inner.cookies.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); + } + if !cookie.is_empty() { + head.headers.insert( + header::COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); } req diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index 78b999703..ea0c5eb9a 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -1,11 +1,8 @@ use actix_service::NewService; -use bytes::{Bytes, BytesMut}; +use bytes::Bytes; use futures::future::{self, ok}; -use futures::{Future, Stream}; -use actix_http::{ - error::PayloadError, http, HttpMessage, HttpService, Request, Response, -}; +use actix_http::{http, HttpService, Request, Response}; use actix_http_test::TestServer; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ @@ -30,16 +27,6 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World"; -fn load_body(stream: S) -> impl Future -where - S: Stream, -{ - stream.fold(BytesMut::new(), move |mut body, chunk| { - body.extend_from_slice(&chunk); - Ok::<_, PayloadError>(body) - }) -} - #[test] fn test_h1_v2() { env_logger::init(); diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml index a2b8e0e15..6569286cb 100644 --- a/actix-session/Cargo.toml +++ b/actix-session/Cargo.toml @@ -21,13 +21,13 @@ path = "src/lib.rs" default = ["cookie-session"] # sessions feature, session require "ring" crate and c compiler -cookie-session = ["cookie/secure"] +cookie-session = ["actix-web/secure-cookies"] [dependencies] -actix-web = "1.0.0-alpha.1" +#actix-web = "1.0.0-alpha.1" +actix-web = { path = ".." } actix-service = "0.3.3" bytes = "0.4" -cookie = { version="0.11", features=["percent-encode"], optional=true } derive_more = "0.14" futures = "0.1.25" hashbrown = "0.1.8" diff --git a/actix-session/src/cookie.rs b/actix-session/src/cookie.rs index 2d5bd7089..9e4fe78b2 100644 --- a/actix-session/src/cookie.rs +++ b/actix-session/src/cookie.rs @@ -19,10 +19,10 @@ use std::collections::HashMap; use std::rc::Rc; use actix_service::{Service, Transform}; +use actix_web::cookie::{Cookie, CookieJar, Key, SameSite}; use actix_web::dev::{ServiceRequest, ServiceResponse}; use actix_web::http::{header::SET_COOKIE, HeaderValue}; use actix_web::{Error, HttpMessage, ResponseError}; -use cookie::{Cookie, CookieJar, Key, SameSite}; use derive_more::{Display, From}; use futures::future::{ok, Future, FutureResult}; use futures::Poll; diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index c0ef89fac..55cfd47d7 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -18,14 +18,16 @@ name = "actix_web_actors" path = "src/lib.rs" [dependencies] -actix = "0.8.0-alpha.1" -actix-web = "1.0.0-alpha.1" -actix-http = "0.1.0-alpha.1" +actix = "0.8.0-alpha.2" +#actix-web = "1.0.0-alpha.1" +#actix-http = "0.1.0-alpha.1" +actix-web = { path=".." } +actix-http = { path="../actix-http" } actix-codec = "0.1.2" bytes = "0.4" futures = "0.1.25" [dev-dependencies] env_logger = "0.6" -actix-http = { version = "0.1.0-alpha.1", features=["ssl"] } -actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } +#actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } +actix-http-test = { path = "../test-server", features=["ssl"] } diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 1f04cfd57..c3d88883f 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -16,6 +16,9 @@ quote = "0.6" syn = { version = "0.15", features = ["full", "parsing"] } [dev-dependencies] -actix-web = { version = "1.0.0-alpha.1" } -actix-http = { version = "0.1.0-alpha.1", features=["ssl"] } -actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } +#actix-web = { version = "1.0.0-alpha.1" } +#actix-http = { version = "0.1.0-alpha.1", features=["ssl"] } +#actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } +actix-web = { path = ".." } +actix-http = { path = "../actix-http", features=["ssl"] } +actix-http-test = { path = "../test-server", features=["ssl"] } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index cd94057fb..148bf97de 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -18,17 +18,14 @@ name = "awc" path = "src/lib.rs" [package.metadata.docs.rs] -features = ["ssl", "brotli", "flate2-zlib", "cookies"] +features = ["ssl", "brotli", "flate2-zlib"] [features] -default = ["cookies", "brotli", "flate2-zlib"] +default = ["brotli", "flate2-zlib"] # openssl ssl = ["openssl", "actix-http/ssl"] -# cookies integration -cookies = ["cookie", "actix-http/cookies"] - # brotli encoding, requires c compiler brotli = ["actix-http/brotli"] @@ -53,8 +50,6 @@ serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.5.3" tokio-timer = "0.2.8" - -cookie = { version="0.11", features=["percent-encode"], optional = true } openssl = { version="0.10", optional = true } [dev-dependencies] diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 92f749d03..ff1fb3fef 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -24,7 +24,7 @@ use std::cell::RefCell; use std::rc::Rc; use std::time::Duration; -pub use actix_http::{client::Connector, http}; +pub use actix_http::{client::Connector, cookie, http}; use actix_http::http::{HeaderMap, HttpTryFrom, Method, Uri}; use actix_http::RequestHead; diff --git a/awc/src/request.rs b/awc/src/request.rs index bdde6faf5..a462479ec 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -1,18 +1,19 @@ use std::fmt; +use std::fmt::Write as FmtWrite; use std::io::Write; use std::rc::Rc; use std::time::Duration; use bytes::{BufMut, Bytes, BytesMut}; -#[cfg(feature = "cookies")] -use cookie::{Cookie, CookieJar}; use futures::future::{err, Either}; use futures::{Future, Stream}; +use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use serde::Serialize; use serde_json; use tokio_timer::Timeout; use actix_http::body::{Body, BodyStream}; +use actix_http::cookie::{Cookie, CookieJar}; use actix_http::encoding::Decoder; use actix_http::http::header::{self, ContentEncoding, Header, IntoHeaderValue}; use actix_http::http::{ @@ -59,7 +60,6 @@ const HTTPS_ENCODING: &str = "gzip, deflate"; pub struct ClientRequest { pub(crate) head: RequestHead, err: Option, - #[cfg(feature = "cookies")] cookies: Option, default_headers: bool, response_decompress: bool, @@ -77,7 +77,6 @@ impl ClientRequest { config, head: RequestHead::default(), err: None, - #[cfg(feature = "cookies")] cookies: None, timeout: None, default_headers: true, @@ -268,7 +267,6 @@ impl ClientRequest { self.header(header::AUTHORIZATION, format!("Bearer {}", token)) } - #[cfg(feature = "cookies")] /// Set a cookie /// /// ```rust @@ -437,28 +435,20 @@ impl ClientRequest { } }; - #[allow(unused_mut)] let mut head = slf.head; - #[cfg(feature = "cookies")] - { - use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; - use std::fmt::Write; - - // set cookies - if let Some(ref mut jar) = slf.cookies { - let mut cookie = String::new(); - for c in jar.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); - let value = - percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); + // set cookies + if let Some(ref mut jar) = slf.cookies { + let mut cookie = String::new(); + for c in jar.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); } + head.headers.insert( + header::COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); } let config = slf.config; diff --git a/awc/src/response.rs b/awc/src/response.rs index 3b77eaa60..038a9a330 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -9,10 +9,8 @@ use actix_http::http::header::{CONTENT_LENGTH, SET_COOKIE}; use actix_http::http::{HeaderMap, StatusCode, Version}; use actix_http::{Extensions, HttpMessage, Payload, PayloadStream, ResponseHead}; -#[cfg(feature = "cookies")] +use actix_http::cookie::Cookie; use actix_http::error::CookieParseError; -#[cfg(feature = "cookies")] -use cookie::Cookie; /// Client Response pub struct ClientResponse { @@ -41,7 +39,6 @@ impl HttpMessage for ClientResponse { /// Load request cookies. #[inline] - #[cfg(feature = "cookies")] fn cookies(&self) -> Result>>, CookieParseError> { struct Cookies(Vec>); diff --git a/awc/src/test.rs b/awc/src/test.rs index 7723b9d2c..5e595d152 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -1,10 +1,12 @@ //! Test helpers for actix http client to use during testing. -use actix_http::http::header::{Header, IntoHeaderValue}; +use std::fmt::Write as FmtWrite; + +use actix_http::cookie::{Cookie, CookieJar}; +use actix_http::http::header::{self, Header, HeaderValue, IntoHeaderValue}; use actix_http::http::{HeaderName, HttpTryFrom, Version}; use actix_http::{h1, Payload, ResponseHead}; use bytes::Bytes; -#[cfg(feature = "cookies")] -use cookie::{Cookie, CookieJar}; +use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use crate::ClientResponse; @@ -30,7 +32,6 @@ where /// Test `ClientResponse` builder pub struct TestResponse { head: ResponseHead, - #[cfg(feature = "cookies")] cookies: CookieJar, payload: Option, } @@ -39,7 +40,6 @@ impl Default for TestResponse { fn default() -> TestResponse { TestResponse { head: ResponseHead::default(), - #[cfg(feature = "cookies")] cookies: CookieJar::new(), payload: None, } @@ -87,7 +87,6 @@ impl TestResponse { } /// Set cookie for this response - #[cfg(feature = "cookies")] pub fn cookie<'a>(mut self, cookie: Cookie<'a>) -> Self { self.cookies.add(cookie.into_owned()); self @@ -105,25 +104,17 @@ impl TestResponse { pub fn finish(self) -> ClientResponse { let mut head = self.head; - #[cfg(feature = "cookies")] - { - use std::fmt::Write as FmtWrite; - - use actix_http::http::header::{self, HeaderValue}; - use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; - - let mut cookie = String::new(); - for c in self.cookies.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); - let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - if !cookie.is_empty() { - head.headers.insert( - header::SET_COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); - } + let mut cookie = String::new(); + for c in self.cookies.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); + } + if !cookie.is_empty() { + head.headers.insert( + header::SET_COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); } if let Some(pl) = self.payload { diff --git a/awc/src/ws.rs b/awc/src/ws.rs index bc023c069..bbeaa061b 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -1,14 +1,15 @@ //! Websockets client +use std::fmt::Write as FmtWrite; use std::io::Write; use std::rc::Rc; use std::{fmt, str}; use actix_codec::Framed; +use actix_http::cookie::{Cookie, CookieJar}; use actix_http::{ws, Payload, RequestHead}; use bytes::{BufMut, BytesMut}; -#[cfg(feature = "cookies")] -use cookie::{Cookie, CookieJar}; use futures::future::{err, Either, Future}; +use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use tokio_timer::Timeout; pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message}; @@ -33,7 +34,6 @@ pub struct WebsocketsRequest { max_size: usize, server_mode: bool, default_headers: bool, - #[cfg(feature = "cookies")] cookies: Option, config: Rc, } @@ -62,7 +62,6 @@ impl WebsocketsRequest { protocols: None, max_size: 65_536, server_mode: false, - #[cfg(feature = "cookies")] cookies: None, default_headers: true, } @@ -82,7 +81,6 @@ impl WebsocketsRequest { self } - #[cfg(feature = "cookies")] /// Set a cookie pub fn cookie<'c>(mut self, cookie: Cookie<'c>) -> Self { if self.cookies.is_none() { @@ -264,28 +262,20 @@ impl WebsocketsRequest { self }; - #[allow(unused_mut)] let mut head = slf.head; - #[cfg(feature = "cookies")] - { - use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; - use std::fmt::Write; - - // set cookies - if let Some(ref mut jar) = slf.cookies { - let mut cookie = String::new(); - for c in jar.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); - let value = - percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); + // set cookies + if let Some(ref mut jar) = slf.cookies { + let mut cookie = String::new(); + for c in jar.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); } + head.headers.insert( + header::COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); } // origin diff --git a/src/lib.rs b/src/lib.rs index 7a4f4bfbb..2bef6a526 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,7 +108,7 @@ pub use actix_web_codegen::*; // re-export for convenience pub use actix_http::Response as HttpResponse; -pub use actix_http::{http, Error, HttpMessage, ResponseError, Result}; +pub use actix_http::{cookie, http, Error, HttpMessage, ResponseError, Result}; pub use crate::app::App; pub use crate::extract::FromRequest; diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs index e94f99db1..34979e167 100644 --- a/src/middleware/identity.rs +++ b/src/middleware/identity.rs @@ -51,11 +51,11 @@ use std::cell::RefCell; use std::rc::Rc; use actix_service::{Service, Transform}; -use cookie::{Cookie, CookieJar, Key, SameSite}; use futures::future::{ok, Either, FutureResult}; use futures::{Future, IntoFuture, Poll}; use time::Duration; +use crate::cookie::{Cookie, CookieJar, Key, SameSite}; use crate::error::{Error, Result}; use crate::http::header::{self, HeaderValue}; use crate::request::HttpRequest; diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 037d00067..6b6253fbc 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -18,5 +18,5 @@ mod logger; pub use self::defaultheaders::DefaultHeaders; pub use self::logger::Logger; -#[cfg(feature = "cookies")] +#[cfg(feature = "secure-cookies")] pub mod identity; diff --git a/src/request.rs b/src/request.rs index 33b63acd7..c524d4978 100644 --- a/src/request.rs +++ b/src/request.rs @@ -263,14 +263,12 @@ mod tests { } #[test] - #[cfg(feature = "cookies")] fn test_no_request_cookies() { let req = TestRequest::default().to_http_request(); assert!(req.cookies().unwrap().is_empty()); } #[test] - #[cfg(feature = "cookies")] fn test_request_cookies() { let req = TestRequest::default() .header(header::COOKIE, "cookie1=value1") diff --git a/src/test.rs b/src/test.rs index 4fdb6ea38..a9aa22789 100644 --- a/src/test.rs +++ b/src/test.rs @@ -2,6 +2,7 @@ use std::cell::RefCell; use std::rc::Rc; +use actix_http::cookie::Cookie; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, StatusCode, Version}; use actix_http::test::TestRequest as HttpTestRequest; @@ -11,8 +12,6 @@ use actix_rt::Runtime; use actix_server_config::ServerConfig; use actix_service::{FnService, IntoNewService, NewService, Service}; use bytes::Bytes; -#[cfg(feature = "cookies")] -use cookie::Cookie; use futures::future::{lazy, Future}; use crate::config::{AppConfig, AppConfigInner}; @@ -285,7 +284,6 @@ impl TestRequest { self } - #[cfg(feature = "cookies")] /// Set cookie for this request pub fn cookie(mut self, cookie: Cookie) -> Self { self.req.cookie(cookie); diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index a9b58483c..97947230f 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -17,17 +17,14 @@ edition = "2018" workspace = ".." [package.metadata.docs.rs] -features = ["session"] +features = [] [lib] name = "actix_http_test" path = "src/lib.rs" [features] -default = ["session"] - -# sessions feature, session require "ring" crate and c compiler -session = ["cookie/secure"] +default = [] # openssl ssl = ["openssl", "actix-server/ssl", "awc/ssl"] @@ -42,7 +39,6 @@ awc = { path = "../awc" } base64 = "0.10" bytes = "0.4" -cookie = { version="0.11", features=["percent-encode"] } futures = "0.1" http = "0.1.8" log = "0.4" @@ -60,4 +56,5 @@ tokio-timer = "0.2" openssl = { version="0.10", optional = true } [dev-dependencies] -actix-web = "1.0.0-alpha.1" +actix-web = { path = ".." } +actix-http = { path = "../actix-http" } diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 9eec065cd..75f75b1e9 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -23,7 +23,7 @@ use net2::TcpBuilder; /// use actix_http::HttpService; /// use actix_http_test::TestServer; /// use actix_web::{web, App, HttpResponse}; -/// # +/// /// fn my_handler() -> HttpResponse { /// HttpResponse::Ok().into() /// } From a20b9fd354f530ac689292bd77ac2f410b939707 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 29 Mar 2019 22:06:14 -0700 Subject: [PATCH 392/427] prepare aplha2 release --- CHANGES.md | 6 ++++++ Cargo.toml | 10 +++++----- actix-files/Cargo.toml | 11 ++++------- actix-http/Cargo.toml | 4 +--- actix-http/src/response.rs | 35 +++++++++++++++-------------------- actix-session/CHANGES.md | 6 ++++++ actix-session/Cargo.toml | 7 +++---- actix-web-actors/CHANGES.md | 4 ++++ actix-web-actors/Cargo.toml | 11 ++++------- actix-web-codegen/Cargo.toml | 9 +++------ awc/Cargo.toml | 10 +++++----- test-server/CHANGES.md | 7 +++++++ test-server/Cargo.toml | 9 ++------- 13 files changed, 65 insertions(+), 64 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 47b0f9875..e18635916 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,8 +2,14 @@ ## [1.0.0-alpha.2] - 2019-03-29 +### Added + +* rustls support + ### Changed +* use forked cookie + * multipart::Field renamed to MultipartField ## [1.0.0-alpha.1] - 2019-03-28 diff --git a/Cargo.toml b/Cargo.toml index 1bd089f02..f7f285091 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "1.0.0-alpha.1" +version = "1.0.0-alpha.2" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" @@ -71,11 +71,11 @@ actix-utils = "0.3.4" actix-router = "0.1.0" actix-rt = "0.2.2" actix-web-codegen = "0.1.0-alpha.1" -actix-http = { path = "actix-http", features=["fail"] } +actix-http = { version = "0.1.0-alpha.2", features=["fail"] } actix-server = "0.4.1" actix-server-config = "0.1.0" actix-threadpool = "0.1.0" -awc = { path = "awc", optional = true } +awc = { version = "0.1.0-alpha.2", optional = true } bytes = "0.4" derive_more = "0.14" @@ -100,8 +100,8 @@ openssl = { version="0.10", optional = true } rustls = { version = "^0.15", optional = true } [dev-dependencies] -actix-http = { path = "actix-http", features=["ssl", "brotli", "flate2-zlib"] } -actix-http-test = { path = "test-server", features=["ssl"] } +actix-http = { version = "0.1.0-alpha.2", features=["ssl", "brotli", "flate2-zlib"] } +actix-http-test = { version = "0.1.0-alpha.2", features=["ssl"] } rand = "0.6" env_logger = "0.6" serde_derive = "1.0" diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 058a19987..3f1bad69e 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.1.0-alpha.1" +version = "0.1.0-alpha.2" authors = ["Nikolay Kim "] description = "Static files support for actix web." readme = "README.md" @@ -18,10 +18,8 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -#actix-web = "1.0.0-alpha.1" -actix-web = { path = ".." } -actix-service = "0.3.3" - +actix-web = "1.0.0-alpha.2" +actix-service = "0.3.4" bitflags = "1" bytes = "0.4" futures = "0.1.25" @@ -33,5 +31,4 @@ percent-encoding = "1.0" v_htmlescape = "0.4" [dev-dependencies] -#actix-web = { version = "1.0.0-alpha.1", features=["ssl"] } -actix-web = { path = "..", features=["ssl"] } +actix-web = { version = "1.0.0-alpha.2", features=["ssl"] } diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index e9bb5d9bb..2de624b7c 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -100,9 +100,7 @@ openssl = { version="0.10", optional = true } actix-rt = "0.2.2" actix-server = { version = "0.4.1", features=["ssl"] } actix-connect = { version = "0.1.0", features=["ssl"] } -#actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } -actix-http-test = { path = "../test-server", features=["ssl"] } - +actix-http-test = { version = "0.1.0-alpha.2", features=["ssl"] } env_logger = "0.6" serde_derive = "1.0" openssl = { version="0.10" } diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index a6e8f613c..88eb7dccb 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -333,11 +333,10 @@ impl ResponseBuilder { /// Set a header. /// - /// ```rust,ignore - /// # extern crate actix_web; - /// use actix_web::{http, Request, Response, Result}; + /// ```rust + /// use actix_http::{http, Request, Response, Result}; /// - /// fn index(req: HttpRequest) -> Result { + /// fn index(req: Request) -> Result { /// Ok(Response::Ok() /// .set(http::header::IfModifiedSince( /// "Sun, 07 Nov 1994 08:48:37 GMT".parse()?, @@ -361,11 +360,10 @@ impl ResponseBuilder { /// Append a header to existing headers. /// - /// ```rust,ignore - /// # extern crate actix_web; - /// use actix_web::{http, Request, Response}; + /// ```rust + /// use actix_http::{http, Request, Response}; /// - /// fn index(req: HttpRequest) -> Response { + /// fn index(req: Request) -> Response { /// Response::Ok() /// .header("X-TEST", "value") /// .header(http::header::CONTENT_TYPE, "application/json") @@ -394,11 +392,10 @@ impl ResponseBuilder { /// Set a header. /// - /// ```rust,ignore - /// # extern crate actix_web; - /// use actix_web::{http, Request, Response}; + /// ```rust + /// use actix_http::{http, Request, Response}; /// - /// fn index(req: HttpRequest) -> Response { + /// fn index(req: Request) -> Response { /// Response::Ok() /// .set_header("X-TEST", "value") /// .set_header(http::header::CONTENT_TYPE, "application/json") @@ -500,11 +497,10 @@ impl ResponseBuilder { /// Set a cookie /// - /// ```rust,ignore - /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, Response, Result}; + /// ```rust + /// use actix_http::{http, Request, Response}; /// - /// fn index(req: HttpRequest) -> Response { + /// fn index(req: Request) -> Response { /// Response::Ok() /// .cookie( /// http::Cookie::build("name", "value") @@ -530,11 +526,10 @@ impl ResponseBuilder { /// Remove cookie /// - /// ```rust,ignore - /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, Response, Result}; + /// ```rust + /// use actix_http::{http, Request, Response, HttpMessage}; /// - /// fn index(req: &HttpRequest) -> Response { + /// fn index(req: Request) -> Response { /// let mut builder = Response::Ok(); /// /// if let Some(ref cookie) = req.cookie("name") { diff --git a/actix-session/CHANGES.md b/actix-session/CHANGES.md index 95ec1c35f..3cd156092 100644 --- a/actix-session/CHANGES.md +++ b/actix-session/CHANGES.md @@ -1,5 +1,11 @@ # Changes +## [0.1.0-alpha.2] - 2019-03-29 + +* Update actix-web + +* Use new feature name for secure cookies + ## [0.1.0-alpha.1] - 2019-03-28 * Initial impl diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml index 6569286cb..e39dc7140 100644 --- a/actix-session/Cargo.toml +++ b/actix-session/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-session" -version = "0.1.0-alpha.1" +version = "0.1.0-alpha.2" authors = ["Nikolay Kim "] description = "Session for actix web framework." readme = "README.md" @@ -24,9 +24,8 @@ default = ["cookie-session"] cookie-session = ["actix-web/secure-cookies"] [dependencies] -#actix-web = "1.0.0-alpha.1" -actix-web = { path = ".." } -actix-service = "0.3.3" +actix-web = "1.0.0-alpha.2" +actix-service = "0.3.4" bytes = "0.4" derive_more = "0.14" futures = "0.1.25" diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 95ec1c35f..9b1427980 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,5 +1,9 @@ # Changes +## [0.1.0-alpha.2] - 2019-03-29 + +* Update actix-http and actix-web + ## [0.1.0-alpha.1] - 2019-03-28 * Initial impl diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 55cfd47d7..759d6fc31 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "1.0.0-alpha.1" +version = "1.0.0-alpha.2" authors = ["Nikolay Kim "] description = "Actix actors support for actix web framework." readme = "README.md" @@ -19,15 +19,12 @@ path = "src/lib.rs" [dependencies] actix = "0.8.0-alpha.2" -#actix-web = "1.0.0-alpha.1" -#actix-http = "0.1.0-alpha.1" -actix-web = { path=".." } -actix-http = { path="../actix-http" } +actix-web = "1.0.0-alpha.2" +actix-http = "0.1.0-alpha.2" actix-codec = "0.1.2" bytes = "0.4" futures = "0.1.25" [dev-dependencies] env_logger = "0.6" -#actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } -actix-http-test = { path = "../test-server", features=["ssl"] } +actix-http-test = { version = "0.1.0-alpha.2", features=["ssl"] } diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index c3d88883f..da2640760 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -16,9 +16,6 @@ quote = "0.6" syn = { version = "0.15", features = ["full", "parsing"] } [dev-dependencies] -#actix-web = { version = "1.0.0-alpha.1" } -#actix-http = { version = "0.1.0-alpha.1", features=["ssl"] } -#actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } -actix-web = { path = ".." } -actix-http = { path = "../actix-http", features=["ssl"] } -actix-http-test = { path = "../test-server", features=["ssl"] } +actix-web = { version = "1.0.0-alpha.2" } +actix-http = { version = "0.1.0-alpha.2", features=["ssl"] } +actix-http-test = { version = "0.1.0-alpha.2", features=["ssl"] } \ No newline at end of file diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 148bf97de..c2cc9e7f0 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -38,7 +38,7 @@ flate2-rust = ["actix-http/flate2-rust"] [dependencies] actix-codec = "0.1.1" actix-service = "0.3.4" -actix-http = { path = "../actix-http" } +actix-http = "0.1.0-alpa.2" base64 = "0.10.1" bytes = "0.4" derive_more = "0.14" @@ -54,11 +54,11 @@ openssl = { version="0.10", optional = true } [dev-dependencies] actix-rt = "0.2.2" -actix-web = { path = "..", features=["ssl"] } -actix-http = { path = "../actix-http", features=["ssl"] } -actix-http-test = { path = "../test-server", features=["ssl"] } +actix-web = { version = "1.0.0-alpha.2", features=["ssl"] } +actix-http = { version = "0.1.0-alpa.2", features=["ssl"] } +actix-http-test = { version = "0.1.0-alpha.2", features=["ssl"] } actix-utils = "0.3.4" -actix-server = { version = "0.4.0", features=["ssl"] } +actix-server = { version = "0.4.1", features=["ssl"] } brotli2 = { version="0.3.2" } flate2 = { version="1.0.2" } env_logger = "0.6" diff --git a/test-server/CHANGES.md b/test-server/CHANGES.md index 95ec1c35f..cac5a2afe 100644 --- a/test-server/CHANGES.md +++ b/test-server/CHANGES.md @@ -1,5 +1,12 @@ # Changes +## [0.1.0-alpha.2] - 2019-03-29 + +* Added TestServerRuntime::load_body() method + +* Update actix-http and awc libraries + + ## [0.1.0-alpha.1] - 2019-03-28 * Initial impl diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 97947230f..838f2d8d2 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "0.1.0-alpha.1" +version = "0.1.0-alpha.2" authors = ["Nikolay Kim "] description = "Actix http test server" readme = "README.md" @@ -35,7 +35,7 @@ actix-rt = "0.2.1" actix-service = "0.3.4" actix-server = "0.4.0" actix-utils = "0.3.4" -awc = { path = "../awc" } +awc = "0.1.0-alpha.2" base64 = "0.10" bytes = "0.4" @@ -52,9 +52,4 @@ serde_urlencoded = "0.5.3" time = "0.1" tokio-tcp = "0.1" tokio-timer = "0.2" - openssl = { version="0.10", optional = true } - -[dev-dependencies] -actix-web = { path = ".." } -actix-http = { path = "../actix-http" } From 2e159d1eb9b55a0bf3755e5af12d4e03548eb34b Mon Sep 17 00:00:00 2001 From: Douman Date: Sat, 30 Mar 2019 17:53:45 +0300 Subject: [PATCH 393/427] test-server: Request functions should accept path (#743) --- test-server/src/lib.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 75f75b1e9..b64ff433e 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -159,33 +159,33 @@ impl TestServerRuntime { } /// Create `GET` request - pub fn get(&self) -> ClientRequest { - self.client.get(self.url("/").as_str()) + pub fn get>(&self, path: S) -> ClientRequest { + self.client.get(self.url(path.as_ref()).as_str()) } /// Create https `GET` request - pub fn sget(&self) -> ClientRequest { - self.client.get(self.surl("/").as_str()) + pub fn sget>(&self, path: S) -> ClientRequest { + self.client.get(self.surl(path.as_ref()).as_str()) } /// Create `POST` request - pub fn post(&self) -> ClientRequest { - self.client.post(self.url("/").as_str()) + pub fn post>(&self, path: S) -> ClientRequest { + self.client.post(self.url(path.as_ref()).as_str()) } /// Create https `POST` request - pub fn spost(&self) -> ClientRequest { - self.client.post(self.surl("/").as_str()) + pub fn spost>(&self, path: S) -> ClientRequest { + self.client.post(self.surl(path.as_ref()).as_str()) } /// Create `HEAD` request - pub fn head(&self) -> ClientRequest { - self.client.head(self.url("/").as_str()) + pub fn head>(&self, path: S) -> ClientRequest { + self.client.head(self.url(path.as_ref()).as_str()) } /// Create https `HEAD` request - pub fn shead(&self) -> ClientRequest { - self.client.head(self.surl("/").as_str()) + pub fn shead>(&self, path: S) -> ClientRequest { + self.client.head(self.surl(path.as_ref()).as_str()) } /// Connect to test http server From 724e9c2efb62e52aaff716217d7e2bb09564d1b5 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 30 Mar 2019 07:56:09 -0700 Subject: [PATCH 394/427] replace deprecated fn --- .travis.yml | 2 +- src/app.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7f61201f6..c3698625e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,7 +42,7 @@ script: after_success: - | if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then - cargo doc --all-features --no-deps && + cargo doc --all-features && echo "" > target/doc/index.html && git clone https://github.com/davisp/ghp-import.git && ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc && diff --git a/src/app.rs b/src/app.rs index a6dfdd554..9cdfc436a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,7 +8,7 @@ use actix_http::encoding::{Decoder, Encoder}; use actix_server_config::ServerConfig; use actix_service::boxed::{self, BoxedNewService}; use actix_service::{ - ApplyTransform, IntoNewService, IntoTransform, NewService, Transform, + apply_transform, IntoNewService, IntoTransform, NewService, Transform, }; #[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] use bytes::Bytes; @@ -138,7 +138,7 @@ where F: IntoTransform>, { let fref = Rc::new(RefCell::new(None)); - let endpoint = ApplyTransform::new(mw, AppEntry::new(fref.clone())); + let endpoint = apply_transform(mw, AppEntry::new(fref.clone())); AppRouter { endpoint, chain: self.chain, @@ -426,7 +426,7 @@ where B1: MessageBody, F: IntoTransform, { - let endpoint = ApplyTransform::new(mw, self.endpoint); + let endpoint = apply_transform(mw, self.endpoint); AppRouter { endpoint, chain: self.chain, From 457b75c9950657b1c402570da4b2538f5a4e0141 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 30 Mar 2019 10:04:38 -0700 Subject: [PATCH 395/427] update api docs; move web to submodule --- CHANGES.md | 5 + Cargo.toml | 2 +- src/lib.rs | 213 +-------------------------------------- src/test.rs | 2 + src/web.rs | 285 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 298 insertions(+), 209 deletions(-) create mode 100644 src/web.rs diff --git a/CHANGES.md b/CHANGES.md index e18635916..e8389910e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # Changes +### Removed + +* Removed unused `actix_web::web::md()` + + ## [1.0.0-alpha.2] - 2019-03-29 ### Added diff --git a/Cargo.toml b/Cargo.toml index f7f285091..fb53008eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "1.0.0-alpha.2" +version = "1.0.0-alpha.3" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" diff --git a/src/lib.rs b/src/lib.rs index 2bef6a526..cb29fa5b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,9 @@ //! represents an HTTP server instance and is used to instantiate and //! configure servers. //! +//! * [web](web/index.html): This module +//! provide essentials helper functions and types for application registration. +//! //! * [HttpRequest](struct.HttpRequest.html) and //! [HttpResponse](struct.HttpResponse.html): These structs //! represent HTTP requests and responses and expose various methods @@ -67,7 +70,7 @@ //! * `tls` - enables ssl support via `native-tls` crate //! * `ssl` - enables ssl support via `openssl` crate, supports `http/2` //! * `rust-tls` - enables ssl support via `rustls` crate, supports `http/2` -//! * `cookies` - enables cookies support, includes `ring` crate as +//! * `secure-cookies` - enables secure cookies support, includes `ring` crate as //! dependency //! * `brotli` - enables `brotli` compression support, requires `c` //! compiler @@ -98,6 +101,7 @@ mod server; mod service; pub mod test; mod types; +pub mod web; #[allow(unused_imports)] #[macro_use] @@ -159,213 +163,6 @@ pub mod dev { } } -pub mod web { - //! Various types - use actix_http::{http::Method, Response}; - use actix_service::{fn_transform, Service, Transform}; - use futures::{Future, IntoFuture}; - - pub use actix_http::Response as HttpResponse; - pub use bytes::{Bytes, BytesMut}; - - use crate::error::{BlockingError, Error}; - use crate::extract::FromRequest; - use crate::handler::{AsyncFactory, Factory}; - use crate::resource::Resource; - use crate::responder::Responder; - use crate::route::Route; - use crate::scope::Scope; - use crate::service::{ServiceRequest, ServiceResponse}; - - pub use crate::data::{Data, RouteData}; - pub use crate::request::HttpRequest; - pub use crate::types::*; - - /// Create resource for a specific path. - /// - /// Resources may have variable path segments. For example, a - /// resource with the path `/a/{name}/c` would match all incoming - /// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. - /// - /// A variable segment is specified in the form `{identifier}`, - /// where the identifier can be used later in a request handler to - /// access the matched value for that segment. This is done by - /// looking up the identifier in the `Params` object returned by - /// `HttpRequest.match_info()` method. - /// - /// By default, each segment matches the regular expression `[^{}/]+`. - /// - /// You can also specify a custom regex in the form `{identifier:regex}`: - /// - /// For instance, to route `GET`-requests on any route matching - /// `/users/{userid}/{friend}` and store `userid` and `friend` in - /// the exposed `Params` object: - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{web, http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/users/{userid}/{friend}") - /// .route(web::get().to(|| HttpResponse::Ok())) - /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) - /// ); - /// } - /// ``` - pub fn resource(path: &str) -> Resource

    { - Resource::new(path) - } - - /// Configure scope for common root path. - /// - /// Scopes collect multiple paths under a common path prefix. - /// Scope path can contain variable path segments as resources. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{web, App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/{project_id}") - /// .service(web::resource("/path1").to(|| HttpResponse::Ok())) - /// .service(web::resource("/path2").to(|| HttpResponse::Ok())) - /// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) - /// ); - /// } - /// ``` - /// - /// In the above example, three routes get added: - /// * /{project_id}/path1 - /// * /{project_id}/path2 - /// * /{project_id}/path3 - /// - pub fn scope(path: &str) -> Scope

    { - Scope::new(path) - } - - /// Create *route* without configuration. - pub fn route() -> Route

    { - Route::new() - } - - /// Create *route* with `GET` method guard. - pub fn get() -> Route

    { - Route::new().method(Method::GET) - } - - /// Create *route* with `POST` method guard. - pub fn post() -> Route

    { - Route::new().method(Method::POST) - } - - /// Create *route* with `PUT` method guard. - pub fn put() -> Route

    { - Route::new().method(Method::PUT) - } - - /// Create *route* with `PATCH` method guard. - pub fn patch() -> Route

    { - Route::new().method(Method::PATCH) - } - - /// Create *route* with `DELETE` method guard. - pub fn delete() -> Route

    { - Route::new().method(Method::DELETE) - } - - /// Create *route* with `HEAD` method guard. - pub fn head() -> Route

    { - Route::new().method(Method::HEAD) - } - - /// Create *route* and add method guard. - pub fn method(method: Method) -> Route

    { - Route::new().method(method) - } - - /// Create a new route and add handler. - /// - /// ```rust - /// use actix_web::{web, App, HttpResponse}; - /// - /// fn index() -> HttpResponse { - /// unimplemented!() - /// } - /// - /// App::new().service( - /// web::resource("/").route( - /// web::to(index)) - /// ); - /// ``` - pub fn to(handler: F) -> Route

    - where - F: Factory + 'static, - I: FromRequest

    + 'static, - R: Responder + 'static, - { - Route::new().to(handler) - } - - /// Create a new route and add async handler. - /// - /// ```rust - /// use actix_web::{web, App, HttpResponse, Error}; - /// - /// fn index() -> impl futures::Future { - /// futures::future::ok(HttpResponse::Ok().finish()) - /// } - /// - /// App::new().service(web::resource("/").route( - /// web::to_async(index)) - /// ); - /// ``` - pub fn to_async(handler: F) -> Route

    - where - F: AsyncFactory, - I: FromRequest

    + 'static, - R: IntoFuture + 'static, - R::Item: Into, - R::Error: Into, - { - Route::new().to_async(handler) - } - - /// Execute blocking function on a thread pool, returns future that resolves - /// to result of the function execution. - pub fn block(f: F) -> impl Future> - where - F: FnOnce() -> Result + Send + 'static, - I: Send + 'static, - E: Send + std::fmt::Debug + 'static, - { - actix_threadpool::run(f).from_err() - } - - /// Create middleare - pub fn md( - f: F, - ) -> impl Transform< - S, - Request = ServiceRequest

    , - Response = ServiceResponse, - Error = Error, - InitError = (), - > - where - S: Service< - Request = ServiceRequest

    , - Response = ServiceResponse, - Error = Error, - >, - F: FnMut(ServiceRequest

    , &mut S) -> R + Clone, - R: IntoFuture, Error = Error>, - { - fn_transform(f) - } -} - #[cfg(feature = "client")] pub mod client { //! An HTTP Client diff --git a/src/test.rs b/src/test.rs index a9aa22789..f18fc2b31 100644 --- a/src/test.rs +++ b/src/test.rs @@ -56,6 +56,7 @@ where .unwrap() } +/// Create service that always responds with `HttpResponse::Ok()` pub fn ok_service() -> impl Service< Request = ServiceRequest, Response = ServiceResponse, @@ -64,6 +65,7 @@ pub fn ok_service() -> impl Service< default_service(StatusCode::OK) } +/// Create service that responds with response with specified status code pub fn default_service( status_code: StatusCode, ) -> impl Service< diff --git a/src/web.rs b/src/web.rs new file mode 100644 index 000000000..65b3cfc70 --- /dev/null +++ b/src/web.rs @@ -0,0 +1,285 @@ +//! Essentials helper functions and types for application registration. +use actix_http::{http::Method, Response}; +use futures::{Future, IntoFuture}; + +pub use actix_http::Response as HttpResponse; +pub use bytes::{Bytes, BytesMut}; + +use crate::error::{BlockingError, Error}; +use crate::extract::FromRequest; +use crate::handler::{AsyncFactory, Factory}; +use crate::resource::Resource; +use crate::responder::Responder; +use crate::route::Route; +use crate::scope::Scope; + +pub use crate::data::{Data, RouteData}; +pub use crate::request::HttpRequest; +pub use crate::types::*; + +/// Create resource for a specific path. +/// +/// Resources may have variable path segments. For example, a +/// resource with the path `/a/{name}/c` would match all incoming +/// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. +/// +/// A variable segment is specified in the form `{identifier}`, +/// where the identifier can be used later in a request handler to +/// access the matched value for that segment. This is done by +/// looking up the identifier in the `Params` object returned by +/// `HttpRequest.match_info()` method. +/// +/// By default, each segment matches the regular expression `[^{}/]+`. +/// +/// You can also specify a custom regex in the form `{identifier:regex}`: +/// +/// For instance, to route `GET`-requests on any route matching +/// `/users/{userid}/{friend}` and store `userid` and `friend` in +/// the exposed `Params` object: +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::{web, App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/users/{userid}/{friend}") +/// .route(web::get().to(|| HttpResponse::Ok())) +/// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) +/// ); +/// } +/// ``` +pub fn resource(path: &str) -> Resource

    { + Resource::new(path) +} + +/// Configure scope for common root path. +/// +/// Scopes collect multiple paths under a common path prefix. +/// Scope path can contain variable path segments as resources. +/// +/// ```rust +/// use actix_web::{web, App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new().service( +/// web::scope("/{project_id}") +/// .service(web::resource("/path1").to(|| HttpResponse::Ok())) +/// .service(web::resource("/path2").to(|| HttpResponse::Ok())) +/// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) +/// ); +/// } +/// ``` +/// +/// In the above example, three routes get added: +/// * /{project_id}/path1 +/// * /{project_id}/path2 +/// * /{project_id}/path3 +/// +pub fn scope(path: &str) -> Scope

    { + Scope::new(path) +} + +/// Create *route* without configuration. +pub fn route() -> Route

    { + Route::new() +} + +/// Create *route* with `GET` method guard. +/// +/// ```rust +/// use actix_web::{web, App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{project_id}") +/// .route(web::get().to(|| HttpResponse::Ok())) +/// ); +/// } +/// ``` +/// +/// In the above example, one `GET` route get added: +/// * /{project_id} +/// +pub fn get() -> Route

    { + Route::new().method(Method::GET) +} + +/// Create *route* with `POST` method guard. +/// +/// ```rust +/// use actix_web::{web, App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{project_id}") +/// .route(web::post().to(|| HttpResponse::Ok())) +/// ); +/// } +/// ``` +/// +/// In the above example, one `POST` route get added: +/// * /{project_id} +/// +pub fn post() -> Route

    { + Route::new().method(Method::POST) +} + +/// Create *route* with `PUT` method guard. +/// +/// ```rust +/// use actix_web::{web, App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{project_id}") +/// .route(web::put().to(|| HttpResponse::Ok())) +/// ); +/// } +/// ``` +/// +/// In the above example, one `PUT` route get added: +/// * /{project_id} +/// +pub fn put() -> Route

    { + Route::new().method(Method::PUT) +} + +/// Create *route* with `PATCH` method guard. +/// +/// ```rust +/// use actix_web::{web, App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{project_id}") +/// .route(web::patch().to(|| HttpResponse::Ok())) +/// ); +/// } +/// ``` +/// +/// In the above example, one `PATCH` route get added: +/// * /{project_id} +/// +pub fn patch() -> Route

    { + Route::new().method(Method::PATCH) +} + +/// Create *route* with `DELETE` method guard. +/// +/// ```rust +/// use actix_web::{web, App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{project_id}") +/// .route(web::delete().to(|| HttpResponse::Ok())) +/// ); +/// } +/// ``` +/// +/// In the above example, one `DELETE` route get added: +/// * /{project_id} +/// +pub fn delete() -> Route

    { + Route::new().method(Method::DELETE) +} + +/// Create *route* with `HEAD` method guard. +/// +/// ```rust +/// use actix_web::{web, App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{project_id}") +/// .route(web::head().to(|| HttpResponse::Ok())) +/// ); +/// } +/// ``` +/// +/// In the above example, one `HEAD` route get added: +/// * /{project_id} +/// +pub fn head() -> Route

    { + Route::new().method(Method::HEAD) +} + +/// Create *route* and add method guard. +/// +/// ```rust +/// use actix_web::{web, http, App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{project_id}") +/// .route(web::method(http::Method::GET).to(|| HttpResponse::Ok())) +/// ); +/// } +/// ``` +/// +/// In the above example, one `GET` route get added: +/// * /{project_id} +/// +pub fn method(method: Method) -> Route

    { + Route::new().method(method) +} + +/// Create a new route and add handler. +/// +/// ```rust +/// use actix_web::{web, App, HttpResponse}; +/// +/// fn index() -> HttpResponse { +/// unimplemented!() +/// } +/// +/// App::new().service( +/// web::resource("/").route( +/// web::to(index)) +/// ); +/// ``` +pub fn to(handler: F) -> Route

    +where + F: Factory + 'static, + I: FromRequest

    + 'static, + R: Responder + 'static, +{ + Route::new().to(handler) +} + +/// Create a new route and add async handler. +/// +/// ```rust +/// # use futures::future::{ok, Future}; +/// use actix_web::{web, App, HttpResponse, Error}; +/// +/// fn index() -> impl Future { +/// ok(HttpResponse::Ok().finish()) +/// } +/// +/// App::new().service(web::resource("/").route( +/// web::to_async(index)) +/// ); +/// ``` +pub fn to_async(handler: F) -> Route

    +where + F: AsyncFactory, + I: FromRequest

    + 'static, + R: IntoFuture + 'static, + R::Item: Into, + R::Error: Into, +{ + Route::new().to_async(handler) +} + +/// Execute blocking function on a thread pool, returns future that resolves +/// to result of the function execution. +pub fn block(f: F) -> impl Future> +where + F: FnOnce() -> Result + Send + 'static, + I: Send + 'static, + E: Send + std::fmt::Debug + 'static, +{ + actix_threadpool::run(f).from_err() +} From 6fcbe4bcdabbc8623bbf8a2676e04f8ef9af292b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 30 Mar 2019 11:33:31 -0700 Subject: [PATCH 396/427] add fn_guard --- src/guard.rs | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/guard.rs b/src/guard.rs index f9565d0fb..4dcd7ba81 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -39,15 +39,35 @@ pub trait Guard { fn check(&self, request: &RequestHead) -> bool; } -#[doc(hidden)] -pub struct FnGuard bool + 'static>(F); - -impl Guard for F +/// Return guard that matches if all of the supplied guards. +/// +/// ```rust +/// use actix_web::{guard, web, App, HttpResponse}; +/// +/// fn main() { +/// App::new().service(web::resource("/index.html").route( +/// web::route() +/// .guard( +/// guard::fn_guard(|req| req.headers().contains_key("content-type"))) +/// .to(|| HttpResponse::MethodNotAllowed())) +/// ); +/// } +/// ``` +pub fn fn_guard(f: F) -> impl Guard where - F: Fn(&RequestHead) -> bool + 'static, + F: Fn(&RequestHead) -> bool, +{ + FnGuard(f) +} + +struct FnGuard bool>(F); + +impl Guard for FnGuard +where + F: Fn(&RequestHead) -> bool, { fn check(&self, head: &RequestHead) -> bool { - (*self)(head) + (self.0)(head) } } @@ -93,7 +113,6 @@ impl Guard for AnyGuard { /// Return guard that matches if all of the supplied guards. /// /// ```rust -/// # extern crate actix_web; /// use actix_web::{guard, web, App, HttpResponse}; /// /// fn main() { From 351df84cca19bfc0ac4c8d44bc9749d5d23f3607 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 30 Mar 2019 11:37:56 -0700 Subject: [PATCH 397/427] update stable release api doc link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9829ab864..35ea0bf0e 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust. * [User Guide](https://actix.rs/docs/) * [API Documentation (Development)](https://actix.rs/actix-web/actix_web/) -* [API Documentation (Releases)](https://docs.rs/actix-web/0.7.18/actix_web/) +* [API Documentation (0.7 Release)](https://docs.rs/actix-web/0.7.19/actix_web/) * [Chat on gitter](https://gitter.im/actix/actix) * Cargo package: [actix-web](https://crates.io/crates/actix-web) * Minimum supported Rust version: 1.32 or later From 1a871d708e70aabcad9a1f97a48642b41fca8732 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 30 Mar 2019 12:13:21 -0700 Subject: [PATCH 398/427] update guard doc test --- Cargo.toml | 2 +- src/guard.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fb53008eb..3abe3129b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ actix-router = "0.1.0" actix-rt = "0.2.2" actix-web-codegen = "0.1.0-alpha.1" actix-http = { version = "0.1.0-alpha.2", features=["fail"] } -actix-server = "0.4.1" +actix-server = "0.4.2" actix-server-config = "0.1.0" actix-threadpool = "0.1.0" awc = { version = "0.1.0-alpha.2", optional = true } diff --git a/src/guard.rs b/src/guard.rs index 4dcd7ba81..fa9088e26 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -19,7 +19,7 @@ //! App::new().service(web::resource("/index.html").route( //! web::route() //! .guard(guard::Post()) -//! .guard(|head: &dev::RequestHead| head.method == http::Method::GET) +//! .guard(guard::fn_guard(|head| head.method == http::Method::GET)) //! .to(|| HttpResponse::MethodNotAllowed())) //! ); //! } From 7596d0b7cbf4558355b7fe79a2a8e5b9d9015f34 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 30 Mar 2019 20:48:00 -0700 Subject: [PATCH 399/427] fix fn_guard doc string --- src/guard.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/guard.rs b/src/guard.rs index fa9088e26..44e4891e6 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -39,7 +39,7 @@ pub trait Guard { fn check(&self, request: &RequestHead) -> bool; } -/// Return guard that matches if all of the supplied guards. +/// Create guard object for supplied function. /// /// ```rust /// use actix_web::{guard, web, App, HttpResponse}; @@ -48,7 +48,9 @@ pub trait Guard { /// App::new().service(web::resource("/index.html").route( /// web::route() /// .guard( -/// guard::fn_guard(|req| req.headers().contains_key("content-type"))) +/// guard::fn_guard( +/// |req| req.headers() +/// .contains_key("content-type"))) /// .to(|| HttpResponse::MethodNotAllowed())) /// ); /// } From ddf5089bffec1f4729560891e0daf35f010fce8c Mon Sep 17 00:00:00 2001 From: Llaurence <40535137+Llaurence@users.noreply.github.com> Date: Sun, 31 Mar 2019 13:26:56 +0000 Subject: [PATCH 400/427] Warn when an unsealed private cookie isn't valid UTF-8 (#746) --- actix-http/src/cookie/secure/private.rs | 98 +++++++++++++++++-------- 1 file changed, 68 insertions(+), 30 deletions(-) diff --git a/actix-http/src/cookie/secure/private.rs b/actix-http/src/cookie/secure/private.rs index 8b56991f1..74352d72b 100644 --- a/actix-http/src/cookie/secure/private.rs +++ b/actix-http/src/cookie/secure/private.rs @@ -1,3 +1,6 @@ +use std::str; + +use log::warn; use ring::aead::{open_in_place, seal_in_place, Aad, Algorithm, Nonce, AES_256_GCM}; use ring::aead::{OpeningKey, SealingKey}; use ring::rand::{SecureRandom, SystemRandom}; @@ -57,9 +60,14 @@ impl<'a> PrivateJar<'a> { let unsealed = open_in_place(&key, nonce, ad, 0, sealed) .map_err(|_| "invalid key/nonce/value: bad seal")?; - ::std::str::from_utf8(unsealed) - .map(|s| s.to_string()) - .map_err(|_| "bad unsealed utf8") + if let Ok(unsealed_utf8) = str::from_utf8(unsealed) { + Ok(unsealed_utf8.to_string()) + } else { + warn!("Private cookie does not have utf8 content! +It is likely the secret key used to encrypt them has been leaked. +Please change it as soon as possible."); + Err("bad unsealed utf8") + } } /// Returns a reference to the `Cookie` inside this jar with the name `name` @@ -147,34 +155,12 @@ impl<'a> PrivateJar<'a> { /// Encrypts the cookie's value with /// authenticated encryption assuring confidentiality, integrity, and authenticity. fn encrypt_cookie(&self, cookie: &mut Cookie) { - let mut data; - let output_len = { - // Create the `SealingKey` structure. - let key = SealingKey::new(ALGO, &self.key).expect("sealing key creation"); - - // Create a vec to hold the [nonce | cookie value | overhead]. - let overhead = ALGO.tag_len(); - let cookie_val = cookie.value().as_bytes(); - data = vec![0; NONCE_LEN + cookie_val.len() + overhead]; - - // Randomly generate the nonce, then copy the cookie value as input. - let (nonce, in_out) = data.split_at_mut(NONCE_LEN); - SystemRandom::new() - .fill(nonce) - .expect("couldn't random fill nonce"); - in_out[..cookie_val.len()].copy_from_slice(cookie_val); - let nonce = Nonce::try_assume_unique_for_key(nonce) - .expect("invalid length of `nonce`"); - - // Use cookie's name as associated data to prevent value swapping. - let ad = Aad::from(cookie.name().as_bytes()); - - // Perform the actual sealing operation and get the output length. - seal_in_place(&key, nonce, ad, in_out, overhead).expect("in-place seal") - }; + let name = cookie.name().as_bytes(); + let value = cookie.value().as_bytes(); + let data = encrypt_name_value(name, value, &self.key); // Base64 encode the nonce and encrypted value. - let sealed_value = base64::encode(&data[..(NONCE_LEN + output_len)]); + let sealed_value = base64::encode(&data); cookie.set_value(sealed_value); } @@ -206,9 +192,38 @@ impl<'a> PrivateJar<'a> { } } +fn encrypt_name_value(name: &[u8], value: &[u8], key: &[u8]) -> Vec { + // Create the `SealingKey` structure. + let key = SealingKey::new(ALGO, key).expect("sealing key creation"); + + // Create a vec to hold the [nonce | cookie value | overhead]. + let overhead = ALGO.tag_len(); + let mut data = vec![0; NONCE_LEN + value.len() + overhead]; + + // Randomly generate the nonce, then copy the cookie value as input. + let (nonce, in_out) = data.split_at_mut(NONCE_LEN); + SystemRandom::new() + .fill(nonce) + .expect("couldn't random fill nonce"); + in_out[..value.len()].copy_from_slice(value); + let nonce = Nonce::try_assume_unique_for_key(nonce) + .expect("invalid length of `nonce`"); + + // Use cookie's name as associated data to prevent value swapping. + let ad = Aad::from(name); + + // Perform the actual sealing operation and get the output length. + let output_len = seal_in_place(&key, nonce, ad, in_out, overhead) + .expect("in-place seal"); + + // Remove the overhead and return the sealed content. + data.truncate(NONCE_LEN + output_len); + data +} + #[cfg(test)] mod test { - use super::{Cookie, CookieJar, Key}; + use super::{Cookie, CookieJar, Key, encrypt_name_value}; #[test] fn simple() { @@ -223,4 +238,27 @@ mod test { let mut jar = CookieJar::new(); assert_secure_behaviour!(jar, jar.private(&key)); } + + #[test] + fn non_utf8() { + let key = Key::generate(); + let mut jar = CookieJar::new(); + + let name = "malicious"; + let mut assert_non_utf8 = |value: &[u8]| { + let sealed = encrypt_name_value(name.as_bytes(), value, &key.encryption()); + let encoded = base64::encode(&sealed); + assert_eq!(jar.private(&key).unseal(name, &encoded), Err("bad unsealed utf8")); + jar.add(Cookie::new(name, encoded)); + assert_eq!(jar.private(&key).get(name), None); + }; + + assert_non_utf8(&[0x72, 0xfb, 0xdf, 0x74]); // rûst in ISO/IEC 8859-1 + + let mut malicious = String::from(r#"{"id":"abc123??%X","admin":true}"#) + .into_bytes(); + malicious[8] |= 0b1100_0000; + malicious[9] |= 0b1100_0000; + assert_non_utf8(&malicious); + } } From ce8294740e919c9f7960902a74fde0592515be2f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 31 Mar 2019 17:04:34 -0700 Subject: [PATCH 401/427] fix tests with disabled features --- actix-http/src/cookie/jar.rs | 4 +++- actix-http/src/cookie/secure/private.rs | 27 +++++++++++++++---------- actix-http/src/lib.rs | 1 + actix-http/tests/test_server.rs | 11 ++++++++++ 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/actix-http/src/cookie/jar.rs b/actix-http/src/cookie/jar.rs index d9ab8f056..b60d73fe6 100644 --- a/actix-http/src/cookie/jar.rs +++ b/actix-http/src/cookie/jar.rs @@ -470,7 +470,9 @@ impl<'a> Iterator for Iter<'a> { #[cfg(test)] mod test { - use super::{Cookie, CookieJar, Key}; + #[cfg(feature = "secure-cookies")] + use super::Key; + use super::{Cookie, CookieJar}; #[test] #[allow(deprecated)] diff --git a/actix-http/src/cookie/secure/private.rs b/actix-http/src/cookie/secure/private.rs index 74352d72b..323687303 100644 --- a/actix-http/src/cookie/secure/private.rs +++ b/actix-http/src/cookie/secure/private.rs @@ -63,9 +63,11 @@ impl<'a> PrivateJar<'a> { if let Ok(unsealed_utf8) = str::from_utf8(unsealed) { Ok(unsealed_utf8.to_string()) } else { - warn!("Private cookie does not have utf8 content! + warn!( + "Private cookie does not have utf8 content! It is likely the secret key used to encrypt them has been leaked. -Please change it as soon as possible."); +Please change it as soon as possible." + ); Err("bad unsealed utf8") } } @@ -206,15 +208,15 @@ fn encrypt_name_value(name: &[u8], value: &[u8], key: &[u8]) -> Vec { .fill(nonce) .expect("couldn't random fill nonce"); in_out[..value.len()].copy_from_slice(value); - let nonce = Nonce::try_assume_unique_for_key(nonce) - .expect("invalid length of `nonce`"); + let nonce = + Nonce::try_assume_unique_for_key(nonce).expect("invalid length of `nonce`"); // Use cookie's name as associated data to prevent value swapping. let ad = Aad::from(name); // Perform the actual sealing operation and get the output length. - let output_len = seal_in_place(&key, nonce, ad, in_out, overhead) - .expect("in-place seal"); + let output_len = + seal_in_place(&key, nonce, ad, in_out, overhead).expect("in-place seal"); // Remove the overhead and return the sealed content. data.truncate(NONCE_LEN + output_len); @@ -223,7 +225,7 @@ fn encrypt_name_value(name: &[u8], value: &[u8], key: &[u8]) -> Vec { #[cfg(test)] mod test { - use super::{Cookie, CookieJar, Key, encrypt_name_value}; + use super::{encrypt_name_value, Cookie, CookieJar, Key}; #[test] fn simple() { @@ -248,15 +250,18 @@ mod test { let mut assert_non_utf8 = |value: &[u8]| { let sealed = encrypt_name_value(name.as_bytes(), value, &key.encryption()); let encoded = base64::encode(&sealed); - assert_eq!(jar.private(&key).unseal(name, &encoded), Err("bad unsealed utf8")); + assert_eq!( + jar.private(&key).unseal(name, &encoded), + Err("bad unsealed utf8") + ); jar.add(Cookie::new(name, encoded)); assert_eq!(jar.private(&key).get(name), None); }; - assert_non_utf8(&[0x72, 0xfb, 0xdf, 0x74]); // rûst in ISO/IEC 8859-1 + assert_non_utf8(&[0x72, 0xfb, 0xdf, 0x74]); // rûst in ISO/IEC 8859-1 - let mut malicious = String::from(r#"{"id":"abc123??%X","admin":true}"#) - .into_bytes(); + let mut malicious = + String::from(r#"{"id":"abc123??%X","admin":true}"#).into_bytes(); malicious[8] |= 0b1100_0000; malicious[9] |= 0b1100_0000; assert_non_utf8(&malicious); diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index a8c44e833..088125ae0 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -8,6 +8,7 @@ pub mod body; mod builder; pub mod client; mod config; +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust", feature = "brotli"))] pub mod encoding; mod extensions; mod header; diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index f1f82b089..a18d19626 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -16,6 +16,7 @@ use actix_http::{ body, error, http, http::header, Error, HttpService, KeepAlive, Request, Response, }; +#[cfg(feature = "ssl")] fn load_body(stream: S) -> impl Future where S: Stream, @@ -346,6 +347,7 @@ fn test_content_length() { } } +#[cfg(feature = "ssl")] #[test] fn test_h2_content_length() { use actix_http::http::{ @@ -443,6 +445,7 @@ fn test_h1_headers() { assert_eq!(bytes, Bytes::from(data2)); } +#[cfg(feature = "ssl")] #[test] fn test_h2_headers() { let data = STR.repeat(10); @@ -523,6 +526,7 @@ fn test_h1_body() { assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } +#[cfg(feature = "ssl")] #[test] fn test_h2_body2() { let openssl = ssl_acceptor().unwrap(); @@ -567,6 +571,7 @@ fn test_h1_head_empty() { assert!(bytes.is_empty()); } +#[cfg(feature = "ssl")] #[test] fn test_h2_head_empty() { let openssl = ssl_acceptor().unwrap(); @@ -622,6 +627,7 @@ fn test_h1_head_binary() { assert!(bytes.is_empty()); } +#[cfg(feature = "ssl")] #[test] fn test_h2_head_binary() { let openssl = ssl_acceptor().unwrap(); @@ -674,6 +680,7 @@ fn test_h1_head_binary2() { } } +#[cfg(feature = "ssl")] #[test] fn test_h2_head_binary2() { let openssl = ssl_acceptor().unwrap(); @@ -720,6 +727,7 @@ fn test_h1_body_length() { assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } +#[cfg(feature = "ssl")] #[test] fn test_h2_body_length() { let openssl = ssl_acceptor().unwrap(); @@ -779,6 +787,7 @@ fn test_h1_body_chunked_explicit() { assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } +#[cfg(feature = "ssl")] #[test] fn test_h2_body_chunked_explicit() { let openssl = ssl_acceptor().unwrap(); @@ -861,6 +870,7 @@ fn test_h1_response_http_error_handling() { assert!(bytes.is_empty()); } +#[cfg(feature = "ssl")] #[test] fn test_h2_response_http_error_handling() { let openssl = ssl_acceptor().unwrap(); @@ -908,6 +918,7 @@ fn test_h1_service_error() { assert!(bytes.is_empty()); } +#[cfg(feature = "ssl")] #[test] fn test_h2_service_error() { let openssl = ssl_acceptor().unwrap(); From e4b3f7945803747e70ccfc4bbd2b1dc6e1c93a0f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 31 Mar 2019 17:05:02 -0700 Subject: [PATCH 402/427] allocate enough space --- actix-http/src/ws/frame.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/actix-http/src/ws/frame.rs b/actix-http/src/ws/frame.rs index d4c15627f..652746b89 100644 --- a/actix-http/src/ws/frame.rs +++ b/actix-http/src/ws/frame.rs @@ -172,13 +172,14 @@ impl Parser { }; if payload_len < 126 { + dst.reserve(p_len + 2 + if mask { 4 } else { 0 }); dst.put_slice(&[one, two | payload_len as u8]); } else if payload_len <= 65_535 { - dst.reserve(p_len + 4); + dst.reserve(p_len + 4 + if mask { 4 } else { 0 }); dst.put_slice(&[one, two | 126]); dst.put_u16_be(payload_len as u16); } else { - dst.reserve(p_len + 10); + dst.reserve(p_len + 10 + if mask { 4 } else { 0 }); dst.put_slice(&[one, two | 127]); dst.put_u64_be(payload_len as u64); }; @@ -186,7 +187,7 @@ impl Parser { if mask { let mask = rand::random::(); dst.put_u32_le(mask); - dst.extend_from_slice(payload.as_ref()); + dst.put_slice(payload.as_ref()); let pos = dst.len() - payload_len; apply_mask(&mut dst[pos..], mask); } else { From ab45974e355277c3265ffd5fb54fb6b171bf9c12 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 31 Mar 2019 18:19:18 -0700 Subject: [PATCH 403/427] add default handler --- actix-files/CHANGES.md | 5 + actix-files/src/lib.rs | 236 +++++++++++++++++++++++++++------------ actix-files/src/named.rs | 2 +- src/resource.rs | 4 +- 4 files changed, 171 insertions(+), 76 deletions(-) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 95ec1c35f..4fe8fadb3 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,5 +1,10 @@ # Changes +## [0.1.0-alpha.2] - 2019-04-xx + +* Add default handler support + + ## [0.1.0-alpha.1] - 2019-03-28 * Initial impl diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 8254c5fe5..60ccd81de 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -7,23 +7,23 @@ use std::path::{Path, PathBuf}; use std::rc::Rc; use std::{cmp, io}; +use actix_service::boxed::{self, BoxedNewService, BoxedService}; +use actix_service::{IntoNewService, NewService, Service}; +use actix_web::dev::{ + HttpServiceFactory, Payload, ResourceDef, ServiceConfig, ServiceFromRequest, + ServiceRequest, ServiceResponse, +}; +use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; +use actix_web::http::header::DispositionType; +use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder}; use bytes::Bytes; +use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, Poll, Stream}; use mime; use mime_guess::get_mime_type; use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; use v_htmlescape::escape as escape_html_entity; -use actix_service::{boxed::BoxedNewService, NewService, Service}; -use actix_web::dev::{ - HttpServiceFactory, ResourceDef, ServiceConfig, ServiceFromRequest, ServiceRequest, - ServiceResponse, -}; -use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; -use actix_web::http::header::DispositionType; -use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder}; -use futures::future::{ok, FutureResult}; - mod error; mod named; mod range; @@ -32,6 +32,7 @@ use self::error::{FilesError, UriSegmentError}; pub use crate::named::NamedFile; pub use crate::range::HttpRange; +type HttpService

    = BoxedService, ServiceResponse, Error>; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; @@ -232,8 +233,6 @@ pub struct Files { default: Rc>>>>, renderer: Rc, mime_override: Option>, - _chunk_size: usize, - _follow_symlinks: bool, file_flags: named::Flags, } @@ -245,8 +244,6 @@ impl Clone for Files { show_index: self.show_index, default: self.default.clone(), renderer: self.renderer.clone(), - _chunk_size: self._chunk_size, - _follow_symlinks: self._follow_symlinks, file_flags: self.file_flags, path: self.path.clone(), mime_override: self.mime_override.clone(), @@ -274,8 +271,6 @@ impl Files { default: Rc::new(RefCell::new(None)), renderer: Rc::new(directory_listing), mime_override: None, - _chunk_size: 0, - _follow_symlinks: false, file_flags: named::Flags::default(), } } @@ -334,6 +329,24 @@ impl Files { self.file_flags.set(named::Flags::LAST_MD, value); self } + + /// Sets default handler which is used when no matched file could be found. + pub fn default_handler(mut self, f: F) -> Self + where + F: IntoNewService, + U: NewService< + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, + > + 'static, + { + // create and configure default resource + self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::new_service( + f.into_new_service().map_init_err(|_| ()), + ))))); + + self + } } impl

    HttpServiceFactory

    for Files

    @@ -353,41 +366,95 @@ where } } -impl

    NewService for Files

    { +impl NewService for Files

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = Error; - type Service = Self; + type Service = FilesService

    ; type InitError = (); - type Future = FutureResult; + type Future = Box>; fn new_service(&self, _: &()) -> Self::Future { - ok(self.clone()) + let mut srv = FilesService { + directory: self.directory.clone(), + index: self.index.clone(), + show_index: self.show_index, + default: None, + renderer: self.renderer.clone(), + mime_override: self.mime_override.clone(), + file_flags: self.file_flags, + }; + + if let Some(ref default) = *self.default.borrow() { + Box::new( + default + .new_service(&()) + .map(move |default| { + srv.default = Some(default); + srv + }) + .map_err(|_| ()), + ) + } else { + Box::new(ok(srv)) + } } } -impl

    Service for Files

    { +pub struct FilesService

    { + directory: PathBuf, + index: Option, + show_index: bool, + default: Option>, + renderer: Rc, + mime_override: Option>, + file_flags: named::Flags, +} + +impl

    FilesService

    { + fn handle_err( + &mut self, + e: io::Error, + req: HttpRequest, + payload: Payload

    , + ) -> Either< + FutureResult, + Box>, + > { + log::debug!("Files: Failed to handle {}: {}", req.path(), e); + if let Some(ref mut default) = self.default { + Either::B(default.call(ServiceRequest::from_parts(req, payload))) + } else { + Either::A(ok(ServiceResponse::from_err(e, req.clone()))) + } + } +} + +impl

    Service for FilesService

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = Error; - type Future = FutureResult; + type Future = Either< + FutureResult, + Box>, + >; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { - let (req, _) = req.into_parts(); + let (req, pl) = req.into_parts(); let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) { Ok(item) => item, - Err(e) => return ok(ServiceResponse::from_err(e, req.clone())), + Err(e) => return Either::A(ok(ServiceResponse::from_err(e, req.clone()))), }; // full filepath let path = match self.directory.join(&real_path.0).canonicalize() { Ok(path) => path, - Err(e) => return ok(ServiceResponse::from_err(e, req.clone())), + Err(e) => return self.handle_err(e, req, pl), }; if path.is_dir() { @@ -403,25 +470,25 @@ impl

    Service for Files

    { } named_file.flags = self.file_flags; - match named_file.respond_to(&req) { - Ok(item) => ok(ServiceResponse::new(req.clone(), item)), - Err(e) => ok(ServiceResponse::from_err(e, req.clone())), - } + Either::A(ok(match named_file.respond_to(&req) { + Ok(item) => ServiceResponse::new(req.clone(), item), + Err(e) => ServiceResponse::from_err(e, req.clone()), + })) } - Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + Err(e) => return self.handle_err(e, req, pl), } } else if self.show_index { let dir = Directory::new(self.directory.clone(), path); let x = (self.renderer)(&dir, &req); match x { - Ok(resp) => ok(resp), - Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + Ok(resp) => Either::A(ok(resp)), + Err(e) => return self.handle_err(e, req, pl), } } else { - ok(ServiceResponse::from_err( + Either::A(ok(ServiceResponse::from_err( FilesError::IsDirectory, req.clone(), - )) + ))) } } else { match NamedFile::open(path) { @@ -434,11 +501,15 @@ impl

    Service for Files

    { named_file.flags = self.file_flags; match named_file.respond_to(&req) { - Ok(item) => ok(ServiceResponse::new(req.clone(), item)), - Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + Ok(item) => { + Either::A(ok(ServiceResponse::new(req.clone(), item))) + } + Err(e) => { + Either::A(ok(ServiceResponse::from_err(e, req.clone()))) + } } } - Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + Err(e) => self.handle_err(e, req, pl), } } } @@ -833,15 +904,15 @@ mod tests { let mut response = test::call_success(&mut srv, request); // with enabled compression - // { - // let te = response - // .headers() - // .get(header::TRANSFER_ENCODING) - // .unwrap() - // .to_str() - // .unwrap(); - // assert_eq!(te, "chunked"); - // } + { + let te = response + .headers() + .get(header::TRANSFER_ENCODING) + .unwrap() + .to_str() + .unwrap(); + assert_eq!(te, "chunked"); + } let bytes = test::block_on(response.take_body().fold(BytesMut::new(), |mut b, c| { @@ -890,20 +961,32 @@ mod tests { assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } - // #[test] - // fn test_named_file_content_encoding() { - // let req = TestRequest::default().method(Method::GET).finish(); - // let file = NamedFile::open("Cargo.toml").unwrap(); + #[test] + fn test_named_file_content_encoding() { + let mut srv = test::init_service(App::new().enable_encoding().service( + web::resource("/").to(|| { + NamedFile::open("Cargo.toml") + .unwrap() + .set_content_encoding(header::ContentEncoding::Identity) + }), + )); - // assert!(file.encoding.is_none()); - // let resp = file - // .set_content_encoding(ContentEncoding::Identity) - // .respond_to(&req) - // .unwrap(); + let request = TestRequest::get() + .uri("/") + .header(header::ACCEPT_ENCODING, "gzip") + .to_request(); + let res = test::call_success(&mut srv, request); + assert_eq!(res.status(), StatusCode::OK); - // assert!(resp.content_encoding().is_some()); - // assert_eq!(resp.content_encoding().unwrap().as_str(), "identity"); - // } + assert_eq!( + res.headers() + .get(header::CONTENT_ENCODING) + .unwrap() + .to_str() + .unwrap(), + "identity" + ); + } #[test] fn test_named_file_allowed_method() { @@ -954,22 +1037,29 @@ mod tests { let _st: Files<()> = Files::new("/", "Cargo.toml"); } - // #[test] - // fn test_default_handler_file_missing() { - // let st = Files::new(".") - // .default_handler(|_: &_| "default content"); - // let req = TestRequest::with_uri("/missing") - // .param("tail", "missing") - // .finish(); + #[test] + fn test_default_handler_file_missing() { + let mut st = test::block_on( + Files::new("/", ".") + .default_handler(|req: ServiceRequest<_>| { + Ok(req.into_response(HttpResponse::Ok().body("default content"))) + }) + .new_service(&()), + ) + .unwrap(); + let req = TestRequest::with_uri("/missing").to_service(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::OK); - // assert_eq!( - // resp.body(), - // &Body::Binary(Binary::Slice(b"default content")) - // ); - // } + let mut resp = test::call_success(&mut st, req); + assert_eq!(resp.status(), StatusCode::OK); + let bytes = + test::block_on(resp.take_body().fold(BytesMut::new(), |mut b, c| { + b.extend(c); + Ok::<_, Error>(b) + })) + .unwrap(); + + assert_eq!(bytes.freeze(), Bytes::from_static(b"default content")); + } // #[test] // fn test_serve_index() { diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 842a0e5e0..4ee1a3caa 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -43,7 +43,7 @@ pub struct NamedFile { pub(crate) content_disposition: header::ContentDisposition, pub(crate) md: Metadata, modified: Option, - encoding: Option, + pub(crate) encoding: Option, pub(crate) status_code: StatusCode, pub(crate) flags: Flags, } diff --git a/src/resource.rs b/src/resource.rs index b24e8dd51..957795cd7 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use actix_http::{Error, Response}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ - ApplyTransform, IntoNewService, IntoTransform, NewService, Service, Transform, + apply_transform, IntoNewService, IntoTransform, NewService, Service, Transform, }; use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; @@ -254,7 +254,7 @@ where >, F: IntoTransform, { - let endpoint = ApplyTransform::new(mw, self.endpoint); + let endpoint = apply_transform(mw, self.endpoint); Resource { endpoint, rdef: self.rdef, From 15c5a3bcfb23bd779507c235235de9e90547612b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 31 Mar 2019 18:57:54 -0700 Subject: [PATCH 404/427] fix test --- actix-files/src/lib.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 60ccd81de..54b4f9618 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -904,15 +904,15 @@ mod tests { let mut response = test::call_success(&mut srv, request); // with enabled compression - { - let te = response - .headers() - .get(header::TRANSFER_ENCODING) - .unwrap() - .to_str() - .unwrap(); - assert_eq!(te, "chunked"); - } + // { + // let te = response + // .headers() + // .get(header::TRANSFER_ENCODING) + // .unwrap() + // .to_str() + // .unwrap(); + // assert_eq!(te, "chunked"); + // } let bytes = test::block_on(response.take_body().fold(BytesMut::new(), |mut b, c| { From 34695f4bcebdeef38f098bd21341a661d0bf706c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 31 Mar 2019 20:43:00 -0700 Subject: [PATCH 405/427] rename test methods; update tests --- CHANGES.md | 7 +++ src/middleware/cors.rs | 34 +++++++------- src/middleware/defaultheaders.rs | 6 +-- src/middleware/errhandlers.rs | 4 +- src/middleware/logger.rs | 62 ++++++++++++------------- src/service.rs | 27 +++++++++++ src/test.rs | 12 ++--- tests/test_httpserver.rs | 79 +++++++++++++++++++++++++++++++- 8 files changed, 169 insertions(+), 62 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e8389910e..39975fb46 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ # Changes +### Changed + +* Renamed `TestRequest::to_service()` to `TestRequest::to_srv_request()` + +* Renamed `TestRequest::to_response()` to `TestRequest::to_srv_response()` + + ### Removed * Removed unused `actix_web::web::md()` diff --git a/src/middleware/cors.rs b/src/middleware/cors.rs index 2ece543d2..8924eb0ab 100644 --- a/src/middleware/cors.rs +++ b/src/middleware/cors.rs @@ -848,8 +848,8 @@ mod tests { #[test] fn validate_origin_allows_all_origins() { let mut cors = Cors::new().finish(test::ok_service()); - let req = - TestRequest::with_header("Origin", "https://www.example.com").to_service(); + let req = TestRequest::with_header("Origin", "https://www.example.com") + .to_srv_request(); let resp = test::call_success(&mut cors, req); assert_eq!(resp.status(), StatusCode::OK); @@ -867,7 +867,7 @@ mod tests { let req = TestRequest::with_header("Origin", "https://www.example.com") .method(Method::OPTIONS) - .to_service(); + .to_srv_request(); assert!(cors.inner.validate_allowed_method(&req).is_err()); assert!(cors.inner.validate_allowed_headers(&req).is_err()); @@ -877,7 +877,7 @@ mod tests { let req = TestRequest::with_header("Origin", "https://www.example.com") .header(header::ACCESS_CONTROL_REQUEST_METHOD, "put") .method(Method::OPTIONS) - .to_service(); + .to_srv_request(); assert!(cors.inner.validate_allowed_method(&req).is_err()); assert!(cors.inner.validate_allowed_headers(&req).is_err()); @@ -889,7 +889,7 @@ mod tests { "AUTHORIZATION,ACCEPT", ) .method(Method::OPTIONS) - .to_service(); + .to_srv_request(); let resp = test::call_success(&mut cors, req); assert_eq!( @@ -935,7 +935,7 @@ mod tests { "AUTHORIZATION,ACCEPT", ) .method(Method::OPTIONS) - .to_service(); + .to_srv_request(); let resp = test::call_success(&mut cors, req); assert_eq!(resp.status(), StatusCode::OK); @@ -960,7 +960,7 @@ mod tests { let req = TestRequest::with_header("Origin", "https://www.unknown.com") .method(Method::GET) - .to_service(); + .to_srv_request(); cors.inner.validate_origin(&req).unwrap(); cors.inner.validate_allowed_method(&req).unwrap(); cors.inner.validate_allowed_headers(&req).unwrap(); @@ -974,7 +974,7 @@ mod tests { let req = TestRequest::with_header("Origin", "https://www.example.com") .method(Method::GET) - .to_service(); + .to_srv_request(); let resp = test::call_success(&mut cors, req); assert_eq!(resp.status(), StatusCode::OK); @@ -984,7 +984,7 @@ mod tests { fn test_no_origin_response() { let mut cors = Cors::new().disable_preflight().finish(test::ok_service()); - let req = TestRequest::default().method(Method::GET).to_service(); + let req = TestRequest::default().method(Method::GET).to_srv_request(); let resp = test::call_success(&mut cors, req); assert!(resp .headers() @@ -993,7 +993,7 @@ mod tests { let req = TestRequest::with_header("Origin", "https://www.example.com") .method(Method::OPTIONS) - .to_service(); + .to_srv_request(); let resp = test::call_success(&mut cors, req); assert_eq!( &b"https://www.example.com"[..], @@ -1019,7 +1019,7 @@ mod tests { let req = TestRequest::with_header("Origin", "https://www.example.com") .method(Method::OPTIONS) - .to_service(); + .to_srv_request(); let resp = test::call_success(&mut cors, req); assert_eq!( @@ -1066,7 +1066,7 @@ mod tests { })); let req = TestRequest::with_header("Origin", "https://www.example.com") .method(Method::OPTIONS) - .to_service(); + .to_srv_request(); let resp = test::call_success(&mut cors, req); assert_eq!( &b"Accept, Origin"[..], @@ -1082,7 +1082,7 @@ mod tests { let req = TestRequest::with_header("Origin", "https://www.example.com") .method(Method::OPTIONS) .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST") - .to_service(); + .to_srv_request(); let resp = test::call_success(&mut cors, req); let origins_str = resp @@ -1105,7 +1105,7 @@ mod tests { let req = TestRequest::with_header("Origin", "https://example.com") .method(Method::GET) - .to_service(); + .to_srv_request(); let resp = test::call_success(&mut cors, req); assert_eq!( @@ -1118,7 +1118,7 @@ mod tests { let req = TestRequest::with_header("Origin", "https://example.org") .method(Method::GET) - .to_service(); + .to_srv_request(); let resp = test::call_success(&mut cors, req); assert_eq!( @@ -1141,7 +1141,7 @@ mod tests { let req = TestRequest::with_header("Origin", "https://example.com") .header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET") .method(Method::OPTIONS) - .to_service(); + .to_srv_request(); let resp = test::call_success(&mut cors, req); assert_eq!( @@ -1155,7 +1155,7 @@ mod tests { let req = TestRequest::with_header("Origin", "https://example.org") .header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET") .method(Method::OPTIONS) - .to_service(); + .to_srv_request(); let resp = test::call_success(&mut cors, req); assert_eq!( diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index ca5b8f807..72e866dbd 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -166,11 +166,11 @@ mod tests { ) .unwrap(); - let req = TestRequest::default().to_service(); + let req = TestRequest::default().to_srv_request(); let resp = block_on(mw.call(req)).unwrap(); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - let req = TestRequest::default().to_service(); + let req = TestRequest::default().to_srv_request(); let srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish()) }); @@ -192,7 +192,7 @@ mod tests { let mut mw = block_on(DefaultHeaders::new().content_type().new_transform(srv)).unwrap(); - let req = TestRequest::default().to_service(); + let req = TestRequest::default().to_srv_request(); let resp = block_on(mw.call(req)).unwrap(); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), diff --git a/src/middleware/errhandlers.rs b/src/middleware/errhandlers.rs index 4f2537221..a69bdaf9f 100644 --- a/src/middleware/errhandlers.rs +++ b/src/middleware/errhandlers.rs @@ -180,7 +180,7 @@ mod tests { ) .unwrap(); - let resp = test::call_success(&mut mw, TestRequest::default().to_service()); + let resp = test::call_success(&mut mw, TestRequest::default().to_srv_request()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } @@ -206,7 +206,7 @@ mod tests { ) .unwrap(); - let resp = test::call_success(&mut mw, TestRequest::default().to_service()); + let resp = test::call_success(&mut mw, TestRequest::default().to_srv_request()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } } diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index cd52048f7..d9c9b138a 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -469,44 +469,40 @@ mod tests { header::USER_AGENT, header::HeaderValue::from_static("ACTIX-WEB"), ) - .to_service(); + .to_srv_request(); let _res = block_on(srv.call(req)); } - // #[test] - // fn test_default_format() { - // let format = Format::default(); + #[test] + fn test_default_format() { + let mut format = Format::default(); - // let req = TestRequest::with_header( - // header::USER_AGENT, - // header::HeaderValue::from_static("ACTIX-WEB"), - // ) - // .finish(); - // let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); - // let entry_time = time::now(); + let req = TestRequest::with_header( + header::USER_AGENT, + header::HeaderValue::from_static("ACTIX-WEB"), + ) + .to_srv_request(); - // let render = |fmt: &mut Formatter| { - // for unit in &format.0 { - // unit.render(fmt, &req, &resp, entry_time)?; - // } - // Ok(()) - // }; - // let s = format!("{}", FormatDisplay(&render)); - // assert!(s.contains("GET / HTTP/1.1")); - // assert!(s.contains("200 0")); - // assert!(s.contains("ACTIX-WEB")); + let now = time::now(); + for unit in &mut format.0 { + unit.render_request(now, &req); + } - // let req = TestRequest::with_uri("/?test").finish(); - // let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); - // let entry_time = time::now(); + let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); + for unit in &mut format.0 { + unit.render_response(&resp); + } - // let render = |fmt: &mut Formatter| { - // for unit in &format.0 { - // unit.render(fmt, &req, &resp, entry_time)?; - // } - // Ok(()) - // }; - // let s = format!("{}", FormatDisplay(&render)); - // assert!(s.contains("GET /?test HTTP/1.1")); - // } + let entry_time = time::now(); + let render = |fmt: &mut Formatter| { + for unit in &format.0 { + unit.render(fmt, 1024, entry_time)?; + } + Ok(()) + }; + let s = format!("{}", FormatDisplay(&render)); + assert!(s.contains("GET / HTTP/1.1")); + assert!(s.contains("200 1024")); + assert!(s.contains("ACTIX-WEB")); + } } diff --git a/src/service.rs b/src/service.rs index 5a0422089..c260f25b2 100644 --- a/src/service.rs +++ b/src/service.rs @@ -449,3 +449,30 @@ impl fmt::Debug for ServiceResponse { res } } + +#[cfg(test)] +mod tests { + use crate::test::TestRequest; + use crate::HttpResponse; + + #[test] + fn test_fmt_debug() { + let req = TestRequest::get() + .uri("/index.html?test=1") + .header("x-test", "111") + .to_srv_request(); + let s = format!("{:?}", req); + assert!(s.contains("ServiceRequest")); + assert!(s.contains("test=1")); + assert!(s.contains("x-test")); + + let res = HttpResponse::Ok().header("x-test", "111").finish(); + let res = TestRequest::post() + .uri("/index.html?test=1") + .to_srv_response(res); + + let s = format!("{:?}", res); + assert!(s.contains("ServiceResponse")); + assert!(s.contains("x-test")); + } +} diff --git a/src/test.rs b/src/test.rs index f18fc2b31..209edac54 100644 --- a/src/test.rs +++ b/src/test.rs @@ -320,7 +320,7 @@ impl TestRequest { } /// Complete request creation and generate `ServiceRequest` instance - pub fn to_service(mut self) -> ServiceRequest { + pub fn to_srv_request(mut self) -> ServiceRequest { let req = self.req.finish(); ServiceRequest::new( @@ -331,16 +331,16 @@ impl TestRequest { ) } + /// Complete request creation and generate `ServiceResponse` instance + pub fn to_srv_response(self, res: HttpResponse) -> ServiceResponse { + self.to_srv_request().into_response(res) + } + /// Complete request creation and generate `Request` instance pub fn to_request(mut self) -> Request { self.req.finish() } - /// Complete request creation and generate `ServiceResponse` instance - pub fn to_response(self, res: HttpResponse) -> ServiceResponse { - self.to_service().into_response(res) - } - /// Complete request creation and generate `HttpRequest` instance pub fn to_http_request(mut self) -> HttpRequest { let req = self.req.finish(); diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index bafe578e9..dca3377c9 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -2,8 +2,11 @@ use net2::TcpBuilder; use std::sync::mpsc; use std::{net, thread, time::Duration}; +#[cfg(feature = "ssl")] +use openssl::ssl::SslAcceptorBuilder; + use actix_http::Response; -use actix_web::{web, App, HttpServer}; +use actix_web::{test, web, App, HttpServer}; fn unused_addr() -> net::SocketAddr { let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); @@ -76,3 +79,77 @@ fn test_start() { thread::sleep(Duration::from_millis(100)); let _ = sys.stop(); } + +#[cfg(feature = "ssl")] +fn ssl_acceptor() -> std::io::Result { + use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; + // load ssl keys + let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); + builder + .set_private_key_file("tests/key.pem", SslFiletype::PEM) + .unwrap(); + builder + .set_certificate_chain_file("tests/cert.pem") + .unwrap(); + Ok(builder) +} + +#[test] +#[cfg(feature = "ssl")] +fn test_start_ssl() { + let addr = unused_addr(); + let (tx, rx) = mpsc::channel(); + + thread::spawn(move || { + let sys = actix_rt::System::new("test"); + let builder = ssl_acceptor().unwrap(); + + let srv = HttpServer::new(|| { + App::new().service( + web::resource("/").route(web::to(|| Response::Ok().body("test"))), + ) + }) + .workers(1) + .shutdown_timeout(1) + .system_exit() + .disable_signals() + .bind_ssl(format!("{}", addr), builder) + .unwrap() + .start(); + + let _ = tx.send((srv, actix_rt::System::current())); + let _ = sys.run(); + }); + let (srv, sys) = rx.recv().unwrap(); + + let client = test::run_on(|| { + use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; + let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); + builder.set_verify(SslVerifyMode::NONE); + let _ = builder + .set_alpn_protos(b"\x02h2\x08http/1.1") + .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); + + Ok::<_, ()>( + awc::Client::build() + .connector( + awc::Connector::new() + .ssl(builder.build()) + .timeout(Duration::from_millis(100)) + .service(), + ) + .finish(), + ) + }) + .unwrap(); + let host = format!("https://{}", addr); + + let response = test::block_on(client.get(host.clone()).send()).unwrap(); + assert!(response.status().is_success()); + + // stop + let _ = srv.stop(false); + + thread::sleep(Duration::from_millis(100)); + let _ = sys.stop(); +} From 220c04b7b38ef39beb8cba0ef977d6c4f9e50aa4 Mon Sep 17 00:00:00 2001 From: dowwie Date: Mon, 1 Apr 2019 09:30:11 -0400 Subject: [PATCH 406/427] added docs for wrap and wrap_fn --- src/app.rs | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/app.rs b/src/app.rs index 9cdfc436a..0c5671f7b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -112,7 +112,26 @@ where self } - /// Register a middleware. + /// Registers heavyweight Application-level middleware, in the form of a + /// middleware type, that runs during inbound and/or outbound processing in the + /// request lifecycle (request -> response). + /// + /// ```rust + /// use actix_service::Service; + /// # use futures::Future; + /// use actix_web::{middleware, web, App}; + /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; + /// + /// fn index() -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new() + /// .wrap(middleware::Logger::default()) + /// .route("/index.html", web::get().to(index)); + /// } + /// ``` pub fn wrap( self, mw: F, @@ -152,7 +171,9 @@ where } } - /// Register a middleware function. + /// Registers lightweight Application-level middleware, in the form of a + /// closure, that runs during inbound and/or outbound processing in the + /// request lifecycle (request -> response). /// /// ```rust /// use actix_service::Service; @@ -400,7 +421,9 @@ where self } - /// Register a middleware. + /// Registers heavyweight Route-level middleware, in the form of a + /// middleware type, that runs during inbound and/or outbound processing in the + /// request lifecycle (request -> response). pub fn wrap( self, mw: F, @@ -440,7 +463,9 @@ where } } - /// Register a middleware function. + /// Registers lightweight Route-level middleware, in the form of a + /// closure, that runs during inbound and/or outbound processing in the + /// request lifecycle (request -> response). pub fn wrap_fn( self, mw: F, From 8800b8ef13d8b52d78a2125ffbb8951156638bca Mon Sep 17 00:00:00 2001 From: dowwie Date: Mon, 1 Apr 2019 09:59:21 -0400 Subject: [PATCH 407/427] mentioned re-use in wrap doc --- src/app.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app.rs b/src/app.rs index 0c5671f7b..cfa6c98e3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -113,8 +113,8 @@ where } /// Registers heavyweight Application-level middleware, in the form of a - /// middleware type, that runs during inbound and/or outbound processing in the - /// request lifecycle (request -> response). + /// re-usable middleware type, that runs during inbound and/or outbound + /// processing in the request lifecycle (request -> response). /// /// ```rust /// use actix_service::Service; @@ -422,8 +422,8 @@ where } /// Registers heavyweight Route-level middleware, in the form of a - /// middleware type, that runs during inbound and/or outbound processing in the - /// request lifecycle (request -> response). + /// re-usable middleware type, that runs during inbound and/or outbound + /// processing in the request lifecycle (request -> response). pub fn wrap( self, mw: F, From 96fd61f3d5451bbd97588f53378beb8274715863 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 1 Apr 2019 10:26:09 -0700 Subject: [PATCH 408/427] rust 1.31.0 compatibility --- .travis.yml | 1 + Cargo.toml | 4 ++++ actix-files/src/lib.rs | 6 ++++-- actix-http/CHANGES.md | 7 +++++++ actix-http/src/cookie/mod.rs | 12 ++++++------ src/lib.rs | 2 +- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index c3698625e..00f64d246 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ cache: matrix: include: + - rust: 1.31.0 - rust: stable - rust: beta - rust: nightly-2019-03-02 diff --git a/Cargo.toml b/Cargo.toml index 3abe3129b..b5d0e8769 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,3 +113,7 @@ flate2 = "1.0.2" lto = true opt-level = 3 codegen-units = 1 + +[patch.crates-io] +actix = { git = "https://github.com/actix/actix.git" } +actix-http = { path = "actix-http" } diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 54b4f9618..d5a47653e 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -566,7 +566,10 @@ mod tests { use bytes::BytesMut; use super::*; - use actix_web::http::{header, header::DispositionType, Method, StatusCode}; + use actix_web::http::header::{ + self, ContentDisposition, DispositionParam, DispositionType, + }; + use actix_web::http::{Method, StatusCode}; use actix_web::test::{self, TestRequest}; use actix_web::App; @@ -683,7 +686,6 @@ mod tests { #[test] fn test_named_file_image_attachment() { - use header::{ContentDisposition, DispositionParam, DispositionType}; let cd = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::Filename(String::from("test.png"))], diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 5659597c6..c5c02865c 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,5 +1,12 @@ # Changes +## [0.1.0-alpha.3] - 2019-04-xx + +### Fixed + +* Rust 1.31.0 compatibility + + ## [0.1.0-alpha.2] - 2019-03-29 ### Added diff --git a/actix-http/src/cookie/mod.rs b/actix-http/src/cookie/mod.rs index 5545624a8..0f5f45488 100644 --- a/actix-http/src/cookie/mod.rs +++ b/actix-http/src/cookie/mod.rs @@ -59,7 +59,7 @@ mod parse; #[macro_use] mod secure; #[cfg(feature = "secure-cookies")] -pub use secure::*; +pub use self::secure::*; use std::borrow::Cow; use std::fmt; @@ -68,11 +68,11 @@ use std::str::FromStr; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use time::{Duration, Tm}; -pub use builder::CookieBuilder; -pub use draft::*; -pub use jar::{CookieJar, Delta, Iter}; -use parse::parse_cookie; -pub use parse::ParseError; +pub use self::builder::CookieBuilder; +pub use self::draft::*; +pub use self::jar::{CookieJar, Delta, Iter}; +use self::parse::parse_cookie; +pub use self::parse::ParseError; #[derive(Debug, Clone)] enum CookieStr { diff --git a/src/lib.rs b/src/lib.rs index cb29fa5b0..ca4968833 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ //! * SSL support with OpenSSL or `native-tls` //! * Middlewares (`Logger`, `Session`, `CORS`, `CSRF`, `DefaultHeaders`) //! * Supports [Actix actor framework](https://github.com/actix/actix) -//! * Supported Rust version: 1.32 or later +//! * Supported Rust version: 1.31 or later //! //! ## Package feature //! From 6c195d85215b31f280e1f21399c2643e7bff922c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 1 Apr 2019 10:26:25 -0700 Subject: [PATCH 409/427] add Derev for ClientRequest --- awc/CHANGES.md | 8 ++++++++ awc/src/request.rs | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index e0e832144..6e4720359 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,5 +1,13 @@ # Changes + +## [0.1.0-alpha.3] - 2019-04-xx + +### Added + +* Added `Deref` for `ClientRequest`. + + ## [0.1.0-alpha.2] - 2019-03-29 ### Added diff --git a/awc/src/request.rs b/awc/src/request.rs index a462479ec..f732657d9 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -554,6 +554,20 @@ impl ClientRequest { } } +impl std::ops::Deref for ClientRequest { + type Target = RequestHead; + + fn deref(&self) -> &RequestHead { + &self.head + } +} + +impl std::ops::DerefMut for ClientRequest { + fn deref_mut(&mut self) -> &mut RequestHead { + &mut self.head + } +} + impl fmt::Debug for ClientRequest { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!( From c5fa6c1abe686ce9afc5a0fd456b1325dab54a01 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 1 Apr 2019 11:29:26 -0700 Subject: [PATCH 410/427] do not consume response --- awc/CHANGES.md | 7 +++++++ awc/Cargo.toml | 2 +- awc/src/lib.rs | 2 +- awc/src/response.rs | 17 +++++++++-------- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 6e4720359..9ca5b22d1 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -7,6 +7,13 @@ * Added `Deref` for `ClientRequest`. +* Export `MessageBody` type + + +### Changed + +* `ClientResponse::body()` does not consume response object. + ## [0.1.0-alpha.2] - 2019-03-29 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index c2cc9e7f0..ef9143ec2 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "0.1.0-alpha.2" +version = "0.1.0-alpha.3" authors = ["Nikolay Kim "] description = "Actix http client." readme = "README.md" diff --git a/awc/src/lib.rs b/awc/src/lib.rs index ff1fb3fef..e2c04dbb8 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -39,7 +39,7 @@ pub mod ws; pub use self::builder::ClientBuilder; pub use self::request::ClientRequest; -pub use self::response::ClientResponse; +pub use self::response::{ClientResponse, MessageBody}; use self::connect::{Connect, ConnectorWrapper}; diff --git a/awc/src/response.rs b/awc/src/response.rs index 038a9a330..2548d7197 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -105,7 +105,7 @@ where S: Stream + 'static, { /// Load http response's body. - pub fn body(self) -> MessageBody { + pub fn body(&mut self) -> MessageBody { MessageBody::new(self) } } @@ -137,7 +137,7 @@ impl fmt::Debug for ClientResponse { pub struct MessageBody { limit: usize, length: Option, - stream: Option>, + stream: Option>, err: Option, fut: Option>>, } @@ -147,7 +147,7 @@ where S: Stream + 'static, { /// Create `MessageBody` for request. - pub fn new(res: ClientResponse) -> MessageBody { + pub fn new(res: &mut ClientResponse) -> MessageBody { let mut len = None; if let Some(l) = res.headers().get(CONTENT_LENGTH) { if let Ok(s) = l.to_str() { @@ -164,7 +164,7 @@ where MessageBody { limit: 262_144, length: len, - stream: Some(res), + stream: Some(res.take_payload()), fut: None, err: None, } @@ -239,19 +239,20 @@ mod tests { #[test] fn test_body() { - let req = TestResponse::with_header(header::CONTENT_LENGTH, "xxxx").finish(); + let mut req = TestResponse::with_header(header::CONTENT_LENGTH, "xxxx").finish(); match req.body().poll().err().unwrap() { PayloadError::UnknownLength => (), _ => unreachable!("error"), } - let req = TestResponse::with_header(header::CONTENT_LENGTH, "1000000").finish(); + let mut req = + TestResponse::with_header(header::CONTENT_LENGTH, "1000000").finish(); match req.body().poll().err().unwrap() { PayloadError::Overflow => (), _ => unreachable!("error"), } - let req = TestResponse::default() + let mut req = TestResponse::default() .set_payload(Bytes::from_static(b"test")) .finish(); match req.body().poll().ok().unwrap() { @@ -259,7 +260,7 @@ mod tests { _ => unreachable!("error"), } - let req = TestResponse::default() + let mut req = TestResponse::default() .set_payload(Bytes::from_static(b"11111111111111")) .finish(); match req.body().limit(5).poll().err().unwrap() { From 5c4e4edda4e057e9dac379b01c5b183a4c70278d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 1 Apr 2019 11:51:18 -0700 Subject: [PATCH 411/427] add ClientResponse::json() --- awc/CHANGES.md | 2 + awc/Cargo.toml | 2 +- awc/src/error.rs | 27 ++++++ awc/src/lib.rs | 2 +- awc/src/response.rs | 194 ++++++++++++++++++++++++++++++++++++++++++-- awc/src/test.rs | 12 ++- src/error.rs | 2 +- 7 files changed, 232 insertions(+), 9 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 9ca5b22d1..c33593185 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -9,6 +9,8 @@ * Export `MessageBody` type +* `ClientResponse::json()` - Loads and parse `application/json` encoded body + ### Changed diff --git a/awc/Cargo.toml b/awc/Cargo.toml index ef9143ec2..fdaf0a55b 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -44,6 +44,7 @@ bytes = "0.4" derive_more = "0.14" futures = "0.1.25" log =" 0.4" +mime = "0.3" percent-encoding = "1.0" rand = "0.6" serde = "1.0" @@ -62,6 +63,5 @@ actix-server = { version = "0.4.1", features=["ssl"] } brotli2 = { version="0.3.2" } flate2 = { version="1.0.2" } env_logger = "0.6" -mime = "0.3" rand = "0.6" tokio-tcp = "0.1" \ No newline at end of file diff --git a/awc/src/error.rs b/awc/src/error.rs index 8f51fd7db..bbfd9b971 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -4,6 +4,9 @@ pub use actix_http::error::PayloadError; pub use actix_http::ws::HandshakeError as WsHandshakeError; pub use actix_http::ws::ProtocolError as WsProtocolError; +use actix_http::{Response, ResponseError}; +use serde_json::error::Error as JsonError; + use actix_http::http::{header::HeaderValue, Error as HttpError, StatusCode}; use derive_more::{Display, From}; @@ -47,3 +50,27 @@ impl From for WsClientError { WsClientError::SendRequest(err.into()) } } + +/// A set of errors that can occur during parsing json payloads +#[derive(Debug, Display, From)] +pub enum JsonPayloadError { + /// Payload size is bigger than allowed. (default: 32kB) + #[display(fmt = "Json payload size is bigger than allowed.")] + Overflow, + /// Content type error + #[display(fmt = "Content type error")] + ContentType, + /// Deserialize error + #[display(fmt = "Json deserialize error: {}", _0)] + Deserialize(JsonError), + /// Payload error + #[display(fmt = "Error that occur during reading payload: {}", _0)] + Payload(PayloadError), +} + +/// Return `InternlaServerError` for `JsonPayloadError` +impl ResponseError for JsonPayloadError { + fn error_response(&self) -> Response { + Response::new(StatusCode::INTERNAL_SERVER_ERROR) + } +} diff --git a/awc/src/lib.rs b/awc/src/lib.rs index e2c04dbb8..8d0ac6a58 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -39,7 +39,7 @@ pub mod ws; pub use self::builder::ClientBuilder; pub use self::request::ClientRequest; -pub use self::response::{ClientResponse, MessageBody}; +pub use self::response::{ClientResponse, JsonBody, MessageBody}; use self::connect::{Connect, ConnectorWrapper}; diff --git a/awc/src/response.rs b/awc/src/response.rs index 2548d7197..b91735208 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -4,13 +4,14 @@ use std::fmt; use bytes::{Bytes, BytesMut}; use futures::{Future, Poll, Stream}; -use actix_http::error::PayloadError; +use actix_http::cookie::Cookie; +use actix_http::error::{CookieParseError, PayloadError}; use actix_http::http::header::{CONTENT_LENGTH, SET_COOKIE}; use actix_http::http::{HeaderMap, StatusCode, Version}; use actix_http::{Extensions, HttpMessage, Payload, PayloadStream, ResponseHead}; +use serde::de::DeserializeOwned; -use actix_http::cookie::Cookie; -use actix_http::error::CookieParseError; +use crate::error::JsonPayloadError; /// Client Response pub struct ClientResponse { @@ -104,10 +105,21 @@ impl ClientResponse where S: Stream + 'static, { - /// Load http response's body. + /// Loads http response's body. pub fn body(&mut self) -> MessageBody { MessageBody::new(self) } + + /// Loads and parse `application/json` encoded body. + /// Return `JsonBody` future. It resolves to a `T` value. + /// + /// Returns error: + /// + /// * content type is not `application/json` + /// * content length is greater than 256k + pub fn json(&mut self) -> JsonBody { + JsonBody::new(self) + } } impl Stream for ClientResponse @@ -230,12 +242,115 @@ where } } +/// Response's payload json parser, it resolves to a deserialized `T` value. +/// +/// Returns error: +/// +/// * content type is not `application/json` +/// * content length is greater than 64k +pub struct JsonBody { + limit: usize, + length: Option, + stream: Payload, + err: Option, + fut: Option>>, +} + +impl JsonBody +where + S: Stream + 'static, + U: DeserializeOwned, +{ + /// Create `JsonBody` for request. + pub fn new(req: &mut ClientResponse) -> Self { + // check content-type + let json = if let Ok(Some(mime)) = req.mime_type() { + mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) + } else { + false + }; + if !json { + return JsonBody { + limit: 65536, + length: None, + stream: Payload::None, + fut: None, + err: Some(JsonPayloadError::ContentType), + }; + } + + let mut len = None; + if let Some(l) = req.headers().get(CONTENT_LENGTH) { + if let Ok(s) = l.to_str() { + if let Ok(l) = s.parse::() { + len = Some(l) + } + } + } + + JsonBody { + limit: 65536, + length: len, + stream: req.take_payload(), + fut: None, + err: None, + } + } + + /// Change max size of payload. By default max size is 64Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } +} + +impl Future for JsonBody +where + T: Stream + 'static, + U: DeserializeOwned + 'static, +{ + type Item = U; + type Error = JsonPayloadError; + + fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.fut { + return fut.poll(); + } + + if let Some(err) = self.err.take() { + return Err(err); + } + + let limit = self.limit; + if let Some(len) = self.length.take() { + if len > limit { + return Err(JsonPayloadError::Overflow); + } + } + + let fut = std::mem::replace(&mut self.stream, Payload::None) + .from_err() + .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { + if (body.len() + chunk.len()) > limit { + Err(JsonPayloadError::Overflow) + } else { + body.extend_from_slice(&chunk); + Ok(body) + } + }) + .and_then(|body| Ok(serde_json::from_slice::(&body)?)); + self.fut = Some(Box::new(fut)); + self.poll() + } +} + #[cfg(test)] mod tests { use super::*; use futures::Async; + use serde::{Deserialize, Serialize}; - use crate::{http::header, test::TestResponse}; + use crate::{http::header, test::block_on, test::TestResponse}; #[test] fn test_body() { @@ -268,4 +383,73 @@ mod tests { _ => unreachable!("error"), } } + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct MyObject { + name: String, + } + + fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { + match err { + JsonPayloadError::Overflow => match other { + JsonPayloadError::Overflow => true, + _ => false, + }, + JsonPayloadError::ContentType => match other { + JsonPayloadError::ContentType => true, + _ => false, + }, + _ => false, + } + } + + #[test] + fn test_json_body() { + let mut req = TestResponse::default().finish(); + let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); + assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); + + let mut req = TestResponse::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/text"), + ) + .finish(); + let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); + assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); + + let mut req = TestResponse::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ) + .header( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("10000"), + ) + .finish(); + + let json = block_on(JsonBody::<_, MyObject>::new(&mut req).limit(100)); + assert!(json_eq(json.err().unwrap(), JsonPayloadError::Overflow)); + + let mut req = TestResponse::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ) + .header( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + ) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .finish(); + + let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); + assert_eq!( + json.ok().unwrap(), + MyObject { + name: "test".to_owned() + } + ); + } } diff --git a/awc/src/test.rs b/awc/src/test.rs index 5e595d152..1c772905e 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -6,6 +6,8 @@ use actix_http::http::header::{self, Header, HeaderValue, IntoHeaderValue}; use actix_http::http::{HeaderName, HttpTryFrom, Version}; use actix_http::{h1, Payload, ResponseHead}; use bytes::Bytes; +#[cfg(test)] +use futures::Future; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use crate::ClientResponse; @@ -18,7 +20,7 @@ thread_local! { } #[cfg(test)] -pub fn run_on(f: F) -> R +pub(crate) fn run_on(f: F) -> R where F: Fn() -> R, { @@ -29,6 +31,14 @@ where .unwrap() } +#[cfg(test)] +pub(crate) fn block_on(f: F) -> Result +where + F: Future, +{ + RT.with(move |rt| rt.borrow_mut().block_on(f)) +} + /// Test `ClientResponse` builder pub struct TestResponse { head: ResponseHead, diff --git a/src/error.rs b/src/error.rs index 02e17241f..78dc2fb6a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -79,7 +79,7 @@ pub enum JsonPayloadError { Payload(PayloadError), } -/// Return `BadRequest` for `UrlencodedError` +/// Return `BadRequest` for `JsonPayloadError` impl ResponseError for JsonPayloadError { fn error_response(&self) -> HttpResponse { match *self { From 03dfbdfcdd16ea7e863d76e59c742af802f2f513 Mon Sep 17 00:00:00 2001 From: dowwie Date: Mon, 1 Apr 2019 14:52:05 -0400 Subject: [PATCH 412/427] updated wrap and wrap fn descriptions, still requiring viable examples --- src/app.rs | 38 ++++++++++++++++++++++++++------------ src/scope.rs | 20 ++++++++++++-------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/app.rs b/src/app.rs index cfa6c98e3..9535dac21 100644 --- a/src/app.rs +++ b/src/app.rs @@ -112,9 +112,12 @@ where self } - /// Registers heavyweight Application-level middleware, in the form of a - /// re-usable middleware type, that runs during inbound and/or outbound - /// processing in the request lifecycle (request -> response). + /// Registers middleware, in the form of a middleware component (type), + /// that runs during inbound and/or outbound processing in the request + /// lifecycle (request -> response), modifying request/response as + /// necessary, across all requests managed by the *Application*. + /// + /// Use middleware when you need to read or modify *every* request or response in some way. /// /// ```rust /// use actix_service::Service; @@ -171,9 +174,12 @@ where } } - /// Registers lightweight Application-level middleware, in the form of a - /// closure, that runs during inbound and/or outbound processing in the - /// request lifecycle (request -> response). + /// Registers middleware, in the form of a closure, that runs during inbound + /// and/or outbound processing in the request lifecycle (request -> response), + /// modifying request/response as necessary, across all requests managed by + /// the *Application*. + /// + /// Use middleware when you need to read or modify *every* request or response in some way. /// /// ```rust /// use actix_service::Service; @@ -421,9 +427,13 @@ where self } - /// Registers heavyweight Route-level middleware, in the form of a - /// re-usable middleware type, that runs during inbound and/or outbound - /// processing in the request lifecycle (request -> response). + /// Registers middleware, in the form of a middleware component (type), + /// that runs during inbound and/or outbound processing in the request + /// lifecycle (request -> response), modifying request/response as + /// necessary, across all requests managed by the *Route*. + /// + /// Use middleware when you need to read or modify *every* request or response in some way. + /// pub fn wrap( self, mw: F, @@ -463,9 +473,13 @@ where } } - /// Registers lightweight Route-level middleware, in the form of a - /// closure, that runs during inbound and/or outbound processing in the - /// request lifecycle (request -> response). + /// Registers middleware, in the form of a closure, that runs during inbound + /// and/or outbound processing in the request lifecycle (request -> response), + /// modifying request/response as necessary, across all requests managed by + /// the *Route*. + /// + /// Use middleware when you need to read or modify *every* request or response in some way. + /// pub fn wrap_fn( self, mw: F, diff --git a/src/scope.rs b/src/scope.rs index d45609c5e..3fdc4ccb5 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -200,11 +200,14 @@ where self } - /// Register a scope level middleware. + /// Registers middleware, in the form of a middleware component (type), + /// that runs during inbound processing in the request + /// lifecycle (request -> response), modifying request as + /// necessary, across all requests managed by the *Scope*. Note that + /// Scope-level middleware is only used for inbound requests, not outbound + /// responses. /// - /// This is similar to `App's` middlewares, but middleware get invoked on scope level. - /// Scope level middlewares are not allowed to change response - /// type (i.e modify response's body). + /// Use middleware when you need to read or modify *every* request in some way. pub fn wrap( self, mw: F, @@ -238,10 +241,11 @@ where } } - /// Register a scope level middleware function. - /// - /// This function accepts instance of `ServiceRequest` type and - /// mutable reference to the next middleware in chain. + /// Registers middleware, in the form of a closure, that runs during inbound + /// processing in the request lifecycle (request -> response), modifying + /// request as necessary, across all requests managed by the *Scope*. + /// Note that Scope-level middleware is only used for inbound requests, + /// not outbound responses. /// /// ```rust /// use actix_service::Service; From 3dd3f7bc92aa25ef700d05ddf23ff346d8318204 Mon Sep 17 00:00:00 2001 From: dowwie Date: Mon, 1 Apr 2019 15:10:28 -0400 Subject: [PATCH 413/427] updated scope wrap doc --- src/scope.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/scope.rs b/src/scope.rs index 3fdc4ccb5..0dfaaf066 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -203,9 +203,10 @@ where /// Registers middleware, in the form of a middleware component (type), /// that runs during inbound processing in the request /// lifecycle (request -> response), modifying request as - /// necessary, across all requests managed by the *Scope*. Note that - /// Scope-level middleware is only used for inbound requests, not outbound - /// responses. + /// necessary, across all requests managed by the *Scope*. Scope-level + /// middleware is more limited in what it can modify, relative to Route or + /// Application level middleware, in that Scope-level middleware can not modify + /// ServiceResponse. /// /// Use middleware when you need to read or modify *every* request in some way. pub fn wrap( @@ -244,8 +245,9 @@ where /// Registers middleware, in the form of a closure, that runs during inbound /// processing in the request lifecycle (request -> response), modifying /// request as necessary, across all requests managed by the *Scope*. - /// Note that Scope-level middleware is only used for inbound requests, - /// not outbound responses. + /// Scope-level middleware is more limited in what it can modify, relative + /// to Route or Application level middleware, in that Scope-level middleware + /// can not modify ServiceResponse. /// /// ```rust /// use actix_service::Service; From 38afc933046f3ddaf953576ae91be39506b8bbbf Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 1 Apr 2019 15:19:34 -0700 Subject: [PATCH 414/427] Use non-consuming builder pattern for ClientRequest --- awc/CHANGES.md | 2 + awc/src/lib.rs | 4 +- awc/src/request.rs | 281 +++++++++++++++++++++++++-------------------- 3 files changed, 161 insertions(+), 126 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index c33593185..4ae63493e 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -14,6 +14,8 @@ ### Changed +* Use non-consuming builder pattern for `ClientRequest`. + * `ClientResponse::body()` does not consume response object. diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 8d0ac6a58..db994431b 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -105,7 +105,7 @@ impl Client { let mut req = ClientRequest::new(method, url, self.0.clone()); for (key, value) in &self.0.headers { - req.head.headers.insert(key.clone(), value.clone()); + req.set_header_if_none(key.clone(), value.clone()); } req } @@ -120,7 +120,7 @@ impl Client { { let mut req = self.request(head.method.clone(), url); for (key, value) in &head.headers { - req.head.headers.insert(key.clone(), value.clone()); + req.set_header_if_none(key.clone(), value.clone()); } req } diff --git a/awc/src/request.rs b/awc/src/request.rs index f732657d9..807a28978 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -58,7 +58,7 @@ const HTTPS_ENCODING: &str = "gzip, deflate"; /// } /// ``` pub struct ClientRequest { - pub(crate) head: RequestHead, + pub(crate) head: Option, err: Option, cookies: Option, default_headers: bool, @@ -73,36 +73,40 @@ impl ClientRequest { where Uri: HttpTryFrom, { - ClientRequest { + let mut req = ClientRequest { config, - head: RequestHead::default(), + head: Some(RequestHead::default()), err: None, cookies: None, timeout: None, default_headers: true, response_decompress: true, - } - .method(method) - .uri(uri) + }; + req.method(method).uri(uri); + req } /// Set HTTP URI of request. #[inline] - pub fn uri(mut self, uri: U) -> Self + pub fn uri(&mut self, uri: U) -> &mut Self where Uri: HttpTryFrom, { - match Uri::try_from(uri) { - Ok(uri) => self.head.uri = uri, - Err(e) => self.err = Some(e.into()), + if let Some(head) = parts(&mut self.head, &self.err) { + match Uri::try_from(uri) { + Ok(uri) => head.uri = uri, + Err(e) => self.err = Some(e.into()), + } } self } /// Set HTTP method of this request. #[inline] - pub fn method(mut self, method: Method) -> Self { - self.head.method = method; + pub fn method(&mut self, method: Method) -> &mut Self { + if let Some(head) = parts(&mut self.head, &self.err) { + head.method = method; + } self } @@ -111,8 +115,10 @@ impl ClientRequest { /// /// By default requests's HTTP version depends on network stream #[inline] - pub fn version(mut self, version: Version) -> Self { - self.head.version = version; + pub fn version(&mut self, version: Version) -> &mut Self { + if let Some(head) = parts(&mut self.head, &self.err) { + head.version = version; + } self } @@ -129,12 +135,14 @@ impl ClientRequest { /// # })); /// } /// ``` - pub fn set(mut self, hdr: H) -> Self { - match hdr.try_into() { - Ok(value) => { - self.head.headers.insert(H::name(), value); + pub fn set(&mut self, hdr: H) -> &mut Self { + if let Some(head) = parts(&mut self.head, &self.err) { + match hdr.try_into() { + Ok(value) => { + head.headers.insert(H::name(), value); + } + Err(e) => self.err = Some(e.into()), } - Err(e) => self.err = Some(e.into()), } self } @@ -157,95 +165,106 @@ impl ClientRequest { /// # })); /// } /// ``` - pub fn header(mut self, key: K, value: V) -> Self + pub fn header(&mut self, key: K, value: V) -> &mut Self where HeaderName: HttpTryFrom, V: IntoHeaderValue, { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - self.head.headers.append(key, value); - } + if let Some(head) = parts(&mut self.head, &self.err) { + match HeaderName::try_from(key) { + Ok(key) => match value.try_into() { + Ok(value) => { + head.headers.append(key, value); + } + Err(e) => self.err = Some(e.into()), + }, Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), + } } self } /// Insert a header, replaces existing header. - pub fn set_header(mut self, key: K, value: V) -> Self + pub fn set_header(&mut self, key: K, value: V) -> &mut Self where HeaderName: HttpTryFrom, V: IntoHeaderValue, { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - self.head.headers.insert(key, value); - } + if let Some(head) = parts(&mut self.head, &self.err) { + match HeaderName::try_from(key) { + Ok(key) => match value.try_into() { + Ok(value) => { + head.headers.insert(key, value); + } + Err(e) => self.err = Some(e.into()), + }, Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), + } } self } /// Insert a header only if it is not yet set. - pub fn set_header_if_none(mut self, key: K, value: V) -> Self + pub fn set_header_if_none(&mut self, key: K, value: V) -> &mut Self where HeaderName: HttpTryFrom, V: IntoHeaderValue, { - match HeaderName::try_from(key) { - Ok(key) => { - if !self.head.headers.contains_key(&key) { - match value.try_into() { - Ok(value) => { - self.head.headers.insert(key, value); + if let Some(head) = parts(&mut self.head, &self.err) { + match HeaderName::try_from(key) { + Ok(key) => { + if !head.headers.contains_key(&key) { + match value.try_into() { + Ok(value) => { + head.headers.insert(key, value); + } + Err(e) => self.err = Some(e.into()), } - Err(e) => self.err = Some(e.into()), } } + Err(e) => self.err = Some(e.into()), } - Err(e) => self.err = Some(e.into()), } self } - /// Close connection + /// Close connection instead of returning it back to connections pool. + /// This setting affect only http/1 connections. #[inline] - pub fn close_connection(mut self) -> Self { - self.head.set_connection_type(ConnectionType::Close); + pub fn close_connection(&mut self) -> &mut Self { + if let Some(head) = parts(&mut self.head, &self.err) { + head.set_connection_type(ConnectionType::Close); + } self } /// Set request's content type #[inline] - pub fn content_type(mut self, value: V) -> Self + pub fn content_type(&mut self, value: V) -> &mut Self where HeaderValue: HttpTryFrom, { - match HeaderValue::try_from(value) { - Ok(value) => { - let _ = self.head.headers.insert(header::CONTENT_TYPE, value); + if let Some(head) = parts(&mut self.head, &self.err) { + match HeaderValue::try_from(value) { + Ok(value) => { + let _ = head.headers.insert(header::CONTENT_TYPE, value); + } + Err(e) => self.err = Some(e.into()), } - Err(e) => self.err = Some(e.into()), } self } /// Set content length #[inline] - pub fn content_length(self, len: u64) -> Self { + pub fn content_length(&mut self, len: u64) -> &mut Self { let mut wrt = BytesMut::new().writer(); let _ = write!(wrt, "{}", len); self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze()) } /// Set HTTP basic authorization header - pub fn basic_auth(self, username: U, password: Option<&str>) -> Self + pub fn basic_auth(&mut self, username: U, password: Option<&str>) -> &mut Self where U: fmt::Display, { @@ -260,7 +279,7 @@ impl ClientRequest { } /// Set HTTP bearer authentication header - pub fn bearer_auth(self, token: T) -> Self + pub fn bearer_auth(&mut self, token: T) -> &mut Self where T: fmt::Display, { @@ -292,7 +311,7 @@ impl ClientRequest { /// })); /// } /// ``` - pub fn cookie<'c>(mut self, cookie: Cookie<'c>) -> Self { + pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { if self.cookies.is_none() { let mut jar = CookieJar::new(); jar.add(cookie.into_owned()); @@ -305,13 +324,13 @@ impl ClientRequest { /// Do not add default request headers. /// By default `Date` and `User-Agent` headers are set. - pub fn no_default_headers(mut self) -> Self { + pub fn no_default_headers(&mut self) -> &mut Self { self.default_headers = false; self } /// Disable automatic decompress of response's body - pub fn no_decompress(mut self) -> Self { + pub fn no_decompress(&mut self) -> &mut Self { self.response_decompress = false; self } @@ -320,38 +339,38 @@ impl ClientRequest { /// /// Request timeout is the total time before a response must be received. /// Default value is 5 seconds. - pub fn timeout(mut self, timeout: Duration) -> Self { + pub fn timeout(&mut self, timeout: Duration) -> &mut Self { self.timeout = Some(timeout); self } /// This method calls provided closure with builder reference if /// value is `true`. - pub fn if_true(mut self, value: bool, f: F) -> Self + pub fn if_true(&mut self, value: bool, f: F) -> &mut Self where F: FnOnce(&mut ClientRequest), { if value { - f(&mut self); + f(self); } self } /// This method calls provided closure with builder reference if /// value is `Some`. - pub fn if_some(mut self, value: Option, f: F) -> Self + pub fn if_some(&mut self, value: Option, f: F) -> &mut Self where F: FnOnce(T, &mut ClientRequest), { if let Some(val) = value { - f(val, &mut self); + f(val, self); } self } /// Complete request construction and send body. pub fn send_body( - mut self, + &mut self, body: B, ) -> impl Future< Item = ClientResponse>, @@ -364,8 +383,10 @@ impl ClientRequest { return Either::A(err(e.into())); } + let mut head = self.head.take().expect("cannot reuse response builder"); + // validate uri - let uri = &self.head.uri; + let uri = &head.uri; if uri.host().is_none() { return Either::A(err(InvalidUrl::MissingHost.into())); } else if uri.scheme_part().is_none() { @@ -380,20 +401,20 @@ impl ClientRequest { } // set default headers - let slf = if self.default_headers { + if self.default_headers { // set request host header - if let Some(host) = self.head.uri.host() { - if !self.head.headers.contains_key(header::HOST) { + if let Some(host) = head.uri.host() { + if !head.headers.contains_key(header::HOST) { let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); - let _ = match self.head.uri.port_u16() { + let _ = match head.uri.port_u16() { None | Some(80) | Some(443) => write!(wrt, "{}", host), Some(port) => write!(wrt, "{}:{}", host, port), }; match wrt.get_mut().take().freeze().try_into() { Ok(value) => { - self.head.headers.insert(header::HOST, value); + head.headers.insert(header::HOST, value); } Err(e) => return Either::A(err(HttpError::from(e).into())), } @@ -404,14 +425,11 @@ impl ClientRequest { self.set_header_if_none( header::USER_AGENT, concat!("awc/", env!("CARGO_PKG_VERSION")), - ) - } else { - self - }; + ); + } // enable br only for https - let https = slf - .head + let https = head .uri .scheme_part() .map(|s| s == &uri::Scheme::HTTPS) @@ -422,23 +440,19 @@ impl ClientRequest { feature = "flate2-zlib", feature = "flate2-rust" ))] - let mut slf = { + { if https { - slf.set_header_if_none(header::ACCEPT_ENCODING, HTTPS_ENCODING) + self.set_header_if_none(header::ACCEPT_ENCODING, HTTPS_ENCODING); } else { #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] { - slf.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate") + self.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate"); } - #[cfg(not(any(feature = "flate2-zlib", feature = "flate2-rust")))] - slf } - }; - - let mut head = slf.head; + } // set cookies - if let Some(ref mut jar) = slf.cookies { + if let Some(ref mut jar) = self.cookies { let mut cookie = String::new(); for c in jar.delta() { let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); @@ -451,8 +465,8 @@ impl ClientRequest { ); } - let config = slf.config; - let response_decompress = slf.response_decompress; + let config = self.config.as_ref(); + let response_decompress = self.response_decompress; let fut = config .connector @@ -469,7 +483,7 @@ impl ClientRequest { }); // set request timeout - if let Some(timeout) = slf.timeout.or_else(|| config.timeout.clone()) { + if let Some(timeout) = self.timeout.or_else(|| config.timeout.clone()) { Either::B(Either::A(Timeout::new(fut, timeout).map_err(|e| { if let Some(e) = e.into_inner() { e @@ -484,7 +498,7 @@ impl ClientRequest { /// Set a JSON body and generate `ClientRequest` pub fn send_json( - self, + &mut self, value: T, ) -> impl Future< Item = ClientResponse>, @@ -494,21 +508,18 @@ impl ClientRequest { Ok(body) => body, Err(e) => return Either::A(err(Error::from(e).into())), }; - // set content-type - let slf = if !self.head.headers.contains_key(header::CONTENT_TYPE) { - self.header(header::CONTENT_TYPE, "application/json") - } else { - self - }; - Either::B(slf.send_body(Body::Bytes(Bytes::from(body)))) + // set content-type + self.set_header_if_none(header::CONTENT_TYPE, "application/json"); + + Either::B(self.send_body(Body::Bytes(Bytes::from(body)))) } /// Set a urlencoded body and generate `ClientRequest` /// /// `ClientRequestBuilder` can not be used after this call. pub fn send_form( - self, + &mut self, value: T, ) -> impl Future< Item = ClientResponse>, @@ -519,18 +530,18 @@ impl ClientRequest { Err(e) => return Either::A(err(Error::from(e).into())), }; - let slf = if !self.head.headers.contains_key(header::CONTENT_TYPE) { - self.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") - } else { - self - }; + // set content-type + self.set_header_if_none( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ); - Either::B(slf.send_body(Body::Bytes(Bytes::from(body)))) + Either::B(self.send_body(Body::Bytes(Bytes::from(body)))) } /// Set an streaming body and generate `ClientRequest`. pub fn send_stream( - self, + &mut self, stream: S, ) -> impl Future< Item = ClientResponse>, @@ -545,7 +556,7 @@ impl ClientRequest { /// Set an empty body and generate `ClientRequest`. pub fn send( - self, + &mut self, ) -> impl Future< Item = ClientResponse>, Error = SendRequestError, @@ -558,31 +569,44 @@ impl std::ops::Deref for ClientRequest { type Target = RequestHead; fn deref(&self) -> &RequestHead { - &self.head + self.head.as_ref().expect("cannot reuse response builder") } } impl std::ops::DerefMut for ClientRequest { fn deref_mut(&mut self) -> &mut RequestHead { - &mut self.head + self.head.as_mut().expect("cannot reuse response builder") } } impl fmt::Debug for ClientRequest { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let head = self.head.as_ref().expect("cannot reuse response builder"); + writeln!( f, "\nClientRequest {:?} {}:{}", - self.head.version, self.head.method, self.head.uri + head.version, head.method, head.uri )?; writeln!(f, " headers:")?; - for (key, val) in self.head.headers.iter() { + for (key, val) in head.headers.iter() { writeln!(f, " {:?}: {:?}", key, val)?; } Ok(()) } } +#[inline] +fn parts<'a>( + parts: &'a mut Option, + err: &Option, +) -> Option<&'a mut RequestHead> { + if err.is_some() { + return None; + } + parts.as_mut() +} + #[cfg(test)] mod tests { use super::*; @@ -591,7 +615,8 @@ mod tests { #[test] fn test_debug() { test::run_on(|| { - let request = Client::new().get("/").header("x-test", "111"); + let mut request = Client::new().get("/"); + request.header("x-test", "111"); let repr = format!("{:?}", request); assert!(repr.contains("ClientRequest")); assert!(repr.contains("x-test")); @@ -608,6 +633,8 @@ mod tests { assert_eq!( req.head + .as_ref() + .unwrap() .headers .get(header::CONTENT_TYPE) .unwrap() @@ -621,14 +648,16 @@ mod tests { #[test] fn test_client_header_override() { test::run_on(|| { - let req = Client::build() + let mut req = Client::build() .header(header::CONTENT_TYPE, "111") .finish() - .get("/") - .set_header(header::CONTENT_TYPE, "222"); + .get("/"); + req.set_header(header::CONTENT_TYPE, "222"); assert_eq!( req.head + .as_ref() + .unwrap() .headers .get(header::CONTENT_TYPE) .unwrap() @@ -642,12 +671,12 @@ mod tests { #[test] fn client_basic_auth() { test::run_on(|| { - let client = Client::new() - .get("/") - .basic_auth("username", Some("password")); + let mut req = Client::new().get("/"); + req.basic_auth("username", Some("password")); assert_eq!( - client - .head + req.head + .as_ref() + .unwrap() .headers .get(header::AUTHORIZATION) .unwrap() @@ -656,10 +685,12 @@ mod tests { "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" ); - let client = Client::new().get("/").basic_auth("username", None); + let mut req = Client::new().get("/"); + req.basic_auth("username", None); assert_eq!( - client - .head + req.head + .as_ref() + .unwrap() .headers .get(header::AUTHORIZATION) .unwrap() @@ -673,10 +704,12 @@ mod tests { #[test] fn client_bearer_auth() { test::run_on(|| { - let client = Client::new().get("/").bearer_auth("someS3cr3tAutht0k3n"); + let mut req = Client::new().get("/"); + req.bearer_auth("someS3cr3tAutht0k3n"); assert_eq!( - client - .head + req.head + .as_ref() + .unwrap() .headers .get(header::AUTHORIZATION) .unwrap() From 2d4348927880c873bbe59af66ae7d3cd689a178e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 1 Apr 2019 17:53:30 -0700 Subject: [PATCH 415/427] ClientRequest::json() accepts reference instead of object --- awc/CHANGES.md | 2 ++ awc/src/request.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 4ae63493e..cd9635074 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -16,6 +16,8 @@ * Use non-consuming builder pattern for `ClientRequest`. +* `ClientRequest::json()` accepts reference instead of object. + * `ClientResponse::body()` does not consume response object. diff --git a/awc/src/request.rs b/awc/src/request.rs index 807a28978..78404b31b 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -499,7 +499,7 @@ impl ClientRequest { /// Set a JSON body and generate `ClientRequest` pub fn send_json( &mut self, - value: T, + value: &T, ) -> impl Future< Item = ClientResponse>, Error = SendRequestError, From 1bd0995d7aedbe263ecb26e4d798024f5543df79 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 1 Apr 2019 18:00:38 -0700 Subject: [PATCH 416/427] remove unneded & --- awc/src/request.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awc/src/request.rs b/awc/src/request.rs index 78404b31b..0b89581a2 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -504,7 +504,7 @@ impl ClientRequest { Item = ClientResponse>, Error = SendRequestError, > { - let body = match serde_json::to_string(&value) { + let body = match serde_json::to_string(value) { Ok(body) => body, Err(e) => return Either::A(err(Error::from(e).into())), }; @@ -520,12 +520,12 @@ impl ClientRequest { /// `ClientRequestBuilder` can not be used after this call. pub fn send_form( &mut self, - value: T, + value: &T, ) -> impl Future< Item = ClientResponse>, Error = SendRequestError, > { - let body = match serde_urlencoded::to_string(&value) { + let body = match serde_urlencoded::to_string(value) { Ok(body) => body, Err(e) => return Either::A(err(Error::from(e).into())), }; From c27fbdc35f2c86fa56dc62088abed2eb108aeccd Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 2 Apr 2019 10:19:56 -0700 Subject: [PATCH 417/427] Preallocate read buffer for h1 codec, #749 --- actix-http/CHANGES.md | 2 ++ actix-http/Cargo.toml | 2 +- actix-http/src/h1/codec.rs | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index c5c02865c..79187e7a5 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -6,6 +6,8 @@ * Rust 1.31.0 compatibility +* Preallocate read buffer for h1 codec + ## [0.1.0-alpha.2] - 2019-03-29 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 2de624b7c..a9fda44eb 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "0.1.0-alpha.2" +version = "0.1.0-alpha.3" authors = ["Nikolay Kim "] description = "Actix http primitives" readme = "README.md" diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index ceb1027e5..e4895f2dd 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -19,6 +19,9 @@ use crate::message::{ConnectionType, Head, ResponseHead}; use crate::request::Request; use crate::response::Response; +const LW: usize = 2 * 1024; +const HW: usize = 32 * 1024; + bitflags! { struct Flags: u8 { const HEAD = 0b0000_0001; @@ -105,6 +108,11 @@ impl Decoder for Codec { type Error = ParseError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + let cap = src.capacity(); + if cap < LW { + src.reserve(HW - cap); + } + if self.payload.is_some() { Ok(match self.payload.as_mut().unwrap().decode(src)? { Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))), From d067b1d5f1cec0edcc4bdfcf0887b65763cad0b0 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 2 Apr 2019 10:53:44 -0700 Subject: [PATCH 418/427] do not use static --- awc/src/error.rs | 3 - awc/src/response.rs | 141 ++++++++++++++++++++++---------------------- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/awc/src/error.rs b/awc/src/error.rs index bbfd9b971..20654bdf4 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -54,9 +54,6 @@ impl From for WsClientError { /// A set of errors that can occur during parsing json payloads #[derive(Debug, Display, From)] pub enum JsonPayloadError { - /// Payload size is bigger than allowed. (default: 32kB) - #[display(fmt = "Json payload size is bigger than allowed.")] - Overflow, /// Content type error #[display(fmt = "Content type error")] ContentType, diff --git a/awc/src/response.rs b/awc/src/response.rs index b91735208..a4719a9a4 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -1,8 +1,9 @@ use std::cell::{Ref, RefMut}; use std::fmt; +use std::marker::PhantomData; use bytes::{Bytes, BytesMut}; -use futures::{Future, Poll, Stream}; +use futures::{Async, Future, Poll, Stream}; use actix_http::cookie::Cookie; use actix_http::error::{CookieParseError, PayloadError}; @@ -103,7 +104,7 @@ impl ClientResponse { impl ClientResponse where - S: Stream + 'static, + S: Stream, { /// Loads http response's body. pub fn body(&mut self) -> MessageBody { @@ -147,16 +148,14 @@ impl fmt::Debug for ClientResponse { /// Future that resolves to a complete http message body. pub struct MessageBody { - limit: usize, length: Option, - stream: Option>, err: Option, - fut: Option>>, + fut: Option>, } impl MessageBody where - S: Stream + 'static, + S: Stream, { /// Create `MessageBody` for request. pub fn new(res: &mut ClientResponse) -> MessageBody { @@ -174,24 +173,22 @@ where } MessageBody { - limit: 262_144, length: len, - stream: Some(res.take_payload()), - fut: None, err: None, + fut: Some(ReadBody::new(res.take_payload(), 262_144)), } } /// Change max size of payload. By default max size is 256Kb pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; + if let Some(ref mut fut) = self.fut { + fut.limit = limit; + } self } fn err(e: PayloadError) -> Self { MessageBody { - stream: None, - limit: 262_144, fut: None, err: Some(e), length: None, @@ -201,44 +198,23 @@ where impl Future for MessageBody where - S: Stream + 'static, + S: Stream, { type Item = Bytes; type Error = PayloadError; fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut { - return fut.poll(); - } - if let Some(err) = self.err.take() { return Err(err); } if let Some(len) = self.length.take() { - if len > self.limit { + if len > self.fut.as_ref().unwrap().limit { return Err(PayloadError::Overflow); } } - // future - let limit = self.limit; - self.fut = Some(Box::new( - self.stream - .take() - .expect("Can not be used second time") - .from_err() - .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { - if (body.len() + chunk.len()) > limit { - Err(PayloadError::Overflow) - } else { - body.extend_from_slice(&chunk); - Ok(body) - } - }) - .map(|body| body.freeze()), - )); - self.poll() + self.fut.as_mut().unwrap().poll() } } @@ -249,16 +225,15 @@ where /// * content type is not `application/json` /// * content length is greater than 64k pub struct JsonBody { - limit: usize, length: Option, - stream: Payload, err: Option, - fut: Option>>, + fut: Option>, + _t: PhantomData, } impl JsonBody where - S: Stream + 'static, + S: Stream, U: DeserializeOwned, { /// Create `JsonBody` for request. @@ -271,11 +246,10 @@ where }; if !json { return JsonBody { - limit: 65536, length: None, - stream: Payload::None, fut: None, err: Some(JsonPayloadError::ContentType), + _t: PhantomData, }; } @@ -289,58 +263,84 @@ where } JsonBody { - limit: 65536, length: len, - stream: req.take_payload(), - fut: None, err: None, + fut: Some(ReadBody::new(req.take_payload(), 65536)), + _t: PhantomData, } } /// Change max size of payload. By default max size is 64Kb pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; + if let Some(ref mut fut) = self.fut { + fut.limit = limit; + } self } } impl Future for JsonBody where - T: Stream + 'static, + T: Stream, U: DeserializeOwned + 'static, { type Item = U; type Error = JsonPayloadError; fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut { - return fut.poll(); - } - if let Some(err) = self.err.take() { return Err(err); } - let limit = self.limit; if let Some(len) = self.length.take() { - if len > limit { - return Err(JsonPayloadError::Overflow); + if len > self.fut.as_ref().unwrap().limit { + return Err(JsonPayloadError::Payload(PayloadError::Overflow)); } } - let fut = std::mem::replace(&mut self.stream, Payload::None) - .from_err() - .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { - if (body.len() + chunk.len()) > limit { - Err(JsonPayloadError::Overflow) - } else { - body.extend_from_slice(&chunk); - Ok(body) + let body = futures::try_ready!(self.fut.as_mut().unwrap().poll()); + Ok(Async::Ready(serde_json::from_slice::(&body)?)) + } +} + +struct ReadBody { + stream: Payload, + buf: BytesMut, + limit: usize, +} + +impl ReadBody { + fn new(stream: Payload, limit: usize) -> Self { + Self { + stream, + buf: BytesMut::with_capacity(std::cmp::min(limit, 32768)), + limit, + } + } +} + +impl Future for ReadBody +where + S: Stream, +{ + type Item = Bytes; + type Error = PayloadError; + + fn poll(&mut self) -> Poll { + loop { + return match self.stream.poll()? { + Async::Ready(Some(chunk)) => { + if (self.buf.len() + chunk.len()) > self.limit { + Err(PayloadError::Overflow) + } else { + self.buf.extend_from_slice(&chunk); + continue; + } } - }) - .and_then(|body| Ok(serde_json::from_slice::(&body)?)); - self.fut = Some(Box::new(fut)); - self.poll() + Async::Ready(None) => Ok(Async::Ready(self.buf.take().freeze())), + Async::NotReady => Ok(Async::NotReady), + }; + } } } @@ -391,8 +391,8 @@ mod tests { fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { match err { - JsonPayloadError::Overflow => match other { - JsonPayloadError::Overflow => true, + JsonPayloadError::Payload(PayloadError::Overflow) => match other { + JsonPayloadError::Payload(PayloadError::Overflow) => true, _ => false, }, JsonPayloadError::ContentType => match other { @@ -430,7 +430,10 @@ mod tests { .finish(); let json = block_on(JsonBody::<_, MyObject>::new(&mut req).limit(100)); - assert!(json_eq(json.err().unwrap(), JsonPayloadError::Overflow)); + assert!(json_eq( + json.err().unwrap(), + JsonPayloadError::Payload(PayloadError::Overflow) + )); let mut req = TestResponse::default() .header( From 49a499ce7460db83b0e6b697c8b6d84498b179bc Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 2 Apr 2019 11:11:32 -0700 Subject: [PATCH 419/427] properly allocate read buffer --- actix-http/src/h1/client.rs | 8 ++++++-- actix-http/src/h1/codec.rs | 16 ++++++---------- actix-http/src/h1/mod.rs | 12 +++++++++++- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/actix-http/src/h1/client.rs b/actix-http/src/h1/client.rs index 6a50c0271..f93bc496a 100644 --- a/actix-http/src/h1/client.rs +++ b/actix-http/src/h1/client.rs @@ -10,7 +10,7 @@ use http::header::{ use http::{Method, Version}; use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; -use super::{decoder, encoder}; +use super::{decoder, encoder, reserve_readbuf}; use super::{Message, MessageType}; use crate::body::BodySize; use crate::config::ServiceConfig; @@ -150,6 +150,7 @@ impl Decoder for ClientCodec { } else { self.inner.payload = None; } + reserve_readbuf(src); Ok(Some(req)) } else { Ok(None) @@ -168,7 +169,10 @@ impl Decoder for ClientPayloadCodec { ); Ok(match self.inner.payload.as_mut().unwrap().decode(src)? { - Some(PayloadItem::Chunk(chunk)) => Some(Some(chunk)), + Some(PayloadItem::Chunk(chunk)) => { + reserve_readbuf(src); + Some(Some(chunk)) + } Some(PayloadItem::Eof) => { self.inner.payload.take(); Some(None) diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index e4895f2dd..6e891e7cd 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -9,7 +9,7 @@ use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCOD use http::{Method, StatusCode, Version}; use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; -use super::{decoder, encoder}; +use super::{decoder, encoder, reserve_readbuf}; use super::{Message, MessageType}; use crate::body::BodySize; use crate::config::ServiceConfig; @@ -19,9 +19,6 @@ use crate::message::{ConnectionType, Head, ResponseHead}; use crate::request::Request; use crate::response::Response; -const LW: usize = 2 * 1024; -const HW: usize = 32 * 1024; - bitflags! { struct Flags: u8 { const HEAD = 0b0000_0001; @@ -108,14 +105,12 @@ impl Decoder for Codec { type Error = ParseError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - let cap = src.capacity(); - if cap < LW { - src.reserve(HW - cap); - } - if self.payload.is_some() { Ok(match self.payload.as_mut().unwrap().decode(src)? { - Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))), + Some(PayloadItem::Chunk(chunk)) => { + reserve_readbuf(src); + Some(Message::Chunk(Some(chunk))) + } Some(PayloadItem::Eof) => { self.payload.take(); Some(Message::Chunk(None)) @@ -140,6 +135,7 @@ impl Decoder for Codec { self.flags.insert(Flags::STREAM); } } + reserve_readbuf(src); Ok(Some(Message::Item(req))) } else { Ok(None) diff --git a/actix-http/src/h1/mod.rs b/actix-http/src/h1/mod.rs index e3d63c521..472d73477 100644 --- a/actix-http/src/h1/mod.rs +++ b/actix-http/src/h1/mod.rs @@ -1,5 +1,5 @@ //! HTTP/1 implementation -use bytes::Bytes; +use bytes::{Bytes, BytesMut}; mod client; mod codec; @@ -38,6 +38,16 @@ pub enum MessageType { Stream, } +const LW: usize = 2 * 1024; +const HW: usize = 32 * 1024; + +pub(crate) fn reserve_readbuf(src: &mut BytesMut) { + let cap = src.capacity(); + if cap < LW { + src.reserve(HW - cap); + } +} + #[cfg(test)] mod tests { use super::*; From e282ef792522e5398acd72b29b04127cd0b75d6e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 2 Apr 2019 12:51:16 -0700 Subject: [PATCH 420/427] return back consuming builder --- awc/CHANGES.md | 4 - awc/src/lib.rs | 4 +- awc/src/request.rs | 317 ++++++++++++++++++++------------------------- src/app.rs | 12 +- src/scope.rs | 10 +- 5 files changed, 152 insertions(+), 195 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index cd9635074..4bc9fc0b1 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -5,8 +5,6 @@ ### Added -* Added `Deref` for `ClientRequest`. - * Export `MessageBody` type * `ClientResponse::json()` - Loads and parse `application/json` encoded body @@ -14,8 +12,6 @@ ### Changed -* Use non-consuming builder pattern for `ClientRequest`. - * `ClientRequest::json()` accepts reference instead of object. * `ClientResponse::body()` does not consume response object. diff --git a/awc/src/lib.rs b/awc/src/lib.rs index db994431b..5f9adb463 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -105,7 +105,7 @@ impl Client { let mut req = ClientRequest::new(method, url, self.0.clone()); for (key, value) in &self.0.headers { - req.set_header_if_none(key.clone(), value.clone()); + req = req.set_header_if_none(key.clone(), value.clone()); } req } @@ -120,7 +120,7 @@ impl Client { { let mut req = self.request(head.method.clone(), url); for (key, value) in &head.headers { - req.set_header_if_none(key.clone(), value.clone()); + req = req.set_header_if_none(key.clone(), value.clone()); } req } diff --git a/awc/src/request.rs b/awc/src/request.rs index 0b89581a2..4e3ab47d6 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -17,8 +17,8 @@ use actix_http::cookie::{Cookie, CookieJar}; use actix_http::encoding::Decoder; use actix_http::http::header::{self, ContentEncoding, Header, IntoHeaderValue}; use actix_http::http::{ - uri, ConnectionType, Error as HttpError, HeaderName, HeaderValue, HttpTryFrom, - Method, Uri, Version, + uri, ConnectionType, Error as HttpError, HeaderMap, HeaderName, HeaderValue, + HttpTryFrom, Method, Uri, Version, }; use actix_http::{Error, Payload, RequestHead}; @@ -58,7 +58,7 @@ const HTTPS_ENCODING: &str = "gzip, deflate"; /// } /// ``` pub struct ClientRequest { - pub(crate) head: Option, + pub(crate) head: RequestHead, err: Option, cookies: Option, default_headers: bool, @@ -73,40 +73,36 @@ impl ClientRequest { where Uri: HttpTryFrom, { - let mut req = ClientRequest { + ClientRequest { config, - head: Some(RequestHead::default()), + head: RequestHead::default(), err: None, cookies: None, timeout: None, default_headers: true, response_decompress: true, - }; - req.method(method).uri(uri); - req + } + .method(method) + .uri(uri) } /// Set HTTP URI of request. #[inline] - pub fn uri(&mut self, uri: U) -> &mut Self + pub fn uri(mut self, uri: U) -> Self where Uri: HttpTryFrom, { - if let Some(head) = parts(&mut self.head, &self.err) { - match Uri::try_from(uri) { - Ok(uri) => head.uri = uri, - Err(e) => self.err = Some(e.into()), - } + match Uri::try_from(uri) { + Ok(uri) => self.head.uri = uri, + Err(e) => self.err = Some(e.into()), } self } /// Set HTTP method of this request. #[inline] - pub fn method(&mut self, method: Method) -> &mut Self { - if let Some(head) = parts(&mut self.head, &self.err) { - head.method = method; - } + pub fn method(mut self, method: Method) -> Self { + self.head.method = method; self } @@ -115,13 +111,23 @@ impl ClientRequest { /// /// By default requests's HTTP version depends on network stream #[inline] - pub fn version(&mut self, version: Version) -> &mut Self { - if let Some(head) = parts(&mut self.head, &self.err) { - head.version = version; - } + pub fn version(mut self, version: Version) -> Self { + self.head.version = version; self } + #[inline] + /// Returns request's headers. + pub fn headers(&self) -> &HeaderMap { + &self.head.headers + } + + #[inline] + /// Returns request's mutable headers. + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.head.headers + } + /// Set a header. /// /// ```rust @@ -135,14 +141,12 @@ impl ClientRequest { /// # })); /// } /// ``` - pub fn set(&mut self, hdr: H) -> &mut Self { - if let Some(head) = parts(&mut self.head, &self.err) { - match hdr.try_into() { - Ok(value) => { - head.headers.insert(H::name(), value); - } - Err(e) => self.err = Some(e.into()), + pub fn set(mut self, hdr: H) -> Self { + match hdr.try_into() { + Ok(value) => { + self.head.headers.insert(H::name(), value); } + Err(e) => self.err = Some(e.into()), } self } @@ -165,65 +169,59 @@ impl ClientRequest { /// # })); /// } /// ``` - pub fn header(&mut self, key: K, value: V) -> &mut Self + pub fn header(mut self, key: K, value: V) -> Self where HeaderName: HttpTryFrom, V: IntoHeaderValue, { - if let Some(head) = parts(&mut self.head, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - head.headers.append(key, value); - } - Err(e) => self.err = Some(e.into()), - }, + match HeaderName::try_from(key) { + Ok(key) => match value.try_into() { + Ok(value) => { + let _ = self.head.headers.append(key, value); + } Err(e) => self.err = Some(e.into()), - } + }, + Err(e) => self.err = Some(e.into()), } self } /// Insert a header, replaces existing header. - pub fn set_header(&mut self, key: K, value: V) -> &mut Self + pub fn set_header(mut self, key: K, value: V) -> Self where HeaderName: HttpTryFrom, V: IntoHeaderValue, { - if let Some(head) = parts(&mut self.head, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - head.headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - }, + match HeaderName::try_from(key) { + Ok(key) => match value.try_into() { + Ok(value) => { + let _ = self.head.headers.insert(key, value); + } Err(e) => self.err = Some(e.into()), - } + }, + Err(e) => self.err = Some(e.into()), } self } /// Insert a header only if it is not yet set. - pub fn set_header_if_none(&mut self, key: K, value: V) -> &mut Self + pub fn set_header_if_none(mut self, key: K, value: V) -> Self where HeaderName: HttpTryFrom, V: IntoHeaderValue, { - if let Some(head) = parts(&mut self.head, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => { - if !head.headers.contains_key(&key) { - match value.try_into() { - Ok(value) => { - head.headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), + match HeaderName::try_from(key) { + Ok(key) => { + if !self.head.headers.contains_key(&key) { + match value.try_into() { + Ok(value) => { + let _ = self.head.headers.insert(key, value); } + Err(e) => self.err = Some(e.into()), } } - Err(e) => self.err = Some(e.into()), } + Err(e) => self.err = Some(e.into()), } self } @@ -231,40 +229,36 @@ impl ClientRequest { /// Close connection instead of returning it back to connections pool. /// This setting affect only http/1 connections. #[inline] - pub fn close_connection(&mut self) -> &mut Self { - if let Some(head) = parts(&mut self.head, &self.err) { - head.set_connection_type(ConnectionType::Close); - } + pub fn close_connection(mut self) -> Self { + self.head.set_connection_type(ConnectionType::Close); self } /// Set request's content type #[inline] - pub fn content_type(&mut self, value: V) -> &mut Self + pub fn content_type(mut self, value: V) -> Self where HeaderValue: HttpTryFrom, { - if let Some(head) = parts(&mut self.head, &self.err) { - match HeaderValue::try_from(value) { - Ok(value) => { - let _ = head.headers.insert(header::CONTENT_TYPE, value); - } - Err(e) => self.err = Some(e.into()), + match HeaderValue::try_from(value) { + Ok(value) => { + let _ = self.head.headers.insert(header::CONTENT_TYPE, value); } + Err(e) => self.err = Some(e.into()), } self } /// Set content length #[inline] - pub fn content_length(&mut self, len: u64) -> &mut Self { + pub fn content_length(self, len: u64) -> Self { let mut wrt = BytesMut::new().writer(); let _ = write!(wrt, "{}", len); self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze()) } /// Set HTTP basic authorization header - pub fn basic_auth(&mut self, username: U, password: Option<&str>) -> &mut Self + pub fn basic_auth(self, username: U, password: Option<&str>) -> Self where U: fmt::Display, { @@ -279,7 +273,7 @@ impl ClientRequest { } /// Set HTTP bearer authentication header - pub fn bearer_auth(&mut self, token: T) -> &mut Self + pub fn bearer_auth(self, token: T) -> Self where T: fmt::Display, { @@ -311,7 +305,7 @@ impl ClientRequest { /// })); /// } /// ``` - pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { + pub fn cookie<'c>(mut self, cookie: Cookie<'c>) -> Self { if self.cookies.is_none() { let mut jar = CookieJar::new(); jar.add(cookie.into_owned()); @@ -324,13 +318,13 @@ impl ClientRequest { /// Do not add default request headers. /// By default `Date` and `User-Agent` headers are set. - pub fn no_default_headers(&mut self) -> &mut Self { + pub fn no_default_headers(mut self) -> Self { self.default_headers = false; self } /// Disable automatic decompress of response's body - pub fn no_decompress(&mut self) -> &mut Self { + pub fn no_decompress(mut self) -> Self { self.response_decompress = false; self } @@ -339,38 +333,38 @@ impl ClientRequest { /// /// Request timeout is the total time before a response must be received. /// Default value is 5 seconds. - pub fn timeout(&mut self, timeout: Duration) -> &mut Self { + pub fn timeout(mut self, timeout: Duration) -> Self { self.timeout = Some(timeout); self } /// This method calls provided closure with builder reference if /// value is `true`. - pub fn if_true(&mut self, value: bool, f: F) -> &mut Self + pub fn if_true(mut self, value: bool, f: F) -> Self where F: FnOnce(&mut ClientRequest), { if value { - f(self); + f(&mut self); } self } /// This method calls provided closure with builder reference if /// value is `Some`. - pub fn if_some(&mut self, value: Option, f: F) -> &mut Self + pub fn if_some(mut self, value: Option, f: F) -> Self where F: FnOnce(T, &mut ClientRequest), { if let Some(val) = value { - f(val, self); + f(val, &mut self); } self } /// Complete request construction and send body. pub fn send_body( - &mut self, + mut self, body: B, ) -> impl Future< Item = ClientResponse>, @@ -383,10 +377,8 @@ impl ClientRequest { return Either::A(err(e.into())); } - let mut head = self.head.take().expect("cannot reuse response builder"); - // validate uri - let uri = &head.uri; + let uri = &self.head.uri; if uri.host().is_none() { return Either::A(err(InvalidUrl::MissingHost.into())); } else if uri.scheme_part().is_none() { @@ -403,18 +395,18 @@ impl ClientRequest { // set default headers if self.default_headers { // set request host header - if let Some(host) = head.uri.host() { - if !head.headers.contains_key(header::HOST) { + if let Some(host) = self.head.uri.host() { + if !self.head.headers.contains_key(header::HOST) { let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); - let _ = match head.uri.port_u16() { + let _ = match self.head.uri.port_u16() { None | Some(80) | Some(443) => write!(wrt, "{}", host), Some(port) => write!(wrt, "{}:{}", host, port), }; match wrt.get_mut().take().freeze().try_into() { Ok(value) => { - head.headers.insert(header::HOST, value); + self.head.headers.insert(header::HOST, value); } Err(e) => return Either::A(err(HttpError::from(e).into())), } @@ -422,32 +414,11 @@ impl ClientRequest { } // user agent - self.set_header_if_none( - header::USER_AGENT, - concat!("awc/", env!("CARGO_PKG_VERSION")), - ); - } - - // enable br only for https - let https = head - .uri - .scheme_part() - .map(|s| s == &uri::Scheme::HTTPS) - .unwrap_or(true); - - #[cfg(any( - feature = "brotli", - feature = "flate2-zlib", - feature = "flate2-rust" - ))] - { - if https { - self.set_header_if_none(header::ACCEPT_ENCODING, HTTPS_ENCODING); - } else { - #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] - { - self.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate"); - } + if !self.head.headers.contains_key(&header::USER_AGENT) { + self.head.headers.insert( + header::USER_AGENT, + HeaderValue::from_static(concat!("awc/", env!("CARGO_PKG_VERSION"))), + ); } } @@ -459,14 +430,43 @@ impl ClientRequest { let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); let _ = write!(&mut cookie, "; {}={}", name, value); } - head.headers.insert( + self.head.headers.insert( header::COOKIE, HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), ); } - let config = self.config.as_ref(); - let response_decompress = self.response_decompress; + let slf = self; + + // enable br only for https + #[cfg(any( + feature = "brotli", + feature = "flate2-zlib", + feature = "flate2-rust" + ))] + let slf = { + let https = slf + .head + .uri + .scheme_part() + .map(|s| s == &uri::Scheme::HTTPS) + .unwrap_or(true); + + if https { + slf.set_header_if_none(header::ACCEPT_ENCODING, HTTPS_ENCODING) + } else { + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] + { + slf.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate") + } + #[cfg(not(any(feature = "flate2-zlib", feature = "flate2-rust")))] + slf + } + }; + + let head = slf.head; + let config = slf.config.as_ref(); + let response_decompress = slf.response_decompress; let fut = config .connector @@ -483,7 +483,7 @@ impl ClientRequest { }); // set request timeout - if let Some(timeout) = self.timeout.or_else(|| config.timeout.clone()) { + if let Some(timeout) = slf.timeout.or_else(|| config.timeout.clone()) { Either::B(Either::A(Timeout::new(fut, timeout).map_err(|e| { if let Some(e) = e.into_inner() { e @@ -498,7 +498,7 @@ impl ClientRequest { /// Set a JSON body and generate `ClientRequest` pub fn send_json( - &mut self, + self, value: &T, ) -> impl Future< Item = ClientResponse>, @@ -510,16 +510,16 @@ impl ClientRequest { }; // set content-type - self.set_header_if_none(header::CONTENT_TYPE, "application/json"); + let slf = self.set_header_if_none(header::CONTENT_TYPE, "application/json"); - Either::B(self.send_body(Body::Bytes(Bytes::from(body)))) + Either::B(slf.send_body(Body::Bytes(Bytes::from(body)))) } /// Set a urlencoded body and generate `ClientRequest` /// /// `ClientRequestBuilder` can not be used after this call. pub fn send_form( - &mut self, + self, value: &T, ) -> impl Future< Item = ClientResponse>, @@ -531,17 +531,17 @@ impl ClientRequest { }; // set content-type - self.set_header_if_none( + let slf = self.set_header_if_none( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ); - Either::B(self.send_body(Body::Bytes(Bytes::from(body)))) + Either::B(slf.send_body(Body::Bytes(Bytes::from(body)))) } /// Set an streaming body and generate `ClientRequest`. pub fn send_stream( - &mut self, + self, stream: S, ) -> impl Future< Item = ClientResponse>, @@ -556,7 +556,7 @@ impl ClientRequest { /// Set an empty body and generate `ClientRequest`. pub fn send( - &mut self, + self, ) -> impl Future< Item = ClientResponse>, Error = SendRequestError, @@ -565,48 +565,21 @@ impl ClientRequest { } } -impl std::ops::Deref for ClientRequest { - type Target = RequestHead; - - fn deref(&self) -> &RequestHead { - self.head.as_ref().expect("cannot reuse response builder") - } -} - -impl std::ops::DerefMut for ClientRequest { - fn deref_mut(&mut self) -> &mut RequestHead { - self.head.as_mut().expect("cannot reuse response builder") - } -} - impl fmt::Debug for ClientRequest { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let head = self.head.as_ref().expect("cannot reuse response builder"); - writeln!( f, "\nClientRequest {:?} {}:{}", - head.version, head.method, head.uri + self.head.version, self.head.method, self.head.uri )?; writeln!(f, " headers:")?; - for (key, val) in head.headers.iter() { + for (key, val) in self.head.headers.iter() { writeln!(f, " {:?}: {:?}", key, val)?; } Ok(()) } } -#[inline] -fn parts<'a>( - parts: &'a mut Option, - err: &Option, -) -> Option<&'a mut RequestHead> { - if err.is_some() { - return None; - } - parts.as_mut() -} - #[cfg(test)] mod tests { use super::*; @@ -615,8 +588,7 @@ mod tests { #[test] fn test_debug() { test::run_on(|| { - let mut request = Client::new().get("/"); - request.header("x-test", "111"); + let request = Client::new().get("/").header("x-test", "111"); let repr = format!("{:?}", request); assert!(repr.contains("ClientRequest")); assert!(repr.contains("x-test")); @@ -633,8 +605,6 @@ mod tests { assert_eq!( req.head - .as_ref() - .unwrap() .headers .get(header::CONTENT_TYPE) .unwrap() @@ -648,16 +618,14 @@ mod tests { #[test] fn test_client_header_override() { test::run_on(|| { - let mut req = Client::build() + let req = Client::build() .header(header::CONTENT_TYPE, "111") .finish() - .get("/"); - req.set_header(header::CONTENT_TYPE, "222"); + .get("/") + .set_header(header::CONTENT_TYPE, "222"); assert_eq!( req.head - .as_ref() - .unwrap() .headers .get(header::CONTENT_TYPE) .unwrap() @@ -671,12 +639,11 @@ mod tests { #[test] fn client_basic_auth() { test::run_on(|| { - let mut req = Client::new().get("/"); - req.basic_auth("username", Some("password")); + let req = Client::new() + .get("/") + .basic_auth("username", Some("password")); assert_eq!( req.head - .as_ref() - .unwrap() .headers .get(header::AUTHORIZATION) .unwrap() @@ -685,12 +652,9 @@ mod tests { "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" ); - let mut req = Client::new().get("/"); - req.basic_auth("username", None); + let req = Client::new().get("/").basic_auth("username", None); assert_eq!( req.head - .as_ref() - .unwrap() .headers .get(header::AUTHORIZATION) .unwrap() @@ -704,12 +668,9 @@ mod tests { #[test] fn client_bearer_auth() { test::run_on(|| { - let mut req = Client::new().get("/"); - req.bearer_auth("someS3cr3tAutht0k3n"); + let req = Client::new().get("/").bearer_auth("someS3cr3tAutht0k3n"); assert_eq!( req.head - .as_ref() - .unwrap() .headers .get(header::AUTHORIZATION) .unwrap() diff --git a/src/app.rs b/src/app.rs index 9535dac21..fd91d0728 100644 --- a/src/app.rs +++ b/src/app.rs @@ -112,9 +112,9 @@ where self } - /// Registers middleware, in the form of a middleware component (type), - /// that runs during inbound and/or outbound processing in the request - /// lifecycle (request -> response), modifying request/response as + /// Registers middleware, in the form of a middleware component (type), + /// that runs during inbound and/or outbound processing in the request + /// lifecycle (request -> response), modifying request/response as /// necessary, across all requests managed by the *Application*. /// /// Use middleware when you need to read or modify *every* request or response in some way. @@ -427,9 +427,9 @@ where self } - /// Registers middleware, in the form of a middleware component (type), - /// that runs during inbound and/or outbound processing in the request - /// lifecycle (request -> response), modifying request/response as + /// Registers middleware, in the form of a middleware component (type), + /// that runs during inbound and/or outbound processing in the request + /// lifecycle (request -> response), modifying request/response as /// necessary, across all requests managed by the *Route*. /// /// Use middleware when you need to read or modify *every* request or response in some way. diff --git a/src/scope.rs b/src/scope.rs index 0dfaaf066..874240e73 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -200,12 +200,12 @@ where self } - /// Registers middleware, in the form of a middleware component (type), - /// that runs during inbound processing in the request - /// lifecycle (request -> response), modifying request as + /// Registers middleware, in the form of a middleware component (type), + /// that runs during inbound processing in the request + /// lifecycle (request -> response), modifying request as /// necessary, across all requests managed by the *Scope*. Scope-level /// middleware is more limited in what it can modify, relative to Route or - /// Application level middleware, in that Scope-level middleware can not modify + /// Application level middleware, in that Scope-level middleware can not modify /// ServiceResponse. /// /// Use middleware when you need to read or modify *every* request in some way. @@ -243,7 +243,7 @@ where } /// Registers middleware, in the form of a closure, that runs during inbound - /// processing in the request lifecycle (request -> response), modifying + /// processing in the request lifecycle (request -> response), modifying /// request as necessary, across all requests managed by the *Scope*. /// Scope-level middleware is more limited in what it can modify, relative /// to Route or Application level middleware, in that Scope-level middleware From bca31eb7adbc9010291e530c42c5e2f99921715a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 2 Apr 2019 13:35:01 -0700 Subject: [PATCH 421/427] remove Deref --- CHANGES.md | 2 + Cargo.toml | 3 ++ actix-http/tests/test_client.rs | 10 +++-- actix-http/tests/test_server.rs | 48 +++++++++++----------- awc/src/response.rs | 2 +- awc/tests/test_client.rs | 32 +++++++-------- src/data.rs | 2 +- src/guard.rs | 63 ++++++++++++++++------------- src/middleware/compress.rs | 4 +- src/middleware/cors.rs | 28 ++++++------- src/middleware/decompress.rs | 1 - src/middleware/identity.rs | 4 +- src/middleware/logger.rs | 4 +- src/request.rs | 31 +++++++++----- src/responder.rs | 2 +- src/scope.rs | 6 +-- src/service.rs | 72 ++++++++++++++++----------------- src/types/form.rs | 2 +- src/types/json.rs | 2 +- src/types/path.rs | 2 +- src/types/query.rs | 2 +- tests/test_server.rs | 42 +++++++++---------- 22 files changed, 192 insertions(+), 172 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 39975fb46..655c23cef 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,8 @@ * Renamed `TestRequest::to_response()` to `TestRequest::to_srv_response()` +* Removed `Deref` impls + ### Removed diff --git a/Cargo.toml b/Cargo.toml index b5d0e8769..b8bd6efcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,4 +116,7 @@ codegen-units = 1 [patch.crates-io] actix = { git = "https://github.com/actix/actix.git" } +actix-web = { path = "." } actix-http = { path = "actix-http" } +actix-http-test = { path = "test-server" } +awc = { path = "awc" } diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index ea0c5eb9a..109a3e4c4 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -35,10 +35,10 @@ fn test_h1_v2() { .finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) .map(|_| ()) }); - let response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get("/").send()).unwrap(); assert!(response.status().is_success()); - let request = srv.get().header("x-test", "111").send(); + let request = srv.get("/").header("x-test", "111").send(); let response = srv.block_on(request).unwrap(); assert!(response.status().is_success()); @@ -46,7 +46,7 @@ fn test_h1_v2() { let bytes = srv.load_body(response).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); - let response = srv.block_on(srv.post().send()).unwrap(); + let response = srv.block_on(srv.post("/").send()).unwrap(); assert!(response.status().is_success()); // read response @@ -61,7 +61,9 @@ fn test_connection_close() { .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .map(|_| ()) }); - let response = srv.block_on(srv.get().close_connection().send()).unwrap(); + let response = srv + .block_on(srv.get("/").close_connection().send()) + .unwrap(); assert!(response.status().is_success()); } diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index a18d19626..85cab929c 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -37,7 +37,7 @@ fn test_h1() { .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) }); - let response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get("/").send()).unwrap(); assert!(response.status().is_success()); } @@ -55,7 +55,7 @@ fn test_h1_2() { .map(|_| ()) }); - let response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get("/").send()).unwrap(); assert!(response.status().is_success()); } @@ -98,7 +98,7 @@ fn test_h2() -> std::io::Result<()> { ) }); - let response = srv.block_on(srv.sget().send()).unwrap(); + let response = srv.block_on(srv.sget("/").send()).unwrap(); assert!(response.status().is_success()); Ok(()) } @@ -121,7 +121,7 @@ fn test_h2_1() -> std::io::Result<()> { ) }); - let response = srv.block_on(srv.sget().send()).unwrap(); + let response = srv.block_on(srv.sget("/").send()).unwrap(); assert!(response.status().is_success()); Ok(()) } @@ -145,7 +145,7 @@ fn test_h2_body() -> std::io::Result<()> { ) }); - let response = srv.block_on(srv.sget().send_body(data.clone())).unwrap(); + let response = srv.block_on(srv.sget("/").send_body(data.clone())).unwrap(); assert!(response.status().is_success()); let body = srv.load_body(response).unwrap(); @@ -437,7 +437,7 @@ fn test_h1_headers() { }) }); - let response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get("/").send()).unwrap(); assert!(response.status().is_success()); // read response @@ -482,7 +482,7 @@ fn test_h2_headers() { }).map_err(|_| ())) }); - let response = srv.block_on(srv.sget().send()).unwrap(); + let response = srv.block_on(srv.sget("/").send()).unwrap(); assert!(response.status().is_success()); // read response @@ -518,7 +518,7 @@ fn test_h1_body() { HttpService::build().h1(|_| future::ok::<_, ()>(Response::Ok().body(STR))) }); - let response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get("/").send()).unwrap(); assert!(response.status().is_success()); // read response @@ -541,7 +541,7 @@ fn test_h2_body2() { ) }); - let response = srv.block_on(srv.sget().send()).unwrap(); + let response = srv.block_on(srv.sget("/").send()).unwrap(); assert!(response.status().is_success()); // read response @@ -555,7 +555,7 @@ fn test_h1_head_empty() { HttpService::build().h1(|_| ok::<_, ()>(Response::Ok().body(STR))) }); - let response = srv.block_on(srv.head().send()).unwrap(); + let response = srv.block_on(srv.head("/").send()).unwrap(); assert!(response.status().is_success()); { @@ -586,7 +586,7 @@ fn test_h2_head_empty() { ) }); - let response = srv.block_on(srv.shead().send()).unwrap(); + let response = srv.block_on(srv.shead("/").send()).unwrap(); assert!(response.status().is_success()); assert_eq!(response.version(), http::Version::HTTP_2); @@ -611,7 +611,7 @@ fn test_h1_head_binary() { }) }); - let response = srv.block_on(srv.head().send()).unwrap(); + let response = srv.block_on(srv.head("/").send()).unwrap(); assert!(response.status().is_success()); { @@ -646,7 +646,7 @@ fn test_h2_head_binary() { ) }); - let response = srv.block_on(srv.shead().send()).unwrap(); + let response = srv.block_on(srv.shead("/").send()).unwrap(); assert!(response.status().is_success()); { @@ -668,7 +668,7 @@ fn test_h1_head_binary2() { HttpService::build().h1(|_| ok::<_, ()>(Response::Ok().body(STR))) }); - let response = srv.block_on(srv.head().send()).unwrap(); + let response = srv.block_on(srv.head("/").send()).unwrap(); assert!(response.status().is_success()); { @@ -695,7 +695,7 @@ fn test_h2_head_binary2() { ) }); - let response = srv.block_on(srv.shead().send()).unwrap(); + let response = srv.block_on(srv.shead("/").send()).unwrap(); assert!(response.status().is_success()); { @@ -719,7 +719,7 @@ fn test_h1_body_length() { }) }); - let response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get("/").send()).unwrap(); assert!(response.status().is_success()); // read response @@ -747,7 +747,7 @@ fn test_h2_body_length() { ) }); - let response = srv.block_on(srv.sget().send()).unwrap(); + let response = srv.block_on(srv.sget("/").send()).unwrap(); assert!(response.status().is_success()); // read response @@ -768,7 +768,7 @@ fn test_h1_body_chunked_explicit() { }) }); - let response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get("/").send()).unwrap(); assert!(response.status().is_success()); assert_eq!( response @@ -810,7 +810,7 @@ fn test_h2_body_chunked_explicit() { ) }); - let response = srv.block_on(srv.sget().send()).unwrap(); + let response = srv.block_on(srv.sget("/").send()).unwrap(); assert!(response.status().is_success()); assert!(!response.headers().contains_key(header::TRANSFER_ENCODING)); @@ -830,7 +830,7 @@ fn test_h1_body_chunked_implicit() { }) }); - let response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get("/").send()).unwrap(); assert!(response.status().is_success()); assert_eq!( response @@ -862,7 +862,7 @@ fn test_h1_response_http_error_handling() { })) }); - let response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get("/").send()).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response @@ -895,7 +895,7 @@ fn test_h2_response_http_error_handling() { ) }); - let response = srv.block_on(srv.sget().send()).unwrap(); + let response = srv.block_on(srv.sget("/").send()).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response @@ -910,7 +910,7 @@ fn test_h1_service_error() { .h1(|_| Err::(error::ErrorBadRequest("error"))) }); - let response = srv.block_on(srv.get().send()).unwrap(); + let response = srv.block_on(srv.get("/").send()).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response @@ -934,7 +934,7 @@ fn test_h2_service_error() { ) }); - let response = srv.block_on(srv.sget().send()).unwrap(); + let response = srv.block_on(srv.sget("/").send()).unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); // read response diff --git a/awc/src/response.rs b/awc/src/response.rs index a4719a9a4..73194d673 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -83,7 +83,7 @@ impl ClientResponse { } #[inline] - /// Returns Request's headers. + /// Returns request's headers. pub fn headers(&self) -> &HeaderMap { &self.head().headers } diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 51791d67a..6aed72e41 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -44,7 +44,7 @@ fn test_simple() { )) }); - let request = srv.get().header("x-test", "111").send(); + let request = srv.get("/").header("x-test", "111").send(); let response = srv.block_on(request).unwrap(); assert!(response.status().is_success()); @@ -52,7 +52,7 @@ fn test_simple() { let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); - let response = srv.block_on(srv.post().send()).unwrap(); + let response = srv.block_on(srv.post("/").send()).unwrap(); assert!(response.status().is_success()); // read response @@ -114,7 +114,7 @@ fn test_timeout_override() { // let mut srv = // test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); -// let request = srv.get().header("Connection", "close").finish().unwrap(); +// let request = srv.get("/").header("Connection", "close").finish().unwrap(); // let response = srv.execute(request.send()).unwrap(); // assert!(response.status().is_success()); // } @@ -128,7 +128,7 @@ fn test_timeout_override() { // }) // }); -// let request = srv.get().uri(srv.url("/?qp=5").as_str()).finish().unwrap(); +// let request = srv.get("/").uri(srv.url("/?qp=5").as_str()).finish().unwrap(); // let response = srv.execute(request.send()).unwrap(); // assert!(response.status().is_success()); @@ -139,7 +139,7 @@ fn test_timeout_override() { // let mut srv = // test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); -// let request = srv.get().disable_decompress().finish().unwrap(); +// let request = srv.get("/").disable_decompress().finish().unwrap(); // let response = srv.execute(request.send()).unwrap(); // assert!(response.status().is_success()); @@ -177,7 +177,7 @@ fn test_client_gzip_encoding() { }); // client request - let response = srv.block_on(srv.post().send()).unwrap(); + let response = srv.block_on(srv.post("/").send()).unwrap(); assert!(response.status().is_success()); // read response @@ -200,7 +200,7 @@ fn test_client_gzip_encoding_large() { }); // client request - let response = srv.block_on(srv.post().send()).unwrap(); + let response = srv.block_on(srv.post("/").send()).unwrap(); assert!(response.status().is_success()); // read response @@ -229,7 +229,7 @@ fn test_client_gzip_encoding_large_random() { }); // client request - let response = srv.block_on(srv.post().send_body(data.clone())).unwrap(); + let response = srv.block_on(srv.post("/").send_body(data.clone())).unwrap(); assert!(response.status().is_success()); // read response @@ -253,7 +253,7 @@ fn test_client_brotli_encoding() { }); // client request - let response = srv.block_on(srv.post().send_body(STR)).unwrap(); + let response = srv.block_on(srv.post("/").send_body(STR)).unwrap(); assert!(response.status().is_success()); // read response @@ -375,7 +375,7 @@ fn test_client_brotli_encoding() { // let body = once(Ok(Bytes::from_static(STR.as_ref()))); -// let request = srv.get().body(Body::Streaming(Box::new(body))).unwrap(); +// let request = srv.get("/").body(Body::Streaming(Box::new(body))).unwrap(); // let response = srv.execute(request.send()).unwrap(); // assert!(response.status().is_success()); @@ -395,7 +395,7 @@ fn test_client_brotli_encoding() { // }) // }); -// let request = srv.get().finish().unwrap(); +// let request = srv.get("/").finish().unwrap(); // let response = srv.execute(request.send()).unwrap(); // assert!(response.status().is_success()); @@ -459,7 +459,7 @@ fn test_client_cookie_handling() { )) }); - let request = srv.get().cookie(cookie1.clone()).cookie(cookie2.clone()); + let request = srv.get("/").cookie(cookie1.clone()).cookie(cookie2.clone()); let response = srv.block_on(request.send()).unwrap(); assert!(response.status().is_success()); let c1 = response.cookie("cookie1").expect("Missing cookie1"); @@ -472,7 +472,7 @@ fn test_client_cookie_handling() { // fn test_default_headers() { // let srv = test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); -// let request = srv.get().finish().unwrap(); +// let request = srv.get("/").finish().unwrap(); // let repr = format!("{:?}", request); // assert!(repr.contains("\"accept-encoding\": \"gzip, deflate\"")); // assert!(repr.contains(concat!( @@ -482,7 +482,7 @@ fn test_client_cookie_handling() { // ))); // let request_override = srv -// .get() +// .get("/") // .header("User-Agent", "test") // .header("Accept-Encoding", "over_test") // .finish() @@ -551,7 +551,7 @@ fn client_basic_auth() { }); // set authorization header to Basic - let request = srv.get().basic_auth("username", Some("password")); + let request = srv.get("/").basic_auth("username", Some("password")); let response = srv.block_on(request.send()).unwrap(); assert!(response.status().is_success()); } @@ -579,7 +579,7 @@ fn client_bearer_auth() { }); // set authorization header to Bearer - let request = srv.get().bearer_auth("someS3cr3tAutht0k3n"); + let request = srv.get("/").bearer_auth("someS3cr3tAutht0k3n"); let response = srv.block_on(request.send()).unwrap(); assert!(response.status().is_success()); } diff --git a/src/data.rs b/src/data.rs index a53015c23..a79a303bc 100644 --- a/src/data.rs +++ b/src/data.rs @@ -92,7 +92,7 @@ impl FromRequest

    for Data { #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - if let Some(st) = req.config().extensions().get::>() { + if let Some(st) = req.request().config().extensions().get::>() { Ok(st.clone()) } else { Err(ErrorInternalServerError( diff --git a/src/guard.rs b/src/guard.rs index 44e4891e6..0990e876a 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -73,6 +73,15 @@ where } } +impl Guard for F +where + F: Fn(&RequestHead) -> bool, +{ + fn check(&self, head: &RequestHead) -> bool { + (self)(head) + } +} + /// Return guard that matches if any of supplied guards. /// /// ```rust @@ -300,13 +309,13 @@ mod tests { .to_http_request(); let pred = Header("transfer-encoding", "chunked"); - assert!(pred.check(&req)); + assert!(pred.check(req.head())); let pred = Header("transfer-encoding", "other"); - assert!(!pred.check(&req)); + assert!(!pred.check(req.head())); let pred = Header("content-type", "other"); - assert!(!pred.check(&req)); + assert!(!pred.check(req.head())); } // #[test] @@ -332,50 +341,50 @@ mod tests { .method(Method::POST) .to_http_request(); - assert!(Get().check(&req)); - assert!(!Get().check(&req2)); - assert!(Post().check(&req2)); - assert!(!Post().check(&req)); + assert!(Get().check(req.head())); + assert!(!Get().check(req2.head())); + assert!(Post().check(req2.head())); + assert!(!Post().check(req.head())); let r = TestRequest::default().method(Method::PUT).to_http_request(); - assert!(Put().check(&r)); - assert!(!Put().check(&req)); + assert!(Put().check(r.head())); + assert!(!Put().check(req.head())); let r = TestRequest::default() .method(Method::DELETE) .to_http_request(); - assert!(Delete().check(&r)); - assert!(!Delete().check(&req)); + assert!(Delete().check(r.head())); + assert!(!Delete().check(req.head())); let r = TestRequest::default() .method(Method::HEAD) .to_http_request(); - assert!(Head().check(&r)); - assert!(!Head().check(&req)); + assert!(Head().check(r.head())); + assert!(!Head().check(req.head())); let r = TestRequest::default() .method(Method::OPTIONS) .to_http_request(); - assert!(Options().check(&r)); - assert!(!Options().check(&req)); + assert!(Options().check(r.head())); + assert!(!Options().check(req.head())); let r = TestRequest::default() .method(Method::CONNECT) .to_http_request(); - assert!(Connect().check(&r)); - assert!(!Connect().check(&req)); + assert!(Connect().check(r.head())); + assert!(!Connect().check(req.head())); let r = TestRequest::default() .method(Method::PATCH) .to_http_request(); - assert!(Patch().check(&r)); - assert!(!Patch().check(&req)); + assert!(Patch().check(r.head())); + assert!(!Patch().check(req.head())); let r = TestRequest::default() .method(Method::TRACE) .to_http_request(); - assert!(Trace().check(&r)); - assert!(!Trace().check(&req)); + assert!(Trace().check(r.head())); + assert!(!Trace().check(req.head())); } #[test] @@ -384,13 +393,13 @@ mod tests { .method(Method::TRACE) .to_http_request(); - assert!(Not(Get()).check(&r)); - assert!(!Not(Trace()).check(&r)); + assert!(Not(Get()).check(r.head())); + assert!(!Not(Trace()).check(r.head())); - assert!(All(Trace()).and(Trace()).check(&r)); - assert!(!All(Get()).and(Trace()).check(&r)); + assert!(All(Trace()).and(Trace()).check(r.head())); + assert!(!All(Get()).and(Trace()).check(r.head())); - assert!(Any(Get()).or(Trace()).check(&r)); - assert!(!Any(Get()).or(Get()).check(&r)); + assert!(Any(Get()).or(Trace()).check(r.head())); + assert!(!Any(Get()).or(Get()).check(r.head())); } } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index d797e1250..f74754402 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -113,7 +113,7 @@ where fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { // negotiate content-encoding - let encoding = if let Some(val) = req.headers.get(ACCEPT_ENCODING) { + let encoding = if let Some(val) = req.headers().get(ACCEPT_ENCODING) { if let Ok(enc) = val.to_str() { AcceptEncoding::parse(enc, self.encoding) } else { @@ -157,7 +157,7 @@ where fn poll(&mut self) -> Poll { let resp = futures::try_ready!(self.fut.poll()); - let enc = if let Some(enc) = resp.head().extensions().get::() { + let enc = if let Some(enc) = resp.response().extensions().get::() { enc.0 } else { self.encoding diff --git a/src/middleware/cors.rs b/src/middleware/cors.rs index 8924eb0ab..920b480bb 100644 --- a/src/middleware/cors.rs +++ b/src/middleware/cors.rs @@ -51,7 +51,7 @@ use crate::error::{ResponseError, Result}; use crate::http::header::{self, HeaderName, HeaderValue}; use crate::http::{self, HttpTryFrom, Method, StatusCode, Uri}; use crate::service::{ServiceRequest, ServiceResponse}; -use crate::{HttpMessage, HttpResponse}; +use crate::HttpResponse; /// A set of errors that can occur during processing CORS #[derive(Debug, Display)] @@ -702,9 +702,9 @@ where if self.inner.preflight && Method::OPTIONS == *req.method() { if let Err(e) = self .inner - .validate_origin(&req) - .and_then(|_| self.inner.validate_allowed_method(&req)) - .and_then(|_| self.inner.validate_allowed_headers(&req)) + .validate_origin(req.head()) + .and_then(|_| self.inner.validate_allowed_method(req.head())) + .and_then(|_| self.inner.validate_allowed_headers(req.head())) { return Either::A(ok(req.error_response(e))); } @@ -739,7 +739,7 @@ where let _ = resp.header(header::ACCESS_CONTROL_ALLOW_HEADERS, headers); }) .if_some( - self.inner.access_control_allow_origin(&req), + self.inner.access_control_allow_origin(req.head()), |origin, resp| { let _ = resp.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin); }, @@ -762,7 +762,7 @@ where Either::A(ok(req.into_response(res))) } else if req.headers().contains_key(header::ORIGIN) { // Only check requests with a origin header. - if let Err(e) = self.inner.validate_origin(&req) { + if let Err(e) = self.inner.validate_origin(req.head()) { return Either::A(ok(req.error_response(e))); } @@ -771,7 +771,7 @@ where Either::B(Either::B(Box::new(self.service.call(req).and_then( move |mut res| { if let Some(origin) = - inner.access_control_allow_origin(&res.request()) + inner.access_control_allow_origin(res.request().head()) { res.headers_mut() .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone()); @@ -869,8 +869,8 @@ mod tests { .method(Method::OPTIONS) .to_srv_request(); - assert!(cors.inner.validate_allowed_method(&req).is_err()); - assert!(cors.inner.validate_allowed_headers(&req).is_err()); + assert!(cors.inner.validate_allowed_method(req.head()).is_err()); + assert!(cors.inner.validate_allowed_headers(req.head()).is_err()); let resp = test::call_success(&mut cors, req); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); @@ -879,8 +879,8 @@ mod tests { .method(Method::OPTIONS) .to_srv_request(); - assert!(cors.inner.validate_allowed_method(&req).is_err()); - assert!(cors.inner.validate_allowed_headers(&req).is_err()); + assert!(cors.inner.validate_allowed_method(req.head()).is_err()); + assert!(cors.inner.validate_allowed_headers(req.head()).is_err()); let req = TestRequest::with_header("Origin", "https://www.example.com") .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST") @@ -961,9 +961,9 @@ mod tests { let req = TestRequest::with_header("Origin", "https://www.unknown.com") .method(Method::GET) .to_srv_request(); - cors.inner.validate_origin(&req).unwrap(); - cors.inner.validate_allowed_method(&req).unwrap(); - cors.inner.validate_allowed_headers(&req).unwrap(); + cors.inner.validate_origin(req.head()).unwrap(); + cors.inner.validate_allowed_method(req.head()).unwrap(); + cors.inner.validate_allowed_headers(req.head()).unwrap(); } #[test] diff --git a/src/middleware/decompress.rs b/src/middleware/decompress.rs index 84d357375..13735143a 100644 --- a/src/middleware/decompress.rs +++ b/src/middleware/decompress.rs @@ -10,7 +10,6 @@ use futures::{Async, Poll, Stream}; use crate::dev::Payload; use crate::error::{Error, PayloadError}; use crate::service::ServiceRequest; -use crate::HttpMessage; /// `Middleware` for decompressing request's payload. /// `Decompress` middleware must be added with `App::chain()` method. diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs index 34979e167..7a2c9f376 100644 --- a/src/middleware/identity.rs +++ b/src/middleware/identity.rs @@ -148,7 +148,7 @@ impl

    FromRequest

    for Identity { #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - Ok(Identity(req.clone())) + Ok(Identity(req.request().clone())) } } @@ -507,7 +507,7 @@ mod tests { let resp = test::call_success(&mut srv, TestRequest::with_uri("/login").to_request()); assert_eq!(resp.status(), StatusCode::OK); - let c = resp.cookies().next().unwrap().to_owned(); + let c = resp.response().cookies().next().unwrap().to_owned(); let resp = test::call_success( &mut srv, diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index d9c9b138a..bdcc00f28 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -15,7 +15,7 @@ use time; use crate::dev::{BodySize, MessageBody, ResponseBody}; use crate::error::{Error, Result}; use crate::service::{ServiceRequest, ServiceResponse}; -use crate::{HttpMessage, HttpResponse}; +use crate::HttpResponse; /// `Middleware` for logging request and response info to the terminal. /// @@ -201,7 +201,7 @@ where if let Some(ref mut format) = self.format { for unit in &mut format.0 { - unit.render_response(&res); + unit.render_response(res.response()); } } diff --git a/src/request.rs b/src/request.rs index c524d4978..b5ba74122 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,6 +1,5 @@ use std::cell::{Ref, RefMut}; use std::fmt; -use std::ops::Deref; use std::rc::Rc; use actix_http::http::{HeaderMap, Method, Uri, Version}; @@ -66,6 +65,12 @@ impl HttpRequest { self.head().version } + #[inline] + /// Returns request's headers. + pub fn headers(&self) -> &HeaderMap { + &self.head().headers + } + /// The target path of this Request. #[inline] pub fn path(&self) -> &str { @@ -111,6 +116,18 @@ impl HttpRequest { } } + /// Request extensions + #[inline] + pub fn extensions(&self) -> Ref { + self.head().extensions() + } + + /// Mutable reference to a the request's extensions + #[inline] + pub fn extensions_mut(&self) -> RefMut { + self.head().extensions_mut() + } + /// Generate url for named resource /// /// ```rust @@ -154,15 +171,7 @@ impl HttpRequest { /// Get *ConnectionInfo* for the current request. #[inline] pub fn connection_info(&self) -> Ref { - ConnectionInfo::get(&*self, &*self.config()) - } -} - -impl Deref for HttpRequest { - type Target = RequestHead; - - fn deref(&self) -> &RequestHead { - self.head() + ConnectionInfo::get(self.head(), &*self.config()) } } @@ -219,7 +228,7 @@ impl

    FromRequest

    for HttpRequest { #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - Ok(req.clone()) + Ok(req.request().clone()) } } diff --git a/src/responder.rs b/src/responder.rs index 50467883c..3e0676289 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -313,7 +313,7 @@ pub(crate) mod tests { let req = TestRequest::with_uri("/some").to_request(); let resp = TestRequest::block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); - match resp.body() { + match resp.response().body() { ResponseBody::Body(Body::Bytes(ref b)) => { let bytes: Bytes = b.clone().into(); assert_eq!(bytes, Bytes::from_static(b"some")); diff --git a/src/scope.rs b/src/scope.rs index 874240e73..7ad2d95eb 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -693,7 +693,7 @@ mod tests { let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); - match resp.body() { + match resp.response().body() { ResponseBody::Body(Body::Bytes(ref b)) => { let bytes: Bytes = b.clone().into(); assert_eq!(bytes, Bytes::from_static(b"project: project1")); @@ -799,7 +799,7 @@ mod tests { let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); - match resp.body() { + match resp.response().body() { ResponseBody::Body(Body::Bytes(ref b)) => { let bytes: Bytes = b.clone().into(); assert_eq!(bytes, Bytes::from_static(b"project: project_1")); @@ -826,7 +826,7 @@ mod tests { let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); - match resp.body() { + match resp.response().body() { ResponseBody::Body(Body::Bytes(ref b)) => { let bytes: Bytes = b.clone().into(); assert_eq!(bytes, Bytes::from_static(b"project: test - 1")); diff --git a/src/service.rs b/src/service.rs index c260f25b2..13aae8692 100644 --- a/src/service.rs +++ b/src/service.rs @@ -4,7 +4,7 @@ use std::marker::PhantomData; use std::rc::Rc; use actix_http::body::{Body, MessageBody, ResponseBody}; -use actix_http::http::{HeaderMap, Method, Uri, Version}; +use actix_http::http::{HeaderMap, Method, StatusCode, Uri, Version}; use actix_http::{ Error, Extensions, HttpMessage, Payload, PayloadStream, Request, RequestHead, Response, ResponseHead, @@ -123,7 +123,13 @@ impl

    ServiceRequest

    { } #[inline] - /// Returns mutable Request's headers. + /// Returns request's headers. + pub fn headers(&self) -> &HeaderMap { + &self.head().headers + } + + #[inline] + /// Returns mutable request's headers. pub fn headers_mut(&mut self) -> &mut HeaderMap { &mut self.head_mut().headers } @@ -202,20 +208,6 @@ impl

    HttpMessage for ServiceRequest

    { } } -impl

    std::ops::Deref for ServiceRequest

    { - type Target = RequestHead; - - fn deref(&self) -> &RequestHead { - self.req.head() - } -} - -impl

    std::ops::DerefMut for ServiceRequest

    { - fn deref_mut(&mut self) -> &mut RequestHead { - self.head_mut() - } -} - impl

    fmt::Debug for ServiceRequest

    { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!( @@ -255,11 +247,19 @@ impl

    ServiceFromRequest

    { } #[inline] + /// Get reference to inner HttpRequest + pub fn request(&self) -> &HttpRequest { + &self.req + } + + #[inline] + /// Convert this request into a HttpRequest pub fn into_request(self) -> HttpRequest { self.req } #[inline] + /// Get match information for this request pub fn match_info_mut(&mut self) -> &mut Path { &mut self.req.path } @@ -281,14 +281,6 @@ impl

    ServiceFromRequest

    { } } -impl

    std::ops::Deref for ServiceFromRequest

    { - type Target = HttpRequest; - - fn deref(&self) -> &HttpRequest { - &self.req - } -} - impl

    HttpMessage for ServiceFromRequest

    { type Stream = P; @@ -366,6 +358,24 @@ impl ServiceResponse { &mut self.response } + /// Get the response status code + #[inline] + pub fn status(&self) -> StatusCode { + self.response.status() + } + + #[inline] + /// Returns response's headers. + pub fn headers(&self) -> &HeaderMap { + self.response.headers() + } + + #[inline] + /// Returns mutable response's headers. + pub fn headers_mut(&mut self) -> &mut HeaderMap { + self.response.headers_mut() + } + /// Execute closure and in case of error convert it to response. pub fn checked_expr(mut self, f: F) -> Self where @@ -402,20 +412,6 @@ impl ServiceResponse { } } -impl std::ops::Deref for ServiceResponse { - type Target = Response; - - fn deref(&self) -> &Response { - self.response() - } -} - -impl std::ops::DerefMut for ServiceResponse { - fn deref_mut(&mut self) -> &mut Response { - self.response_mut() - } -} - impl Into> for ServiceResponse { fn into(self) -> Response { self.response diff --git a/src/types/form.rs b/src/types/form.rs index cd4d09bb5..812a08e52 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -80,7 +80,7 @@ where #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - let req2 = req.clone(); + let req2 = req.request().clone(); let (limit, err) = req .route_data::() .map(|c| (c.limit, c.ehandler.clone())) diff --git a/src/types/json.rs b/src/types/json.rs index 9e13d994e..c8ed5afd3 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -174,7 +174,7 @@ where #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - let req2 = req.clone(); + let req2 = req.request().clone(); let (limit, err) = req .route_data::() .map(|c| (c.limit, c.ehandler.clone())) diff --git a/src/types/path.rs b/src/types/path.rs index 4e6784794..fbd106630 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -170,7 +170,7 @@ where #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - Self::extract(req).map_err(ErrorNotFound) + Self::extract(req.request()).map_err(ErrorNotFound) } } diff --git a/src/types/query.rs b/src/types/query.rs index f0eb6a7ae..85dab0610 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -119,7 +119,7 @@ where #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - serde_urlencoded::from_str::(req.query_string()) + serde_urlencoded::from_str::(req.request().query_string()) .map(|val| Ok(Query(val))) .unwrap_or_else(|e| Err(e.into())) } diff --git a/tests/test_server.rs b/tests/test_server.rs index fc590ff0b..3c5d09066 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -54,7 +54,7 @@ fn test_body() { ) }); - let mut response = srv.block_on(srv.get().send()).unwrap(); + let mut response = srv.block_on(srv.get("/").send()).unwrap(); assert!(response.status().is_success()); // read response @@ -73,7 +73,7 @@ fn test_body_gzip() { ) }); - let mut response = srv.block_on(srv.get().no_decompress().send()).unwrap(); + let mut response = srv.block_on(srv.get("/").no_decompress().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -111,7 +111,7 @@ fn test_body_encoding_override() { }); // Builder - let mut response = srv.block_on(srv.get().no_decompress().send()).unwrap(); + let mut response = srv.block_on(srv.get("/").no_decompress().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -161,7 +161,7 @@ fn test_body_gzip_large() { ) }); - let mut response = srv.block_on(srv.get().no_decompress().send()).unwrap(); + let mut response = srv.block_on(srv.get("/").no_decompress().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -195,7 +195,7 @@ fn test_body_gzip_large_random() { ) }); - let mut response = srv.block_on(srv.get().no_decompress().send()).unwrap(); + let mut response = srv.block_on(srv.get("/").no_decompress().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -224,7 +224,7 @@ fn test_body_chunked_implicit() { ) }); - let mut response = srv.block_on(srv.get().no_decompress().send()).unwrap(); + let mut response = srv.block_on(srv.get("/").no_decompress().send()).unwrap(); assert!(response.status().is_success()); assert_eq!( response.headers().get(TRANSFER_ENCODING).unwrap(), @@ -258,7 +258,7 @@ fn test_body_br_streaming() { let mut response = srv .block_on( - srv.get() + srv.get("/") .header(ACCEPT_ENCODING, "br") .no_decompress() .send(), @@ -284,7 +284,7 @@ fn test_head_binary() { ))) }); - let mut response = srv.block_on(srv.head().send()).unwrap(); + let mut response = srv.block_on(srv.head("/").send()).unwrap(); assert!(response.status().is_success()); { @@ -310,7 +310,7 @@ fn test_no_chunking() { )))) }); - let mut response = srv.block_on(srv.get().send()).unwrap(); + let mut response = srv.block_on(srv.get("/").send()).unwrap(); assert!(response.status().is_success()); assert!(!response.headers().contains_key(TRANSFER_ENCODING)); @@ -333,7 +333,7 @@ fn test_body_deflate() { }); // client request - let mut response = srv.block_on(srv.get().no_decompress().send()).unwrap(); + let mut response = srv.block_on(srv.get("/").no_decompress().send()).unwrap(); assert!(response.status().is_success()); // read response @@ -362,7 +362,7 @@ fn test_body_brotli() { // client request let mut response = srv .block_on( - srv.get() + srv.get("/") .header(ACCEPT_ENCODING, "br") .no_decompress() .send(), @@ -398,7 +398,7 @@ fn test_encoding() { let enc = e.finish().unwrap(); let request = srv - .post() + .post("/") .header(CONTENT_ENCODING, "gzip") .send_body(enc.clone()); let mut response = srv.block_on(request).unwrap(); @@ -427,7 +427,7 @@ fn test_gzip_encoding() { let enc = e.finish().unwrap(); let request = srv - .post() + .post("/") .header(CONTENT_ENCODING, "gzip") .send_body(enc.clone()); let mut response = srv.block_on(request).unwrap(); @@ -457,7 +457,7 @@ fn test_gzip_encoding_large() { let enc = e.finish().unwrap(); let request = srv - .post() + .post("/") .header(CONTENT_ENCODING, "gzip") .send_body(enc.clone()); let mut response = srv.block_on(request).unwrap(); @@ -491,7 +491,7 @@ fn test_reading_gzip_encoding_large_random() { let enc = e.finish().unwrap(); let request = srv - .post() + .post("/") .header(CONTENT_ENCODING, "gzip") .send_body(enc.clone()); let mut response = srv.block_on(request).unwrap(); @@ -521,7 +521,7 @@ fn test_reading_deflate_encoding() { // client request let request = srv - .post() + .post("/") .header(CONTENT_ENCODING, "deflate") .send_body(enc.clone()); let mut response = srv.block_on(request).unwrap(); @@ -551,7 +551,7 @@ fn test_reading_deflate_encoding_large() { // client request let request = srv - .post() + .post("/") .header(CONTENT_ENCODING, "deflate") .send_body(enc.clone()); let mut response = srv.block_on(request).unwrap(); @@ -585,7 +585,7 @@ fn test_reading_deflate_encoding_large_random() { // client request let request = srv - .post() + .post("/") .header(CONTENT_ENCODING, "deflate") .send_body(enc.clone()); let mut response = srv.block_on(request).unwrap(); @@ -615,7 +615,7 @@ fn test_brotli_encoding() { // client request let request = srv - .post() + .post("/") .header(CONTENT_ENCODING, "br") .send_body(enc.clone()); let mut response = srv.block_on(request).unwrap(); @@ -645,7 +645,7 @@ fn test_brotli_encoding_large() { // client request let request = srv - .post() + .post("/") .header(CONTENT_ENCODING, "br") .send_body(enc.clone()); let mut response = srv.block_on(request).unwrap(); @@ -912,7 +912,7 @@ fn test_reading_deflate_encoding_large_random_ssl() { // .finish(); // let second_cookie = http::Cookie::new("second", "second_value"); -// let request = srv.get().finish().unwrap(); +// let request = srv.get("/").finish().unwrap(); // let response = srv.execute(request.send()).unwrap(); // assert!(response.status().is_success()); From deac983bc702b0b0ed0f40a9017fcc32b064067d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 2 Apr 2019 14:04:28 -0700 Subject: [PATCH 422/427] fix test-server workspace setup --- Cargo.toml | 1 + test-server/Cargo.toml | 4 ++++ test-server/src/lib.rs | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b8bd6efcc..3634d8e1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ members = [ "actix-session", "actix-web-actors", "actix-web-codegen", + "test-server", ] [package.metadata.docs.rs] diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 838f2d8d2..16a992792 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -53,3 +53,7 @@ time = "0.1" tokio-tcp = "0.1" tokio-timer = "0.2" openssl = { version="0.10", optional = true } + +[dev-dependencies] +actix-web = "1.0.0-alpa.2" +actix-http = "0.1.0-alpa.2" diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index b64ff433e..154345b42 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -36,7 +36,7 @@ use net2::TcpBuilder; /// ) /// ); /// -/// let req = srv.get(); +/// let req = srv.get("/"); /// let response = srv.block_on(req.send()).unwrap(); /// assert!(response.status().is_success()); /// } From f100976ef0d7309e88ef72e26165d02446fd0f65 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 2 Apr 2019 14:08:30 -0700 Subject: [PATCH 423/427] rename close_connection to force_close --- awc/CHANGES.md | 2 ++ awc/src/request.rs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 4bc9fc0b1..a91b28bee 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -16,6 +16,8 @@ * `ClientResponse::body()` does not consume response object. +* Renamed `ClientRequest::close_connection()` to `ClientRequest::force_close()` + ## [0.1.0-alpha.2] - 2019-03-29 diff --git a/awc/src/request.rs b/awc/src/request.rs index 4e3ab47d6..b96b39e2f 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -226,10 +226,10 @@ impl ClientRequest { self } - /// Close connection instead of returning it back to connections pool. + /// Force close connection instead of returning it back to connections pool. /// This setting affect only http/1 connections. #[inline] - pub fn close_connection(mut self) -> Self { + pub fn force_close(mut self) -> Self { self.head.set_connection_type(ConnectionType::Close); self } From 00000fb316d7813324e31b8c8ea2f10fb618b57d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 2 Apr 2019 14:27:54 -0700 Subject: [PATCH 424/427] mut obj --- test-server/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 154345b42..98bef99be 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -195,7 +195,7 @@ impl TestServerRuntime { pub fn load_body( &mut self, - response: ClientResponse, + mut response: ClientResponse, ) -> Result where S: Stream + 'static, From db1f7651a3405d15a73adea5498240eba84be0ea Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 2 Apr 2019 14:47:59 -0700 Subject: [PATCH 425/427] more patch cratesio --- Cargo.toml | 4 ++++ actix-http/tests/test_client.rs | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3634d8e1f..c1a2e1844 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,4 +120,8 @@ actix = { git = "https://github.com/actix/actix.git" } actix-web = { path = "." } actix-http = { path = "actix-http" } actix-http-test = { path = "test-server" } +actix-web-codegen = { path = "actix-web-codegen" } +actix-web-actors = { path = "actix-web-actors" } +actix-session = { path = "actix-session" } +actix-files = { path = "actix-files" } awc = { path = "awc" } diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index 109a3e4c4..817164f81 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -61,9 +61,7 @@ fn test_connection_close() { .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .map(|_| ()) }); - let response = srv - .block_on(srv.get("/").close_connection().send()) - .unwrap(); + let response = srv.block_on(srv.get("/").force_close().send()).unwrap(); assert!(response.status().is_success()); } From 4227cddd307cead39121aaff07d19e27b235ebb8 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 2 Apr 2019 15:00:10 -0700 Subject: [PATCH 426/427] fix dev dependencies --- Cargo.toml | 2 +- actix-files/src/lib.rs | 4 ++-- actix-http/Cargo.toml | 2 +- actix-session/src/cookie.rs | 2 ++ actix-session/src/lib.rs | 2 +- actix-web-actors/src/ws.rs | 2 +- awc/Cargo.toml | 2 +- awc/tests/test_client.rs | 12 ++++++------ 8 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c1a2e1844..c5ef5f06c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "1.0.0-alpha.3" +version = "1.0.0-alpha.2" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index d5a47653e..8404ab319 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -552,7 +552,7 @@ impl

    FromRequest

    for PathBufWrp { type Future = Result; fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - PathBufWrp::get_pathbuf(req.match_info().path()) + PathBufWrp::get_pathbuf(req.request().match_info().path()) } } @@ -1049,7 +1049,7 @@ mod tests { .new_service(&()), ) .unwrap(); - let req = TestRequest::with_uri("/missing").to_service(); + let req = TestRequest::with_uri("/missing").to_srv_request(); let mut resp = test::call_success(&mut st, req); assert_eq!(resp.status(), StatusCode::OK); diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index a9fda44eb..2de624b7c 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "0.1.0-alpha.3" +version = "0.1.0-alpha.2" authors = ["Nikolay Kim "] description = "Actix http primitives" readme = "README.md" diff --git a/actix-session/src/cookie.rs b/actix-session/src/cookie.rs index 9e4fe78b2..f7b4ec03a 100644 --- a/actix-session/src/cookie.rs +++ b/actix-session/src/cookie.rs @@ -333,6 +333,7 @@ mod tests { let request = test::TestRequest::get().to_request(); let response = test::block_on(app.call(request)).unwrap(); assert!(response + .response() .cookies() .find(|c| c.name() == "actix-session") .is_some()); @@ -352,6 +353,7 @@ mod tests { let request = test::TestRequest::get().to_request(); let response = test::block_on(app.call(request)).unwrap(); assert!(response + .response() .cookies() .find(|c| c.name() == "actix-session") .is_some()); diff --git a/actix-session/src/lib.rs b/actix-session/src/lib.rs index 819773c6a..0cd1b9ed8 100644 --- a/actix-session/src/lib.rs +++ b/actix-session/src/lib.rs @@ -190,7 +190,7 @@ mod tests { #[test] fn session() { - let mut req = test::TestRequest::default().to_service(); + let mut req = test::TestRequest::default().to_srv_request(); Session::set_session( vec![("key".to_string(), "\"value\"".to_string())].into_iter(), diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index 436011888..b2c0d4043 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -64,7 +64,7 @@ pub fn handshake(req: &HttpRequest) -> Result"] description = "Actix http client." readme = "README.md" diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 6aed72e41..a2882708a 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -45,14 +45,14 @@ fn test_simple() { }); let request = srv.get("/").header("x-test", "111").send(); - let response = srv.block_on(request).unwrap(); + let mut response = srv.block_on(request).unwrap(); assert!(response.status().is_success()); // read response let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); - let response = srv.block_on(srv.post("/").send()).unwrap(); + let mut response = srv.block_on(srv.post("/").send()).unwrap(); assert!(response.status().is_success()); // read response @@ -177,7 +177,7 @@ fn test_client_gzip_encoding() { }); // client request - let response = srv.block_on(srv.post("/").send()).unwrap(); + let mut response = srv.block_on(srv.post("/").send()).unwrap(); assert!(response.status().is_success()); // read response @@ -200,7 +200,7 @@ fn test_client_gzip_encoding_large() { }); // client request - let response = srv.block_on(srv.post("/").send()).unwrap(); + let mut response = srv.block_on(srv.post("/").send()).unwrap(); assert!(response.status().is_success()); // read response @@ -229,7 +229,7 @@ fn test_client_gzip_encoding_large_random() { }); // client request - let response = srv.block_on(srv.post("/").send_body(data.clone())).unwrap(); + let mut response = srv.block_on(srv.post("/").send_body(data.clone())).unwrap(); assert!(response.status().is_success()); // read response @@ -253,7 +253,7 @@ fn test_client_brotli_encoding() { }); // client request - let response = srv.block_on(srv.post("/").send_body(STR)).unwrap(); + let mut response = srv.block_on(srv.post("/").send_body(STR)).unwrap(); assert!(response.status().is_success()); // read response From 51d5006ccf2f16ab0cfecf8b8d95f81894850c12 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 2 Apr 2019 20:50:25 -0700 Subject: [PATCH 427/427] Detect socket disconnection during protocol selection --- actix-http/CHANGES.md | 2 ++ actix-http/src/service/service.rs | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 79187e7a5..e5a162310 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -8,6 +8,8 @@ * Preallocate read buffer for h1 codec +* Detect socket disconnection during protocol selection + ## [0.1.0-alpha.2] - 2019-03-29 diff --git a/actix-http/src/service/service.rs b/actix-http/src/service/service.rs index 0bc1634d8..50a1a6bdf 100644 --- a/actix-http/src/service/service.rs +++ b/actix-http/src/service/service.rs @@ -247,7 +247,10 @@ where loop { unsafe { let b = item.1.bytes_mut(); - let n = { try_ready!(item.0.poll_read(b)) }; + let n = try_ready!(item.0.poll_read(b)); + if n == 0 { + return Ok(Async::Ready(())); + } item.1.advance_mut(n); if item.1.len() >= HTTP2_PREFACE.len() { break;