mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-03 17:41:30 +02:00
Compare commits
14 Commits
files-v0.3
...
codegen-v0
Author | SHA1 | Date | |
---|---|---|---|
c53e9468bc | |||
162121bf8d | |||
f7bcad9567 | |||
f9e3f78e45 | |||
1596893ef7 | |||
2a2474ca09 | |||
509b2e6eec | |||
d707704556 | |||
a429ee6646 | |||
7f8073233a | |||
4b4c9d1b93 | |||
3fde3be3d8 | |||
f861508789 | |||
a4546f02d2 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -13,3 +13,6 @@ guide/build/
|
|||||||
|
|
||||||
# These are backup files generated by rustfmt
|
# These are backup files generated by rustfmt
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# Configuration directory generated by CLion
|
||||||
|
.idea
|
||||||
|
14
CHANGES.md
14
CHANGES.md
@ -3,6 +3,20 @@
|
|||||||
## Unreleased - 2020-xx-xx
|
## Unreleased - 2020-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
## 3.0.0 - 2020-09-11
|
||||||
* No significant changes from `3.0.0-beta.4`.
|
* No significant changes from `3.0.0-beta.4`.
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-web"
|
name = "actix-web"
|
||||||
version = "3.0.0"
|
version = "3.0.2"
|
||||||
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 powerful, pragmatic, and extremely fast web framework for Rust."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["actix", "http", "web", "framework", "async"]
|
keywords = ["actix", "http", "web", "framework", "async"]
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
@ -99,11 +99,11 @@ time = { version = "0.2.7", default-features = false, features = ["std"] }
|
|||||||
url = "2.1"
|
url = "2.1"
|
||||||
open-ssl = { package = "openssl", version = "0.10", optional = true }
|
open-ssl = { package = "openssl", version = "0.10", optional = true }
|
||||||
rust-tls = { package = "rustls", version = "0.18.0", 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]
|
[dev-dependencies]
|
||||||
actix = "0.10.0"
|
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"
|
rand = "0.7"
|
||||||
env_logger = "0.7"
|
env_logger = "0.7"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
|
11
MIGRATION.md
11
MIGRATION.md
@ -3,12 +3,23 @@
|
|||||||
|
|
||||||
## 3.0.0
|
## 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
|
* Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now
|
||||||
result in `SameSite=None` being sent with the response Set-Cookie header.
|
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.
|
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
|
* actix-http support for Actors messages was moved to actix-http crate and is enabled
|
||||||
with feature `actors`
|
with feature `actors`
|
||||||
|
|
||||||
* content_length function is removed from actix-http.
|
* content_length function is removed from actix-http.
|
||||||
You can set Content-Length by normally setting the response body or calling no_chunking function.
|
You can set Content-Length by normally setting the response body or calling no_chunking function.
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@ path = "src/lib.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = { version = "3.0.0", default-features = false }
|
actix-web = { version = "3.0.0", default-features = false }
|
||||||
actix-http = "2.0.0"
|
|
||||||
actix-service = "1.0.6"
|
actix-service = "1.0.6"
|
||||||
bitflags = "1"
|
bitflags = "1"
|
||||||
bytes = "0.5.3"
|
bytes = "0.5.3"
|
||||||
|
94
actix-files/src/chunked.rs
Normal file
94
actix-files/src/chunked.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
114
actix-files/src/directory.rs
Normal file
114
actix-files/src/directory.rs
Normal 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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// " -- " & -- & ' -- ' < -- < > -- > / -- /
|
||||||
|
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
250
actix-files/src/files.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)]
|
#![deny(rust_2018_idioms)]
|
||||||
#![allow(clippy::borrow_interior_mutable_const)]
|
#![warn(missing_docs, missing_debug_implementations)]
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::io;
|
||||||
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 actix_service::boxed::{self, BoxService, BoxServiceFactory};
|
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
||||||
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
|
use actix_web::{
|
||||||
use actix_web::dev::{
|
dev::{ServiceRequest, ServiceResponse},
|
||||||
AppService, HttpServiceFactory, Payload, ResourceDef, ServiceRequest,
|
error::{BlockingError, Error, ErrorInternalServerError},
|
||||||
ServiceResponse,
|
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 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 error;
|
||||||
|
mod files;
|
||||||
mod named;
|
mod named;
|
||||||
|
mod path_buf;
|
||||||
mod range;
|
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::named::NamedFile;
|
||||||
pub use crate::range::HttpRange;
|
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 HttpService = BoxService<ServiceRequest, ServiceResponse, Error>;
|
||||||
type HttpNewService = BoxServiceFactory<(), 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()
|
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 {
|
match err {
|
||||||
BlockingError::Error(err) => err.into(),
|
BlockingError::Error(err) => err.into(),
|
||||||
BlockingError::Canceled => ErrorInternalServerError("Unexpected error"),
|
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)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// " -- " & -- & ' -- ' < -- < > -- > / -- /
|
|
||||||
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;
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::fs;
|
use std::{
|
||||||
use std::iter::FromIterator;
|
fs::{self, File},
|
||||||
use std::ops::Add;
|
ops::Add,
|
||||||
use std::time::{Duration, SystemTime};
|
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 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]
|
#[actix_rt::test]
|
||||||
async fn test_file_extension_to_mime() {
|
async fn test_file_extension_to_mime() {
|
||||||
@ -1013,7 +446,7 @@ mod tests {
|
|||||||
|
|
||||||
// Check file contents
|
// Check file contents
|
||||||
let bytes = response.body().await.unwrap();
|
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);
|
assert_eq!(bytes, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1046,7 +479,7 @@ mod tests {
|
|||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
let bytes = test::read_body(response).await;
|
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);
|
assert_eq!(bytes, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1224,7 +657,7 @@ mod tests {
|
|||||||
let resp = test::call_service(&mut st, req).await;
|
let resp = test::call_service(&mut st, req).await;
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
let bytes = test::read_body(resp).await;
|
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]
|
// #[actix_rt::test]
|
||||||
@ -1340,36 +773,4 @@ mod tests {
|
|||||||
// let response = srv.execute(request.send()).unwrap();
|
// let response = srv.execute(request.send()).unwrap();
|
||||||
// assert_eq!(response.status(), StatusCode::OK);
|
// 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"])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -7,17 +7,20 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::MetadataExt;
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
|
||||||
use bitflags::bitflags;
|
use actix_web::{
|
||||||
use mime_guess::from_path;
|
dev::{BodyEncoding, SizedStream},
|
||||||
|
http::{
|
||||||
use actix_http::body::SizedStream;
|
header::{
|
||||||
use actix_web::dev::BodyEncoding;
|
self, Charset, ContentDisposition, DispositionParam, DispositionType,
|
||||||
use actix_web::http::header::{
|
ExtendedValue,
|
||||||
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
|
},
|
||||||
|
ContentEncoding, StatusCode,
|
||||||
|
},
|
||||||
|
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
|
||||||
};
|
};
|
||||||
use actix_web::http::{ContentEncoding, StatusCode};
|
use bitflags::bitflags;
|
||||||
use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder};
|
|
||||||
use futures_util::future::{ready, Ready};
|
use futures_util::future::{ready, Ready};
|
||||||
|
use mime_guess::from_path;
|
||||||
|
|
||||||
use crate::range::HttpRange;
|
use crate::range::HttpRange;
|
||||||
use crate::ChunkedReadFile;
|
use crate::ChunkedReadFile;
|
||||||
@ -93,8 +96,10 @@ 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 =
|
let mut parameters =
|
||||||
vec![DispositionParam::Filename(String::from(filename.as_ref()))];
|
vec![DispositionParam::Filename(String::from(filename.as_ref()))];
|
||||||
|
|
||||||
if !filename.is_ascii() {
|
if !filename.is_ascii() {
|
||||||
parameters.push(DispositionParam::FilenameExt(ExtendedValue {
|
parameters.push(DispositionParam::FilenameExt(ExtendedValue {
|
||||||
charset: Charset::Ext(String::from("UTF-8")),
|
charset: Charset::Ext(String::from("UTF-8")),
|
||||||
@ -102,16 +107,19 @@ impl NamedFile {
|
|||||||
value: filename.into_owned().into_bytes(),
|
value: filename.into_owned().into_bytes(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
let cd = ContentDisposition {
|
let cd = ContentDisposition {
|
||||||
disposition,
|
disposition,
|
||||||
parameters,
|
parameters,
|
||||||
};
|
};
|
||||||
|
|
||||||
(ct, cd)
|
(ct, cd)
|
||||||
};
|
};
|
||||||
|
|
||||||
let md = file.metadata()?;
|
let md = file.metadata()?;
|
||||||
let modified = md.modified().ok();
|
let modified = md.modified().ok();
|
||||||
let encoding = None;
|
let encoding = None;
|
||||||
|
|
||||||
Ok(NamedFile {
|
Ok(NamedFile {
|
||||||
path,
|
path,
|
||||||
file,
|
file,
|
||||||
@ -242,6 +250,7 @@ impl NamedFile {
|
|||||||
let dur = mtime
|
let dur = mtime
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.expect("modification time must be after epoch");
|
.expect("modification time must be after epoch");
|
||||||
|
|
||||||
header::EntityTag::strong(format!(
|
header::EntityTag::strong(format!(
|
||||||
"{:x}:{:x}:{:x}:{:x}",
|
"{:x}:{:x}:{:x}:{:x}",
|
||||||
ino,
|
ino,
|
||||||
@ -256,9 +265,11 @@ impl NamedFile {
|
|||||||
self.modified.map(|mtime| mtime.into())
|
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> {
|
pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> {
|
||||||
if self.status_code != StatusCode::OK {
|
if self.status_code != StatusCode::OK {
|
||||||
let mut resp = HttpResponse::build(self.status_code);
|
let mut resp = HttpResponse::build(self.status_code);
|
||||||
|
|
||||||
resp.set(header::ContentType(self.content_type.clone()))
|
resp.set(header::ContentType(self.content_type.clone()))
|
||||||
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
|
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
|
||||||
res.header(
|
res.header(
|
||||||
@ -266,9 +277,11 @@ impl NamedFile {
|
|||||||
self.content_disposition.to_string(),
|
self.content_disposition.to_string(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(current_encoding) = self.encoding {
|
if let Some(current_encoding) = self.encoding {
|
||||||
resp.encoding(current_encoding);
|
resp.encoding(current_encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
let reader = ChunkedReadFile {
|
let reader = ChunkedReadFile {
|
||||||
size: self.md.len(),
|
size: self.md.len(),
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@ -276,6 +289,7 @@ impl NamedFile {
|
|||||||
fut: None,
|
fut: None,
|
||||||
counter: 0,
|
counter: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok(resp.streaming(reader));
|
return Ok(resp.streaming(reader));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,6 +298,7 @@ impl NamedFile {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let last_modified = if self.flags.contains(Flags::LAST_MD) {
|
let last_modified = if self.flags.contains(Flags::LAST_MD) {
|
||||||
self.last_modified()
|
self.last_modified()
|
||||||
} else {
|
} else {
|
||||||
@ -298,6 +313,7 @@ impl NamedFile {
|
|||||||
{
|
{
|
||||||
let t1: SystemTime = m.clone().into();
|
let t1: SystemTime = m.clone().into();
|
||||||
let t2: SystemTime = since.clone().into();
|
let t2: SystemTime = since.clone().into();
|
||||||
|
|
||||||
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
|
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
|
||||||
(Ok(t1), Ok(t2)) => t1 > t2,
|
(Ok(t1), Ok(t2)) => t1 > t2,
|
||||||
_ => false,
|
_ => false,
|
||||||
@ -309,13 +325,14 @@ impl NamedFile {
|
|||||||
// check last modified
|
// check last modified
|
||||||
let not_modified = if !none_match(etag.as_ref(), req) {
|
let not_modified = if !none_match(etag.as_ref(), req) {
|
||||||
true
|
true
|
||||||
} else if req.headers().contains_key(&header::IF_NONE_MATCH) {
|
} else if req.headers().contains_key(header::IF_NONE_MATCH) {
|
||||||
false
|
false
|
||||||
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
|
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
|
||||||
(last_modified, req.get_header())
|
(last_modified, req.get_header())
|
||||||
{
|
{
|
||||||
let t1: SystemTime = m.clone().into();
|
let t1: SystemTime = m.clone().into();
|
||||||
let t2: SystemTime = since.clone().into();
|
let t2: SystemTime = since.clone().into();
|
||||||
|
|
||||||
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
|
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
|
||||||
(Ok(t1), Ok(t2)) => t1 <= t2,
|
(Ok(t1), Ok(t2)) => t1 <= t2,
|
||||||
_ => false,
|
_ => false,
|
||||||
@ -332,6 +349,7 @@ impl NamedFile {
|
|||||||
self.content_disposition.to_string(),
|
self.content_disposition.to_string(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// default compressing
|
// default compressing
|
||||||
if let Some(current_encoding) = self.encoding {
|
if let Some(current_encoding) = self.encoding {
|
||||||
resp.encoding(current_encoding);
|
resp.encoding(current_encoding);
|
||||||
@ -350,11 +368,12 @@ impl NamedFile {
|
|||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
|
|
||||||
// check for range header
|
// check for range header
|
||||||
if let Some(ranges) = req.headers().get(&header::RANGE) {
|
if let Some(ranges) = req.headers().get(header::RANGE) {
|
||||||
if let Ok(rangesheader) = ranges.to_str() {
|
if let Ok(ranges_header) = ranges.to_str() {
|
||||||
if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) {
|
if let Ok(ranges) = HttpRange::parse(ranges_header, length) {
|
||||||
length = rangesvec[0].length;
|
length = ranges[0].length;
|
||||||
offset = rangesvec[0].start;
|
offset = ranges[0].start;
|
||||||
|
|
||||||
resp.encoding(ContentEncoding::Identity);
|
resp.encoding(ContentEncoding::Identity);
|
||||||
resp.header(
|
resp.header(
|
||||||
header::CONTENT_RANGE,
|
header::CONTENT_RANGE,
|
||||||
@ -414,6 +433,7 @@ impl DerefMut for NamedFile {
|
|||||||
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||||
match req.get_header::<header::IfMatch>() {
|
match req.get_header::<header::IfMatch>() {
|
||||||
None | Some(header::IfMatch::Any) => true,
|
None | Some(header::IfMatch::Any) => true,
|
||||||
|
|
||||||
Some(header::IfMatch::Items(ref items)) => {
|
Some(header::IfMatch::Items(ref items)) => {
|
||||||
if let Some(some_etag) = etag {
|
if let Some(some_etag) = etag {
|
||||||
for item in items {
|
for item in items {
|
||||||
@ -422,6 +442,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
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 {
|
fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||||
match req.get_header::<header::IfNoneMatch>() {
|
match req.get_header::<header::IfNoneMatch>() {
|
||||||
Some(header::IfNoneMatch::Any) => false,
|
Some(header::IfNoneMatch::Any) => false,
|
||||||
|
|
||||||
Some(header::IfNoneMatch::Items(ref items)) => {
|
Some(header::IfNoneMatch::Items(ref items)) => {
|
||||||
if let Some(some_etag) = etag {
|
if let Some(some_etag) = etag {
|
||||||
for item in items {
|
for item in items {
|
||||||
@ -439,8 +461,10 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
None => true,
|
None => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
99
actix-files/src/path_buf.rs
Normal file
99
actix-files/src/path_buf.rs
Normal 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"])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,14 @@
|
|||||||
/// HTTP Range header representation.
|
/// HTTP Range header representation.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct HttpRange {
|
pub struct HttpRange {
|
||||||
|
/// Start of range.
|
||||||
pub start: u64,
|
pub start: u64,
|
||||||
|
|
||||||
|
/// Length of range.
|
||||||
pub length: u64,
|
pub length: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
static PREFIX: &str = "bytes=";
|
const PREFIX: &str = "bytes=";
|
||||||
const PREFIX_LEN: usize = 6;
|
const PREFIX_LEN: usize = 6;
|
||||||
|
|
||||||
impl HttpRange {
|
impl HttpRange {
|
||||||
|
167
actix-files/src/service.rs
Normal file
167
actix-files/src/service.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,12 +13,11 @@ Actix http
|
|||||||
## Example
|
## Example
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// see examples/framed_hello.rs for complete list of used crates.
|
|
||||||
use std::{env, io};
|
use std::{env, io};
|
||||||
|
|
||||||
use actix_http::{HttpService, Response};
|
use actix_http::{HttpService, Response};
|
||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use futures::future;
|
use futures_util::future;
|
||||||
use http::header::HeaderValue;
|
use http::header::HeaderValue;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
|
@ -3,6 +3,14 @@
|
|||||||
## Unreleased - 2020-xx-xx
|
## 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.
|
||||||
|
|
||||||
|
[#1677]: https://github.com/actix/actix-web/pull/1677
|
||||||
|
[#1674]: https://github.com/actix/actix-web/pull/1674
|
||||||
|
|
||||||
|
|
||||||
## 0.3.0 - 2020-09-11
|
## 0.3.0 - 2020-09-11
|
||||||
* No significant changes from `0.3.0-beta.1`.
|
* No significant changes from `0.3.0-beta.1`.
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-web-codegen"
|
name = "actix-web-codegen"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
description = "Actix web proc macros"
|
description = "Actix web proc macros"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
@ -19,6 +19,8 @@ syn = { version = "1", features = ["full", "parsing"] }
|
|||||||
proc-macro2 = "1"
|
proc-macro2 = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "1.0.0"
|
actix-rt = "1.1.1"
|
||||||
actix-web = "3.0.0"
|
actix-web = "3.0.0"
|
||||||
futures-util = { version = "0.3.5", default-features = false }
|
futures-util = { version = "0.3.5", default-features = false }
|
||||||
|
trybuild = "1"
|
||||||
|
rustversion = "1"
|
||||||
|
@ -1,8 +1,22 @@
|
|||||||
# Helper and convenience macros for Actix-web. [](https://travis-ci.org/actix/actix-web) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-web-codegen) [](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
|
||||||
|
|
||||||
|
[](https://crates.io/crates/actix-web-codegen)
|
||||||
|
[](https://docs.rs/actix-web)
|
||||||
|
[](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
|
||||||
|
[](https://travis-ci.org/actix/actix-web)
|
||||||
|
[](https://codecov.io/gh/actix/actix-web)
|
||||||
|
[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
* [API Documentation](https://docs.rs/actix-web-codegen/)
|
- [API Documentation](https://docs.rs/actix-web-codegen)
|
||||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||||
* Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen)
|
- Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen)
|
||||||
* Minimum supported Rust version: 1.40 or later
|
- 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
|
||||||
|
@ -1,158 +1,174 @@
|
|||||||
#![recursion_limit = "512"]
|
//! Macros for reducing boilerplate code in Actix Web applications.
|
||||||
|
|
||||||
//! Helper and convenience macros for Actix-web.
|
|
||||||
//!
|
//!
|
||||||
//! ## 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)
|
//! # Runtime Setup
|
||||||
//!
|
//! Used for setting up the actix async runtime. See [main] macro docs.
|
||||||
//! ## 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:
|
|
||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! use actix_web::HttpResponse;
|
//! #[actix_web_codegen::main] // or `#[actix_web::main]` in Actix Web apps
|
||||||
//! use actix_web_codegen::get;
|
//! async fn main() {
|
||||||
//!
|
//! async { println!("Hello world"); }.await
|
||||||
//! #[get("/test")]
|
|
||||||
//! async fn async_test() -> Result<HttpResponse, actix_web::Error> {
|
|
||||||
//! Ok(HttpResponse::Ok().finish())
|
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
|
//!
|
||||||
|
//! # 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;
|
#![recursion_limit = "512"]
|
||||||
|
|
||||||
mod route;
|
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
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:
|
/// # Attributes
|
||||||
///
|
/// - `"path"` - Raw literal string with path for which to register handler.
|
||||||
/// - `"path"` - Raw literal string with path for which to register handler. Mandatory.
|
/// - `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`
|
/// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`
|
||||||
/// - `wrap="Middleware"` - Registers a resource middleware.
|
/// - `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]
|
#[proc_macro_attribute]
|
||||||
pub fn get(args: TokenStream, input: TokenStream) -> TokenStream {
|
pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
route::generate(args, input, route::GuardType::Get)
|
route::with_method(None, args, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates route handler with `POST` method guard.
|
macro_rules! doc_comment {
|
||||||
///
|
($x:expr; $($tt:tt)*) => {
|
||||||
/// Syntax: `#[post("path"[, attributes])]`
|
#[doc = $x]
|
||||||
///
|
$($tt)*
|
||||||
/// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates route handler with `PUT` method guard.
|
macro_rules! method_macro {
|
||||||
///
|
(
|
||||||
/// Syntax: `#[put("path"[, attributes])]`
|
$($variant:ident, $method:ident,)+
|
||||||
///
|
) => {
|
||||||
/// Attributes are the same as in [get](attr.get.html)
|
$(doc_comment! {
|
||||||
#[proc_macro_attribute]
|
concat!("
|
||||||
pub fn put(args: TokenStream, input: TokenStream) -> TokenStream {
|
Creates route handler with `actix_web::guard::", stringify!($variant), "`.
|
||||||
route::generate(args, input, route::GuardType::Put)
|
|
||||||
|
# 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.
|
method_macro! {
|
||||||
///
|
Get, get,
|
||||||
/// Syntax: `#[delete("path"[, attributes])]`
|
Post, post,
|
||||||
///
|
Put, put,
|
||||||
/// Attributes are the same as in [get](attr.get.html)
|
Delete, delete,
|
||||||
#[proc_macro_attribute]
|
Head, head,
|
||||||
pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream {
|
Connect, connect,
|
||||||
route::generate(args, input, route::GuardType::Delete)
|
Options, options,
|
||||||
}
|
Trace, trace,
|
||||||
|
Patch, patch,
|
||||||
/// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks async main function as the actix system entry-point.
|
/// 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
|
/// ```rust
|
||||||
/// #[actix_web::main]
|
/// #[actix_web_codegen::main]
|
||||||
/// async fn main() {
|
/// async fn main() {
|
||||||
/// async { println!("Hello world"); }.await
|
/// async { println!("Hello world"); }.await
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
#[cfg(not(test))] // Work around for rust-lang/rust#62127
|
|
||||||
pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
extern crate proc_macro;
|
extern crate proc_macro;
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||||
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
|
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
|
||||||
@ -17,53 +20,81 @@ impl ToTokens for ResourceType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
macro_rules! method_type {
|
||||||
pub enum GuardType {
|
(
|
||||||
Get,
|
$($variant:ident, $upper:ident,)+
|
||||||
Post,
|
) => {
|
||||||
Put,
|
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||||
Delete,
|
pub enum MethodType {
|
||||||
Head,
|
$(
|
||||||
Connect,
|
$variant,
|
||||||
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",
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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) {
|
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||||
let ident = Ident::new(self.as_str(), Span::call_site());
|
let ident = Ident::new(self.as_str(), Span::call_site());
|
||||||
stream.append(ident);
|
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 {
|
struct Args {
|
||||||
path: syn::LitStr,
|
path: syn::LitStr,
|
||||||
guards: Vec<Ident>,
|
guards: Vec<Ident>,
|
||||||
wrappers: Vec<syn::Type>,
|
wrappers: Vec<syn::Type>,
|
||||||
|
methods: HashSet<MethodType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Args {
|
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 path = None;
|
||||||
let mut guards = Vec::new();
|
let mut guards = Vec::new();
|
||||||
let mut wrappers = 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 {
|
for arg in args {
|
||||||
match arg {
|
match arg {
|
||||||
NestedMeta::Lit(syn::Lit::Str(lit)) => match path {
|
NestedMeta::Lit(syn::Lit::Str(lit)) => match path {
|
||||||
@ -96,10 +127,33 @@ impl Args {
|
|||||||
"Attribute wrap expects type",
|
"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 {
|
} else {
|
||||||
return Err(syn::Error::new_spanned(
|
return Err(syn::Error::new_spanned(
|
||||||
nv.path,
|
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(),
|
path: path.unwrap(),
|
||||||
guards,
|
guards,
|
||||||
wrappers,
|
wrappers,
|
||||||
|
methods,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -121,7 +176,6 @@ pub struct Route {
|
|||||||
args: Args,
|
args: Args,
|
||||||
ast: syn::ItemFn,
|
ast: syn::ItemFn,
|
||||||
resource_type: ResourceType,
|
resource_type: ResourceType,
|
||||||
guard: GuardType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn guess_resource_type(typ: &syn::Type) -> ResourceType {
|
fn guess_resource_type(typ: &syn::Type) -> ResourceType {
|
||||||
@ -150,21 +204,30 @@ impl Route {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
args: AttributeArgs,
|
args: AttributeArgs,
|
||||||
input: TokenStream,
|
input: TokenStream,
|
||||||
guard: GuardType,
|
method: Option<MethodType>,
|
||||||
) -> syn::Result<Self> {
|
) -> syn::Result<Self> {
|
||||||
if args.is_empty() {
|
if args.is_empty() {
|
||||||
return Err(syn::Error::new(
|
return Err(syn::Error::new(
|
||||||
Span::call_site(),
|
Span::call_site(),
|
||||||
format!(
|
format!(
|
||||||
r#"invalid server definition, expected #[{}("<some path>")]"#,
|
r#"invalid service definition, expected #[{}("<some path>")]"#,
|
||||||
guard.as_str().to_ascii_lowercase()
|
method
|
||||||
|
.map(|it| it.as_str())
|
||||||
|
.unwrap_or("route")
|
||||||
|
.to_ascii_lowercase()
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let ast: syn::ItemFn = syn::parse(input)?;
|
let ast: syn::ItemFn = syn::parse(input)?;
|
||||||
let name = ast.sig.ident.clone();
|
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() {
|
let resource_type = if ast.sig.asyncness.is_some() {
|
||||||
ResourceType::Async
|
ResourceType::Async
|
||||||
@ -185,7 +248,6 @@ impl Route {
|
|||||||
args,
|
args,
|
||||||
ast,
|
ast,
|
||||||
resource_type,
|
resource_type,
|
||||||
guard,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -194,17 +256,36 @@ impl ToTokens for Route {
|
|||||||
fn to_tokens(&self, output: &mut TokenStream2) {
|
fn to_tokens(&self, output: &mut TokenStream2) {
|
||||||
let Self {
|
let Self {
|
||||||
name,
|
name,
|
||||||
guard,
|
|
||||||
ast,
|
ast,
|
||||||
args:
|
args:
|
||||||
Args {
|
Args {
|
||||||
path,
|
path,
|
||||||
guards,
|
guards,
|
||||||
wrappers,
|
wrappers,
|
||||||
|
methods,
|
||||||
},
|
},
|
||||||
resource_type,
|
resource_type,
|
||||||
} = self;
|
} = self;
|
||||||
let resource_name = name.to_string();
|
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! {
|
let stream = quote! {
|
||||||
#[allow(non_camel_case_types, missing_docs)]
|
#[allow(non_camel_case_types, missing_docs)]
|
||||||
pub struct #name;
|
pub struct #name;
|
||||||
@ -214,7 +295,7 @@ impl ToTokens for Route {
|
|||||||
#ast
|
#ast
|
||||||
let __resource = actix_web::Resource::new(#path)
|
let __resource = actix_web::Resource::new(#path)
|
||||||
.name(#resource_name)
|
.name(#resource_name)
|
||||||
.guard(actix_web::guard::#guard())
|
#method_guards
|
||||||
#(.guard(actix_web::guard::fn_guard(#guards)))*
|
#(.guard(actix_web::guard::fn_guard(#guards)))*
|
||||||
#(.wrap(#wrappers))*
|
#(.wrap(#wrappers))*
|
||||||
.#resource_type(#name);
|
.#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,
|
args: TokenStream,
|
||||||
input: TokenStream,
|
input: TokenStream,
|
||||||
guard: GuardType,
|
|
||||||
) -> TokenStream {
|
) -> TokenStream {
|
||||||
let args = parse_macro_input!(args as syn::AttributeArgs);
|
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(),
|
Ok(route) => route.into_token_stream().into(),
|
||||||
Err(err) => err.to_compile_error().into(),
|
Err(err) => err.to_compile_error().into(),
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,9 @@ use std::task::{Context, Poll};
|
|||||||
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
|
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
|
||||||
use actix_web::http::header::{HeaderName, HeaderValue};
|
use actix_web::http::header::{HeaderName, HeaderValue};
|
||||||
use actix_web::{http, test, web::Path, App, Error, HttpResponse, Responder};
|
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;
|
use futures_util::future;
|
||||||
|
|
||||||
// Make sure that we can name function as 'config'
|
// Make sure that we can name function as 'config'
|
||||||
@ -79,6 +81,11 @@ async fn get_param_test(_: Path<String>) -> impl Responder {
|
|||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[route("/multi", method = "GET", method = "POST", method = "HEAD")]
|
||||||
|
async fn route_test() -> impl Responder {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ChangeStatusCode;
|
pub struct ChangeStatusCode;
|
||||||
|
|
||||||
impl<S, B> Transform<S> for ChangeStatusCode
|
impl<S, B> Transform<S> for ChangeStatusCode
|
||||||
@ -172,6 +179,7 @@ async fn test_body() {
|
|||||||
.service(trace_test)
|
.service(trace_test)
|
||||||
.service(patch_test)
|
.service(patch_test)
|
||||||
.service(test_handler)
|
.service(test_handler)
|
||||||
|
.service(route_test)
|
||||||
});
|
});
|
||||||
let request = srv.request(http::Method::GET, srv.url("/test"));
|
let request = srv.request(http::Method::GET, srv.url("/test"));
|
||||||
let response = request.send().await.unwrap();
|
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 request = srv.request(http::Method::GET, srv.url("/test"));
|
||||||
let response = request.send().await.unwrap();
|
let response = request.send().await.unwrap();
|
||||||
assert!(response.status().is_success());
|
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]
|
#[actix_rt::test]
|
||||||
|
27
actix-web-codegen/tests/trybuild.rs
Normal file
27
actix-web-codegen/tests/trybuild.rs
Normal 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) {}
|
@ -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());
|
||||||
|
}
|
@ -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
|
@ -0,0 +1 @@
|
|||||||
|
route-missing-method-fail.rs
|
@ -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
|
@ -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());
|
||||||
|
}
|
@ -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
|
17
actix-web-codegen/tests/trybuild/route-ok.rs
Normal file
17
actix-web-codegen/tests/trybuild/route-ok.rs
Normal 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());
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
@ -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
|
30
actix-web-codegen/tests/trybuild/simple-fail.rs
Normal file
30
actix-web-codegen/tests/trybuild/simple-fail.rs
Normal 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() {}
|
29
actix-web-codegen/tests/trybuild/simple-fail.stderr
Normal file
29
actix-web-codegen/tests/trybuild/simple-fail.stderr
Normal 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")]
|
||||||
|
| ^^^^^^^^^^^^
|
16
actix-web-codegen/tests/trybuild/simple.rs
Normal file
16
actix-web-codegen/tests/trybuild/simple.rs
Normal 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());
|
||||||
|
}
|
@ -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.
|
//! Actix web is a powerful, pragmatic, and extremely fast web framework for Rust.
|
||||||
//!
|
//!
|
||||||
//! ## Example
|
//! ## Example
|
||||||
@ -68,6 +65,11 @@
|
|||||||
//! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2`
|
//! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2`
|
||||||
//! * `secure-cookies` - secure cookies support
|
//! * `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;
|
||||||
mod app_service;
|
mod app_service;
|
||||||
mod config;
|
mod config;
|
||||||
|
@ -9,7 +9,7 @@ mod condition;
|
|||||||
mod defaultheaders;
|
mod defaultheaders;
|
||||||
pub mod errhandlers;
|
pub mod errhandlers;
|
||||||
mod logger;
|
mod logger;
|
||||||
mod normalize;
|
pub mod normalize;
|
||||||
|
|
||||||
pub use self::condition::Condition;
|
pub use self::condition::Condition;
|
||||||
pub use self::defaultheaders::DefaultHeaders;
|
pub use self::defaultheaders::DefaultHeaders;
|
||||||
|
@ -79,6 +79,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
pub struct NormalizePathNormalization<S> {
|
pub struct NormalizePathNormalization<S> {
|
||||||
service: S,
|
service: S,
|
||||||
merge_slash: Regex,
|
merge_slash: Regex,
|
||||||
@ -113,6 +114,10 @@ where
|
|||||||
// normalize multiple /'s to one /
|
// normalize multiple /'s to one /
|
||||||
let path = self.merge_slash.replace_all(&path, "/");
|
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
|
// Check whether the path has been changed
|
||||||
//
|
//
|
||||||
// This check was previously implemented as string length comparison
|
// This check was previously implemented as string length comparison
|
||||||
@ -158,10 +163,23 @@ mod tests {
|
|||||||
let mut app = init_service(
|
let mut app = init_service(
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(NormalizePath::default())
|
.wrap(NormalizePath::default())
|
||||||
|
.service(web::resource("/").to(HttpResponse::Ok))
|
||||||
.service(web::resource("/v1/something/").to(HttpResponse::Ok)),
|
.service(web::resource("/v1/something/").to(HttpResponse::Ok)),
|
||||||
)
|
)
|
||||||
.await;
|
.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 req = TestRequest::with_uri("/v1//something////").to_request();
|
||||||
let res = call_service(&mut app, req).await;
|
let res = call_service(&mut app, req).await;
|
||||||
assert!(res.status().is_success());
|
assert!(res.status().is_success());
|
||||||
@ -184,10 +202,24 @@ mod tests {
|
|||||||
let mut app = init_service(
|
let mut app = init_service(
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(NormalizePath(TrailingSlash::Trim))
|
.wrap(NormalizePath(TrailingSlash::Trim))
|
||||||
|
.service(web::resource("/").to(HttpResponse::Ok))
|
||||||
.service(web::resource("/v1/something").to(HttpResponse::Ok)),
|
.service(web::resource("/v1/something").to(HttpResponse::Ok)),
|
||||||
)
|
)
|
||||||
.await;
|
.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 req = TestRequest::with_uri("/v1/something////").to_request();
|
||||||
let res = call_service(&mut app, req).await;
|
let res = call_service(&mut app, req).await;
|
||||||
assert!(res.status().is_success());
|
assert!(res.status().is_success());
|
||||||
|
@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
## Unreleased - 2020-xx-xx
|
## 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
|
## 2.0.0 - 2020-09-11
|
||||||
* Update actix-codec and actix-utils dependencies.
|
* Update actix-codec and actix-utils dependencies.
|
||||||
|
|
||||||
|
|
||||||
## 2.0.0-alpha.1 - 2020-05-23
|
## 2.0.0-alpha.1 - 2020-05-23
|
||||||
* Update the `time` dependency to 0.2.7
|
* Update the `time` dependency to 0.2.7
|
||||||
* Update `actix-connect` dependency to 2.0.0-alpha.2
|
* Update `actix-connect` dependency to 2.0.0-alpha.2
|
||||||
|
@ -44,12 +44,20 @@ pub use actix_testing::*;
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer {
|
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();
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
// run server in separate thread
|
// run server in separate thread
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let sys = System::new("actix-test-server");
|
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();
|
let local_addr = tcp.local_addr().unwrap();
|
||||||
|
|
||||||
Server::build()
|
Server::build()
|
||||||
|
@ -16,7 +16,8 @@ use futures_util::ready;
|
|||||||
use rand::{distributions::Alphanumeric, Rng};
|
use rand::{distributions::Alphanumeric, Rng};
|
||||||
|
|
||||||
use actix_web::dev::BodyEncoding;
|
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};
|
use actix_web::{dev, test, web, App, Error, HttpResponse};
|
||||||
|
|
||||||
const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
|
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"));
|
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")]
|
// #[cfg(feature = "openssl")]
|
||||||
// #[actix_rt::test]
|
// #[actix_rt::test]
|
||||||
// async fn test_ssl_handshake_timeout() {
|
// async fn test_ssl_handshake_timeout() {
|
||||||
|
Reference in New Issue
Block a user