"]
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
-readme = "README.md"
keywords = ["actix", "http", "web", "framework", "async"]
+categories = [
+ "network-programming",
+ "asynchronous",
+ "web-programming::http-server",
+ "web-programming::websocket"
+]
homepage = "https://actix.rs"
-repository = "https://github.com/actix/actix-web.git"
-documentation = "https://docs.rs/actix-web/"
-categories = ["network-programming", "asynchronous",
- "web-programming::http-server",
- "web-programming::websocket"]
+repository = "https://github.com/actix/actix-web"
license = "MIT OR Apache-2.0"
edition = "2018"
[package.metadata.docs.rs]
# features that docs.rs will build with
-features = ["openssl", "rustls", "compress", "secure-cookies"]
-
-[badges]
-travis-ci = { repository = "actix/actix-web", branch = "master" }
-codecov = { repository = "actix/actix-web", branch = "master", service = "github" }
+features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"]
[lib]
name = "actix_web"
@@ -28,103 +25,96 @@ path = "src/lib.rs"
[workspace]
members = [
- ".",
- "awc",
- "actix-http",
- "actix-files",
- "actix-multipart",
- "actix-web-actors",
- "actix-web-codegen",
- "actix-http-test",
+ ".",
+ "awc",
+ "actix-http",
+ "actix-files",
+ "actix-multipart",
+ "actix-web-actors",
+ "actix-web-codegen",
+ "actix-http-test",
+ "actix-test",
]
+# enable when MSRV is 1.51+
+# resolver = "2"
[features]
-default = ["compress", "cookies"]
+default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
-# content-encoding support
-compress = ["actix-http/compress", "awc/compress"]
+# Brotli algorithm content-encoding support
+compress-brotli = ["actix-http/compress-brotli", "__compress"]
+# Gzip and deflate algorithms content-encoding support
+compress-gzip = ["actix-http/compress-gzip", "__compress"]
+# Zstd algorithm content-encoding support
+compress-zstd = ["actix-http/compress-zstd", "__compress"]
# support for cookies
-cookies = ["actix-http/cookies", "awc/cookies"]
+cookies = ["cookie"]
# secure cookies feature
-secure-cookies = ["actix-http/secure-cookies"]
+secure-cookies = ["cookie/secure"]
# openssl
-openssl = ["tls-openssl", "actix-tls/accept", "actix-tls/openssl", "awc/openssl"]
+openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
# rustls
-rustls = ["tls-rustls", "actix-tls/accept", "actix-tls/rustls", "awc/rustls"]
+rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
-[[example]]
-name = "basic"
-required-features = ["compress"]
-
-[[example]]
-name = "uds"
-required-features = ["compress"]
-
-[[test]]
-name = "test_server"
-required-features = ["compress"]
-
-[[example]]
-name = "on_connect"
-required-features = []
-
-[[example]]
-name = "client"
-required-features = ["rustls"]
+# Internal (PRIVATE!) features used to aid testing and cheking feature status.
+# Don't rely on these whatsoever. They may disappear at anytime.
+__compress = []
[dependencies]
-actix-codec = "0.4.0-beta.1"
-actix-macros = "0.2.0"
+actix-codec = "0.4.0"
+actix-macros = "0.2.1"
actix-router = "0.2.7"
-actix-rt = "2"
+actix-rt = "2.2"
actix-server = "2.0.0-beta.3"
-actix-service = "2.0.0-beta.4"
-actix-utils = "3.0.0-beta.2"
-actix-tls = { version = "3.0.0-beta.3", default-features = false, optional = true }
+actix-service = "2.0.0"
+actix-utils = "3.0.0"
+actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true }
-actix-web-codegen = "0.5.0-beta.1"
-actix-http = "3.0.0-beta.3"
-awc = { version = "3.0.0-beta.2", default-features = false }
+actix-web-codegen = "0.5.0-beta.2"
+actix-http = "3.0.0-beta.8"
ahash = "0.7"
bytes = "1"
+cfg-if = "1"
+cookie = { version = "0.15", features = ["percent-encode"], optional = true }
derive_more = "0.99.5"
either = "1.5.3"
encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false }
+itoa = "0.4"
+language-tags = "0.3"
+once_cell = "1.5"
log = "0.4"
mime = "0.3"
+paste = "1"
pin-project = "1.0.0"
regex = "1.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_urlencoded = "0.7"
smallvec = "1.6"
-socket2 = "0.3.16"
+socket2 = "0.4.0"
time = { version = "0.2.23", default-features = false, features = ["std"] }
-tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
-tls-rustls = { package = "rustls", version = "0.19.0", optional = true }
url = "2.1"
-[target.'cfg(windows)'.dependencies.tls-openssl]
-version = "0.10.9"
-package = "openssl"
-features = ["vendored"]
-optional = true
-
[dev-dependencies]
+actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] }
+awc = { version = "3.0.0-beta.7", features = ["openssl"] }
+
brotli2 = "0.3.2"
-criterion = "0.3"
+criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.8"
flate2 = "1.0.13"
+zstd = "0.7"
rand = "0.8"
rcgen = "0.8"
-serde_derive = "1.0"
+tls-openssl = { package = "openssl", version = "0.10.9" }
+tls-rustls = { package = "rustls", version = "0.19.0" }
[profile.release]
lto = true
@@ -132,15 +122,32 @@ opt-level = 3
codegen-units = 1
[patch.crates-io]
-actix-web = { path = "." }
+actix-files = { path = "actix-files" }
actix-http = { path = "actix-http" }
actix-http-test = { path = "actix-http-test" }
+actix-multipart = { path = "actix-multipart" }
+actix-test = { path = "actix-test" }
+actix-web = { path = "." }
actix-web-actors = { path = "actix-web-actors" }
actix-web-codegen = { path = "actix-web-codegen" }
-actix-multipart = { path = "actix-multipart" }
-actix-files = { path = "actix-files" }
awc = { path = "awc" }
+[[test]]
+name = "test_server"
+required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
+
+[[example]]
+name = "basic"
+required-features = ["compress-gzip"]
+
+[[example]]
+name = "uds"
+required-features = ["compress-gzip"]
+
+[[example]]
+name = "on_connect"
+required-features = []
+
[[bench]]
name = "server"
harness = false
diff --git a/LICENSE-MIT b/LICENSE-MIT
index 95938ef15..d559b1cd1 100644
--- a/LICENSE-MIT
+++ b/LICENSE-MIT
@@ -1,4 +1,4 @@
-Copyright (c) 2017 Actix Team
+Copyright (c) 2017-NOW Actix Team
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
diff --git a/MIGRATION.md b/MIGRATION.md
index e01702868..9c29b8db9 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -10,6 +10,18 @@
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
+* Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd).
+ By default all compression algorithms are enabled.
+ To select algorithm you want to include with `middleware::Compress` use following flags:
+ - `compress-brotli`
+ - `compress-gzip`
+ - `compress-zstd`
+ If you have set in your `Cargo.toml` dedicated `actix-web` features and you still want
+ to have compression enabled. Please change features selection like bellow:
+
+ Before: `"compress"`
+ After: `"compress-brotli", "compress-gzip", "compress-zstd"`
+
## 3.0.0
diff --git a/README.md b/README.md
index bf68e7961..309a18466 100644
--- a/README.md
+++ b/README.md
@@ -6,10 +6,10 @@
[](https://crates.io/crates/actix-web)
-[](https://docs.rs/actix-web/4.0.0-beta.2)
+[](https://docs.rs/actix-web/4.0.0-beta.8)
[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)

-[](https://deps.rs/crate/actix-web/4.0.0-beta.2)
+[](https://deps.rs/crate/actix-web/4.0.0-beta.8)
[](https://github.com/actix/actix-web/actions)
[](https://codecov.io/gh/actix/actix-web)
@@ -25,13 +25,13 @@
* Streaming and pipelining
* Keep-alive and slow requests handling
* Client/server [WebSockets](https://actix.rs/docs/websockets/) support
-* Transparent content compression/decompression (br, gzip, deflate)
+* Transparent content compression/decompression (br, gzip, deflate, zstd)
* Powerful [request routing](https://actix.rs/docs/url-dispatch/)
* Multipart streams
* Static assets
* SSL support using OpenSSL or Rustls
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
-* Includes an async [HTTP client](https://docs.rs/actix-web/latest/actix_web/client/index.html)
+* Includes an async [HTTP client](https://docs.rs/awc/)
* Runs on stable Rust 1.46+
## Documentation
@@ -71,18 +71,18 @@ async fn main() -> std::io::Result<()> {
### More examples
-* [Basic Setup](https://github.com/actix/examples/tree/master/basics/)
-* [Application State](https://github.com/actix/examples/tree/master/state/)
-* [JSON Handling](https://github.com/actix/examples/tree/master/json/)
-* [Multipart Streams](https://github.com/actix/examples/tree/master/multipart/)
-* [Diesel Integration](https://github.com/actix/examples/tree/master/diesel/)
-* [r2d2 Integration](https://github.com/actix/examples/tree/master/r2d2/)
-* [Simple WebSocket](https://github.com/actix/examples/tree/master/websocket/)
-* [Tera Templates](https://github.com/actix/examples/tree/master/template_tera/)
-* [Askama Templates](https://github.com/actix/examples/tree/master/template_askama/)
-* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/rustls/)
-* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/openssl/)
-* [WebSocket Chat](https://github.com/actix/examples/tree/master/websocket-chat/)
+* [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics/)
+* [Application State](https://github.com/actix/examples/tree/master/basics/state/)
+* [JSON Handling](https://github.com/actix/examples/tree/master/json/json/)
+* [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart/)
+* [Diesel Integration](https://github.com/actix/examples/tree/master/database_interactions/diesel/)
+* [r2d2 Integration](https://github.com/actix/examples/tree/master/database_interactions/r2d2/)
+* [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets/websocket/)
+* [Tera Templates](https://github.com/actix/examples/tree/master/template_engines/tera/)
+* [Askama Templates](https://github.com/actix/examples/tree/master/template_engines/askama/)
+* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/security/rustls/)
+* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/)
+* [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/)
You may consider checking out
[this directory](https://github.com/actix/examples/tree/master/) for more examples.
@@ -90,7 +90,7 @@ You may consider checking out
## Benchmarks
One of the fastest web frameworks available according to the
-[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r19).
+[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite).
## License
diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md
index 6e2a241ac..db047c44c 100644
--- a/actix-files/CHANGES.md
+++ b/actix-files/CHANGES.md
@@ -3,6 +3,36 @@
## Unreleased - 2021-xx-xx
+## 0.6.0-beta.6 - 2021-06-26
+* Added `Files::path_filter()`. [#2274]
+* `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228]
+
+[#2274]: https://github.com/actix/actix-web/pull/2274
+[#2228]: https://github.com/actix/actix-web/pull/2228
+
+
+## 0.6.0-beta.5 - 2021-06-17
+* `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
+* For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
+* `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
+* `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257]
+
+[#2135]: https://github.com/actix/actix-web/pull/2135
+[#2156]: https://github.com/actix/actix-web/pull/2156
+[#2225]: https://github.com/actix/actix-web/pull/2225
+[#2257]: https://github.com/actix/actix-web/pull/2257
+
+
+## 0.6.0-beta.4 - 2021-04-02
+* Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046]
+
+[#2046]: https://github.com/actix/actix-web/pull/2046
+
+
+## 0.6.0-beta.3 - 2021-03-09
+* No notable changes.
+
+
## 0.6.0-beta.2 - 2021-02-10
* Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887]
* Replace `v_htmlescape` with `askama_escape`. [#1953]
diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml
index 08b7b36fc..ef288215b 100644
--- a/actix-files/Cargo.toml
+++ b/actix-files/Cargo.toml
@@ -1,13 +1,11 @@
[package]
name = "actix-files"
-version = "0.6.0-beta.2"
+version = "0.6.0-beta.6"
authors = ["Nikolay Kim "]
description = "Static file serving for Actix Web"
-readme = "README.md"
keywords = ["actix", "http", "async", "futures"]
homepage = "https://actix.rs"
-repository = "https://github.com/actix/actix-web.git"
-documentation = "https://docs.rs/actix-files/"
+repository = "https://github.com/actix/actix-web"
categories = ["asynchronous", "web-programming::http-server"]
license = "MIT OR Apache-2.0"
edition = "2018"
@@ -17,14 +15,15 @@ name = "actix_files"
path = "src/lib.rs"
[dependencies]
-actix-web = { version = "4.0.0-beta.3", default-features = false }
-actix-service = "2.0.0-beta.4"
+actix-web = { version = "4.0.0-beta.8", default-features = false }
+actix-http = "3.0.0-beta.8"
+actix-service = "2.0.0"
+actix-utils = "3.0.0"
askama_escape = "0.10"
bitflags = "1"
bytes = "1"
-futures-core = { version = "0.3.7", default-features = false }
-futures-util = { version = "0.3.7", default-features = false }
+futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
http-range = "0.1.4"
derive_more = "0.99.5"
log = "0.4"
@@ -33,5 +32,6 @@ mime_guess = "2.0.1"
percent-encoding = "2.1"
[dev-dependencies]
-actix-rt = "2"
-actix-web = "4.0.0-beta.3"
+actix-rt = "2.2"
+actix-web = "4.0.0-beta.8"
+actix-test = "0.1.0-beta.3"
diff --git a/actix-files/README.md b/actix-files/README.md
index 463f20224..13c301c56 100644
--- a/actix-files/README.md
+++ b/actix-files/README.md
@@ -3,17 +3,16 @@
> Static file serving for Actix Web
[](https://crates.io/crates/actix-files)
-[](https://docs.rs/actix-files/0.5.0)
+[](https://docs.rs/actix-files/0.6.0-beta.6)
[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)

-[](https://deps.rs/crate/actix-files/0.5.0)
+[](https://deps.rs/crate/actix-files/0.6.0-beta.6)
[](https://crates.io/crates/actix-files)
-[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-files/)
-- [Example Project](https://github.com/actix/examples/tree/master/static_index)
-- [Chat on Gitter](https://gitter.im/actix/actix-web)
+- [Example Project](https://github.com/actix/examples/tree/master/basics/static_index)
- Minimum supported Rust version: 1.46 or later
diff --git a/actix-files/src/error.rs b/actix-files/src/error.rs
index 9b30cbaa2..e5f2d4779 100644
--- a/actix-files/src/error.rs
+++ b/actix-files/src/error.rs
@@ -1,4 +1,4 @@
-use actix_web::{http::StatusCode, HttpResponse, ResponseError};
+use actix_web::{http::StatusCode, ResponseError};
use derive_more::Display;
/// Errors which can occur when serving static files.
@@ -16,8 +16,8 @@ pub enum FilesError {
/// Return `NotFound` for `FilesError`
impl ResponseError for FilesError {
- fn error_response(&self) -> HttpResponse {
- HttpResponse::new(StatusCode::NOT_FOUND)
+ fn status_code(&self) -> StatusCode {
+ StatusCode::NOT_FOUND
}
}
diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs
index 6f8b28bbf..49d81eb03 100644
--- a/actix-files/src/files.rs
+++ b/actix-files/src/files.rs
@@ -1,25 +1,34 @@
-use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc};
+use std::{
+ cell::RefCell,
+ fmt, io,
+ path::{Path, PathBuf},
+ rc::Rc,
+};
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
+use actix_utils::future::ok;
use actix_web::{
- dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse},
+ dev::{
+ AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest,
+ ServiceResponse,
+ },
error::Error,
guard::Guard,
http::header::DispositionType,
HttpRequest,
};
-use futures_util::future::{ok, FutureExt, LocalBoxFuture};
+use futures_core::future::LocalBoxFuture;
use crate::{
directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService,
- MimeOverride,
+ MimeOverride, PathFilter,
};
/// Static files handling service.
///
/// `Files` service must be registered with `App::service()` method.
///
-/// ```rust
+/// ```
/// use actix_web::App;
/// use actix_files::Files;
///
@@ -35,8 +44,10 @@ pub struct Files {
default: Rc>>>,
renderer: Rc,
mime_override: Option>,
+ path_filter: Option>,
file_flags: named::Flags,
- guards: Option>,
+ use_guards: Option>,
+ guards: Vec>,
hidden_files: bool,
}
@@ -58,6 +69,8 @@ impl Clone for Files {
file_flags: self.file_flags,
path: self.path.clone(),
mime_override: self.mime_override.clone(),
+ path_filter: self.path_filter.clone(),
+ use_guards: self.use_guards.clone(),
guards: self.guards.clone(),
hidden_files: self.hidden_files,
}
@@ -79,10 +92,9 @@ impl Files {
/// If the mount path is set as the root path `/`, services registered after this one will
/// be inaccessible. Register more specific handlers and services first.
///
- /// `Files` uses a threadpool for blocking filesystem operations. By default, the pool uses a
- /// max number of threads equal to `512 * HttpServer::worker`. Real time thread count are
- /// adjusted with work load. More threads would spawn when need and threads goes idle for a
- /// period of time would be de-spawned.
+ /// `Files` utilizes the existing Tokio thread-pool for blocking filesystem operations.
+ /// The number of running threads is adjusted over time as needed, up to a maximum of 512 times
+ /// the number of server [workers](actix_web::HttpServer::workers), by default.
pub fn new>(mount_path: &str, serve_from: T) -> Files {
let orig_dir = serve_from.into();
let dir = match orig_dir.canonicalize() {
@@ -102,8 +114,10 @@ impl Files {
default: Rc::new(RefCell::new(None)),
renderer: Rc::new(directory_listing),
mime_override: None,
+ path_filter: None,
file_flags: named::Flags::default(),
- guards: None,
+ use_guards: None,
+ guards: Vec::new(),
hidden_files: false,
}
}
@@ -111,6 +125,9 @@ impl Files {
/// Show files listing for directories.
///
/// By default show files listing is disabled.
+ ///
+ /// When used with [`Files::index_file()`], files listing is shown as a fallback
+ /// when the index file is not found.
pub fn show_files_listing(mut self) -> Self {
self.show_index = true;
self
@@ -143,10 +160,45 @@ impl Files {
self
}
+ /// Sets path filtering closure.
+ ///
+ /// The path provided to the closure is relative to `serve_from` path.
+ /// You can safely join this path with the `serve_from` path to get the real path.
+ /// However, the real path may not exist since the filter is called before checking path existence.
+ ///
+ /// When a path doesn't pass the filter, [`Files::default_handler`] is called if set, otherwise,
+ /// `404 Not Found` is returned.
+ ///
+ /// # Examples
+ /// ```
+ /// use std::path::Path;
+ /// use actix_files::Files;
+ ///
+ /// // prevent searching subdirectories and following symlinks
+ /// let files_service = Files::new("/", "./static").path_filter(|path, _| {
+ /// path.components().count() == 1
+ /// && Path::new("./static")
+ /// .join(path)
+ /// .symlink_metadata()
+ /// .map(|m| !m.file_type().is_symlink())
+ /// .unwrap_or(false)
+ /// });
+ /// ```
+ pub fn path_filter(mut self, f: F) -> Self
+ where
+ F: Fn(&Path, &RequestHead) -> bool + 'static,
+ {
+ self.path_filter = Some(Rc::new(f));
+ self
+ }
+
/// Set index file
///
- /// Shows specific index file for directory "/" instead of
+ /// Shows specific index file for directories instead of
/// showing files listing.
+ ///
+ /// If the index file is not found, files listing is shown as a fallback if
+ /// [`Files::show_files_listing()`] is set.
pub fn index_file>(mut self, index: T) -> Self {
self.index = Some(index.into());
self
@@ -155,7 +207,6 @@ impl Files {
/// Specifies whether to use ETag or not.
///
/// Default is true.
- #[inline]
pub fn use_etag(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::ETAG, value);
self
@@ -164,7 +215,6 @@ impl Files {
/// Specifies whether to use Last-Modified or not.
///
/// Default is true.
- #[inline]
pub fn use_last_modified(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::LAST_MD, value);
self
@@ -173,31 +223,74 @@ impl Files {
/// Specifies whether text responses should signal a UTF-8 encoding.
///
/// Default is false (but will default to true in a future version).
- #[inline]
pub fn prefer_utf8(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::PREFER_UTF8, value);
self
}
- /// Specifies custom guards to use for directory listings and files.
+ /// Adds a routing guard.
///
- /// Default behaviour allows GET and HEAD.
- #[inline]
- pub fn use_guards(mut self, guards: G) -> Self {
- self.guards = Some(Rc::new(guards));
+ /// Use this to allow multiple chained file services that respond to strictly different
+ /// properties of a request. Due to the way routing works, if a guard check returns true and the
+ /// request starts being handled by the file service, it will not be able to back-out and try
+ /// the next service, you will simply get a 404 (or 405) error response.
+ ///
+ /// To allow `POST` requests to retrieve files, see [`Files::use_guards`].
+ ///
+ /// # Examples
+ /// ```
+ /// use actix_web::{guard::Header, App};
+ /// use actix_files::Files;
+ ///
+ /// App::new().service(
+ /// Files::new("/","/my/site/files")
+ /// .guard(Header("Host", "example.com"))
+ /// );
+ /// ```
+ pub fn guard(mut self, guard: G) -> Self {
+ self.guards.push(Rc::new(guard));
self
}
+ /// Specifies guard to check before fetching directory listings or files.
+ ///
+ /// Note that this guard has no effect on routing; it's main use is to guard on the request's
+ /// method just before serving the file, only allowing `GET` and `HEAD` requests by default.
+ /// See [`Files::guard`] for routing guards.
+ pub fn method_guard(mut self, guard: G) -> Self {
+ self.use_guards = Some(Rc::new(guard));
+ self
+ }
+
+ #[doc(hidden)]
+ #[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")]
+ /// See [`Files::method_guard`].
+ pub fn use_guards(self, guard: G) -> Self {
+ self.method_guard(guard)
+ }
+
/// 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.
+ ///
+ /// # Examples
+ /// Setting a fallback static file handler:
+ /// ```
+ /// use actix_files::{Files, NamedFile};
+ ///
+ /// # fn run() -> Result<(), actix_web::Error> {
+ /// let files = Files::new("/", "./static")
+ /// .index_file("index.html")
+ /// .default_handler(NamedFile::open("./static/404.html")?);
+ /// # Ok(())
+ /// # }
+ /// ```
pub fn default_handler(mut self, f: F) -> Self
where
F: IntoServiceFactory,
@@ -217,7 +310,6 @@ impl Files {
}
/// Enables serving hidden files and directories, allowing a leading dots in url fragments.
- #[inline]
pub fn use_hidden_files(mut self) -> Self {
self.hidden_files = true;
self
@@ -225,7 +317,19 @@ impl Files {
}
impl HttpServiceFactory for Files {
- fn register(self, config: &mut AppService) {
+ fn register(mut self, config: &mut AppService) {
+ let guards = if self.guards.is_empty() {
+ None
+ } else {
+ let guards = std::mem::take(&mut self.guards);
+ Some(
+ guards
+ .into_iter()
+ .map(|guard| -> Box { Box::new(guard) })
+ .collect::>(),
+ )
+ };
+
if self.default.borrow().is_none() {
*self.default.borrow_mut() = Some(config.default_service());
}
@@ -236,7 +340,7 @@ impl HttpServiceFactory for Files {
ResourceDef::prefix(&self.path)
};
- config.register_service(rdef, None, self, None)
+ config.register_service(rdef, guards, self, None)
}
}
@@ -257,24 +361,25 @@ impl ServiceFactory for Files {
default: None,
renderer: self.renderer.clone(),
mime_override: self.mime_override.clone(),
+ path_filter: self.path_filter.clone(),
file_flags: self.file_flags,
- guards: self.guards.clone(),
+ guards: self.use_guards.clone(),
hidden_files: self.hidden_files,
};
if let Some(ref default) = *self.default.borrow() {
- default
- .new_service(())
- .map(move |result| match result {
+ let fut = default.new_service(());
+ Box::pin(async {
+ match fut.await {
Ok(default) => {
srv.default = Some(default);
Ok(srv)
}
Err(_) => Err(()),
- })
- .boxed_local()
+ }
+ })
} else {
- ok(srv).boxed_local()
+ Box::pin(ok(srv))
}
}
}
diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs
index 04dd9f07f..1eb091aaf 100644
--- a/actix-files/src/lib.rs
+++ b/actix-files/src/lib.rs
@@ -3,7 +3,7 @@
//! Provides a non-blocking service for serving static files from disk.
//!
//! # Example
-//! ```rust
+//! ```
//! use actix_web::App;
//! use actix_files::Files;
//!
@@ -16,11 +16,12 @@
use actix_service::boxed::{BoxService, BoxServiceFactory};
use actix_web::{
- dev::{ServiceRequest, ServiceResponse},
+ dev::{RequestHead, ServiceRequest, ServiceResponse},
error::Error,
http::header::DispositionType,
};
use mime_guess::from_ext;
+use std::path::Path;
mod chunked;
mod directory;
@@ -56,6 +57,8 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
+type PathFilter = dyn Fn(&Path, &RequestHead) -> bool;
+
#[cfg(test)]
mod tests {
use std::{
@@ -65,6 +68,7 @@ mod tests {
};
use actix_service::ServiceFactory;
+ use actix_utils::future::ok;
use actix_web::{
guard,
http::{
@@ -76,7 +80,6 @@ mod tests {
web::{self, Bytes},
App, HttpResponse, Responder,
};
- use futures_util::future::ok;
use super::*;
@@ -279,6 +282,22 @@ mod tests {
);
}
+ #[actix_rt::test]
+ async fn test_named_file_javascript() {
+ let file = NamedFile::open("tests/test.js").unwrap();
+
+ let req = TestRequest::default().to_http_request();
+ let resp = file.respond_to(&req).await.unwrap();
+ assert_eq!(
+ resp.headers().get(header::CONTENT_TYPE).unwrap(),
+ "application/javascript"
+ );
+ assert_eq!(
+ resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
+ "inline; filename=\"test.js\""
+ );
+ }
+
#[actix_rt::test]
async fn test_named_file_image_attachment() {
let cd = ContentDisposition {
@@ -413,7 +432,7 @@ mod tests {
#[actix_rt::test]
async fn test_named_file_content_range_headers() {
- let srv = test::start(|| App::new().service(Files::new("/", ".")));
+ let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
// Valid range header
let response = srv
@@ -438,7 +457,7 @@ mod tests {
#[actix_rt::test]
async fn test_named_file_content_length_headers() {
- let srv = test::start(|| App::new().service(Files::new("/", ".")));
+ let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
// Valid range header
let response = srv
@@ -477,7 +496,7 @@ mod tests {
#[actix_rt::test]
async fn test_head_content_length_headers() {
- let srv = test::start(|| App::new().service(Files::new("/", ".")));
+ let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
let response = srv.head("/tests/test.binary").send().await.unwrap();
@@ -532,7 +551,7 @@ mod tests {
#[actix_rt::test]
async fn test_files_guards() {
let srv = test::init_service(
- App::new().service(Files::new("/", ".").use_guards(guard::Post())),
+ App::new().service(Files::new("/", ".").method_guard(guard::Post())),
)
.await;
@@ -632,7 +651,7 @@ mod tests {
#[actix_rt::test]
async fn test_redirect_to_slash_directory() {
- // should not redirect if no index
+ // should not redirect if no index and files listing is disabled
let srv = test::init_service(
App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
)
@@ -654,6 +673,19 @@ mod tests {
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
+ // should redirect if files listing is enabled
+ let srv = test::init_service(
+ App::new().service(
+ Files::new("/", ".")
+ .show_files_listing()
+ .redirect_to_slash_directory(),
+ ),
+ )
+ .await;
+ let req = TestRequest::with_uri("/tests").to_request();
+ let resp = test::call_service(&srv, req).await;
+ assert_eq!(resp.status(), StatusCode::FOUND);
+
// should not redirect if the path is wrong
let req = TestRequest::with_uri("/not_existing").to_request();
let resp = test::call_service(&srv, req).await;
@@ -662,8 +694,12 @@ mod tests {
#[actix_rt::test]
async fn test_static_files_bad_directory() {
- let _st: Files = Files::new("/", "missing");
- let _st: Files = Files::new("/", "Cargo.toml");
+ let service = Files::new("/", "./missing").new_service(()).await.unwrap();
+
+ let req = TestRequest::with_uri("/").to_srv_request();
+ let resp = test::call_service(&service, req).await;
+
+ assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[actix_rt::test]
@@ -676,75 +712,34 @@ mod tests {
.await
.unwrap();
let req = TestRequest::with_uri("/missing").to_srv_request();
-
let resp = test::call_service(&st, req).await;
+
assert_eq!(resp.status(), StatusCode::OK);
let bytes = test::read_body(resp).await;
assert_eq!(bytes, web::Bytes::from_static(b"default content"));
}
- // #[actix_rt::test]
- // async fn test_serve_index() {
- // let st = Files::new(".").index_file("test.binary");
- // let req = TestRequest::default().uri("/tests").finish();
+ #[actix_rt::test]
+ async fn test_serve_index_nested() {
+ let service = Files::new(".", ".")
+ .index_file("lib.rs")
+ .new_service(())
+ .await
+ .unwrap();
- // let resp = st.handle(&req).respond_to(&req).unwrap();
- // let resp = resp.as_msg();
- // assert_eq!(resp.status(), StatusCode::OK);
- // assert_eq!(
- // resp.headers()
- // .get(header::CONTENT_TYPE)
- // .expect("content type"),
- // "application/octet-stream"
- // );
- // assert_eq!(
- // resp.headers()
- // .get(header::CONTENT_DISPOSITION)
- // .expect("content disposition"),
- // "attachment; filename=\"test.binary\""
- // );
+ let req = TestRequest::default().uri("/src").to_srv_request();
+ let resp = test::call_service(&service, req).await;
- // let req = TestRequest::default().uri("/tests/").finish();
- // let resp = st.handle(&req).respond_to(&req).unwrap();
- // let resp = resp.as_msg();
- // assert_eq!(resp.status(), StatusCode::OK);
- // assert_eq!(
- // resp.headers().get(header::CONTENT_TYPE).unwrap(),
- // "application/octet-stream"
- // );
- // assert_eq!(
- // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
- // "attachment; filename=\"test.binary\""
- // );
-
- // // nonexistent index file
- // let req = TestRequest::default().uri("/tests/unknown").finish();
- // let resp = st.handle(&req).respond_to(&req).unwrap();
- // let resp = resp.as_msg();
- // assert_eq!(resp.status(), StatusCode::NOT_FOUND);
-
- // let req = TestRequest::default().uri("/tests/unknown/").finish();
- // let resp = st.handle(&req).respond_to(&req).unwrap();
- // let resp = resp.as_msg();
- // assert_eq!(resp.status(), StatusCode::NOT_FOUND);
- // }
-
- // #[actix_rt::test]
- // async fn test_serve_index_nested() {
- // let st = Files::new(".").index_file("mod.rs");
- // let req = TestRequest::default().uri("/src/client").finish();
- // let resp = st.handle(&req).respond_to(&req).unwrap();
- // let resp = resp.as_msg();
- // assert_eq!(resp.status(), StatusCode::OK);
- // assert_eq!(
- // resp.headers().get(header::CONTENT_TYPE).unwrap(),
- // "text/x-rust"
- // );
- // assert_eq!(
- // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
- // "inline; filename=\"mod.rs\""
- // );
- // }
+ assert_eq!(resp.status(), StatusCode::OK);
+ assert_eq!(
+ resp.headers().get(header::CONTENT_TYPE).unwrap(),
+ "text/x-rust"
+ );
+ assert_eq!(
+ resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
+ "inline; filename=\"lib.rs\""
+ );
+ }
#[actix_rt::test]
async fn integration_serve_index() {
@@ -791,4 +786,158 @@ mod tests {
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
}
+
+ #[actix_rt::test]
+ async fn test_serve_named_file() {
+ let srv =
+ test::init_service(App::new().service(NamedFile::open("Cargo.toml").unwrap()))
+ .await;
+
+ let req = TestRequest::get().uri("/Cargo.toml").to_request();
+ let res = test::call_service(&srv, req).await;
+ assert_eq!(res.status(), StatusCode::OK);
+
+ let bytes = test::read_body(res).await;
+ let data = Bytes::from(fs::read("Cargo.toml").unwrap());
+ assert_eq!(bytes, data);
+
+ let req = TestRequest::get().uri("/test/unknown").to_request();
+ let res = test::call_service(&srv, req).await;
+ assert_eq!(res.status(), StatusCode::NOT_FOUND);
+ }
+
+ #[actix_rt::test]
+ async fn test_serve_named_file_prefix() {
+ let srv = test::init_service(
+ App::new()
+ .service(web::scope("/test").service(NamedFile::open("Cargo.toml").unwrap())),
+ )
+ .await;
+
+ let req = TestRequest::get().uri("/test/Cargo.toml").to_request();
+ let res = test::call_service(&srv, req).await;
+ assert_eq!(res.status(), StatusCode::OK);
+
+ let bytes = test::read_body(res).await;
+ let data = Bytes::from(fs::read("Cargo.toml").unwrap());
+ assert_eq!(bytes, data);
+
+ let req = TestRequest::get().uri("/Cargo.toml").to_request();
+ let res = test::call_service(&srv, req).await;
+ assert_eq!(res.status(), StatusCode::NOT_FOUND);
+ }
+
+ #[actix_rt::test]
+ async fn test_named_file_default_service() {
+ let srv = test::init_service(
+ App::new().default_service(NamedFile::open("Cargo.toml").unwrap()),
+ )
+ .await;
+
+ for route in ["/foobar", "/baz", "/"].iter() {
+ let req = TestRequest::get().uri(route).to_request();
+ let res = test::call_service(&srv, req).await;
+ assert_eq!(res.status(), StatusCode::OK);
+
+ let bytes = test::read_body(res).await;
+ let data = Bytes::from(fs::read("Cargo.toml").unwrap());
+ assert_eq!(bytes, data);
+ }
+ }
+
+ #[actix_rt::test]
+ async fn test_default_handler_named_file() {
+ let st = Files::new("/", ".")
+ .default_handler(NamedFile::open("Cargo.toml").unwrap())
+ .new_service(())
+ .await
+ .unwrap();
+ let req = TestRequest::with_uri("/missing").to_srv_request();
+ let resp = test::call_service(&st, req).await;
+
+ assert_eq!(resp.status(), StatusCode::OK);
+ let bytes = test::read_body(resp).await;
+ let data = Bytes::from(fs::read("Cargo.toml").unwrap());
+ assert_eq!(bytes, data);
+ }
+
+ #[actix_rt::test]
+ async fn test_symlinks() {
+ let srv = test::init_service(App::new().service(Files::new("test", "."))).await;
+
+ let req = TestRequest::get()
+ .uri("/test/tests/symlink-test.png")
+ .to_request();
+ let res = test::call_service(&srv, req).await;
+ assert_eq!(res.status(), StatusCode::OK);
+ assert_eq!(
+ res.headers().get(header::CONTENT_DISPOSITION).unwrap(),
+ "inline; filename=\"symlink-test.png\""
+ );
+ }
+
+ #[actix_rt::test]
+ async fn test_index_with_show_files_listing() {
+ let service = Files::new(".", ".")
+ .index_file("lib.rs")
+ .show_files_listing()
+ .new_service(())
+ .await
+ .unwrap();
+
+ // Serve the index if exists
+ let req = TestRequest::default().uri("/src").to_srv_request();
+ let resp = test::call_service(&service, req).await;
+ assert_eq!(resp.status(), StatusCode::OK);
+ assert_eq!(
+ resp.headers().get(header::CONTENT_TYPE).unwrap(),
+ "text/x-rust"
+ );
+
+ // Show files listing, otherwise.
+ let req = TestRequest::default().uri("/tests").to_srv_request();
+ let resp = test::call_service(&service, req).await;
+ assert_eq!(
+ resp.headers().get(header::CONTENT_TYPE).unwrap(),
+ "text/html; charset=utf-8"
+ );
+ let bytes = test::read_body(resp).await;
+ assert!(format!("{:?}", bytes).contains("/tests/test.png"));
+ }
+
+ #[actix_rt::test]
+ async fn test_path_filter() {
+ // prevent searching subdirectories
+ let st = Files::new("/", ".")
+ .path_filter(|path, _| path.components().count() == 1)
+ .new_service(())
+ .await
+ .unwrap();
+
+ let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
+ let resp = test::call_service(&st, req).await;
+ assert_eq!(resp.status(), StatusCode::OK);
+
+ let req = TestRequest::with_uri("/src/lib.rs").to_srv_request();
+ let resp = test::call_service(&st, req).await;
+ assert_eq!(resp.status(), StatusCode::NOT_FOUND);
+ }
+
+ #[actix_rt::test]
+ async fn test_default_handler_filter() {
+ let st = Files::new("/", ".")
+ .default_handler(|req: ServiceRequest| {
+ ok(req.into_response(HttpResponse::Ok().body("default content")))
+ })
+ .path_filter(|path, _| path.extension() == Some("png".as_ref()))
+ .new_service(())
+ .await
+ .unwrap();
+ let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
+ let resp = test::call_service(&st, req).await;
+
+ assert_eq!(resp.status(), StatusCode::OK);
+ let bytes = test::read_body(resp).await;
+ assert_eq!(bytes, web::Bytes::from_static(b"default content"));
+ }
}
diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs
index 697651bb4..0f0c5e5d1 100644
--- a/actix-files/src/named.rs
+++ b/actix-files/src/named.rs
@@ -1,3 +1,6 @@
+use actix_service::{Service, ServiceFactory};
+use actix_utils::future::{ok, ready, Ready};
+use actix_web::dev::{AppService, HttpServiceFactory, ResourceDef};
use std::fs::{File, Metadata};
use std::io;
use std::ops::{Deref, DerefMut};
@@ -8,14 +11,14 @@ use std::time::{SystemTime, UNIX_EPOCH};
use std::os::unix::fs::MetadataExt;
use actix_web::{
- dev::{BodyEncoding, SizedStream},
+ dev::{BodyEncoding, ServiceRequest, ServiceResponse, SizedStream},
http::{
header::{
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
},
ContentEncoding, StatusCode,
},
- HttpMessage, HttpRequest, HttpResponse, Responder,
+ Error, HttpMessage, HttpRequest, HttpResponse, Responder,
};
use bitflags::bitflags;
use mime_guess::from_path;
@@ -39,6 +42,29 @@ impl Default for Flags {
}
/// A file with an associated name.
+///
+/// `NamedFile` can be registered as services:
+/// ```
+/// use actix_web::App;
+/// use actix_files::NamedFile;
+///
+/// # fn run() -> Result<(), Box> {
+/// let app = App::new()
+/// .service(NamedFile::open("./static/index.html")?);
+/// # Ok(())
+/// # }
+/// ```
+///
+/// They can also be returned from handlers:
+/// ```
+/// use actix_web::{Responder, get};
+/// use actix_files::NamedFile;
+///
+/// #[get("/")]
+/// async fn index() -> impl Responder {
+/// NamedFile::open("./static/index.html")
+/// }
+/// ```
#[derive(Debug)]
pub struct NamedFile {
path: PathBuf,
@@ -60,7 +86,7 @@ impl NamedFile {
///
/// # Examples
///
- /// ```rust
+ /// ```
/// use actix_files::NamedFile;
/// use std::io::{self, Write};
/// use std::env;
@@ -94,6 +120,11 @@ impl NamedFile {
let disposition = match ct.type_() {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
+ mime::APPLICATION => match ct.subtype() {
+ mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
+ name if name == "wasm" => DispositionType::Inline,
+ _ => DispositionType::Attachment,
+ },
_ => DispositionType::Attachment,
};
@@ -137,7 +168,7 @@ impl NamedFile {
///
/// # Examples
///
- /// ```rust
+ /// ```
/// use actix_files::NamedFile;
///
/// let file = NamedFile::open("foo.txt");
@@ -156,7 +187,7 @@ impl NamedFile {
///
/// # Examples
///
- /// ```rust
+ /// ```
/// # use std::io;
/// use actix_files::NamedFile;
///
@@ -223,9 +254,11 @@ impl NamedFile {
/// Set the Content-Disposition for serving this file. This allows
/// changing the inline/attachment disposition as well as the filename
- /// sent to the peer. By default the disposition is `inline` for text,
- /// image, and video content types, and `attachment` otherwise, and
- /// the filename is taken from the path provided in the `open` method
+ /// sent to the peer.
+ ///
+ /// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and
+ /// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise,
+ /// and the filename is taken from the path provided in the `open` method
/// after converting it to UTF-8 using.
/// [`std::ffi::OsStr::to_string_lossy`]
#[inline]
@@ -245,6 +278,8 @@ impl NamedFile {
}
/// Set content encoding for serving this file
+ ///
+ /// Must be used with [`actix_web::middleware::Compress`] to take effect.
#[inline]
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
self.encoding = Some(enc);
@@ -356,8 +391,8 @@ impl NamedFile {
} else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) =
(last_modified, req.get_header())
{
- let t1: SystemTime = m.clone().into();
- let t2: SystemTime = since.clone().into();
+ let t1: SystemTime = (*m).into();
+ let t2: SystemTime = (*since).into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
(Ok(t1), Ok(t2)) => t1.as_secs() > t2.as_secs(),
@@ -375,8 +410,8 @@ impl NamedFile {
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
(last_modified, req.get_header())
{
- let t1: SystemTime = m.clone().into();
- let t2: SystemTime = since.clone().into();
+ let t1: SystemTime = (*m).into();
+ let t2: SystemTime = (*since).into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
(Ok(t1), Ok(t2)) => t1.as_secs() <= t2.as_secs(),
@@ -516,3 +551,53 @@ impl Responder for NamedFile {
self.into_response(req)
}
}
+
+impl ServiceFactory for NamedFile {
+ type Response = ServiceResponse;
+ type Error = Error;
+ type Config = ();
+ type InitError = ();
+ type Service = NamedFileService;
+ type Future = Ready>;
+
+ fn new_service(&self, _: ()) -> Self::Future {
+ ok(NamedFileService {
+ path: self.path.clone(),
+ })
+ }
+}
+
+#[doc(hidden)]
+#[derive(Debug)]
+pub struct NamedFileService {
+ path: PathBuf,
+}
+
+impl Service for NamedFileService {
+ type Response = ServiceResponse;
+ type Error = Error;
+ type Future = Ready>;
+
+ actix_service::always_ready!();
+
+ fn call(&self, req: ServiceRequest) -> Self::Future {
+ let (req, _) = req.into_parts();
+ ready(
+ NamedFile::open(&self.path)
+ .map_err(|e| e.into())
+ .map(|f| f.into_response(&req))
+ .map(|res| ServiceResponse::new(req, res)),
+ )
+ }
+}
+
+impl HttpServiceFactory for NamedFile {
+ fn register(self, config: &mut AppService) {
+ config.register_service(
+ ResourceDef::root_prefix(self.path.to_string_lossy().as_ref()),
+ None,
+ self,
+ None,
+ )
+ }
+}
diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs
index dd8e5b503..8a87acd5d 100644
--- a/actix-files/src/path_buf.rs
+++ b/actix-files/src/path_buf.rs
@@ -3,8 +3,8 @@ use std::{
str::FromStr,
};
+use actix_utils::future::{ready, Ready};
use actix_web::{dev::Payload, FromRequest, HttpRequest};
-use futures_util::future::{ready, Ready};
use crate::error::UriSegmentError;
diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs
index 14eea6ebc..09122c63e 100644
--- a/actix-files/src/service.rs
+++ b/actix-files/src/service.rs
@@ -1,6 +1,7 @@
-use std::{fmt, io, path::PathBuf, rc::Rc, task::Poll};
+use std::{fmt, io, path::PathBuf, rc::Rc};
use actix_service::Service;
+use actix_utils::future::ok;
use actix_web::{
dev::{ServiceRequest, ServiceResponse},
error::Error,
@@ -8,11 +9,11 @@ use actix_web::{
http::{header, Method},
HttpResponse,
};
-use futures_util::future::{ok, Either, LocalBoxFuture, Ready};
+use futures_core::future::LocalBoxFuture;
use crate::{
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile,
- PathBufWrap,
+ PathBufWrap, PathFilter,
};
/// Assembled file serving service.
@@ -24,24 +25,24 @@ pub struct FilesService {
pub(crate) default: Option,
pub(crate) renderer: Rc,
pub(crate) mime_override: Option>,
+ pub(crate) path_filter: Option>,
pub(crate) file_flags: named::Flags,
pub(crate) guards: Option>,
pub(crate) hidden_files: bool,
}
-type FilesServiceFuture = Either<
- Ready>,
- LocalBoxFuture<'static, Result>,
->;
-
impl FilesService {
- fn handle_err(&self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture {
- log::debug!("Failed to handle {}: {}", req.path(), e);
+ fn handle_err(
+ &self,
+ err: io::Error,
+ req: ServiceRequest,
+ ) -> LocalBoxFuture<'static, Result> {
+ log::debug!("error handling {}: {}", req.path(), err);
if let Some(ref default) = self.default {
- Either::Right(default.call(req))
+ Box::pin(default.call(req))
} else {
- Either::Left(ok(req.error_response(e)))
+ Box::pin(ok(req.error_response(err)))
}
}
}
@@ -55,7 +56,7 @@ impl fmt::Debug for FilesService {
impl Service for FilesService {
type Response = ServiceResponse;
type Error = Error;
- type Future = FilesServiceFuture;
+ type Future = LocalBoxFuture<'static, Result>;
actix_service::always_ready!();
@@ -69,7 +70,7 @@ impl Service for FilesService {
};
if !is_method_valid {
- return Either::Left(ok(req.into_response(
+ return Box::pin(ok(req.into_response(
actix_web::HttpResponse::MethodNotAllowed()
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
.body("Request did not meet this resource's requirements."),
@@ -79,60 +80,76 @@ impl Service for FilesService {
let real_path =
match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) {
Ok(item) => item,
- Err(e) => return Either::Left(ok(req.error_response(e))),
+ Err(e) => return Box::pin(ok(req.error_response(e))),
};
+ if let Some(filter) = &self.path_filter {
+ if !filter(real_path.as_ref(), req.head()) {
+ if let Some(ref default) = self.default {
+ return Box::pin(default.call(req));
+ } else {
+ return Box::pin(ok(
+ req.into_response(actix_web::HttpResponse::NotFound().finish())
+ ));
+ }
+ }
+ }
+
// full file path
- let path = match self.directory.join(&real_path).canonicalize() {
- Ok(path) => path,
- Err(e) => return self.handle_err(e, req),
- };
+ let path = self.directory.join(&real_path);
+ if let Err(err) = path.canonicalize() {
+ return Box::pin(self.handle_err(err, 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());
+ if self.redirect_to_slash
+ && !req.path().ends_with('/')
+ && (self.index.is_some() || self.show_index)
+ {
+ let redirect_to = format!("{}/", req.path());
- return Either::Left(ok(req.into_response(
- HttpResponse::Found()
- .insert_header((header::LOCATION, redirect_to))
- .body("")
- .into_body(),
- )));
+ return Box::pin(ok(req.into_response(
+ HttpResponse::Found()
+ .insert_header((header::LOCATION, redirect_to))
+ .finish(),
+ )));
+ }
+
+ let serve_named_file = |req: ServiceRequest, mut named_file: NamedFile| {
+ 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 path = path.join(redir_index);
+ let (req, _) = req.into_parts();
+ let res = named_file.into_response(&req);
+ Box::pin(ok(ServiceResponse::new(req, res)))
+ };
- 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();
- let res = named_file.into_response(&req);
- Either::Left(ok(ServiceResponse::new(req, res)))
- }
- Err(e) => self.handle_err(e, req),
- }
- } else if self.show_index {
- let dir = Directory::new(self.directory.clone(), path);
+ let show_index = |req: ServiceRequest| {
+ let dir = Directory::new(self.directory.clone(), path.clone());
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(
+ Box::pin(match x {
+ Ok(resp) => ok(resp),
+ Err(err) => ok(ServiceResponse::from_err(err, req)),
+ })
+ };
+
+ match self.index {
+ Some(ref index) => match NamedFile::open(path.join(index)) {
+ Ok(named_file) => serve_named_file(req, named_file),
+ Err(_) if self.show_index => show_index(req),
+ Err(err) => self.handle_err(err, req),
+ },
+ None if self.show_index => show_index(req),
+ _ => Box::pin(ok(ServiceResponse::from_err(
FilesError::IsDirectory,
req.into_parts().0,
- )))
+ ))),
}
} else {
match NamedFile::open(path) {
@@ -145,9 +162,9 @@ impl Service for FilesService {
let (req, _) = req.into_parts();
let res = named_file.into_response(&req);
- Either::Left(ok(ServiceResponse::new(req, res)))
+ Box::pin(ok(ServiceResponse::new(req, res)))
}
- Err(e) => self.handle_err(e, req),
+ Err(err) => self.handle_err(err, req),
}
}
}
diff --git a/actix-files/tests/fixtures/guards/first/index.txt b/actix-files/tests/fixtures/guards/first/index.txt
new file mode 100644
index 000000000..fe4f02ad0
--- /dev/null
+++ b/actix-files/tests/fixtures/guards/first/index.txt
@@ -0,0 +1 @@
+first
\ No newline at end of file
diff --git a/actix-files/tests/fixtures/guards/second/index.txt b/actix-files/tests/fixtures/guards/second/index.txt
new file mode 100644
index 000000000..2147e4188
--- /dev/null
+++ b/actix-files/tests/fixtures/guards/second/index.txt
@@ -0,0 +1 @@
+second
\ No newline at end of file
diff --git a/actix-files/tests/guard.rs b/actix-files/tests/guard.rs
new file mode 100644
index 000000000..8b1785e7f
--- /dev/null
+++ b/actix-files/tests/guard.rs
@@ -0,0 +1,36 @@
+use actix_files::Files;
+use actix_web::{
+ guard::Host,
+ http::StatusCode,
+ test::{self, TestRequest},
+ App,
+};
+use bytes::Bytes;
+
+#[actix_rt::test]
+async fn test_guard_filter() {
+ let srv = test::init_service(
+ App::new()
+ .service(Files::new("/", "./tests/fixtures/guards/first").guard(Host("first.com")))
+ .service(
+ Files::new("/", "./tests/fixtures/guards/second").guard(Host("second.com")),
+ ),
+ )
+ .await;
+
+ let req = TestRequest::with_uri("/index.txt")
+ .append_header(("Host", "first.com"))
+ .to_request();
+ let res = test::call_service(&srv, req).await;
+
+ assert_eq!(res.status(), StatusCode::OK);
+ assert_eq!(test::read_body(res).await, Bytes::from("first"));
+
+ let req = TestRequest::with_uri("/index.txt")
+ .append_header(("Host", "second.com"))
+ .to_request();
+ let res = test::call_service(&srv, req).await;
+
+ assert_eq!(res.status(), StatusCode::OK);
+ assert_eq!(test::read_body(res).await, Bytes::from("second"));
+}
diff --git a/actix-files/tests/symlink-test.png b/actix-files/tests/symlink-test.png
new file mode 120000
index 000000000..65c0dcfd6
--- /dev/null
+++ b/actix-files/tests/symlink-test.png
@@ -0,0 +1 @@
+test.png
\ No newline at end of file
diff --git a/actix-files/tests/test.js b/actix-files/tests/test.js
new file mode 100644
index 000000000..2ee135561
--- /dev/null
+++ b/actix-files/tests/test.js
@@ -0,0 +1 @@
+// this file is empty.
diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md
index 2f47d700d..1dbd9a15b 100644
--- a/actix-http-test/CHANGES.md
+++ b/actix-http-test/CHANGES.md
@@ -3,6 +3,16 @@
## Unreleased - 2021-xx-xx
+## 3.0.0-beta.4 - 2021-04-02
+* Added `TestServer::client_headers` method. [#2097]
+
+[#2097]: https://github.com/actix/actix-web/pull/2097
+
+
+## 3.0.0-beta.3 - 2021-03-09
+* No notable changes.
+
+
## 3.0.0-beta.2 - 2021-02-10
* No notable changes.
diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml
index 6dcf73637..c04b5da49 100644
--- a/actix-http-test/Cargo.toml
+++ b/actix-http-test/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "actix-http-test"
-version = "3.0.0-beta.2"
+version = "3.0.0-beta.4"
authors = ["Nikolay Kim "]
description = "Various helpers for Actix applications to use during testing"
readme = "README.md"
@@ -29,20 +29,20 @@ default = []
openssl = ["tls-openssl", "awc/openssl"]
[dependencies]
-actix-service = "2.0.0-beta.4"
-actix-codec = "0.4.0-beta.1"
-actix-tls = "3.0.0-beta.3"
-actix-utils = "3.0.0-beta.2"
-actix-rt = "2"
+actix-service = "2.0.0"
+actix-codec = "0.4.0"
+actix-tls = "3.0.0-beta.5"
+actix-utils = "3.0.0"
+actix-rt = "2.2"
actix-server = "2.0.0-beta.3"
-awc = "3.0.0-beta.2"
+awc = { version = "3.0.0-beta.7", default-features = false }
base64 = "0.13"
bytes = "1"
futures-core = { version = "0.3.7", default-features = false }
http = "0.2.2"
log = "0.4"
-socket2 = "0.3"
+socket2 = "0.4"
serde = "1.0"
serde_json = "1.0"
slab = "0.4"
@@ -50,12 +50,6 @@ serde_urlencoded = "0.7"
time = { version = "0.2.23", default-features = false, features = ["std"] }
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
-[target.'cfg(windows)'.dependencies.tls-openssl]
-version = "0.10.9"
-package = "openssl"
-features = ["vendored"]
-optional = true
-
[dev-dependencies]
-actix-web = "4.0.0-beta.3"
-actix-http = "3.0.0-beta.3"
+actix-web = { version = "4.0.0-beta.8", default-features = false, features = ["cookies"] }
+actix-http = "3.0.0-beta.8"
diff --git a/actix-http-test/README.md b/actix-http-test/README.md
index 66f15979d..74260a352 100644
--- a/actix-http-test/README.md
+++ b/actix-http-test/README.md
@@ -3,13 +3,15 @@
> Various helpers for Actix applications to use during testing.
[](https://crates.io/crates/actix-http-test)
-[](https://docs.rs/actix-http-test/2.1.0)
+[](https://docs.rs/actix-http-test/3.0.0-beta.4)
+[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)

-[](https://deps.rs/crate/actix-http-test/2.1.0)
-[](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+
+[](https://deps.rs/crate/actix-http-test/3.0.0-beta.4)
+[](https://crates.io/crates/actix-http-test)
+[](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-http-test)
-- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum Supported Rust Version (MSRV): 1.46.0
diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs
index 8de07c8d3..0f126c99a 100644
--- a/actix-http-test/src/lib.rs
+++ b/actix-http-test/src/lib.rs
@@ -13,7 +13,9 @@ use std::{net, thread, time};
use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::{net::TcpStream, System};
use actix_server::{Server, ServiceFactory};
-use awc::{error::PayloadError, ws, Client, ClientRequest, ClientResponse, Connector};
+use awc::{
+ error::PayloadError, http::HeaderMap, ws, Client, ClientRequest, ClientResponse, Connector,
+};
use bytes::Bytes;
use futures_core::stream::Stream;
use http::Method;
@@ -26,7 +28,7 @@ use socket2::{Domain, Protocol, Socket, Type};
///
/// # Examples
///
-/// ```rust
+/// ```
/// use actix_http::HttpService;
/// use actix_http_test::TestServer;
/// use actix_web::{web, App, HttpResponse, Error};
@@ -115,16 +117,6 @@ pub async fn test_server_with_addr>(
}
}
-/// Get first available unused address
-pub fn unused_addr() -> net::SocketAddr {
- let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
- let socket = Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap();
- socket.bind(&addr.into()).unwrap();
- socket.set_reuse_address(true).unwrap();
- let tcp = socket.into_tcp_listener();
- tcp.local_addr().unwrap()
-}
-
/// Test server controller
pub struct TestServer {
addr: net::SocketAddr,
@@ -258,6 +250,14 @@ impl TestServer {
self.ws_at("/").await
}
+ /// Get default HeaderMap of Client.
+ ///
+ /// Returns Some(&mut HeaderMap) when Client object is unique
+ /// (No other clone of client exists at the same time).
+ pub fn client_headers(&mut self) -> Option<&mut HeaderMap> {
+ self.client.headers()
+ }
+
/// Stop HTTP server
fn stop(&mut self) {
self.system.stop();
@@ -269,3 +269,13 @@ impl Drop for TestServer {
self.stop()
}
}
+
+/// Get a localhost socket address with random, unused port.
+pub fn unused_addr() -> net::SocketAddr {
+ let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
+ let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP)).unwrap();
+ socket.bind(&addr.into()).unwrap();
+ socket.set_reuse_address(true).unwrap();
+ let tcp = net::TcpListener::from(socket);
+ tcp.local_addr().unwrap()
+}
diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md
index 6ba111eb3..8ead43718 100644
--- a/actix-http/CHANGES.md
+++ b/actix-http/CHANGES.md
@@ -1,15 +1,129 @@
# Changes
## Unreleased - 2021-xx-xx
+
+
+## 3.0.0-beta.8 - 2021-06-26
### Changed
-* Feature `cookies` is now optional and disabled by default. [#1981]
+* Change compression algorithm features flags. [#2250]
### Removed
-* re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994]
+* `downcast` and `downcast_get_type_id` macros. [#2291]
+
+[#2291]: https://github.com/actix/actix-web/pull/2291
+[#2250]: https://github.com/actix/actix-web/pull/2250
+
+
+## 3.0.0-beta.7 - 2021-06-17
+### Added
+* Alias `body::Body` as `body::AnyBody`. [#2215]
+* `BoxAnyBody`: a boxed message body with boxed errors. [#2183]
+* Re-export `http` crate's `Error` type as `error::HttpError`. [#2171]
+* Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171]
+* Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171]
+* `Response::into_body` that consumes response and returns body type. [#2201]
+* `impl Default` for `Response`. [#2201]
+* Add zstd support for `ContentEncoding`. [#2244]
+
+### Changed
+* The `MessageBody` trait now has an associated `Error` type. [#2183]
+* All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253]
+* All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253]
+* Places in `Response` where `ResponseBody` was received or returned now simply use `B`. [#2201]
+* `header` mod is now public. [#2171]
+* `uri` mod is now public. [#2171]
+* Update `language-tags` to `0.3`.
+* Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201]
+* `ResponseBuilder::message_body` now returns a `Result`. [#2201]
+* Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253]
+* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
+
+### Removed
+* Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171]
+* Down-casting for `MessageBody` types. [#2183]
+* `error::Result` alias. [#2201]
+* Error field from `Response` and `Response::error`. [#2205]
+* `impl Future` for `Response`. [#2201]
+* `Response::take_body` and old `Response::into_body` method that casted body type. [#2201]
+* `InternalError` and all the error types it constructed. [#2215]
+* Conversion (`impl Into`) of `Response` and `ResponseBuilder` to `Error`. [#2215]
+
+[#2171]: https://github.com/actix/actix-web/pull/2171
+[#2183]: https://github.com/actix/actix-web/pull/2183
+[#2196]: https://github.com/actix/actix-web/pull/2196
+[#2201]: https://github.com/actix/actix-web/pull/2201
+[#2205]: https://github.com/actix/actix-web/pull/2205
+[#2215]: https://github.com/actix/actix-web/pull/2215
+[#2253]: https://github.com/actix/actix-web/pull/2253
+[#2244]: https://github.com/actix/actix-web/pull/2244
+
+
+
+## 3.0.0-beta.6 - 2021-04-17
+### Added
+* `impl MessageBody for Pin>`. [#2152]
+* `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159]
+* Helper `body::to_bytes` for async collecting message body into Bytes. [#2158]
+
+### Changes
+* The type parameter of `Response` no longer has a default. [#2152]
+* The `Message` variant of `body::Body` is now `Pin>`. [#2152]
+* `BodyStream` and `SizedStream` are no longer restricted to Unpin types. [#2152]
+* Error enum types are marked `#[non_exhaustive]`. [#2161]
+
+### Removed
+* `cookies` feature flag. [#2065]
+* Top-level `cookies` mod (re-export). [#2065]
+* `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065]
+* `impl ResponseError for CookieParseError`. [#2065]
+* Deprecated methods on `ResponseBuilder`: `if_true`, `if_some`. [#2148]
+* `ResponseBuilder::json`. [#2148]
+* `ResponseBuilder::{set_header, header}`. [#2148]
+* `impl From for Body`. [#2148]
+* `Response::build_from`. [#2159]
+* Most of the status code builders on `Response`. [#2159]
+
+[#2065]: https://github.com/actix/actix-web/pull/2065
+[#2148]: https://github.com/actix/actix-web/pull/2148
+[#2152]: https://github.com/actix/actix-web/pull/2152
+[#2159]: https://github.com/actix/actix-web/pull/2159
+[#2158]: https://github.com/actix/actix-web/pull/2158
+[#2161]: https://github.com/actix/actix-web/pull/2161
+
+
+## 3.0.0-beta.5 - 2021-04-02
+### Added
+* `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081]
+* `client::ConnectorService` as `client::Connector::finish` method's return type [#2081]
+* `client::ConnectionIo` trait alias [#2081]
+
+### Changed
+* `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063]
+
+### Removed
+* Common typed HTTP headers were moved to actix-web. [2094]
+* `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127]
+
+[#2063]: https://github.com/actix/actix-web/pull/2063
+[#2081]: https://github.com/actix/actix-web/pull/2081
+[#2094]: https://github.com/actix/actix-web/pull/2094
+[#2127]: https://github.com/actix/actix-web/pull/2127
+
+
+## 3.0.0-beta.4 - 2021-03-08
+### Changed
+* Feature `cookies` is now optional and disabled by default. [#1981]
+* `ws::hash_key` now returns array. [#2035]
+* `ResponseBuilder::json` now takes `impl Serialize`. [#2052]
+
+### Removed
+* Re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994]
* `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994]
[#1981]: https://github.com/actix/actix-web/pull/1981
[#1994]: https://github.com/actix/actix-web/pull/1994
+[#2035]: https://github.com/actix/actix-web/pull/2035
+[#2052]: https://github.com/actix/actix-web/pull/2052
## 3.0.0-beta.3 - 2021-02-10
diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml
index 78fb55079..a12fed4b9 100644
--- a/actix-http/Cargo.toml
+++ b/actix-http/Cargo.toml
@@ -1,13 +1,11 @@
[package]
name = "actix-http"
-version = "3.0.0-beta.3"
+version = "3.0.0-beta.8"
authors = ["Nikolay Kim "]
description = "HTTP primitives for the Actix ecosystem"
-readme = "README.md"
keywords = ["actix", "http", "framework", "async", "futures"]
homepage = "https://actix.rs"
-repository = "https://github.com/actix/actix-web.git"
-documentation = "https://docs.rs/actix-http/"
+repository = "https://github.com/actix/actix-web"
categories = ["network-programming", "asynchronous",
"web-programming::http-server",
"web-programming::websocket"]
@@ -16,7 +14,7 @@ edition = "2018"
[package.metadata.docs.rs]
# features that docs.rs will build with
-features = ["openssl", "rustls", "compress", "cookies", "secure-cookies"]
+features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
[lib]
name = "actix_http"
@@ -32,50 +30,48 @@ openssl = ["actix-tls/openssl"]
rustls = ["actix-tls/rustls"]
# enable compression support
-compress = ["flate2", "brotli2"]
-
-# support for cookies
-cookies = ["cookie"]
-
-# support for secure cookies
-secure-cookies = ["cookies", "cookie/secure"]
+compress-brotli = ["brotli2", "__compress"]
+compress-gzip = ["flate2", "__compress"]
+compress-zstd = ["zstd", "__compress"]
# trust-dns as client dns resolver
trust-dns = ["trust-dns-resolver"]
+# Internal (PRIVATE!) features used to aid testing and cheking feature status.
+# Don't rely on these whatsoever. They may disappear at anytime.
+__compress = []
+
[dependencies]
-actix-service = "2.0.0-beta.4"
-actix-codec = "0.4.0-beta.1"
-actix-utils = "3.0.0-beta.2"
-actix-rt = "2"
-actix-tls = "3.0.0-beta.2"
+actix-service = "2.0.0"
+actix-codec = "0.4.0"
+actix-utils = "3.0.0"
+actix-rt = "2.2"
+actix-tls = { version = "3.0.0-beta.5", features = ["accept", "connect"] }
ahash = "0.7"
base64 = "0.13"
bitflags = "1.2"
bytes = "1"
bytestring = "1"
-cfg-if = "1"
-cookie = { version = "0.14.1", features = ["percent-encode"], optional = true }
derive_more = "0.99.5"
encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
-h2 = "0.3.0"
+h2 = "0.3.1"
http = "0.2.2"
httparse = "1.3"
itoa = "0.4"
-language-tags = "0.2"
-lazy_static = "1.4"
+language-tags = "0.3"
+local-channel = "0.1"
+once_cell = "1.5"
log = "0.4"
mime = "0.3"
percent-encoding = "2.1"
pin-project = "1.0.0"
+pin-project-lite = "0.2"
rand = "0.8"
regex = "1.3"
serde = "1.0"
-serde_json = "1.0"
-serde_urlencoded = "0.7"
sha-1 = "0.9"
smallvec = "1.6"
time = { version = "0.2.23", default-features = false, features = ["std"] }
@@ -84,24 +80,27 @@ tokio = { version = "1.2", features = ["sync"] }
# compression
brotli2 = { version="0.3.2", optional = true }
flate2 = { version = "1.0.13", optional = true }
+zstd = { version = "0.7", optional = true }
trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies]
actix-server = "2.0.0-beta.3"
-actix-http-test = { version = "3.0.0-beta.2", features = ["openssl"] }
-actix-tls = { version = "3.0.0-beta.2", features = ["openssl"] }
-criterion = "0.3"
+actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] }
+actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] }
+async-stream = "0.3"
+criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.8"
rcgen = "0.8"
-serde_derive = "1.0"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
tls-openssl = { version = "0.10", package = "openssl" }
tls-rustls = { version = "0.19", package = "rustls" }
+webpki = { version = "0.21.0" }
-[target.'cfg(windows)'.dev-dependencies.tls-openssl]
-version = "0.10.9"
-package = "openssl"
-features = ["vendored"]
+[[example]]
+name = "ws"
+required-features = ["rustls"]
[[bench]]
name = "write-camel-case"
diff --git a/actix-http/README.md b/actix-http/README.md
index 881fbc8c5..de1ef0a9b 100644
--- a/actix-http/README.md
+++ b/actix-http/README.md
@@ -3,18 +3,17 @@
> HTTP primitives for the Actix ecosystem.
[](https://crates.io/crates/actix-http)
-[](https://docs.rs/actix-http/3.0.0-beta.3)
+[](https://docs.rs/actix-http/3.0.0-beta.8)
[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)

-[](https://deps.rs/crate/actix-http/3.0.0-beta.3)
+[](https://deps.rs/crate/actix-http/3.0.0-beta.8)
[](https://crates.io/crates/actix-http)
-[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-http)
-- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum Supported Rust Version (MSRV): 1.46.0
## Example
diff --git a/actix-http/benches/uninit-headers.rs b/actix-http/benches/uninit-headers.rs
index 83e74171c..53a2528ab 100644
--- a/actix-http/benches/uninit-headers.rs
+++ b/actix-http/benches/uninit-headers.rs
@@ -78,12 +78,12 @@ impl HeaderIndex {
// test cases taken from:
// https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs
-const REQ_SHORT: &'static [u8] = b"\
+const REQ_SHORT: &[u8] = b"\
GET / HTTP/1.0\r\n\
Host: example.com\r\n\
Cookie: session=60; user_id=1\r\n\r\n";
-const REQ: &'static [u8] = b"\
+const REQ: &[u8] = b"\
GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\
Host: www.kittyhell.com\r\n\
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n\
@@ -119,6 +119,8 @@ mod _original {
use std::mem::MaybeUninit;
pub fn parse_headers(src: &mut BytesMut) -> usize {
+ #![allow(clippy::uninit_assumed_init)]
+
let mut headers: [HeaderIndex; MAX_HEADERS] =
unsafe { MaybeUninit::uninit().assume_init() };
diff --git a/actix-http/examples/echo.rs b/actix-http/examples/echo.rs
index 90d768cbe..6cfe3a675 100644
--- a/actix-http/examples/echo.rs
+++ b/actix-http/examples/echo.rs
@@ -1,19 +1,17 @@
-use std::{env, io};
+use std::io;
-use actix_http::{Error, HttpService, Request, Response};
+use actix_http::{http::StatusCode, Error, HttpService, Request, Response};
use actix_server::Server;
use bytes::BytesMut;
-use futures_util::StreamExt;
+use futures_util::StreamExt as _;
use http::header::HeaderValue;
-use log::info;
#[actix_rt::main]
async fn main() -> io::Result<()> {
- env::set_var("RUST_LOG", "echo=info");
- env_logger::init();
+ env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
Server::build()
- .bind("echo", "127.0.0.1:8080", || {
+ .bind("echo", ("127.0.0.1", 8080), || {
HttpService::build()
.client_timeout(1000)
.client_disconnect(1000)
@@ -23,9 +21,10 @@ async fn main() -> io::Result<()> {
body.extend_from_slice(&item?);
}
- info!("request body: {:?}", body);
+ log::info!("request body: {:?}", body);
+
Ok::<_, Error>(
- Response::Ok()
+ Response::build(StatusCode::OK)
.insert_header((
"x-head",
HeaderValue::from_static("dummy value!"),
diff --git a/actix-http/examples/echo2.rs b/actix-http/examples/echo2.rs
index bc932ce8f..db195d65b 100644
--- a/actix-http/examples/echo2.rs
+++ b/actix-http/examples/echo2.rs
@@ -1,31 +1,30 @@
-use std::{env, io};
+use std::io;
-use actix_http::http::HeaderValue;
+use actix_http::{body::Body, http::HeaderValue, http::StatusCode};
use actix_http::{Error, HttpService, Request, Response};
use actix_server::Server;
use bytes::BytesMut;
-use futures_util::StreamExt;
-use log::info;
+use futures_util::StreamExt as _;
-async fn handle_request(mut req: Request) -> Result {
+async fn handle_request(mut req: Request) -> Result, Error> {
let mut body = BytesMut::new();
while let Some(item) = req.payload().next().await {
body.extend_from_slice(&item?)
}
- info!("request body: {:?}", body);
- Ok(Response::Ok()
+ log::info!("request body: {:?}", body);
+
+ Ok(Response::build(StatusCode::OK)
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
.body(body))
}
#[actix_rt::main]
async fn main() -> io::Result<()> {
- env::set_var("RUST_LOG", "echo=info");
- env_logger::init();
+ env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
Server::build()
- .bind("echo", "127.0.0.1:8080", || {
+ .bind("echo", ("127.0.0.1", 8080), || {
HttpService::build().finish(handle_request).tcp()
})?
.run()
diff --git a/actix-http/examples/hello-world.rs b/actix-http/examples/hello-world.rs
index a84e9aac6..9a593c66a 100644
--- a/actix-http/examples/hello-world.rs
+++ b/actix-http/examples/hello-world.rs
@@ -1,29 +1,28 @@
-use std::{env, io};
+use std::{convert::Infallible, io};
-use actix_http::{HttpService, Response};
+use actix_http::{http::StatusCode, HttpService, Response};
use actix_server::Server;
-use futures_util::future;
use http::header::HeaderValue;
-use log::info;
#[actix_rt::main]
async fn main() -> io::Result<()> {
- env::set_var("RUST_LOG", "hello_world=info");
- env_logger::init();
+ env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
Server::build()
- .bind("hello-world", "127.0.0.1:8080", || {
+ .bind("hello-world", ("127.0.0.1", 8080), || {
HttpService::build()
.client_timeout(1000)
.client_disconnect(1000)
- .finish(|_req| {
- info!("{:?}", _req);
- let mut res = Response::Ok();
+ .finish(|req| async move {
+ log::info!("{:?}", req);
+
+ let mut res = Response::build(StatusCode::OK);
res.insert_header((
"x-head",
HeaderValue::from_static("dummy value!"),
));
- future::ok::<_, ()>(res.body("Hello world!"))
+
+ Ok::<_, Infallible>(res.body("Hello world!"))
})
.tcp()
})?
diff --git a/actix-http/examples/streaming-error.rs b/actix-http/examples/streaming-error.rs
new file mode 100644
index 000000000..3988cbac2
--- /dev/null
+++ b/actix-http/examples/streaming-error.rs
@@ -0,0 +1,40 @@
+//! Example showing response body (chunked) stream erroring.
+//!
+//! Test using `nc` or `curl`.
+//! ```sh
+//! $ curl -vN 127.0.0.1:8080
+//! $ echo 'GET / HTTP/1.1\n\n' | nc 127.0.0.1 8080
+//! ```
+
+use std::{convert::Infallible, io, time::Duration};
+
+use actix_http::{body::BodyStream, HttpService, Response};
+use actix_server::Server;
+use async_stream::stream;
+use bytes::Bytes;
+
+#[actix_rt::main]
+async fn main() -> io::Result<()> {
+ env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
+
+ Server::build()
+ .bind("streaming-error", ("127.0.0.1", 8080), || {
+ HttpService::build()
+ .finish(|req| async move {
+ log::info!("{:?}", req);
+ let res = Response::ok();
+
+ Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {
+ yield Ok(Bytes::from("123"));
+ yield Ok(Bytes::from("456"));
+
+ actix_rt::time::sleep(Duration::from_millis(1000)).await;
+
+ yield Err(io::Error::new(io::ErrorKind::Other, ""));
+ })))
+ })
+ .tcp()
+ })?
+ .run()
+ .await
+}
diff --git a/actix-http/examples/ws.rs b/actix-http/examples/ws.rs
new file mode 100644
index 000000000..d3cedf870
--- /dev/null
+++ b/actix-http/examples/ws.rs
@@ -0,0 +1,106 @@
+//! Sets up a WebSocket server over TCP and TLS.
+//! Sends a heartbeat message every 4 seconds but does not respond to any incoming frames.
+
+extern crate tls_rustls as rustls;
+
+use std::{
+ io,
+ pin::Pin,
+ task::{Context, Poll},
+ time::Duration,
+};
+
+use actix_codec::Encoder;
+use actix_http::{body::BodyStream, error::Error, ws, HttpService, Request, Response};
+use actix_rt::time::{interval, Interval};
+use actix_server::Server;
+use bytes::{Bytes, BytesMut};
+use bytestring::ByteString;
+use futures_core::{ready, Stream};
+
+#[actix_rt::main]
+async fn main() -> io::Result<()> {
+ env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
+
+ Server::build()
+ .bind("tcp", ("127.0.0.1", 8080), || {
+ HttpService::build().h1(handler).tcp()
+ })?
+ .bind("tls", ("127.0.0.1", 8443), || {
+ HttpService::build().finish(handler).rustls(tls_config())
+ })?
+ .run()
+ .await
+}
+
+async fn handler(req: Request) -> Result>, Error> {
+ log::info!("handshaking");
+ let mut res = ws::handshake(req.head())?;
+
+ // handshake will always fail under HTTP/2
+
+ log::info!("responding");
+ Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))?)
+}
+
+struct Heartbeat {
+ codec: ws::Codec,
+ interval: Interval,
+}
+
+impl Heartbeat {
+ fn new(codec: ws::Codec) -> Self {
+ Self {
+ codec,
+ interval: interval(Duration::from_secs(4)),
+ }
+ }
+}
+
+impl Stream for Heartbeat {
+ type Item = Result;
+
+ fn poll_next(
+ mut self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ ) -> Poll