1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-08-21 21:25:36 +02:00

Compare commits

..

25 Commits

Author SHA1 Message Date
Nikolay Kim
cc6e0c6d04 Fix client payload decompression #674 2019-03-28 20:40:25 -07:00
Zeyi Fan
d9496d46d1 [0.7] Fix never-ending HTTP2 empty response (#737)
* Fix never-ending HTTP2 empty response #737
2019-03-28 17:40:12 -07:00
Jannik Keye
bf8262196f feat: enable use of patch as request method (#718) 2019-03-14 11:36:10 +03:00
Luca Bruno
17ecdd63d2 httpresponse: add constructor for HttpResponseBuilder (#697) 2019-03-13 17:20:18 +03:00
David McGuire
cc7f6b5eef Fix preflight CORS header compliance; refactor previous patch. (#717) 2019-03-11 07:26:54 +03:00
Stephen Ellis
ceca96da28 Added HTTP Authentication for Client (#540) 2019-03-06 12:56:12 +03:00
Douman
42f030d3f4 Ensure that Content-Length zero is specified in empty request 2019-03-05 08:37:15 +03:00
Hugo Benício
6d11ee683f fixing little typo in docs (#711) 2019-03-01 11:34:58 +03:00
Douman
80d4cbe301 Add change notes for new HttpResponseBuilder 2019-02-27 21:37:20 +03:00
Kornel
69d710dbce Add insert and remove() to response builder (#707) 2019-02-27 15:52:42 +03:00
Michael Edwards
0059a55dfb Fix typo 2019-02-13 14:31:28 +03:00
cuebyte
c695358bcb Ignored the If-Modified-Since if If-None-Match is specified (#680) (#692) 2019-02-09 00:33:00 +03:00
Jason Hills
b018e4abaf Fixes TestRequest::with_cookie panic 2019-02-07 07:55:27 +03:00
Vladislav Stepanov
346d85a884 Serve static file directly instead of redirecting (#676) 2019-02-04 13:20:46 +03:00
wildarch
9968afe4a6 Use NamedFile with an existing File (#670) 2019-01-28 08:07:28 +03:00
Tomas Izquierdo Garcia-Faria
f5bec968c7 Bump v_htmlescape version to 0.4 2019-01-25 11:31:42 +03:00
Neil Jensen
a534fdd125 Add io handling for ECONNRESET when data has already been received 2019-01-20 08:45:33 +03:00
rishflab
3431fff4d7 Fixed example in client documentation. This closes #665. 2019-01-14 07:44:30 +03:00
Sameer Puri
d6df2e3399 Fix HttpResponse doc spelling "os" to "of" 2019-01-11 08:45:15 +03:00
Douman
1fbb52ad3b 0.7.18 Bump 2019-01-10 17:05:18 +03:00
Julian Tescher
e5cdd22720 Fix test server listener thread leak (#655) 2019-01-08 10:42:22 -08:00
Douman
4f2e970732 Tidy up CHANGES.md 2019-01-08 10:49:03 +03:00
Douman
4d45313f9d Decode special characters when handling static files 2019-01-08 10:46:58 +03:00
Juan Aguilar
55a2a59906 Improve change askama_escape in favor of v_htmlescape (#651) 2019-01-03 22:34:18 +03:00
Ji Qu
61883042c2 Add with-cookie init-method for TestRequest (#647) 2019-01-02 13:24:08 +03:00
19 changed files with 650 additions and 141 deletions

View File

@@ -1,5 +1,44 @@
# Changes
## [0.7.19] - 2019-03-29
### 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`
* 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
* 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
* Fix never-ending HTTP2 request when response is empty (#709). #737
* Fix client payload decompression #674
## [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

View File

@@ -1,6 +1,6 @@
[package]
name = "actix-web"
version = "0.7.17"
version = "0.7.19"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
readme = "README.md"
@@ -64,7 +64,7 @@ cell = ["actix-net/cell"]
actix = "0.7.9"
actix-net = "0.2.6"
askama_escape = "0.1.0"
v_htmlescape = "0.4"
base64 = "0.10"
bitflags = "1.0"
failure = "^0.1.2"

View File

@@ -5,9 +5,9 @@
//! # extern crate actix;
//! # extern crate futures;
//! # extern crate tokio;
//! # use futures::Future;
//! # use std::process;
//! use actix_web::client;
//! use futures::Future;
//!
//! fn main() {
//! actix::run(
@@ -66,9 +66,9 @@ impl ResponseError for SendRequestError {
/// # extern crate futures;
/// # extern crate tokio;
/// # extern crate env_logger;
/// # use futures::Future;
/// # use std::process;
/// use actix_web::client;
/// use futures::Future;
///
/// fn main() {
/// actix::run(
@@ -105,6 +105,13 @@ pub fn post<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
builder
}
/// Create request builder for `PATCH` requests
pub fn patch<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
builder.method(Method::PATCH).uri(uri);
builder
}
/// Create request builder for `PUT` requests
pub fn put<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();

View File

@@ -6,8 +6,8 @@ use std::time::{Duration, Instant};
use std::{io, mem};
use tokio_timer::Delay;
use actix_inner::dev::Request;
use actix::{Addr, SystemService};
use actix_inner::dev::Request;
use super::{
ClientConnector, ClientConnectorError, ClientRequest, ClientResponse, Connect,
@@ -88,7 +88,8 @@ impl SendRequest {
}
pub(crate) fn with_connector(
req: ClientRequest, conn: Addr<ClientConnector>,
req: ClientRequest,
conn: Addr<ClientConnector>,
) -> SendRequest {
SendRequest {
req,
@@ -363,11 +364,11 @@ impl Pipeline {
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),
Ok(None) => continue,
Err(ref err)
if err.kind() == io::ErrorKind::WouldBlock =>
{
continue
continue;
}
Err(err) => return Err(err.into()),
}

View File

@@ -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;
@@ -111,6 +112,13 @@ impl ClientRequest {
builder
}
/// Create request builder for `PATCH` request
pub fn patch<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
builder.method(Method::PATCH).uri(uri);
builder
}
/// Create request builder for `PUT` request
pub fn put<U: AsRef<str>>(uri: U) -> ClientRequestBuilder {
let mut builder = ClientRequest::build();
@@ -485,6 +493,29 @@ impl ClientRequestBuilder {
self
}
/// Set HTTP basic authorization
pub fn basic_auth<U, P>(&mut self, username: U, password: Option<P>) -> &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<T>( &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 {

View File

@@ -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"),
}
}
}

View File

@@ -193,7 +193,7 @@ impl<T: fmt::Display> fmt::Display for Path<T> {
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
/// Extract typed information from from the request's query.
/// Extract typed information from the request's query.
///
/// ## Example
///

255
src/fs.rs
View File

@@ -11,7 +11,7 @@ use std::{cmp, io};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use askama_escape::{escape as escape_html_entity};
use v_htmlescape::escape as escape_html_entity;
use bytes::Bytes;
use futures::{Async, Future, Poll, Stream};
use futures_cpupool::{CpuFuture, CpuPool};
@@ -120,6 +120,32 @@ pub struct NamedFile<C = DefaultConfig> {
}
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
///
/// ```no_run
/// 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<P: AsRef<Path>>(file: File, path: P) -> io::Result<NamedFile> {
Self::from_file_with_config(file, path, DefaultConfig)
}
/// Attempts to open a file in read-only mode.
///
/// # Examples
@@ -135,16 +161,29 @@ impl NamedFile {
}
impl<C: StaticFileConfig> NamedFile<C> {
/// Attempts to open a file in read-only mode using provided configiration.
/// 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_web::fs::{DefaultConfig, NamedFile};
/// ```no_run
/// extern crate actix_web;
///
/// let file = NamedFile::open_with_config("foo.txt", DefaultConfig);
/// 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 open_with_config<P: AsRef<Path>>(path: P, _: C) -> io::Result<NamedFile<C>> {
pub fn from_file_with_config<P: AsRef<Path>>(file: File, path: P, _: C) -> io::Result<NamedFile<C>> {
let path = path.as_ref().to_path_buf();
// Get the name of the file and use it to construct default Content-Type
@@ -169,7 +208,6 @@ impl<C: StaticFileConfig> NamedFile<C> {
(ct, cd)
};
let file = File::open(&path)?;
let md = file.metadata()?;
let modified = md.modified().ok();
let cpu_pool = None;
@@ -188,6 +226,19 @@ impl<C: StaticFileConfig> NamedFile<C> {
})
}
/// Attempts to open a file in read-only mode using provided configuration.
///
/// # Examples
///
/// ```rust
/// use actix_web::fs::{DefaultConfig, NamedFile};
///
/// let file = NamedFile::open_with_config("foo.txt", DefaultConfig);
/// ```
pub fn open_with_config<P: AsRef<Path>>(path: P, config: C) -> io::Result<NamedFile<C>> {
Self::from_file_with_config(File::open(&path)?, path, config)
}
/// Returns reference to the underlying `File` object.
#[inline]
pub fn file(&self) -> &File {
@@ -390,6 +441,8 @@ impl<C: StaticFileConfig> Responder for NamedFile<C> {
// 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())
{
@@ -739,7 +792,7 @@ impl<S: 'static, C: StaticFileConfig> StaticFiles<S, C> {
/// Set index file
///
/// Redirects to specific index file for directory "/" instead of
/// Shows specific index file for directory "/" instead of
/// showing files listing.
pub fn index_file<T: Into<String>>(mut self, index: T) -> StaticFiles<S, C> {
self.index = Some(index.into());
@@ -756,7 +809,7 @@ impl<S: 'static, C: StaticFileConfig> StaticFiles<S, C> {
&self,
req: &HttpRequest<S>,
) -> Result<AsyncResult<HttpResponse>, Error> {
let tail: String = req.match_info().query("tail")?;
let tail: String = req.match_info().get_decoded("tail").unwrap_or_else(|| "".to_string());
let relpath = PathBuf::from_param(tail.trim_left_matches('/'))?;
// full filepath
@@ -764,17 +817,11 @@ impl<S: 'static, C: StaticFileConfig> StaticFiles<S, C> {
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()
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)
} else if self.show_index {
let dir = Directory::new(self.directory.clone(), path);
@@ -899,6 +946,8 @@ impl HttpRange {
#[cfg(test)]
mod tests {
use std::fs;
use std::time::Duration;
use std::ops::Add;
use super::*;
use application::App;
@@ -918,6 +967,43 @@ mod tests {
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());
@@ -1298,6 +1384,27 @@ mod tests {
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 {
@@ -1410,43 +1517,66 @@ mod tests {
}
#[test]
fn test_redirect_to_index() {
let st = StaticFiles::new(".").unwrap().index_file("index.html");
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::FOUND);
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::LOCATION).unwrap(),
"/tests/index.html"
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::FOUND);
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::LOCATION).unwrap(),
"/tests/index.html"
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_redirect_to_index_nested() {
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::FOUND);
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::LOCATION).unwrap(),
"/src/client/mod.rs"
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_redirect_to_index_with_prefix() {
fn integration_serve_index_with_prefix() {
let mut srv = test::TestServer::with_factory(|| {
App::new()
.prefix("public")
@@ -1455,29 +1585,21 @@ mod tests {
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");
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::FOUND);
let loc = response
.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap();
assert_eq!(loc, "/public/Cargo.toml");
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_redirect_to_index() {
fn integration_serve_index() {
let mut srv = test::TestServer::with_factory(|| {
App::new().handler(
"test",
@@ -1487,25 +1609,26 @@ mod tests {
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");
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::FOUND);
let loc = response
.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap();
assert_eq!(loc, "/test/Cargo.toml");
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]

View File

@@ -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
@@ -246,7 +246,7 @@ impl HttpResponse {
self
}
/// Get body os this response
/// Get body of this response
#[inline]
pub fn body(&self) -> &Body {
&self.get_ref().body
@@ -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 {
@@ -366,7 +372,7 @@ impl HttpResponseBuilder {
self
}
/// Set a header.
/// Append a header.
///
/// ```rust
/// # extern crate actix_web;
@@ -394,7 +400,7 @@ impl HttpResponseBuilder {
self
}
/// Set a header.
/// Append a header.
///
/// ```rust
/// # extern crate actix_web;
@@ -426,6 +432,65 @@ impl HttpResponseBuilder {
}
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<K, V>(&mut self, key: K, value: V) -> &mut Self
where
HeaderName: HttpTryFrom<K>,
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<K>(&mut self, key: K) -> &mut Self
where HeaderName: HttpTryFrom<K>
{
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]
@@ -1118,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()
@@ -1128,6 +1201,40 @@ mod tests {
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();

View File

@@ -100,7 +100,6 @@ extern crate failure;
extern crate lazy_static;
#[macro_use]
extern crate futures;
extern crate askama_escape;
extern crate cookie;
extern crate futures_cpupool;
extern crate http as modhttp;
@@ -137,6 +136,7 @@ extern crate serde_urlencoded;
extern crate percent_encoding;
extern crate serde_json;
extern crate smallvec;
extern crate v_htmlescape;
extern crate actix_net;
#[macro_use]

View File

@@ -307,6 +307,32 @@ impl Cors {
}
}
fn access_control_allow_origin(&self, req: &Request) -> Option<HeaderValue> {
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<S> Middleware<S> 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<S> Middleware<S> for Cors {
fn response(
&self, req: &HttpRequest<S>, mut resp: HttpResponse,
) -> Result<Response> {
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()
);
}
}

View File

@@ -107,6 +107,12 @@ impl<S: 'static> Resource<S> {
self.routes.last_mut().unwrap().filter(pred::Post())
}
/// Register a new `PATCH` route.
pub fn patch(&mut self) -> &mut Route<S> {
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<S> {
self.routes.push(Route::default());

View File

@@ -234,6 +234,16 @@ impl<H: 'static> Writer for H2Writer<H> {
stream.reserve_capacity(cmp::min(self.buffer.len(), CHUNK_SIZE));
}
if self.flags.contains(Flags::EOF)
&& !self.flags.contains(Flags::RESERVED)
&& self.buffer.is_empty()
{
if let Err(e) = stream.send_data(Bytes::new(), true) {
return Err(io::Error::new(io::ErrorKind::Other, e));
}
return Ok(Async::Ready(()));
}
loop {
match stream.poll_capacity() {
Ok(Async::NotReady) => return Ok(Async::NotReady),

View File

@@ -451,9 +451,8 @@ impl<H: IntoHttpHandler, F: Fn() -> H + Send + Clone> HttpServer<H, F> {
/// 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.
/// This methods panics if no socket address can be bound or an `Actix` system is not yet
/// configured.
///
/// ```rust
/// extern crate actix_web;

View File

@@ -303,6 +303,8 @@ pub trait IoStream: AsyncRead + AsyncWrite + 'static {
} else {
Ok(Async::NotReady)
}
} else if e.kind() == io::ErrorKind::ConnectionReset && read_some {
Ok(Async::Ready((read_some, true)))
} else {
Err(e)
};

View File

@@ -5,7 +5,9 @@ use std::sync::mpsc;
use std::{net, thread};
use actix::{Actor, Addr, System};
use actix::actors::signal;
use actix_net::server::Server;
use cookie::Cookie;
use futures::Future;
use http::header::HeaderName;
@@ -66,6 +68,7 @@ pub struct TestServer {
ssl: bool,
conn: Addr<ClientConnector>,
rt: Runtime,
backend: Addr<Server>,
}
impl TestServer {
@@ -112,24 +115,25 @@ impl TestServer {
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
let local_addr = tcp.local_addr().unwrap();
let _ = HttpServer::new(factory)
let srv = HttpServer::new(factory)
.disable_signals()
.listen(tcp)
.keep_alive(5)
.start();
tx.send((System::current(), local_addr, TestServer::get_conn()))
tx.send((System::current(), local_addr, TestServer::get_conn(), srv))
.unwrap();
sys.run();
});
let (system, addr, conn) = rx.recv().unwrap();
let (system, addr, conn, backend) = rx.recv().unwrap();
System::set_current(system);
TestServer {
addr,
conn,
ssl: false,
rt: Runtime::new().unwrap(),
backend,
}
}
@@ -197,6 +201,7 @@ impl TestServer {
/// Stop http server
fn stop(&mut self) {
let _ = self.backend.send(signal::Signal(signal::SignalType::Term)).wait();
System::current().stop();
}
@@ -234,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())
@@ -333,8 +343,7 @@ where
.keep_alive(5)
.disable_signals();
tx.send((System::current(), addr, TestServer::get_conn()))
.unwrap();
#[cfg(any(feature = "alpn", feature = "ssl"))]
{
@@ -356,18 +365,22 @@ where
let tcp = net::TcpListener::bind(addr).unwrap();
srv = srv.listen(tcp);
}
srv.start();
let backend = srv.start();
tx.send((System::current(), addr, TestServer::get_conn(), backend))
.unwrap();
sys.run();
});
let (system, addr, conn) = rx.recv().unwrap();
let (system, addr, conn, backend) = rx.recv().unwrap();
System::set_current(system);
TestServer {
addr,
conn,
ssl: has_ssl,
rt: Runtime::new().unwrap(),
backend,
}
}
}
@@ -507,6 +520,11 @@ 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<S: 'static> TestRequest<S> {
@@ -543,6 +561,25 @@ impl<S: 'static> TestRequest<S> {
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<H: Header>(mut self, hdr: H) -> Self {
if let Ok(value) = hdr.try_into() {

1
tests/test space.binary Normal file
View File

@@ -0,0 +1 @@
<EFBFBD>TǑɂV<EFBFBD>2<EFBFBD>vI<EFBFBD><EFBFBD><EFBFBD>\<5C><52><CB99><EFBFBD>e<EFBFBD><04>vD<76>:藽<>RV<03>Yp<59><70>;<3B><>G<><47>p!2<7F>C<EFBFBD>.<2E> <0C><><EFBFBD><EFBFBD>pA !<21>ߦ<EFBFBD>x j+Uc<55><63><EFBFBD>X<13>c%<17>;<3B>"y<10><>AI

View File

@@ -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 <base64 encoded username:password>
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 <token>
let request = srv
.get()
.bearer_auth("someS3cr3tAutht0k3n")
.finish()
.unwrap();
let repr = format!("{:?}", request);
assert!(repr.contains("Bearer someS3cr3tAutht0k3n"));
}

View File

@@ -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());
}