1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-15 22:34:47 +02:00

Compare commits

...

19 Commits

Author SHA1 Message Date
Rob Ede
aa11231ee5 prepare web release 3.1.0 (#1716) 2020-09-30 11:07:35 +01:00
PeterUlb
b5812b15f0 Remove Sized Bound for web::Data (#1712) 2020-09-29 22:44:12 +01:00
Matt Gathu
b4e02fe29a Fix cyclic references in ResourceMap (#1708) 2020-09-25 17:42:49 +01:00
Matt Gathu
37c76a39ab Fix Multipart consuming payload before header checks (#1704)
* Fix Multipart consuming payload before header checks

What
--
Split up logic in the constructor into two functions:

- **from_boundary:** build Multipart from boundary and stream
- **from_error:** build Multipart for MultipartError

Also we make the `boundary`, `from_boundary`, `from_error`  methods public within the crate so that we can use them in the extractor.

The extractor is then able to perform header checks and only consume the
payload if the checks pass.

* Add tests

* Add payload consumption test

Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-09-25 14:50:37 +01:00
LIU An (劉安)
60e7e52276 Add TrailingSlash::MergeOnly behavior (#1695)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-09-25 12:50:59 +01:00
Rob Ede
c53e9468bc prepare codegen 0.4.0 release (#1702) 2020-09-24 23:54:01 +01:00
Arniu Tseng
162121bf8d Unify route macros (#1705) 2020-09-22 22:42:51 +01:00
Rob Ede
f7bcad9567 split up files lib (#1685) 2020-09-20 23:18:25 +01:00
Igor Aleksanov
f9e3f78e45 eemove non-relevant comment from actix-http README.md (#1701) 2020-09-20 17:21:53 +01:00
Silentdoer
1596893ef7 update actix-http dev-dependencies (#1696)
Co-authored-by: luojinming <luojm@hxsmart.com>
2020-09-19 23:20:34 +09:00
Lokathor
2a2474ca09 Update tinyvec to 1.0 (#1689) 2020-09-17 18:09:42 +01:00
Matt Gathu
509b2e6eec Provide attribute macro for multiple HTTP methods (#1674)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-09-16 22:37:41 +01:00
Rob Ede
d707704556 prepare web release 3.0.2 (#1681) 2020-09-15 13:14:14 +01:00
Aleksandrov Vladimir
a429ee6646 Add possibility to set address for test_server (#1645) 2020-09-15 12:09:16 +01:00
Rob Ede
7f8073233a fix trimming to inaccessible root path (#1678) 2020-09-15 11:32:31 +01:00
Rob Ede
4b4c9d1b93 update migration guide
closes #1680
2020-09-14 22:26:03 +01:00
Rob Ede
3fde3be3d8 add trybuild tests to routing codegen (#1677) 2020-09-13 16:31:08 +01:00
Rob Ede
f861508789 prepare web release 3.0.1 (#1676) 2020-09-13 03:24:44 +01:00
Damian Lesiuk
a4546f02d2 make TrailingSlash enum accessible (#1673)
Co-authored-by: Damian Lesiuk <lesiuk@sabre.com>
2020-09-13 00:55:39 +01:00
44 changed files with 1666 additions and 902 deletions

3
.gitignore vendored
View File

@@ -13,3 +13,6 @@ guide/build/
# These are backup files generated by rustfmt
**/*.rs.bk
# Configuration directory generated by CLion
.idea

View File

@@ -3,6 +3,35 @@
## Unreleased - 2020-xx-xx
## 3.1.0 - 2020-09-29
### Changed
* Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allows `NormalizePath`
to retain any trailing slashes. [#1695]
* Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc<dyn Trait>`
via `web::Data::from` [#1710]
### Fixed
* `ResourceMap` debug printing is no longer infinitely recursive. [#1708]
[#1695]: https://github.com/actix/actix-web/pull/1695
[#1708]: https://github.com/actix/actix-web/pull/1708
[#1710]: https://github.com/actix/actix-web/pull/1710
## 3.0.2 - 2020-09-15
### Fixed
* `NormalizePath` when used with `TrailingSlash::Trim` no longer trims the root path "/". [#1678]
[#1678]: https://github.com/actix/actix-web/pull/1678
## 3.0.1 - 2020-09-13
### Changed
* `middleware::normalize::TrailingSlash` enum is now accessible. [#1673]
[#1673]: https://github.com/actix/actix-web/pull/1673
## 3.0.0 - 2020-09-11
* No significant changes from `3.0.0-beta.4`.
@@ -157,7 +186,7 @@
### Deleted
* Delete HttpServer::run(), it is not useful witht async/await
* Delete HttpServer::run(), it is not useful with async/await
## [2.0.0-alpha.3] - 2019-12-07
@@ -202,7 +231,7 @@
### Changed
* Make UrlEncodedError::Overflow more informativve
* Make UrlEncodedError::Overflow more informative
* Use actix-testing for testing utils
@@ -220,7 +249,7 @@
* Re-implement Host predicate (#989)
* Form immplements Responder, returning a `application/x-www-form-urlencoded` response
* Form implements Responder, returning a `application/x-www-form-urlencoded` response
* Add `into_inner` to `Data`

View File

@@ -1,8 +1,8 @@
[package]
name = "actix-web"
version = "3.0.0"
version = "3.1.0"
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 powerful, pragmatic, and extremely fast web framework for Rust."
readme = "README.md"
keywords = ["actix", "http", "web", "framework", "async"]
homepage = "https://actix.rs"
@@ -99,11 +99,11 @@ time = { version = "0.2.7", default-features = false, features = ["std"] }
url = "2.1"
open-ssl = { package = "openssl", version = "0.10", optional = true }
rust-tls = { package = "rustls", version = "0.18.0", optional = true }
tinyvec = { version = "0.3", features = ["alloc"] }
tinyvec = { version = "1", features = ["alloc"] }
[dev-dependencies]
actix = "0.10.0"
actix-http = { version = "2.0.0-beta.4", features = ["actors"] }
actix-http = { version = "2.0.0", features = ["actors"] }
rand = "0.7"
env_logger = "0.7"
serde_derive = "1.0"

View File

@@ -3,12 +3,23 @@
## 3.0.0
* The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to
simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
* Cookie handling has been offloaded to the `cookie` crate:
* `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
* Some types now require lifetime parameters.
* The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects
any `actix-web` method previously expecting a time v0.1 input.
* Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now
result in `SameSite=None` being sent with the response Set-Cookie header.
To create a cookie without a SameSite attribute, remove any calls setting same_site.
* actix-http support for Actors messages was moved to actix-http crate and is enabled
with feature `actors`
* content_length function is removed from actix-http.
You can set Content-Length by normally setting the response body or calling no_chunking function.
@@ -37,7 +48,7 @@
* `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one.
It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`,
or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::default())`.
or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`.
* `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`.

View File

@@ -18,7 +18,6 @@ path = "src/lib.rs"
[dependencies]
actix-web = { version = "3.0.0", default-features = false }
actix-http = "2.0.0"
actix-service = "1.0.6"
bitflags = "1"
bytes = "0.5.3"

View File

@@ -0,0 +1,94 @@
use std::{
cmp, fmt,
fs::File,
future::Future,
io::{self, Read, Seek},
pin::Pin,
task::{Context, Poll},
};
use actix_web::{
error::{BlockingError, Error},
web,
};
use bytes::Bytes;
use futures_core::{ready, Stream};
use futures_util::future::{FutureExt, LocalBoxFuture};
use crate::handle_error;
type ChunkedBoxFuture =
LocalBoxFuture<'static, Result<(File, Bytes), BlockingError<io::Error>>>;
#[doc(hidden)]
/// A helper created from a `std::fs::File` which reads the file
/// chunk-by-chunk on a `ThreadPool`.
pub struct ChunkedReadFile {
pub(crate) size: u64,
pub(crate) offset: u64,
pub(crate) file: Option<File>,
pub(crate) fut: Option<ChunkedBoxFuture>,
pub(crate) counter: u64,
}
impl fmt::Debug for ChunkedReadFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ChunkedReadFile")
}
}
impl Stream for ChunkedReadFile {
type Item = Result<Bytes, Error>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
if let Some(ref mut fut) = self.fut {
return match ready!(Pin::new(fut).poll(cx)) {
Ok((file, bytes)) => {
self.fut.take();
self.file = Some(file);
self.offset += bytes.len() as u64;
self.counter += bytes.len() as u64;
Poll::Ready(Some(Ok(bytes)))
}
Err(e) => Poll::Ready(Some(Err(handle_error(e)))),
};
}
let size = self.size;
let offset = self.offset;
let counter = self.counter;
if size == counter {
Poll::Ready(None)
} else {
let mut file = self.file.take().expect("Use after completion");
self.fut = Some(
web::block(move || {
let max_bytes =
cmp::min(size.saturating_sub(counter), 65_536) as usize;
let mut buf = Vec::with_capacity(max_bytes);
file.seek(io::SeekFrom::Start(offset))?;
let n_bytes =
file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
if n_bytes == 0 {
return Err(io::ErrorKind::UnexpectedEof.into());
}
Ok((file, Bytes::from(buf)))
})
.boxed_local(),
);
self.poll_next(cx)
}
}
}

View File

@@ -0,0 +1,114 @@
use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf};
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
use percent_encoding::{utf8_percent_encode, CONTROLS};
use v_htmlescape::escape as escape_html_entity;
/// A directory; responds with the generated directory listing.
#[derive(Debug)]
pub struct Directory {
/// Base directory.
pub base: PathBuf,
/// Path of subdirectory to generate listing for.
pub path: PathBuf,
}
impl Directory {
/// Create a new directory
pub fn new(base: PathBuf, path: PathBuf) -> Directory {
Directory { base, path }
}
/// Is this entry visible from this directory?
pub fn is_visible(&self, entry: &io::Result<DirEntry>) -> bool {
if let Ok(ref entry) = *entry {
if let Some(name) = entry.file_name().to_str() {
if name.starts_with('.') {
return false;
}
}
if let Ok(ref md) = entry.metadata() {
let ft = md.file_type();
return ft.is_dir() || ft.is_file() || ft.is_symlink();
}
}
false
}
}
pub(crate) type DirectoryRenderer =
dyn Fn(&Directory, &HttpRequest) -> Result<ServiceResponse, io::Error>;
// show file url as relative to static path
macro_rules! encode_file_url {
($path:ident) => {
utf8_percent_encode(&$path, CONTROLS)
};
}
// " -- &quot; & -- &amp; ' -- &#x27; < -- &lt; > -- &gt; / -- &#x2f;
macro_rules! encode_file_name {
($entry:ident) => {
escape_html_entity(&$entry.file_name().to_string_lossy())
};
}
pub(crate) fn directory_listing(
dir: &Directory,
req: &HttpRequest,
) -> Result<ServiceResponse, io::Error> {
let index_of = format!("Index of {}", req.path());
let mut body = String::new();
let base = Path::new(req.path());
for entry in dir.path.read_dir()? {
if dir.is_visible(&entry) {
let entry = entry.unwrap();
let p = match entry.path().strip_prefix(&dir.path) {
Ok(p) if cfg!(windows) => {
base.join(p).to_string_lossy().replace("\\", "/")
}
Ok(p) => base.join(p).to_string_lossy().into_owned(),
Err(_) => continue,
};
// if file is a directory, add '/' to the end of the name
if let Ok(metadata) = entry.metadata() {
if metadata.is_dir() {
let _ = write!(
body,
"<li><a href=\"{}\">{}/</a></li>",
encode_file_url!(p),
encode_file_name!(entry),
);
} else {
let _ = write!(
body,
"<li><a href=\"{}\">{}</a></li>",
encode_file_url!(p),
encode_file_name!(entry),
);
}
} else {
continue;
}
}
}
let html = format!(
"<html>\
<head><title>{}</title></head>\
<body><h1>{}</h1>\
<ul>\
{}\
</ul></body>\n</html>",
index_of, index_of, body
);
Ok(ServiceResponse::new(
req.clone(),
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(html),
))
}

250
actix-files/src/files.rs Normal file
View File

@@ -0,0 +1,250 @@
use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc};
use actix_service::{boxed, IntoServiceFactory, ServiceFactory};
use actix_web::{
dev::{
AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse,
},
error::Error,
guard::Guard,
http::header::DispositionType,
HttpRequest,
};
use futures_util::future::{ok, FutureExt, LocalBoxFuture};
use crate::{
directory_listing, named, Directory, DirectoryRenderer, FilesService,
HttpNewService, MimeOverride,
};
/// Static files handling service.
///
/// `Files` service must be registered with `App::service()` method.
///
/// ```rust
/// use actix_web::App;
/// use actix_files::Files;
///
/// let app = App::new()
/// .service(Files::new("/static", "."));
/// ```
pub struct Files {
path: String,
directory: PathBuf,
index: Option<String>,
show_index: bool,
redirect_to_slash: bool,
default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
renderer: Rc<DirectoryRenderer>,
mime_override: Option<Rc<MimeOverride>>,
file_flags: named::Flags,
guards: Option<Rc<dyn Guard>>,
}
impl fmt::Debug for Files {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Files")
}
}
impl Clone for Files {
fn clone(&self) -> Self {
Self {
directory: self.directory.clone(),
index: self.index.clone(),
show_index: self.show_index,
redirect_to_slash: self.redirect_to_slash,
default: self.default.clone(),
renderer: self.renderer.clone(),
file_flags: self.file_flags,
path: self.path.clone(),
mime_override: self.mime_override.clone(),
guards: self.guards.clone(),
}
}
}
impl Files {
/// Create new `Files` instance for specified base directory.
///
/// `File` uses `ThreadPool` for blocking filesystem operations.
/// By default pool with 5x threads of available cpus is used.
/// Pool size can be changed by setting ACTIX_THREADPOOL environment variable.
pub fn new<T: Into<PathBuf>>(path: &str, dir: T) -> Files {
let orig_dir = dir.into();
let dir = match orig_dir.canonicalize() {
Ok(canon_dir) => canon_dir,
Err(_) => {
log::error!("Specified path is not a directory: {:?}", orig_dir);
PathBuf::new()
}
};
Files {
path: path.to_string(),
directory: dir,
index: None,
show_index: false,
redirect_to_slash: false,
default: Rc::new(RefCell::new(None)),
renderer: Rc::new(directory_listing),
mime_override: None,
file_flags: named::Flags::default(),
guards: None,
}
}
/// Show files listing for directories.
///
/// By default show files listing is disabled.
pub fn show_files_listing(mut self) -> Self {
self.show_index = true;
self
}
/// 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
pub fn files_listing_renderer<F>(mut self, f: F) -> Self
where
for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error>
+ 'static,
{
self.renderer = Rc::new(f);
self
}
/// Specifies mime override callback
pub fn mime_override<F>(mut self, f: F) -> Self
where
F: Fn(&mime::Name<'_>) -> DispositionType + 'static,
{
self.mime_override = Some(Rc::new(f));
self
}
/// Set index file
///
/// Shows specific index file for directory "/" instead of
/// showing files listing.
pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
self.index = Some(index.into());
self
}
#[inline]
/// Specifies whether to use ETag or not.
///
/// Default is true.
pub fn use_etag(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::ETAG, value);
self
}
#[inline]
/// Specifies whether to use Last-Modified or not.
///
/// Default is true.
pub fn use_last_modified(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::LAST_MD, value);
self
}
/// 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(guards));
self
}
/// Disable `Content-Disposition` header.
///
/// By default Content-Disposition` header is enabled.
#[inline]
pub fn disable_content_disposition(mut self) -> Self {
self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
self
}
/// Sets default handler which is used when no matched file could be found.
pub fn default_handler<F, U>(mut self, f: F) -> Self
where
F: IntoServiceFactory<U>,
U: ServiceFactory<
Config = (),
Request = ServiceRequest,
Response = ServiceResponse,
Error = Error,
> + 'static,
{
// create and configure default resource
self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory(
f.into_factory().map_init_err(|_| ()),
)))));
self
}
}
impl HttpServiceFactory for Files {
fn register(self, config: &mut AppService) {
if self.default.borrow().is_none() {
*self.default.borrow_mut() = Some(config.default_service());
}
let rdef = if config.is_root() {
ResourceDef::root_prefix(&self.path)
} else {
ResourceDef::prefix(&self.path)
};
config.register_service(rdef, None, self, None)
}
}
impl ServiceFactory for Files {
type Request = ServiceRequest;
type Response = ServiceResponse;
type Error = Error;
type Config = ();
type Service = FilesService;
type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
let mut srv = FilesService {
directory: self.directory.clone(),
index: self.index.clone(),
show_index: self.show_index,
redirect_to_slash: self.redirect_to_slash,
default: None,
renderer: self.renderer.clone(),
mime_override: self.mime_override.clone(),
file_flags: self.file_flags,
guards: self.guards.clone(),
};
if let Some(ref default) = *self.default.borrow() {
default
.new_service(())
.map(move |result| match result {
Ok(default) => {
srv.default = Some(default);
Ok(srv)
}
Err(_) => Err(()),
})
.boxed_local()
} else {
ok(srv).boxed_local()
}
}
}

View File

@@ -1,44 +1,52 @@
//! Static files support
//! Static files support for Actix Web.
//!
//! Provides a non-blocking service for serving static files from disk.
//!
//! # Example
//! ```rust
//! use actix_web::App;
//! use actix_files::Files;
//!
//! let app = App::new()
//! .service(Files::new("/static", "."));
//! ```
//!
//! # Implementation Quirks
//! - If a filename contains non-ascii characters, that file will be served with the `charset=utf-8`
//! extension on the Content-Type header.
#![deny(rust_2018_idioms)]
#![allow(clippy::borrow_interior_mutable_const)]
#![warn(missing_docs, missing_debug_implementations)]
use std::cell::RefCell;
use std::fmt::Write;
use std::fs::{DirEntry, File};
use std::future::Future;
use std::io::{Read, Seek};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll};
use std::{cmp, io};
use std::io;
use actix_service::boxed::{self, BoxService, BoxServiceFactory};
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
use actix_web::dev::{
AppService, HttpServiceFactory, Payload, ResourceDef, ServiceRequest,
ServiceResponse,
use actix_service::boxed::{BoxService, BoxServiceFactory};
use actix_web::{
dev::{ServiceRequest, ServiceResponse},
error::{BlockingError, Error, ErrorInternalServerError},
http::header::DispositionType,
};
use actix_web::error::{BlockingError, Error, ErrorInternalServerError};
use actix_web::guard::Guard;
use actix_web::http::header::{self, DispositionType};
use actix_web::http::Method;
use actix_web::{web, FromRequest, HttpRequest, HttpResponse};
use bytes::Bytes;
use futures_core::Stream;
use futures_util::future::{ok, ready, Either, FutureExt, LocalBoxFuture, Ready};
use mime_guess::from_ext;
use percent_encoding::{utf8_percent_encode, CONTROLS};
use v_htmlescape::escape as escape_html_entity;
mod chunked;
mod directory;
mod error;
mod files;
mod named;
mod path_buf;
mod range;
mod service;
use self::error::{FilesError, UriSegmentError};
pub use crate::chunked::ChunkedReadFile;
pub use crate::directory::Directory;
pub use crate::files::Files;
pub use crate::named::NamedFile;
pub use crate::range::HttpRange;
pub use crate::service::FilesService;
use self::directory::{directory_listing, DirectoryRenderer};
use self::error::FilesError;
use self::path_buf::PathBufWrap;
type HttpService = BoxService<ServiceRequest, ServiceResponse, Error>;
type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>;
@@ -51,612 +59,37 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
from_ext(ext).first_or_octet_stream()
}
fn handle_error(err: BlockingError<io::Error>) -> Error {
pub(crate) fn handle_error(err: BlockingError<io::Error>) -> Error {
match err {
BlockingError::Error(err) => err.into(),
BlockingError::Canceled => ErrorInternalServerError("Unexpected error"),
}
}
#[doc(hidden)]
/// A helper created from a `std::fs::File` which reads the file
/// chunk-by-chunk on a `ThreadPool`.
pub struct ChunkedReadFile {
size: u64,
offset: u64,
file: Option<File>,
#[allow(clippy::type_complexity)]
fut:
Option<LocalBoxFuture<'static, Result<(File, Bytes), BlockingError<io::Error>>>>,
counter: u64,
}
impl Stream for ChunkedReadFile {
type Item = Result<Bytes, Error>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
if let Some(ref mut fut) = self.fut {
return match Pin::new(fut).poll(cx) {
Poll::Ready(Ok((file, bytes))) => {
self.fut.take();
self.file = Some(file);
self.offset += bytes.len() as u64;
self.counter += bytes.len() as u64;
Poll::Ready(Some(Ok(bytes)))
}
Poll::Ready(Err(e)) => Poll::Ready(Some(Err(handle_error(e)))),
Poll::Pending => Poll::Pending,
};
}
let size = self.size;
let offset = self.offset;
let counter = self.counter;
if size == counter {
Poll::Ready(None)
} else {
let mut file = self.file.take().expect("Use after completion");
self.fut = Some(
web::block(move || {
let max_bytes: usize;
max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
let mut buf = Vec::with_capacity(max_bytes);
file.seek(io::SeekFrom::Start(offset))?;
let nbytes =
file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
if nbytes == 0 {
return Err(io::ErrorKind::UnexpectedEof.into());
}
Ok((file, Bytes::from(buf)))
})
.boxed_local(),
);
self.poll_next(cx)
}
}
}
type DirectoryRenderer =
dyn Fn(&Directory, &HttpRequest) -> Result<ServiceResponse, io::Error>;
/// A directory; responds with the generated directory listing.
#[derive(Debug)]
pub struct Directory {
/// Base directory
pub base: PathBuf,
/// Path of subdirectory to generate listing for
pub path: PathBuf,
}
impl Directory {
/// Create a new directory
pub fn new(base: PathBuf, path: PathBuf) -> Directory {
Directory { base, path }
}
/// Is this entry visible from this directory?
pub fn is_visible(&self, entry: &io::Result<DirEntry>) -> bool {
if let Ok(ref entry) = *entry {
if let Some(name) = entry.file_name().to_str() {
if name.starts_with('.') {
return false;
}
}
if let Ok(ref md) = entry.metadata() {
let ft = md.file_type();
return ft.is_dir() || ft.is_file() || ft.is_symlink();
}
}
false
}
}
// show file url as relative to static path
macro_rules! encode_file_url {
($path:ident) => {
utf8_percent_encode(&$path, CONTROLS)
};
}
// " -- &quot; & -- &amp; ' -- &#x27; < -- &lt; > -- &gt; / -- &#x2f;
macro_rules! encode_file_name {
($entry:ident) => {
escape_html_entity(&$entry.file_name().to_string_lossy())
};
}
fn directory_listing(
dir: &Directory,
req: &HttpRequest,
) -> Result<ServiceResponse, io::Error> {
let index_of = format!("Index of {}", req.path());
let mut body = String::new();
let base = Path::new(req.path());
for entry in dir.path.read_dir()? {
if dir.is_visible(&entry) {
let entry = entry.unwrap();
let p = match entry.path().strip_prefix(&dir.path) {
Ok(p) if cfg!(windows) => {
base.join(p).to_string_lossy().replace("\\", "/")
}
Ok(p) => base.join(p).to_string_lossy().into_owned(),
Err(_) => continue,
};
// if file is a directory, add '/' to the end of the name
if let Ok(metadata) = entry.metadata() {
if metadata.is_dir() {
let _ = write!(
body,
"<li><a href=\"{}\">{}/</a></li>",
encode_file_url!(p),
encode_file_name!(entry),
);
} else {
let _ = write!(
body,
"<li><a href=\"{}\">{}</a></li>",
encode_file_url!(p),
encode_file_name!(entry),
);
}
} else {
continue;
}
}
}
let html = format!(
"<html>\
<head><title>{}</title></head>\
<body><h1>{}</h1>\
<ul>\
{}\
</ul></body>\n</html>",
index_of, index_of, body
);
Ok(ServiceResponse::new(
req.clone(),
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(html),
))
}
type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
/// Static files handling
///
/// `Files` service must be registered with `App::service()` method.
///
/// ```rust
/// use actix_web::App;
/// use actix_files::Files;
///
/// let app = App::new()
/// .service(Files::new("/static", "."));
/// ```
pub struct Files {
path: String,
directory: PathBuf,
index: Option<String>,
show_index: bool,
redirect_to_slash: bool,
default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
renderer: Rc<DirectoryRenderer>,
mime_override: Option<Rc<MimeOverride>>,
file_flags: named::Flags,
// FIXME: Should re-visit later.
#[allow(clippy::redundant_allocation)]
guards: Option<Rc<Box<dyn Guard>>>,
}
impl Clone for Files {
fn clone(&self) -> Self {
Self {
directory: self.directory.clone(),
index: self.index.clone(),
show_index: self.show_index,
redirect_to_slash: self.redirect_to_slash,
default: self.default.clone(),
renderer: self.renderer.clone(),
file_flags: self.file_flags,
path: self.path.clone(),
mime_override: self.mime_override.clone(),
guards: self.guards.clone(),
}
}
}
impl Files {
/// Create new `Files` instance for specified base directory.
///
/// `File` uses `ThreadPool` for blocking filesystem operations.
/// By default pool with 5x threads of available cpus is used.
/// Pool size can be changed by setting ACTIX_THREADPOOL environment variable.
pub fn new<T: Into<PathBuf>>(path: &str, dir: T) -> Files {
let orig_dir = dir.into();
let dir = match orig_dir.canonicalize() {
Ok(canon_dir) => canon_dir,
Err(_) => {
log::error!("Specified path is not a directory: {:?}", orig_dir);
PathBuf::new()
}
};
Files {
path: path.to_string(),
directory: dir,
index: None,
show_index: false,
redirect_to_slash: false,
default: Rc::new(RefCell::new(None)),
renderer: Rc::new(directory_listing),
mime_override: None,
file_flags: named::Flags::default(),
guards: None,
}
}
/// Show files listing for directories.
///
/// By default show files listing is disabled.
pub fn show_files_listing(mut self) -> Self {
self.show_index = true;
self
}
/// 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
pub fn files_listing_renderer<F>(mut self, f: F) -> Self
where
for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error>
+ 'static,
{
self.renderer = Rc::new(f);
self
}
/// Specifies mime override callback
pub fn mime_override<F>(mut self, f: F) -> Self
where
F: Fn(&mime::Name<'_>) -> DispositionType + 'static,
{
self.mime_override = Some(Rc::new(f));
self
}
/// Set index file
///
/// Shows specific index file for directory "/" instead of
/// showing files listing.
pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
self.index = Some(index.into());
self
}
#[inline]
/// Specifies whether to use ETag or not.
///
/// Default is true.
pub fn use_etag(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::ETAG, value);
self
}
#[inline]
/// Specifies whether to use Last-Modified or not.
///
/// Default is true.
pub fn use_last_modified(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::LAST_MD, value);
self
}
/// 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.
///
/// By default Content-Disposition` header is enabled.
#[inline]
pub fn disable_content_disposition(mut self) -> Self {
self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
self
}
/// Sets default handler which is used when no matched file could be found.
pub fn default_handler<F, U>(mut self, f: F) -> Self
where
F: IntoServiceFactory<U>,
U: ServiceFactory<
Config = (),
Request = ServiceRequest,
Response = ServiceResponse,
Error = Error,
> + 'static,
{
// create and configure default resource
self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory(
f.into_factory().map_init_err(|_| ()),
)))));
self
}
}
impl HttpServiceFactory for Files {
fn register(self, config: &mut AppService) {
if self.default.borrow().is_none() {
*self.default.borrow_mut() = Some(config.default_service());
}
let rdef = if config.is_root() {
ResourceDef::root_prefix(&self.path)
} else {
ResourceDef::prefix(&self.path)
};
config.register_service(rdef, None, self, None)
}
}
impl ServiceFactory for Files {
type Request = ServiceRequest;
type Response = ServiceResponse;
type Error = Error;
type Config = ();
type Service = FilesService;
type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
let mut srv = FilesService {
directory: self.directory.clone(),
index: self.index.clone(),
show_index: self.show_index,
redirect_to_slash: self.redirect_to_slash,
default: None,
renderer: self.renderer.clone(),
mime_override: self.mime_override.clone(),
file_flags: self.file_flags,
guards: self.guards.clone(),
};
if let Some(ref default) = *self.default.borrow() {
default
.new_service(())
.map(move |result| match result {
Ok(default) => {
srv.default = Some(default);
Ok(srv)
}
Err(_) => Err(()),
})
.boxed_local()
} else {
ok(srv).boxed_local()
}
}
}
pub struct FilesService {
directory: PathBuf,
index: Option<String>,
show_index: bool,
redirect_to_slash: bool,
default: Option<HttpService>,
renderer: Rc<DirectoryRenderer>,
mime_override: Option<Rc<MimeOverride>>,
file_flags: named::Flags,
// FIXME: Should re-visit later.
#[allow(clippy::redundant_allocation)]
guards: Option<Rc<Box<dyn Guard>>>,
}
impl FilesService {
#[allow(clippy::type_complexity)]
fn handle_err(
&mut self,
e: io::Error,
req: ServiceRequest,
) -> Either<
Ready<Result<ServiceResponse, Error>>,
LocalBoxFuture<'static, Result<ServiceResponse, Error>>,
> {
log::debug!("Files: Failed to handle {}: {}", req.path(), e);
if let Some(ref mut default) = self.default {
Either::Right(default.call(req))
} else {
Either::Left(ok(req.error_response(e)))
}
}
}
impl Service for FilesService {
type Request = ServiceRequest;
type Response = ServiceResponse;
type Error = Error;
#[allow(clippy::type_complexity)]
type Future = Either<
Ready<Result<Self::Response, Self::Error>>,
LocalBoxFuture<'static, Result<Self::Response, Self::Error>>,
>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
let is_method_valid = if let Some(guard) = &self.guards {
// execute user defined guards
(**guard).check(req.head())
} else {
// default behavior
matches!(*req.method(), Method::HEAD | Method::GET)
};
if !is_method_valid {
return Either::Left(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()) {
Ok(item) => item,
Err(e) => return Either::Left(ok(req.error_response(e))),
};
// full file path
let path = match self.directory.join(&real_path.0).canonicalize() {
Ok(path) => path,
Err(e) => return self.handle_err(e, req),
};
if path.is_dir() {
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::Left(ok(req.into_response(
HttpResponse::Found()
.header(header::LOCATION, redirect_to)
.body("")
.into_body(),
)));
}
let path = path.join(redir_index);
match NamedFile::open(path) {
Ok(mut named_file) => {
if let Some(ref mime_override) = self.mime_override {
let new_disposition =
mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = self.file_flags;
let (req, _) = req.into_parts();
Either::Left(ok(match named_file.into_response(&req) {
Ok(item) => ServiceResponse::new(req, item),
Err(e) => ServiceResponse::from_err(e, req),
}))
}
Err(e) => self.handle_err(e, req),
}
} else if self.show_index {
let dir = Directory::new(self.directory.clone(), path);
let (req, _) = req.into_parts();
let x = (self.renderer)(&dir, &req);
match x {
Ok(resp) => Either::Left(ok(resp)),
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
}
} else {
Either::Left(ok(ServiceResponse::from_err(
FilesError::IsDirectory,
req.into_parts().0,
)))
}
} else {
match NamedFile::open(path) {
Ok(mut named_file) => {
if let Some(ref mime_override) = self.mime_override {
let new_disposition =
mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = self.file_flags;
let (req, _) = req.into_parts();
match named_file.into_response(&req) {
Ok(item) => {
Either::Left(ok(ServiceResponse::new(req.clone(), item)))
}
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
}
}
Err(e) => self.handle_err(e, req),
}
}
}
}
#[derive(Debug)]
struct PathBufWrp(PathBuf);
impl PathBufWrp {
fn get_pathbuf(path: &str) -> Result<Self, UriSegmentError> {
let mut buf = PathBuf::new();
for segment in path.split('/') {
if segment == ".." {
buf.pop();
} else if segment.starts_with('.') {
return Err(UriSegmentError::BadStart('.'));
} else if segment.starts_with('*') {
return Err(UriSegmentError::BadStart('*'));
} else if segment.ends_with(':') {
return Err(UriSegmentError::BadEnd(':'));
} else if segment.ends_with('>') {
return Err(UriSegmentError::BadEnd('>'));
} else if segment.ends_with('<') {
return Err(UriSegmentError::BadEnd('<'));
} else if segment.is_empty() {
continue;
} else if cfg!(windows) && segment.contains('\\') {
return Err(UriSegmentError::BadChar('\\'));
} else {
buf.push(segment)
}
}
Ok(PathBufWrp(buf))
}
}
impl FromRequest for PathBufWrp {
type Error = UriSegmentError;
type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(PathBufWrp::get_pathbuf(req.match_info().path()))
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::iter::FromIterator;
use std::ops::Add;
use std::time::{Duration, SystemTime};
use std::{
fs::{self, File},
ops::Add,
time::{Duration, SystemTime},
};
use actix_service::ServiceFactory;
use actix_web::{
guard,
http::{
header::{self, ContentDisposition, DispositionParam, DispositionType},
Method, StatusCode,
},
middleware::Compress,
test::{self, TestRequest},
web, App, HttpResponse, Responder,
};
use futures_util::future::ok;
use super::*;
use actix_web::guard;
use actix_web::http::header::{
self, ContentDisposition, DispositionParam, DispositionType,
};
use actix_web::http::{Method, StatusCode};
use actix_web::middleware::Compress;
use actix_web::test::{self, TestRequest};
use actix_web::{App, Responder};
#[actix_rt::test]
async fn test_file_extension_to_mime() {
@@ -1013,7 +446,7 @@ mod tests {
// Check file contents
let bytes = response.body().await.unwrap();
let data = Bytes::from(fs::read("tests/test.binary").unwrap());
let data = web::Bytes::from(fs::read("tests/test.binary").unwrap());
assert_eq!(bytes, data);
}
@@ -1046,7 +479,7 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK);
let bytes = test::read_body(response).await;
let data = Bytes::from(fs::read("tests/test space.binary").unwrap());
let data = web::Bytes::from(fs::read("tests/test space.binary").unwrap());
assert_eq!(bytes, data);
}
@@ -1224,7 +657,7 @@ mod tests {
let resp = test::call_service(&mut st, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let bytes = test::read_body(resp).await;
assert_eq!(bytes, Bytes::from_static(b"default content"));
assert_eq!(bytes, web::Bytes::from_static(b"default content"));
}
// #[actix_rt::test]
@@ -1340,36 +773,4 @@ mod tests {
// let response = srv.execute(request.send()).unwrap();
// assert_eq!(response.status(), StatusCode::OK);
// }
#[actix_rt::test]
async fn test_path_buf() {
assert_eq!(
PathBufWrp::get_pathbuf("/test/.tt").map(|t| t.0),
Err(UriSegmentError::BadStart('.'))
);
assert_eq!(
PathBufWrp::get_pathbuf("/test/*tt").map(|t| t.0),
Err(UriSegmentError::BadStart('*'))
);
assert_eq!(
PathBufWrp::get_pathbuf("/test/tt:").map(|t| t.0),
Err(UriSegmentError::BadEnd(':'))
);
assert_eq!(
PathBufWrp::get_pathbuf("/test/tt<").map(|t| t.0),
Err(UriSegmentError::BadEnd('<'))
);
assert_eq!(
PathBufWrp::get_pathbuf("/test/tt>").map(|t| t.0),
Err(UriSegmentError::BadEnd('>'))
);
assert_eq!(
PathBufWrp::get_pathbuf("/seg1/seg2/").unwrap().0,
PathBuf::from_iter(vec!["seg1", "seg2"])
);
assert_eq!(
PathBufWrp::get_pathbuf("/seg1/../seg2/").unwrap().0,
PathBuf::from_iter(vec!["seg2"])
);
}
}

View File

@@ -7,17 +7,20 @@ use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use bitflags::bitflags;
use mime_guess::from_path;
use actix_http::body::SizedStream;
use actix_web::dev::BodyEncoding;
use actix_web::http::header::{
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
use actix_web::{
dev::{BodyEncoding, SizedStream},
http::{
header::{
self, Charset, ContentDisposition, DispositionParam, DispositionType,
ExtendedValue,
},
ContentEncoding, StatusCode,
},
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
};
use actix_web::http::{ContentEncoding, StatusCode};
use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder};
use bitflags::bitflags;
use futures_util::future::{ready, Ready};
use mime_guess::from_path;
use crate::range::HttpRange;
use crate::ChunkedReadFile;
@@ -93,8 +96,10 @@ impl NamedFile {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
_ => 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")),
@@ -102,16 +107,19 @@ impl NamedFile {
value: filename.into_owned().into_bytes(),
}))
}
let cd = ContentDisposition {
disposition,
parameters,
};
(ct, cd)
};
let md = file.metadata()?;
let modified = md.modified().ok();
let encoding = None;
Ok(NamedFile {
path,
file,
@@ -242,6 +250,7 @@ impl NamedFile {
let dur = mtime
.duration_since(UNIX_EPOCH)
.expect("modification time must be after epoch");
header::EntityTag::strong(format!(
"{:x}:{:x}:{:x}:{:x}",
ino,
@@ -256,9 +265,11 @@ impl NamedFile {
self.modified.map(|mtime| mtime.into())
}
/// Creates an `HttpResponse` with file as a streaming body.
pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> {
if self.status_code != StatusCode::OK {
let mut resp = HttpResponse::build(self.status_code);
resp.set(header::ContentType(self.content_type.clone()))
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
res.header(
@@ -266,9 +277,11 @@ impl NamedFile {
self.content_disposition.to_string(),
);
});
if let Some(current_encoding) = self.encoding {
resp.encoding(current_encoding);
}
let reader = ChunkedReadFile {
size: self.md.len(),
offset: 0,
@@ -276,6 +289,7 @@ impl NamedFile {
fut: None,
counter: 0,
};
return Ok(resp.streaming(reader));
}
@@ -284,6 +298,7 @@ impl NamedFile {
} else {
None
};
let last_modified = if self.flags.contains(Flags::LAST_MD) {
self.last_modified()
} else {
@@ -298,6 +313,7 @@ impl NamedFile {
{
let t1: SystemTime = m.clone().into();
let t2: SystemTime = since.clone().into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
(Ok(t1), Ok(t2)) => t1 > t2,
_ => false,
@@ -309,13 +325,14 @@ impl NamedFile {
// check last modified
let not_modified = if !none_match(etag.as_ref(), req) {
true
} else if req.headers().contains_key(&header::IF_NONE_MATCH) {
} 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())
{
let t1: SystemTime = m.clone().into();
let t2: SystemTime = since.clone().into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
(Ok(t1), Ok(t2)) => t1 <= t2,
_ => false,
@@ -332,6 +349,7 @@ impl NamedFile {
self.content_disposition.to_string(),
);
});
// default compressing
if let Some(current_encoding) = self.encoding {
resp.encoding(current_encoding);
@@ -350,11 +368,12 @@ impl NamedFile {
let mut offset = 0;
// check for range header
if let Some(ranges) = req.headers().get(&header::RANGE) {
if let Ok(rangesheader) = ranges.to_str() {
if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) {
length = rangesvec[0].length;
offset = rangesvec[0].start;
if let Some(ranges) = req.headers().get(header::RANGE) {
if let Ok(ranges_header) = ranges.to_str() {
if let Ok(ranges) = HttpRange::parse(ranges_header, length) {
length = ranges[0].length;
offset = ranges[0].start;
resp.encoding(ContentEncoding::Identity);
resp.header(
header::CONTENT_RANGE,
@@ -414,6 +433,7 @@ impl DerefMut for NamedFile {
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
match req.get_header::<header::IfMatch>() {
None | Some(header::IfMatch::Any) => true,
Some(header::IfMatch::Items(ref items)) => {
if let Some(some_etag) = etag {
for item in items {
@@ -422,6 +442,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
}
}
}
false
}
}
@@ -431,6 +452,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
match req.get_header::<header::IfNoneMatch>() {
Some(header::IfNoneMatch::Any) => false,
Some(header::IfNoneMatch::Items(ref items)) => {
if let Some(some_etag) = etag {
for item in items {
@@ -439,8 +461,10 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
}
}
}
true
}
None => true,
}
}

View File

@@ -0,0 +1,99 @@
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use actix_web::{dev::Payload, FromRequest, HttpRequest};
use futures_util::future::{ready, Ready};
use crate::error::UriSegmentError;
#[derive(Debug)]
pub(crate) struct PathBufWrap(PathBuf);
impl FromStr for PathBufWrap {
type Err = UriSegmentError;
fn from_str(path: &str) -> Result<Self, Self::Err> {
let mut buf = PathBuf::new();
for segment in path.split('/') {
if segment == ".." {
buf.pop();
} else if segment.starts_with('.') {
return Err(UriSegmentError::BadStart('.'));
} else if segment.starts_with('*') {
return Err(UriSegmentError::BadStart('*'));
} else if segment.ends_with(':') {
return Err(UriSegmentError::BadEnd(':'));
} else if segment.ends_with('>') {
return Err(UriSegmentError::BadEnd('>'));
} else if segment.ends_with('<') {
return Err(UriSegmentError::BadEnd('<'));
} else if segment.is_empty() {
continue;
} else if cfg!(windows) && segment.contains('\\') {
return Err(UriSegmentError::BadChar('\\'));
} else {
buf.push(segment)
}
}
Ok(PathBufWrap(buf))
}
}
impl AsRef<Path> for PathBufWrap {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
impl FromRequest for PathBufWrap {
type Error = UriSegmentError;
type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(req.match_info().path().parse())
}
}
#[cfg(test)]
mod tests {
use std::iter::FromIterator;
use super::*;
#[test]
fn test_path_buf() {
assert_eq!(
PathBufWrap::from_str("/test/.tt").map(|t| t.0),
Err(UriSegmentError::BadStart('.'))
);
assert_eq!(
PathBufWrap::from_str("/test/*tt").map(|t| t.0),
Err(UriSegmentError::BadStart('*'))
);
assert_eq!(
PathBufWrap::from_str("/test/tt:").map(|t| t.0),
Err(UriSegmentError::BadEnd(':'))
);
assert_eq!(
PathBufWrap::from_str("/test/tt<").map(|t| t.0),
Err(UriSegmentError::BadEnd('<'))
);
assert_eq!(
PathBufWrap::from_str("/test/tt>").map(|t| t.0),
Err(UriSegmentError::BadEnd('>'))
);
assert_eq!(
PathBufWrap::from_str("/seg1/seg2/").unwrap().0,
PathBuf::from_iter(vec!["seg1", "seg2"])
);
assert_eq!(
PathBufWrap::from_str("/seg1/../seg2/").unwrap().0,
PathBuf::from_iter(vec!["seg2"])
);
}
}

View File

@@ -1,11 +1,14 @@
/// HTTP Range header representation.
#[derive(Debug, Clone, Copy)]
pub struct HttpRange {
/// Start of range.
pub start: u64,
/// Length of range.
pub length: u64,
}
static PREFIX: &str = "bytes=";
const PREFIX: &str = "bytes=";
const PREFIX_LEN: usize = 6;
impl HttpRange {

167
actix-files/src/service.rs Normal file
View File

@@ -0,0 +1,167 @@
use std::{
fmt, io,
path::PathBuf,
rc::Rc,
task::{Context, Poll},
};
use actix_service::Service;
use actix_web::{
dev::{ServiceRequest, ServiceResponse},
error::Error,
guard::Guard,
http::{header, Method},
HttpResponse,
};
use futures_util::future::{ok, Either, LocalBoxFuture, Ready};
use crate::{
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride,
NamedFile, PathBufWrap,
};
/// Assembled file serving service.
pub struct FilesService {
pub(crate) directory: PathBuf,
pub(crate) index: Option<String>,
pub(crate) show_index: bool,
pub(crate) redirect_to_slash: bool,
pub(crate) default: Option<HttpService>,
pub(crate) renderer: Rc<DirectoryRenderer>,
pub(crate) mime_override: Option<Rc<MimeOverride>>,
pub(crate) file_flags: named::Flags,
pub(crate) guards: Option<Rc<dyn Guard>>,
}
type FilesServiceFuture = Either<
Ready<Result<ServiceResponse, Error>>,
LocalBoxFuture<'static, Result<ServiceResponse, Error>>,
>;
impl FilesService {
fn handle_err(&mut self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture {
log::debug!("Failed to handle {}: {}", req.path(), e);
if let Some(ref mut default) = self.default {
Either::Right(default.call(req))
} else {
Either::Left(ok(req.error_response(e)))
}
}
}
impl fmt::Debug for FilesService {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("FilesService")
}
}
impl Service for FilesService {
type Request = ServiceRequest;
type Response = ServiceResponse;
type Error = Error;
type Future = FilesServiceFuture;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
let is_method_valid = if let Some(guard) = &self.guards {
// execute user defined guards
(**guard).check(req.head())
} else {
// default behavior
matches!(*req.method(), Method::HEAD | Method::GET)
};
if !is_method_valid {
return Either::Left(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: PathBufWrap = match req.match_info().path().parse() {
Ok(item) => item,
Err(e) => return Either::Left(ok(req.error_response(e))),
};
// full file path
let path = match self.directory.join(&real_path).canonicalize() {
Ok(path) => path,
Err(e) => return self.handle_err(e, req),
};
if path.is_dir() {
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::Left(ok(req.into_response(
HttpResponse::Found()
.header(header::LOCATION, redirect_to)
.body("")
.into_body(),
)));
}
let path = path.join(redir_index);
match NamedFile::open(path) {
Ok(mut named_file) => {
if let Some(ref mime_override) = self.mime_override {
let new_disposition =
mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = self.file_flags;
let (req, _) = req.into_parts();
Either::Left(ok(match named_file.into_response(&req) {
Ok(item) => ServiceResponse::new(req, item),
Err(e) => ServiceResponse::from_err(e, req),
}))
}
Err(e) => self.handle_err(e, req),
}
} else if self.show_index {
let dir = Directory::new(self.directory.clone(), path);
let (req, _) = req.into_parts();
let x = (self.renderer)(&dir, &req);
match x {
Ok(resp) => Either::Left(ok(resp)),
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
}
} else {
Either::Left(ok(ServiceResponse::from_err(
FilesError::IsDirectory,
req.into_parts().0,
)))
}
} else {
match NamedFile::open(path) {
Ok(mut named_file) => {
if let Some(ref mime_override) = self.mime_override {
let new_disposition =
mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = self.file_flags;
let (req, _) = req.into_parts();
match named_file.into_response(&req) {
Ok(item) => {
Either::Left(ok(ServiceResponse::new(req.clone(), item)))
}
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
}
}
Err(e) => self.handle_err(e, req),
}
}
}
}

View File

@@ -13,12 +13,11 @@ Actix http
## Example
```rust
// see examples/framed_hello.rs for complete list of used crates.
use std::{env, io};
use actix_http::{HttpService, Response};
use actix_server::Server;
use futures::future;
use futures_util::future;
use http::header::HeaderValue;
use log::info;

View File

@@ -1,6 +1,7 @@
# Changes
## Unreleased - 2020-xx-xx
* Fix multipart consuming payload before header checks #1513
## 3.0.0 - 2020-09-11

View File

@@ -36,6 +36,9 @@ impl FromRequest for Multipart {
#[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
ok(Multipart::new(req.headers(), payload.take()))
ok(match Multipart::boundary(req.headers()) {
Ok(boundary) => Multipart::from_boundary(boundary, payload.take()),
Err(err) => Multipart::from_error(err),
})
}
}

View File

@@ -64,26 +64,13 @@ impl Multipart {
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin + 'static,
{
match Self::boundary(headers) {
Ok(boundary) => Multipart {
error: None,
safety: Safety::new(),
inner: Some(Rc::new(RefCell::new(InnerMultipart {
boundary,
payload: PayloadRef::new(PayloadBuffer::new(Box::new(stream))),
state: InnerState::FirstBoundary,
item: InnerMultipartItem::None,
}))),
},
Err(err) => Multipart {
error: Some(err),
safety: Safety::new(),
inner: None,
},
Ok(boundary) => Multipart::from_boundary(boundary, stream),
Err(err) => Multipart::from_error(err),
}
}
/// Extract boundary info from headers.
fn boundary(headers: &HeaderMap) -> Result<String, MultipartError> {
pub(crate) fn boundary(headers: &HeaderMap) -> Result<String, MultipartError> {
if let Some(content_type) = headers.get(&header::CONTENT_TYPE) {
if let Ok(content_type) = content_type.to_str() {
if let Ok(ct) = content_type.parse::<mime::Mime>() {
@@ -102,6 +89,32 @@ impl Multipart {
Err(MultipartError::NoContentType)
}
}
/// Create multipart instance for given boundary and stream
pub(crate) fn from_boundary<S>(boundary: String, stream: S) -> Multipart
where
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin + 'static,
{
Multipart {
error: None,
safety: Safety::new(),
inner: Some(Rc::new(RefCell::new(InnerMultipart {
boundary,
payload: PayloadRef::new(PayloadBuffer::new(Box::new(stream))),
state: InnerState::FirstBoundary,
item: InnerMultipartItem::None,
}))),
}
}
/// Create Multipart instance from MultipartError
pub(crate) fn from_error(err: MultipartError) -> Multipart {
Multipart {
error: Some(err),
safety: Safety::new(),
inner: None,
}
}
}
impl Stream for Multipart {
@@ -815,6 +828,8 @@ mod tests {
use actix_http::h1::Payload;
use actix_utils::mpsc;
use actix_web::http::header::{DispositionParam, DispositionType};
use actix_web::test::TestRequest;
use actix_web::FromRequest;
use bytes::Bytes;
use futures_util::future::lazy;
@@ -1151,4 +1166,38 @@ mod tests {
);
assert_eq!(payload.buf.len(), 0);
}
#[actix_rt::test]
async fn test_multipart_from_error() {
let err = MultipartError::NoContentType;
let mut multipart = Multipart::from_error(err);
assert!(multipart.next().await.unwrap().is_err())
}
#[actix_rt::test]
async fn test_multipart_from_boundary() {
let (_, payload) = create_stream();
let (_, headers) = create_simple_request_with_header();
let boundary = Multipart::boundary(&headers);
assert!(boundary.is_ok());
let _ = Multipart::from_boundary(boundary.unwrap(), payload);
}
#[actix_rt::test]
async fn test_multipart_payload_consumption() {
// with sample payload and HttpRequest with no headers
let (_, inner_payload) = Payload::create(false);
let mut payload = actix_web::dev::Payload::from(inner_payload);
let req = TestRequest::default().to_http_request();
// multipart should generate an error
let mut mp = Multipart::from_request(&req, &mut payload).await.unwrap();
assert!(mp.next().await.unwrap().is_err());
// and should not consume the payload
match payload {
actix_web::dev::Payload::H1(_) => {} //expected
_ => unreachable!(),
}
}
}

View File

@@ -3,6 +3,14 @@
## Unreleased - 2020-xx-xx
## 0.4.0 - 2020-09-20
* Added compile success and failure testing. [#1677]
* Add `route` macro for supporting multiple HTTP methods guards. [#1674]
[#1677]: https://github.com/actix/actix-web/pull/1677
[#1674]: https://github.com/actix/actix-web/pull/1674
## 0.3.0 - 2020-09-11
* No significant changes from `0.3.0-beta.1`.
@@ -13,47 +21,48 @@
[#1559]: https://github.com/actix/actix-web/pull/1559
## [0.2.2] - 2020-05-23
## 0.2.2 - 2020-05-23
* Add resource middleware on actix-web-codegen [#1467]
[#1467]: https://github.com/actix/actix-web/pull/1467
## [0.2.1] - 2020-02-25
## 0.2.1 - 2020-02-25
* Add `#[allow(missing_docs)]` attribute to generated structs [#1368]
* Allow the handler function to be named as `config` [#1290]
[#1368]: https://github.com/actix/actix-web/issues/1368
[#1290]: https://github.com/actix/actix-web/issues/1290
## [0.2.0] - 2019-12-13
## 0.2.0 - 2019-12-13
* Generate code for actix-web 2.0
## [0.1.3] - 2019-10-14
## 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
## [0.1.1] - 2019-06-01
## 0.1.1 - 2019-06-01
* Add syn "extra-traits" feature
## [0.1.0] - 2019-05-18
## 0.1.0 - 2019-05-18
* Release
## [0.1.0-beta.1] - 2019-04-20
## 0.1.0-beta.1 - 2019-04-20
* Gen code for actix-web 1.0.0-beta.1
## [0.1.0-alpha.6] - 2019-04-14
## 0.1.0-alpha.6 - 2019-04-14
* Gen code for actix-web 1.0.0-alpha.6
## [0.1.0-alpha.1] - 2019-03-28
## 0.1.0-alpha.1 - 2019-03-28
* Initial impl

View File

@@ -1,6 +1,6 @@
[package]
name = "actix-web-codegen"
version = "0.3.0"
version = "0.4.0"
description = "Actix web proc macros"
readme = "README.md"
homepage = "https://actix.rs"
@@ -19,6 +19,8 @@ syn = { version = "1", features = ["full", "parsing"] }
proc-macro2 = "1"
[dev-dependencies]
actix-rt = "1.0.0"
actix-rt = "1.1.1"
actix-web = "3.0.0"
futures-util = { version = "0.3.5", default-features = false }
trybuild = "1"
rustversion = "1"

View File

@@ -1,8 +1,22 @@
# Helper and convenience macros for Actix-web. [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web-codegen)](https://crates.io/crates/actix-web-codegen) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
# actix-web-codegen
> Helper and convenience macros for Actix Web
[![crates.io](https://meritbadge.herokuapp.com/actix-web-codegen)](https://crates.io/crates/actix-web-codegen)
[![Documentation](https://docs.rs/actix-web-codegen/badge.svg)](https://docs.rs/actix-web)
[![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
[![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources
* [API Documentation](https://docs.rs/actix-web-codegen/)
* [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen)
* Minimum supported Rust version: 1.40 or later
- [API Documentation](https://docs.rs/actix-web-codegen)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen)
- Minimum supported Rust version: 1.42 or later.
## Compile Testing
Uses the [`trybuild`] crate. All compile fail tests should include a stderr file generated by `trybuild`. See the [workflow section](https://github.com/dtolnay/trybuild#workflow) of the trybuild docs for info on how to do this.
[`trybuild`]: https://github.com/dtolnay/trybuild

View File

@@ -1,158 +1,174 @@
#![recursion_limit = "512"]
//! Helper and convenience macros for Actix-web.
//! Macros for reducing boilerplate code in Actix Web applications.
//!
//! ## Runtime Setup
//! ## Actix Web Re-exports
//! Actix Web re-exports a version of this crate in it's entirety so you usually don't have to
//! specify a dependency on this crate explicitly. Sometimes, however, updates are made to this
//! crate before the actix-web dependency is updated. Therefore, code examples here will show
//! explicit imports. Check the latest [actix-web attributes docs] to see which macros
//! are re-exported.
//!
//! - [main](attr.main.html)
//!
//! ## Resource Macros:
//!
//! - [get](attr.get.html)
//! - [post](attr.post.html)
//! - [put](attr.put.html)
//! - [delete](attr.delete.html)
//! - [head](attr.head.html)
//! - [connect](attr.connect.html)
//! - [options](attr.options.html)
//! - [trace](attr.trace.html)
//! - [patch](attr.patch.html)
//!
//! ### Attributes:
//!
//! - `"path"` - Raw literal string with path for which to register handle. Mandatory.
//! - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`
//! - `wrap="Middleware"` - Registers a resource middleware.
//!
//! ### Notes
//!
//! Function name can be specified as any expression that is going to be accessible to the generate
//! code (e.g `my_guard` or `my_module::my_guard`)
//!
//! ### Example:
//! # Runtime Setup
//! Used for setting up the actix async runtime. See [main] macro docs.
//!
//! ```rust
//! use actix_web::HttpResponse;
//! use actix_web_codegen::get;
//!
//! #[get("/test")]
//! async fn async_test() -> Result<HttpResponse, actix_web::Error> {
//! Ok(HttpResponse::Ok().finish())
//! #[actix_web_codegen::main] // or `#[actix_web::main]` in Actix Web apps
//! async fn main() {
//! async { println!("Hello world"); }.await
//! }
//! ```
//!
//! # Single Method Handler
//! There is a macro to set up a handler for each of the most common HTTP methods that also define
//! additional guards and route-specific middleware.
//!
//! See docs for: [GET], [POST], [PATCH], [PUT], [DELETE], [HEAD], [CONNECT], [OPTIONS], [TRACE]
//!
//! ```rust
//! # use actix_web::HttpResponse;
//! # use actix_web_codegen::get;
//! #[get("/test")]
//! async fn get_handler() -> HttpResponse {
//! HttpResponse::Ok().finish()
//! }
//! ```
//!
//! # Multiple Method Handlers
//! Similar to the single method handler macro but takes one or more arguments for the HTTP methods
//! it should respond to. See [route] macro docs.
//!
//! ```rust
//! # use actix_web::HttpResponse;
//! # use actix_web_codegen::route;
//! #[route("/test", method="GET", method="HEAD")]
//! async fn get_and_head_handler() -> HttpResponse {
//! HttpResponse::Ok().finish()
//! }
//! ```
//!
//! [actix-web attributes docs]: https://docs.rs/actix-web/*/actix_web/#attributes
//! [main]: attr.main.html
//! [route]: attr.route.html
//! [GET]: attr.get.html
//! [POST]: attr.post.html
//! [PUT]: attr.put.html
//! [DELETE]: attr.delete.html
//! [HEAD]: attr.head.html
//! [CONNECT]: attr.connect.html
//! [OPTIONS]: attr.options.html
//! [TRACE]: attr.trace.html
//! [PATCH]: attr.patch.html
extern crate proc_macro;
mod route;
#![recursion_limit = "512"]
use proc_macro::TokenStream;
/// Creates route handler with `GET` method guard.
mod route;
/// Creates resource handler, allowing multiple HTTP method guards.
///
/// Syntax: `#[get("path"[, attributes])]`
/// # Syntax
/// ```text
/// #[route("path", method="HTTP_METHOD"[, attributes])]
/// ```
///
/// ## Attributes:
///
/// - `"path"` - Raw literal string with path for which to register handler. Mandatory.
/// # Attributes
/// - `"path"` - Raw literal string with path for which to register handler.
/// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. Upper-case string, "GET", "POST" for example.
/// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`
/// - `wrap="Middleware"` - Registers a resource middleware.
///
/// # Notes
/// Function name can be specified as any expression that is going to be accessible to the generate
/// code, e.g `my_guard` or `my_module::my_guard`.
///
/// # Example
///
/// ```rust
/// # use actix_web::HttpResponse;
/// # use actix_web_codegen::route;
/// #[route("/test", method="GET", method="HEAD")]
/// async fn example() -> HttpResponse {
/// HttpResponse::Ok().finish()
/// }
/// ```
#[proc_macro_attribute]
pub fn get(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Get)
pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(None, args, input)
}
/// Creates route handler with `POST` method guard.
///
/// Syntax: `#[post("path"[, attributes])]`
///
/// Attributes are the same as in [get](attr.get.html)
#[proc_macro_attribute]
pub fn post(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Post)
macro_rules! doc_comment {
($x:expr; $($tt:tt)*) => {
#[doc = $x]
$($tt)*
};
}
/// Creates route handler with `PUT` method guard.
///
/// Syntax: `#[put("path"[, attributes])]`
///
/// Attributes are the same as in [get](attr.get.html)
#[proc_macro_attribute]
pub fn put(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Put)
macro_rules! method_macro {
(
$($variant:ident, $method:ident,)+
) => {
$(doc_comment! {
concat!("
Creates route handler with `actix_web::guard::", stringify!($variant), "`.
# Syntax
```text
#[", stringify!($method), r#"("path"[, attributes])]
```
# Attributes
- `"path"` - Raw literal string with path for which to register handler.
- `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`.
- `wrap="Middleware"` - Registers a resource middleware.
# Notes
Function name can be specified as any expression that is going to be accessible to the generate
code, e.g `my_guard` or `my_module::my_guard`.
# Example
```rust
# use actix_web::HttpResponse;
# use actix_web_codegen::"#, stringify!($method), ";
#[", stringify!($method), r#"("/")]
async fn example() -> HttpResponse {
HttpResponse::Ok().finish()
}
```
"#);
#[proc_macro_attribute]
pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(Some(route::MethodType::$variant), args, input)
}
})+
};
}
/// Creates route handler with `DELETE` method guard.
///
/// Syntax: `#[delete("path"[, attributes])]`
///
/// Attributes are the same as in [get](attr.get.html)
#[proc_macro_attribute]
pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Delete)
}
/// Creates route handler with `HEAD` method guard.
///
/// Syntax: `#[head("path"[, attributes])]`
///
/// Attributes are the same as in [head](attr.head.html)
#[proc_macro_attribute]
pub fn head(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Head)
}
/// Creates route handler with `CONNECT` method guard.
///
/// Syntax: `#[connect("path"[, attributes])]`
///
/// Attributes are the same as in [connect](attr.connect.html)
#[proc_macro_attribute]
pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Connect)
}
/// Creates route handler with `OPTIONS` method guard.
///
/// Syntax: `#[options("path"[, attributes])]`
///
/// Attributes are the same as in [options](attr.options.html)
#[proc_macro_attribute]
pub fn options(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Options)
}
/// Creates route handler with `TRACE` method guard.
///
/// Syntax: `#[trace("path"[, attributes])]`
///
/// Attributes are the same as in [trace](attr.trace.html)
#[proc_macro_attribute]
pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Trace)
}
/// Creates route handler with `PATCH` method guard.
///
/// Syntax: `#[patch("path"[, attributes])]`
///
/// Attributes are the same as in [patch](attr.patch.html)
#[proc_macro_attribute]
pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Patch)
method_macro! {
Get, get,
Post, post,
Put, put,
Delete, delete,
Head, head,
Connect, connect,
Options, options,
Trace, trace,
Patch, patch,
}
/// Marks async main function as the actix system entry-point.
///
/// ## Usage
/// # Actix Web Re-export
/// This macro can be applied with `#[actix_web::main]` when used in Actix Web applications.
///
/// # Usage
/// ```rust
/// #[actix_web::main]
/// #[actix_web_codegen::main]
/// async fn main() {
/// async { println!("Hello world"); }.await
/// }
/// ```
#[proc_macro_attribute]
#[cfg(not(test))] // Work around for rust-lang/rust#62127
pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
use quote::quote;

View File

@@ -1,5 +1,8 @@
extern crate proc_macro;
use std::collections::HashSet;
use std::convert::TryFrom;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
@@ -17,53 +20,81 @@ impl ToTokens for ResourceType {
}
}
#[derive(PartialEq)]
pub enum GuardType {
Get,
Post,
Put,
Delete,
Head,
Connect,
Options,
Trace,
Patch,
}
impl GuardType {
fn as_str(&self) -> &'static str {
match self {
GuardType::Get => "Get",
GuardType::Post => "Post",
GuardType::Put => "Put",
GuardType::Delete => "Delete",
GuardType::Head => "Head",
GuardType::Connect => "Connect",
GuardType::Options => "Options",
GuardType::Trace => "Trace",
GuardType::Patch => "Patch",
macro_rules! method_type {
(
$($variant:ident, $upper:ident,)+
) => {
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum MethodType {
$(
$variant,
)+
}
}
impl MethodType {
fn as_str(&self) -> &'static str {
match self {
$(Self::$variant => stringify!($variant),)+
}
}
fn parse(method: &str) -> Result<Self, String> {
match method {
$(stringify!($upper) => Ok(Self::$variant),)+
_ => Err(format!("Unexpected HTTP method: `{}`", method)),
}
}
}
};
}
impl ToTokens for GuardType {
method_type! {
Get, GET,
Post, POST,
Put, PUT,
Delete, DELETE,
Head, HEAD,
Connect, CONNECT,
Options, OPTIONS,
Trace, TRACE,
Patch, PATCH,
}
impl ToTokens for MethodType {
fn to_tokens(&self, stream: &mut TokenStream2) {
let ident = Ident::new(self.as_str(), Span::call_site());
stream.append(ident);
}
}
impl TryFrom<&syn::LitStr> for MethodType {
type Error = syn::Error;
fn try_from(value: &syn::LitStr) -> Result<Self, Self::Error> {
Self::parse(value.value().as_str())
.map_err(|message| syn::Error::new_spanned(value, message))
}
}
struct Args {
path: syn::LitStr,
guards: Vec<Ident>,
wrappers: Vec<syn::Type>,
methods: HashSet<MethodType>,
}
impl Args {
fn new(args: AttributeArgs) -> syn::Result<Self> {
fn new(args: AttributeArgs, method: Option<MethodType>) -> syn::Result<Self> {
let mut path = None;
let mut guards = Vec::new();
let mut wrappers = Vec::new();
let mut methods = HashSet::new();
let is_route_macro = method.is_none();
if let Some(method) = method {
methods.insert(method);
}
for arg in args {
match arg {
NestedMeta::Lit(syn::Lit::Str(lit)) => match path {
@@ -96,10 +127,33 @@ impl Args {
"Attribute wrap expects type",
));
}
} else if nv.path.is_ident("method") {
if !is_route_macro {
return Err(syn::Error::new_spanned(
&nv,
"HTTP method forbidden here. To handle multiple methods, use `route` instead",
));
} else if let syn::Lit::Str(ref lit) = nv.lit {
let method = MethodType::try_from(lit)?;
if !methods.insert(method) {
return Err(syn::Error::new_spanned(
&nv.lit,
&format!(
"HTTP method defined more than once: `{}`",
lit.value()
),
));
}
} else {
return Err(syn::Error::new_spanned(
nv.lit,
"Attribute method expects literal string!",
));
}
} else {
return Err(syn::Error::new_spanned(
nv.path,
"Unknown attribute key is specified. Allowed: guard and wrap",
"Unknown attribute key is specified. Allowed: guard, method and wrap",
));
}
}
@@ -112,6 +166,7 @@ impl Args {
path: path.unwrap(),
guards,
wrappers,
methods,
})
}
}
@@ -121,7 +176,6 @@ pub struct Route {
args: Args,
ast: syn::ItemFn,
resource_type: ResourceType,
guard: GuardType,
}
fn guess_resource_type(typ: &syn::Type) -> ResourceType {
@@ -150,21 +204,30 @@ impl Route {
pub fn new(
args: AttributeArgs,
input: TokenStream,
guard: GuardType,
method: Option<MethodType>,
) -> syn::Result<Self> {
if args.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
format!(
r#"invalid server definition, expected #[{}("<some path>")]"#,
guard.as_str().to_ascii_lowercase()
r#"invalid service definition, expected #[{}("<some path>")]"#,
method
.map(|it| it.as_str())
.unwrap_or("route")
.to_ascii_lowercase()
),
));
}
let ast: syn::ItemFn = syn::parse(input)?;
let name = ast.sig.ident.clone();
let args = Args::new(args)?;
let args = Args::new(args, method)?;
if args.methods.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
"The #[route(..)] macro requires at least one `method` attribute",
));
}
let resource_type = if ast.sig.asyncness.is_some() {
ResourceType::Async
@@ -185,7 +248,6 @@ impl Route {
args,
ast,
resource_type,
guard,
})
}
}
@@ -194,17 +256,36 @@ impl ToTokens for Route {
fn to_tokens(&self, output: &mut TokenStream2) {
let Self {
name,
guard,
ast,
args:
Args {
path,
guards,
wrappers,
methods,
},
resource_type,
} = self;
let resource_name = name.to_string();
let method_guards = {
let mut others = methods.iter();
// unwrapping since length is checked to be at least one
let first = others.next().unwrap();
if methods.len() > 1 {
quote! {
.guard(
actix_web::guard::Any(actix_web::guard::#first())
#(.or(actix_web::guard::#others()))*
)
}
} else {
quote! {
.guard(actix_web::guard::#first())
}
}
};
let stream = quote! {
#[allow(non_camel_case_types, missing_docs)]
pub struct #name;
@@ -214,7 +295,7 @@ impl ToTokens for Route {
#ast
let __resource = actix_web::Resource::new(#path)
.name(#resource_name)
.guard(actix_web::guard::#guard())
#method_guards
#(.guard(actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))*
.#resource_type(#name);
@@ -228,13 +309,13 @@ impl ToTokens for Route {
}
}
pub(crate) fn generate(
pub(crate) fn with_method(
method: Option<MethodType>,
args: TokenStream,
input: TokenStream,
guard: GuardType,
) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs);
match Route::new(args, input, guard) {
match Route::new(args, input, method) {
Ok(route) => route.into_token_stream().into(),
Err(err) => err.to_compile_error().into(),
}

View File

@@ -5,7 +5,9 @@ use std::task::{Context, Poll};
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::http::header::{HeaderName, HeaderValue};
use actix_web::{http, test, web::Path, App, Error, HttpResponse, Responder};
use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, trace};
use actix_web_codegen::{
connect, delete, get, head, options, patch, post, put, route, trace,
};
use futures_util::future;
// Make sure that we can name function as 'config'
@@ -79,6 +81,11 @@ async fn get_param_test(_: Path<String>) -> impl Responder {
HttpResponse::Ok()
}
#[route("/multi", method = "GET", method = "POST", method = "HEAD")]
async fn route_test() -> impl Responder {
HttpResponse::Ok()
}
pub struct ChangeStatusCode;
impl<S, B> Transform<S> for ChangeStatusCode
@@ -172,6 +179,7 @@ async fn test_body() {
.service(trace_test)
.service(patch_test)
.service(test_handler)
.service(route_test)
});
let request = srv.request(http::Method::GET, srv.url("/test"));
let response = request.send().await.unwrap();
@@ -210,6 +218,22 @@ async fn test_body() {
let request = srv.request(http::Method::GET, srv.url("/test"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/multi"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::POST, srv.url("/multi"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::HEAD, srv.url("/multi"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::PATCH, srv.url("/multi"));
let response = request.send().await.unwrap();
assert!(!response.status().is_success());
}
#[actix_rt::test]

View File

@@ -0,0 +1,27 @@
#[test]
fn compile_macros() {
let t = trybuild::TestCases::new();
t.pass("tests/trybuild/simple.rs");
t.compile_fail("tests/trybuild/simple-fail.rs");
t.pass("tests/trybuild/route-ok.rs");
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
test_route_missing_method(&t)
}
#[rustversion::stable(1.42)]
fn test_route_missing_method(t: &trybuild::TestCases) {
t.compile_fail("tests/trybuild/route-missing-method-fail-msrv.rs");
}
#[rustversion::not(stable(1.42))]
#[rustversion::not(nightly)]
fn test_route_missing_method(t: &trybuild::TestCases) {
t.compile_fail("tests/trybuild/route-missing-method-fail.rs");
}
#[rustversion::nightly]
fn test_route_missing_method(_t: &trybuild::TestCases) {}

View File

@@ -0,0 +1,17 @@
use actix_web_codegen::*;
#[route("/", method="GET", method="GET")]
async fn index() -> String {
"Hello World!".to_owned()
}
#[actix_web::main]
async fn main() {
use actix_web::{App, test};
let srv = test::start(|| App::new().service(index));
let request = srv.get("/");
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@@ -0,0 +1,11 @@
error: HTTP method defined more than once: `GET`
--> $DIR/route-duplicate-method-fail.rs:3:35
|
3 | #[route("/", method="GET", method="GET")]
| ^^^^^
error[E0425]: cannot find value `index` in this scope
--> $DIR/route-duplicate-method-fail.rs:12:49
|
12 | let srv = test::start(|| App::new().service(index));
| ^^^^^ not found in this scope

View File

@@ -0,0 +1 @@
route-missing-method-fail.rs

View File

@@ -0,0 +1,11 @@
error: The #[route(..)] macro requires at least one `method` attribute
--> $DIR/route-missing-method-fail-msrv.rs:3:1
|
3 | #[route("/")]
| ^^^^^^^^^^^^^
error[E0425]: cannot find value `index` in this scope
--> $DIR/route-missing-method-fail-msrv.rs:12:49
|
12 | let srv = test::start(|| App::new().service(index));
| ^^^^^ not found in this scope

View File

@@ -0,0 +1,17 @@
use actix_web_codegen::*;
#[route("/")]
async fn index() -> String {
"Hello World!".to_owned()
}
#[actix_web::main]
async fn main() {
use actix_web::{App, test};
let srv = test::start(|| App::new().service(index));
let request = srv.get("/");
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@@ -0,0 +1,13 @@
error: The #[route(..)] macro requires at least one `method` attribute
--> $DIR/route-missing-method-fail.rs:3:1
|
3 | #[route("/")]
| ^^^^^^^^^^^^^
|
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0425]: cannot find value `index` in this scope
--> $DIR/route-missing-method-fail.rs:12:49
|
12 | let srv = test::start(|| App::new().service(index));
| ^^^^^ not found in this scope

View File

@@ -0,0 +1,17 @@
use actix_web_codegen::*;
#[route("/", method="GET", method="HEAD")]
async fn index() -> String {
"Hello World!".to_owned()
}
#[actix_web::main]
async fn main() {
use actix_web::{App, test};
let srv = test::start(|| App::new().service(index));
let request = srv.get("/");
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@@ -0,0 +1,17 @@
use actix_web_codegen::*;
#[route("/", method="UNEXPECTED")]
async fn index() -> String {
"Hello World!".to_owned()
}
#[actix_web::main]
async fn main() {
use actix_web::{App, test};
let srv = test::start(|| App::new().service(index));
let request = srv.get("/");
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@@ -0,0 +1,11 @@
error: Unexpected HTTP method: `UNEXPECTED`
--> $DIR/route-unexpected-method-fail.rs:3:21
|
3 | #[route("/", method="UNEXPECTED")]
| ^^^^^^^^^^^^
error[E0425]: cannot find value `index` in this scope
--> $DIR/route-unexpected-method-fail.rs:12:49
|
12 | let srv = test::start(|| App::new().service(index));
| ^^^^^ not found in this scope

View File

@@ -0,0 +1,30 @@
use actix_web_codegen::*;
#[get("/one", other)]
async fn one() -> String {
"Hello World!".to_owned()
}
#[post(/two)]
async fn two() -> String {
"Hello World!".to_owned()
}
static PATCH_PATH: &str = "/three";
#[patch(PATCH_PATH)]
async fn three() -> String {
"Hello World!".to_owned()
}
#[delete("/four", "/five")]
async fn four() -> String {
"Hello World!".to_owned()
}
#[delete("/five", method="GET")]
async fn five() -> String {
"Hello World!".to_owned()
}
fn main() {}

View File

@@ -0,0 +1,29 @@
error: Unknown attribute.
--> $DIR/simple-fail.rs:3:15
|
3 | #[get("/one", other)]
| ^^^^^
error: expected identifier or literal
--> $DIR/simple-fail.rs:8:8
|
8 | #[post(/two)]
| ^
error: Unknown attribute.
--> $DIR/simple-fail.rs:15:9
|
15 | #[patch(PATCH_PATH)]
| ^^^^^^^^^^
error: Multiple paths specified! Should be only one!
--> $DIR/simple-fail.rs:20:19
|
20 | #[delete("/four", "/five")]
| ^^^^^^^
error: HTTP method forbidden here. To handle multiple methods, use `route` instead
--> $DIR/simple-fail.rs:25:19
|
25 | #[delete("/five", method="GET")]
| ^^^^^^^^^^^^

View File

@@ -0,0 +1,16 @@
use actix_web::{Responder, HttpResponse, App, test};
use actix_web_codegen::*;
#[get("/config")]
async fn config() -> impl Responder {
HttpResponse::Ok()
}
#[actix_web::main]
async fn main() {
let srv = test::start(|| App::new().service(config));
let request = srv.get("/config");
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@@ -66,7 +66,7 @@ pub(crate) type FnDataFactory =
/// }
/// ```
#[derive(Debug)]
pub struct Data<T>(Arc<T>);
pub struct Data<T: ?Sized>(Arc<T>);
impl<T> Data<T> {
/// Create new `Data` instance.
@@ -89,7 +89,7 @@ impl<T> Data<T> {
}
}
impl<T> Deref for Data<T> {
impl<T: ?Sized> Deref for Data<T> {
type Target = Arc<T>;
fn deref(&self) -> &Arc<T> {
@@ -97,19 +97,19 @@ impl<T> Deref for Data<T> {
}
}
impl<T> Clone for Data<T> {
impl<T: ?Sized> Clone for Data<T> {
fn clone(&self) -> Data<T> {
Data(self.0.clone())
}
}
impl<T> From<Arc<T>> for Data<T> {
impl<T: ?Sized> From<Arc<T>> for Data<T> {
fn from(arc: Arc<T>) -> Self {
Data(arc)
}
}
impl<T: 'static> FromRequest for Data<T> {
impl<T: ?Sized + 'static> FromRequest for Data<T> {
type Config = ();
type Error = Error;
type Future = Ready<Result<Self, Error>>;
@@ -131,7 +131,7 @@ impl<T: 'static> FromRequest for Data<T> {
}
}
impl<T: 'static> DataFactory for Data<T> {
impl<T: ?Sized + 'static> DataFactory for Data<T> {
fn create(&self, extensions: &mut Extensions) -> bool {
if !extensions.contains::<Data<T>>() {
extensions.insert(Data(self.0.clone()));
@@ -293,4 +293,24 @@ mod tests {
let data_from_arc = Data::from(Arc::new(String::from("test-123")));
assert_eq!(data_new.0, data_from_arc.0)
}
#[actix_rt::test]
async fn test_data_from_dyn_arc() {
trait TestTrait {
fn get_num(&self) -> i32;
}
struct A {}
impl TestTrait for A {
fn get_num(&self) -> i32 {
42
}
}
// This works when Sized is required
let dyn_arc_box: Arc<Box<dyn TestTrait>> = Arc::new(Box::new(A {}));
let data_arc_box = Data::from(dyn_arc_box);
// This works when Data Sized Bound is removed
let dyn_arc: Arc<dyn TestTrait> = Arc::new(A {});
let data_arc = Data::from(dyn_arc);
assert_eq!(data_arc_box.get_num(), data_arc.get_num())
}
}

View File

@@ -1,6 +1,3 @@
#![deny(rust_2018_idioms)]
#![allow(clippy::needless_doctest_main, clippy::type_complexity)]
//! Actix web is a powerful, pragmatic, and extremely fast web framework for Rust.
//!
//! ## Example
@@ -68,6 +65,11 @@
//! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2`
//! * `secure-cookies` - secure cookies support
#![deny(rust_2018_idioms)]
#![allow(clippy::needless_doctest_main, clippy::type_complexity)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
mod app;
mod app_service;
mod config;

View File

@@ -9,7 +9,7 @@ mod condition;
mod defaultheaders;
pub mod errhandlers;
mod logger;
mod normalize;
pub mod normalize;
pub use self::condition::Condition;
pub use self::defaultheaders::DefaultHeaders;

View File

@@ -17,6 +17,10 @@ pub enum TrailingSlash {
/// Always add a trailing slash to the end of the path.
/// This will require all routes to end in a trailing slash for them to be accessible.
Always,
/// Only merge any present multiple trailing slashes.
///
/// Note: This option provides the best compatibility with the v2 version of this middlware.
MergeOnly,
/// Trim trailing slashes from the end of the path.
Trim,
}
@@ -33,7 +37,8 @@ impl Default for TrailingSlash {
/// Performs following:
///
/// - Merges multiple slashes into one.
/// - Appends a trailing slash if one is not present, or removes one if present, depending on the supplied `TrailingSlash`.
/// - Appends a trailing slash if one is not present, removes one if present, or keeps trailing
/// slashes as-is, depending on the supplied `TrailingSlash` variant.
///
/// ```rust
/// use actix_web::{web, http, middleware, App, HttpResponse};
@@ -79,6 +84,7 @@ where
}
}
#[doc(hidden)]
pub struct NormalizePathNormalization<S> {
service: S,
merge_slash: Regex,
@@ -107,12 +113,17 @@ where
// Either adds a string to the end (duplicates will be removed anyways) or trims all slashes from the end
let path = match self.trailing_slash_behavior {
TrailingSlash::Always => original_path.to_string() + "/",
TrailingSlash::MergeOnly => original_path.to_string(),
TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(),
};
// normalize multiple /'s to one /
let path = self.merge_slash.replace_all(&path, "/");
// Ensure root paths are still resolvable. If resulting path is blank after previous step
// it means the path was one or more slashes. Reduce to single slash.
let path = if path.is_empty() { "/" } else { path.as_ref() };
// Check whether the path has been changed
//
// This check was previously implemented as string length comparison
@@ -158,10 +169,23 @@ mod tests {
let mut app = init_service(
App::new()
.wrap(NormalizePath::default())
.service(web::resource("/").to(HttpResponse::Ok))
.service(web::resource("/v1/something/").to(HttpResponse::Ok)),
)
.await;
let req = TestRequest::with_uri("/").to_request();
let res = call_service(&mut app, req).await;
assert!(res.status().is_success());
let req = TestRequest::with_uri("/?query=test").to_request();
let res = call_service(&mut app, req).await;
assert!(res.status().is_success());
let req = TestRequest::with_uri("///").to_request();
let res = call_service(&mut app, req).await;
assert!(res.status().is_success());
let req = TestRequest::with_uri("/v1//something////").to_request();
let res = call_service(&mut app, req).await;
assert!(res.status().is_success());
@@ -184,10 +208,24 @@ mod tests {
let mut app = init_service(
App::new()
.wrap(NormalizePath(TrailingSlash::Trim))
.service(web::resource("/").to(HttpResponse::Ok))
.service(web::resource("/v1/something").to(HttpResponse::Ok)),
)
.await;
// root paths should still work
let req = TestRequest::with_uri("/").to_request();
let res = call_service(&mut app, req).await;
assert!(res.status().is_success());
let req = TestRequest::with_uri("/?query=test").to_request();
let res = call_service(&mut app, req).await;
assert!(res.status().is_success());
let req = TestRequest::with_uri("///").to_request();
let res = call_service(&mut app, req).await;
assert!(res.status().is_success());
let req = TestRequest::with_uri("/v1/something////").to_request();
let res = call_service(&mut app, req).await;
assert!(res.status().is_success());
@@ -205,6 +243,38 @@ mod tests {
assert!(res4.status().is_success());
}
#[actix_rt::test]
async fn keep_trailing_slash_unchange() {
let mut app = init_service(
App::new()
.wrap(NormalizePath(TrailingSlash::MergeOnly))
.service(web::resource("/").to(HttpResponse::Ok))
.service(web::resource("/v1/something").to(HttpResponse::Ok))
.service(web::resource("/v1/").to(HttpResponse::Ok)),
)
.await;
let tests = vec![
("/", true), // root paths should still work
("/?query=test", true),
("///", true),
("/v1/something////", false),
("/v1/something/", false),
("//v1//something", true),
("/v1/", true),
("/v1", false),
("/v1////", true),
("//v1//", true),
("///v1", false),
];
for (path, success) in tests {
let req = TestRequest::with_uri(path).to_request();
let res = call_service(&mut app, req).await;
assert_eq!(res.status().is_success(), success);
}
}
#[actix_rt::test]
async fn test_in_place_normalization() {
let srv = |req: ServiceRequest| {

View File

@@ -1,5 +1,5 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::rc::{Rc, Weak};
use actix_router::ResourceDef;
use fxhash::FxHashMap;
@@ -11,7 +11,7 @@ use crate::request::HttpRequest;
#[derive(Clone, Debug)]
pub struct ResourceMap {
root: ResourceDef,
parent: RefCell<Option<Rc<ResourceMap>>>,
parent: RefCell<Weak<ResourceMap>>,
named: FxHashMap<String, ResourceDef>,
patterns: Vec<(ResourceDef, Option<Rc<ResourceMap>>)>,
}
@@ -20,7 +20,7 @@ impl ResourceMap {
pub fn new(root: ResourceDef) -> Self {
ResourceMap {
root,
parent: RefCell::new(None),
parent: RefCell::new(Weak::new()),
named: FxHashMap::default(),
patterns: Vec::new(),
}
@@ -38,7 +38,7 @@ impl ResourceMap {
pub(crate) fn finish(&self, current: Rc<ResourceMap>) {
for (_, nested) in &self.patterns {
if let Some(ref nested) = nested {
*nested.parent.borrow_mut() = Some(current.clone());
*nested.parent.borrow_mut() = Rc::downgrade(&current);
nested.finish(nested.clone());
}
}
@@ -210,7 +210,7 @@ impl ResourceMap {
U: Iterator<Item = I>,
I: AsRef<str>,
{
if let Some(ref parent) = *self.parent.borrow() {
if let Some(ref parent) = self.parent.borrow().upgrade() {
parent.fill_root(path, elements)?;
}
if self.root.resource_path(path, elements) {
@@ -230,7 +230,7 @@ impl ResourceMap {
U: Iterator<Item = I>,
I: AsRef<str>,
{
if let Some(ref parent) = *self.parent.borrow() {
if let Some(ref parent) = self.parent.borrow().upgrade() {
if let Some(pattern) = parent.named.get(name) {
self.fill_root(path, elements)?;
if pattern.resource_path(path, elements) {
@@ -367,4 +367,42 @@ mod tests {
assert_eq!(root.match_name("/user/22/"), None);
assert_eq!(root.match_name("/user/22/post/55"), Some("user_post"));
}
#[test]
fn bug_fix_issue_1582_debug_print_exits() {
// ref: https://github.com/actix/actix-web/issues/1582
let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
let mut user_map = ResourceMap::new(ResourceDef::root_prefix(""));
user_map.add(&mut ResourceDef::new("/"), None);
user_map.add(&mut ResourceDef::new("/profile"), None);
user_map.add(&mut ResourceDef::new("/article/{id}"), None);
user_map.add(&mut ResourceDef::new("/post/{post_id}"), None);
user_map.add(
&mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"),
None,
);
root.add(
&mut ResourceDef::root_prefix("/user/{id}"),
Some(Rc::new(user_map)),
);
let root = Rc::new(root);
root.finish(Rc::clone(&root));
// check root has no parent
assert!(root.parent.borrow().upgrade().is_none());
// check child has parent reference
assert!(root.patterns[0].1.is_some());
// check child's parent root id matches root's root id
assert_eq!(
root.patterns[0].1.as_ref().unwrap().root.id(),
root.root.id()
);
let output = format!("{:?}", root);
assert!(output.starts_with("ResourceMap {"));
assert!(output.ends_with(" }"));
}
}

View File

@@ -2,11 +2,13 @@
## Unreleased - 2020-xx-xx
* add ability to set address for `TestServer` [#1645]
[#1645]: https://github.com/actix/actix-web/pull/1645
## 2.0.0 - 2020-09-11
* Update actix-codec and actix-utils dependencies.
## 2.0.0-alpha.1 - 2020-05-23
* Update the `time` dependency to 0.2.7
* Update `actix-connect` dependency to 2.0.0-alpha.2

View File

@@ -44,12 +44,20 @@ pub use actix_testing::*;
/// }
/// ```
pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer {
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
test_server_with_addr(tcp, factory).await
}
/// Start [`test server`](./fn.test_server.html) on a concrete Address
pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
tcp: net::TcpListener,
factory: F,
) -> TestServer {
let (tx, rx) = mpsc::channel();
// run server in separate thread
thread::spawn(move || {
let sys = System::new("actix-test-server");
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
let local_addr = tcp.local_addr().unwrap();
Server::build()

View File

@@ -16,7 +16,8 @@ use futures_util::ready;
use rand::{distributions::Alphanumeric, Rng};
use actix_web::dev::BodyEncoding;
use actix_web::middleware::Compress;
use actix_web::middleware::normalize::TrailingSlash;
use actix_web::middleware::{Compress, NormalizePath};
use actix_web::{dev, test, web, App, Error, HttpResponse};
const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
@@ -866,6 +867,20 @@ async fn test_slow_request() {
assert!(data.starts_with("HTTP/1.1 408 Request Timeout"));
}
#[actix_rt::test]
async fn test_normalize() {
let srv = test::start_with(test::config().h1(), || {
App::new()
.wrap(NormalizePath::new(TrailingSlash::Trim))
.service(
web::resource("/one").route(web::to(|| HttpResponse::Ok().finish())),
)
});
let response = srv.get("/one/").send().await.unwrap();
assert!(response.status().is_success());
}
// #[cfg(feature = "openssl")]
// #[actix_rt::test]
// async fn test_ssl_handshake_timeout() {