mirror of
https://github.com/fafhrd91/actix-web
synced 2025-08-21 21:25:36 +02:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cc6e0c6d04 | ||
|
d9496d46d1 | ||
|
bf8262196f | ||
|
17ecdd63d2 | ||
|
cc7f6b5eef | ||
|
ceca96da28 | ||
|
42f030d3f4 | ||
|
6d11ee683f | ||
|
80d4cbe301 | ||
|
69d710dbce | ||
|
0059a55dfb | ||
|
c695358bcb | ||
|
b018e4abaf | ||
|
346d85a884 | ||
|
9968afe4a6 | ||
|
f5bec968c7 | ||
|
a534fdd125 | ||
|
3431fff4d7 | ||
|
d6df2e3399 | ||
|
1fbb52ad3b | ||
|
e5cdd22720 | ||
|
4f2e970732 | ||
|
4d45313f9d | ||
|
55a2a59906 | ||
|
61883042c2 |
39
CHANGES.md
39
CHANGES.md
@@ -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
|
||||
|
@@ -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"
|
||||
|
@@ -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();
|
||||
|
@@ -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()),
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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"),
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -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
255
src/fs.rs
@@ -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]
|
||||
|
@@ -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();
|
||||
|
@@ -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]
|
||||
|
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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());
|
||||
|
@@ -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),
|
||||
|
@@ -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;
|
||||
|
@@ -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)
|
||||
};
|
||||
|
51
src/test.rs
51
src/test.rs
@@ -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
1
tests/test space.binary
Normal file
@@ -0,0 +1 @@
|
||||
<EFBFBD>TǑɂV<EFBFBD>2<EFBFBD>vI<EFBFBD><EFBFBD><EFBFBD>\<5C>R˙<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
|
@@ -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"));
|
||||
}
|
@@ -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());
|
||||
}
|
Reference in New Issue
Block a user