mirror of
https://github.com/actix/actix-web.git
synced 2024-11-26 19:41:12 +00:00
complete impl for client request and response
This commit is contained in:
parent
6416a796c3
commit
b686f39d0b
10 changed files with 474 additions and 68 deletions
|
@ -1,7 +1,9 @@
|
||||||
mod parser;
|
mod parser;
|
||||||
mod request;
|
mod request;
|
||||||
mod response;
|
mod response;
|
||||||
|
mod writer;
|
||||||
|
|
||||||
|
pub(crate) use self::writer::HttpClientWriter;
|
||||||
pub use self::request::{ClientRequest, ClientRequestBuilder};
|
pub use self::request::{ClientRequest, ClientRequestBuilder};
|
||||||
pub use self::response::ClientResponse;
|
pub use self::response::ClientResponse;
|
||||||
pub use self::parser::{HttpResponseParser, HttpResponseParserError};
|
pub use self::parser::{HttpResponseParser, HttpResponseParserError};
|
||||||
|
|
|
@ -13,6 +13,7 @@ use server::h1::{Decoder, chunked};
|
||||||
use server::encoding::PayloadType;
|
use server::encoding::PayloadType;
|
||||||
|
|
||||||
use super::ClientResponse;
|
use super::ClientResponse;
|
||||||
|
use super::response::ClientMessage;
|
||||||
|
|
||||||
const MAX_BUFFER_SIZE: usize = 131_072;
|
const MAX_BUFFER_SIZE: usize = 131_072;
|
||||||
const MAX_HEADERS: usize = 96;
|
const MAX_HEADERS: usize = 96;
|
||||||
|
@ -225,10 +226,16 @@ impl HttpResponseParser {
|
||||||
decoder: decoder,
|
decoder: decoder,
|
||||||
};
|
};
|
||||||
Ok(Async::Ready(
|
Ok(Async::Ready(
|
||||||
(ClientResponse::new(status, version, hdrs, Some(payload)), Some(info))))
|
(ClientResponse::new(
|
||||||
|
ClientMessage{
|
||||||
|
status: status, version: version,
|
||||||
|
headers: hdrs, cookies: None, payload: Some(payload)}), Some(info))))
|
||||||
} else {
|
} else {
|
||||||
Ok(Async::Ready(
|
Ok(Async::Ready(
|
||||||
(ClientResponse::new(status, version, hdrs, None), None)))
|
(ClientResponse::new(
|
||||||
|
ClientMessage{
|
||||||
|
status: status, version: version,
|
||||||
|
headers: hdrs, cookies: None, payload: None}), None)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ use body::Body;
|
||||||
use error::Error;
|
use error::Error;
|
||||||
use headers::ContentEncoding;
|
use headers::ContentEncoding;
|
||||||
|
|
||||||
|
/// An HTTP Client Request
|
||||||
pub struct ClientRequest {
|
pub struct ClientRequest {
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
method: Method,
|
method: Method,
|
||||||
|
@ -38,6 +38,44 @@ impl Default for ClientRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ClientRequest {
|
||||||
|
|
||||||
|
/// Create request builder for `GET` request
|
||||||
|
pub fn get<U>(uri: U) -> ClientRequestBuilder where Uri: HttpTryFrom<U> {
|
||||||
|
let mut builder = ClientRequest::build();
|
||||||
|
builder.method(Method::GET).uri(uri);
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create request builder for `HEAD` request
|
||||||
|
pub fn head<U>(uri: U) -> ClientRequestBuilder where Uri: HttpTryFrom<U> {
|
||||||
|
let mut builder = ClientRequest::build();
|
||||||
|
builder.method(Method::HEAD).uri(uri);
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create request builder for `POST` request
|
||||||
|
pub fn post<U>(uri: U) -> ClientRequestBuilder where Uri: HttpTryFrom<U> {
|
||||||
|
let mut builder = ClientRequest::build();
|
||||||
|
builder.method(Method::POST).uri(uri);
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create request builder for `PUT` request
|
||||||
|
pub fn put<U>(uri: U) -> ClientRequestBuilder where Uri: HttpTryFrom<U> {
|
||||||
|
let mut builder = ClientRequest::build();
|
||||||
|
builder.method(Method::PUT).uri(uri);
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create request builder for `DELETE` request
|
||||||
|
pub fn delete<U>(uri: U) -> ClientRequestBuilder where Uri: HttpTryFrom<U> {
|
||||||
|
let mut builder = ClientRequest::build();
|
||||||
|
builder.method(Method::DELETE).uri(uri);
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ClientRequest {
|
impl ClientRequest {
|
||||||
|
|
||||||
/// Create client request builder
|
/// Create client request builder
|
||||||
|
@ -73,6 +111,18 @@ impl ClientRequest {
|
||||||
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
|
/// Get the headers from the request
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn headers(&self) -> &HeaderMap {
|
pub fn headers(&self) -> &HeaderMap {
|
||||||
|
@ -120,6 +170,10 @@ impl fmt::Debug for ClientRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// An HTTP client request builder
|
||||||
|
///
|
||||||
|
/// This type can be used to construct an instance of `ClientRequest` through a
|
||||||
|
/// builder-like pattern.
|
||||||
pub struct ClientRequestBuilder {
|
pub struct ClientRequestBuilder {
|
||||||
request: Option<ClientRequest>,
|
request: Option<ClientRequest>,
|
||||||
err: Option<HttpError>,
|
err: Option<HttpError>,
|
||||||
|
@ -165,7 +219,10 @@ impl ClientRequestBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a header.
|
/// Add a header.
|
||||||
|
///
|
||||||
|
/// Header get appended to existing header.
|
||||||
|
/// To override header use `set_header()` method.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # extern crate http;
|
/// # extern crate http;
|
||||||
|
@ -266,9 +323,10 @@ impl ClientRequestBuilder {
|
||||||
/// # use actix_web::httpcodes::*;
|
/// # use actix_web::httpcodes::*;
|
||||||
/// #
|
/// #
|
||||||
/// use actix_web::headers::Cookie;
|
/// use actix_web::headers::Cookie;
|
||||||
|
/// use actix_web::client::ClientRequest;
|
||||||
///
|
///
|
||||||
/// fn index(req: HttpRequest) -> Result<HttpResponse> {
|
/// fn main() {
|
||||||
/// Ok(HTTPOk.build()
|
/// let req = ClientRequest::build()
|
||||||
/// .cookie(
|
/// .cookie(
|
||||||
/// Cookie::build("name", "value")
|
/// Cookie::build("name", "value")
|
||||||
/// .domain("www.rust-lang.org")
|
/// .domain("www.rust-lang.org")
|
||||||
|
@ -276,9 +334,8 @@ impl ClientRequestBuilder {
|
||||||
/// .secure(true)
|
/// .secure(true)
|
||||||
/// .http_only(true)
|
/// .http_only(true)
|
||||||
/// .finish())
|
/// .finish())
|
||||||
/// .finish()?)
|
/// .finish().unwrap();
|
||||||
/// }
|
/// }
|
||||||
/// fn main() {}
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self {
|
pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self {
|
||||||
if self.cookies.is_none() {
|
if self.cookies.is_none() {
|
||||||
|
|
|
@ -1,70 +1,241 @@
|
||||||
#![allow(dead_code)]
|
use std::{fmt, str};
|
||||||
use std::fmt;
|
use std::rc::Rc;
|
||||||
|
use std::cell::UnsafeCell;
|
||||||
|
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
use cookie::Cookie;
|
||||||
|
use futures::{Async, Future, Poll, Stream};
|
||||||
|
use http_range::HttpRange;
|
||||||
use http::{HeaderMap, StatusCode, Version};
|
use http::{HeaderMap, StatusCode, Version};
|
||||||
use http::header::HeaderValue;
|
use http::header::{self, HeaderValue};
|
||||||
|
use mime::Mime;
|
||||||
|
use serde_json;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
use payload::Payload;
|
use payload::{Payload, ReadAny};
|
||||||
|
use multipart::Multipart;
|
||||||
|
use httprequest::UrlEncoded;
|
||||||
|
use error::{CookieParseError, ParseError, PayloadError, JsonPayloadError, HttpRangeError};
|
||||||
|
|
||||||
|
|
||||||
pub struct ClientResponse {
|
pub(crate) struct ClientMessage {
|
||||||
/// The response's status
|
pub status: StatusCode,
|
||||||
status: StatusCode,
|
pub version: Version,
|
||||||
|
pub headers: HeaderMap<HeaderValue>,
|
||||||
/// The response's version
|
pub cookies: Option<Vec<Cookie<'static>>>,
|
||||||
version: Version,
|
pub payload: Option<Payload>,
|
||||||
|
|
||||||
/// The response's headers
|
|
||||||
headers: HeaderMap<HeaderValue>,
|
|
||||||
|
|
||||||
payload: Option<Payload>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientResponse {
|
impl Default for ClientMessage {
|
||||||
pub fn new(status: StatusCode, version: Version,
|
|
||||||
headers: HeaderMap<HeaderValue>, payload: Option<Payload>) -> Self {
|
fn default() -> ClientMessage {
|
||||||
ClientResponse {
|
ClientMessage {
|
||||||
status: status, version: version, headers: headers, payload: payload
|
status: StatusCode::OK,
|
||||||
|
version: Version::HTTP_11,
|
||||||
|
headers: HeaderMap::with_capacity(16),
|
||||||
|
cookies: None,
|
||||||
|
payload: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the HTTP version of this response.
|
pub struct ClientResponse(Rc<UnsafeCell<ClientMessage>>);
|
||||||
|
|
||||||
|
impl ClientResponse {
|
||||||
|
|
||||||
|
pub(crate) fn new(msg: ClientMessage) -> ClientResponse {
|
||||||
|
ClientResponse(Rc::new(UnsafeCell::new(msg)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn as_ref(&self) -> &ClientMessage {
|
||||||
|
unsafe{ &*self.0.get() }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref))]
|
||||||
|
fn as_mut(&self) -> &mut ClientMessage {
|
||||||
|
unsafe{ &mut *self.0.get() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the HTTP version of this response.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn version(&self) -> Version {
|
pub fn version(&self) -> Version {
|
||||||
self.version
|
self.as_ref().version
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the headers from the response.
|
/// Get the headers from the response.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn headers(&self) -> &HeaderMap {
|
pub fn headers(&self) -> &HeaderMap {
|
||||||
&self.headers
|
&self.as_ref().headers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a mutable reference to the headers.
|
/// Get a mutable reference to the headers.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||||
&mut self.headers
|
&mut self.as_mut().headers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the status from the server.
|
/// Get the status from the server.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn status(&self) -> StatusCode {
|
pub fn status(&self) -> StatusCode {
|
||||||
self.status
|
self.as_ref().status
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the `StatusCode` for this response.
|
/// Set the `StatusCode` for this response.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn status_mut(&mut self) -> &mut StatusCode {
|
pub fn set_status(&mut self, status: StatusCode) {
|
||||||
&mut self.status
|
self.as_mut().status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load request cookies.
|
||||||
|
pub fn cookies(&self) -> Result<&Vec<Cookie<'static>>, CookieParseError> {
|
||||||
|
if self.as_ref().cookies.is_none() {
|
||||||
|
let msg = self.as_mut();
|
||||||
|
let mut cookies = Vec::new();
|
||||||
|
if let Some(val) = msg.headers.get(header::COOKIE) {
|
||||||
|
let s = str::from_utf8(val.as_bytes())
|
||||||
|
.map_err(CookieParseError::from)?;
|
||||||
|
for cookie in s.split("; ") {
|
||||||
|
cookies.push(Cookie::parse_encoded(cookie)?.into_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg.cookies = Some(cookies)
|
||||||
|
}
|
||||||
|
Ok(self.as_ref().cookies.as_ref().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return request cookie.
|
||||||
|
pub fn cookie(&self, name: &str) -> Option<&Cookie> {
|
||||||
|
if let Ok(cookies) = self.cookies() {
|
||||||
|
for cookie in cookies {
|
||||||
|
if cookie.name() == name {
|
||||||
|
return Some(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the request content type. If request does not contain
|
||||||
|
/// *Content-Type* header, empty str get returned.
|
||||||
|
pub 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert the request content type to a known mime type.
|
||||||
|
pub fn mime_type(&self) -> Option<Mime> {
|
||||||
|
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) => Some(mt),
|
||||||
|
Err(_) => None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if request has chunked transfer encoding
|
||||||
|
pub fn chunked(&self) -> Result<bool, ParseError> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses Range HTTP header string as per RFC 2616.
|
||||||
|
/// `size` is full size of response (file).
|
||||||
|
pub fn range(&self, size: u64) -> Result<Vec<HttpRange>, HttpRangeError> {
|
||||||
|
if let Some(range) = self.headers().get(header::RANGE) {
|
||||||
|
HttpRange::parse(unsafe{str::from_utf8_unchecked(range.as_bytes())}, size)
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
} else {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns reference to the associated http payload.
|
||||||
|
#[inline]
|
||||||
|
pub fn payload(&self) -> &Payload {
|
||||||
|
let msg = self.as_mut();
|
||||||
|
if msg.payload.is_none() {
|
||||||
|
msg.payload = Some(Payload::empty());
|
||||||
|
}
|
||||||
|
msg.payload.as_ref().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns mutable reference to the associated http payload.
|
||||||
|
#[inline]
|
||||||
|
pub fn payload_mut(&mut self) -> &mut Payload {
|
||||||
|
let msg = self.as_mut();
|
||||||
|
if msg.payload.is_none() {
|
||||||
|
msg.payload = Some(Payload::empty());
|
||||||
|
}
|
||||||
|
msg.payload.as_mut().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load request body.
|
||||||
|
///
|
||||||
|
/// By default only 256Kb payload reads to a memory, then `ResponseBody`
|
||||||
|
/// resolves to an error. Use `RequestBody::limit()`
|
||||||
|
/// method to change upper limit.
|
||||||
|
pub fn body(&self) -> ResponseBody {
|
||||||
|
ResponseBody::from_response(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Return stream to http payload processes as multipart.
|
||||||
|
///
|
||||||
|
/// Content-type: multipart/form-data;
|
||||||
|
pub fn multipart(&mut self) -> Multipart {
|
||||||
|
Multipart::from_response(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse `application/x-www-form-urlencoded` encoded body.
|
||||||
|
/// Return `UrlEncoded` future. It resolves to a `HashMap<String, String>` which
|
||||||
|
/// contains decoded parameters.
|
||||||
|
///
|
||||||
|
/// Returns error:
|
||||||
|
///
|
||||||
|
/// * content type is not `application/x-www-form-urlencoded`
|
||||||
|
/// * transfer encoding is `chunked`.
|
||||||
|
/// * content-length is greater than 256k
|
||||||
|
pub fn urlencoded(&self) -> UrlEncoded {
|
||||||
|
UrlEncoded::from(self.payload().clone(),
|
||||||
|
self.headers(),
|
||||||
|
self.chunked().unwrap_or(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse `application/json` encoded body.
|
||||||
|
/// Return `JsonResponse<T>` future. It resolves to a `T` value.
|
||||||
|
///
|
||||||
|
/// Returns error:
|
||||||
|
///
|
||||||
|
/// * content type is not `application/json`
|
||||||
|
/// * content length is greater than 256k
|
||||||
|
pub fn json<T: DeserializeOwned>(&self) -> JsonResponse<T> {
|
||||||
|
JsonResponse::from_response(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for ClientResponse {
|
impl fmt::Debug for ClientResponse {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let res = write!(
|
let res = write!(
|
||||||
f, "\nClientResponse {:?} {}\n", self.version, self.status);
|
f, "\nClientResponse {:?} {}\n", self.version(), self.status());
|
||||||
let _ = write!(f, " headers:\n");
|
let _ = write!(f, " headers:\n");
|
||||||
for key in self.headers.keys() {
|
for key in self.headers().keys() {
|
||||||
let vals: Vec<_> = self.headers.get_all(key).iter().collect();
|
let vals: Vec<_> = self.headers().get_all(key).iter().collect();
|
||||||
if vals.len() > 1 {
|
if vals.len() > 1 {
|
||||||
let _ = write!(f, " {:?}: {:?}\n", key, vals);
|
let _ = write!(f, " {:?}: {:?}\n", key, vals);
|
||||||
} else {
|
} else {
|
||||||
|
@ -74,3 +245,160 @@ impl fmt::Debug for ClientResponse {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Clone for ClientResponse {
|
||||||
|
fn clone(&self) -> ClientResponse {
|
||||||
|
ClientResponse(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Future that resolves to a complete request body.
|
||||||
|
pub struct ResponseBody {
|
||||||
|
pl: ReadAny,
|
||||||
|
body: BytesMut,
|
||||||
|
limit: usize,
|
||||||
|
resp: Option<ClientResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseBody {
|
||||||
|
|
||||||
|
/// Create `RequestBody` for request.
|
||||||
|
pub fn from_response(resp: &ClientResponse) -> ResponseBody {
|
||||||
|
let pl = resp.payload().readany();
|
||||||
|
ResponseBody {
|
||||||
|
pl: pl,
|
||||||
|
body: BytesMut::new(),
|
||||||
|
limit: 262_144,
|
||||||
|
resp: Some(resp.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 ResponseBody {
|
||||||
|
type Item = Bytes;
|
||||||
|
type Error = PayloadError;
|
||||||
|
|
||||||
|
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||||
|
if let Some(resp) = self.resp.take() {
|
||||||
|
if let Some(len) = resp.headers().get(header::CONTENT_LENGTH) {
|
||||||
|
if let Ok(s) = len.to_str() {
|
||||||
|
if let Ok(len) = s.parse::<u64>() {
|
||||||
|
if len > 262_144 {
|
||||||
|
return Err(PayloadError::Overflow);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(PayloadError::UnknownLength);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(PayloadError::UnknownLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
return match self.pl.poll() {
|
||||||
|
Ok(Async::NotReady) => Ok(Async::NotReady),
|
||||||
|
Ok(Async::Ready(None)) => {
|
||||||
|
Ok(Async::Ready(self.body.take().freeze()))
|
||||||
|
},
|
||||||
|
Ok(Async::Ready(Some(chunk))) => {
|
||||||
|
if (self.body.len() + chunk.len()) > self.limit {
|
||||||
|
Err(PayloadError::Overflow)
|
||||||
|
} else {
|
||||||
|
self.body.extend_from_slice(&chunk);
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Client response payload json parser that resolves to a deserialized `T` value.
|
||||||
|
///
|
||||||
|
/// Returns error:
|
||||||
|
///
|
||||||
|
/// * content type is not `application/json`
|
||||||
|
/// * content length is greater than 256k
|
||||||
|
pub struct JsonResponse<T: DeserializeOwned>{
|
||||||
|
limit: usize,
|
||||||
|
ct: &'static str,
|
||||||
|
resp: Option<ClientResponse>,
|
||||||
|
fut: Option<Box<Future<Item=T, Error=JsonPayloadError>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DeserializeOwned> JsonResponse<T> {
|
||||||
|
|
||||||
|
/// Create `JsonBody` for request.
|
||||||
|
pub fn from_response(resp: &ClientResponse) -> Self {
|
||||||
|
JsonResponse{
|
||||||
|
limit: 262_144,
|
||||||
|
resp: Some(resp.clone()),
|
||||||
|
fut: None,
|
||||||
|
ct: "application/json",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change max size of payload. By default max size is 256Kb
|
||||||
|
pub fn limit(mut self, limit: usize) -> Self {
|
||||||
|
self.limit = limit;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set allowed content type.
|
||||||
|
///
|
||||||
|
/// By default *application/json* content type is used. Set content type
|
||||||
|
/// to empty string if you want to disable content type check.
|
||||||
|
pub fn content_type(mut self, ct: &'static str) -> Self {
|
||||||
|
self.ct = ct;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DeserializeOwned + 'static> Future for JsonResponse<T> {
|
||||||
|
type Item = T;
|
||||||
|
type Error = JsonPayloadError;
|
||||||
|
|
||||||
|
fn poll(&mut self) -> Poll<T, JsonPayloadError> {
|
||||||
|
if let Some(resp) = self.resp.take() {
|
||||||
|
if let Some(len) = resp.headers().get(header::CONTENT_LENGTH) {
|
||||||
|
if let Ok(s) = len.to_str() {
|
||||||
|
if let Ok(len) = s.parse::<usize>() {
|
||||||
|
if len > self.limit {
|
||||||
|
return Err(JsonPayloadError::Overflow);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(JsonPayloadError::Overflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check content-type
|
||||||
|
if !self.ct.is_empty() && resp.content_type() != self.ct {
|
||||||
|
return Err(JsonPayloadError::ContentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
let limit = self.limit;
|
||||||
|
let fut = resp.payload().readany()
|
||||||
|
.from_err()
|
||||||
|
.fold(BytesMut::new(), 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::<T>(&body)?));
|
||||||
|
self.fut = Some(Box::new(fut));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fut.as_mut().expect("JsonResponse could not be used second time").poll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::fmt::Write;
|
||||||
use bytes::BufMut;
|
use bytes::BufMut;
|
||||||
use futures::{Async, Poll};
|
use futures::{Async, Poll};
|
||||||
use tokio_io::AsyncWrite;
|
use tokio_io::AsyncWrite;
|
||||||
|
@ -23,17 +24,17 @@ bitflags! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Writer {
|
pub(crate) struct HttpClientWriter {
|
||||||
flags: Flags,
|
flags: Flags,
|
||||||
written: u64,
|
written: u64,
|
||||||
headers_size: u32,
|
headers_size: u32,
|
||||||
buffer: SharedBytes,
|
buffer: SharedBytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Writer {
|
impl HttpClientWriter {
|
||||||
|
|
||||||
pub fn new(buf: SharedBytes) -> Writer {
|
pub fn new(buf: SharedBytes) -> HttpClientWriter {
|
||||||
Writer {
|
HttpClientWriter {
|
||||||
flags: Flags::empty(),
|
flags: Flags::empty(),
|
||||||
written: 0,
|
written: 0,
|
||||||
headers_size: 0,
|
headers_size: 0,
|
||||||
|
@ -73,7 +74,7 @@ impl Writer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Writer {
|
impl HttpClientWriter {
|
||||||
|
|
||||||
pub fn start(&mut self, msg: &mut ClientRequest) {
|
pub fn start(&mut self, msg: &mut ClientRequest) {
|
||||||
// prepare task
|
// prepare task
|
||||||
|
@ -85,11 +86,8 @@ impl Writer {
|
||||||
buffer.reserve(256 + msg.headers().len() * AVERAGE_HEADER_SIZE);
|
buffer.reserve(256 + msg.headers().len() * AVERAGE_HEADER_SIZE);
|
||||||
|
|
||||||
// status line
|
// status line
|
||||||
// helpers::write_status_line(version, msg.status().as_u16(), &mut buffer);
|
let _ = write!(buffer, "{} {} {:?}\r\n",
|
||||||
// buffer.extend_from_slice(msg.reason().as_bytes());
|
msg.method(), msg.uri().path(), msg.version());
|
||||||
buffer.extend_from_slice(b"GET ");
|
|
||||||
buffer.extend_from_slice(msg.uri().path().as_ref());
|
|
||||||
buffer.extend_from_slice(b" HTTP/1.1\r\n");
|
|
||||||
|
|
||||||
// write headers
|
// write headers
|
||||||
for (key, value) in msg.headers() {
|
for (key, value) in msg.headers() {
|
|
@ -541,7 +541,9 @@ impl<S> HttpRequest<S> {
|
||||||
/// # fn main() {}
|
/// # fn main() {}
|
||||||
/// ```
|
/// ```
|
||||||
pub fn urlencoded(&self) -> UrlEncoded {
|
pub fn urlencoded(&self) -> UrlEncoded {
|
||||||
UrlEncoded::from_request(self)
|
UrlEncoded::from(self.payload().clone(),
|
||||||
|
self.headers(),
|
||||||
|
self.chunked().unwrap_or(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse `application/json` encoded body.
|
/// Parse `application/json` encoded body.
|
||||||
|
@ -624,16 +626,16 @@ pub struct UrlEncoded {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UrlEncoded {
|
impl UrlEncoded {
|
||||||
pub fn from_request<S>(req: &HttpRequest<S>) -> UrlEncoded {
|
pub fn from(pl: Payload, headers: &HeaderMap, chunked: bool) -> UrlEncoded {
|
||||||
let mut encoded = UrlEncoded {
|
let mut encoded = UrlEncoded {
|
||||||
pl: req.payload().clone(),
|
pl: pl,
|
||||||
body: BytesMut::new(),
|
body: BytesMut::new(),
|
||||||
error: None
|
error: None
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(true) = req.chunked() {
|
if chunked {
|
||||||
encoded.error = Some(UrlencodedError::Chunked);
|
encoded.error = Some(UrlencodedError::Chunked);
|
||||||
} else if let Some(len) = req.headers().get(header::CONTENT_LENGTH) {
|
} else if let Some(len) = headers.get(header::CONTENT_LENGTH) {
|
||||||
if let Ok(s) = len.to_str() {
|
if let Ok(s) = len.to_str() {
|
||||||
if let Ok(len) = s.parse::<u64>() {
|
if let Ok(len) = s.parse::<u64>() {
|
||||||
if len > 262_144 {
|
if len > 262_144 {
|
||||||
|
@ -649,7 +651,7 @@ impl UrlEncoded {
|
||||||
|
|
||||||
// check content type
|
// check content type
|
||||||
if encoded.error.is_none() {
|
if encoded.error.is_none() {
|
||||||
if let Some(content_type) = req.headers().get(header::CONTENT_TYPE) {
|
if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
|
||||||
if let Ok(content_type) = content_type.to_str() {
|
if let Ok(content_type) = content_type.to_str() {
|
||||||
if content_type.to_lowercase() == "application/x-www-form-urlencoded" {
|
if content_type.to_lowercase() == "application/x-www-form-urlencoded" {
|
||||||
return encoded
|
return encoded
|
||||||
|
|
|
@ -43,6 +43,8 @@
|
||||||
#![cfg_attr(actix_nightly, feature(
|
#![cfg_attr(actix_nightly, feature(
|
||||||
specialization, // for impl ErrorResponse for std::error::Error
|
specialization, // for impl ErrorResponse for std::error::Error
|
||||||
))]
|
))]
|
||||||
|
#![cfg_attr(feature = "cargo-clippy", allow(
|
||||||
|
decimal_literal_representation,))]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
|
@ -14,6 +14,7 @@ use futures::task::{Task, current as current_task};
|
||||||
|
|
||||||
use error::{ParseError, PayloadError, MultipartError};
|
use error::{ParseError, PayloadError, MultipartError};
|
||||||
use payload::Payload;
|
use payload::Payload;
|
||||||
|
use client::ClientResponse;
|
||||||
use httprequest::HttpRequest;
|
use httprequest::HttpRequest;
|
||||||
|
|
||||||
const MAX_HEADERS: usize = 32;
|
const MAX_HEADERS: usize = 32;
|
||||||
|
@ -97,6 +98,19 @@ impl Multipart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create multipart instance for client response.
|
||||||
|
pub fn from_response(resp: &mut ClientResponse) -> Multipart {
|
||||||
|
match Multipart::boundary(resp.headers()) {
|
||||||
|
Ok(boundary) => Multipart::new(boundary, resp.payload().clone()),
|
||||||
|
Err(err) =>
|
||||||
|
Multipart {
|
||||||
|
error: Some(err),
|
||||||
|
safety: Safety::new(),
|
||||||
|
inner: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract boundary info from headers.
|
/// Extract boundary info from headers.
|
||||||
pub fn boundary(headers: &HeaderMap) -> Result<String, MultipartError> {
|
pub fn boundary(headers: &HeaderMap) -> Result<String, MultipartError> {
|
||||||
if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
|
if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
|
||||||
|
|
|
@ -21,12 +21,11 @@ use server::shared::SharedBytes;
|
||||||
|
|
||||||
use server::{utils, IoStream};
|
use server::{utils, IoStream};
|
||||||
use client::{ClientRequest, ClientRequestBuilder,
|
use client::{ClientRequest, ClientRequestBuilder,
|
||||||
HttpResponseParser, HttpResponseParserError};
|
HttpResponseParser, HttpResponseParserError, HttpClientWriter};
|
||||||
|
|
||||||
use super::Message;
|
use super::Message;
|
||||||
use super::proto::{CloseCode, OpCode};
|
use super::proto::{CloseCode, OpCode};
|
||||||
use super::frame::Frame;
|
use super::frame::Frame;
|
||||||
use super::writer::Writer;
|
|
||||||
use super::connect::{TcpConnector, TcpConnectorError};
|
use super::connect::{TcpConnector, TcpConnectorError};
|
||||||
|
|
||||||
/// Websockt client error
|
/// Websockt client error
|
||||||
|
@ -86,7 +85,7 @@ impl From<HttpResponseParserError> for WsClientError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type WsFuture<T> = Future<Item=(WsReader<T>, WsWriter<T>), Error=WsClientError>;
|
pub type WsFuture<T> = Future<Item=(WsReader<T>, WsWriter<T>), Error=WsClientError>;
|
||||||
|
|
||||||
/// Websockt client
|
/// Websockt client
|
||||||
pub struct WsClient {
|
pub struct WsClient {
|
||||||
|
@ -190,7 +189,7 @@ impl WsClient {
|
||||||
|
|
||||||
struct WsInner<T> {
|
struct WsInner<T> {
|
||||||
stream: T,
|
stream: T,
|
||||||
writer: Writer,
|
writer: HttpClientWriter,
|
||||||
parser: HttpResponseParser,
|
parser: HttpResponseParser,
|
||||||
parser_buf: BytesMut,
|
parser_buf: BytesMut,
|
||||||
closed: bool,
|
closed: bool,
|
||||||
|
@ -218,7 +217,7 @@ impl<T: IoStream> WsHandshake<T> {
|
||||||
|
|
||||||
let inner = WsInner {
|
let inner = WsInner {
|
||||||
stream: stream,
|
stream: stream,
|
||||||
writer: Writer::new(SharedBytes::default()),
|
writer: HttpClientWriter::new(SharedBytes::default()),
|
||||||
parser: HttpResponseParser::default(),
|
parser: HttpResponseParser::default(),
|
||||||
parser_buf: BytesMut::new(),
|
parser_buf: BytesMut::new(),
|
||||||
closed: false,
|
closed: false,
|
||||||
|
|
|
@ -59,18 +59,15 @@ mod frame;
|
||||||
mod proto;
|
mod proto;
|
||||||
mod context;
|
mod context;
|
||||||
mod mask;
|
mod mask;
|
||||||
|
mod client;
|
||||||
|
|
||||||
mod connect;
|
mod connect;
|
||||||
mod writer;
|
|
||||||
|
|
||||||
pub mod client;
|
use self::frame::Frame;
|
||||||
|
use self::proto::{hash_key, OpCode};
|
||||||
use ws::frame::Frame;
|
pub use self::proto::CloseCode;
|
||||||
use ws::proto::{hash_key, OpCode};
|
pub use self::context::WebsocketContext;
|
||||||
pub use ws::proto::CloseCode;
|
pub use self::client::{WsClient, WsClientError, WsReader, WsWriter, WsFuture};
|
||||||
pub use ws::context::WebsocketContext;
|
|
||||||
|
|
||||||
pub use self::client::{WsClient, WsClientError, WsReader, WsWriter};
|
|
||||||
|
|
||||||
const SEC_WEBSOCKET_ACCEPT: &str = "SEC-WEBSOCKET-ACCEPT";
|
const SEC_WEBSOCKET_ACCEPT: &str = "SEC-WEBSOCKET-ACCEPT";
|
||||||
const SEC_WEBSOCKET_KEY: &str = "SEC-WEBSOCKET-KEY";
|
const SEC_WEBSOCKET_KEY: &str = "SEC-WEBSOCKET-KEY";
|
||||||
|
|
Loading…
Reference in a new issue