1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-05 02:16:33 +02:00

Compare commits

...

33 Commits

Author SHA1 Message Date
0212c618c6 prepare actix-web release 2019-11-14 08:55:37 +06:00
88110ed268 Add security note to ConnectionInfo::remote() (#1158) 2019-11-14 08:32:47 +06:00
fba02fdd8c prep awc release 2019-11-06 11:33:25 -08:00
b2934ad8d2 prep actix-file release 2019-11-06 11:25:26 -08:00
f7f410d033 fix test order dep 2019-11-06 11:20:47 -08:00
885ff7396e prepare actox-http release 2019-11-06 10:35:13 -08:00
61b38e8d0d Increase timeouts in test-server (#1153) 2019-11-06 06:09:22 -08:00
edcde67076 Fix escaping/encoding problems in Content-Disposition header (#1151)
* Fix filename encoding in Content-Disposition of acitx_files::NamedFile

* Add more comments on how to use Content-Disposition header properly & Fix some trivial problems

* Improve Content-Disposition filename(*) parameters of actix_files::NamedFile

* Tweak Content-Disposition parse to accept empty param value in quoted-string

* Fix typos in comments in .../content_disposition.rs (pointed out by @JohnTitor)

* Update CHANGES.md

* Update CHANGES.md again
2019-11-06 06:08:37 -08:00
f0612f7570 awc: Add support for setting query from Serialize type for client request (#1130)
Signed-off-by: Jonathas-Conceicao <jadoliveira@inf.ufpel.edu.br>
2019-10-26 08:27:14 +03:00
ace98e3a1e support Host guards when Host header is unset (#1129) 2019-10-15 05:05:54 +06:00
1ca9d87f0a prep actix-web-codegen release 2019-10-14 21:35:53 +06:00
967f965405 Update syn & quote to 1.0 (#1133)
* chore(actix-web-codegen): Upgrade syn and quote to 1.0

* feat(actix-web-codegen): Generate better error message

* doc(actix-web-codegen): Update CHANGES.md

* fix: Build with stable rust
2019-10-14 21:34:17 +06:00
062e51e8ce prep actix-file release 2019-10-14 21:26:26 +06:00
a48e616def feat(files): add possibility to redirect to slash-ended path (#1134)
When accessing to a folder without a final slash, the index file will be loaded ok, but if it has
references (like a css or an image in an html file) these resources won't be loaded correctly if
they are using relative paths. In order to solve this, this PR adds the possibility to detect
folders without a final slash and make a 302 redirect to mitigate this issue. The behavior is off by
default. We're adding a new method called `redirect_to_slash_directory` which can be used to enable
this behavior.
2019-10-14 21:23:15 +06:00
effa96f5e4 Removed httpcode 'MovedPermanenty'. (#1128) 2019-10-12 06:45:12 +06:00
cc0b4be5b7 Fix typo in response.rs body() comment (#1126)
Fixes https://github.com/actix/actix-web/issues/1125
2019-10-09 19:11:55 +06:00
a464ffc23d prepare actix-files release 2019-10-08 10:13:16 +06:00
4de2e8a898 [actix-files] Allow user defined guards for NamedFile (actix#1113) (#1115)
* [actix-files] remove request method checks from NamedFile

* [actix-files] added custom guard checks to FilesService

* [actix-files] modify method check tests (NamedFile -> Files)

* [actix-files] add test for custom guards in Files

* [actix-files] update changelog
2019-10-08 10:09:40 +06:00
0f09415469 Convert documentation examples to Rust 2018 edition (#1120)
* Convert types::query examples to rust-2018 edition

* Convert types::json examples to rust-2018 edition

* Convert types::path examples to rust-2018 edition

* Convert types::form examples to rust-2018 edition

* Convert rest of the examples to rust-2018 edition.
2019-10-07 11:29:11 +06:00
f089cf185b Let ResponseError render w/ 'text/plain; charset=utf-8' header (#1118) (#1119)
* Let ResponseError render w/ 'text/plain; charset=utf-8' header (#1118)

Trait ResponseError originally render Error messages with header
`text/plain` , which causes browsers (i.e. Firefox 70.0) with
Non-English locale unable to render UTF-8 responses with non-English
characters correctly. i.e. emoji.

This fix solved this problem by specifying the charset of `text/plain`
as utf-8, which is the default charset in rust.

Before actix-web consider to support other charsets, this hotfix is
 enough.

Test case:

fn test() -> Result<String, actix_web::Error> {
    Err(actix_web::error::ErrorForbidden("😋test"))
}

* Update actix-http/CHANGES.md for #1118
2019-10-07 10:56:24 +06:00
15d3c1ae81 Update docs of guard.rs (#1116)
* Update guard.rs
2019-10-07 12:05:17 +09:00
fba31d4e0a Expose ContentDisposition in actix-multipart to fix broken doc link (#1114)
* Expose ContentDisposition in actix-multipart to fix broken doc link

* Revert "Expose ContentDisposition in actix-multipart to fix broken doc link"

This reverts commit e90d71d16c.

* Unhide actix-http::header::common docs

These types are used in other exported documented interfaces and create
broken links if not documented.
See `actix_multipart::Field.content_disposition`
2019-10-02 09:48:25 +06:00
f81ae37677 Add From<Payload> for crate::dev::Payload (#1110)
* Add From<Payload> for crate::dev::Payload

* Make dev::Payload field of Payload public and add into_inner method

* Add changelog entry
2019-10-01 14:05:38 +06:00
5169d306ae update ConnectionInfo.remote() doc string 2019-09-27 07:03:12 +06:00
4f3e97fff8 prepare actix-web release 2019-09-25 15:39:09 +06:00
3ff01a9fc4 Add changelog entry for #1101 (#1102) 2019-09-25 15:35:28 +06:00
3d4e45a0e5 prepare release 2019-09-25 15:30:20 +06:00
c659c33919 Feature uds: Add listen_uds to ServerBuilder (#1085)
Allows using an existing Unix Listener instead of binding to a path.
Useful for when running as a daemon under systemd.

Change-Id: I54a0e78c321d8b7a9ded381083217af590e9a7fa
2019-09-25 15:16:51 +06:00
959f7754b2 Merge pull request #1101 from actix/add-awc-get-head-methods
Add remaining getter methods from private head field
2019-09-25 10:23:23 +02:00
23f04c4f38 Add remaining getter methods from private head field 2019-09-25 08:50:45 +02:00
d9af8f66ba Use actix-testing for testing utils 2019-09-25 10:28:41 +06:00
aa39b8ca6f Add support for serde_json::Value to be passed as argument to ResponseBuilder.body() (#1096)
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()

* Update actix-http/CHANGES.md
2019-09-25 09:33:52 +06:00
58c7065f08 Implement register_data method on Resource and Scope. (#1094)
* Implement `register_data` method on `Resource` and `Scope`.

* Split Scope::register_data tests out from Scope::data tests.

* CHANGES.md: Mention {Scope,Resource}::register_data.
2019-09-18 06:36:39 +06:00
39 changed files with 781 additions and 325 deletions

View File

@ -1,15 +1,37 @@
# Changes # Changes
## [1.0.8] - 2019-09-xx ## [1.0.9] - 2019-11-14
### Added ### Added
* Add `middleware::Conditon` that conditionally enables another middleware * Add `Payload::into_inner` method and make stored `def::Payload` public. (#1110)
### Changed
* Support `Host` guards when the `Host` header is unset (e.g. HTTP/2 requests) (#1129)
## [1.0.8] - 2019-09-25
### Added
* Add `Scope::register_data` and `Resource::register_data` methods, parallel to
`App::register_data`.
* Add `middleware::Condition` that conditionally enables another middleware
* Allow to re-construct `ServiceRequest` from `HttpRequest` and `Payload` * Allow to re-construct `ServiceRequest` from `HttpRequest` and `Payload`
* Add `HttpServer::listen_uds` for ability to listen on UDS FD rather than path,
which is useful for example with systemd.
### Changed
* Make UrlEncodedError::Overflow more informativve * Make UrlEncodedError::Overflow more informativve
* Use actix-testing for testing utils
## [1.0.7] - 2019-08-29 ## [1.0.7] - 2019-08-29
### Fixed ### Fixed

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "1.0.7" version = "1.0.9"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
readme = "README.md" readme = "README.md"
@ -78,17 +78,18 @@ actix-utils = "0.4.4"
actix-router = "0.1.5" actix-router = "0.1.5"
actix-rt = "0.2.4" actix-rt = "0.2.4"
actix-web-codegen = "0.1.2" actix-web-codegen = "0.1.2"
actix-http = "0.2.9" actix-http = "0.2.11"
actix-server = "0.6.0" actix-server = "0.6.1"
actix-server-config = "0.1.2" actix-server-config = "0.1.2"
actix-testing = "0.1.0"
actix-threadpool = "0.1.1" actix-threadpool = "0.1.1"
awc = { version = "0.2.4", optional = true } awc = { version = "0.2.7", optional = true }
bytes = "0.4" bytes = "0.4"
derive_more = "0.15.0" derive_more = "0.15.0"
encoding_rs = "0.8" encoding_rs = "0.8"
futures = "0.1.25" futures = "0.1.25"
hashbrown = "0.5.0" hashbrown = "0.6.3"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
net2 = "0.2.33" net2 = "0.2.33"

View File

@ -1,21 +1,29 @@
# Changes # Changes
## [0.1.5] - unreleased ## [0.1.7] - 2019-11-06
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
## [0.1.6] - 2019-10-14
* Add option to redirect to a slash-ended path `Files` #1132
## [0.1.5] - 2019-10-08
* Bump up `mime_guess` crate version to 2.0.1 * Bump up `mime_guess` crate version to 2.0.1
* Bump up `percent-encoding` crate version to 2.1 * Bump up `percent-encoding` crate version to 2.1
* Allow user defined request guards for `Files` #1113
## [0.1.4] - 2019-07-20 ## [0.1.4] - 2019-07-20
* Allow to disable `Content-Disposition` header #686 * Allow to disable `Content-Disposition` header #686
## [0.1.3] - 2019-06-28 ## [0.1.3] - 2019-06-28
* Do not set `Content-Length` header, let actix-http set it #930 * Do not set `Content-Length` header, let actix-http set it #930
## [0.1.2] - 2019-06-13 ## [0.1.2] - 2019-06-13
* Content-Length is 0 for NamedFile HEAD request #914 * Content-Length is 0 for NamedFile HEAD request #914

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.1.4" version = "0.1.7"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Static files support for actix web." description = "Static files support for actix web."
readme = "README.md" readme = "README.md"
@ -18,8 +18,8 @@ name = "actix_files"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = { version = "1.0.2", default-features = false } actix-web = { version = "1.0.8", default-features = false }
actix-http = "0.2.9" actix-http = "0.2.11"
actix-service = "0.4.1" actix-service = "0.4.1"
bitflags = "1" bitflags = "1"
bytes = "0.4" bytes = "0.4"
@ -32,4 +32,4 @@ percent-encoding = "2.1"
v_htmlescape = "0.4" v_htmlescape = "0.4"
[dev-dependencies] [dev-dependencies]
actix-web = { version = "1.0.2", features=["ssl"] } actix-web = { version = "1.0.8", features=["ssl"] }

View File

@ -16,7 +16,9 @@ use actix_web::dev::{
ServiceResponse, ServiceResponse,
}; };
use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; use actix_web::error::{BlockingError, Error, ErrorInternalServerError};
use actix_web::http::header::DispositionType; use actix_web::guard::Guard;
use actix_web::http::header::{self, DispositionType};
use actix_web::http::Method;
use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder}; use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder};
use bytes::Bytes; use bytes::Bytes;
use futures::future::{ok, Either, FutureResult}; use futures::future::{ok, Either, FutureResult};
@ -231,10 +233,12 @@ pub struct Files {
directory: PathBuf, directory: PathBuf,
index: Option<String>, index: Option<String>,
show_index: bool, show_index: bool,
redirect_to_slash: bool,
default: Rc<RefCell<Option<Rc<HttpNewService>>>>, default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
renderer: Rc<DirectoryRenderer>, renderer: Rc<DirectoryRenderer>,
mime_override: Option<Rc<MimeOverride>>, mime_override: Option<Rc<MimeOverride>>,
file_flags: named::Flags, file_flags: named::Flags,
guards: Option<Rc<Box<dyn Guard>>>,
} }
impl Clone for Files { impl Clone for Files {
@ -243,11 +247,13 @@ impl Clone for Files {
directory: self.directory.clone(), directory: self.directory.clone(),
index: self.index.clone(), index: self.index.clone(),
show_index: self.show_index, show_index: self.show_index,
redirect_to_slash: self.redirect_to_slash,
default: self.default.clone(), default: self.default.clone(),
renderer: self.renderer.clone(), renderer: self.renderer.clone(),
file_flags: self.file_flags, file_flags: self.file_flags,
path: self.path.clone(), path: self.path.clone(),
mime_override: self.mime_override.clone(), mime_override: self.mime_override.clone(),
guards: self.guards.clone(),
} }
} }
} }
@ -269,10 +275,12 @@ impl Files {
directory: dir, directory: dir,
index: None, index: None,
show_index: false, show_index: false,
redirect_to_slash: false,
default: Rc::new(RefCell::new(None)), default: Rc::new(RefCell::new(None)),
renderer: Rc::new(directory_listing), renderer: Rc::new(directory_listing),
mime_override: None, mime_override: None,
file_flags: named::Flags::default(), file_flags: named::Flags::default(),
guards: None,
} }
} }
@ -284,6 +292,14 @@ impl Files {
self self
} }
/// Redirects to a slash-ended path when browsing a directory.
///
/// By default never redirect.
pub fn redirect_to_slash_directory(mut self) -> Self {
self.redirect_to_slash = true;
self
}
/// Set custom directory renderer /// Set custom directory renderer
pub fn files_listing_renderer<F>(mut self, f: F) -> Self pub fn files_listing_renderer<F>(mut self, f: F) -> Self
where where
@ -331,6 +347,15 @@ impl Files {
self self
} }
/// Specifies custom guards to use for directory listings and files.
///
/// Default behaviour allows GET and HEAD.
#[inline]
pub fn use_guards<G: Guard + 'static>(mut self, guards: G) -> Self {
self.guards = Some(Rc::new(Box::new(guards)));
self
}
/// Disable `Content-Disposition` header. /// Disable `Content-Disposition` header.
/// ///
/// By default Content-Disposition` header is enabled. /// By default Content-Disposition` header is enabled.
@ -375,10 +400,10 @@ impl HttpServiceFactory for Files {
} }
impl NewService for Files { impl NewService for Files {
type Config = ();
type Request = ServiceRequest; type Request = ServiceRequest;
type Response = ServiceResponse; type Response = ServiceResponse;
type Error = Error; type Error = Error;
type Config = ();
type Service = FilesService; type Service = FilesService;
type InitError = (); type InitError = ();
type Future = Box<dyn Future<Item = Self::Service, Error = Self::InitError>>; type Future = Box<dyn Future<Item = Self::Service, Error = Self::InitError>>;
@ -388,10 +413,12 @@ impl NewService for Files {
directory: self.directory.clone(), directory: self.directory.clone(),
index: self.index.clone(), index: self.index.clone(),
show_index: self.show_index, show_index: self.show_index,
redirect_to_slash: self.redirect_to_slash,
default: None, default: None,
renderer: self.renderer.clone(), renderer: self.renderer.clone(),
mime_override: self.mime_override.clone(), mime_override: self.mime_override.clone(),
file_flags: self.file_flags, file_flags: self.file_flags,
guards: self.guards.clone(),
}; };
if let Some(ref default) = *self.default.borrow() { if let Some(ref default) = *self.default.borrow() {
@ -414,10 +441,12 @@ pub struct FilesService {
directory: PathBuf, directory: PathBuf,
index: Option<String>, index: Option<String>,
show_index: bool, show_index: bool,
redirect_to_slash: bool,
default: Option<HttpService>, default: Option<HttpService>,
renderer: Rc<DirectoryRenderer>, renderer: Rc<DirectoryRenderer>,
mime_override: Option<Rc<MimeOverride>>, mime_override: Option<Rc<MimeOverride>>,
file_flags: named::Flags, file_flags: named::Flags,
guards: Option<Rc<Box<dyn Guard>>>,
} }
impl FilesService { impl FilesService {
@ -454,6 +483,25 @@ impl Service for FilesService {
fn call(&mut self, req: ServiceRequest) -> Self::Future { fn call(&mut self, req: ServiceRequest) -> Self::Future {
// let (req, pl) = req.into_parts(); // let (req, pl) = req.into_parts();
let is_method_valid = if let Some(guard) = &self.guards {
// execute user defined guards
(**guard).check(req.head())
} else {
// default behaviour
match *req.method() {
Method::HEAD | Method::GET => true,
_ => false,
}
};
if !is_method_valid {
return Either::A(ok(req.into_response(
actix_web::HttpResponse::MethodNotAllowed()
.header(header::CONTENT_TYPE, "text/plain")
.body("Request did not meet this resource's requirements."),
)));
}
let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) { let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) {
Ok(item) => item, Ok(item) => item,
Err(e) => return Either::A(ok(req.error_response(e))), Err(e) => return Either::A(ok(req.error_response(e))),
@ -467,6 +515,16 @@ impl Service for FilesService {
if path.is_dir() { if path.is_dir() {
if let Some(ref redir_index) = self.index { if let Some(ref redir_index) = self.index {
if self.redirect_to_slash && !req.path().ends_with('/') {
let redirect_to = format!("{}/", req.path());
return Either::A(ok(req.into_response(
HttpResponse::Found()
.header(header::LOCATION, redirect_to)
.body("")
.into_body(),
)));
}
let path = path.join(redir_index); let path = path.join(redir_index);
match NamedFile::open(path) { match NamedFile::open(path) {
@ -576,6 +634,7 @@ mod tests {
use bytes::BytesMut; use bytes::BytesMut;
use super::*; use super::*;
use actix_web::guard;
use actix_web::http::header::{ use actix_web::http::header::{
self, ContentDisposition, DispositionParam, DispositionType, self, ContentDisposition, DispositionParam, DispositionType,
}; };
@ -674,6 +733,31 @@ mod tests {
assert!(resp.headers().get(header::CONTENT_DISPOSITION).is_none()); assert!(resp.headers().get(header::CONTENT_DISPOSITION).is_none());
} }
#[test]
fn test_named_file_non_ascii_file_name() {
let mut file =
NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml")
.unwrap();
{
file.file();
let _f: &File = &file;
}
{
let _f: &mut File = &mut file;
}
let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).unwrap();
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/x-toml"
);
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"貨物.toml\"; filename*=UTF-8''%E8%B2%A8%E7%89%A9.toml"
);
}
#[test] #[test]
fn test_named_file_set_content_type() { fn test_named_file_set_content_type() {
let mut file = NamedFile::open("Cargo.toml") let mut file = NamedFile::open("Cargo.toml")
@ -1010,20 +1094,41 @@ mod tests {
} }
#[test] #[test]
fn test_named_file_not_allowed() { fn test_files_not_allowed() {
let file = NamedFile::open("Cargo.toml").unwrap(); let mut srv = test::init_service(App::new().service(Files::new("/", ".")));
let req = TestRequest::default() let req = TestRequest::default()
.uri("/Cargo.toml")
.method(Method::POST) .method(Method::POST)
.to_http_request(); .to_request();
let resp = file.respond_to(&req).unwrap();
let resp = test::call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let file = NamedFile::open("Cargo.toml").unwrap(); let mut srv = test::init_service(App::new().service(Files::new("/", ".")));
let req = TestRequest::default().method(Method::PUT).to_http_request(); let req = TestRequest::default()
let resp = file.respond_to(&req).unwrap(); .method(Method::PUT)
.uri("/Cargo.toml")
.to_request();
let resp = test::call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
} }
#[test]
fn test_files_guards() {
let mut srv = test::init_service(
App::new().service(Files::new("/", ".").use_guards(guard::Post())),
);
let req = TestRequest::default()
.uri("/Cargo.toml")
.method(Method::POST)
.to_request();
let resp = test::call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::OK);
}
#[test] #[test]
fn test_named_file_content_encoding() { fn test_named_file_content_encoding() {
let mut srv = test::init_service(App::new().wrap(Compress::default()).service( let mut srv = test::init_service(App::new().wrap(Compress::default()).service(
@ -1112,6 +1217,34 @@ mod tests {
assert!(format!("{:?}", bytes).contains("/tests/test.png")); assert!(format!("{:?}", bytes).contains("/tests/test.png"));
} }
#[test]
fn test_redirect_to_slash_directory() {
// should not redirect if no index
let mut srv = test::init_service(
App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
);
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
// should redirect if index present
let mut srv = test::init_service(
App::new().service(
Files::new("/", ".")
.index_file("test.png")
.redirect_to_slash_directory(),
),
);
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::FOUND);
// should not redirect if the path is wrong
let req = TestRequest::with_uri("/not_existing").to_request();
let resp = test::call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[test] #[test]
fn test_static_files_bad_directory() { fn test_static_files_bad_directory() {
let _st: Files = Files::new("/", "missing"); let _st: Files = Files::new("/", "missing");

View File

@ -13,9 +13,9 @@ use mime_guess::from_path;
use actix_http::body::SizedStream; use actix_http::body::SizedStream;
use actix_web::http::header::{ use actix_web::http::header::{
self, ContentDisposition, DispositionParam, DispositionType, self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
}; };
use actix_web::http::{ContentEncoding, Method, StatusCode}; use actix_web::http::{ContentEncoding, StatusCode};
use actix_web::middleware::BodyEncoding; use actix_web::middleware::BodyEncoding;
use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder}; use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder};
@ -93,9 +93,18 @@ impl NamedFile {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
_ => DispositionType::Attachment, _ => DispositionType::Attachment,
}; };
let mut parameters =
vec![DispositionParam::Filename(String::from(filename.as_ref()))];
if !filename.is_ascii() {
parameters.push(DispositionParam::FilenameExt(ExtendedValue {
charset: Charset::Ext(String::from("UTF-8")),
language_tag: None,
value: filename.into_owned().into_bytes(),
}))
}
let cd = ContentDisposition { let cd = ContentDisposition {
disposition: disposition_type, disposition: disposition_type,
parameters: vec![DispositionParam::Filename(filename.into_owned())], parameters: parameters,
}; };
(ct, cd) (ct, cd)
}; };
@ -324,16 +333,6 @@ impl Responder for NamedFile {
return Ok(resp.streaming(reader)); return Ok(resp.streaming(reader));
} }
match *req.method() {
Method::HEAD | Method::GET => (),
_ => {
return Ok(HttpResponse::MethodNotAllowed()
.header(header::CONTENT_TYPE, "text/plain")
.header(header::ALLOW, "GET, HEAD")
.body("This resource only supports GET and HEAD."));
}
}
let etag = if self.flags.contains(Flags::ETAG) { let etag = if self.flags.contains(Flags::ETAG) {
self.etag() self.etag()
} else { } else {

View File

@ -1,14 +1,26 @@
# Changes # Changes
## [0.2.11] - 2019-09-11 ## [0.2.11] - 2019-11-06
### Added
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
* Allow to use `std::convert::Infallible` as `actix_http::error::Error`
### Fixed
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; charset=utf-8` header #1118
## [0.2.10] - 2019-09-11
### Added ### Added
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests with `RequestHead` * Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests with `RequestHead`
* Allow to use `std::convert::Infallible` as `actix_http::error::Error`
### Fixed ### Fixed
* h2 will use error response #1080 * h2 will use error response #1080

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "0.2.10" version = "0.2.11"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix http primitives" description = "Actix http primitives"
readme = "README.md" readme = "README.md"
@ -62,7 +62,7 @@ derive_more = "0.15.0"
either = "1.5.2" either = "1.5.2"
encoding_rs = "0.8" encoding_rs = "0.8"
futures = "0.1.25" futures = "0.1.25"
hashbrown = "0.5.0" hashbrown = "0.6.3"
h2 = "0.1.16" h2 = "0.1.16"
http = "0.1.17" http = "0.1.17"
httparse = "1.3" httparse = "1.3"

View File

@ -234,6 +234,12 @@ impl From<BytesMut> for Body {
} }
} }
impl From<serde_json::Value> for Body {
fn from(v: serde_json::Value) -> Body {
Body::Bytes(v.to_string().into())
}
}
impl<S> From<SizedStream<S>> for Body impl<S> From<SizedStream<S>> for Body
where where
S: Stream<Item = Bytes, Error = Error> + 'static, S: Stream<Item = Bytes, Error = Error> + 'static,
@ -548,4 +554,17 @@ mod tests {
assert!(format!("{:?}", Body::Empty).contains("Body::Empty")); assert!(format!("{:?}", Body::Empty).contains("Body::Empty"));
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains("1")); assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains("1"));
} }
#[test]
fn test_serde_json() {
use serde_json::json;
assert_eq!(
Body::from(serde_json::Value::String("test".into())).size(),
BodySize::Sized(6)
);
assert_eq!(
Body::from(json!({"test-key":"test-value"})).size(),
BodySize::Sized(25)
);
}
} }

View File

@ -212,7 +212,7 @@ where
pub fn finish( pub fn finish(
self, self,
) -> impl Service<Request = Connect, Response = impl Connection, Error = ConnectError> ) -> impl Service<Request = Connect, Response = impl Connection, Error = ConnectError>
+ Clone { + Clone {
#[cfg(not(any(feature = "ssl", feature = "rust-tls")))] #[cfg(not(any(feature = "ssl", feature = "rust-tls")))]
{ {
let connector = TimeoutService::new( let connector = TimeoutService::new(

View File

@ -75,7 +75,7 @@ pub trait ResponseError: fmt::Debug + fmt::Display {
let _ = write!(Writer(&mut buf), "{}", self); let _ = write!(Writer(&mut buf), "{}", self);
resp.headers_mut().insert( resp.headers_mut().insert(
header::CONTENT_TYPE, header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain"), header::HeaderValue::from_static("text/plain; charset=utf-8"),
); );
resp.set_body(Body::from(buf)) resp.set_body(Body::from(buf))
} }
@ -536,7 +536,7 @@ where
let _ = write!(Writer(&mut buf), "{}", self); let _ = write!(Writer(&mut buf), "{}", self);
res.headers_mut().insert( res.headers_mut().insert(
header::CONTENT_TYPE, header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain"), header::HeaderValue::from_static("text/plain; charset=utf-8"),
); );
res.set_body(Body::from(buf)) res.set_body(Body::from(buf))
} }

View File

@ -548,10 +548,11 @@ mod tests {
ConnectionType::Close, ConnectionType::Close,
&ServiceConfig::default(), &ServiceConfig::default(),
); );
assert_eq!( let data = String::from_utf8(Vec::from(bytes.take().freeze().as_ref())).unwrap();
bytes.take().freeze(), assert!(data.contains("Content-Length: 0\r\n"));
Bytes::from_static(b"\r\nContent-Length: 0\r\nConnection: close\r\nDate: date\r\nContent-Type: plain/text\r\n\r\n") assert!(data.contains("Connection: close\r\n"));
); assert!(data.contains("Content-Type: plain/text\r\n"));
assert!(data.contains("Date: date\r\n"));
let _ = head.encode_headers( let _ = head.encode_headers(
&mut bytes, &mut bytes,
@ -560,10 +561,10 @@ mod tests {
ConnectionType::KeepAlive, ConnectionType::KeepAlive,
&ServiceConfig::default(), &ServiceConfig::default(),
); );
assert_eq!( let data = String::from_utf8(Vec::from(bytes.take().freeze().as_ref())).unwrap();
bytes.take().freeze(), assert!(data.contains("Transfer-Encoding: chunked\r\n"));
Bytes::from_static(b"\r\nTransfer-Encoding: chunked\r\nDate: date\r\nContent-Type: plain/text\r\n\r\n") assert!(data.contains("Content-Type: plain/text\r\n"));
); assert!(data.contains("Date: date\r\n"));
let _ = head.encode_headers( let _ = head.encode_headers(
&mut bytes, &mut bytes,
@ -572,10 +573,10 @@ mod tests {
ConnectionType::KeepAlive, ConnectionType::KeepAlive,
&ServiceConfig::default(), &ServiceConfig::default(),
); );
assert_eq!( let data = String::from_utf8(Vec::from(bytes.take().freeze().as_ref())).unwrap();
bytes.take().freeze(), assert!(data.contains("Content-Length: 100\r\n"));
Bytes::from_static(b"\r\nContent-Length: 100\r\nDate: date\r\nContent-Type: plain/text\r\n\r\n") assert!(data.contains("Content-Type: plain/text\r\n"));
); assert!(data.contains("Date: date\r\n"));
let mut head = RequestHead::default(); let mut head = RequestHead::default();
head.set_camel_case_headers(false); head.set_camel_case_headers(false);
@ -586,7 +587,6 @@ mod tests {
.append(CONTENT_TYPE, HeaderValue::from_static("xml")); .append(CONTENT_TYPE, HeaderValue::from_static("xml"));
let mut head = RequestHeadType::Owned(head); let mut head = RequestHeadType::Owned(head);
let _ = head.encode_headers( let _ = head.encode_headers(
&mut bytes, &mut bytes,
Version::HTTP_11, Version::HTTP_11,
@ -594,10 +594,11 @@ mod tests {
ConnectionType::KeepAlive, ConnectionType::KeepAlive,
&ServiceConfig::default(), &ServiceConfig::default(),
); );
assert_eq!( let data = String::from_utf8(Vec::from(bytes.take().freeze().as_ref())).unwrap();
bytes.take().freeze(), assert!(data.contains("transfer-encoding: chunked\r\n"));
Bytes::from_static(b"\r\ntransfer-encoding: chunked\r\ndate: date\r\ncontent-type: xml\r\ncontent-type: plain/text\r\n\r\n") assert!(data.contains("content-type: xml\r\n"));
); assert!(data.contains("content-type: plain/text\r\n"));
assert!(data.contains("date: date\r\n"));
} }
#[test] #[test]
@ -626,9 +627,10 @@ mod tests {
ConnectionType::Close, ConnectionType::Close,
&ServiceConfig::default(), &ServiceConfig::default(),
); );
assert_eq!( let data = String::from_utf8(Vec::from(bytes.take().freeze().as_ref())).unwrap();
bytes.take().freeze(), assert!(data.contains("content-length: 0\r\n"));
Bytes::from_static(b"\r\ncontent-length: 0\r\nconnection: close\r\nauthorization: another authorization\r\ndate: date\r\n\r\n") assert!(data.contains("connection: close\r\n"));
); assert!(data.contains("authorization: another authorization\r\n"));
assert!(data.contains("date: date\r\n"));
} }
} }

View File

@ -76,6 +76,11 @@ pub enum DispositionParam {
/// the form. /// the form.
Name(String), Name(String),
/// A plain file name. /// A plain file name.
///
/// It is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any
/// non-ASCII characters when used in a *Content-Disposition* HTTP response header, where
/// [`FilenameExt`](DispositionParam::FilenameExt) with charset UTF-8 may be used instead
/// in case there are Unicode characters in file names.
Filename(String), Filename(String),
/// An extended file name. It must not exist for `ContentType::Formdata` according to /// An extended file name. It must not exist for `ContentType::Formdata` according to
/// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2). /// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2).
@ -220,7 +225,16 @@ impl DispositionParam {
/// ext-token = <the characters in token, followed by "*"> /// ext-token = <the characters in token, followed by "*">
/// ``` /// ```
/// ///
/// **Note**: filename* [must not](https://tools.ietf.org/html/rfc7578#section-4.2) be used within /// # Note
///
/// filename is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any
/// non-ASCII characters when used in a *Content-Disposition* HTTP response header, where
/// filename* with charset UTF-8 may be used instead in case there are Unicode characters in file
/// names.
/// filename is [acceptable](https://tools.ietf.org/html/rfc7578#section-4.2) to be UTF-8 encoded
/// directly in a *Content-Disposition* header for *multipart/form-data*, though.
///
/// filename* [must not](https://tools.ietf.org/html/rfc7578#section-4.2) be used within
/// *multipart/form-data*. /// *multipart/form-data*.
/// ///
/// # Example /// # Example
@ -251,6 +265,22 @@ impl DispositionParam {
/// }; /// };
/// assert_eq!(cd2.get_name(), Some("file")); // field name /// assert_eq!(cd2.get_name(), Some("file")); // field name
/// assert_eq!(cd2.get_filename(), Some("bill.odt")); /// assert_eq!(cd2.get_filename(), Some("bill.odt"));
///
/// // HTTP response header with Unicode characters in file names
/// let cd3 = ContentDisposition {
/// disposition: DispositionType::Attachment,
/// parameters: vec![
/// DispositionParam::FilenameExt(ExtendedValue {
/// charset: Charset::Ext(String::from("UTF-8")),
/// language_tag: None,
/// value: String::from("\u{1f600}.svg").into_bytes(),
/// }),
/// // fallback for better compatibility
/// DispositionParam::Filename(String::from("Grinning-Face-Emoji.svg"))
/// ],
/// };
/// assert_eq!(cd3.get_filename_ext().map(|ev| ev.value.as_ref()),
/// Some("\u{1f600}.svg".as_bytes()));
/// ``` /// ```
/// ///
/// # WARN /// # WARN
@ -333,15 +363,17 @@ impl ContentDisposition {
// token: won't contains semicolon according to RFC 2616 Section 2.2 // token: won't contains semicolon according to RFC 2616 Section 2.2
let (token, new_left) = split_once_and_trim(left, ';'); let (token, new_left) = split_once_and_trim(left, ';');
left = new_left; left = new_left;
if token.is_empty() {
// quoted-string can be empty, but token cannot be empty
return Err(crate::error::ParseError::Header);
}
token.to_owned() token.to_owned()
}; };
if value.is_empty() {
return Err(crate::error::ParseError::Header);
}
let param = if param_name.eq_ignore_ascii_case("name") { let param = if param_name.eq_ignore_ascii_case("name") {
DispositionParam::Name(value) DispositionParam::Name(value)
} else if param_name.eq_ignore_ascii_case("filename") { } else if param_name.eq_ignore_ascii_case("filename") {
// See also comments in test_from_raw_uncessary_percent_decode.
DispositionParam::Filename(value) DispositionParam::Filename(value)
} else { } else {
DispositionParam::Unknown(param_name.to_owned(), value) DispositionParam::Unknown(param_name.to_owned(), value)
@ -466,11 +498,40 @@ impl fmt::Display for DispositionType {
impl fmt::Display for DispositionParam { impl fmt::Display for DispositionParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// All ASCII control charaters (0-30, 127) excepting horizontal tab, double quote, and // All ASCII control characters (0-30, 127) including horizontal tab, double quote, and
// backslash should be escaped in quoted-string (i.e. "foobar"). // backslash should be escaped in quoted-string (i.e. "foobar").
// Ref: RFC6266 S4.1 -> RFC2616 S2.2; RFC 7578 S4.2 -> RFC2183 S2 -> ... . // Ref: RFC6266 S4.1 -> RFC2616 S3.6
// filename-parm = "filename" "=" value
// value = token | quoted-string
// quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
// qdtext = <any TEXT except <">>
// quoted-pair = "\" CHAR
// TEXT = <any OCTET except CTLs,
// but including LWS>
// LWS = [CRLF] 1*( SP | HT )
// OCTET = <any 8-bit sequence of data>
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character
// (octets 0 - 31) and DEL (127)>
//
// Ref: RFC7578 S4.2 -> RFC2183 S2 -> RFC2045 S5.1
// parameter := attribute "=" value
// attribute := token
// ; Matching of attributes
// ; is ALWAYS case-insensitive.
// value := token / quoted-string
// token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
// or tspecials>
// tspecials := "(" / ")" / "<" / ">" / "@" /
// "," / ";" / ":" / "\" / <">
// "/" / "[" / "]" / "?" / "="
// ; Must be in quoted-string,
// ; to use within parameter values
//
//
// See also comments in test_from_raw_uncessary_percent_decode.
lazy_static! { lazy_static! {
static ref RE: Regex = Regex::new("[\x01-\x08\x10\x1F\x7F\"\\\\]").unwrap(); static ref RE: Regex = Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap();
} }
match self { match self {
DispositionParam::Name(ref value) => write!(f, "name={}", value), DispositionParam::Name(ref value) => write!(f, "name={}", value),
@ -774,8 +835,18 @@ mod tests {
#[test] #[test]
fn test_from_raw_uncessary_percent_decode() { fn test_from_raw_uncessary_percent_decode() {
// In fact, RFC7578 (multipart/form-data) Section 2 and 4.2 suggests that filename with
// non-ASCII characters MAY be percent-encoded.
// On the contrary, RFC6266 or other RFCs related to Content-Disposition response header
// do not mention such percent-encoding.
// So, it appears to be undecidable whether to percent-decode or not without
// knowing the usage scenario (multipart/form-data v.s. HTTP response header) and
// inevitable to unnecessarily percent-decode filename with %XX in the former scenario.
// Fortunately, it seems that almost all mainstream browsers just send UTF-8 encoded file
// names in quoted-string format (tested on Edge, IE11, Chrome and Firefox) without
// percent-encoding. So we do not bother to attempt to percent-decode.
let a = HeaderValue::from_static( let a = HeaderValue::from_static(
"form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"", // Should not be decoded! "form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"",
); );
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition { let b = ContentDisposition {
@ -811,6 +882,9 @@ mod tests {
let a = HeaderValue::from_static("inline; filename= "); let a = HeaderValue::from_static("inline; filename= ");
assert!(ContentDisposition::from_raw(&a).is_err()); assert!(ContentDisposition::from_raw(&a).is_err());
let a = HeaderValue::from_static("inline; filename=\"\"");
assert!(ContentDisposition::from_raw(&a).expect("parse cd").get_filename().expect("filename").is_empty());
} }
#[test] #[test]

View File

@ -16,7 +16,6 @@ use crate::httpmessage::HttpMessage;
mod common; mod common;
pub(crate) mod map; pub(crate) mod map;
mod shared; mod shared;
#[doc(hidden)]
pub use self::common::*; pub use self::common::*;
#[doc(hidden)] #[doc(hidden)]
pub use self::shared::*; pub use self::shared::*;

View File

@ -29,7 +29,6 @@ impl Response {
STATIC_RESP!(AlreadyReported, StatusCode::ALREADY_REPORTED); STATIC_RESP!(AlreadyReported, StatusCode::ALREADY_REPORTED);
STATIC_RESP!(MultipleChoices, StatusCode::MULTIPLE_CHOICES); STATIC_RESP!(MultipleChoices, StatusCode::MULTIPLE_CHOICES);
STATIC_RESP!(MovedPermanenty, StatusCode::MOVED_PERMANENTLY);
STATIC_RESP!(MovedPermanently, StatusCode::MOVED_PERMANENTLY); STATIC_RESP!(MovedPermanently, StatusCode::MOVED_PERMANENTLY);
STATIC_RESP!(Found, StatusCode::FOUND); STATIC_RESP!(Found, StatusCode::FOUND);
STATIC_RESP!(SeeOther, StatusCode::SEE_OTHER); STATIC_RESP!(SeeOther, StatusCode::SEE_OTHER);

View File

@ -194,7 +194,7 @@ impl<B> Response<B> {
self.head.extensions.borrow_mut() self.head.extensions.borrow_mut()
} }
/// Get body os this response /// Get body of this response
#[inline] #[inline]
pub fn body(&self) -> &ResponseBody<B> { pub fn body(&self) -> &ResponseBody<B> {
&self.body &self.body
@ -992,6 +992,14 @@ mod tests {
assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]"); assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]");
} }
#[test]
fn test_serde_json_in_body() {
use serde_json::json;
let resp =
Response::build(StatusCode::OK).body(json!({"test-key":"test-value"}));
assert_eq!(resp.body().get_ref(), br#"{"test-key":"test-value"}"#);
}
#[test] #[test]
fn test_into_response() { fn test_into_response() {
let resp: Response = "test".into(); let resp: Response = "test".into();

View File

@ -1,5 +1,11 @@
# Changes # Changes
## [0.1.3] - 2019-10-14
* Bump up `syn` & `quote` to 1.0
* Provide better error message
## [0.1.2] - 2019-06-04 ## [0.1.2] - 2019-06-04
* Add macros for head, options, trace, connect and patch http methods * Add macros for head, options, trace, connect and patch http methods

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-codegen" name = "actix-web-codegen"
version = "0.1.2" version = "0.1.3"
description = "Actix web proc macros" description = "Actix web proc macros"
readme = "README.md" readme = "README.md"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
@ -12,8 +12,9 @@ workspace = ".."
proc-macro = true proc-macro = true
[dependencies] [dependencies]
quote = "0.6.12" quote = "1"
syn = { version = "0.15.34", features = ["full", "parsing", "extra-traits"] } syn = { version = "1", features = ["full", "parsing"] }
proc-macro2 = "1"
[dev-dependencies] [dev-dependencies]
actix-web = { version = "1.0.0" } actix-web = { version = "1.0.0" }

View File

@ -58,7 +58,10 @@ use syn::parse_macro_input;
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { pub fn get(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Get); let gen = match route::Route::new(args, input, route::GuardType::Get) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -70,7 +73,10 @@ pub fn get(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { pub fn post(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Post); let gen = match route::Route::new(args, input, route::GuardType::Post) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -82,7 +88,10 @@ pub fn post(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { pub fn put(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Put); let gen = match route::Route::new(args, input, route::GuardType::Put) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -94,7 +103,10 @@ pub fn put(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream { pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Delete); let gen = match route::Route::new(args, input, route::GuardType::Delete) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -106,7 +118,10 @@ pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn head(args: TokenStream, input: TokenStream) -> TokenStream { pub fn head(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Head); let gen = match route::Route::new(args, input, route::GuardType::Head) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -118,7 +133,10 @@ pub fn head(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream { pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Connect); let gen = match route::Route::new(args, input, route::GuardType::Connect) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -130,7 +148,10 @@ pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn options(args: TokenStream, input: TokenStream) -> TokenStream { pub fn options(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Options); let gen = match route::Route::new(args, input, route::GuardType::Options) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -142,7 +163,10 @@ pub fn options(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream { pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Trace); let gen = match route::Route::new(args, input, route::GuardType::Trace) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -154,6 +178,9 @@ pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream { pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Patch); let gen = match route::Route::new(args, input, route::GuardType::Patch) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }

View File

@ -1,21 +1,23 @@
extern crate proc_macro; extern crate proc_macro;
use std::fmt;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{AttributeArgs, Ident, NestedMeta};
enum ResourceType { enum ResourceType {
Async, Async,
Sync, Sync,
} }
impl fmt::Display for ResourceType { impl ToTokens for ResourceType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn to_tokens(&self, stream: &mut TokenStream2) {
match *self { let ident = match self {
ResourceType::Async => write!(f, "to_async"), ResourceType::Async => "to_async",
ResourceType::Sync => write!(f, "to"), ResourceType::Sync => "to",
} };
let ident = Ident::new(ident, Span::call_site());
stream.append(ident);
} }
} }
@ -32,63 +34,89 @@ pub enum GuardType {
Patch, Patch,
} }
impl fmt::Display for GuardType { impl GuardType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn as_str(&self) -> &'static str {
match *self { match self {
GuardType::Get => write!(f, "Get"), GuardType::Get => "Get",
GuardType::Post => write!(f, "Post"), GuardType::Post => "Post",
GuardType::Put => write!(f, "Put"), GuardType::Put => "Put",
GuardType::Delete => write!(f, "Delete"), GuardType::Delete => "Delete",
GuardType::Head => write!(f, "Head"), GuardType::Head => "Head",
GuardType::Connect => write!(f, "Connect"), GuardType::Connect => "Connect",
GuardType::Options => write!(f, "Options"), GuardType::Options => "Options",
GuardType::Trace => write!(f, "Trace"), GuardType::Trace => "Trace",
GuardType::Patch => write!(f, "Patch"), GuardType::Patch => "Patch",
} }
} }
} }
pub struct Args { impl ToTokens for GuardType {
name: syn::Ident, fn to_tokens(&self, stream: &mut TokenStream2) {
path: String, let ident = self.as_str();
ast: syn::ItemFn, let ident = Ident::new(ident, Span::call_site());
resource_type: ResourceType, stream.append(ident);
pub guard: GuardType, }
pub extra_guards: Vec<String>,
} }
impl fmt::Display for Args { struct Args {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { path: syn::LitStr,
let ast = &self.ast; guards: Vec<Ident>,
let guards = format!(".guard(actix_web::guard::{}())", self.guard); }
let guards = self.extra_guards.iter().fold(guards, |acc, val| {
format!("{}.guard(actix_web::guard::fn_guard({}))", acc, val)
});
write!( impl Args {
f, fn new(args: AttributeArgs) -> syn::Result<Self> {
" let mut path = None;
#[allow(non_camel_case_types)] let mut guards = Vec::new();
pub struct {name}; for arg in args {
match arg {
impl actix_web::dev::HttpServiceFactory for {name} {{ NestedMeta::Lit(syn::Lit::Str(lit)) => match path {
fn register(self, config: &mut actix_web::dev::AppService) {{ None => {
{ast} path = Some(lit);
}
let resource = actix_web::Resource::new(\"{path}\"){guards}.{to}({name}); _ => {
return Err(syn::Error::new_spanned(
actix_web::dev::HttpServiceFactory::register(resource, config) lit,
}} "Multiple paths specified! Should be only one!",
}}", ));
name = self.name, }
ast = quote!(#ast), },
path = self.path, NestedMeta::Meta(syn::Meta::NameValue(nv)) => {
guards = guards, if nv.path.is_ident("guard") {
to = self.resource_type if let syn::Lit::Str(lit) = nv.lit {
) guards.push(Ident::new(&lit.value(), Span::call_site()));
} else {
return Err(syn::Error::new_spanned(
nv.lit,
"Attribute guard expects literal string!",
));
}
} else {
return Err(syn::Error::new_spanned(
nv.path,
"Unknown attribute key is specified. Allowed: guard",
));
}
}
arg => {
return Err(syn::Error::new_spanned(arg, "Unknown attribute"));
}
}
}
Ok(Args {
path: path.unwrap(),
guards,
})
} }
} }
pub struct Route {
name: syn::Ident,
args: Args,
ast: syn::ItemFn,
resource_type: ResourceType,
guard: GuardType,
}
fn guess_resource_type(typ: &syn::Type) -> ResourceType { fn guess_resource_type(typ: &syn::Type) -> ResourceType {
let mut guess = ResourceType::Sync; let mut guess = ResourceType::Sync;
@ -111,75 +139,73 @@ fn guess_resource_type(typ: &syn::Type) -> ResourceType {
guess guess
} }
impl Args { impl Route {
pub fn new(args: &[syn::NestedMeta], input: TokenStream, guard: GuardType) -> Self { pub fn new(
args: AttributeArgs,
input: TokenStream,
guard: GuardType,
) -> syn::Result<Self> {
if args.is_empty() { if args.is_empty() {
panic!( return Err(syn::Error::new(
"invalid server definition, expected: #[{}(\"some path\")]", Span::call_site(),
guard format!(
); r#"invalid server definition, expected #[{}("<some path>")]"#,
guard.as_str().to_ascii_lowercase()
),
));
} }
let ast: syn::ItemFn = syn::parse(input)?;
let name = ast.sig.ident.clone();
let ast: syn::ItemFn = syn::parse(input).expect("Parse input as function"); let args = Args::new(args)?;
let name = ast.ident.clone();
let mut extra_guards = Vec::new(); let resource_type = if ast.sig.asyncness.is_some() {
let mut path = None;
for arg in args {
match arg {
syn::NestedMeta::Literal(syn::Lit::Str(ref fname)) => {
if path.is_some() {
panic!("Multiple paths specified! Should be only one!")
}
let fname = quote!(#fname).to_string();
path = Some(fname.as_str()[1..fname.len() - 1].to_owned())
}
syn::NestedMeta::Meta(syn::Meta::NameValue(ident)) => {
match ident.ident.to_string().to_lowercase().as_str() {
"guard" => match ident.lit {
syn::Lit::Str(ref text) => extra_guards.push(text.value()),
_ => panic!("Attribute guard expects literal string!"),
},
attr => panic!(
"Unknown attribute key is specified: {}. Allowed: guard",
attr
),
}
}
attr => panic!("Unknown attribute{:?}", attr),
}
}
let resource_type = if ast.asyncness.is_some() {
ResourceType::Async ResourceType::Async
} else { } else {
match ast.decl.output { match ast.sig.output {
syn::ReturnType::Default => panic!( syn::ReturnType::Default => {
"Function {} has no return type. Cannot be used as handler", return Err(syn::Error::new_spanned(
name ast,
), "Function has no return type. Cannot be used as handler",
));
}
syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()), syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()),
} }
}; };
let path = path.unwrap(); Ok(Self {
Self {
name, name,
path, args,
ast, ast,
resource_type, resource_type,
guard, guard,
extra_guards, })
}
} }
pub fn generate(&self) -> TokenStream { pub fn generate(&self) -> TokenStream {
let text = self.to_string(); let name = &self.name;
let guard = &self.guard;
let ast = &self.ast;
let path = &self.args.path;
let extra_guards = &self.args.guards;
let resource_type = &self.resource_type;
let stream = quote! {
#[allow(non_camel_case_types)]
pub struct #name;
match text.parse() { impl actix_web::dev::HttpServiceFactory for #name {
Ok(res) => res, fn register(self, config: &mut actix_web::dev::AppService) {
Err(error) => panic!("Error: {:?}\nGenerated code: {}", error, text), #ast
}
let resource = actix_web::Resource::new(#path)
.guard(actix_web::guard::#guard())
#(.guard(actix_web::guard::fn_guard(#extra_guards)))*
.#resource_type(#name);
actix_web::dev::HttpServiceFactory::register(resource, config)
}
}
};
stream.into()
} }
} }

View File

@ -1,5 +1,17 @@
# Changes # Changes
## [0.2.8] - 2019-11-06
* Add support for setting query from Serialize type for client request.
## [0.2.7] - 2019-09-25
### Added
* Remaining getter methods for `ClientRequest`'s private `head` field #1101
## [0.2.6] - 2019-09-12 ## [0.2.6] - 2019-09-12
### Added ### Added

View File

@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "0.2.6" version = "0.2.8"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix http client." description = "Actix http client."
readme = "README.md" readme = "README.md"
@ -44,7 +44,7 @@ flate2-rust = ["actix-http/flate2-rust"]
[dependencies] [dependencies]
actix-codec = "0.1.2" actix-codec = "0.1.2"
actix-service = "0.4.1" actix-service = "0.4.1"
actix-http = "0.2.10" actix-http = "0.2.11"
base64 = "0.10.1" base64 = "0.10.1"
bytes = "0.4" bytes = "0.4"
derive_more = "0.15.0" derive_more = "0.15.0"
@ -62,8 +62,8 @@ rustls = { version = "0.15.2", optional = true }
[dev-dependencies] [dev-dependencies]
actix-rt = "0.2.2" actix-rt = "0.2.2"
actix-web = { version = "1.0.0", features=["ssl"] } actix-web = { version = "1.0.8", features=["ssl"] }
actix-http = { version = "0.2.10", features=["ssl"] } actix-http = { version = "0.2.11", features=["ssl"] }
actix-http-test = { version = "0.2.0", features=["ssl"] } actix-http-test = { version = "0.2.0", features=["ssl"] }
actix-utils = "0.4.1" actix-utils = "0.4.1"
actix-server = { version = "0.6.0", features=["ssl", "rust-tls"] } actix-server = { version = "0.6.0", features=["ssl", "rust-tls"] }

View File

@ -96,7 +96,7 @@ impl ClientRequest {
self self
} }
/// Get HTTP URI of request /// Get HTTP URI of request.
pub fn get_uri(&self) -> &Uri { pub fn get_uri(&self) -> &Uri {
&self.head.uri &self.head.uri
} }
@ -132,6 +132,16 @@ impl ClientRequest {
self self
} }
/// Get HTTP version of this request.
pub fn get_version(&self) -> &Version {
&self.head.version
}
/// Get peer address of this request.
pub fn get_peer_addr(&self) -> &Option<net::SocketAddr> {
&self.head.peer_addr
}
#[inline] #[inline]
/// Returns request's headers. /// Returns request's headers.
pub fn headers(&self) -> &HeaderMap { pub fn headers(&self) -> &HeaderMap {
@ -372,6 +382,27 @@ impl ClientRequest {
} }
} }
/// Sets the query part of the request
pub fn query<T: Serialize>(
mut self,
query: &T,
) -> Result<Self, serde_urlencoded::ser::Error> {
let mut parts = self.head.uri.clone().into_parts();
if let Some(path_and_query) = parts.path_and_query {
let query = serde_urlencoded::to_string(query)?;
let path = path_and_query.path();
parts.path_and_query = format!("{}?{}", path, query).parse().ok();
match Uri::from_parts(parts) {
Ok(uri) => self.head.uri = uri,
Err(e) => self.err = Some(e.into()),
}
}
Ok(self)
}
/// Freeze request builder and construct `FrozenClientRequest`, /// Freeze request builder and construct `FrozenClientRequest`,
/// which could be used for sending same request multiple times. /// which could be used for sending same request multiple times.
pub fn freeze(self) -> Result<FrozenClientRequest, FreezeRequestError> { pub fn freeze(self) -> Result<FrozenClientRequest, FreezeRequestError> {
@ -680,4 +711,13 @@ mod tests {
"Bearer someS3cr3tAutht0k3n" "Bearer someS3cr3tAutht0k3n"
); );
} }
#[test]
fn client_query() {
let req = Client::new()
.get("/")
.query(&[("key1", "val1"), ("key2", "val2")])
.unwrap();
assert_eq!(req.get_uri().query().unwrap(), "key1=val1&key2=val2");
}
} }

View File

@ -46,9 +46,9 @@ pub trait FromRequest: Sized {
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate serde_derive;
/// use actix_web::{web, dev, App, Error, HttpRequest, FromRequest}; /// use actix_web::{web, dev, App, Error, HttpRequest, FromRequest};
/// use actix_web::error::ErrorBadRequest; /// use actix_web::error::ErrorBadRequest;
/// use serde_derive::Deserialize;
/// use rand; /// use rand;
/// ///
/// #[derive(Debug, Deserialize)] /// #[derive(Debug, Deserialize)]
@ -119,9 +119,9 @@ where
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate serde_derive;
/// use actix_web::{web, dev, App, Result, Error, HttpRequest, FromRequest}; /// use actix_web::{web, dev, App, Result, Error, HttpRequest, FromRequest};
/// use actix_web::error::ErrorBadRequest; /// use actix_web::error::ErrorBadRequest;
/// use serde_derive::Deserialize;
/// use rand; /// use rand;
/// ///
/// #[derive(Debug, Deserialize)] /// #[derive(Debug, Deserialize)]

View File

@ -1,16 +1,16 @@
//! Route match guards. //! Route match guards.
//! //!
//! Guards are one of the way how actix-web router chooses //! Guards are one of the ways how actix-web router chooses a
//! handler service. In essence it just function that accepts //! handler service. In essence it is just a function that accepts a
//! reference to a `RequestHead` instance and returns boolean. //! reference to a `RequestHead` instance and returns a boolean.
//! It is possible to add guards to *scopes*, *resources* //! It is possible to add guards to *scopes*, *resources*
//! and *routes*. Actix provide several guards by default, like various //! and *routes*. Actix provide several guards by default, like various
//! http methods, header, etc. To become a guard, type must implement `Guard` //! http methods, header, etc. To become a guard, type must implement `Guard`
//! trait. Simple functions coulds guards as well. //! trait. Simple functions coulds guards as well.
//! //!
//! Guard can not modify request object. But it is possible to //! Guards can not modify the request object. But it is possible
//! to store extra attributes on a request by using `Extensions` container. //! to store extra attributes on a request by using the `Extensions` container.
//! Extensions container available via `RequestHead::extensions()` method. //! Extensions containers are available via the `RequestHead::extensions()` method.
//! //!
//! ```rust //! ```rust
//! use actix_web::{web, http, dev, guard, App, HttpResponse}; //! use actix_web::{web, http, dev, guard, App, HttpResponse};
@ -29,11 +29,11 @@
use actix_http::http::{self, header, uri::Uri, HttpTryFrom}; use actix_http::http::{self, header, uri::Uri, HttpTryFrom};
use actix_http::RequestHead; use actix_http::RequestHead;
/// Trait defines resource guards. Guards are used for routes selection. /// Trait defines resource guards. Guards are used for route selection.
/// ///
/// Guard can not modify request object. But it is possible to /// Guards can not modify the request object. But it is possible
/// to store extra attributes on request by using `Extensions` container, /// to store extra attributes on a request by using the `Extensions` container.
/// Extensions container available via `RequestHead::extensions()` method. /// Extensions containers are available via the `RequestHead::extensions()` method.
pub trait Guard { pub trait Guard {
/// Check if request matches predicate /// Check if request matches predicate
fn check(&self, request: &RequestHead) -> bool; fn check(&self, request: &RequestHead) -> bool;
@ -276,10 +276,12 @@ pub fn Host<H: AsRef<str>>(host: H) -> HostGuard {
fn get_host_uri(req: &RequestHead) -> Option<Uri> { fn get_host_uri(req: &RequestHead) -> Option<Uri> {
use core::str::FromStr; use core::str::FromStr;
let host_value = req.headers.get(header::HOST)?; req.headers
let host = host_value.to_str().ok()?; .get(header::HOST)
let uri = Uri::from_str(host).ok()?; .and_then(|host_value| host_value.to_str().ok())
Some(uri) .or_else(|| req.uri.host())
.map(|host: &str| Uri::from_str(host).ok())
.and_then(|host_success| host_success)
} }
#[doc(hidden)] #[doc(hidden)]
@ -400,6 +402,31 @@ mod tests {
assert!(!pred.check(req.head())); assert!(!pred.check(req.head()));
} }
#[test]
fn test_host_without_header() {
let req = TestRequest::default()
.uri("www.rust-lang.org")
.to_http_request();
let pred = Host("www.rust-lang.org");
assert!(pred.check(req.head()));
let pred = Host("www.rust-lang.org").scheme("https");
assert!(pred.check(req.head()));
let pred = Host("blog.rust-lang.org");
assert!(!pred.check(req.head()));
let pred = Host("blog.rust-lang.org").scheme("https");
assert!(!pred.check(req.head()));
let pred = Host("crates.io");
assert!(!pred.check(req.head()));
let pred = Host("localhost");
assert!(!pred.check(req.head()));
}
#[test] #[test]
fn test_methods() { fn test_methods() {
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();

View File

@ -155,13 +155,19 @@ impl ConnectionInfo {
&self.host &self.host
} }
/// Remote IP of client initiated HTTP request. /// Remote socket addr of client initiated HTTP request.
/// ///
/// The IP is resolved through the following headers, in this order: /// The addr is resolved through the following headers, in this order:
/// ///
/// - Forwarded /// - Forwarded
/// - X-Forwarded-For /// - X-Forwarded-For
/// - peer name of opened socket /// - peer name of opened socket
///
/// # Security
/// Do not use this function for security purposes, unless you can ensure the Forwarded and
/// X-Forwarded-For headers cannot be spoofed by the client. If you want the client's socket
/// address explicitly, use
/// [`HttpRequest::peer_addr()`](../web/struct.HttpRequest.html#method.peer_addr) instead.
#[inline] #[inline]
pub fn remote(&self) -> Option<&str> { pub fn remote(&self) -> Option<&str> {
if let Some(ref r) = self.remote { if let Some(ref r) = self.remote {

View File

@ -271,8 +271,8 @@ impl Drop for HttpRequest {
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App, HttpRequest}; /// use actix_web::{web, App, HttpRequest};
/// use serde_derive::Deserialize;
/// ///
/// /// extract `Thing` from request /// /// extract `Thing` from request
/// fn index(req: HttpRequest) -> String { /// fn index(req: HttpRequest) -> String {

View File

@ -189,11 +189,21 @@ where
/// )); /// ));
/// } /// }
/// ``` /// ```
pub fn data<U: 'static>(mut self, data: U) -> Self { pub fn data<U: 'static>(self, data: U) -> Self {
self.register_data(Data::new(data))
}
/// Set or override application data.
///
/// This method has the same effect as [`Resource::data`](#method.data),
/// except that instead of taking a value of some type `T`, it expects a
/// value of type `Data<T>`. Use a `Data<T>` extractor to retrieve its
/// value.
pub fn register_data<U: 'static>(mut self, data: Data<U>) -> Self {
if self.data.is_none() { if self.data.is_none() {
self.data = Some(Extensions::new()); self.data = Some(Extensions::new());
} }
self.data.as_mut().unwrap().insert(Data::new(data)); self.data.as_mut().unwrap().insert(data);
self self
} }
@ -763,4 +773,34 @@ mod tests {
let resp = call_service(&mut srv, req); let resp = call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::NO_CONTENT); assert_eq!(resp.status(), StatusCode::NO_CONTENT);
} }
#[test]
fn test_data() {
let mut srv = init_service(
App::new()
.data(1.0f64)
.data(1usize)
.register_data(web::Data::new('-'))
.service(
web::resource("/test")
.data(10usize)
.register_data(web::Data::new('*'))
.guard(guard::Get())
.to(
|data1: web::Data<usize>,
data2: web::Data<char>,
data3: web::Data<f64>| {
assert_eq!(*data1, 10);
assert_eq!(*data2, '*');
assert_eq!(*data3, 1.0);
HttpResponse::Ok()
},
),
),
);
let req = TestRequest::get().uri("/test").to_request();
let resp = call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::OK);
}
} }

View File

@ -178,8 +178,8 @@ impl Route {
/// Set handler function, use request extractors for parameters. /// Set handler function, use request extractors for parameters.
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, http, App}; /// use actix_web::{web, http, App};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {
@ -239,9 +239,9 @@ impl Route {
/// ///
/// ```rust /// ```rust
/// # use futures::future::ok; /// # use futures::future::ok;
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App, Error}; /// use actix_web::{web, App, Error};
/// use futures::Future; /// use futures::Future;
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {

View File

@ -148,11 +148,20 @@ where
/// ); /// );
/// } /// }
/// ``` /// ```
pub fn data<U: 'static>(mut self, data: U) -> Self { pub fn data<U: 'static>(self, data: U) -> Self {
self.register_data(Data::new(data))
}
/// Set or override application data.
///
/// This method has the same effect as [`Scope::data`](#method.data), except
/// that instead of taking a value of some type `T`, it expects a value of
/// type `Data<T>`. Use a `Data<T>` extractor to retrieve its value.
pub fn register_data<U: 'static>(mut self, data: Data<U>) -> Self {
if self.data.is_none() { if self.data.is_none() {
self.data = Some(Extensions::new()); self.data = Some(Extensions::new());
} }
self.data.as_mut().unwrap().insert(Data::new(data)); self.data.as_mut().unwrap().insert(data);
self self
} }
@ -1082,6 +1091,28 @@ mod tests {
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
#[test]
fn test_override_register_data() {
let mut srv = init_service(
App::new().register_data(web::Data::new(1usize)).service(
web::scope("app")
.register_data(web::Data::new(10usize))
.route(
"/t",
web::get().to(|data: web::Data<usize>| {
assert_eq!(*data, 10);
let _ = data.clone();
HttpResponse::Ok()
}),
),
),
);
let req = TestRequest::with_uri("/app/t").to_request();
let resp = call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::OK);
}
#[test] #[test]
fn test_scope_config() { fn test_scope_config() {
let mut srv = let mut srv =

View File

@ -435,6 +435,37 @@ where
Ok(self) Ok(self)
} }
#[cfg(feature = "uds")]
/// Start listening for unix domain connections on existing listener.
///
/// This method is available with `uds` feature.
pub fn listen_uds(
mut self,
lst: std::os::unix::net::UnixListener,
) -> io::Result<Self> {
let cfg = self.config.clone();
let factory = self.factory.clone();
// todo duplicated:
self.sockets.push(Socket {
scheme: "http",
addr: net::SocketAddr::new(
net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)),
8080,
),
});
let addr = format!("actix-web-service-{:?}", lst.local_addr()?);
self.builder = self.builder.listen_uds(addr, lst, move || {
let c = cfg.lock();
HttpService::build()
.keep_alive(c.keep_alive)
.client_timeout(c.client_timeout)
.finish(factory())
})?;
Ok(self)
}
#[cfg(feature = "uds")] #[cfg(feature = "uds")]
/// Start listening for incoming unix domain connections. /// Start listening for incoming unix domain connections.
/// ///

View File

@ -1,5 +1,4 @@
//! Various helpers for Actix applications to use during testing. //! Various helpers for Actix applications to use during testing.
use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use actix_http::http::header::{ContentType, Header, HeaderName, IntoHeaderValue}; use actix_http::http::header::{ContentType, Header, HeaderName, IntoHeaderValue};
@ -7,17 +6,17 @@ use actix_http::http::{HttpTryFrom, Method, StatusCode, Uri, Version};
use actix_http::test::TestRequest as HttpTestRequest; use actix_http::test::TestRequest as HttpTestRequest;
use actix_http::{cookie::Cookie, Extensions, Request}; use actix_http::{cookie::Cookie, Extensions, Request};
use actix_router::{Path, ResourceDef, Url}; use actix_router::{Path, ResourceDef, Url};
use actix_rt::{System, SystemRunner};
use actix_server_config::ServerConfig; use actix_server_config::ServerConfig;
use actix_service::{IntoNewService, IntoService, NewService, Service}; use actix_service::{IntoNewService, IntoService, NewService, Service};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures::future::{lazy, ok, Future, IntoFuture}; use futures::future::{ok, Future};
use futures::Stream; use futures::Stream;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use serde_json; use serde_json;
pub use actix_http::test::TestBuffer; pub use actix_http::test::TestBuffer;
pub use actix_testing::{block_fn, block_on, run_on};
use crate::config::{AppConfig, AppConfigInner}; use crate::config::{AppConfig, AppConfigInner};
use crate::data::Data; use crate::data::Data;
@ -27,78 +26,6 @@ use crate::rmap::ResourceMap;
use crate::service::{ServiceRequest, ServiceResponse}; use crate::service::{ServiceRequest, ServiceResponse};
use crate::{Error, HttpRequest, HttpResponse}; use crate::{Error, HttpRequest, HttpResponse};
thread_local! {
static RT: RefCell<Inner> = {
RefCell::new(Inner(Some(System::builder().build())))
};
}
struct Inner(Option<SystemRunner>);
impl Inner {
fn get_mut(&mut self) -> &mut SystemRunner {
self.0.as_mut().unwrap()
}
}
impl Drop for Inner {
fn drop(&mut self) {
std::mem::forget(self.0.take().unwrap())
}
}
/// Runs the provided future, blocking the current thread until the future
/// completes.
///
/// This function can be used to synchronously block the current thread
/// until the provided `future` has resolved either successfully or with an
/// error. The result of the future is then returned from this function
/// call.
///
/// Note that this function is intended to be used only for testing purpose.
/// This function panics on nested call.
pub fn block_on<F>(f: F) -> Result<F::Item, F::Error>
where
F: IntoFuture,
{
RT.with(move |rt| rt.borrow_mut().get_mut().block_on(f.into_future()))
}
/// Runs the provided function, blocking the current thread until the result
/// future completes.
///
/// This function can be used to synchronously block the current thread
/// until the provided `future` has resolved either successfully or with an
/// error. The result of the future is then returned from this function
/// call.
///
/// Note that this function is intended to be used only for testing purpose.
/// This function panics on nested call.
pub fn block_fn<F, R>(f: F) -> Result<R::Item, R::Error>
where
F: FnOnce() -> R,
R: IntoFuture,
{
RT.with(move |rt| rt.borrow_mut().get_mut().block_on(lazy(f)))
}
#[doc(hidden)]
/// Runs the provided function, with runtime enabled.
///
/// Note that this function is intended to be used only for testing purpose.
/// This function panics on nested call.
pub fn run_on<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
RT.with(move |rt| {
rt.borrow_mut()
.get_mut()
.block_on(lazy(|| Ok::<_, ()>(f())))
})
.unwrap()
}
/// Create service that always responds with `HttpResponse::Ok()` /// Create service that always responds with `HttpResponse::Ok()`
pub fn ok_service( pub fn ok_service(
) -> impl Service<Request = ServiceRequest, Response = ServiceResponse<Body>, Error = Error> ) -> impl Service<Request = ServiceRequest, Response = ServiceResponse<Body>, Error = Error>

View File

@ -35,9 +35,8 @@ use crate::responder::Responder;
/// ///
/// ### Example /// ### Example
/// ```rust /// ```rust
/// # extern crate actix_web; /// use actix_web::web;
/// #[macro_use] extern crate serde_derive; /// use serde_derive::Deserialize;
/// use actix_web::{web, App};
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct FormData { /// struct FormData {
@ -61,9 +60,9 @@ use crate::responder::Responder;
/// ///
/// ### Example /// ### Example
/// ```rust /// ```rust
/// # #[macro_use] extern crate serde_derive; /// use actix_web::*;
/// # use actix_web::*; /// use serde_derive::Serialize;
/// # ///
/// #[derive(Serialize)] /// #[derive(Serialize)]
/// struct SomeForm { /// struct SomeForm {
/// name: String, /// name: String,
@ -167,8 +166,8 @@ impl<T: Serialize> Responder for Form<T> {
/// Form extractor configuration /// Form extractor configuration
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App, FromRequest, Result}; /// use actix_web::{web, App, FromRequest, Result};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct FormData { /// struct FormData {

View File

@ -33,8 +33,8 @@ use crate::responder::Responder;
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App}; /// use actix_web::{web, App};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {
@ -60,9 +60,9 @@ use crate::responder::Responder;
/// trait from *serde*. /// trait from *serde*.
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate serde_derive; /// use actix_web::*;
/// # use actix_web::*; /// use serde_derive::Serialize;
/// # ///
/// #[derive(Serialize)] /// #[derive(Serialize)]
/// struct MyObj { /// struct MyObj {
/// name: String, /// name: String,
@ -144,8 +144,8 @@ impl<T: Serialize> Responder for Json<T> {
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App}; /// use actix_web::{web, App};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {
@ -203,8 +203,8 @@ where
/// Json extractor configuration /// Json extractor configuration
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{error, web, App, FromRequest, HttpResponse}; /// use actix_web::{error, web, App, FromRequest, HttpResponse};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {

View File

@ -39,8 +39,8 @@ use crate::FromRequest;
/// implements `Deserialize` trait from *serde*. /// implements `Deserialize` trait from *serde*.
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App, Error}; /// use actix_web::{web, App, Error};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {
@ -134,8 +134,8 @@ impl<T: fmt::Display> fmt::Display for Path<T> {
/// implements `Deserialize` trait from *serde*. /// implements `Deserialize` trait from *serde*.
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App, Error}; /// use actix_web::{web, App, Error};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {
@ -190,10 +190,9 @@ where
/// Path extractor configuration /// Path extractor configuration
/// ///
/// ```rust /// ```rust
/// # #[macro_use]
/// # extern crate serde_derive;
/// use actix_web::web::PathConfig; /// use actix_web::web::PathConfig;
/// use actix_web::{error, web, App, FromRequest, HttpResponse}; /// use actix_web::{error, web, App, FromRequest, HttpResponse};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize, Debug)] /// #[derive(Deserialize, Debug)]
/// enum Folder { /// enum Folder {

View File

@ -43,7 +43,14 @@ use crate::request::HttpRequest;
/// ); /// );
/// } /// }
/// ``` /// ```
pub struct Payload(crate::dev::Payload); pub struct Payload(pub crate::dev::Payload);
impl Payload {
/// Deconstruct to a inner value
pub fn into_inner(self) -> crate::dev::Payload {
self.0
}
}
impl Stream for Payload { impl Stream for Payload {
type Item = Bytes; type Item = Bytes;

View File

@ -21,8 +21,8 @@ use crate::request::HttpRequest;
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App}; /// use actix_web::{web, App};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Debug, Deserialize)] /// #[derive(Debug, Deserialize)]
/// pub enum ResponseType { /// pub enum ResponseType {
@ -99,8 +99,8 @@ impl<T: fmt::Display> fmt::Display for Query<T> {
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App}; /// use actix_web::{web, App};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Debug, Deserialize)] /// #[derive(Debug, Deserialize)]
/// pub enum ResponseType { /// pub enum ResponseType {
@ -169,8 +169,8 @@ where
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{error, web, App, FromRequest, HttpResponse}; /// use actix_web::{error, web, App, FromRequest, HttpResponse};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {

View File

@ -5,6 +5,7 @@
### Changed ### Changed
* Update serde_urlencoded to "0.6.1" * Update serde_urlencoded to "0.6.1"
* Increase TestServerRuntime timeouts from 500ms to 3000ms
### Fixed ### Fixed

View File

@ -144,7 +144,7 @@ impl TestServer {
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
Connector::new() Connector::new()
.conn_lifetime(time::Duration::from_secs(0)) .conn_lifetime(time::Duration::from_secs(0))
.timeout(time::Duration::from_millis(500)) .timeout(time::Duration::from_millis(3000))
.ssl(builder.build()) .ssl(builder.build())
.finish() .finish()
} }
@ -152,7 +152,7 @@ impl TestServer {
{ {
Connector::new() Connector::new()
.conn_lifetime(time::Duration::from_secs(0)) .conn_lifetime(time::Duration::from_secs(0))
.timeout(time::Duration::from_millis(500)) .timeout(time::Duration::from_millis(3000))
.finish() .finish()
} }
}; };