diff --git a/.appveyor.yml b/.appveyor.yml
index f9e79ce7c..2f0a4a7dd 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -1,43 +1,21 @@
environment:
global:
- PROJECT_NAME: actix
+ PROJECT_NAME: actix-web
matrix:
# Stable channel
- - TARGET: i686-pc-windows-gnu
- CHANNEL: 1.21.0
- - TARGET: i686-pc-windows-msvc
- CHANNEL: 1.21.0
- - TARGET: x86_64-pc-windows-gnu
- CHANNEL: 1.21.0
- - TARGET: x86_64-pc-windows-msvc
- CHANNEL: 1.21.0
- # Stable channel
- - TARGET: i686-pc-windows-gnu
- CHANNEL: stable
- TARGET: i686-pc-windows-msvc
CHANNEL: stable
- TARGET: x86_64-pc-windows-gnu
CHANNEL: stable
- TARGET: x86_64-pc-windows-msvc
CHANNEL: stable
- # Beta channel
- - TARGET: i686-pc-windows-gnu
- CHANNEL: beta
- - TARGET: i686-pc-windows-msvc
- CHANNEL: beta
- - TARGET: x86_64-pc-windows-gnu
- CHANNEL: beta
- - TARGET: x86_64-pc-windows-msvc
- CHANNEL: beta
# Nightly channel
- - TARGET: i686-pc-windows-gnu
- CHANNEL: nightly-2017-12-21
- TARGET: i686-pc-windows-msvc
- CHANNEL: nightly-2017-12-21
+ CHANNEL: nightly
- TARGET: x86_64-pc-windows-gnu
- CHANNEL: nightly-2017-12-21
+ CHANNEL: nightly
- TARGET: x86_64-pc-windows-msvc
- CHANNEL: nightly-2017-12-21
+ CHANNEL: nightly
# Install Rust and Cargo
# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml)
@@ -59,4 +37,5 @@ build: false
# Equivalent to Travis' `script` phase
test_script:
- - cargo test --no-default-features
+ - cargo clean
+ - cargo test --no-default-features --features="flate2-rust"
diff --git a/.travis.yml b/.travis.yml
index 7aa8ebaa9..f10f82a48 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,25 +1,18 @@
language: rust
-sudo: false
+sudo: required
dist: trusty
cache:
- cargo: true
+ # cargo: true
apt: true
matrix:
include:
- - rust: 1.21.0
- rust: stable
- rust: beta
- - rust: nightly
+ - rust: nightly-2019-11-20
allow_failures:
- - rust: nightly
-
-#rust:
-# - 1.21.0
-# - stable
-# - beta
-# - nightly-2018-01-03
+ - rust: nightly-2019-11-20
env:
global:
@@ -29,67 +22,40 @@ env:
before_install:
- sudo add-apt-repository -y ppa:0k53d-karl-f830m/openssl
- sudo apt-get update -qq
- - sudo apt-get install -qq libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev
+ - sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev
+
+before_cache: |
+ if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-11-20" ]]; then
+ RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install --version 0.6.11 cargo-tarpaulin
+ fi
# Add clippy
before_script:
- - |
- if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then
- ( ( cargo install clippy && export CLIPPY=true ) || export CLIPPY=false );
- fi
- export PATH=$PATH:~/.cargo/bin
script:
+ - cargo update
+ - cargo check --all --no-default-features
- |
- if [[ "$TRAVIS_RUST_VERSION" == "stable" ]]; then
- cargo clean
- USE_SKEPTIC=1 cargo test --features=alpn
- else
- cargo clean
- cargo test -- --nocapture
- # --features=alpn
- fi
-
- - |
- if [[ "$TRAVIS_RUST_VERSION" == "stable" ]]; then
- cd examples/basics && cargo check && cd ../..
- cd examples/hello-world && cargo check && cd ../..
- cd examples/http-proxy && cargo check && cd ../..
- cd examples/multipart && cargo check && cd ../..
- cd examples/json && cargo check && cd ../..
- cd examples/juniper && cargo check && cd ../..
- cd examples/protobuf && cargo check && cd ../..
- cd examples/state && cargo check && cd ../..
- cd examples/template_tera && cargo check && cd ../..
- cd examples/diesel && cargo check && cd ../..
- cd examples/r2d2 && cargo check && cd ../..
- cd examples/tls && cargo check && cd ../..
- cd examples/websocket-chat && cargo check && cd ../..
- cd examples/websocket && cargo check && cd ../..
- cd examples/unix-socket && cargo check && cd ../..
- fi
- - |
- if [[ "$TRAVIS_RUST_VERSION" == "nightly" && $CLIPPY ]]; then
- cargo clippy
+ if [[ "$TRAVIS_RUST_VERSION" == "stable" || "$TRAVIS_RUST_VERSION" == "beta" ]]; then
+ cargo test --all-features --all -- --nocapture
+ cd actix-http; cargo test --no-default-features --features="rustls" -- --nocapture; cd ..
+ cd awc; cargo test --no-default-features --features="rustls" -- --nocapture; cd ..
fi
# Upload docs
after_success:
- |
- if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "beta" ]]; then
- cargo doc --features "alpn, tls, session" --no-deps &&
+ if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then
+ cargo doc --no-deps --all-features &&
echo "" > target/doc/index.html &&
- curl -sL https://github.com/rust-lang-nursery/mdBook/releases/download/v0.1.5/mdbook-v0.1.5-x86_64-unknown-linux-gnu.tar.gz | tar xvz -C $HOME/.cargo/bin &&
- cd guide && mdbook build -d ../target/doc/guide && cd .. &&
git clone https://github.com/davisp/ghp-import.git &&
./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc &&
echo "Uploaded documentation"
fi
-
- |
- if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_RUST_VERSION" == "1.21.0" ]]; then
- bash <(curl https://raw.githubusercontent.com/xd009642/tarpaulin/master/travis-install.sh)
- USE_SKEPTIC=1 cargo tarpaulin --out Xml
- bash <(curl -s https://codecov.io/bash)
- echo "Uploaded code coverage"
+ if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-11-20" ]]; then
+ taskset -c 0 cargo tarpaulin --out Xml --all --all-features
+ bash <(curl -s https://codecov.io/bash)
+ echo "Uploaded code coverage"
fi
diff --git a/CHANGES.md b/CHANGES.md
index 03f5b5e94..a7569862d 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,257 +1,357 @@
# Changes
-## 0.5.0
+## [2.0.0-alpha.1] - 2019-11-22
-* Type-safe path/query/form parameter handling, using serde #70
+### Changed
-* HttpResponse builder's methods `.body()`, `.finish()`, `.json()`
- return `HttpResponse` instead of `Result`
+* Migrated to `std::future`
-* Use more ergonomic `actix_web::Error` instead of `http::Error` for `ClientRequestBuilder::body()`
+* Remove implementation of `Responder` for `()`. (#1167)
-* Added `HttpRequest::resource()`, returns current matched resource
-* Added `ErrorHandlers` middleware
+## [1.0.9] - 2019-11-14
-* Router cannot parse Non-ASCII characters in URL #137
+### Added
-* Fix long client urls #129
+* Add `Payload::into_inner` method and make stored `def::Payload` public. (#1110)
-* Fix panic on invalid URL characters #130
+### Changed
-* Fix client connection pooling
+* Support `Host` guards when the `Host` header is unset (e.g. HTTP/2 requests) (#1129)
-* Fix logger request duration calculation #152
+## [1.0.8] - 2019-09-25
-## 0.4.10 (2018-03-20)
+### Added
-* Use `Error` instead of `InternalError` for `error::ErrorXXXX` methods
+* Add `Scope::register_data` and `Resource::register_data` methods, parallel to
+ `App::register_data`.
-* Allow to set client request timeout
+* Add `middleware::Condition` that conditionally enables another middleware
-* Allow to set client websocket handshake timeout
+* Allow to re-construct `ServiceRequest` from `HttpRequest` and `Payload`
-* Refactor `TestServer` configuration
+* Add `HttpServer::listen_uds` for ability to listen on UDS FD rather than path,
+ which is useful for example with systemd.
-* Fix server websockets big payloads support
+### Changed
-* Fix http/2 date header generation
+* Make UrlEncodedError::Overflow more informativve
+* Use actix-testing for testing utils
-## 0.4.9 (2018-03-16)
-* Allow to disable http/2 support
+## [1.0.7] - 2019-08-29
-* Wake payload reading task when data is available
+### Fixed
-* Fix server keep-alive handling
+* Request Extensions leak #1062
-* Send Query Parameters in client requests #120
-* Move brotli encoding to a feature
+## [1.0.6] - 2019-08-28
-* Add option of default handler for `StaticFiles` handler #57
+### Added
-* Add basic client connection pooling
+* Re-implement Host predicate (#989)
+* Form immplements Responder, returning a `application/x-www-form-urlencoded` response
-## 0.4.8 (2018-03-12)
+* Add `into_inner` to `Data`
-* Allow to set read buffer capacity for server request
+* Add `test::TestRequest::set_form()` convenience method to automatically serialize data and set
+ the header in test requests.
-* Handle WouldBlock error for socket accept call
+### Changed
+* `Query` payload made `pub`. Allows user to pattern-match the payload.
-## 0.4.7 (2018-03-11)
+* Enable `rust-tls` feature for client #1045
-* Fix panic on unknown content encoding
+* Update serde_urlencoded to 0.6.1
-* Fix connection get closed too early
+* Update url to 2.1
-* Fix streaming response handling for http/2
-* Better sleep on error support
+## [1.0.5] - 2019-07-18
+### Added
-## 0.4.6 (2018-03-10)
+* Unix domain sockets (HttpServer::bind_uds) #92
-* Fix client cookie handling
+* Actix now logs errors resulting in "internal server error" responses always, with the `error`
+ logging level
-* Fix json content type detection
+### Fixed
-* Fix CORS middleware #117
+* Restored logging of errors through the `Logger` middleware
-* Optimize websockets stream support
+## [1.0.4] - 2019-07-17
-## 0.4.5 (2018-03-07)
+### Added
-* Fix compression #103 and #104
+* Add `Responder` impl for `(T, StatusCode) where T: Responder`
-* Fix client cookie handling #111
+* Allow to access app's resource map via
+ `ServiceRequest::resource_map()` and `HttpRequest::resource_map()` methods.
-* Non-blocking processing of a `NamedFile`
+### Changed
-* Enable compression support for `NamedFile`
+* Upgrade `rand` dependency version to 0.7
-* Better support for `NamedFile` type
-* Add `ResponseError` impl for `SendRequestError`. This improves ergonomics of the client.
+## [1.0.3] - 2019-06-28
-* Add native-tls support for client
+### Added
-* Allow client connection timeout to be set #108
+* Support asynchronous data factories #850
-* Allow to use std::net::TcpListener for HttpServer
+### Changed
-* Handle panics in worker threads
+* Use `encoding_rs` crate instead of unmaintained `encoding` crate
-## 0.4.4 (2018-03-04)
+## [1.0.2] - 2019-06-17
-* Allow to use Arc> as response/request body
+### Changed
-* Fix handling of requests with an encoded body with a length > 8192 #93
+* Move cors middleware to `actix-cors` crate.
-## 0.4.3 (2018-03-03)
+* Move identity middleware to `actix-identity` crate.
-* Fix request body read bug
-* Fix segmentation fault #79
+## [1.0.1] - 2019-06-17
-* Set reuse address before bind #90
+### Added
+* Add support for PathConfig #903
-## 0.4.2 (2018-03-02)
+* Add `middleware::identity::RequestIdentity` trait to `get_identity` from `HttpMessage`.
-* Better naming for websockets implementation
+### Changed
-* Add `Pattern::with_prefix()`, make it more usable outside of actix
+* Move cors middleware to `actix-cors` crate.
-* Add csrf middleware for filter for cross-site request forgery #89
+* Move identity middleware to `actix-identity` crate.
-* Fix disconnect on idle connections
+* Disable default feature `secure-cookies`.
+* Allow to test an app that uses async actors #897
-## 0.4.1 (2018-03-01)
+* Re-apply patch from #637 #894
-* Rename `Route::p()` to `Route::filter()`
+### Fixed
-* Better naming for http codes
+* HttpRequest::url_for is broken with nested scopes #915
-* Fix payload parse in situation when socket data is not ready.
-* Fix Session mutable borrow lifetime #87
+## [1.0.0] - 2019-06-05
+### Added
-## 0.4.0 (2018-02-28)
+* Add `Scope::configure()` method.
-* Actix 0.5 compatibility
+* Add `ServiceRequest::set_payload()` method.
-* Fix request json/urlencoded loaders
+* Add `test::TestRequest::set_json()` convenience method to automatically
+ serialize data and set header in test requests.
-* Simplify HttpServer type definition
+* Add macros for head, options, trace, connect and patch http methods
-* Added HttpRequest::encoding() method
+### Changed
-* Added HttpRequest::mime_type() method
+* Drop an unnecessary `Option<_>` indirection around `ServerBuilder` from `HttpServer`. #863
-* Added HttpRequest::uri_mut(), allows to modify request uri
+### Fixed
-* Added StaticFiles::index_file()
+* Fix Logger request time format, and use rfc3339. #867
-* Added http client
+* Clear http requests pool on app service drop #860
-* Added websocket client
-* Added TestServer::ws(), test websockets client
+## [1.0.0-rc] - 2019-05-18
-* Added TestServer http client support
+### Add
-* Allow to override content encoding on application level
+* Add `Query::from_query()` to extract parameters from a query string. #846
+* `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors.
+### Changed
-## 0.3.3 (2018-01-25)
+* `JsonConfig` is now `Send + Sync`, this implies that `error_handler` must be `Send + Sync` too.
-* Stop processing any events after context stop
+### Fixed
-* Re-enable write back-pressure for h1 connections
+* Codegen with parameters in the path only resolves the first registered endpoint #841
-* Refactor HttpServer::start_ssl() method
-* Upgrade openssl to 0.10
+## [1.0.0-beta.4] - 2019-05-12
+### Add
-## 0.3.2 (2018-01-21)
+* Allow to set/override app data on scope level
-* Fix HEAD requests handling
+### Changed
-* Log request processing errors
+* `App::configure` take an `FnOnce` instead of `Fn`
+* Upgrade actix-net crates
-* Always enable content encoding if encoding explicitly selected
-* Allow multiple Applications on a single server with different state #49
+## [1.0.0-beta.3] - 2019-05-04
-* CORS middleware: allowed_headers is defaulting to None #50
+### Added
+* Add helper function for executing futures `test::block_fn()`
-## 0.3.1 (2018-01-13)
+### Changed
-* Fix directory entry path #47
+* Extractor configuration could be registered with `App::data()`
+ or with `Resource::data()` #775
-* Do not enable chunked encoding for HTTP/1.0
+* Route data is unified with app data, `Route::data()` moved to resource
+ level to `Resource::data()`
-* Allow explicitly disable chunked encoding
+* CORS handling without headers #702
+* Allow to construct `Data` instances to avoid double `Arc` for `Send + Sync` types.
-## 0.3.0 (2018-01-12)
+### Fixed
-* HTTP/2 Support
+* Fix `NormalizePath` middleware impl #806
-* Refactor streaming responses
+### Deleted
-* Refactor error handling
+* `App::data_factory()` is deleted.
-* Asynchronous middlewares
-* Refactor logger middleware
+## [1.0.0-beta.2] - 2019-04-24
-* Content compression/decompression (br, gzip, deflate)
+### Added
-* Server multi-threading
+* Add raw services support via `web::service()`
-* Gracefull shutdown support
+* Add helper functions for reading response body `test::read_body()`
+* Add support for `remainder match` (i.e "/path/{tail}*")
-## 0.2.1 (2017-11-03)
+* Extend `Responder` trait, allow to override status code and headers.
-* Allow to start tls server with `HttpServer::serve_tls`
+* Store visit and login timestamp in the identity cookie #502
-* Export `Frame` enum
+### Changed
-* Add conversion impl from `HttpResponse` and `BinaryBody` to a `Frame`
+* `.to_async()` handler can return `Responder` type #792
+### Fixed
-## 0.2.0 (2017-10-30)
+* Fix async web::Data factory handling
-* Do not use `http::Uri` as it can not parse some valid paths
-* Refactor response `Body`
+## [1.0.0-beta.1] - 2019-04-20
-* Refactor `RouteRecognizer` usability
+### Added
-* Refactor `HttpContext::write`
+* Add helper functions for reading test response body,
+ `test::read_response()` and test::read_response_json()`
-* Refactor `Payload` stream
+* Add `.peer_addr()` #744
-* Re-use `BinaryBody` for `Frame::Payload`
+* Add `NormalizePath` middleware
-* Stop http actor on `write_eof`
+### Changed
-* Fix disconnection handling.
+* Rename `RouterConfig` to `ServiceConfig`
+* Rename `test::call_success` to `test::call_service`
-## 0.1.0 (2017-10-23)
+* Removed `ServiceRequest::from_parts()` as it is unsafe to create from parts.
-* First release
+* `CookieIdentityPolicy::max_age()` accepts value in seconds
+
+### Fixed
+
+* Fixed `TestRequest::app_data()`
+
+
+## [1.0.0-alpha.6] - 2019-04-14
+
+### Changed
+
+* Allow to use any service as default service.
+
+* Remove generic type for request payload, always use default.
+
+* Removed `Decompress` middleware. Bytes, String, Json, Form extractors
+ automatically decompress payload.
+
+* Make extractor config type explicit. Add `FromRequest::Config` associated type.
+
+
+## [1.0.0-alpha.5] - 2019-04-12
+
+### Added
+
+* Added async io `TestBuffer` for testing.
+
+### Deleted
+
+* Removed native-tls support
+
+
+## [1.0.0-alpha.4] - 2019-04-08
+
+### Added
+
+* `App::configure()` allow to offload app configuration to different methods
+
+* Added `URLPath` option for logger
+
+* Added `ServiceRequest::app_data()`, returns `Data`
+
+* Added `ServiceFromRequest::app_data()`, returns `Data`
+
+### Changed
+
+* `FromRequest` trait refactoring
+
+* Move multipart support to actix-multipart crate
+
+### Fixed
+
+* Fix body propagation in Response::from_error. #760
+
+
+## [1.0.0-alpha.3] - 2019-04-02
+
+### Changed
+
+* Renamed `TestRequest::to_service()` to `TestRequest::to_srv_request()`
+
+* Renamed `TestRequest::to_response()` to `TestRequest::to_srv_response()`
+
+* Removed `Deref` impls
+
+### Removed
+
+* Removed unused `actix_web::web::md()`
+
+
+## [1.0.0-alpha.2] - 2019-03-29
+
+### Added
+
+* rustls support
+
+### Changed
+
+* use forked cookie
+
+* multipart::Field renamed to MultipartField
+
+## [1.0.0-alpha.1] - 2019-03-28
+
+### Changed
+
+* Complete architecture re-design.
+
+* Return 405 response if no matching route found within resource #538
diff --git a/Cargo.toml b/Cargo.toml
index e5a17e9ea..689f7b147 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,130 +1,142 @@
[package]
name = "actix-web"
-version = "0.5.0-dev"
+version = "2.0.0-alpha.1"
authors = ["Nikolay Kim "]
-description = "Actix web is a simple, pragmatic, extremely fast, web framework for Rust."
+description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
readme = "README.md"
-keywords = ["http", "web", "framework", "async", "futures"]
-homepage = "https://github.com/actix/actix-web"
+keywords = ["actix", "http", "web", "framework", "async"]
+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::http-client",
"web-programming::websocket"]
license = "MIT/Apache-2.0"
-exclude = [".gitignore", ".travis.yml", ".cargo/config",
- "appveyor.yml", "/examples/**"]
-build = "build.rs"
+exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"]
+edition = "2018"
+
+[package.metadata.docs.rs]
+features = ["openssl", "brotli", "flate2-zlib", "secure-cookies", "client"]
[badges]
travis-ci = { repository = "actix/actix-web", branch = "master" }
-appveyor = { repository = "fafhrd91/actix-web-hdy9d" }
codecov = { repository = "actix/actix-web", branch = "master", service = "github" }
[lib]
name = "actix_web"
path = "src/lib.rs"
-[features]
-default = ["session", "brotli"]
+[workspace]
+members = [
+ ".",
+ "awc",
+ "actix-http",
+ "actix-cors",
+ "actix-files",
+ "actix-framed",
+ "actix-session",
+ "actix-identity",
+ "actix-multipart",
+ "actix-web-actors",
+ "actix-web-codegen",
+ "test-server",
+]
-# tls
-tls = ["native-tls", "tokio-tls"]
+[features]
+default = ["brotli", "flate2-zlib", "client", "fail"]
+
+# http client
+client = ["awc"]
+
+# brotli encoding, requires c compiler
+brotli = ["actix-http/brotli"]
+
+# miniz-sys backend for flate2 crate
+flate2-zlib = ["actix-http/flate2-zlib"]
+
+# rust backend for flate2 crate
+flate2-rust = ["actix-http/flate2-rust"]
+
+# sessions feature, session require "ring" crate and c compiler
+secure-cookies = ["actix-http/secure-cookies"]
+
+fail = ["actix-http/fail"]
# openssl
-alpn = ["openssl", "openssl/v102", "openssl/v110", "tokio-openssl"]
+openssl = ["open-ssl", "actix-server/openssl", "awc/openssl"]
-# sessions
-session = ["cookie/secure"]
-
-# brotli encoding
-brotli = ["brotli2"]
+# rustls
+# rustls = ["rust-tls", "actix-server/rustls", "awc/rustls"]
[dependencies]
-actix = "^0.5.5"
+actix-codec = "0.2.0-alpha.1"
+actix-service = "1.0.0-alpha.1"
+actix-utils = "0.5.0-alpha.1"
+actix-router = "0.1.5"
+actix-rt = "1.0.0-alpha.1"
+actix-web-codegen = "0.2.0-alpha.1"
+actix-http = "0.3.0-alpha.1"
+actix-server = "0.8.0-alpha.1"
+actix-server-config = "0.3.0-alpha.1"
+actix-testing = "0.3.0-alpha.1"
+actix-threadpool = "0.2.0-alpha.1"
+awc = { version = "0.3.0-alpha.1", optional = true }
-base64 = "0.9"
-bitflags = "1.0"
-failure = "0.1.1"
-flate2 = "1.0"
-h2 = "0.1"
-http = "^0.1.5"
-httparse = "1.2"
-http-range = "0.1"
-libc = "0.2"
+bytes = "0.4"
+derive_more = "0.99.2"
+encoding_rs = "0.8"
+futures = "0.3.1"
+hashbrown = "0.6.3"
log = "0.4"
mime = "0.3"
-mime_guess = "2.0.0-alpha"
-num_cpus = "1.0"
-percent-encoding = "1.0"
-rand = "0.4"
-regex = "0.2"
-serde = "1.0"
+net2 = "0.2.33"
+parking_lot = "0.9"
+pin-project = "0.4.5"
+regex = "1.0"
+serde = { version = "1.0", features=["derive"] }
serde_json = "1.0"
-serde_urlencoded = "0.5"
-sha1 = "0.6"
-smallvec = "0.6"
-time = "0.1"
-encoding = "0.2"
-language-tags = "0.2"
-lazy_static = "1.0"
-url = { version="1.7", features=["query_encoding"] }
-cookie = { version="0.10", features=["percent-encode"] }
-brotli2 = { version="^0.3.2", optional = true }
+serde_urlencoded = "0.6.1"
+time = "0.1.42"
+url = "2.1"
-# io
-mio = "^0.6.13"
-net2 = "0.2"
-bytes = "0.4"
-byteorder = "1"
-futures = "0.1"
-futures-cpupool = "0.1"
-tokio-io = "0.1"
-tokio-core = "0.1"
-trust-dns-resolver = "0.8"
-
-# native-tls
-native-tls = { version="0.1", optional = true }
-tokio-tls = { version="0.1", optional = true }
-
-# openssl
-openssl = { version="0.10", optional = true }
-tokio-openssl = { version="0.2", optional = true }
+# ssl support
+open-ssl = { version="0.10", package="openssl", optional = true }
+# rust-tls = { version = "0.16", package="rustls", optional = true }
[dev-dependencies]
-env_logger = "0.5"
-skeptic = "0.13"
+# actix = "0.8.3"
+actix-connect = "0.3.0-alpha.1"
+actix-http-test = "0.3.0-alpha.1"
+rand = "0.7"
+env_logger = "0.6"
serde_derive = "1.0"
-
-[build-dependencies]
-skeptic = "0.13"
-version_check = "0.1"
+brotli2 = "0.3.2"
+flate2 = "1.0.2"
[profile.release]
lto = true
opt-level = 3
codegen-units = 1
-[workspace]
-members = [
- "./",
- "examples/basics",
- "examples/juniper",
- "examples/diesel",
- "examples/r2d2",
- "examples/json",
- "examples/protobuf",
- "examples/hello-world",
- "examples/http-proxy",
- "examples/multipart",
- "examples/state",
- "examples/redis-session",
- "examples/template_tera",
- "examples/tls",
- "examples/websocket",
- "examples/websocket-chat",
- "examples/web-cors/backend",
- "examples/unix-socket",
- "tools/wsload/",
-]
+[patch.crates-io]
+actix-web = { path = "." }
+actix-http = { path = "actix-http" }
+actix-http-test = { path = "test-server" }
+actix-web-codegen = { path = "actix-web-codegen" }
+# actix-web-actors = { path = "actix-web-actors" }
+actix-cors = { path = "actix-cors" }
+actix-identity = { path = "actix-identity" }
+actix-session = { path = "actix-session" }
+actix-files = { path = "actix-files" }
+actix-multipart = { path = "actix-multipart" }
+awc = { path = "awc" }
+
+actix-codec = { git = "https://github.com/actix/actix-net.git" }
+actix-connect = { git = "https://github.com/actix/actix-net.git" }
+actix-rt = { git = "https://github.com/actix/actix-net.git" }
+actix-macros = { git = "https://github.com/actix/actix-net.git" }
+actix-server = { git = "https://github.com/actix/actix-net.git" }
+actix-server-config = { git = "https://github.com/actix/actix-net.git" }
+actix-service = { git = "https://github.com/actix/actix-net.git" }
+actix-testing = { git = "https://github.com/actix/actix-net.git" }
+actix-utils = { git = "https://github.com/actix/actix-net.git" }
diff --git a/MIGRATION.md b/MIGRATION.md
new file mode 100644
index 000000000..dd3a1b043
--- /dev/null
+++ b/MIGRATION.md
@@ -0,0 +1,556 @@
+## 2.0.0
+
+* Sync handlers has been removed. `.to_async()` method has been renamed to `.to()`
+
+ replace `fn` with `async fn` to convert sync handler to async
+
+* `TestServer::new()` renamed to `TestServer::start()`
+
+
+## 1.0.1
+
+* Cors middleware has been moved to `actix-cors` crate
+
+ instead of
+
+ ```rust
+ use actix_web::middleware::cors::Cors;
+ ```
+
+ use
+
+ ```rust
+ use actix_cors::Cors;
+ ```
+
+* Identity middleware has been moved to `actix-identity` crate
+
+ instead of
+
+ ```rust
+ use actix_web::middleware::identity::{Identity, CookieIdentityPolicy, IdentityService};
+ ```
+
+ use
+
+ ```rust
+ use actix_identity::{Identity, CookieIdentityPolicy, IdentityService};
+ ```
+
+
+## 1.0.0
+
+* Extractor configuration. In version 1.0 this is handled with the new `Data` mechanism for both setting and retrieving the configuration
+
+ instead of
+
+ ```rust
+
+ #[derive(Default)]
+ struct ExtractorConfig {
+ config: String,
+ }
+
+ impl FromRequest for YourExtractor {
+ type Config = ExtractorConfig;
+ type Result = Result;
+
+ fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result {
+ println!("use the config: {:?}", cfg.config);
+ ...
+ }
+ }
+
+ App::new().resource("/route_with_config", |r| {
+ r.post().with_config(handler_fn, |cfg| {
+ cfg.0.config = "test".to_string();
+ })
+ })
+
+ ```
+
+ use the HttpRequest to get the configuration like any other `Data` with `req.app_data::()` and set it with the `data()` method on the `resource`
+
+ ```rust
+ #[derive(Default)]
+ struct ExtractorConfig {
+ config: String,
+ }
+
+ impl FromRequest for YourExtractor {
+ type Error = Error;
+ type Future = Result;
+ type Config = ExtractorConfig;
+
+ fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
+ let cfg = req.app_data::();
+ println!("config data?: {:?}", cfg.unwrap().role);
+ ...
+ }
+ }
+
+ App::new().service(
+ resource("/route_with_config")
+ .data(ExtractorConfig {
+ config: "test".to_string(),
+ })
+ .route(post().to(handler_fn)),
+ )
+ ```
+
+* Resource registration. 1.0 version uses generalized resource
+ registration via `.service()` method.
+
+ instead of
+
+ ```rust
+ App.new().resource("/welcome", |r| r.f(welcome))
+ ```
+
+ use App's or Scope's `.service()` method. `.service()` method accepts
+ object that implements `HttpServiceFactory` trait. By default
+ actix-web provides `Resource` and `Scope` services.
+
+ ```rust
+ App.new().service(
+ web::resource("/welcome")
+ .route(web::get().to(welcome))
+ .route(web::post().to(post_handler))
+ ```
+
+* Scope registration.
+
+ instead of
+
+ ```rust
+ let app = App::new().scope("/{project_id}", |scope| {
+ scope
+ .resource("/path1", |r| r.f(|_| HttpResponse::Ok()))
+ .resource("/path2", |r| r.f(|_| HttpResponse::Ok()))
+ .resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed()))
+ });
+ ```
+
+ use `.service()` for registration and `web::scope()` as scope object factory.
+
+ ```rust
+ let app = App::new().service(
+ web::scope("/{project_id}")
+ .service(web::resource("/path1").to(|| HttpResponse::Ok()))
+ .service(web::resource("/path2").to(|| HttpResponse::Ok()))
+ .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed()))
+ );
+ ```
+
+* `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`.
+
+ instead of
+
+ ```rust
+ App.new().resource("/welcome", |r| r.with(welcome))
+ ```
+
+ use `.to()` or `.to_async()` methods
+
+ ```rust
+ App.new().service(web::resource("/welcome").to(welcome))
+ ```
+
+* Passing arguments to handler with extractors, multiple arguments are allowed
+
+ instead of
+
+ ```rust
+ fn welcome((body, req): (Bytes, HttpRequest)) -> ... {
+ ...
+ }
+ ```
+
+ use multiple arguments
+
+ ```rust
+ fn welcome(body: Bytes, req: HttpRequest) -> ... {
+ ...
+ }
+ ```
+
+* `.f()`, `.a()` and `.h()` handler registration methods have been removed.
+ Use `.to()` for handlers and `.to_async()` for async handlers. Handler function
+ must use extractors.
+
+ instead of
+
+ ```rust
+ App.new().resource("/welcome", |r| r.f(welcome))
+ ```
+
+ use App's `to()` or `to_async()` methods
+
+ ```rust
+ App.new().service(web::resource("/welcome").to(welcome))
+ ```
+
+* `HttpRequest` does not provide access to request's payload stream.
+
+ instead of
+
+ ```rust
+ fn index(req: &HttpRequest) -> Box> {
+ req
+ .payload()
+ .from_err()
+ .fold((), |_, chunk| {
+ ...
+ })
+ .map(|_| HttpResponse::Ok().finish())
+ .responder()
+ }
+ ```
+
+ use `Payload` extractor
+
+ ```rust
+ fn index(stream: web::Payload) -> impl Future- {
+ stream
+ .from_err()
+ .fold((), |_, chunk| {
+ ...
+ })
+ .map(|_| HttpResponse::Ok().finish())
+ }
+ ```
+
+* `State` is now `Data`. You register Data during the App initialization process
+ and then access it from handlers either using a Data extractor or using
+ HttpRequest's api.
+
+ instead of
+
+ ```rust
+ App.with_state(T)
+ ```
+
+ use App's `data` method
+
+ ```rust
+ App.new()
+ .data(T)
+ ```
+
+ and either use the Data extractor within your handler
+
+ ```rust
+ use actix_web::web::Data;
+
+ fn endpoint_handler(Data)){
+ ...
+ }
+ ```
+
+ .. or access your Data element from the HttpRequest
+
+ ```rust
+ fn endpoint_handler(req: HttpRequest) {
+ let data: Option> = req.app_data::();
+ }
+ ```
+
+
+* AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type.
+
+ instead of
+
+ ```rust
+ use actix_web::AsyncResponder;
+
+ fn endpoint_handler(...) -> impl Future
- {
+ ...
+ .responder()
+ }
+ ```
+
+ .. simply omit AsyncResponder and the corresponding responder() finish method
+
+
+* Middleware
+
+ instead of
+
+ ```rust
+ let app = App::new()
+ .middleware(middleware::Logger::default())
+ ```
+
+ use `.wrap()` method
+
+ ```rust
+ let app = App::new()
+ .wrap(middleware::Logger::default())
+ .route("/index.html", web::get().to(index));
+ ```
+
+* `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()`
+ method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead.
+
+ instead of
+
+ ```rust
+ fn index(req: &HttpRequest) -> Responder {
+ req.body()
+ .and_then(|body| {
+ ...
+ })
+ }
+ ```
+
+ use
+
+ ```rust
+ fn index(body: Bytes) -> Responder {
+ ...
+ }
+ ```
+
+* `actix_web::server` module has been removed. To start http server use `actix_web::HttpServer` type
+
+* StaticFiles and NamedFile has been move to separate create.
+
+ instead of `use actix_web::fs::StaticFile`
+
+ use `use actix_files::Files`
+
+ instead of `use actix_web::fs::Namedfile`
+
+ use `use actix_files::NamedFile`
+
+* Multipart has been move to separate create.
+
+ instead of `use actix_web::multipart::Multipart`
+
+ use `use actix_multipart::Multipart`
+
+* Response compression is not enabled by default.
+ To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`.
+
+* Session middleware moved to actix-session crate
+
+* Actors support have been moved to `actix-web-actors` crate
+
+* Custom Error
+
+ Instead of error_response method alone, ResponseError now provides two methods: error_response and render_response respectively. Where, error_response creates the error response and render_response returns the error response to the caller.
+
+ Simplest migration from 0.7 to 1.0 shall include below method to the custom implementation of ResponseError:
+
+ ```rust
+ fn render_response(&self) -> HttpResponse {
+ self.error_response()
+ }
+ ```
+
+## 0.7.15
+
+* The `' '` character is not percent decoded anymore before matching routes. If you need to use it in
+ your routes, you should use `%20`.
+
+ instead of
+
+ ```rust
+ fn main() {
+ let app = App::new().resource("/my index", |r| {
+ r.method(http::Method::GET)
+ .with(index);
+ });
+ }
+ ```
+
+ use
+
+ ```rust
+ fn main() {
+ let app = App::new().resource("/my%20index", |r| {
+ r.method(http::Method::GET)
+ .with(index);
+ });
+ }
+ ```
+
+* If you used `AsyncResult::async` you need to replace it with `AsyncResult::future`
+
+
+## 0.7.4
+
+* `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple
+ even for handler with one parameter.
+
+
+## 0.7
+
+* `HttpRequest` does not implement `Stream` anymore. If you need to read request payload
+ use `HttpMessage::payload()` method.
+
+ instead of
+
+ ```rust
+ fn index(req: HttpRequest) -> impl Responder {
+ req
+ .from_err()
+ .fold(...)
+ ....
+ }
+ ```
+
+ use `.payload()`
+
+ ```rust
+ fn index(req: HttpRequest) -> impl Responder {
+ req
+ .payload() // <- get request payload stream
+ .from_err()
+ .fold(...)
+ ....
+ }
+ ```
+
+* [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html)
+ trait uses `&HttpRequest` instead of `&mut HttpRequest`.
+
+* Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead.
+
+ instead of
+
+ ```rust
+ fn index(query: Query<..>, info: Json impl Responder {}
+ ```
+
+ use tuple of extractors and use `.with()` for registration:
+
+ ```rust
+ fn index((query, json): (Query<..>, Json impl Responder {}
+ ```
+
+* `Handler::handle()` uses `&self` instead of `&mut self`
+
+* `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value
+
+* Removed deprecated `HttpServer::threads()`, use
+ [HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead.
+
+* Renamed `client::ClientConnectorError::Connector` to
+ `client::ClientConnectorError::Resolver`
+
+* `Route::with()` does not return `ExtractorConfig`, to configure
+ extractor use `Route::with_config()`
+
+ instead of
+
+ ```rust
+ fn main() {
+ let app = App::new().resource("/index.html", |r| {
+ r.method(http::Method::GET)
+ .with(index)
+ .limit(4096); // <- limit size of the payload
+ });
+ }
+ ```
+
+ use
+
+ ```rust
+
+ fn main() {
+ let app = App::new().resource("/index.html", |r| {
+ r.method(http::Method::GET)
+ .with_config(index, |cfg| { // <- register handler
+ cfg.limit(4096); // <- limit size of the payload
+ })
+ });
+ }
+ ```
+
+* `Route::with_async()` does not return `ExtractorConfig`, to configure
+ extractor use `Route::with_async_config()`
+
+
+## 0.6
+
+* `Path` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest`
+
+* `ws::Message::Close` now includes optional close reason.
+ `ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed.
+
+* `HttpServer::threads()` renamed to `HttpServer::workers()`.
+
+* `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated.
+ Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead.
+
+* `HttpRequest::extensions()` returns read only reference to the request's Extension
+ `HttpRequest::extensions_mut()` returns mutable reference.
+
+* Instead of
+
+ `use actix_web::middleware::{
+ CookieSessionBackend, CookieSessionError, RequestSession,
+ Session, SessionBackend, SessionImpl, SessionStorage};`
+
+ use `actix_web::middleware::session`
+
+ `use actix_web::middleware::session{CookieSessionBackend, CookieSessionError,
+ RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};`
+
+* `FromRequest::from_request()` accepts mutable reference to a request
+
+* `FromRequest::Result` has to implement `Into>`
+
+* [`Responder::respond_to()`](
+ https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to)
+ is generic over `S`
+
+* Use `Query` extractor instead of HttpRequest::query()`.
+
+ ```rust
+ fn index(q: Query>) -> Result<..> {
+ ...
+ }
+ ```
+
+ or
+
+ ```rust
+ let q = Query::>::extract(req);
+ ```
+
+* Websocket operations are implemented as `WsWriter` trait.
+ you need to use `use actix_web::ws::WsWriter`
+
+
+## 0.5
+
+* `HttpResponseBuilder::body()`, `.finish()`, `.json()`
+ methods return `HttpResponse` instead of `Result`
+
+* `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version`
+ moved to `actix_web::http` module
+
+* `actix_web::header` moved to `actix_web::http::header`
+
+* `NormalizePath` moved to `actix_web::http` module
+
+* `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function,
+ shortcut for `actix_web::server::HttpServer::new()`
+
+* `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself
+
+* `StaticFiles::new()`'s show_index parameter removed, use `show_files_listing()` method instead.
+
+* `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type
+
+* `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()`
+ functions should be used instead
+
+* `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>`
+ instead of `Result<_, http::Error>`
+
+* `Application` renamed to a `App`
+
+* `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev`
diff --git a/Makefile b/Makefile
deleted file mode 100644
index fdc3cbbc0..000000000
--- a/Makefile
+++ /dev/null
@@ -1,26 +0,0 @@
-.PHONY: default build test doc book clean
-
-CARGO_FLAGS := --features "$(FEATURES) alpn"
-
-default: test
-
-build:
- cargo build $(CARGO_FLAGS)
-
-test: build clippy
- cargo test $(CARGO_FLAGS)
-
-skeptic:
- USE_SKEPTIC=1 cargo test $(CARGO_FLAGS)
-
-# cd examples/word-count && python setup.py install && pytest -v tests
-
-clippy:
- if $$CLIPPY; then cargo clippy $(CARGO_FLAGS); fi
-
-doc: build
- cargo doc --no-deps $(CARGO_FLAGS)
- cd guide; mdbook build -d ../target/doc/guide/; cd ..
-
-book:
- cd guide; mdbook build -d ../target/doc/guide/; cd ..
diff --git a/README.md b/README.md
index 46f589d6f..b7a1bf28f 100644
--- a/README.md
+++ b/README.md
@@ -1,74 +1,67 @@
-# Actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+# Actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-Actix web is a simple, pragmatic, extremely fast, web framework for Rust.
+Actix web is a simple, pragmatic and extremely fast web framework for Rust.
-* Supported *HTTP/1.x* and [*HTTP/2.0*](https://actix.github.io/actix-web/guide/qs_13.html) protocols
+* Supported *HTTP/1.x* and *HTTP/2.0* protocols
* Streaming and pipelining
* Keep-alive and slow requests handling
-* Client/server [WebSockets](https://actix.github.io/actix-web/guide/qs_9.html) support
+* Client/server [WebSockets](https://actix.rs/docs/websockets/) support
* Transparent content compression/decompression (br, gzip, deflate)
-* Configurable [request routing](https://actix.github.io/actix-web/guide/qs_5.html)
-* Graceful server shutdown
+* Configurable [request routing](https://actix.rs/docs/url-dispatch/)
* Multipart streams
* Static assets
-* SSL support with openssl or native-tls
-* Middlewares ([Logger](https://actix.github.io/actix-web/guide/qs_10.html#logging),
- [Session](https://actix.github.io/actix-web/guide/qs_10.html#user-sessions),
- [Redis sessions](https://github.com/actix/actix-redis),
- [DefaultHeaders](https://actix.github.io/actix-web/guide/qs_10.html#default-headers),
- [CORS](https://actix.github.io/actix-web/actix_web/middleware/cors/index.html),
- [CSRF](https://actix.github.io/actix-web/actix_web/middleware/csrf/index.html))
-* Built on top of [Actix actor framework](https://github.com/actix/actix).
+* SSL support with OpenSSL or Rustls
+* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
+* Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
+* Supports [Actix actor framework](https://github.com/actix/actix)
-## Documentation
+## Documentation & community resources
-* [User Guide](http://actix.github.io/actix-web/guide/)
-* [API Documentation (Development)](http://actix.github.io/actix-web/actix_web/)
-* [API Documentation (Releases)](https://docs.rs/actix-web/)
+* [User Guide](https://actix.rs/docs/)
+* [API Documentation (1.0)](https://docs.rs/actix-web/)
* [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [actix-web](https://crates.io/crates/actix-web)
-* Minimum supported Rust version: 1.21 or later
+* Minimum supported Rust version: 1.39 or later
## Example
```rust
-extern crate actix_web;
-use actix_web::{App, HttpServer, Path};
+use actix_web::{get, App, HttpServer, Responder};
-fn index(info: Path<(String, u32)>) -> String {
- format!("Hello {}! id:{}", info.0, info.1)
+#[get("/{id}/{name}/index.html")]
+async fn index(info: web::Path<(u32, String)>) -> impl Responder {
+ format!("Hello {}! id:{}", info.1, info.0)
}
-fn main() {
- HttpServer::new(
- || App::new()
- .resource("/{name}/{id}/index.html", |r| r.with(index)))
- .bind("127.0.0.1:8080").unwrap()
- .run();
+#[actix_rt::main]
+async fn main() -> std::io::Result<()> {
+ HttpServer::new(|| App::new().service(index))
+ .bind("127.0.0.1:8080")?
+ .start()
+ .await
}
```
### More examples
-* [Basics](https://github.com/actix/actix-web/tree/master/examples/basics/)
-* [Stateful](https://github.com/actix/actix-web/tree/master/examples/state/)
-* [Protobuf support](https://github.com/actix/actix-web/tree/master/examples/protobuf/)
-* [Multipart streams](https://github.com/actix/actix-web/tree/master/examples/multipart/)
-* [Simple websocket session](https://github.com/actix/actix-web/tree/master/examples/websocket/)
-* [Tera templates](https://github.com/actix/actix-web/tree/master/examples/template_tera/)
-* [Diesel integration](https://github.com/actix/actix-web/tree/master/examples/diesel/)
-* [SSL / HTTP/2.0](https://github.com/actix/actix-web/tree/master/examples/tls/)
-* [Tcp/Websocket chat](https://github.com/actix/actix-web/tree/master/examples/websocket-chat/)
-* [Json](https://github.com/actix/actix-web/tree/master/examples/json/)
+* [Basics](https://github.com/actix/examples/tree/master/basics/)
+* [Stateful](https://github.com/actix/examples/tree/master/state/)
+* [Multipart streams](https://github.com/actix/examples/tree/master/multipart/)
+* [Simple websocket](https://github.com/actix/examples/tree/master/websocket/)
+* [Tera](https://github.com/actix/examples/tree/master/template_tera/) /
+ [Askama](https://github.com/actix/examples/tree/master/template_askama/) templates
+* [Diesel integration](https://github.com/actix/examples/tree/master/diesel/)
+* [r2d2](https://github.com/actix/examples/tree/master/r2d2/)
+* [SSL / HTTP/2.0](https://github.com/actix/examples/tree/master/tls/)
+* [Tcp/Websocket chat](https://github.com/actix/examples/tree/master/websocket-chat/)
+* [Json](https://github.com/actix/examples/tree/master/json/)
You may consider checking out
-[this directory](https://github.com/actix/actix-web/tree/master/examples) for more examples.
+[this directory](https://github.com/actix/examples/tree/master/) for more examples.
## Benchmarks
-* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r15&hw=ph&test=plaintext)
-
-* Some basic benchmarks could be found in this [repository](https://github.com/fafhrd91/benchmarks).
+* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r18)
## License
diff --git a/actix-cors/CHANGES.md b/actix-cors/CHANGES.md
new file mode 100644
index 000000000..10e408ede
--- /dev/null
+++ b/actix-cors/CHANGES.md
@@ -0,0 +1,9 @@
+# Changes
+
+## [0.1.1] - unreleased
+
+* Bump `derive_more` crate version to 0.15.0
+
+## [0.1.0] - 2019-06-15
+
+* Move cors middleware to separate crate
diff --git a/actix-cors/Cargo.toml b/actix-cors/Cargo.toml
new file mode 100644
index 000000000..ddb5f307e
--- /dev/null
+++ b/actix-cors/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "actix-cors"
+version = "0.2.0-alpha.1"
+authors = ["Nikolay Kim "]
+description = "Cross-origin resource sharing (CORS) for Actix applications."
+readme = "README.md"
+keywords = ["web", "framework"]
+homepage = "https://actix.rs"
+repository = "https://github.com/actix/actix-web.git"
+documentation = "https://docs.rs/actix-cors/"
+license = "MIT/Apache-2.0"
+edition = "2018"
+workspace = ".."
+
+[lib]
+name = "actix_cors"
+path = "src/lib.rs"
+
+[dependencies]
+actix-web = "2.0.0-alpha.1"
+actix-service = "1.0.0-alpha.1"
+derive_more = "0.99.2"
+futures = "0.3.1"
+
+[dev-dependencies]
+actix-rt = "1.0.0-alpha.1"
diff --git a/actix-cors/LICENSE-APACHE b/actix-cors/LICENSE-APACHE
new file mode 120000
index 000000000..965b606f3
--- /dev/null
+++ b/actix-cors/LICENSE-APACHE
@@ -0,0 +1 @@
+../LICENSE-APACHE
\ No newline at end of file
diff --git a/actix-cors/LICENSE-MIT b/actix-cors/LICENSE-MIT
new file mode 120000
index 000000000..76219eb72
--- /dev/null
+++ b/actix-cors/LICENSE-MIT
@@ -0,0 +1 @@
+../LICENSE-MIT
\ No newline at end of file
diff --git a/actix-cors/README.md b/actix-cors/README.md
new file mode 100644
index 000000000..a77f6c6d3
--- /dev/null
+++ b/actix-cors/README.md
@@ -0,0 +1,9 @@
+# Cors Middleware for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-cors)](https://crates.io/crates/actix-cors) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+
+## Documentation & community resources
+
+* [User Guide](https://actix.rs/docs/)
+* [API Documentation](https://docs.rs/actix-cors/)
+* [Chat on gitter](https://gitter.im/actix/actix)
+* Cargo package: [actix-cors](https://crates.io/crates/actix-cors)
+* Minimum supported Rust version: 1.34 or later
diff --git a/actix-cors/src/lib.rs b/actix-cors/src/lib.rs
new file mode 100644
index 000000000..d3607aa8e
--- /dev/null
+++ b/actix-cors/src/lib.rs
@@ -0,0 +1,1199 @@
+#![allow(clippy::borrow_interior_mutable_const, clippy::type_complexity)]
+//! Cross-origin resource sharing (CORS) for Actix applications
+//!
+//! CORS middleware could be used with application and with resource.
+//! Cors middleware could be used as parameter for `App::wrap()`,
+//! `Resource::wrap()` or `Scope::wrap()` methods.
+//!
+//! # Example
+//!
+//! ```rust
+//! use actix_cors::Cors;
+//! use actix_web::{http, web, App, HttpRequest, HttpResponse, HttpServer};
+//!
+//! async fn index(req: HttpRequest) -> &'static str {
+//! "Hello world"
+//! }
+//!
+//! fn main() -> std::io::Result<()> {
+//! HttpServer::new(|| App::new()
+//! .wrap(
+//! Cors::new() // <- Construct CORS middleware builder
+//! .allowed_origin("https://www.rust-lang.org/")
+//! .allowed_methods(vec!["GET", "POST"])
+//! .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
+//! .allowed_header(http::header::CONTENT_TYPE)
+//! .max_age(3600)
+//! .finish())
+//! .service(
+//! web::resource("/index.html")
+//! .route(web::get().to(index))
+//! .route(web::head().to(|| HttpResponse::MethodNotAllowed()))
+//! ))
+//! .bind("127.0.0.1:8080")?;
+//!
+//! Ok(())
+//! }
+//! ```
+//! In this example custom *CORS* middleware get registered for "/index.html"
+//! endpoint.
+//!
+//! Cors middleware automatically handle *OPTIONS* preflight request.
+use std::collections::HashSet;
+use std::iter::FromIterator;
+use std::rc::Rc;
+use std::task::{Context, Poll};
+
+use actix_service::{Service, Transform};
+use actix_web::dev::{RequestHead, ServiceRequest, ServiceResponse};
+use actix_web::error::{Error, ResponseError, Result};
+use actix_web::http::header::{self, HeaderName, HeaderValue};
+use actix_web::http::{self, HttpTryFrom, Method, StatusCode, Uri};
+use actix_web::HttpResponse;
+use derive_more::Display;
+use futures::future::{ok, Either, FutureExt, LocalBoxFuture, Ready};
+
+/// A set of errors that can occur during processing CORS
+#[derive(Debug, Display)]
+pub enum CorsError {
+ /// The HTTP request header `Origin` is required but was not provided
+ #[display(
+ fmt = "The HTTP request header `Origin` is required but was not provided"
+ )]
+ MissingOrigin,
+ /// The HTTP request header `Origin` could not be parsed correctly.
+ #[display(fmt = "The HTTP request header `Origin` could not be parsed correctly.")]
+ BadOrigin,
+ /// The request header `Access-Control-Request-Method` is required but is
+ /// missing
+ #[display(
+ fmt = "The request header `Access-Control-Request-Method` is required but is missing"
+ )]
+ MissingRequestMethod,
+ /// The request header `Access-Control-Request-Method` has an invalid value
+ #[display(
+ fmt = "The request header `Access-Control-Request-Method` has an invalid value"
+ )]
+ BadRequestMethod,
+ /// The request header `Access-Control-Request-Headers` has an invalid
+ /// value
+ #[display(
+ fmt = "The request header `Access-Control-Request-Headers` has an invalid value"
+ )]
+ BadRequestHeaders,
+ /// Origin is not allowed to make this request
+ #[display(fmt = "Origin is not allowed to make this request")]
+ OriginNotAllowed,
+ /// Requested method is not allowed
+ #[display(fmt = "Requested method is not allowed")]
+ MethodNotAllowed,
+ /// One or more headers requested are not allowed
+ #[display(fmt = "One or more headers requested are not allowed")]
+ HeadersNotAllowed,
+}
+
+impl ResponseError for CorsError {
+ fn status_code(&self) -> StatusCode {
+ StatusCode::BAD_REQUEST
+ }
+
+ fn error_response(&self) -> HttpResponse {
+ HttpResponse::with_body(StatusCode::BAD_REQUEST, format!("{}", self).into())
+ }
+}
+
+/// An enum signifying that some of type T is allowed, or `All` (everything is
+/// allowed).
+///
+/// `Default` is implemented for this enum and is `All`.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum AllOrSome {
+ /// Everything is allowed. Usually equivalent to the "*" value.
+ All,
+ /// Only some of `T` is allowed
+ Some(T),
+}
+
+impl Default for AllOrSome {
+ fn default() -> Self {
+ AllOrSome::All
+ }
+}
+
+impl AllOrSome {
+ /// Returns whether this is an `All` variant
+ pub fn is_all(&self) -> bool {
+ match *self {
+ AllOrSome::All => true,
+ AllOrSome::Some(_) => false,
+ }
+ }
+
+ /// Returns whether this is a `Some` variant
+ pub fn is_some(&self) -> bool {
+ !self.is_all()
+ }
+
+ /// Returns &T
+ pub fn as_ref(&self) -> Option<&T> {
+ match *self {
+ AllOrSome::All => None,
+ AllOrSome::Some(ref t) => Some(t),
+ }
+ }
+}
+
+/// Structure that follows the builder pattern for building `Cors` middleware
+/// structs.
+///
+/// To construct a cors:
+///
+/// 1. Call [`Cors::build`](struct.Cors.html#method.build) to start building.
+/// 2. Use any of the builder methods to set fields in the backend.
+/// 3. Call [finish](struct.Cors.html#method.finish) to retrieve the
+/// constructed backend.
+///
+/// # Example
+///
+/// ```rust
+/// use actix_cors::Cors;
+/// use actix_web::http::header;
+///
+/// # fn main() {
+/// let cors = Cors::new()
+/// .allowed_origin("https://www.rust-lang.org/")
+/// .allowed_methods(vec!["GET", "POST"])
+/// .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
+/// .allowed_header(header::CONTENT_TYPE)
+/// .max_age(3600);
+/// # }
+/// ```
+#[derive(Default)]
+pub struct Cors {
+ cors: Option,
+ methods: bool,
+ error: Option,
+ expose_hdrs: HashSet,
+}
+
+impl Cors {
+ /// Build a new CORS middleware instance
+ pub fn new() -> Cors {
+ Cors {
+ cors: Some(Inner {
+ origins: AllOrSome::All,
+ origins_str: None,
+ methods: HashSet::new(),
+ headers: AllOrSome::All,
+ expose_hdrs: None,
+ max_age: None,
+ preflight: true,
+ send_wildcard: false,
+ supports_credentials: false,
+ vary_header: true,
+ }),
+ methods: false,
+ error: None,
+ expose_hdrs: HashSet::new(),
+ }
+ }
+
+ /// Build a new CORS default middleware
+ pub fn default() -> CorsFactory {
+ let inner = Inner {
+ origins: AllOrSome::default(),
+ origins_str: None,
+ methods: HashSet::from_iter(
+ vec![
+ Method::GET,
+ Method::HEAD,
+ Method::POST,
+ Method::OPTIONS,
+ Method::PUT,
+ Method::PATCH,
+ Method::DELETE,
+ ]
+ .into_iter(),
+ ),
+ headers: AllOrSome::All,
+ expose_hdrs: None,
+ max_age: None,
+ preflight: true,
+ send_wildcard: false,
+ supports_credentials: false,
+ vary_header: true,
+ };
+ CorsFactory {
+ inner: Rc::new(inner),
+ }
+ }
+
+ /// Add an origin that are allowed to make requests.
+ /// Will be verified against the `Origin` request header.
+ ///
+ /// When `All` is set, and `send_wildcard` is set, "*" will be sent in
+ /// the `Access-Control-Allow-Origin` response header. Otherwise, the
+ /// client's `Origin` request header will be echoed back in the
+ /// `Access-Control-Allow-Origin` response header.
+ ///
+ /// When `Some` is set, the client's `Origin` request header will be
+ /// checked in a case-sensitive manner.
+ ///
+ /// This is the `list of origins` in the
+ /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model).
+ ///
+ /// Defaults to `All`.
+ ///
+ /// Builder panics if supplied origin is not valid uri.
+ pub fn allowed_origin(mut self, origin: &str) -> Cors {
+ if let Some(cors) = cors(&mut self.cors, &self.error) {
+ match Uri::try_from(origin) {
+ Ok(_) => {
+ if cors.origins.is_all() {
+ cors.origins = AllOrSome::Some(HashSet::new());
+ }
+ if let AllOrSome::Some(ref mut origins) = cors.origins {
+ origins.insert(origin.to_owned());
+ }
+ }
+ Err(e) => {
+ self.error = Some(e.into());
+ }
+ }
+ }
+ self
+ }
+
+ /// Set a list of methods which the allowed origins are allowed to access
+ /// for requests.
+ ///
+ /// This is the `list of methods` in the
+ /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model).
+ ///
+ /// Defaults to `[GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE]`
+ pub fn allowed_methods(mut self, methods: U) -> Cors
+ where
+ U: IntoIterator
- ,
+ Method: HttpTryFrom,
+ {
+ self.methods = true;
+ if let Some(cors) = cors(&mut self.cors, &self.error) {
+ for m in methods {
+ match Method::try_from(m) {
+ Ok(method) => {
+ cors.methods.insert(method);
+ }
+ Err(e) => {
+ self.error = Some(e.into());
+ break;
+ }
+ }
+ }
+ }
+ self
+ }
+
+ /// Set an allowed header
+ pub fn allowed_header(mut self, header: H) -> Cors
+ where
+ HeaderName: HttpTryFrom,
+ {
+ if let Some(cors) = cors(&mut self.cors, &self.error) {
+ match HeaderName::try_from(header) {
+ Ok(method) => {
+ if cors.headers.is_all() {
+ cors.headers = AllOrSome::Some(HashSet::new());
+ }
+ if let AllOrSome::Some(ref mut headers) = cors.headers {
+ headers.insert(method);
+ }
+ }
+ Err(e) => self.error = Some(e.into()),
+ }
+ }
+ self
+ }
+
+ /// Set a list of header field names which can be used when
+ /// this resource is accessed by allowed origins.
+ ///
+ /// If `All` is set, whatever is requested by the client in
+ /// `Access-Control-Request-Headers` will be echoed back in the
+ /// `Access-Control-Allow-Headers` header.
+ ///
+ /// This is the `list of headers` in the
+ /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model).
+ ///
+ /// Defaults to `All`.
+ pub fn allowed_headers(mut self, headers: U) -> Cors
+ where
+ U: IntoIterator
- ,
+ HeaderName: HttpTryFrom,
+ {
+ if let Some(cors) = cors(&mut self.cors, &self.error) {
+ for h in headers {
+ match HeaderName::try_from(h) {
+ Ok(method) => {
+ if cors.headers.is_all() {
+ cors.headers = AllOrSome::Some(HashSet::new());
+ }
+ if let AllOrSome::Some(ref mut headers) = cors.headers {
+ headers.insert(method);
+ }
+ }
+ Err(e) => {
+ self.error = Some(e.into());
+ break;
+ }
+ }
+ }
+ }
+ self
+ }
+
+ /// Set a list of headers which are safe to expose to the API of a CORS API
+ /// specification. This corresponds to the
+ /// `Access-Control-Expose-Headers` response header.
+ ///
+ /// This is the `list of exposed headers` in the
+ /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model).
+ ///
+ /// This defaults to an empty set.
+ pub fn expose_headers(mut self, headers: U) -> Cors
+ where
+ U: IntoIterator
- ,
+ HeaderName: HttpTryFrom,
+ {
+ for h in headers {
+ match HeaderName::try_from(h) {
+ Ok(method) => {
+ self.expose_hdrs.insert(method);
+ }
+ Err(e) => {
+ self.error = Some(e.into());
+ break;
+ }
+ }
+ }
+ self
+ }
+
+ /// Set a maximum time for which this CORS request maybe cached.
+ /// This value is set as the `Access-Control-Max-Age` header.
+ ///
+ /// This defaults to `None` (unset).
+ pub fn max_age(mut self, max_age: usize) -> Cors {
+ if let Some(cors) = cors(&mut self.cors, &self.error) {
+ cors.max_age = Some(max_age)
+ }
+ self
+ }
+
+ /// Set a wildcard origins
+ ///
+ /// If send wildcard is set and the `allowed_origins` parameter is `All`, a
+ /// wildcard `Access-Control-Allow-Origin` response header is sent,
+ /// rather than the request’s `Origin` header.
+ ///
+ /// This is the `supports credentials flag` in the
+ /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model).
+ ///
+ /// This **CANNOT** be used in conjunction with `allowed_origins` set to
+ /// `All` and `allow_credentials` set to `true`. Depending on the mode
+ /// of usage, this will either result in an `Error::
+ /// CredentialsWithWildcardOrigin` error during actix launch or runtime.
+ ///
+ /// Defaults to `false`.
+ pub fn send_wildcard(mut self) -> Cors {
+ if let Some(cors) = cors(&mut self.cors, &self.error) {
+ cors.send_wildcard = true
+ }
+ self
+ }
+
+ /// Allows users to make authenticated requests
+ ///
+ /// If true, injects the `Access-Control-Allow-Credentials` header in
+ /// responses. This allows cookies and credentials to be submitted
+ /// across domains.
+ ///
+ /// This option cannot be used in conjunction with an `allowed_origin` set
+ /// to `All` and `send_wildcards` set to `true`.
+ ///
+ /// Defaults to `false`.
+ ///
+ /// Builder panics if credentials are allowed, but the Origin is set to "*".
+ /// This is not allowed by W3C
+ pub fn supports_credentials(mut self) -> Cors {
+ if let Some(cors) = cors(&mut self.cors, &self.error) {
+ cors.supports_credentials = true
+ }
+ self
+ }
+
+ /// Disable `Vary` header support.
+ ///
+ /// When enabled the header `Vary: Origin` will be returned as per the W3
+ /// implementation guidelines.
+ ///
+ /// Setting this header when the `Access-Control-Allow-Origin` is
+ /// dynamically generated (e.g. when there is more than one allowed
+ /// origin, and an Origin than '*' is returned) informs CDNs and other
+ /// caches that the CORS headers are dynamic, and cannot be cached.
+ ///
+ /// By default `vary` header support is enabled.
+ pub fn disable_vary_header(mut self) -> Cors {
+ if let Some(cors) = cors(&mut self.cors, &self.error) {
+ cors.vary_header = false
+ }
+ self
+ }
+
+ /// Disable *preflight* request support.
+ ///
+ /// When enabled cors middleware automatically handles *OPTIONS* request.
+ /// This is useful application level middleware.
+ ///
+ /// By default *preflight* support is enabled.
+ pub fn disable_preflight(mut self) -> Cors {
+ if let Some(cors) = cors(&mut self.cors, &self.error) {
+ cors.preflight = false
+ }
+ self
+ }
+
+ /// Construct cors middleware
+ pub fn finish(self) -> CorsFactory {
+ let mut slf = if !self.methods {
+ self.allowed_methods(vec![
+ Method::GET,
+ Method::HEAD,
+ Method::POST,
+ Method::OPTIONS,
+ Method::PUT,
+ Method::PATCH,
+ Method::DELETE,
+ ])
+ } else {
+ self
+ };
+
+ if let Some(e) = slf.error.take() {
+ panic!("{}", e);
+ }
+
+ let mut cors = slf.cors.take().expect("cannot reuse CorsBuilder");
+
+ if cors.supports_credentials && cors.send_wildcard && cors.origins.is_all() {
+ panic!("Credentials are allowed, but the Origin is set to \"*\"");
+ }
+
+ if let AllOrSome::Some(ref origins) = cors.origins {
+ let s = origins
+ .iter()
+ .fold(String::new(), |s, v| format!("{}, {}", s, v));
+ cors.origins_str = Some(HeaderValue::try_from(&s[2..]).unwrap());
+ }
+
+ if !slf.expose_hdrs.is_empty() {
+ cors.expose_hdrs = Some(
+ slf.expose_hdrs
+ .iter()
+ .fold(String::new(), |s, v| format!("{}, {}", s, v.as_str()))[2..]
+ .to_owned(),
+ );
+ }
+
+ CorsFactory {
+ inner: Rc::new(cors),
+ }
+ }
+}
+
+fn cors<'a>(
+ parts: &'a mut Option,
+ err: &Option,
+) -> Option<&'a mut Inner> {
+ if err.is_some() {
+ return None;
+ }
+ parts.as_mut()
+}
+
+/// `Middleware` for Cross-origin resource sharing support
+///
+/// The Cors struct contains the settings for CORS requests to be validated and
+/// for responses to be generated.
+pub struct CorsFactory {
+ inner: Rc,
+}
+
+impl
Transform for CorsFactory
+where
+ S: Service, Error = Error>,
+ S::Future: 'static,
+ B: 'static,
+{
+ type Request = ServiceRequest;
+ type Response = ServiceResponse;
+ type Error = Error;
+ type InitError = ();
+ type Transform = CorsMiddleware;
+ type Future = Ready>;
+
+ fn new_transform(&self, service: S) -> Self::Future {
+ ok(CorsMiddleware {
+ service,
+ inner: self.inner.clone(),
+ })
+ }
+}
+
+/// `Middleware` for Cross-origin resource sharing support
+///
+/// The Cors struct contains the settings for CORS requests to be validated and
+/// for responses to be generated.
+#[derive(Clone)]
+pub struct CorsMiddleware {
+ service: S,
+ inner: Rc,
+}
+
+struct Inner {
+ methods: HashSet,
+ origins: AllOrSome>,
+ origins_str: Option,
+ headers: AllOrSome>,
+ expose_hdrs: Option,
+ max_age: Option,
+ preflight: bool,
+ send_wildcard: bool,
+ supports_credentials: bool,
+ vary_header: bool,
+}
+
+impl Inner {
+ fn validate_origin(&self, req: &RequestHead) -> Result<(), CorsError> {
+ if let Some(hdr) = req.headers().get(&header::ORIGIN) {
+ if let Ok(origin) = hdr.to_str() {
+ return match self.origins {
+ AllOrSome::All => Ok(()),
+ AllOrSome::Some(ref allowed_origins) => allowed_origins
+ .get(origin)
+ .and_then(|_| Some(()))
+ .ok_or_else(|| CorsError::OriginNotAllowed),
+ };
+ }
+ Err(CorsError::BadOrigin)
+ } else {
+ match self.origins {
+ AllOrSome::All => Ok(()),
+ _ => Err(CorsError::MissingOrigin),
+ }
+ }
+ }
+
+ fn access_control_allow_origin(&self, req: &RequestHead) -> Option {
+ match self.origins {
+ AllOrSome::All => {
+ if self.send_wildcard {
+ Some(HeaderValue::from_static("*"))
+ } else if let Some(origin) = req.headers().get(&header::ORIGIN) {
+ Some(origin.clone())
+ } else {
+ None
+ }
+ }
+ AllOrSome::Some(ref origins) => {
+ if let Some(origin) =
+ req.headers()
+ .get(&header::ORIGIN)
+ .filter(|o| match o.to_str() {
+ Ok(os) => origins.contains(os),
+ _ => false,
+ })
+ {
+ Some(origin.clone())
+ } else {
+ Some(self.origins_str.as_ref().unwrap().clone())
+ }
+ }
+ }
+ }
+
+ fn validate_allowed_method(&self, req: &RequestHead) -> Result<(), CorsError> {
+ if let Some(hdr) = req.headers().get(&header::ACCESS_CONTROL_REQUEST_METHOD) {
+ if let Ok(meth) = hdr.to_str() {
+ if let Ok(method) = Method::try_from(meth) {
+ return self
+ .methods
+ .get(&method)
+ .and_then(|_| Some(()))
+ .ok_or_else(|| CorsError::MethodNotAllowed);
+ }
+ }
+ Err(CorsError::BadRequestMethod)
+ } else {
+ Err(CorsError::MissingRequestMethod)
+ }
+ }
+
+ fn validate_allowed_headers(&self, req: &RequestHead) -> Result<(), CorsError> {
+ match self.headers {
+ AllOrSome::All => Ok(()),
+ AllOrSome::Some(ref allowed_headers) => {
+ if let Some(hdr) =
+ req.headers().get(&header::ACCESS_CONTROL_REQUEST_HEADERS)
+ {
+ if let Ok(headers) = hdr.to_str() {
+ let mut hdrs = HashSet::new();
+ for hdr in headers.split(',') {
+ match HeaderName::try_from(hdr.trim()) {
+ Ok(hdr) => hdrs.insert(hdr),
+ Err(_) => return Err(CorsError::BadRequestHeaders),
+ };
+ }
+ // `Access-Control-Request-Headers` must contain 1 or more
+ // `field-name`.
+ if !hdrs.is_empty() {
+ if !hdrs.is_subset(allowed_headers) {
+ return Err(CorsError::HeadersNotAllowed);
+ }
+ return Ok(());
+ }
+ }
+ Err(CorsError::BadRequestHeaders)
+ } else {
+ Ok(())
+ }
+ }
+ }
+ }
+}
+
+impl Service for CorsMiddleware
+where
+ S: Service, Error = Error>,
+ S::Future: 'static,
+ B: 'static,
+{
+ type Request = ServiceRequest;
+ type Response = ServiceResponse;
+ type Error = Error;
+ type Future = Either<
+ Ready>,
+ LocalBoxFuture<'static, Result>,
+ >;
+
+ fn poll_ready(&mut self, cx: &mut Context) -> Poll> {
+ self.service.poll_ready(cx)
+ }
+
+ fn call(&mut self, req: ServiceRequest) -> Self::Future {
+ if self.inner.preflight && Method::OPTIONS == *req.method() {
+ if let Err(e) = self
+ .inner
+ .validate_origin(req.head())
+ .and_then(|_| self.inner.validate_allowed_method(req.head()))
+ .and_then(|_| self.inner.validate_allowed_headers(req.head()))
+ {
+ return Either::Left(ok(req.error_response(e)));
+ }
+
+ // allowed headers
+ let headers = if let Some(headers) = self.inner.headers.as_ref() {
+ Some(
+ HeaderValue::try_from(
+ &headers
+ .iter()
+ .fold(String::new(), |s, v| s + "," + v.as_str())
+ .as_str()[1..],
+ )
+ .unwrap(),
+ )
+ } else if let Some(hdr) =
+ req.headers().get(&header::ACCESS_CONTROL_REQUEST_HEADERS)
+ {
+ Some(hdr.clone())
+ } else {
+ None
+ };
+
+ let res = HttpResponse::Ok()
+ .if_some(self.inner.max_age.as_ref(), |max_age, resp| {
+ let _ = resp.header(
+ header::ACCESS_CONTROL_MAX_AGE,
+ format!("{}", max_age).as_str(),
+ );
+ })
+ .if_some(headers, |headers, resp| {
+ let _ = resp.header(header::ACCESS_CONTROL_ALLOW_HEADERS, headers);
+ })
+ .if_some(
+ self.inner.access_control_allow_origin(req.head()),
+ |origin, resp| {
+ let _ = resp.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin);
+ },
+ )
+ .if_true(self.inner.supports_credentials, |resp| {
+ resp.header(header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
+ })
+ .header(
+ header::ACCESS_CONTROL_ALLOW_METHODS,
+ &self
+ .inner
+ .methods
+ .iter()
+ .fold(String::new(), |s, v| s + "," + v.as_str())
+ .as_str()[1..],
+ )
+ .finish()
+ .into_body();
+
+ Either::Left(ok(req.into_response(res)))
+ } else {
+ if req.headers().contains_key(&header::ORIGIN) {
+ // Only check requests with a origin header.
+ if let Err(e) = self.inner.validate_origin(req.head()) {
+ return Either::Left(ok(req.error_response(e)));
+ }
+ }
+
+ let inner = self.inner.clone();
+ let has_origin = req.headers().contains_key(&header::ORIGIN);
+ let fut = self.service.call(req);
+
+ Either::Right(
+ async move {
+ let res = fut.await;
+
+ if has_origin {
+ let mut res = res?;
+ if let Some(origin) =
+ inner.access_control_allow_origin(res.request().head())
+ {
+ res.headers_mut().insert(
+ header::ACCESS_CONTROL_ALLOW_ORIGIN,
+ origin.clone(),
+ );
+ };
+
+ if let Some(ref expose) = inner.expose_hdrs {
+ res.headers_mut().insert(
+ header::ACCESS_CONTROL_EXPOSE_HEADERS,
+ HeaderValue::try_from(expose.as_str()).unwrap(),
+ );
+ }
+ if inner.supports_credentials {
+ res.headers_mut().insert(
+ header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
+ HeaderValue::from_static("true"),
+ );
+ }
+ if inner.vary_header {
+ let value = if let Some(hdr) =
+ res.headers_mut().get(&header::VARY)
+ {
+ let mut val: Vec =
+ Vec::with_capacity(hdr.as_bytes().len() + 8);
+ val.extend(hdr.as_bytes());
+ val.extend(b", Origin");
+ HeaderValue::try_from(&val[..]).unwrap()
+ } else {
+ HeaderValue::from_static("Origin")
+ };
+ res.headers_mut().insert(header::VARY, value);
+ }
+ Ok(res)
+ } else {
+ res
+ }
+ }
+ .boxed_local(),
+ )
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use actix_service::{service_fn2, Transform};
+ use actix_web::test::{self, TestRequest};
+
+ use super::*;
+
+ #[actix_rt::test]
+ #[should_panic(expected = "Credentials are allowed, but the Origin is set to")]
+ async fn cors_validates_illegal_allow_credentials() {
+ let _cors = Cors::new().supports_credentials().send_wildcard().finish();
+ }
+
+ #[actix_rt::test]
+ async fn validate_origin_allows_all_origins() {
+ let mut cors = Cors::new()
+ .finish()
+ .new_transform(test::ok_service())
+ .await
+ .unwrap();
+ let req = TestRequest::with_header("Origin", "https://www.example.com")
+ .to_srv_request();
+
+ let resp = test::call_service(&mut cors, req).await;
+ assert_eq!(resp.status(), StatusCode::OK);
+ }
+
+ #[actix_rt::test]
+ async fn default() {
+ let mut cors = Cors::default()
+ .new_transform(test::ok_service())
+ .await
+ .unwrap();
+ let req = TestRequest::with_header("Origin", "https://www.example.com")
+ .to_srv_request();
+
+ let resp = test::call_service(&mut cors, req).await;
+ assert_eq!(resp.status(), StatusCode::OK);
+ }
+
+ #[actix_rt::test]
+ async fn test_preflight() {
+ let mut cors = Cors::new()
+ .send_wildcard()
+ .max_age(3600)
+ .allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST])
+ .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
+ .allowed_header(header::CONTENT_TYPE)
+ .finish()
+ .new_transform(test::ok_service())
+ .await
+ .unwrap();
+
+ let req = TestRequest::with_header("Origin", "https://www.example.com")
+ .method(Method::OPTIONS)
+ .header(header::ACCESS_CONTROL_REQUEST_HEADERS, "X-Not-Allowed")
+ .to_srv_request();
+
+ assert!(cors.inner.validate_allowed_method(req.head()).is_err());
+ assert!(cors.inner.validate_allowed_headers(req.head()).is_err());
+ let resp = test::call_service(&mut cors, req).await;
+ assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
+
+ let req = TestRequest::with_header("Origin", "https://www.example.com")
+ .header(header::ACCESS_CONTROL_REQUEST_METHOD, "put")
+ .method(Method::OPTIONS)
+ .to_srv_request();
+
+ assert!(cors.inner.validate_allowed_method(req.head()).is_err());
+ assert!(cors.inner.validate_allowed_headers(req.head()).is_ok());
+
+ let req = TestRequest::with_header("Origin", "https://www.example.com")
+ .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST")
+ .header(
+ header::ACCESS_CONTROL_REQUEST_HEADERS,
+ "AUTHORIZATION,ACCEPT",
+ )
+ .method(Method::OPTIONS)
+ .to_srv_request();
+
+ let resp = test::call_service(&mut cors, req).await;
+ assert_eq!(
+ &b"*"[..],
+ resp.headers()
+ .get(&header::ACCESS_CONTROL_ALLOW_ORIGIN)
+ .unwrap()
+ .as_bytes()
+ );
+ assert_eq!(
+ &b"3600"[..],
+ resp.headers()
+ .get(&header::ACCESS_CONTROL_MAX_AGE)
+ .unwrap()
+ .as_bytes()
+ );
+ let hdr = resp
+ .headers()
+ .get(&header::ACCESS_CONTROL_ALLOW_HEADERS)
+ .unwrap()
+ .to_str()
+ .unwrap();
+ assert!(hdr.contains("authorization"));
+ assert!(hdr.contains("accept"));
+ assert!(hdr.contains("content-type"));
+
+ let methods = resp
+ .headers()
+ .get(header::ACCESS_CONTROL_ALLOW_METHODS)
+ .unwrap()
+ .to_str()
+ .unwrap();
+ assert!(methods.contains("POST"));
+ assert!(methods.contains("GET"));
+ assert!(methods.contains("OPTIONS"));
+
+ Rc::get_mut(&mut cors.inner).unwrap().preflight = false;
+
+ let req = TestRequest::with_header("Origin", "https://www.example.com")
+ .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST")
+ .header(
+ header::ACCESS_CONTROL_REQUEST_HEADERS,
+ "AUTHORIZATION,ACCEPT",
+ )
+ .method(Method::OPTIONS)
+ .to_srv_request();
+
+ let resp = test::call_service(&mut cors, req).await;
+ assert_eq!(resp.status(), StatusCode::OK);
+ }
+
+ // #[actix_rt::test]
+ // #[should_panic(expected = "MissingOrigin")]
+ // async fn test_validate_missing_origin() {
+ // let cors = Cors::build()
+ // .allowed_origin("https://www.example.com")
+ // .finish();
+ // let mut req = HttpRequest::default();
+ // cors.start(&req).unwrap();
+ // }
+
+ #[actix_rt::test]
+ #[should_panic(expected = "OriginNotAllowed")]
+ async fn test_validate_not_allowed_origin() {
+ let cors = Cors::new()
+ .allowed_origin("https://www.example.com")
+ .finish()
+ .new_transform(test::ok_service())
+ .await
+ .unwrap();
+
+ let req = TestRequest::with_header("Origin", "https://www.unknown.com")
+ .method(Method::GET)
+ .to_srv_request();
+ cors.inner.validate_origin(req.head()).unwrap();
+ cors.inner.validate_allowed_method(req.head()).unwrap();
+ cors.inner.validate_allowed_headers(req.head()).unwrap();
+ }
+
+ #[actix_rt::test]
+ async fn test_validate_origin() {
+ let mut cors = Cors::new()
+ .allowed_origin("https://www.example.com")
+ .finish()
+ .new_transform(test::ok_service())
+ .await
+ .unwrap();
+
+ let req = TestRequest::with_header("Origin", "https://www.example.com")
+ .method(Method::GET)
+ .to_srv_request();
+
+ let resp = test::call_service(&mut cors, req).await;
+ assert_eq!(resp.status(), StatusCode::OK);
+ }
+
+ #[actix_rt::test]
+ async fn test_no_origin_response() {
+ let mut cors = Cors::new()
+ .disable_preflight()
+ .finish()
+ .new_transform(test::ok_service())
+ .await
+ .unwrap();
+
+ let req = TestRequest::default().method(Method::GET).to_srv_request();
+ let resp = test::call_service(&mut cors, req).await;
+ assert!(resp
+ .headers()
+ .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
+ .is_none());
+
+ let req = TestRequest::with_header("Origin", "https://www.example.com")
+ .method(Method::OPTIONS)
+ .to_srv_request();
+ let resp = test::call_service(&mut cors, req).await;
+ assert_eq!(
+ &b"https://www.example.com"[..],
+ resp.headers()
+ .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
+ .unwrap()
+ .as_bytes()
+ );
+ }
+
+ #[actix_rt::test]
+ async fn test_response() {
+ let exposed_headers = vec![header::AUTHORIZATION, header::ACCEPT];
+ let mut cors = Cors::new()
+ .send_wildcard()
+ .disable_preflight()
+ .max_age(3600)
+ .allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST])
+ .allowed_headers(exposed_headers.clone())
+ .expose_headers(exposed_headers.clone())
+ .allowed_header(header::CONTENT_TYPE)
+ .finish()
+ .new_transform(test::ok_service())
+ .await
+ .unwrap();
+
+ let req = TestRequest::with_header("Origin", "https://www.example.com")
+ .method(Method::OPTIONS)
+ .to_srv_request();
+
+ let resp = test::call_service(&mut cors, req).await;
+ assert_eq!(
+ &b"*"[..],
+ resp.headers()
+ .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
+ .unwrap()
+ .as_bytes()
+ );
+ assert_eq!(
+ &b"Origin"[..],
+ resp.headers().get(header::VARY).unwrap().as_bytes()
+ );
+
+ {
+ let headers = resp
+ .headers()
+ .get(header::ACCESS_CONTROL_EXPOSE_HEADERS)
+ .unwrap()
+ .to_str()
+ .unwrap()
+ .split(',')
+ .map(|s| s.trim())
+ .collect::>();
+
+ for h in exposed_headers {
+ assert!(headers.contains(&h.as_str()));
+ }
+ }
+
+ let exposed_headers = vec![header::AUTHORIZATION, header::ACCEPT];
+ let mut cors = Cors::new()
+ .send_wildcard()
+ .disable_preflight()
+ .max_age(3600)
+ .allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST])
+ .allowed_headers(exposed_headers.clone())
+ .expose_headers(exposed_headers.clone())
+ .allowed_header(header::CONTENT_TYPE)
+ .finish()
+ .new_transform(service_fn2(|req: ServiceRequest| {
+ ok(req.into_response(
+ HttpResponse::Ok().header(header::VARY, "Accept").finish(),
+ ))
+ }))
+ .await
+ .unwrap();
+ let req = TestRequest::with_header("Origin", "https://www.example.com")
+ .method(Method::OPTIONS)
+ .to_srv_request();
+ let resp = test::call_service(&mut cors, req).await;
+ assert_eq!(
+ &b"Accept, Origin"[..],
+ resp.headers().get(header::VARY).unwrap().as_bytes()
+ );
+
+ let mut cors = Cors::new()
+ .disable_vary_header()
+ .allowed_origin("https://www.example.com")
+ .allowed_origin("https://www.google.com")
+ .finish()
+ .new_transform(test::ok_service())
+ .await
+ .unwrap();
+
+ let req = TestRequest::with_header("Origin", "https://www.example.com")
+ .method(Method::OPTIONS)
+ .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST")
+ .to_srv_request();
+ let resp = test::call_service(&mut cors, req).await;
+
+ let origins_str = resp
+ .headers()
+ .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
+ .unwrap()
+ .to_str()
+ .unwrap();
+
+ assert_eq!("https://www.example.com", origins_str);
+ }
+
+ #[actix_rt::test]
+ async fn test_multiple_origins() {
+ let mut cors = Cors::new()
+ .allowed_origin("https://example.com")
+ .allowed_origin("https://example.org")
+ .allowed_methods(vec![Method::GET])
+ .finish()
+ .new_transform(test::ok_service())
+ .await
+ .unwrap();
+
+ let req = TestRequest::with_header("Origin", "https://example.com")
+ .method(Method::GET)
+ .to_srv_request();
+
+ let resp = test::call_service(&mut cors, req).await;
+ assert_eq!(
+ &b"https://example.com"[..],
+ resp.headers()
+ .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
+ .unwrap()
+ .as_bytes()
+ );
+
+ let req = TestRequest::with_header("Origin", "https://example.org")
+ .method(Method::GET)
+ .to_srv_request();
+
+ let resp = test::call_service(&mut cors, req).await;
+ assert_eq!(
+ &b"https://example.org"[..],
+ resp.headers()
+ .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
+ .unwrap()
+ .as_bytes()
+ );
+ }
+
+ #[actix_rt::test]
+ async fn test_multiple_origins_preflight() {
+ let mut cors = Cors::new()
+ .allowed_origin("https://example.com")
+ .allowed_origin("https://example.org")
+ .allowed_methods(vec![Method::GET])
+ .finish()
+ .new_transform(test::ok_service())
+ .await
+ .unwrap();
+
+ let req = TestRequest::with_header("Origin", "https://example.com")
+ .header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET")
+ .method(Method::OPTIONS)
+ .to_srv_request();
+
+ let resp = test::call_service(&mut cors, req).await;
+ assert_eq!(
+ &b"https://example.com"[..],
+ resp.headers()
+ .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
+ .unwrap()
+ .as_bytes()
+ );
+
+ let req = TestRequest::with_header("Origin", "https://example.org")
+ .header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET")
+ .method(Method::OPTIONS)
+ .to_srv_request();
+
+ let resp = test::call_service(&mut cors, req).await;
+ assert_eq!(
+ &b"https://example.org"[..],
+ resp.headers()
+ .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
+ .unwrap()
+ .as_bytes()
+ );
+ }
+}
diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md
new file mode 100644
index 000000000..5ec56593c
--- /dev/null
+++ b/actix-files/CHANGES.md
@@ -0,0 +1,64 @@
+# Changes
+
+## [0.1.7] - 2019-11-06
+
+* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
+
+## [0.1.6] - 2019-10-14
+
+* Add option to redirect to a slash-ended path `Files` #1132
+
+## [0.1.5] - 2019-10-08
+
+* Bump up `mime_guess` crate version to 2.0.1
+
+* Bump up `percent-encoding` crate version to 2.1
+
+* Allow user defined request guards for `Files` #1113
+
+## [0.1.4] - 2019-07-20
+
+* Allow to disable `Content-Disposition` header #686
+
+## [0.1.3] - 2019-06-28
+
+* Do not set `Content-Length` header, let actix-http set it #930
+
+## [0.1.2] - 2019-06-13
+
+* Content-Length is 0 for NamedFile HEAD request #914
+
+* Fix ring dependency from actix-web default features for #741
+
+## [0.1.1] - 2019-06-01
+
+* Static files are incorrectly served as both chunked and with length #812
+
+## [0.1.0] - 2019-05-25
+
+* NamedFile last-modified check always fails due to nano-seconds
+ in file modified date #820
+
+## [0.1.0-beta.4] - 2019-05-12
+
+* Update actix-web to beta.4
+
+## [0.1.0-beta.1] - 2019-04-20
+
+* Update actix-web to beta.1
+
+## [0.1.0-alpha.6] - 2019-04-14
+
+* Update actix-web to alpha6
+
+## [0.1.0-alpha.4] - 2019-04-08
+
+* Update actix-web to alpha4
+
+## [0.1.0-alpha.2] - 2019-04-02
+
+* Add default handler support
+
+## [0.1.0-alpha.1] - 2019-03-28
+
+* Initial impl
diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml
new file mode 100644
index 000000000..19366b902
--- /dev/null
+++ b/actix-files/Cargo.toml
@@ -0,0 +1,36 @@
+[package]
+name = "actix-files"
+version = "0.2.0-alpha.1"
+authors = ["Nikolay Kim "]
+description = "Static files support 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/"
+categories = ["asynchronous", "web-programming::http-server"]
+license = "MIT/Apache-2.0"
+edition = "2018"
+workspace = ".."
+
+[lib]
+name = "actix_files"
+path = "src/lib.rs"
+
+[dependencies]
+actix-web = { version = "2.0.0-alpha.1", default-features = false }
+actix-http = "0.3.0-alpha.1"
+actix-service = "1.0.0-alpha.1"
+bitflags = "1"
+bytes = "0.4"
+futures = "0.3.1"
+derive_more = "0.99.2"
+log = "0.4"
+mime = "0.3"
+mime_guess = "2.0.1"
+percent-encoding = "2.1"
+v_htmlescape = "0.4"
+
+[dev-dependencies]
+actix-rt = "1.0.0-alpha.1"
+actix-web = { version = "2.0.0-alpha.1", features=["openssl"] }
diff --git a/actix-files/LICENSE-APACHE b/actix-files/LICENSE-APACHE
new file mode 120000
index 000000000..965b606f3
--- /dev/null
+++ b/actix-files/LICENSE-APACHE
@@ -0,0 +1 @@
+../LICENSE-APACHE
\ No newline at end of file
diff --git a/actix-files/LICENSE-MIT b/actix-files/LICENSE-MIT
new file mode 120000
index 000000000..76219eb72
--- /dev/null
+++ b/actix-files/LICENSE-MIT
@@ -0,0 +1 @@
+../LICENSE-MIT
\ No newline at end of file
diff --git a/actix-files/README.md b/actix-files/README.md
new file mode 100644
index 000000000..9585e67a8
--- /dev/null
+++ b/actix-files/README.md
@@ -0,0 +1,9 @@
+# Static files support for actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-files)](https://crates.io/crates/actix-files) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+
+## Documentation & community resources
+
+* [User Guide](https://actix.rs/docs/)
+* [API Documentation](https://docs.rs/actix-files/)
+* [Chat on gitter](https://gitter.im/actix/actix)
+* Cargo package: [actix-files](https://crates.io/crates/actix-files)
+* Minimum supported Rust version: 1.33 or later
diff --git a/actix-files/src/error.rs b/actix-files/src/error.rs
new file mode 100644
index 000000000..49a46e58d
--- /dev/null
+++ b/actix-files/src/error.rs
@@ -0,0 +1,41 @@
+use actix_web::{http::StatusCode, HttpResponse, ResponseError};
+use derive_more::Display;
+
+/// Errors which can occur when serving static files.
+#[derive(Display, Debug, PartialEq)]
+pub enum FilesError {
+ /// Path is not a directory
+ #[display(fmt = "Path is not a directory. Unable to serve static files")]
+ IsNotDirectory,
+
+ /// Cannot render directory
+ #[display(fmt = "Unable to render directory without index file")]
+ IsDirectory,
+}
+
+/// Return `NotFound` for `FilesError`
+impl ResponseError for FilesError {
+ fn error_response(&self) -> HttpResponse {
+ HttpResponse::new(StatusCode::NOT_FOUND)
+ }
+}
+
+#[derive(Display, Debug, PartialEq)]
+pub enum UriSegmentError {
+ /// The segment started with the wrapped invalid character.
+ #[display(fmt = "The segment started with the wrapped invalid character")]
+ BadStart(char),
+ /// The segment contained the wrapped invalid character.
+ #[display(fmt = "The segment contained the wrapped invalid character")]
+ BadChar(char),
+ /// The segment ended with the wrapped invalid character.
+ #[display(fmt = "The segment ended with the wrapped invalid character")]
+ BadEnd(char),
+}
+
+/// Return `BadRequest` for `UriSegmentError`
+impl ResponseError for UriSegmentError {
+ fn status_code(&self) -> StatusCode {
+ StatusCode::BAD_REQUEST
+ }
+}
diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs
new file mode 100644
index 000000000..ed8b6c3b9
--- /dev/null
+++ b/actix-files/src/lib.rs
@@ -0,0 +1,1421 @@
+#![allow(clippy::borrow_interior_mutable_const, clippy::type_complexity)]
+
+//! Static files support
+use std::cell::RefCell;
+use std::fmt::Write;
+use std::fs::{DirEntry, File};
+use std::future::Future;
+use std::io::{Read, Seek};
+use std::path::{Path, PathBuf};
+use std::pin::Pin;
+use std::rc::Rc;
+use std::task::{Context, Poll};
+use std::{cmp, io};
+
+use actix_service::boxed::{self, BoxService, BoxServiceFactory};
+use actix_service::{IntoServiceFactory, Service, ServiceFactory};
+use actix_web::dev::{
+ AppService, HttpServiceFactory, Payload, ResourceDef, ServiceRequest,
+ ServiceResponse,
+};
+use actix_web::error::{Canceled, 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::future::{ok, ready, Either, FutureExt, LocalBoxFuture, Ready};
+use futures::Stream;
+use mime;
+use mime_guess::from_ext;
+use percent_encoding::{utf8_percent_encode, CONTROLS};
+use v_htmlescape::escape as escape_html_entity;
+
+mod error;
+mod named;
+mod range;
+
+use self::error::{FilesError, UriSegmentError};
+pub use crate::named::NamedFile;
+pub use crate::range::HttpRange;
+
+type HttpService = BoxService;
+type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>;
+
+/// Return the MIME type associated with a filename extension (case-insensitive).
+/// If `ext` is empty or no associated type for the extension was found, returns
+/// the type `application/octet-stream`.
+#[inline]
+pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
+ from_ext(ext).first_or_octet_stream()
+}
+
+#[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,
+ fut: Option<
+ LocalBoxFuture<'static, Result, Canceled>>,
+ >,
+ counter: u64,
+}
+
+impl Stream for ChunkedReadFile {
+ type Item = Result;
+
+ fn poll_next(
+ mut self: Pin<&mut Self>,
+ cx: &mut Context,
+ ) -> Poll