diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 2f0a4a7dd..000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,41 +0,0 @@ -environment: - global: - PROJECT_NAME: actix-web - matrix: - # Stable channel - - TARGET: i686-pc-windows-msvc - CHANNEL: stable - - TARGET: x86_64-pc-windows-gnu - CHANNEL: stable - - TARGET: x86_64-pc-windows-msvc - CHANNEL: stable - # Nightly channel - - TARGET: i686-pc-windows-msvc - CHANNEL: nightly - - TARGET: x86_64-pc-windows-gnu - CHANNEL: nightly - - TARGET: x86_64-pc-windows-msvc - CHANNEL: nightly - -# Install Rust and Cargo -# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml) -install: - - ps: >- - If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') { - $Env:PATH += ';C:\msys64\mingw64\bin' - } ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') { - $Env:PATH += ';C:\MinGW\bin' - } - - curl -sSf -o rustup-init.exe https://win.rustup.rs - - rustup-init.exe --default-host %TARGET% --default-toolchain %CHANNEL% -y - - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin - - rustc -Vv - - cargo -V - -# 'cargo test' takes care of building for us, so disable Appveyor's build stage. -build: false - -# Equivalent to Travis' `script` phase -test_script: - - cargo clean - - cargo test --no-default-features --features="flate2-rust" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..3e62958d8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Gitter channel (actix-web) + url: https://gitter.im/actix/actix-web + about: Please ask and answer questions about the actix-web here. + - name: Gitter channel (actix) + url: https://gitter.im/actix/actix + about: Please ask and answer questions about the actix here. diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 08bb81d48..ce8a7da7e 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -16,32 +16,8 @@ jobs: profile: minimal override: true - - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: - command: generate-lockfile - - name: Cache cargo registry - uses: actions/cache@v1 - with: - path: ~/.cargo/registry - key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-registry-trimmed-${{ hashFiles('**/Cargo.lock') }} - - name: Cache cargo index - uses: actions/cache@v1 - with: - path: ~/.cargo/git - key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-cargo-index-trimmed-${{ hashFiles('**/Cargo.lock') }} - - name: Cache cargo build - uses: actions/cache@v1 - with: - path: target - key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-cargo-build-trimmed-${{ hashFiles('**/Cargo.lock') }} - - name: Check benchmark uses: actions-rs/cargo@v1 with: command: bench - - - name: Clear the cargo caches - run: | - cargo install cargo-cache --no-default-features --features ci-autoclean - cargo-cache + args: --bench=server -- --sample-size=15 diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 000000000..ae804cc53 --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,64 @@ +name: CI (Linux) + +on: [push, pull_request] + +jobs: + build_and_test: + strategy: + fail-fast: false + matrix: + version: + - 1.40.0 # MSRV + - stable + - nightly + + name: ${{ matrix.version }} - x86_64-unknown-linux-gnu + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@master + + - name: Install ${{ matrix.version }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: check build + uses: actions-rs/cargo@v1 + with: + command: check + args: --all --bins --examples --tests + + - name: tests + uses: actions-rs/cargo@v1 + timeout-minutes: 40 + with: + command: test + args: --all --all-features --no-fail-fast -- --nocapture + + - name: tests (actix-http) + uses: actions-rs/cargo@v1 + timeout-minutes: 40 + with: + command: test + args: --package=actix-http --no-default-features --features=rustls -- --nocapture + + - name: tests (awc) + uses: actions-rs/cargo@v1 + timeout-minutes: 40 + with: + command: test + args: --package=awc --no-default-features --features=rustls -- --nocapture + + - name: Generate coverage file + if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request') + run: | + cargo install cargo-tarpaulin + cargo tarpaulin --out Xml + - name: Upload to Codecov + if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request') + uses: codecov/codecov-action@v1 + with: + file: cobertura.xml diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 397236a29..6c360bacc 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -24,26 +24,6 @@ jobs: profile: minimal override: true - - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: - command: generate-lockfile - - name: Cache cargo registry - uses: actions/cache@v1 - with: - path: ~/.cargo/registry - key: ${{ matrix.version }}-x86_64-apple-darwin-cargo-registry-trimmed-${{ hashFiles('**/Cargo.lock') }} - - name: Cache cargo index - uses: actions/cache@v1 - with: - path: ~/.cargo/git - key: ${{ matrix.version }}-x86_64-apple-darwin-cargo-index-trimmed-${{ hashFiles('**/Cargo.lock') }} - - name: Cache cargo build - uses: actions/cache@v1 - with: - path: target - key: ${{ matrix.version }}-x86_64-apple-darwin-cargo-build-trimmed-${{ hashFiles('**/Cargo.lock') }} - - name: check build uses: actions-rs/cargo@v1 with: @@ -57,8 +37,3 @@ jobs: args: --all --all-features --no-fail-fast -- --nocapture --skip=test_h2_content_length --skip=test_reading_deflate_encoding_large_random_rustls - - - name: Clear the cargo caches - run: | - cargo install cargo-cache --no-default-features --features ci-autoclean - cargo-cache diff --git a/.github/workflows/upload-doc.yml b/.github/workflows/upload-doc.yml new file mode 100644 index 000000000..75d534b28 --- /dev/null +++ b/.github/workflows/upload-doc.yml @@ -0,0 +1,35 @@ +name: Upload documentation + +on: + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + if: github.repository == 'actix/actix-web' + + steps: + - uses: actions/checkout@master + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: check build + uses: actions-rs/cargo@v1 + with: + command: doc + args: --no-deps --workspace --all-features + + - name: Tweak HTML + run: echo "" > target/doc/index.html + + - name: Upload documentation + run: | + git clone https://github.com/davisp/ghp-import.git + ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://${{ secrets.GITHUB_TOKEN }}@github.com/"${{ github.repository }}.git" target/doc diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 36b224ba6..5fd785fad 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -56,3 +56,4 @@ jobs: --skip=test_slow_request --skip=test_connection_force_close --skip=test_connection_server_close + --skip=test_connection_wait_queue_force_close diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f10f82a48..000000000 --- a/.travis.yml +++ /dev/null @@ -1,61 +0,0 @@ -language: rust -sudo: required -dist: trusty - -cache: - # cargo: true - apt: true - -matrix: - include: - - rust: stable - - rust: beta - - rust: nightly-2019-11-20 - allow_failures: - - rust: nightly-2019-11-20 - -env: - global: - # - RUSTFLAGS="-C link-dead-code" - - OPENSSL_VERSION=openssl-1.0.2 - -before_install: - - sudo add-apt-repository -y ppa:0k53d-karl-f830m/openssl - - sudo apt-get update -qq - - 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: - - export PATH=$PATH:~/.cargo/bin - -script: - - cargo update - - cargo check --all --no-default-features - - | - 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" == "stable" ]]; then - cargo doc --no-deps --all-features && - echo "" > target/doc/index.html && - 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_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 b42635b86..739f1b13c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,15 +1,55 @@ # Changes +## [3.0.0-alpha.3] - 2020-05-21 -## [2.0.NEXT] - 2020-01-xx +### Added + +* Add option to create `Data` from `Arc` [#1509] + +### Changed + +* Resources and Scopes can now access non-overridden data types set on App (or containing scopes) when setting their own data. [#1486] + +* Fix audit issue logging by default peer address [#1485] + +* Bump minimum supported Rust version to 1.40 + +* Replace deprecated `net2` crate with `socket2` + +[#1485]: https://github.com/actix/actix-web/pull/1485 +[#1509]: https://github.com/actix/actix-web/pull/1509 + +## [3.0.0-alpha.2] - 2020-05-08 + +### Changed + +* `{Resource,Scope}::default_service(f)` handlers now support app data extraction. [#1452] +* Implement `std::error::Error` for our custom errors [#1422] +* NormalizePath middleware now appends trailing / so that routes of form /example/ respond to /example requests. [#1433] +* Remove the `failure` feature and support. + +[#1422]: https://github.com/actix/actix-web/pull/1422 +[#1433]: https://github.com/actix/actix-web/pull/1433 +[#1452]: https://github.com/actix/actix-web/pull/1452 +[#1486]: https://github.com/actix/actix-web/pull/1486 + + +## [3.0.0-alpha.1] - 2020-03-11 + +### Added + +* Add helper function for creating routes with `TRACE` method guard `web::trace()` +* Add convenience functions `test::read_body_json()` and `test::TestRequest::send_request()` for testing. ### Changed * Use `sha-1` crate instead of unmaintained `sha1` crate +* Skip empty chunks when returning response from a `Stream` [#1308] +* Update the `time` dependency to 0.2.7 +* Update `actix-tls` dependency to 2.0.0-alpha.1 +* Update `rustls` dependency to 0.17 -* Skip empty chunks when returning response from a `Stream` #1308 - -* Update the `time` dependency to 0.2.5 +[#1308]: https://github.com/actix/actix-web/pull/1308 ## [2.0.0] - 2019-12-25 diff --git a/Cargo.toml b/Cargo.toml index a6783a6db..8c6d461d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "2.0.0" +version = "3.0.0-alpha.3" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" @@ -30,11 +30,7 @@ members = [ ".", "awc", "actix-http", - "actix-cors", "actix-files", - "actix-framed", - "actix-session", - "actix-identity", "actix-multipart", "actix-web-actors", "actix-web-codegen", @@ -42,7 +38,7 @@ members = [ ] [features] -default = ["compress", "failure"] +default = ["compress"] # content-encoding support compress = ["actix-http/compress", "awc/compress"] @@ -50,14 +46,24 @@ compress = ["actix-http/compress", "awc/compress"] # sessions feature, session require "ring" crate and c compiler secure-cookies = ["actix-http/secure-cookies"] -failure = ["actix-http/failure"] - # openssl openssl = ["actix-tls/openssl", "awc/openssl", "open-ssl"] # rustls rustls = ["actix-tls/rustls", "awc/rustls", "rust-tls"] +[[example]] +name = "basic" +required-features = ["compress"] + +[[example]] +name = "uds" +required-features = ["compress"] + +[[test]] +name = "test_server" +required-features = ["compress"] + [dependencies] actix-codec = "0.2.0" actix-service = "1.0.2" @@ -68,34 +74,37 @@ actix-server = "1.0.0" actix-testing = "1.0.0" actix-macros = "0.1.0" actix-threadpool = "0.3.1" -actix-tls = "1.0.0" +actix-tls = "2.0.0-alpha.1" -actix-web-codegen = "0.2.0" -actix-http = "1.0.1" -awc = { version = "1.0.1", default-features = false } +actix-web-codegen = "0.2.2" +actix-http = "2.0.0-alpha.4" +awc = { version = "2.0.0-alpha.2", default-features = false } bytes = "0.5.3" derive_more = "0.99.2" encoding_rs = "0.8" -futures = "0.3.1" +futures-channel = { version = "0.3.5", default-features = false } +futures-core = { version = "0.3.5", default-features = false } +futures-util = { version = "0.3.5", default-features = false } fxhash = "0.2.1" log = "0.4" mime = "0.3" -net2 = "0.2.33" -pin-project = "0.4.6" +socket2 = "0.3" +pin-project = "0.4.17" regex = "1.3" serde = { version = "1.0", features=["derive"] } serde_json = "1.0" serde_urlencoded = "0.6.1" -time = { version = "0.2.5", default-features = false, features = ["std"] } +time = { version = "0.2.7", default-features = false, features = ["std"] } url = "2.1" open-ssl = { version="0.10", package = "openssl", optional = true } -rust-tls = { version = "0.16.0", package = "rustls", optional = true } +rust-tls = { version = "0.17.0", package = "rustls", optional = true } +tinyvec = { version = "0.3", features = ["alloc"] } [dev-dependencies] -actix = "0.9.0" +actix = "0.10.0-alpha.1" rand = "0.7" -env_logger = "0.6" +env_logger = "0.7" serde_derive = "1.0" brotli2 = "0.3.2" flate2 = "1.0.13" @@ -111,9 +120,6 @@ actix-web = { path = "." } actix-http = { path = "actix-http" } actix-http-test = { path = "test-server" } actix-web-codegen = { path = "actix-web-codegen" } -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" } diff --git a/MIGRATION.md b/MIGRATION.md index aef382a21..d2e9735fb 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -4,6 +4,14 @@ result in `SameSite=None` being sent with the response Set-Cookie header. To create a cookie without a SameSite attribute, remove any calls setting same_site. +* actix-http support for Actors messages was moved to actix-http crate and is enabled + with feature `actors` +* content_length function is removed from actix-http. + You can set Content-Length by normally setting the response body or calling no_chunking function. + +* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a + `u64` instead of a `usize`. + ## 2.0.0 * `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to @@ -358,7 +366,7 @@ * `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. +* StaticFiles and NamedFile have been moved to a separate crate. instead of `use actix_web::fs::StaticFile` @@ -368,7 +376,7 @@ use `use actix_files::NamedFile` -* Multipart has been move to separate create. +* Multipart has been moved to a separate crate. instead of `use actix_web::multipart::Multipart` diff --git a/README.md b/README.md index 0b55c5bae..97e3ceeae 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![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](https://docs.rs/actix-web/badge.svg)](https://docs.rs/actix-web) [![Download](https://img.shields.io/crates/d/actix-web.svg)](https://crates.io/crates/actix-web) -[![Version](https://img.shields.io/badge/rustc-1.39+-lightgray.svg)](https://blog.rust-lang.org/2019/11/07/Rust-1.39.0.html) +[![Version](https://img.shields.io/badge/rustc-1.40+-lightgray.svg)](https://blog.rust-lang.org/2019/12/19/Rust-1.40.0.html) ![License](https://img.shields.io/crates/l/actix-web.svg)

@@ -38,6 +38,13 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust. * 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) +* Supports Rust 1.40+ + +## Docs + +* [API documentation (master)](https://actix.rs/actix-web/actix_web) +* [API documentation (docs.rs)](https://docs.rs/actix-web) +* [User guide](https://actix.rs) ## Example @@ -74,7 +81,7 @@ async fn main() -> std::io::Result<()> { * [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/) / +* [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/) diff --git a/actix-cors/CHANGES.md b/actix-cors/CHANGES.md deleted file mode 100644 index 8022ea4e8..000000000 --- a/actix-cors/CHANGES.md +++ /dev/null @@ -1,15 +0,0 @@ -# Changes - -## [0.2.0] - 2019-12-20 - -* Release - -## [0.2.0-alpha.3] - 2019-12-07 - -* Migrate to actix-web 2.0.0 - -* Bump `derive_more` crate version to 0.99.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 deleted file mode 100644 index 3fcd92f4f..000000000 --- a/actix-cors/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "actix-cors" -version = "0.2.0" -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-rc" -actix-service = "1.0.1" -derive_more = "0.99.2" -futures = "0.3.1" - -[dev-dependencies] -actix-rt = "1.0.0" diff --git a/actix-cors/LICENSE-APACHE b/actix-cors/LICENSE-APACHE deleted file mode 120000 index 965b606f3..000000000 --- a/actix-cors/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-APACHE \ No newline at end of file diff --git a/actix-cors/LICENSE-MIT b/actix-cors/LICENSE-MIT deleted file mode 120000 index 76219eb72..000000000 --- a/actix-cors/LICENSE-MIT +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-MIT \ No newline at end of file diff --git a/actix-cors/README.md b/actix-cors/README.md index a77f6c6d3..c860ec5ae 100644 --- a/actix-cors/README.md +++ b/actix-cors/README.md @@ -1,5 +1,7 @@ # 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) +**This crate moved to https://github.com/actix/actix-extras.** + ## Documentation & community resources * [User Guide](https://actix.rs/docs/) diff --git a/actix-cors/src/lib.rs b/actix-cors/src/lib.rs deleted file mode 100644 index 429fe9eab..000000000 --- a/actix-cors/src/lib.rs +++ /dev/null @@ -1,1204 +0,0 @@ -#![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::convert::TryFrom; -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, Error as HttpError, 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: TryFrom, - >::Error: Into, - { - 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: TryFrom, - >::Error: Into, - { - 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: TryFrom, - >::Error: Into, - { - 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: TryFrom, - >::Error: Into, - { - 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::{fn_service, 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(fn_service(|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 index c4918b56d..abf143997 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,5 +1,14 @@ # Changes +## [0.3.0-alpha.1] - 2020-05-23 + +* Update `actix-web` and `actix-http` dependencies to alpha +* Fix some typos in the docs +* Bump minimum supported Rust version to 1.40 +* Support sending Content-Length when Content-Range is specified [#1384] + +[#1384]: https://github.com/actix/actix-web/pull/1384 + ## [0.2.1] - 2019-12-22 * Use the same format for file URLs regardless of platforms diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 104eb3dfa..356c7a413 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.2.1" +version = "0.3.0-alpha.1" authors = ["Nikolay Kim "] description = "Static files support for actix web." readme = "README.md" @@ -18,12 +18,13 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "2.0.0-rc", default-features = false } -actix-http = "1.0.1" +actix-web = { version = "3.0.0-alpha.3", default-features = false } +actix-http = "2.0.0-alpha.4" actix-service = "1.0.1" bitflags = "1" bytes = "0.5.3" -futures = "0.3.1" +futures-core = { version = "0.3.5", default-features = false } +futures-util = { version = "0.3.5", default-features = false } derive_more = "0.99.2" log = "0.4" mime = "0.3" @@ -33,4 +34,4 @@ v_htmlescape = "0.4" [dev-dependencies] actix-rt = "1.0.0" -actix-web = { version = "2.0.0-rc", features=["openssl"] } +actix-web = { version = "3.0.0-alpha.3", features = ["openssl"] } diff --git a/actix-files/README.md b/actix-files/README.md index 9585e67a8..5a5a62083 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -6,4 +6,4 @@ * [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 +* Minimum supported Rust version: 1.40 or later diff --git a/actix-files/src/error.rs b/actix-files/src/error.rs index 49a46e58d..9b30cbaa2 100644 --- a/actix-files/src/error.rs +++ b/actix-files/src/error.rs @@ -5,6 +5,7 @@ use derive_more::Display; #[derive(Display, Debug, PartialEq)] pub enum FilesError { /// Path is not a directory + #[allow(dead_code)] #[display(fmt = "Path is not a directory. Unable to serve static files")] IsNotDirectory, diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index d910b7d5f..76c68ce25 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -24,8 +24,8 @@ 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 futures_core::Stream; +use futures_util::future::{ok, ready, Either, FutureExt, LocalBoxFuture, Ready}; use mime; use mime_guess::from_ext; use percent_encoding::{utf8_percent_encode, CONTROLS}; @@ -521,7 +521,7 @@ impl Service for FilesService { Err(e) => return Either::Left(ok(req.error_response(e))), }; - // full filepath + // full file path let path = match self.directory.join(&real_path.0).canonicalize() { Ok(path) => path, Err(e) => return self.handle_err(e, req), @@ -952,135 +952,92 @@ mod tests { #[actix_rt::test] async fn test_named_file_content_range_headers() { - let mut srv = test::init_service( - App::new().service(Files::new("/test", ".").index_file("tests/test.binary")), - ) - .await; + let srv = test::start(|| { + App::new().service(Files::new("/", ".")) + }); // Valid range header - let request = TestRequest::get() - .uri("/t%65st/tests/test.binary") + let response = srv + .get("/tests/test.binary") .header(header::RANGE, "bytes=10-20") - .to_request(); - - let response = test::call_service(&mut srv, request).await; - let contentrange = response - .headers() - .get(header::CONTENT_RANGE) - .unwrap() - .to_str() + .send() + .await .unwrap(); - - assert_eq!(contentrange, "bytes 10-20/100"); + let content_range = response.headers().get(header::CONTENT_RANGE).unwrap(); + assert_eq!(content_range.to_str().unwrap(), "bytes 10-20/100"); // Invalid range header - let request = TestRequest::get() - .uri("/t%65st/tests/test.binary") + let response = srv + .get("/tests/test.binary") .header(header::RANGE, "bytes=10-5") - .to_request(); - let response = test::call_service(&mut srv, request).await; - - let contentrange = response - .headers() - .get(header::CONTENT_RANGE) - .unwrap() - .to_str() + .send() + .await .unwrap(); - - assert_eq!(contentrange, "bytes */100"); + let content_range = response.headers().get(header::CONTENT_RANGE).unwrap(); + assert_eq!(content_range.to_str().unwrap(), "bytes */100"); } #[actix_rt::test] async fn test_named_file_content_length_headers() { - // use actix_web::body::{MessageBody, ResponseBody}; - - let mut srv = test::init_service( - App::new().service(Files::new("test", ".").index_file("tests/test.binary")), - ) - .await; + let srv = test::start(|| { + App::new().service(Files::new("/", ".")) + }); // Valid range header - let request = TestRequest::get() - .uri("/t%65st/tests/test.binary") + let response = srv + .get("/tests/test.binary") .header(header::RANGE, "bytes=10-20") - .to_request(); - let _response = test::call_service(&mut srv, request).await; + .send() + .await + .unwrap(); + let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap(); + assert_eq!(content_length.to_str().unwrap(), "11"); - // let contentlength = response - // .headers() - // .get(header::CONTENT_LENGTH) - // .unwrap() - // .to_str() - // .unwrap(); - // assert_eq!(contentlength, "11"); - - // Invalid range header - let request = TestRequest::get() - .uri("/t%65st/tests/test.binary") - .header(header::RANGE, "bytes=10-8") - .to_request(); - let response = test::call_service(&mut srv, request).await; - assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); + // Valid range header, starting from 0 + let response = srv + .get("/tests/test.binary") + .header(header::RANGE, "bytes=0-20") + .send() + .await + .unwrap(); + let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap(); + assert_eq!(content_length.to_str().unwrap(), "21"); // Without range header - let request = TestRequest::get() - .uri("/t%65st/tests/test.binary") - // .no_default_headers() - .to_request(); - let _response = test::call_service(&mut srv, request).await; + let mut response = srv.get("/tests/test.binary").send().await.unwrap(); + let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap(); + assert_eq!(content_length.to_str().unwrap(), "100"); - // let contentlength = response - // .headers() - // .get(header::CONTENT_LENGTH) - // .unwrap() - // .to_str() - // .unwrap(); - // assert_eq!(contentlength, "100"); + // Should be no transfer-encoding + let transfer_encoding = response.headers().get(header::TRANSFER_ENCODING); + assert!(transfer_encoding.is_none()); - // chunked - let request = TestRequest::get() - .uri("/t%65st/tests/test.binary") - .to_request(); - let response = test::call_service(&mut srv, request).await; - - // with enabled compression - // { - // let te = response - // .headers() - // .get(header::TRANSFER_ENCODING) - // .unwrap() - // .to_str() - // .unwrap(); - // assert_eq!(te, "chunked"); - // } - - let bytes = test::read_body(response).await; + // Check file contents + let bytes = response.body().await.unwrap(); let data = Bytes::from(fs::read("tests/test.binary").unwrap()); assert_eq!(bytes, data); } #[actix_rt::test] async fn test_head_content_length_headers() { - let mut srv = test::init_service( - App::new().service(Files::new("test", ".").index_file("tests/test.binary")), - ) - .await; + let srv = test::start(|| { + App::new().service(Files::new("/", ".")) + }); - // Valid range header - let request = TestRequest::default() - .method(Method::HEAD) - .uri("/t%65st/tests/test.binary") - .to_request(); - let _response = test::call_service(&mut srv, request).await; + let response = srv + .head("/tests/test.binary") + .send() + .await + .unwrap(); - // TODO: fix check - // let contentlength = response - // .headers() - // .get(header::CONTENT_LENGTH) - // .unwrap() - // .to_str() - // .unwrap(); - // assert_eq!(contentlength, "100"); + let content_length = response + .headers() + .get(header::CONTENT_LENGTH) + .unwrap() + .to_str() + .unwrap(); + + assert_eq!(content_length, "100"); } #[actix_rt::test] diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index fdb055998..6ee561a4b 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -18,7 +18,7 @@ use actix_web::http::header::{ }; use actix_web::http::{ContentEncoding, StatusCode}; use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder}; -use futures::future::{ready, Ready}; +use futures_util::future::{ready, Ready}; use crate::range::HttpRange; use crate::ChunkedReadFile; @@ -388,11 +388,12 @@ impl NamedFile { fut: None, counter: 0, }; + if offset != 0 || length != self.md.len() { - Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader)) - } else { - Ok(resp.body(SizedStream::new(length, reader))) + resp.status(StatusCode::PARTIAL_CONTENT); } + + Ok(resp.body(SizedStream::new(length, reader))) } } diff --git a/actix-framed/Cargo.toml b/actix-framed/Cargo.toml deleted file mode 100644 index 7e322e1d4..000000000 --- a/actix-framed/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "actix-framed" -version = "0.3.0" -authors = ["Nikolay Kim "] -description = "Actix framed app server" -readme = "README.md" -keywords = ["http", "web", "framework", "async", "futures"] -homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-framed/" -categories = ["network-programming", "asynchronous", - "web-programming::http-server", - "web-programming::websocket"] -license = "MIT/Apache-2.0" -edition = "2018" - -[lib] -name = "actix_framed" -path = "src/lib.rs" - -[dependencies] -actix-codec = "0.2.0" -actix-service = "1.0.1" -actix-router = "0.2.1" -actix-rt = "1.0.0" -actix-http = "1.0.1" - -bytes = "0.5.3" -futures = "0.3.1" -pin-project = "0.4.6" -log = "0.4" - -[dev-dependencies] -actix-server = "1.0.0" -actix-connect = { version = "1.0.0", features=["openssl"] } -actix-http-test = { version = "1.0.0", features=["openssl"] } -actix-utils = "1.0.3" diff --git a/actix-framed/LICENSE-APACHE b/actix-framed/LICENSE-APACHE deleted file mode 100644 index 6cdf2d16c..000000000 --- a/actix-framed/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2017-NOW Nikolay Kim - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/actix-framed/LICENSE-MIT b/actix-framed/LICENSE-MIT deleted file mode 100644 index 0f80296ae..000000000 --- a/actix-framed/LICENSE-MIT +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2017 Nikolay Kim - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/actix-framed/README.md b/actix-framed/README.md index 1714b3640..a4eaadf21 100644 --- a/actix-framed/README.md +++ b/actix-framed/README.md @@ -1,8 +1,3 @@ -# Framed app 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-framed)](https://crates.io/crates/actix-framed) [![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) +# Framed app for actix web -## Documentation & community resources - -* [API Documentation](https://docs.rs/actix-framed/) -* [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-framed](https://crates.io/crates/actix-framed) -* Minimum supported Rust version: 1.33 or later +**This crate has been deprecated and removed.** diff --git a/actix-framed/changes.md b/actix-framed/changes.md deleted file mode 100644 index 41c7aed0e..000000000 --- a/actix-framed/changes.md +++ /dev/null @@ -1,24 +0,0 @@ -# Changes - -## [0.3.0] - 2019-12-25 - -* Migrate to actix-http 1.0 - -## [0.2.1] - 2019-07-20 - -* Remove unneeded actix-utils dependency - - -## [0.2.0] - 2019-05-12 - -* Update dependencies - - -## [0.1.0] - 2019-04-16 - -* Update tests - - -## [0.1.0-alpha.1] - 2019-04-12 - -* Initial release diff --git a/actix-framed/src/app.rs b/actix-framed/src/app.rs deleted file mode 100644 index e4b91e6c4..000000000 --- a/actix-framed/src/app.rs +++ /dev/null @@ -1,221 +0,0 @@ -use std::future::Future; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; - -use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_http::h1::{Codec, SendResponse}; -use actix_http::{Error, Request, Response}; -use actix_router::{Path, Router, Url}; -use actix_service::{IntoServiceFactory, Service, ServiceFactory}; -use futures::future::{ok, FutureExt, LocalBoxFuture}; - -use crate::helpers::{BoxedHttpNewService, BoxedHttpService, HttpNewService}; -use crate::request::FramedRequest; -use crate::state::State; - -type BoxedResponse = LocalBoxFuture<'static, Result<(), Error>>; - -pub trait HttpServiceFactory { - type Factory: ServiceFactory; - - fn path(&self) -> &str; - - fn create(self) -> Self::Factory; -} - -/// Application builder -pub struct FramedApp { - state: State, - services: Vec<(String, BoxedHttpNewService>)>, -} - -impl FramedApp { - pub fn new() -> Self { - FramedApp { - state: State::new(()), - services: Vec::new(), - } - } -} - -impl FramedApp { - pub fn with(state: S) -> FramedApp { - FramedApp { - services: Vec::new(), - state: State::new(state), - } - } - - pub fn service(mut self, factory: U) -> Self - where - U: HttpServiceFactory, - U::Factory: ServiceFactory< - Config = (), - Request = FramedRequest, - Response = (), - Error = Error, - InitError = (), - > + 'static, - ::Future: 'static, - ::Service: Service< - Request = FramedRequest, - Response = (), - Error = Error, - Future = LocalBoxFuture<'static, Result<(), Error>>, - >, - { - let path = factory.path().to_string(); - self.services - .push((path, Box::new(HttpNewService::new(factory.create())))); - self - } -} - -impl IntoServiceFactory> for FramedApp -where - T: AsyncRead + AsyncWrite + Unpin + 'static, - S: 'static, -{ - fn into_factory(self) -> FramedAppFactory { - FramedAppFactory { - state: self.state, - services: Rc::new(self.services), - } - } -} - -#[derive(Clone)] -pub struct FramedAppFactory { - state: State, - services: Rc>)>>, -} - -impl ServiceFactory for FramedAppFactory -where - T: AsyncRead + AsyncWrite + Unpin + 'static, - S: 'static, -{ - type Config = (); - type Request = (Request, Framed); - type Response = (); - type Error = Error; - type InitError = (); - type Service = FramedAppService; - type Future = CreateService; - - fn new_service(&self, _: ()) -> Self::Future { - CreateService { - fut: self - .services - .iter() - .map(|(path, service)| { - CreateServiceItem::Future( - Some(path.clone()), - service.new_service(()), - ) - }) - .collect(), - state: self.state.clone(), - } - } -} - -#[doc(hidden)] -pub struct CreateService { - fut: Vec>, - state: State, -} - -enum CreateServiceItem { - Future( - Option, - LocalBoxFuture<'static, Result>, ()>>, - ), - Service(String, BoxedHttpService>), -} - -impl Future for CreateService -where - T: AsyncRead + AsyncWrite + Unpin, -{ - type Output = Result, ()>; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { - let mut done = true; - - // poll http services - for item in &mut self.fut { - let res = match item { - CreateServiceItem::Future(ref mut path, ref mut fut) => { - match Pin::new(fut).poll(cx) { - Poll::Ready(Ok(service)) => { - Some((path.take().unwrap(), service)) - } - Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), - Poll::Pending => { - done = false; - None - } - } - } - CreateServiceItem::Service(_, _) => continue, - }; - - if let Some((path, service)) = res { - *item = CreateServiceItem::Service(path, service); - } - } - - if done { - let router = self - .fut - .drain(..) - .fold(Router::build(), |mut router, item| { - match item { - CreateServiceItem::Service(path, service) => { - router.path(&path, service); - } - CreateServiceItem::Future(_, _) => unreachable!(), - } - router - }); - Poll::Ready(Ok(FramedAppService { - router: router.finish(), - state: self.state.clone(), - })) - } else { - Poll::Pending - } - } -} - -pub struct FramedAppService { - state: State, - router: Router>>, -} - -impl Service for FramedAppService -where - T: AsyncRead + AsyncWrite + Unpin, -{ - type Request = (Request, Framed); - type Response = (); - type Error = Error; - type Future = BoxedResponse; - - fn poll_ready(&mut self, _: &mut Context) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, (req, framed): (Request, Framed)) -> Self::Future { - let mut path = Path::new(Url::new(req.uri().clone())); - - if let Some((srv, _info)) = self.router.recognize_mut(&mut path) { - return srv.call(FramedRequest::new(req, framed, path, self.state.clone())); - } - SendResponse::new(framed, Response::NotFound().finish()) - .then(|_| ok(())) - .boxed_local() - } -} diff --git a/actix-framed/src/helpers.rs b/actix-framed/src/helpers.rs deleted file mode 100644 index 29492e45b..000000000 --- a/actix-framed/src/helpers.rs +++ /dev/null @@ -1,98 +0,0 @@ -use std::task::{Context, Poll}; - -use actix_http::Error; -use actix_service::{Service, ServiceFactory}; -use futures::future::{FutureExt, LocalBoxFuture}; - -pub(crate) type BoxedHttpService = Box< - dyn Service< - Request = Req, - Response = (), - Error = Error, - Future = LocalBoxFuture<'static, Result<(), Error>>, - >, ->; - -pub(crate) type BoxedHttpNewService = Box< - dyn ServiceFactory< - Config = (), - Request = Req, - Response = (), - Error = Error, - InitError = (), - Service = BoxedHttpService, - Future = LocalBoxFuture<'static, Result, ()>>, - >, ->; - -pub(crate) struct HttpNewService(T); - -impl HttpNewService -where - T: ServiceFactory, - T::Response: 'static, - T::Future: 'static, - T::Service: Service>> + 'static, - ::Future: 'static, -{ - pub fn new(service: T) -> Self { - HttpNewService(service) - } -} - -impl ServiceFactory for HttpNewService -where - T: ServiceFactory, - T::Request: 'static, - T::Future: 'static, - T::Service: Service>> + 'static, - ::Future: 'static, -{ - type Config = (); - type Request = T::Request; - type Response = (); - type Error = Error; - type InitError = (); - type Service = BoxedHttpService; - type Future = LocalBoxFuture<'static, Result>; - - fn new_service(&self, _: ()) -> Self::Future { - let fut = self.0.new_service(()); - - async move { - fut.await.map_err(|_| ()).map(|service| { - let service: BoxedHttpService<_> = - Box::new(HttpServiceWrapper { service }); - service - }) - } - .boxed_local() - } -} - -struct HttpServiceWrapper { - service: T, -} - -impl Service for HttpServiceWrapper -where - T: Service< - Response = (), - Future = LocalBoxFuture<'static, Result<(), Error>>, - Error = Error, - >, - T::Request: 'static, -{ - type Request = T::Request; - type Response = (); - type Error = Error; - type Future = LocalBoxFuture<'static, Result<(), Error>>; - - fn poll_ready(&mut self, cx: &mut Context) -> Poll> { - self.service.poll_ready(cx) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - self.service.call(req) - } -} diff --git a/actix-framed/src/lib.rs b/actix-framed/src/lib.rs deleted file mode 100644 index 250533f39..000000000 --- a/actix-framed/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -#![allow(clippy::type_complexity, clippy::new_without_default, dead_code)] -mod app; -mod helpers; -mod request; -mod route; -mod service; -mod state; -pub mod test; - -// re-export for convinience -pub use actix_http::{http, Error, HttpMessage, Response, ResponseError}; - -pub use self::app::{FramedApp, FramedAppService}; -pub use self::request::FramedRequest; -pub use self::route::FramedRoute; -pub use self::service::{SendError, VerifyWebSockets}; -pub use self::state::State; diff --git a/actix-framed/src/request.rs b/actix-framed/src/request.rs deleted file mode 100644 index 1872dcc25..000000000 --- a/actix-framed/src/request.rs +++ /dev/null @@ -1,172 +0,0 @@ -use std::cell::{Ref, RefMut}; - -use actix_codec::Framed; -use actix_http::http::{HeaderMap, Method, Uri, Version}; -use actix_http::{h1::Codec, Extensions, Request, RequestHead}; -use actix_router::{Path, Url}; - -use crate::state::State; - -pub struct FramedRequest { - req: Request, - framed: Framed, - state: State, - pub(crate) path: Path, -} - -impl FramedRequest { - pub fn new( - req: Request, - framed: Framed, - path: Path, - state: State, - ) -> Self { - Self { - req, - framed, - state, - path, - } - } -} - -impl FramedRequest { - /// Split request into a parts - pub fn into_parts(self) -> (Request, Framed, State) { - (self.req, self.framed, self.state) - } - - /// This method returns reference to the request head - #[inline] - pub fn head(&self) -> &RequestHead { - self.req.head() - } - - /// This method returns muttable reference to the request head. - /// panics if multiple references of http request exists. - #[inline] - pub fn head_mut(&mut self) -> &mut RequestHead { - self.req.head_mut() - } - - /// Shared application state - #[inline] - pub fn state(&self) -> &S { - self.state.get_ref() - } - - /// Request's uri. - #[inline] - pub fn uri(&self) -> &Uri { - &self.head().uri - } - - /// Read the Request method. - #[inline] - pub fn method(&self) -> &Method { - &self.head().method - } - - /// Read the Request Version. - #[inline] - pub fn version(&self) -> Version { - self.head().version - } - - #[inline] - /// Returns request's headers. - pub fn headers(&self) -> &HeaderMap { - &self.head().headers - } - - /// The target path of this Request. - #[inline] - pub fn path(&self) -> &str { - self.head().uri.path() - } - - /// The query string in the URL. - /// - /// E.g., id=10 - #[inline] - pub fn query_string(&self) -> &str { - if let Some(query) = self.uri().query().as_ref() { - query - } else { - "" - } - } - - /// Get a reference to the Path parameters. - /// - /// Params is a container for url parameters. - /// A variable segment is specified in the form `{identifier}`, - /// where the identifier can be used later in a request handler to - /// access the matched value for that segment. - #[inline] - pub fn match_info(&self) -> &Path { - &self.path - } - - /// Request extensions - #[inline] - pub fn extensions(&self) -> Ref { - self.head().extensions() - } - - /// Mutable reference to a the request's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut { - self.head().extensions_mut() - } -} - -#[cfg(test)] -mod tests { - use std::convert::TryFrom; - - use actix_http::http::{HeaderName, HeaderValue}; - use actix_http::test::{TestBuffer, TestRequest}; - - use super::*; - - #[test] - fn test_reqest() { - let buf = TestBuffer::empty(); - let framed = Framed::new(buf, Codec::default()); - let req = TestRequest::with_uri("/index.html?q=1") - .header("content-type", "test") - .finish(); - let path = Path::new(Url::new(req.uri().clone())); - - let mut freq = FramedRequest::new(req, framed, path, State::new(10u8)); - assert_eq!(*freq.state(), 10); - assert_eq!(freq.version(), Version::HTTP_11); - assert_eq!(freq.method(), Method::GET); - assert_eq!(freq.path(), "/index.html"); - assert_eq!(freq.query_string(), "q=1"); - assert_eq!( - freq.headers() - .get("content-type") - .unwrap() - .to_str() - .unwrap(), - "test" - ); - - freq.head_mut().headers.insert( - HeaderName::try_from("x-hdr").unwrap(), - HeaderValue::from_static("test"), - ); - assert_eq!( - freq.headers().get("x-hdr").unwrap().to_str().unwrap(), - "test" - ); - - freq.extensions_mut().insert(100usize); - assert_eq!(*freq.extensions().get::().unwrap(), 100usize); - - let (_, _, state) = freq.into_parts(); - assert_eq!(*state, 10); - } -} diff --git a/actix-framed/src/route.rs b/actix-framed/src/route.rs deleted file mode 100644 index 793f46273..000000000 --- a/actix-framed/src/route.rs +++ /dev/null @@ -1,159 +0,0 @@ -use std::fmt; -use std::future::Future; -use std::marker::PhantomData; -use std::task::{Context, Poll}; - -use actix_codec::{AsyncRead, AsyncWrite}; -use actix_http::{http::Method, Error}; -use actix_service::{Service, ServiceFactory}; -use futures::future::{ok, FutureExt, LocalBoxFuture, Ready}; -use log::error; - -use crate::app::HttpServiceFactory; -use crate::request::FramedRequest; - -/// Resource route definition -/// -/// Route uses builder-like pattern for configuration. -/// If handler is not explicitly set, default *404 Not Found* handler is used. -pub struct FramedRoute { - handler: F, - pattern: String, - methods: Vec, - state: PhantomData<(Io, S, R, E)>, -} - -impl FramedRoute { - pub fn new(pattern: &str) -> Self { - FramedRoute { - handler: (), - pattern: pattern.to_string(), - methods: Vec::new(), - state: PhantomData, - } - } - - pub fn get(path: &str) -> FramedRoute { - FramedRoute::new(path).method(Method::GET) - } - - pub fn post(path: &str) -> FramedRoute { - FramedRoute::new(path).method(Method::POST) - } - - pub fn put(path: &str) -> FramedRoute { - FramedRoute::new(path).method(Method::PUT) - } - - pub fn delete(path: &str) -> FramedRoute { - FramedRoute::new(path).method(Method::DELETE) - } - - pub fn method(mut self, method: Method) -> Self { - self.methods.push(method); - self - } - - pub fn to(self, handler: F) -> FramedRoute - where - F: FnMut(FramedRequest) -> R, - R: Future> + 'static, - - E: fmt::Debug, - { - FramedRoute { - handler, - pattern: self.pattern, - methods: self.methods, - state: PhantomData, - } - } -} - -impl HttpServiceFactory for FramedRoute -where - Io: AsyncRead + AsyncWrite + 'static, - F: FnMut(FramedRequest) -> R + Clone, - R: Future> + 'static, - E: fmt::Display, -{ - type Factory = FramedRouteFactory; - - fn path(&self) -> &str { - &self.pattern - } - - fn create(self) -> Self::Factory { - FramedRouteFactory { - handler: self.handler, - methods: self.methods, - _t: PhantomData, - } - } -} - -pub struct FramedRouteFactory { - handler: F, - methods: Vec, - _t: PhantomData<(Io, S, R, E)>, -} - -impl ServiceFactory for FramedRouteFactory -where - Io: AsyncRead + AsyncWrite + 'static, - F: FnMut(FramedRequest) -> R + Clone, - R: Future> + 'static, - E: fmt::Display, -{ - type Config = (); - type Request = FramedRequest; - type Response = (); - type Error = Error; - type InitError = (); - type Service = FramedRouteService; - type Future = Ready>; - - fn new_service(&self, _: ()) -> Self::Future { - ok(FramedRouteService { - handler: self.handler.clone(), - methods: self.methods.clone(), - _t: PhantomData, - }) - } -} - -pub struct FramedRouteService { - handler: F, - methods: Vec, - _t: PhantomData<(Io, S, R, E)>, -} - -impl Service for FramedRouteService -where - Io: AsyncRead + AsyncWrite + 'static, - F: FnMut(FramedRequest) -> R + Clone, - R: Future> + 'static, - E: fmt::Display, -{ - type Request = FramedRequest; - type Response = (); - type Error = Error; - type Future = LocalBoxFuture<'static, Result<(), Error>>; - - fn poll_ready(&mut self, _: &mut Context) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: FramedRequest) -> Self::Future { - let fut = (self.handler)(req); - - async move { - let res = fut.await; - if let Err(e) = res { - error!("Error in request handler: {}", e); - } - Ok(()) - } - .boxed_local() - } -} diff --git a/actix-framed/src/service.rs b/actix-framed/src/service.rs deleted file mode 100644 index 92393ca75..000000000 --- a/actix-framed/src/service.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::marker::PhantomData; -use std::pin::Pin; -use std::task::{Context, Poll}; - -use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_http::body::BodySize; -use actix_http::error::ResponseError; -use actix_http::h1::{Codec, Message}; -use actix_http::ws::{verify_handshake, HandshakeError}; -use actix_http::{Request, Response}; -use actix_service::{Service, ServiceFactory}; -use futures::future::{err, ok, Either, Ready}; -use futures::Future; - -/// Service that verifies incoming request if it is valid websocket -/// upgrade request. In case of error returns `HandshakeError` -pub struct VerifyWebSockets { - _t: PhantomData<(T, C)>, -} - -impl Default for VerifyWebSockets { - fn default() -> Self { - VerifyWebSockets { _t: PhantomData } - } -} - -impl ServiceFactory for VerifyWebSockets { - type Config = C; - type Request = (Request, Framed); - type Response = (Request, Framed); - type Error = (HandshakeError, Framed); - type InitError = (); - type Service = VerifyWebSockets; - type Future = Ready>; - - fn new_service(&self, _: C) -> Self::Future { - ok(VerifyWebSockets { _t: PhantomData }) - } -} - -impl Service for VerifyWebSockets { - type Request = (Request, Framed); - type Response = (Request, Framed); - type Error = (HandshakeError, Framed); - type Future = Ready>; - - fn poll_ready(&mut self, _: &mut Context) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, (req, framed): (Request, Framed)) -> Self::Future { - match verify_handshake(req.head()) { - Err(e) => err((e, framed)), - Ok(_) => ok((req, framed)), - } - } -} - -/// Send http/1 error response -pub struct SendError(PhantomData<(T, R, E, C)>); - -impl Default for SendError -where - T: AsyncRead + AsyncWrite, - E: ResponseError, -{ - fn default() -> Self { - SendError(PhantomData) - } -} - -impl ServiceFactory for SendError -where - T: AsyncRead + AsyncWrite + Unpin + 'static, - R: 'static, - E: ResponseError + 'static, -{ - type Config = C; - type Request = Result)>; - type Response = R; - type Error = (E, Framed); - type InitError = (); - type Service = SendError; - type Future = Ready>; - - fn new_service(&self, _: C) -> Self::Future { - ok(SendError(PhantomData)) - } -} - -impl Service for SendError -where - T: AsyncRead + AsyncWrite + Unpin + 'static, - R: 'static, - E: ResponseError + 'static, -{ - type Request = Result)>; - type Response = R; - type Error = (E, Framed); - type Future = Either)>>, SendErrorFut>; - - fn poll_ready(&mut self, _: &mut Context) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: Result)>) -> Self::Future { - match req { - Ok(r) => Either::Left(ok(r)), - Err((e, framed)) => { - let res = e.error_response().drop_body(); - Either::Right(SendErrorFut { - framed: Some(framed), - res: Some((res, BodySize::Empty).into()), - err: Some(e), - _t: PhantomData, - }) - } - } - } -} - -#[pin_project::pin_project] -pub struct SendErrorFut { - res: Option, BodySize)>>, - framed: Option>, - err: Option, - _t: PhantomData, -} - -impl Future for SendErrorFut -where - E: ResponseError, - T: AsyncRead + AsyncWrite + Unpin, -{ - type Output = Result)>; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { - if let Some(res) = self.res.take() { - if self.framed.as_mut().unwrap().write(res).is_err() { - return Poll::Ready(Err(( - self.err.take().unwrap(), - self.framed.take().unwrap(), - ))); - } - } - match self.framed.as_mut().unwrap().flush(cx) { - Poll::Ready(Ok(_)) => { - Poll::Ready(Err((self.err.take().unwrap(), self.framed.take().unwrap()))) - } - Poll::Ready(Err(_)) => { - Poll::Ready(Err((self.err.take().unwrap(), self.framed.take().unwrap()))) - } - Poll::Pending => Poll::Pending, - } - } -} diff --git a/actix-framed/src/state.rs b/actix-framed/src/state.rs deleted file mode 100644 index 600a639ca..000000000 --- a/actix-framed/src/state.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::ops::Deref; -use std::sync::Arc; - -/// Application state -pub struct State(Arc); - -impl State { - pub fn new(state: S) -> State { - State(Arc::new(state)) - } - - pub fn get_ref(&self) -> &S { - self.0.as_ref() - } -} - -impl Deref for State { - type Target = S; - - fn deref(&self) -> &S { - self.0.as_ref() - } -} - -impl Clone for State { - fn clone(&self) -> State { - State(self.0.clone()) - } -} diff --git a/actix-framed/src/test.rs b/actix-framed/src/test.rs deleted file mode 100644 index b8029531e..000000000 --- a/actix-framed/src/test.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! Various helpers for Actix applications to use during testing. -use std::convert::TryFrom; -use std::future::Future; - -use actix_codec::Framed; -use actix_http::h1::Codec; -use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; -use actix_http::http::{Error as HttpError, Method, Uri, Version}; -use actix_http::test::{TestBuffer, TestRequest as HttpTestRequest}; -use actix_router::{Path, Url}; - -use crate::{FramedRequest, State}; - -/// Test `Request` builder. -pub struct TestRequest { - req: HttpTestRequest, - path: Path, - state: State, -} - -impl Default for TestRequest<()> { - fn default() -> TestRequest { - TestRequest { - req: HttpTestRequest::default(), - path: Path::new(Url::new(Uri::default())), - state: State::new(()), - } - } -} - -impl TestRequest<()> { - /// Create TestRequest and set request uri - pub fn with_uri(path: &str) -> Self { - Self::get().uri(path) - } - - /// Create TestRequest and set header - pub fn with_hdr(hdr: H) -> Self { - Self::default().set(hdr) - } - - /// Create TestRequest and set header - pub fn with_header(key: K, value: V) -> Self - where - HeaderName: TryFrom, - >::Error: Into, - V: IntoHeaderValue, - { - Self::default().header(key, value) - } - - /// Create TestRequest and set method to `Method::GET` - pub fn get() -> Self { - Self::default().method(Method::GET) - } - - /// Create TestRequest and set method to `Method::POST` - pub fn post() -> Self { - Self::default().method(Method::POST) - } -} - -impl TestRequest { - /// Create TestRequest and set request uri - pub fn with_state(state: S) -> TestRequest { - let req = TestRequest::get(); - TestRequest { - state: State::new(state), - req: req.req, - path: req.path, - } - } - - /// Set HTTP version of this request - pub fn version(mut self, ver: Version) -> Self { - self.req.version(ver); - self - } - - /// Set HTTP method of this request - pub fn method(mut self, meth: Method) -> Self { - self.req.method(meth); - self - } - - /// Set HTTP Uri of this request - pub fn uri(mut self, path: &str) -> Self { - self.req.uri(path); - self - } - - /// Set a header - pub fn set(mut self, hdr: H) -> Self { - self.req.set(hdr); - self - } - - /// Set a header - pub fn header(mut self, key: K, value: V) -> Self - where - HeaderName: TryFrom, - >::Error: Into, - V: IntoHeaderValue, - { - self.req.header(key, value); - self - } - - /// Set request path pattern parameter - pub fn param(mut self, name: &'static str, value: &'static str) -> Self { - self.path.add_static(name, value); - self - } - - /// Complete request creation and generate `Request` instance - pub fn finish(mut self) -> FramedRequest { - let req = self.req.finish(); - self.path.get_mut().update(req.uri()); - let framed = Framed::new(TestBuffer::empty(), Codec::default()); - FramedRequest::new(req, framed, self.path, self.state) - } - - /// This method generates `FramedRequest` instance and executes async handler - pub async fn run(self, f: F) -> Result - where - F: FnOnce(FramedRequest) -> R, - R: Future>, - { - f(self.finish()).await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test() { - let req = TestRequest::with_uri("/index.html") - .header("x-test", "test") - .param("test", "123") - .finish(); - - assert_eq!(*req.state(), ()); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(req.method(), Method::GET); - assert_eq!(req.path(), "/index.html"); - assert_eq!(req.query_string(), ""); - assert_eq!( - req.headers().get("x-test").unwrap().to_str().unwrap(), - "test" - ); - assert_eq!(&req.match_info()["test"], "123"); - } -} diff --git a/actix-framed/tests/test_server.rs b/actix-framed/tests/test_server.rs deleted file mode 100644 index 7d6fc08a6..000000000 --- a/actix-framed/tests/test_server.rs +++ /dev/null @@ -1,159 +0,0 @@ -use actix_codec::{AsyncRead, AsyncWrite}; -use actix_http::{body, http::StatusCode, ws, Error, HttpService, Response}; -use actix_http_test::test_server; -use actix_service::{pipeline_factory, IntoServiceFactory, ServiceFactory}; -use actix_utils::framed::Dispatcher; -use bytes::Bytes; -use futures::{future, SinkExt, StreamExt}; - -use actix_framed::{FramedApp, FramedRequest, FramedRoute, SendError, VerifyWebSockets}; - -async fn ws_service( - req: FramedRequest, -) -> Result<(), Error> { - let (req, mut framed, _) = req.into_parts(); - let res = ws::handshake(req.head()).unwrap().message_body(()); - - framed - .send((res, body::BodySize::None).into()) - .await - .unwrap(); - Dispatcher::new(framed.into_framed(ws::Codec::new()), service) - .await - .unwrap(); - - Ok(()) -} - -async fn service(msg: ws::Frame) -> Result { - let msg = match msg { - ws::Frame::Ping(msg) => ws::Message::Pong(msg), - ws::Frame::Text(text) => { - ws::Message::Text(String::from_utf8_lossy(&text).to_string()) - } - ws::Frame::Binary(bin) => ws::Message::Binary(bin), - ws::Frame::Close(reason) => ws::Message::Close(reason), - _ => panic!(), - }; - Ok(msg) -} - -#[actix_rt::test] -async fn test_simple() { - let mut srv = test_server(|| { - HttpService::build() - .upgrade( - FramedApp::new().service(FramedRoute::get("/index.html").to(ws_service)), - ) - .finish(|_| future::ok::<_, Error>(Response::NotFound())) - .tcp() - }); - - assert!(srv.ws_at("/test").await.is_err()); - - // client service - let mut framed = srv.ws_at("/index.html").await.unwrap(); - framed - .send(ws::Message::Text("text".to_string())) - .await - .unwrap(); - let (item, mut framed) = framed.into_future().await; - assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Text(Bytes::from_static(b"text")) - ); - - framed - .send(ws::Message::Binary("text".into())) - .await - .unwrap(); - let (item, mut framed) = framed.into_future().await; - assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Binary(Bytes::from_static(b"text")) - ); - - framed.send(ws::Message::Ping("text".into())).await.unwrap(); - let (item, mut framed) = framed.into_future().await; - assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Pong("text".to_string().into()) - ); - - framed - .send(ws::Message::Close(Some(ws::CloseCode::Normal.into()))) - .await - .unwrap(); - - let (item, _) = framed.into_future().await; - assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Close(Some(ws::CloseCode::Normal.into())) - ); -} - -#[actix_rt::test] -async fn test_service() { - let mut srv = test_server(|| { - pipeline_factory(actix_http::h1::OneRequest::new().map_err(|_| ())).and_then( - pipeline_factory( - pipeline_factory(VerifyWebSockets::default()) - .then(SendError::default()) - .map_err(|_| ()), - ) - .and_then( - FramedApp::new() - .service(FramedRoute::get("/index.html").to(ws_service)) - .into_factory() - .map_err(|_| ()), - ), - ) - }); - - // non ws request - let res = srv.get("/index.html").send().await.unwrap(); - assert_eq!(res.status(), StatusCode::BAD_REQUEST); - - // not found - assert!(srv.ws_at("/test").await.is_err()); - - // client service - let mut framed = srv.ws_at("/index.html").await.unwrap(); - framed - .send(ws::Message::Text("text".to_string())) - .await - .unwrap(); - let (item, mut framed) = framed.into_future().await; - assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Text(Bytes::from_static(b"text")) - ); - - framed - .send(ws::Message::Binary("text".into())) - .await - .unwrap(); - let (item, mut framed) = framed.into_future().await; - assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Binary(Bytes::from_static(b"text")) - ); - - framed.send(ws::Message::Ping("text".into())).await.unwrap(); - let (item, mut framed) = framed.into_future().await; - assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Pong("text".to_string().into()) - ); - - framed - .send(ws::Message::Close(Some(ws::CloseCode::Normal.into()))) - .await - .unwrap(); - - let (item, _) = framed.into_future().await; - assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Close(Some(ws::CloseCode::Normal.into())) - ); -} diff --git a/actix-http/.appveyor.yml b/actix-http/.appveyor.yml deleted file mode 100644 index 780fdd6b5..000000000 --- a/actix-http/.appveyor.yml +++ /dev/null @@ -1,41 +0,0 @@ -environment: - global: - PROJECT_NAME: actix-http - matrix: - # Stable channel - - TARGET: i686-pc-windows-msvc - CHANNEL: stable - - TARGET: x86_64-pc-windows-gnu - CHANNEL: stable - - TARGET: x86_64-pc-windows-msvc - CHANNEL: stable - # Nightly channel - - TARGET: i686-pc-windows-msvc - CHANNEL: nightly - - TARGET: x86_64-pc-windows-gnu - CHANNEL: nightly - - TARGET: x86_64-pc-windows-msvc - CHANNEL: nightly - -# Install Rust and Cargo -# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml) -install: - - ps: >- - If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') { - $Env:PATH += ';C:\msys64\mingw64\bin' - } ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') { - $Env:PATH += ';C:\MinGW\bin' - } - - curl -sSf -o rustup-init.exe https://win.rustup.rs - - rustup-init.exe --default-host %TARGET% --default-toolchain %CHANNEL% -y - - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin - - rustc -Vv - - cargo -V - -# 'cargo test' takes care of building for us, so disable Appveyor's build stage. -build: false - -# Equivalent to Travis' `script` phase -test_script: - - cargo clean - - cargo test diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 511ef4f1c..c19e40e4c 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,10 +1,63 @@ # Changes -# [Unreleased] +## [2.0.0-alpha.4] - 2020-05-21 ### Changed -* Update the `time` dependency to 0.2.5 +* Bump minimum supported Rust version to 1.40 +* content_length function is removed, and you can set Content-Length by calling no_chunking function [#1439] +* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a + `u64` instead of a `usize`. +* Update `base64` dependency to 0.12 + +### Fixed + +* Support parsing of `SameSite=None` [#1503] + +[#1439]: https://github.com/actix/actix-web/pull/1439 +[#1503]: https://github.com/actix/actix-web/pull/1503 + +## [2.0.0-alpha.3] - 2020-05-08 + +### Fixed + +* Correct spelling of ConnectError::Unresolved [#1487] +* Fix a mistake in the encoding of websocket continuation messages wherein + Item::FirstText and Item::FirstBinary are each encoded as the other. + +### Changed + +* Implement `std::error::Error` for our custom errors [#1422] +* Remove `failure` support for `ResponseError` since that crate + will be deprecated in the near future. + +[#1422]: https://github.com/actix/actix-web/pull/1422 +[#1487]: https://github.com/actix/actix-web/pull/1487 + +## [2.0.0-alpha.2] - 2020-03-07 + +### Changed + +* Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395] + +* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB respectively + to improve download speed for awc when downloading large objects. [#1394] + +* client::Connector accepts initial_window_size and initial_connection_window_size HTTP2 configuration. [#1394] + +* client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394] + +[#1394]: https://github.com/actix/actix-web/pull/1394 +[#1395]: https://github.com/actix/actix-web/pull/1395 + +## [2.0.0-alpha.1] - 2020-02-27 + +### Changed + +* Update the `time` dependency to 0.2.7. +* Moved actors messages support from actix crate, enabled with feature `actors`. +* Breaking change: trait MessageBody requires Unpin and accepting Pin<&mut Self> instead of &mut self in the poll_next(). +* MessageBody is not implemented for &'static [u8] anymore. ### Fixed diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index cd813e49f..d2ae7698e 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "1.0.1" +version = "2.0.0-alpha.4" authors = ["Nikolay Kim "] description = "Actix http primitives" readme = "README.md" @@ -15,7 +15,7 @@ license = "MIT/Apache-2.0" edition = "2018" [package.metadata.docs.rs] -features = ["openssl", "rustls", "failure", "compress", "secure-cookies"] +features = ["openssl", "rustls", "compress", "secure-cookies", "actors"] [lib] name = "actix_http" @@ -33,42 +33,44 @@ rustls = ["actix-tls/rustls", "actix-connect/rustls"] # enable compressison support compress = ["flate2", "brotli2"] -# failure integration. actix does not use failure anymore -failure = ["fail-ure"] - # support for secure cookies secure-cookies = ["ring"] +# support for actix Actor messages +actors = ["actix"] + [dependencies] -actix-service = "1.0.1" +actix-service = "1.0.5" actix-codec = "0.2.0" -actix-connect = "1.0.1" -actix-utils = "1.0.3" +actix-connect = "2.0.0-alpha.3" +actix-utils = "1.0.6" actix-rt = "1.0.0" actix-threadpool = "0.3.1" -actix-tls = { version = "1.0.0", optional = true } +actix-tls = { version = "2.0.0-alpha.1", optional = true } +actix = { version = "0.10.0-alpha.1", optional = true } -base64 = "0.11" +base64 = "0.12" bitflags = "1.2" bytes = "0.5.3" copyless = "0.1.4" derive_more = "0.99.2" either = "1.5.3" encoding_rs = "0.8" -futures-core = "0.3.1" -futures-util = "0.3.1" -futures-channel = "0.3.1" +futures-channel = { version = "0.3.5", default-features = false } +futures-core = { version = "0.3.5", default-features = false } +futures-util = { version = "0.3.5", default-features = false } fxhash = "0.2.1" h2 = "0.2.1" http = "0.2.0" httparse = "1.3" indexmap = "1.3" +itoa = "0.4" lazy_static = "1.4" language-tags = "0.2" log = "0.4" mime = "0.3" percent-encoding = "2.1" -pin-project = "0.4.6" +pin-project = "0.4.17" rand = "0.7" regex = "1.3" serde = "1.0" @@ -76,7 +78,7 @@ serde_json = "1.0" sha-1 = "0.8" slab = "0.4" serde_urlencoded = "0.6.1" -time = { version = "0.2.5", default-features = false, features = ["std"] } +time = { version = "0.2.7", default-features = false, features = ["std"] } # for secure cookie ring = { version = "0.16.9", optional = true } @@ -85,16 +87,21 @@ ring = { version = "0.16.9", optional = true } brotli2 = { version="0.3.2", optional = true } flate2 = { version = "1.0.13", optional = true } -# optional deps -fail-ure = { version = "0.1.5", package="failure", optional = true } - [dev-dependencies] -actix-server = "1.0.0" -actix-connect = { version = "1.0.0", features=["openssl"] } -actix-http-test = { version = "1.0.0", features=["openssl"] } -actix-tls = { version = "1.0.0", features=["openssl"] } -futures = "0.3.1" -env_logger = "0.6" +actix-server = "1.0.1" +actix-connect = { version = "2.0.0-alpha.2", features = ["openssl"] } +actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } +actix-tls = { version = "2.0.0-alpha.1", features = ["openssl"] } +criterion = "0.3" +env_logger = "0.7" serde_derive = "1.0" open-ssl = { version="0.10", package = "openssl" } -rust-tls = { version="0.16", package = "rustls" } +rust-tls = { version="0.17", package = "rustls" } + +[[bench]] +name = "content-length" +harness = false + +[[bench]] +name = "status-line" +harness = false diff --git a/actix-http/README.md b/actix-http/README.md index d75e822ba..d4c96f2a7 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -8,25 +8,40 @@ Actix http * [API Documentation](https://docs.rs/actix-http/) * [Chat on gitter](https://gitter.im/actix/actix) * Cargo package: [actix-http](https://crates.io/crates/actix-http) -* Minimum supported Rust version: 1.31 or later +* Minimum supported Rust version: 1.40 or later ## Example ```rust // see examples/framed_hello.rs for complete list of used crates. -extern crate actix_http; -use actix_http::{h1, Response, ServiceConfig}; +use std::{env, io}; -fn main() { - Server::new().bind("framed_hello", "127.0.0.1:8080", || { - IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) // <- create h1 codec - .and_then(TakeItem::new().map_err(|_| ())) // <- read one request - .and_then(|(_req, _framed): (_, Framed<_, _>)| { // <- send response and close conn - SendResponse::send(_framed, Response::Ok().body("Hello world!")) - .map_err(|_| ()) - .map(|_| ()) - }) - }).unwrap().run(); +use actix_http::{HttpService, Response}; +use actix_server::Server; +use futures::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(); + + Server::build() + .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(); + res.header("x-head", HeaderValue::from_static("dummy value!")); + future::ok::<_, ()>(res.body("Hello world!")) + }) + .tcp() + })? + .run() + .await } ``` diff --git a/actix-http/benches/content-length.rs b/actix-http/benches/content-length.rs new file mode 100644 index 000000000..18a55a33b --- /dev/null +++ b/actix-http/benches/content-length.rs @@ -0,0 +1,291 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; + +use bytes::BytesMut; + +// benchmark sending all requests at the same time +fn bench_write_content_length(c: &mut Criterion) { + let mut group = c.benchmark_group("write_content_length"); + + let sizes = [ + 0, 1, 11, 83, 101, 653, 1001, 6323, 10001, 56329, 100001, 123456, 98724245, + 4294967202, + ]; + + for i in sizes.iter() { + group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| { + b.iter(|| { + let mut b = BytesMut::with_capacity(35); + _original::write_content_length(i, &mut b) + }) + }); + + group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| { + b.iter(|| { + let mut b = BytesMut::with_capacity(35); + _new::write_content_length(i, &mut b) + }) + }); + + group.bench_with_input(BenchmarkId::new("itoa", i), i, |b, &i| { + b.iter(|| { + let mut b = BytesMut::with_capacity(35); + _itoa::write_content_length(i, &mut b) + }) + }); + } + + group.finish(); +} + +criterion_group!(benches, bench_write_content_length); +criterion_main!(benches); + +mod _itoa { + use bytes::{BufMut, BytesMut}; + + pub fn write_content_length(n: usize, bytes: &mut BytesMut) { + if n == 0 { + bytes.put_slice(b"\r\ncontent-length: 0\r\n"); + return; + } + + let mut buf = itoa::Buffer::new(); + + bytes.put_slice(b"\r\ncontent-length: "); + bytes.put_slice(buf.format(n).as_bytes()); + bytes.put_slice(b"\r\n"); + } +} + +mod _new { + use bytes::{BufMut, BytesMut}; + + const DIGITS_START: u8 = b'0'; + + /// NOTE: bytes object has to contain enough space + pub fn write_content_length(n: usize, bytes: &mut BytesMut) { + if n == 0 { + bytes.put_slice(b"\r\ncontent-length: 0\r\n"); + return; + } + + bytes.put_slice(b"\r\ncontent-length: "); + + if n < 10 { + bytes.put_u8(DIGITS_START + (n as u8)); + } else if n < 100 { + let n = n as u8; + + let d10 = n / 10; + let d1 = n % 10; + + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); + } else if n < 1000 { + let n = n as u16; + + let d100 = (n / 100) as u8; + let d10 = ((n / 10) % 10) as u8; + let d1 = (n % 10) as u8; + + bytes.put_u8(DIGITS_START + d100); + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); + } else if n < 10_000 { + let n = n as u16; + + let d1000 = (n / 1000) as u8; + let d100 = ((n / 100) % 10) as u8; + let d10 = ((n / 10) % 10) as u8; + let d1 = (n % 10) as u8; + + bytes.put_u8(DIGITS_START + d1000); + bytes.put_u8(DIGITS_START + d100); + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); + } else if n < 100_000 { + let n = n as u32; + + let d10000 = (n / 10000) as u8; + let d1000 = ((n / 1000) % 10) as u8; + let d100 = ((n / 100) % 10) as u8; + let d10 = ((n / 10) % 10) as u8; + let d1 = (n % 10) as u8; + + bytes.put_u8(DIGITS_START + d10000); + bytes.put_u8(DIGITS_START + d1000); + bytes.put_u8(DIGITS_START + d100); + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); + } else if n < 1_000_000 { + let n = n as u32; + + let d100000 = (n / 100000) as u8; + let d10000 = ((n / 10000) % 10) as u8; + let d1000 = ((n / 1000) % 10) as u8; + let d100 = ((n / 100) % 10) as u8; + let d10 = ((n / 10) % 10) as u8; + let d1 = (n % 10) as u8; + + bytes.put_u8(DIGITS_START + d100000); + bytes.put_u8(DIGITS_START + d10000); + bytes.put_u8(DIGITS_START + d1000); + bytes.put_u8(DIGITS_START + d100); + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); + } else { + write_usize(n, bytes); + } + + bytes.put_slice(b"\r\n"); + } + + fn write_usize(n: usize, bytes: &mut BytesMut) { + let mut n = n; + + // 20 chars is max length of a usize (2^64) + // digits will be added to the buffer from lsd to msd + let mut buf = BytesMut::with_capacity(20); + + while n > 9 { + // "pop" the least-significant digit + let lsd = (n % 10) as u8; + + // remove the lsd from n + n = n / 10; + + buf.put_u8(DIGITS_START + lsd); + } + + // put msd to result buffer + bytes.put_u8(DIGITS_START + (n as u8)); + + // put, in reverse (msd to lsd), remaining digits to buffer + for i in (0..buf.len()).rev() { + bytes.put_u8(buf[i]); + } + } +} + +mod _original { + use std::{mem, ptr, slice}; + + use bytes::{BufMut, BytesMut}; + + const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\ + 2021222324252627282930313233343536373839\ + 4041424344454647484950515253545556575859\ + 6061626364656667686970717273747576777879\ + 8081828384858687888990919293949596979899"; + + /// NOTE: bytes object has to contain enough space + pub fn write_content_length(mut n: usize, bytes: &mut BytesMut) { + if n < 10 { + let mut buf: [u8; 21] = [ + b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', + b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'\r', b'\n', + ]; + buf[18] = (n as u8) + b'0'; + bytes.put_slice(&buf); + } else if n < 100 { + let mut buf: [u8; 22] = [ + b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', + b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'\r', b'\n', + ]; + let d1 = n << 1; + unsafe { + ptr::copy_nonoverlapping( + DEC_DIGITS_LUT.as_ptr().add(d1), + buf.as_mut_ptr().offset(18), + 2, + ); + } + bytes.put_slice(&buf); + } else if n < 1000 { + let mut buf: [u8; 23] = [ + b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', + b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'0', b'\r', + b'\n', + ]; + // decode 2 more chars, if > 2 chars + let d1 = (n % 100) << 1; + n /= 100; + unsafe { + ptr::copy_nonoverlapping( + DEC_DIGITS_LUT.as_ptr().add(d1), + buf.as_mut_ptr().offset(19), + 2, + ) + }; + + // decode last 1 + buf[18] = (n as u8) + b'0'; + + bytes.put_slice(&buf); + } else { + bytes.put_slice(b"\r\ncontent-length: "); + convert_usize(n, bytes); + } + } + + pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) { + let mut curr: isize = 39; + let mut buf: [u8; 41] = unsafe { mem::MaybeUninit::uninit().assume_init() }; + buf[39] = b'\r'; + buf[40] = b'\n'; + let buf_ptr = buf.as_mut_ptr(); + let lut_ptr = DEC_DIGITS_LUT.as_ptr(); + + // eagerly decode 4 characters at a time + while n >= 10_000 { + let rem = (n % 10_000) as isize; + n /= 10_000; + + let d1 = (rem / 100) << 1; + let d2 = (rem % 100) << 1; + curr -= 4; + unsafe { + ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); + ptr::copy_nonoverlapping( + lut_ptr.offset(d2), + buf_ptr.offset(curr + 2), + 2, + ); + } + } + + // if we reach here numbers are <= 9999, so at most 4 chars long + let mut n = n as isize; // possibly reduce 64bit math + + // decode 2 more chars, if > 2 chars + if n >= 100 { + let d1 = (n % 100) << 1; + n /= 100; + curr -= 2; + unsafe { + ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); + } + } + + // decode last 1 or 2 chars + if n < 10 { + curr -= 1; + unsafe { + *buf_ptr.offset(curr) = (n as u8) + b'0'; + } + } else { + let d1 = n << 1; + curr -= 2; + unsafe { + ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); + } + } + + unsafe { + bytes.extend_from_slice(slice::from_raw_parts( + buf_ptr.offset(curr), + 41 - curr as usize, + )); + } + } +} diff --git a/actix-http/benches/status-line.rs b/actix-http/benches/status-line.rs new file mode 100644 index 000000000..51f840f89 --- /dev/null +++ b/actix-http/benches/status-line.rs @@ -0,0 +1,222 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; + +use bytes::BytesMut; +use http::Version; + +const CODES: &[u16] = &[201, 303, 404, 515]; + +fn bench_write_status_line_11(c: &mut Criterion) { + let mut group = c.benchmark_group("write_status_line v1.1"); + + let version = Version::HTTP_11; + + for i in CODES.iter() { + group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| { + b.iter(|| { + let mut b = BytesMut::with_capacity(35); + _original::write_status_line(version, i, &mut b); + }) + }); + + group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| { + b.iter(|| { + let mut b = BytesMut::with_capacity(35); + _new::write_status_line(version, i, &mut b); + }) + }); + + group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| { + b.iter(|| { + let mut b = BytesMut::with_capacity(35); + _naive::write_status_line(version, i, &mut b); + }) + }); + } + + group.finish(); +} + +fn bench_write_status_line_10(c: &mut Criterion) { + let mut group = c.benchmark_group("write_status_line v1.0"); + + let version = Version::HTTP_10; + + for i in CODES.iter() { + group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| { + b.iter(|| { + let mut b = BytesMut::with_capacity(35); + _original::write_status_line(version, i, &mut b); + }) + }); + + group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| { + b.iter(|| { + let mut b = BytesMut::with_capacity(35); + _new::write_status_line(version, i, &mut b); + }) + }); + + group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| { + b.iter(|| { + let mut b = BytesMut::with_capacity(35); + _naive::write_status_line(version, i, &mut b); + }) + }); + } + + group.finish(); +} + +fn bench_write_status_line_09(c: &mut Criterion) { + let mut group = c.benchmark_group("write_status_line v0.9"); + + let version = Version::HTTP_09; + + for i in CODES.iter() { + group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| { + b.iter(|| { + let mut b = BytesMut::with_capacity(35); + _original::write_status_line(version, i, &mut b); + }) + }); + + group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| { + b.iter(|| { + let mut b = BytesMut::with_capacity(35); + _new::write_status_line(version, i, &mut b); + }) + }); + + group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| { + b.iter(|| { + let mut b = BytesMut::with_capacity(35); + _naive::write_status_line(version, i, &mut b); + }) + }); + } + + group.finish(); +} + +criterion_group!( + benches, + bench_write_status_line_11, + bench_write_status_line_10, + bench_write_status_line_09 +); +criterion_main!(benches); + +mod _naive { + use bytes::{BufMut, BytesMut}; + use http::Version; + + pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) { + match version { + Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "), + Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "), + Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "), + _ => { + // other HTTP version handlers do not use this method + } + } + + bytes.put_slice(n.to_string().as_bytes()); + } +} + +mod _new { + use bytes::{BufMut, BytesMut}; + use http::Version; + + const DIGITS_START: u8 = b'0'; + + pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) { + match version { + Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "), + Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "), + Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "), + _ => { + // other HTTP version handlers do not use this method + } + } + + let d100 = (n / 100) as u8; + let d10 = ((n / 10) % 10) as u8; + let d1 = (n % 10) as u8; + + bytes.put_u8(DIGITS_START + d100); + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); + + bytes.put_u8(b' '); + } +} + +mod _original { + use std::ptr; + + use bytes::{BufMut, BytesMut}; + use http::Version; + + const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\ + 2021222324252627282930313233343536373839\ + 4041424344454647484950515253545556575859\ + 6061626364656667686970717273747576777879\ + 8081828384858687888990919293949596979899"; + + pub(crate) const STATUS_LINE_BUF_SIZE: usize = 13; + + pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesMut) { + let mut buf: [u8; STATUS_LINE_BUF_SIZE] = *b"HTTP/1.1 "; + + match version { + Version::HTTP_2 => buf[5] = b'2', + Version::HTTP_10 => buf[7] = b'0', + Version::HTTP_09 => { + buf[5] = b'0'; + buf[7] = b'9'; + } + _ => (), + } + + let mut curr: isize = 12; + let buf_ptr = buf.as_mut_ptr(); + let lut_ptr = DEC_DIGITS_LUT.as_ptr(); + let four = n > 999; + + // decode 2 more chars, if > 2 chars + let d1 = (n % 100) << 1; + n /= 100; + curr -= 2; + unsafe { + ptr::copy_nonoverlapping( + lut_ptr.offset(d1 as isize), + buf_ptr.offset(curr), + 2, + ); + } + + // decode last 1 or 2 chars + if n < 10 { + curr -= 1; + unsafe { + *buf_ptr.offset(curr) = (n as u8) + b'0'; + } + } else { + let d1 = n << 1; + curr -= 2; + unsafe { + ptr::copy_nonoverlapping( + lut_ptr.offset(d1 as isize), + buf_ptr.offset(curr), + 2, + ); + } + } + + bytes.put_slice(&buf); + if four { + bytes.put_u8(b' '); + } + } +} diff --git a/actix-http/examples/echo.rs b/actix-http/examples/echo.rs index 3d57a472a..beb1cce2c 100644 --- a/actix-http/examples/echo.rs +++ b/actix-http/examples/echo.rs @@ -3,7 +3,7 @@ use std::{env, io}; use actix_http::{Error, HttpService, Request, Response}; use actix_server::Server; use bytes::BytesMut; -use futures::StreamExt; +use futures_util::StreamExt; use http::header::HeaderValue; use log::info; @@ -17,23 +17,18 @@ async fn main() -> io::Result<()> { HttpService::build() .client_timeout(1000) .client_disconnect(1000) - .finish(|mut req: Request| { - async move { - let mut body = BytesMut::new(); - while let Some(item) = req.payload().next().await { - body.extend_from_slice(&item?); - } - - info!("request body: {:?}", body); - Ok::<_, Error>( - Response::Ok() - .header( - "x-head", - HeaderValue::from_static("dummy value!"), - ) - .body(body), - ) + .finish(|mut req: Request| async move { + let mut body = BytesMut::new(); + while let Some(item) = req.payload().next().await { + body.extend_from_slice(&item?); } + + info!("request body: {:?}", body); + Ok::<_, Error>( + Response::Ok() + .header("x-head", HeaderValue::from_static("dummy value!")) + .body(body), + ) }) .tcp() })? diff --git a/actix-http/examples/echo2.rs b/actix-http/examples/echo2.rs index f89ea2dfb..5b7e504d3 100644 --- a/actix-http/examples/echo2.rs +++ b/actix-http/examples/echo2.rs @@ -4,7 +4,7 @@ use actix_http::http::HeaderValue; use actix_http::{Error, HttpService, Request, Response}; use actix_server::Server; use bytes::BytesMut; -use futures::StreamExt; +use futures_util::StreamExt; use log::info; async fn handle_request(mut req: Request) -> Result { diff --git a/actix-http/examples/hello-world.rs b/actix-http/examples/hello-world.rs index 4134ee734..d6477b152 100644 --- a/actix-http/examples/hello-world.rs +++ b/actix-http/examples/hello-world.rs @@ -2,7 +2,7 @@ use std::{env, io}; use actix_http::{HttpService, Response}; use actix_server::Server; -use futures::future; +use futures_util::future; use http::header::HeaderValue; use log::info; diff --git a/actix-http/src/body.rs b/actix-http/src/body.rs index e2bcce359..0b01aa8ce 100644 --- a/actix-http/src/body.rs +++ b/actix-http/src/body.rs @@ -6,7 +6,7 @@ use std::{fmt, mem}; use bytes::{Bytes, BytesMut}; use futures_core::Stream; use futures_util::ready; -use pin_project::{pin_project, project}; +use pin_project::pin_project; use crate::error::Error; @@ -15,8 +15,7 @@ use crate::error::Error; pub enum BodySize { None, Empty, - Sized(usize), - Sized64(u64), + Sized(u64), Stream, } @@ -25,8 +24,7 @@ impl BodySize { match self { BodySize::None | BodySize::Empty - | BodySize::Sized(0) - | BodySize::Sized64(0) => true, + | BodySize::Sized(0) => true, _ => false, } } @@ -36,33 +34,46 @@ impl BodySize { pub trait MessageBody { fn size(&self) -> BodySize; - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>>; + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>>; + + downcast_get_type_id!(); } +downcast!(MessageBody); + impl MessageBody for () { fn size(&self) -> BodySize { BodySize::Empty } - fn poll_next(&mut self, _: &mut Context<'_>) -> Poll>> { + fn poll_next( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll>> { Poll::Ready(None) } } -impl MessageBody for Box { +impl MessageBody for Box { fn size(&self) -> BodySize { self.as_ref().size() } - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { - self.as_mut().poll_next(cx) + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + Pin::new(self.get_mut().as_mut()).poll_next(cx) } } -#[pin_project] +#[pin_project(project = ResponseBodyProj)] pub enum ResponseBody { - Body(B), - Other(Body), + Body(#[pin] B), + Other(#[pin] Body), } impl ResponseBody { @@ -98,10 +109,13 @@ impl MessageBody for ResponseBody { } } - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { - match self { - ResponseBody::Body(ref mut body) => body.poll_next(cx), - ResponseBody::Other(ref mut body) => body.poll_next(cx), + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + match self.project() { + ResponseBodyProj::Body(body) => body.poll_next(cx), + ResponseBodyProj::Other(body) => body.poll_next(cx), } } } @@ -109,19 +123,18 @@ impl MessageBody for ResponseBody { impl Stream for ResponseBody { type Item = Result; - #[project] fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { - #[project] match self.project() { - ResponseBody::Body(ref mut body) => body.poll_next(cx), - ResponseBody::Other(ref mut body) => body.poll_next(cx), + ResponseBodyProj::Body(body) => body.poll_next(cx), + ResponseBodyProj::Other(body) => body.poll_next(cx), } } } +#[pin_project(project = BodyProj)] /// Represents various types of http message body. pub enum Body { /// Empty response. `Content-Length` header is not set. @@ -131,7 +144,7 @@ pub enum Body { /// Specific response body. Bytes(Bytes), /// Generic message body. - Message(Box), + Message(Box), } impl Body { @@ -141,7 +154,7 @@ impl Body { } /// Create body from generic message body. - pub fn from_message(body: B) -> Body { + pub fn from_message(body: B) -> Body { Body::Message(Box::new(body)) } } @@ -151,24 +164,27 @@ impl MessageBody for Body { match self { Body::None => BodySize::None, Body::Empty => BodySize::Empty, - Body::Bytes(ref bin) => BodySize::Sized(bin.len()), + Body::Bytes(ref bin) => BodySize::Sized(bin.len() as u64), Body::Message(ref body) => body.size(), } } - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { - match self { - Body::None => Poll::Ready(None), - Body::Empty => Poll::Ready(None), - Body::Bytes(ref mut bin) => { + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + match self.project() { + BodyProj::None => Poll::Ready(None), + BodyProj::Empty => Poll::Ready(None), + BodyProj::Bytes(ref mut bin) => { let len = bin.len(); if len == 0 { Poll::Ready(None) } else { - Poll::Ready(Some(Ok(mem::replace(bin, Bytes::new())))) + Poll::Ready(Some(Ok(mem::take(bin)))) } } - Body::Message(ref mut body) => body.poll_next(cx), + BodyProj::Message(ref mut body) => Pin::new(body.as_mut()).poll_next(cx), } } } @@ -254,7 +270,7 @@ impl From for Body { impl From> for Body where - S: Stream> + 'static, + S: Stream> + Unpin + 'static, { fn from(s: SizedStream) -> Body { Body::from_message(s) @@ -263,7 +279,7 @@ where impl From> for Body where - S: Stream> + 'static, + S: Stream> + Unpin + 'static, E: Into + 'static, { fn from(s: BodyStream) -> Body { @@ -273,87 +289,88 @@ where impl MessageBody for Bytes { fn size(&self) -> BodySize { - BodySize::Sized(self.len()) + BodySize::Sized(self.len() as u64) } - fn poll_next(&mut self, _: &mut Context<'_>) -> Poll>> { + fn poll_next( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll>> { if self.is_empty() { Poll::Ready(None) } else { - Poll::Ready(Some(Ok(mem::replace(self, Bytes::new())))) + Poll::Ready(Some(Ok(mem::take(self.get_mut())))) } } } impl MessageBody for BytesMut { fn size(&self) -> BodySize { - BodySize::Sized(self.len()) + BodySize::Sized(self.len() as u64) } - fn poll_next(&mut self, _: &mut Context<'_>) -> Poll>> { + fn poll_next( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll>> { if self.is_empty() { Poll::Ready(None) } else { - Poll::Ready(Some(Ok(mem::replace(self, BytesMut::new()).freeze()))) + Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze()))) } } } impl MessageBody for &'static str { fn size(&self) -> BodySize { - BodySize::Sized(self.len()) + BodySize::Sized(self.len() as u64) } - fn poll_next(&mut self, _: &mut Context<'_>) -> Poll>> { + fn poll_next( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll>> { if self.is_empty() { Poll::Ready(None) } else { Poll::Ready(Some(Ok(Bytes::from_static( - mem::replace(self, "").as_ref(), + mem::take(self.get_mut()).as_ref(), )))) } } } -impl MessageBody for &'static [u8] { - fn size(&self) -> BodySize { - BodySize::Sized(self.len()) - } - - fn poll_next(&mut self, _: &mut Context<'_>) -> Poll>> { - if self.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(Bytes::from_static(mem::replace(self, b""))))) - } - } -} - impl MessageBody for Vec { fn size(&self) -> BodySize { - BodySize::Sized(self.len()) + BodySize::Sized(self.len() as u64) } - fn poll_next(&mut self, _: &mut Context<'_>) -> Poll>> { + fn poll_next( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll>> { if self.is_empty() { Poll::Ready(None) } else { - Poll::Ready(Some(Ok(Bytes::from(mem::replace(self, Vec::new()))))) + Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut()))))) } } } impl MessageBody for String { fn size(&self) -> BodySize { - BodySize::Sized(self.len()) + BodySize::Sized(self.len() as u64) } - fn poll_next(&mut self, _: &mut Context<'_>) -> Poll>> { + fn poll_next( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll>> { if self.is_empty() { Poll::Ready(None) } else { Poll::Ready(Some(Ok(Bytes::from( - mem::replace(self, String::new()).into_bytes(), + mem::take(self.get_mut()).into_bytes(), )))) } } @@ -361,19 +378,21 @@ impl MessageBody for String { /// Type represent streaming body. /// Response does not contain `content-length` header and appropriate transfer encoding is used. -pub struct BodyStream { - stream: Pin>, +#[pin_project] +pub struct BodyStream { + #[pin] + stream: S, _t: PhantomData, } impl BodyStream where - S: Stream>, + S: Stream> + Unpin, E: Into, { pub fn new(stream: S) -> Self { BodyStream { - stream: Box::pin(stream), + stream, _t: PhantomData, } } @@ -381,7 +400,7 @@ where impl MessageBody for BodyStream where - S: Stream>, + S: Stream> + Unpin, E: Into, { fn size(&self) -> BodySize { @@ -393,10 +412,14 @@ where /// Empty values are skipped to prevent [`BodyStream`]'s transmission being /// ended on a zero-length chunk, but rather proceed until the underlying /// [`Stream`] ends. - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { - let mut stream = self.stream.as_mut(); + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let mut stream = self.project().stream; loop { - return Poll::Ready(match ready!(stream.as_mut().poll_next(cx)) { + let stream = stream.as_mut(); + return Poll::Ready(match ready!(stream.poll_next(cx)) { Some(Ok(ref bytes)) if bytes.is_empty() => continue, opt => opt.map(|res| res.map_err(Into::into)), }); @@ -406,26 +429,28 @@ where /// Type represent streaming body. This body implementation should be used /// if total size of stream is known. Data get sent as is without using transfer encoding. -pub struct SizedStream { +#[pin_project] +pub struct SizedStream { size: u64, - stream: Pin>, + #[pin] + stream: S, } impl SizedStream where - S: Stream>, + S: Stream> + Unpin, { pub fn new(size: u64, stream: S) -> Self { - SizedStream { size, stream: Box::pin(stream) } + SizedStream { size, stream } } } impl MessageBody for SizedStream where - S: Stream>, + S: Stream> + Unpin, { fn size(&self) -> BodySize { - BodySize::Sized64(self.size) + BodySize::Sized(self.size as u64) } /// Attempts to pull out the next value of the underlying [`Stream`]. @@ -433,10 +458,14 @@ where /// Empty values are skipped to prevent [`SizedStream`]'s transmission being /// ended on a zero-length chunk, but rather proceed until the underlying /// [`Stream`] ends. - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { - let mut stream = self.stream.as_mut(); + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let mut stream: Pin<&mut S> = self.project().stream; loop { - return Poll::Ready(match ready!(stream.as_mut().poll_next(cx)) { + let stream = stream.as_mut(); + return Poll::Ready(match ready!(stream.poll_next(cx)) { Some(Ok(ref bytes)) if bytes.is_empty() => continue, val => val, }); @@ -447,8 +476,9 @@ where #[cfg(test)] mod tests { use super::*; - use futures::stream; + use futures_util::stream; use futures_util::future::poll_fn; + use futures_util::pin_mut; impl Body { pub(crate) fn get_ref(&self) -> &[u8] { @@ -476,7 +506,10 @@ mod tests { assert_eq!("test".size(), BodySize::Sized(4)); assert_eq!( - poll_fn(|cx| "test".poll_next(cx)).await.unwrap().ok(), + poll_fn(|cx| Pin::new(&mut "test").poll_next(cx)) + .await + .unwrap() + .ok(), Some(Bytes::from("test")) ); } @@ -490,13 +523,12 @@ mod tests { BodySize::Sized(4) ); assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test"); + let sb = Bytes::from(&b"test"[..]); + pin_mut!(sb); - assert_eq!((&b"test"[..]).size(), BodySize::Sized(4)); + assert_eq!(sb.size(), BodySize::Sized(4)); assert_eq!( - poll_fn(|cx| (&b"test"[..]).poll_next(cx)) - .await - .unwrap() - .ok(), + poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(), Some(Bytes::from("test")) ); } @@ -505,10 +537,12 @@ mod tests { async fn test_vec() { assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4)); assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test"); + let test_vec = Vec::from("test"); + pin_mut!(test_vec); - assert_eq!(Vec::from("test").size(), BodySize::Sized(4)); + assert_eq!(test_vec.size(), BodySize::Sized(4)); assert_eq!( - poll_fn(|cx| Vec::from("test").poll_next(cx)) + poll_fn(|cx| test_vec.as_mut().poll_next(cx)) .await .unwrap() .ok(), @@ -518,41 +552,44 @@ mod tests { #[actix_rt::test] async fn test_bytes() { - let mut b = Bytes::from("test"); + let b = Bytes::from("test"); assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); assert_eq!(Body::from(b.clone()).get_ref(), b"test"); + pin_mut!(b); assert_eq!(b.size(), BodySize::Sized(4)); assert_eq!( - poll_fn(|cx| b.poll_next(cx)).await.unwrap().ok(), + poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(), Some(Bytes::from("test")) ); } #[actix_rt::test] async fn test_bytes_mut() { - let mut b = BytesMut::from("test"); + let b = BytesMut::from("test"); assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); assert_eq!(Body::from(b.clone()).get_ref(), b"test"); + pin_mut!(b); assert_eq!(b.size(), BodySize::Sized(4)); assert_eq!( - poll_fn(|cx| b.poll_next(cx)).await.unwrap().ok(), + poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(), Some(Bytes::from("test")) ); } #[actix_rt::test] async fn test_string() { - let mut b = "test".to_owned(); + let b = "test".to_owned(); assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); assert_eq!(Body::from(b.clone()).get_ref(), b"test"); assert_eq!(Body::from(&b).size(), BodySize::Sized(4)); assert_eq!(Body::from(&b).get_ref(), b"test"); + pin_mut!(b); assert_eq!(b.size(), BodySize::Sized(4)); assert_eq!( - poll_fn(|cx| b.poll_next(cx)).await.unwrap().ok(), + poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(), Some(Bytes::from("test")) ); } @@ -560,14 +597,17 @@ mod tests { #[actix_rt::test] async fn test_unit() { assert_eq!(().size(), BodySize::Empty); - assert!(poll_fn(|cx| ().poll_next(cx)).await.is_none()); + assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx)) + .await + .is_none()); } #[actix_rt::test] async fn test_box() { - let mut val = Box::new(()); + let val = Box::new(()); + pin_mut!(val); assert_eq!(val.size(), BodySize::Empty); - assert!(poll_fn(|cx| val.poll_next(cx)).await.is_none()); + assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); } #[actix_rt::test] @@ -605,23 +645,54 @@ mod tests { mod body_stream { use super::*; + //use futures::task::noop_waker; + //use futures::stream::once; #[actix_rt::test] async fn skips_empty_chunks() { - let mut body = BodyStream::new(stream::iter( + let body = BodyStream::new(stream::iter( ["1", "", "2"] .iter() .map(|&v| Ok(Bytes::from(v)) as Result), )); + pin_mut!(body); + assert_eq!( - poll_fn(|cx| body.poll_next(cx)).await.unwrap().ok(), + poll_fn(|cx| body.as_mut().poll_next(cx)) + .await + .unwrap() + .ok(), Some(Bytes::from("1")), ); assert_eq!( - poll_fn(|cx| body.poll_next(cx)).await.unwrap().ok(), + poll_fn(|cx| body.as_mut().poll_next(cx)) + .await + .unwrap() + .ok(), Some(Bytes::from("2")), ); } + + /* Now it does not compile as it should + #[actix_rt::test] + async fn move_pinned_pointer() { + let (sender, receiver) = futures::channel::oneshot::channel(); + let mut body_stream = Ok(BodyStream::new(once(async { + let x = Box::new(0i32); + let y = &x; + receiver.await.unwrap(); + let _z = **y; + Ok::<_, ()>(Bytes::new()) + }))); + + let waker = noop_waker(); + let mut context = Context::from_waker(&waker); + pin_mut!(body_stream); + + let _ = body_stream.as_mut().unwrap().poll_next(&mut context); + sender.send(()).unwrap(); + let _ = std::mem::replace(&mut body_stream, Err([0; 32])).unwrap().poll_next(&mut context); + }*/ } mod sized_stream { @@ -629,18 +700,39 @@ mod tests { #[actix_rt::test] async fn skips_empty_chunks() { - let mut body = SizedStream::new( + let body = SizedStream::new( 2, stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))), ); + pin_mut!(body); assert_eq!( - poll_fn(|cx| body.poll_next(cx)).await.unwrap().ok(), + poll_fn(|cx| body.as_mut().poll_next(cx)) + .await + .unwrap() + .ok(), Some(Bytes::from("1")), ); assert_eq!( - poll_fn(|cx| body.poll_next(cx)).await.unwrap().ok(), + poll_fn(|cx| body.as_mut().poll_next(cx)) + .await + .unwrap() + .ok(), Some(Bytes::from("2")), ); } } + + #[actix_rt::test] + async fn test_body_casting() { + let mut body = String::from("hello cast"); + let resp_body: &mut dyn MessageBody = &mut body; + let body = resp_body.downcast_ref::().unwrap(); + assert_eq!(body, "hello cast"); + let body = &mut resp_body.downcast_mut::().unwrap(); + body.push_str("!"); + let body = resp_body.downcast_ref::().unwrap(); + assert_eq!(body, "hello cast!"); + let not_body = resp_body.downcast_ref::<()>(); + assert!(not_body.is_none()); + } } diff --git a/actix-http/src/client/config.rs b/actix-http/src/client/config.rs new file mode 100644 index 000000000..c86c697a2 --- /dev/null +++ b/actix-http/src/client/config.rs @@ -0,0 +1,39 @@ +use std::time::Duration; + +// These values are taken from hyper/src/proto/h2/client.rs +const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2mb +const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1mb + +/// Connector configuration +#[derive(Clone)] +pub(crate) struct ConnectorConfig { + pub(crate) timeout: Duration, + pub(crate) conn_lifetime: Duration, + pub(crate) conn_keep_alive: Duration, + pub(crate) disconnect_timeout: Option, + pub(crate) limit: usize, + pub(crate) conn_window_size: u32, + pub(crate) stream_window_size: u32, +} + +impl Default for ConnectorConfig { + fn default() -> Self { + Self { + timeout: Duration::from_secs(1), + conn_lifetime: Duration::from_secs(75), + conn_keep_alive: Duration::from_secs(15), + disconnect_timeout: Some(Duration::from_millis(3000)), + limit: 100, + conn_window_size: DEFAULT_H2_CONN_WINDOW, + stream_window_size: DEFAULT_H2_STREAM_WINDOW, + } + } +} + +impl ConnectorConfig { + pub(crate) fn no_disconnect_timeout(&self) -> Self { + let mut res = self.clone(); + res.disconnect_timeout = None; + res + } +} diff --git a/actix-http/src/client/connection.rs b/actix-http/src/client/connection.rs index 0ca788b32..eecf2ee6f 100644 --- a/actix-http/src/client/connection.rs +++ b/actix-http/src/client/connection.rs @@ -1,12 +1,13 @@ +use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use std::{fmt, io, mem, time}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use bytes::{Buf, Bytes}; -use futures_util::future::{err, Either, Future, FutureExt, LocalBoxFuture, Ready}; +use futures_util::future::{err, Either, FutureExt, LocalBoxFuture, Ready}; use h2::client::SendRequest; -use pin_project::{pin_project, project}; +use pin_project::pin_project; use crate::body::MessageBody; use crate::h1::ClientCodec; @@ -204,7 +205,7 @@ where } } -#[pin_project] +#[pin_project(project = EitherIoProj)] pub enum EitherIo { A(#[pin] A), B(#[pin] B), @@ -215,16 +216,14 @@ where A: AsyncRead, B: AsyncRead, { - #[project] fn poll_read( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { - #[project] match self.project() { - EitherIo::A(val) => val.poll_read(cx, buf), - EitherIo::B(val) => val.poll_read(cx, buf), + EitherIoProj::A(val) => val.poll_read(cx, buf), + EitherIoProj::B(val) => val.poll_read(cx, buf), } } @@ -244,41 +243,34 @@ where A: AsyncWrite, B: AsyncWrite, { - #[project] fn poll_write( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { - #[project] match self.project() { - EitherIo::A(val) => val.poll_write(cx, buf), - EitherIo::B(val) => val.poll_write(cx, buf), + EitherIoProj::A(val) => val.poll_write(cx, buf), + EitherIoProj::B(val) => val.poll_write(cx, buf), } } - #[project] fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - #[project] match self.project() { - EitherIo::A(val) => val.poll_flush(cx), - EitherIo::B(val) => val.poll_flush(cx), + EitherIoProj::A(val) => val.poll_flush(cx), + EitherIoProj::B(val) => val.poll_flush(cx), } } - #[project] fn poll_shutdown( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { - #[project] match self.project() { - EitherIo::A(val) => val.poll_shutdown(cx), - EitherIo::B(val) => val.poll_shutdown(cx), + EitherIoProj::A(val) => val.poll_shutdown(cx), + EitherIoProj::B(val) => val.poll_shutdown(cx), } } - #[project] fn poll_write_buf( self: Pin<&mut Self>, cx: &mut Context<'_>, @@ -287,10 +279,9 @@ where where Self: Sized, { - #[project] match self.project() { - EitherIo::A(val) => val.poll_write_buf(cx, buf), - EitherIo::B(val) => val.poll_write_buf(cx, buf), + EitherIoProj::A(val) => val.poll_write_buf(cx, buf), + EitherIoProj::B(val) => val.poll_write_buf(cx, buf), } } } diff --git a/actix-http/src/client/connector.rs b/actix-http/src/client/connector.rs index 055d4276d..e1aed6382 100644 --- a/actix-http/src/client/connector.rs +++ b/actix-http/src/client/connector.rs @@ -11,6 +11,7 @@ use actix_service::{apply_fn, Service}; use actix_utils::timeout::{TimeoutError, TimeoutService}; use http::Uri; +use super::config::ConnectorConfig; use super::connection::Connection; use super::error::ConnectError; use super::pool::{ConnectionPool, Protocol}; @@ -48,11 +49,7 @@ type SslConnector = (); /// ``` pub struct Connector { connector: T, - timeout: Duration, - conn_lifetime: Duration, - conn_keep_alive: Duration, - disconnect_timeout: Duration, - limit: usize, + config: ConnectorConfig, #[allow(dead_code)] ssl: SslConnector, _t: PhantomData, @@ -71,42 +68,47 @@ impl Connector<(), ()> { > + Clone, TcpStream, > { - let ssl = { - #[cfg(feature = "openssl")] - { - use actix_connect::ssl::openssl::SslMethod; - - let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap(); - let _ = ssl - .set_alpn_protos(b"\x02h2\x08http/1.1") - .map_err(|e| error!("Can not set alpn protocol: {:?}", e)); - SslConnector::Openssl(ssl.build()) - } - #[cfg(all(not(feature = "openssl"), feature = "rustls"))] - { - let protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; - let mut config = ClientConfig::new(); - config.set_protocols(&protos); - config - .root_store - .add_server_trust_anchors(&actix_tls::rustls::TLS_SERVER_ROOTS); - SslConnector::Rustls(Arc::new(config)) - } - #[cfg(not(any(feature = "openssl", feature = "rustls")))] - {} - }; - Connector { - ssl, + ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), connector: default_connector(), - timeout: Duration::from_secs(1), - conn_lifetime: Duration::from_secs(75), - conn_keep_alive: Duration::from_secs(15), - disconnect_timeout: Duration::from_millis(3000), - limit: 100, + config: ConnectorConfig::default(), _t: PhantomData, } } + + // Build Ssl connector with openssl, based on supplied alpn protocols + #[cfg(feature = "openssl")] + fn build_ssl(protocols: Vec>) -> SslConnector { + use actix_connect::ssl::openssl::SslMethod; + use bytes::{BufMut, BytesMut}; + + let mut alpn = BytesMut::with_capacity(20); + for proto in protocols.iter() { + alpn.put_u8(proto.len() as u8); + alpn.put(proto.as_slice()); + } + + let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap(); + let _ = ssl + .set_alpn_protos(&alpn) + .map_err(|e| error!("Can not set alpn protocol: {:?}", e)); + SslConnector::Openssl(ssl.build()) + } + + // Build Ssl connector with rustls, based on supplied alpn protocols + #[cfg(all(not(feature = "openssl"), feature = "rustls"))] + fn build_ssl(protocols: Vec>) -> SslConnector { + let mut config = ClientConfig::new(); + config.set_protocols(&protocols); + config + .root_store + .add_server_trust_anchors(&actix_tls::rustls::TLS_SERVER_ROOTS); + SslConnector::Rustls(Arc::new(config)) + } + + // ssl turned off, provides empty ssl connector + #[cfg(not(any(feature = "openssl", feature = "rustls")))] + fn build_ssl(_: Vec>) -> SslConnector {} } impl Connector { @@ -122,11 +124,7 @@ impl Connector { { Connector { connector, - timeout: self.timeout, - conn_lifetime: self.conn_lifetime, - conn_keep_alive: self.conn_keep_alive, - disconnect_timeout: self.disconnect_timeout, - limit: self.limit, + config: self.config, ssl: self.ssl, _t: PhantomData, } @@ -146,7 +144,7 @@ where /// Connection timeout, i.e. max time to connect to remote host including dns name resolution. /// Set to 1 second by default. pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = timeout; + self.config.timeout = timeout; self } @@ -163,12 +161,44 @@ where self } + /// Maximum supported http major version + /// Supported versions http/1.1, http/2 + pub fn max_http_version(mut self, val: http::Version) -> Self { + let versions = match val { + http::Version::HTTP_11 => vec![b"http/1.1".to_vec()], + http::Version::HTTP_2 => vec![b"h2".to_vec(), b"http/1.1".to_vec()], + _ => { + unimplemented!("actix-http:client: supported versions http/1.1, http/2") + } + }; + self.ssl = Connector::build_ssl(versions); + self + } + + /// Indicates the initial window size (in octets) for + /// HTTP2 stream-level flow control for received data. + /// + /// The default value is 65,535 and is good for APIs, but not for big objects. + pub fn initial_window_size(mut self, size: u32) -> Self { + self.config.stream_window_size = size; + self + } + + /// Indicates the initial window size (in octets) for + /// HTTP2 connection-level flow control for received data. + /// + /// The default value is 65,535 and is good for APIs, but not for big objects. + pub fn initial_connection_window_size(mut self, size: u32) -> Self { + self.config.conn_window_size = size; + self + } + /// Set total number of simultaneous connections per type of scheme. /// /// If limit is 0, the connector has no limit. /// The default limit size is 100. pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; + self.config.limit = limit; self } @@ -179,7 +209,7 @@ where /// exceeds this period, the connection is closed. /// Default keep-alive period is 15 seconds. pub fn conn_keep_alive(mut self, dur: Duration) -> Self { - self.conn_keep_alive = dur; + self.config.conn_keep_alive = dur; self } @@ -189,7 +219,7 @@ where /// until it is closed regardless of keep-alive period. /// Default lifetime period is 75 seconds. pub fn conn_lifetime(mut self, dur: Duration) -> Self { - self.conn_lifetime = dur; + self.config.conn_lifetime = dur; self } @@ -202,7 +232,7 @@ where /// /// By default disconnect timeout is set to 3000 milliseconds. pub fn disconnect_timeout(mut self, dur: Duration) -> Self { - self.disconnect_timeout = dur; + self.config.disconnect_timeout = Some(dur); self } @@ -216,7 +246,7 @@ where #[cfg(not(any(feature = "openssl", feature = "rustls")))] { let connector = TimeoutService::new( - self.timeout, + self.config.timeout, apply_fn(self.connector, |msg: Connect, srv| { srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) }) @@ -231,10 +261,7 @@ where connect_impl::InnerConnector { tcp_pool: ConnectionPool::new( connector, - self.conn_lifetime, - self.conn_keep_alive, - None, - self.limit, + self.config.no_disconnect_timeout(), ), } } @@ -248,7 +275,7 @@ where use actix_service::{boxed::service, pipeline}; let ssl_service = TimeoutService::new( - self.timeout, + self.config.timeout, pipeline( apply_fn(self.connector.clone(), |msg: Connect, srv| { srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) @@ -301,7 +328,7 @@ where }); let tcp_service = TimeoutService::new( - self.timeout, + self.config.timeout, apply_fn(self.connector, |msg: Connect, srv| { srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) }) @@ -316,18 +343,9 @@ where connect_impl::InnerConnector { tcp_pool: ConnectionPool::new( tcp_service, - self.conn_lifetime, - self.conn_keep_alive, - None, - self.limit, - ), - ssl_pool: ConnectionPool::new( - ssl_service, - self.conn_lifetime, - self.conn_keep_alive, - Some(self.disconnect_timeout), - self.limit, + self.config.no_disconnect_timeout(), ), + ssl_pool: ConnectionPool::new(ssl_service, self.config), } } } diff --git a/actix-http/src/client/error.rs b/actix-http/src/client/error.rs index 42ea47ee8..ba697bca4 100644 --- a/actix-http/src/client/error.rs +++ b/actix-http/src/client/error.rs @@ -39,7 +39,7 @@ pub enum ConnectError { H2(h2::Error), /// Connecting took too long - #[display(fmt = "Timeout out while establishing connection")] + #[display(fmt = "Timeout while establishing connection")] Timeout, /// Connector has been disconnected @@ -48,20 +48,22 @@ pub enum ConnectError { /// Unresolved host name #[display(fmt = "Connector received `Connect` method with unresolved host")] - Unresolverd, + Unresolved, /// Connection io error #[display(fmt = "{}", _0)] Io(io::Error), } +impl std::error::Error for ConnectError {} + impl From for ConnectError { fn from(err: actix_connect::ConnectError) -> ConnectError { match err { actix_connect::ConnectError::Resolver(e) => ConnectError::Resolver(e), actix_connect::ConnectError::NoRecords => ConnectError::NoRecords, actix_connect::ConnectError::InvalidInput => panic!(), - actix_connect::ConnectError::Unresolverd => ConnectError::Unresolverd, + actix_connect::ConnectError::Unresolved => ConnectError::Unresolved, actix_connect::ConnectError::Io(e) => ConnectError::Io(e), } } @@ -86,6 +88,8 @@ pub enum InvalidUrl { HttpError(http::Error), } +impl std::error::Error for InvalidUrl {} + /// A set of errors that can occur during request sending and response reading #[derive(Debug, Display, From)] pub enum SendRequestError { @@ -106,7 +110,7 @@ pub enum SendRequestError { #[display(fmt = "{}", _0)] H2(h2::Error), /// Response took too long - #[display(fmt = "Timeout out while waiting for response")] + #[display(fmt = "Timeout while waiting for response")] Timeout, /// Tunnels are not supported for http2 connection #[display(fmt = "Tunnels are not supported for http2 connection")] @@ -115,6 +119,8 @@ pub enum SendRequestError { Body(Error), } +impl std::error::Error for SendRequestError {} + /// Convert `SendRequestError` to a server `Response` impl ResponseError for SendRequestError { fn status_code(&self) -> StatusCode { @@ -139,6 +145,8 @@ pub enum FreezeRequestError { Http(HttpError), } +impl std::error::Error for FreezeRequestError {} + impl From for SendRequestError { fn from(e: FreezeRequestError) -> Self { match e { diff --git a/actix-http/src/client/h1proto.rs b/actix-http/src/client/h1proto.rs index a0a20edf6..51e853b3d 100644 --- a/actix-http/src/client/h1proto.rs +++ b/actix-http/src/client/h1proto.rs @@ -8,7 +8,7 @@ use bytes::buf::BufMutExt; use bytes::{Bytes, BytesMut}; use futures_core::Stream; use futures_util::future::poll_fn; -use futures_util::{SinkExt, StreamExt}; +use futures_util::{pin_mut, SinkExt, StreamExt}; use crate::error::PayloadError; use crate::h1; @@ -120,7 +120,7 @@ where /// send request body to the peer pub(crate) async fn send_body( - mut body: B, + body: B, framed: &mut Framed, ) -> Result<(), SendRequestError> where @@ -128,9 +128,10 @@ where B: MessageBody, { let mut eof = false; + pin_mut!(body); while !eof { while !eof && !framed.is_write_buf_full() { - match poll_fn(|cx| body.poll_next(cx)).await { + match poll_fn(|cx| body.as_mut().poll_next(cx)).await { Some(result) => { framed.write(h1::Message::Chunk(Some(result?)))?; } diff --git a/actix-http/src/client/h2proto.rs b/actix-http/src/client/h2proto.rs index eabf54e97..48ab9fe4a 100644 --- a/actix-http/src/client/h2proto.rs +++ b/actix-http/src/client/h2proto.rs @@ -1,10 +1,15 @@ use std::convert::TryFrom; +use std::future::Future; use std::time; use actix_codec::{AsyncRead, AsyncWrite}; use bytes::Bytes; use futures_util::future::poll_fn; -use h2::{client::SendRequest, SendStream}; +use futures_util::pin_mut; +use h2::{ + client::{Builder, Connection, SendRequest}, + SendStream, +}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING}; use http::{request::Request, Method, Version}; @@ -13,6 +18,7 @@ use crate::header::HeaderMap; use crate::message::{RequestHeadType, ResponseHead}; use crate::payload::Payload; +use super::config::ConnectorConfig; use super::connection::{ConnectionType, IoConnection}; use super::error::SendRequestError; use super::pool::Acquired; @@ -58,10 +64,6 @@ where CONTENT_LENGTH, HeaderValue::try_from(format!("{}", len)).unwrap(), ), - BodySize::Sized64(len) => req.headers_mut().insert( - CONTENT_LENGTH, - HeaderValue::try_from(format!("{}", len)).unwrap(), - ), }; // Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate. @@ -123,13 +125,14 @@ where } async fn send_body( - mut body: B, + body: B, mut send: SendStream, ) -> Result<(), SendRequestError> { let mut buf = None; + pin_mut!(body); loop { if buf.is_none() { - match poll_fn(|cx| body.poll_next(cx)).await { + match poll_fn(|cx| body.as_mut().poll_next(cx)).await { Some(Ok(b)) => { send.reserve_capacity(b.len()); buf = Some(b); @@ -183,3 +186,18 @@ fn release( } } } + +pub(crate) fn handshake( + io: Io, + config: &ConnectorConfig, +) -> impl Future, Connection), h2::Error>> +where + Io: AsyncRead + AsyncWrite + Unpin + 'static, +{ + let mut builder = Builder::new(); + builder + .initial_window_size(config.stream_window_size) + .initial_connection_window_size(config.conn_window_size) + .enable_push(false); + builder.handshake(io) +} diff --git a/actix-http/src/client/mod.rs b/actix-http/src/client/mod.rs index a45aebcd5..dd1e9b25a 100644 --- a/actix-http/src/client/mod.rs +++ b/actix-http/src/client/mod.rs @@ -1,6 +1,7 @@ //! Http client api use http::Uri; +mod config; mod connection; mod connector; mod error; diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs index 8c94423ac..5a10725b0 100644 --- a/actix-http/src/client/pool.rs +++ b/actix-http/src/client/pool.rs @@ -13,13 +13,16 @@ use actix_utils::{oneshot, task::LocalWaker}; use bytes::Bytes; use futures_util::future::{poll_fn, FutureExt, LocalBoxFuture}; use fxhash::FxHashMap; -use h2::client::{handshake, Connection, SendRequest}; +use h2::client::{Connection, SendRequest}; use http::uri::Authority; use indexmap::IndexSet; +use pin_project::pin_project; use slab::Slab; +use super::config::ConnectorConfig; use super::connection::{ConnectionType, IoConnection}; use super::error::ConnectError; +use super::h2proto::handshake; use super::Connect; #[derive(Clone, Copy, PartialEq)] @@ -49,20 +52,11 @@ where T: Service + 'static, { - pub(crate) fn new( - connector: T, - conn_lifetime: Duration, - conn_keep_alive: Duration, - disconnect_timeout: Option, - limit: usize, - ) -> Self { + pub(crate) fn new(connector: T, config: ConnectorConfig) -> Self { ConnectionPool( Rc::new(RefCell::new(connector)), Rc::new(RefCell::new(Inner { - conn_lifetime, - conn_keep_alive, - disconnect_timeout, - limit, + config, acquired: 0, waiters: Slab::new(), waiters_queue: IndexSet::new(), @@ -111,7 +105,7 @@ where let key = if let Some(authority) = req.uri.authority() { authority.clone().into() } else { - return Err(ConnectError::Unresolverd); + return Err(ConnectError::Unresolved); }; // acquire connection @@ -128,6 +122,8 @@ where // open tcp connection let (io, proto) = connector.call(req).await?; + let config = inner.borrow().config.clone(); + let guard = OpenGuard::new(key, inner); if proto == Protocol::Http1 { @@ -137,7 +133,7 @@ where Some(guard.consume()), )) } else { - let (snd, connection) = handshake(io).await?; + let (snd, connection) = handshake(io, &config).await?; actix_rt::spawn(connection.map(|_| ())); Ok(IoConnection::new( ConnectionType::H2(snd), @@ -199,7 +195,7 @@ where if let Some(i) = self.inner.take() { let mut inner = i.as_ref().borrow_mut(); inner.release_waiter(&self.key, self.token); - inner.check_availibility(); + inner.check_availability(); } } } @@ -236,7 +232,7 @@ where if let Some(i) = self.inner.take() { let mut inner = i.as_ref().borrow_mut(); inner.release(); - inner.check_availibility(); + inner.check_availability(); } } } @@ -254,10 +250,7 @@ struct AvailableConnection { } pub(crate) struct Inner { - conn_lifetime: Duration, - conn_keep_alive: Duration, - disconnect_timeout: Option, - limit: usize, + config: ConnectorConfig, acquired: usize, available: FxHashMap>>, waiters: Slab< @@ -310,7 +303,7 @@ where fn acquire(&mut self, key: &Key, cx: &mut Context<'_>) -> Acquire { // check limits - if self.limit > 0 && self.acquired >= self.limit { + if self.config.limit > 0 && self.acquired >= self.config.limit { return Acquire::NotAvailable; } @@ -322,10 +315,10 @@ where let now = Instant::now(); while let Some(conn) = connections.pop_back() { // check if it still usable - if (now - conn.used) > self.conn_keep_alive - || (now - conn.created) > self.conn_lifetime + if (now - conn.used) > self.config.conn_keep_alive + || (now - conn.created) > self.config.conn_lifetime { - if let Some(timeout) = self.disconnect_timeout { + if let Some(timeout) = self.config.disconnect_timeout { if let ConnectionType::H1(io) = conn.io { actix_rt::spawn(CloseConnection::new(io, timeout)) } @@ -337,7 +330,7 @@ where match Pin::new(s).poll_read(cx, &mut buf) { Poll::Pending => (), Poll::Ready(Ok(n)) if n > 0 => { - if let Some(timeout) = self.disconnect_timeout { + if let Some(timeout) = self.config.disconnect_timeout { if let ConnectionType::H1(io) = io { actix_rt::spawn(CloseConnection::new( io, timeout, @@ -366,21 +359,21 @@ where created, used: Instant::now(), }); - self.check_availibility(); + self.check_availability(); } fn release_close(&mut self, io: ConnectionType) { self.acquired -= 1; - if let Some(timeout) = self.disconnect_timeout { + if let Some(timeout) = self.config.disconnect_timeout { if let ConnectionType::H1(io) = io { actix_rt::spawn(CloseConnection::new(io, timeout)) } } - self.check_availibility(); + self.check_availability(); } - fn check_availibility(&self) { - if !self.waiters_queue.is_empty() && self.acquired < self.limit { + fn check_availability(&self) { + if !self.waiters_queue.is_empty() && self.acquired < self.config.limit { self.waker.wake(); } } @@ -422,6 +415,7 @@ where } } +#[pin_project] struct ConnectorPoolSupport where Io: AsyncRead + AsyncWrite + Unpin + 'static, @@ -439,7 +433,7 @@ where type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = unsafe { self.get_unchecked_mut() }; + let this = self.project(); let mut inner = this.inner.as_ref().borrow_mut(); inner.waker.register(cx.waker()); @@ -478,6 +472,7 @@ where tx, this.inner.clone(), this.connector.call(connect), + inner.config.clone(), ); } } @@ -504,6 +499,7 @@ where >, rx: Option, ConnectError>>>, inner: Option>>>, + config: ConnectorConfig, } impl OpenWaitingConnection @@ -516,6 +512,7 @@ where rx: oneshot::Sender, ConnectError>>, inner: Rc>>, fut: F, + config: ConnectorConfig, ) { actix_rt::spawn(OpenWaitingConnection { key, @@ -523,6 +520,7 @@ where h2: None, rx: Some(rx), inner: Some(inner), + config, }) } } @@ -536,7 +534,7 @@ where if let Some(inner) = self.project().inner.take() { let mut inner = inner.as_ref().borrow_mut(); inner.release(); - inner.check_availibility(); + inner.check_availability(); } } } @@ -592,7 +590,7 @@ where ))); Poll::Ready(()) } else { - *this.h2 = Some(handshake(io).boxed_local()); + *this.h2 = Some(handshake(io, this.config).boxed_local()); self.poll(cx) } } diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index a38a80e76..abf3d8ff9 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -114,7 +114,7 @@ impl ServiceConfig { } #[inline] - /// Return state of connection keep-alive funcitonality + /// Return state of connection keep-alive functionality pub fn keep_alive_enabled(&self) -> bool { self.0.ka_enabled } @@ -211,7 +211,12 @@ impl Date { } fn update(&mut self) { self.pos = 0; - write!(self, "{}", OffsetDateTime::now().format("%a, %d %b %Y %H:%M:%S GMT")).unwrap(); + write!( + self, + "{}", + OffsetDateTime::now_utc().format("%a, %d %b %Y %H:%M:%S GMT") + ) + .unwrap(); } } @@ -282,7 +287,6 @@ impl DateService { mod tests { use super::*; - // Test modifying the date from within the closure // passed to `set_date` #[test] @@ -290,9 +294,7 @@ mod tests { let service = DateService::new(); // Make sure that `check_date` doesn't try to spawn a task service.0.update(); - service.set_date(|_| { - service.0.reset() - }); + service.set_date(|_| service.0.reset()); } #[test] diff --git a/actix-http/src/cookie/builder.rs b/actix-http/src/cookie/builder.rs index c3820abf0..b64352e35 100644 --- a/actix-http/src/cookie/builder.rs +++ b/actix-http/src/cookie/builder.rs @@ -63,7 +63,7 @@ impl CookieBuilder { /// use actix_http::cookie::Cookie; /// /// let c = Cookie::build("foo", "bar") - /// .expires(time::OffsetDateTime::now()) + /// .expires(time::OffsetDateTime::now_utc()) /// .finish(); /// /// assert!(c.expires().is_some()); @@ -109,7 +109,8 @@ impl CookieBuilder { pub fn max_age_time(mut self, value: Duration) -> CookieBuilder { // Truncate any nanoseconds from the Duration, as they aren't represented within `Max-Age` // and would cause two otherwise identical `Cookie` instances to not be equivalent to one another. - self.cookie.set_max_age(Duration::seconds(value.whole_seconds())); + self.cookie + .set_max_age(Duration::seconds(value.whole_seconds())); self } diff --git a/actix-http/src/cookie/jar.rs b/actix-http/src/cookie/jar.rs index 64922897b..fbefa1bbf 100644 --- a/actix-http/src/cookie/jar.rs +++ b/actix-http/src/cookie/jar.rs @@ -1,5 +1,5 @@ use std::collections::HashSet; -use std::mem::replace; +use std::mem; use time::{Duration, OffsetDateTime}; @@ -13,7 +13,7 @@ use super::secure::{Key, PrivateJar, SignedJar}; /// /// A `CookieJar` provides storage for any number of cookies. Any changes made /// to the jar are tracked; the changes can be retrieved via the -/// [delta](#method.delta) method which returns an interator over the changes. +/// [delta](#method.delta) method which returns an iterator over the changes. /// /// # Usage /// @@ -221,7 +221,7 @@ impl CookieJar { if self.original_cookies.contains(cookie.name()) { cookie.set_value(""); cookie.set_max_age(Duration::zero()); - cookie.set_expires(OffsetDateTime::now() - Duration::days(365)); + cookie.set_expires(OffsetDateTime::now_utc() - Duration::days(365)); self.delta_cookies.replace(DeltaCookie::removed(cookie)); } else { self.delta_cookies.remove(cookie.name()); @@ -273,7 +273,7 @@ impl CookieJar { )] pub fn clear(&mut self) { self.delta_cookies.clear(); - for delta in replace(&mut self.original_cookies, HashSet::new()) { + for delta in mem::take(&mut self.original_cookies) { self.remove(delta.cookie); } } @@ -533,8 +533,8 @@ mod test { #[test] #[cfg(feature = "secure-cookies")] fn delta() { - use time::Duration; use std::collections::HashMap; + use time::Duration; let mut c = CookieJar::new(); diff --git a/actix-http/src/cookie/mod.rs b/actix-http/src/cookie/mod.rs index 09120e19f..b94e0fe0f 100644 --- a/actix-http/src/cookie/mod.rs +++ b/actix-http/src/cookie/mod.rs @@ -47,7 +47,7 @@ //! ``` #![doc(html_root_url = "https://docs.rs/cookie/0.11")] -#![deny(missing_docs)] +#![warn(missing_docs)] mod builder; mod delta; @@ -103,7 +103,7 @@ enum CookieStr { impl CookieStr { /// Retrieves the string `self` corresponds to. If `self` is derived from - /// indexes, the corresponding subslice of `string` is returned. Otherwise, + /// indexes, the corresponding sub-slice of `string` is returned. Otherwise, /// the concrete string is returned. /// /// # Panics @@ -733,7 +733,7 @@ impl<'c> Cookie<'c> { pub fn make_permanent(&mut self) { let twenty_years = Duration::days(365 * 20); self.set_max_age(twenty_years); - self.set_expires(OffsetDateTime::now() + twenty_years); + self.set_expires(OffsetDateTime::now_utc() + twenty_years); } fn fmt_parameters(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -990,7 +990,7 @@ impl<'a, 'b> PartialEq> for Cookie<'a> { #[cfg(test)] mod tests { use super::{Cookie, SameSite}; - use time::{offset, PrimitiveDateTime}; + use time::PrimitiveDateTime; #[test] fn format() { @@ -1015,7 +1015,9 @@ mod tests { assert_eq!(&cookie.to_string(), "foo=bar; Domain=www.rust-lang.org"); let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; - let expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%M:%S").unwrap().using_offset(offset!(UTC)); + let expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%M:%S") + .unwrap() + .assume_utc(); let cookie = Cookie::build("foo", "bar").expires(expires).finish(); assert_eq!( &cookie.to_string(), diff --git a/actix-http/src/cookie/parse.rs b/actix-http/src/cookie/parse.rs index 28eb4f8b6..d472b32b6 100644 --- a/actix-http/src/cookie/parse.rs +++ b/actix-http/src/cookie/parse.rs @@ -6,7 +6,7 @@ use std::fmt; use std::str::Utf8Error; use percent_encoding::percent_decode; -use time::{Duration, offset}; +use time::Duration; use super::{Cookie, CookieStr, SameSite}; @@ -172,6 +172,8 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { cookie.same_site = Some(SameSite::Strict); } else if v.eq_ignore_ascii_case("lax") { cookie.same_site = Some(SameSite::Lax); + } else if v.eq_ignore_ascii_case("none") { + cookie.same_site = Some(SameSite::None); } else { // We do nothing here, for now. When/if the `SameSite` // attribute becomes standard, the spec says that we should @@ -188,7 +190,7 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { .or_else(|| time::parse(v, "%a, %d-%b-%Y %H:%M:%S").ok()); if let Some(time) = tm { - cookie.expires = Some(time.using_offset(offset!(UTC))) + cookie.expires = Some(time.assume_utc()) } } _ => { @@ -216,7 +218,7 @@ where #[cfg(test)] mod tests { use super::{Cookie, SameSite}; - use time::{offset, Duration, PrimitiveDateTime}; + use time::{Duration, PrimitiveDateTime}; macro_rules! assert_eq_parse { ($string:expr, $expected:expr) => { @@ -261,6 +263,16 @@ mod tests { assert_eq_parse!("foo=bar; SameSite=strict", expected); assert_eq_parse!("foo=bar; SameSite=STrICT", expected); assert_eq_parse!("foo=bar; SameSite=STRICT", expected); + + let expected = Cookie::build("foo", "bar") + .same_site(SameSite::None) + .finish(); + + assert_eq_parse!("foo=bar; SameSite=None", expected); + assert_eq_parse!("foo=bar; SameSITE=None", expected); + assert_eq_parse!("foo=bar; SameSite=nOne", expected); + assert_eq_parse!("foo=bar; SameSite=NoNE", expected); + assert_eq_parse!("foo=bar; SameSite=NONE", expected); } #[test] @@ -376,7 +388,9 @@ mod tests { ); let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; - let expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%M:%S").unwrap().using_offset(offset!(UTC)); + let expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%M:%S") + .unwrap() + .assume_utc(); expected.set_expires(expires); assert_eq_parse!( " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ @@ -385,13 +399,38 @@ mod tests { ); unexpected.set_domain("foo.com"); - let bad_expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%S:%M").unwrap().using_offset(offset!(UTC)); + let bad_expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%S:%M") + .unwrap() + .assume_utc(); expected.set_expires(bad_expires); assert_ne_parse!( " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", unexpected ); + + expected.set_expires(expires); + expected.set_same_site(SameSite::Lax); + assert_eq_parse!( + " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ + Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT; \ + SameSite=Lax", + expected + ); + expected.set_same_site(SameSite::Strict); + assert_eq_parse!( + " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ + Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT; \ + SameSite=Strict", + expected + ); + expected.set_same_site(SameSite::None); + assert_eq_parse!( + " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ + Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT; \ + SameSite=None", + expected + ); } #[test] @@ -414,8 +453,15 @@ mod tests { #[test] fn do_not_panic_on_large_max_ages() { let max_duration = Duration::max_value(); - let expected = Cookie::build("foo", "bar").max_age_time(max_duration).finish(); - let overflow_duration = max_duration.checked_add(Duration::nanoseconds(1)).unwrap_or(max_duration); - assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", overflow_duration.whole_seconds()), expected); + let expected = Cookie::build("foo", "bar") + .max_age_time(max_duration) + .finish(); + let overflow_duration = max_duration + .checked_add(Duration::nanoseconds(1)) + .unwrap_or(max_duration); + assert_eq_parse!( + format!(" foo=bar; Max-Age={:?}", overflow_duration.whole_seconds()), + expected + ); } } diff --git a/actix-http/src/cookie/secure/key.rs b/actix-http/src/cookie/secure/key.rs index 779c16b75..41413921f 100644 --- a/actix-http/src/cookie/secure/key.rs +++ b/actix-http/src/cookie/secure/key.rs @@ -84,7 +84,7 @@ impl Key { } /// Generates signing/encryption keys from a secure, random source. Keys are - /// generated nondeterministically. + /// generated non-deterministically. /// /// # Panics /// @@ -103,7 +103,7 @@ impl Key { } /// Attempts to generate signing/encryption keys from a secure, random - /// source. Keys are generated nondeterministically. If randomness cannot be + /// source. Keys are generated non-deterministically. If randomness cannot be /// retrieved from the underlying operating system, returns `None`. /// /// # Example diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index ca04845ab..eb1821285 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -9,6 +9,7 @@ use brotli2::write::BrotliEncoder; use bytes::Bytes; use flate2::write::{GzEncoder, ZlibEncoder}; use futures_core::ready; +use pin_project::pin_project; use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::http::header::{ContentEncoding, CONTENT_ENCODING}; @@ -19,8 +20,10 @@ use super::Writer; const INPLACE: usize = 1024; +#[pin_project] pub struct Encoder { eof: bool, + #[pin] body: EncoderBody, encoder: Option, fut: Option>, @@ -76,67 +79,88 @@ impl Encoder { } } +#[pin_project(project = EncoderBodyProj)] enum EncoderBody { Bytes(Bytes), - Stream(B), - BoxedStream(Box), + Stream(#[pin] B), + BoxedStream(Box), +} + +impl MessageBody for EncoderBody { + fn size(&self) -> BodySize { + match self { + EncoderBody::Bytes(ref b) => b.size(), + EncoderBody::Stream(ref b) => b.size(), + EncoderBody::BoxedStream(ref b) => b.size(), + } + } + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + match self.project() { + EncoderBodyProj::Bytes(b) => { + if b.is_empty() { + Poll::Ready(None) + } else { + Poll::Ready(Some(Ok(std::mem::take(b)))) + } + } + EncoderBodyProj::Stream(b) => b.poll_next(cx), + EncoderBodyProj::BoxedStream(ref mut b) => { + Pin::new(b.as_mut()).poll_next(cx) + } + } + } } impl MessageBody for Encoder { fn size(&self) -> BodySize { if self.encoder.is_none() { - match self.body { - EncoderBody::Bytes(ref b) => b.size(), - EncoderBody::Stream(ref b) => b.size(), - EncoderBody::BoxedStream(ref b) => b.size(), - } + self.body.size() } else { BodySize::Stream } } - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let mut this = self.project(); loop { - if self.eof { + if *this.eof { return Poll::Ready(None); } - if let Some(ref mut fut) = self.fut { + if let Some(ref mut fut) = this.fut { let mut encoder = match ready!(Pin::new(fut).poll(cx)) { Ok(item) => item, Err(e) => return Poll::Ready(Some(Err(e.into()))), }; let chunk = encoder.take(); - self.encoder = Some(encoder); - self.fut.take(); + *this.encoder = Some(encoder); + this.fut.take(); if !chunk.is_empty() { return Poll::Ready(Some(Ok(chunk))); } } - let result = match self.body { - EncoderBody::Bytes(ref mut b) => { - if b.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(std::mem::replace(b, Bytes::new())))) - } - } - EncoderBody::Stream(ref mut b) => b.poll_next(cx), - EncoderBody::BoxedStream(ref mut b) => b.poll_next(cx), - }; + let result = this.body.as_mut().poll_next(cx); + match result { Poll::Ready(Some(Ok(chunk))) => { - if let Some(mut encoder) = self.encoder.take() { + if let Some(mut encoder) = this.encoder.take() { if chunk.len() < INPLACE { encoder.write(&chunk)?; let chunk = encoder.take(); - self.encoder = Some(encoder); + *this.encoder = Some(encoder); if !chunk.is_empty() { return Poll::Ready(Some(Ok(chunk))); } } else { - self.fut = Some(run(move || { + *this.fut = Some(run(move || { encoder.write(&chunk)?; Ok(encoder) })); @@ -146,12 +170,12 @@ impl MessageBody for Encoder { } } Poll::Ready(None) => { - if let Some(encoder) = self.encoder.take() { + if let Some(encoder) = this.encoder.take() { let chunk = encoder.finish()?; if chunk.is_empty() { return Poll::Ready(None); } else { - self.eof = true; + *this.eof = true; return Poll::Ready(Some(Ok(chunk))); } } else { diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index b6637075c..f0a4b70bc 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -1,5 +1,4 @@ //! Error and Result module -use std::any::TypeId; use std::cell::RefCell; use std::io::Write; use std::str::Utf8Error; @@ -15,12 +14,11 @@ use derive_more::{Display, From}; pub use futures_channel::oneshot::Canceled; use http::uri::InvalidUri; use http::{header, Error as HttpError, StatusCode}; -use httparse; use serde::de::value::Error as DeError; use serde_json::error::Error as JsonError; use serde_urlencoded::ser::Error as FormError; -// re-export for convinience +// re-export for convenience use crate::body::Body; pub use crate::cookie::ParseError as CookieParseError; use crate::helpers::Writer; @@ -36,7 +34,7 @@ pub type Result = result::Result; /// General purpose actix web error. /// -/// An actix web error is used to carry errors from `failure` or `std::error` +/// An actix web error is used to carry errors from `std::error` /// through actix in a convenient way. It can be created through /// converting errors with `into()`. /// @@ -60,12 +58,6 @@ impl Error { } } -/// A struct with a private constructor, for use with -/// `__private_get_type_id__`. Its single field is private, -/// ensuring that it can only be constructed from this module -#[doc(hidden)] -pub struct PrivateHelper(()); - /// Error that can be converted to `Response` pub trait ResponseError: fmt::Debug + fmt::Display { /// Response's status code @@ -89,43 +81,10 @@ pub trait ResponseError: fmt::Debug + fmt::Display { resp.set_body(Body::from(buf)) } - /// A helper method to get the type ID of the type - /// this trait is implemented on. - /// This method is unsafe to *implement*, since `downcast_ref` relies - /// on the returned `TypeId` to perform a cast. - /// - /// Unfortunately, Rust has no notion of a trait method that is - /// unsafe to implement (marking it as `unsafe` makes it unsafe - /// to *call*). As a workaround, we require this method - /// to return a private type along with the `TypeId`. This - /// private type (`PrivateHelper`) has a private constructor, - /// making it impossible for safe code to construct outside of - /// this module. This ensures that safe code cannot violate - /// type-safety by implementing this method. - #[doc(hidden)] - fn __private_get_type_id__(&self) -> (TypeId, PrivateHelper) - where - Self: 'static, - { - (TypeId::of::(), PrivateHelper(())) - } + downcast_get_type_id!(); } -impl dyn ResponseError + 'static { - /// Downcasts a response error to a specific type. - pub fn downcast_ref(&self) -> Option<&T> { - if self.__private_get_type_id__().0 == TypeId::of::() { - // Safety: external crates cannot override the default - // implementation of `__private_get_type_id__`, since - // it requires returning a private type. We can therefore - // rely on the returned `TypeId`, which ensures that this - // case is correct. - unsafe { Some(&*(self as *const dyn ResponseError as *const T)) } - } else { - None - } - } -} +downcast!(ResponseError); impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -374,6 +333,8 @@ pub enum PayloadError { Io(io::Error), } +impl std::error::Error for PayloadError {} + impl From for PayloadError { fn from(err: h2::Error) -> Self { PayloadError::Http2Payload(err) @@ -471,7 +432,7 @@ pub enum DispatchError { Unknown, } -/// A set of error that can occure during parsing content type +/// A set of error that can occur during parsing content type #[derive(PartialEq, Debug, Display)] pub enum ContentTypeError { /// Can not parse content type @@ -482,6 +443,8 @@ pub enum ContentTypeError { UnknownEncoding, } +impl std::error::Error for ContentTypeError {} + /// Return `BadRequest` for `ContentTypeError` impl ResponseError for ContentTypeError { fn status_code(&self) -> StatusCode { @@ -987,9 +950,15 @@ where InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into() } -#[cfg(feature = "failure")] -/// Compatibility for `failure::Error` -impl ResponseError for fail_ure::Error {} +#[cfg(feature = "actors")] +/// `InternalServerError` for `actix::MailboxError` +/// This is supported on feature=`actors` only +impl ResponseError for actix::MailboxError {} + +#[cfg(feature = "actors")] +/// `InternalServerError` for `actix::ResolverError` +/// This is supported on feature=`actors` only +impl ResponseError for actix::actors::resolver::ResolverError {} #[cfg(test)] mod tests { diff --git a/actix-http/src/extensions.rs b/actix-http/src/extensions.rs index d85ca184d..4e3918537 100644 --- a/actix-http/src/extensions.rs +++ b/actix-http/src/extensions.rs @@ -6,6 +6,8 @@ use fxhash::FxHashMap; #[derive(Default)] /// A type map of request extensions. pub struct Extensions { + /// Use FxHasher with a std HashMap with for faster + /// lookups on the small `TypeId` (u64 equivalent) keys. map: FxHashMap>, } @@ -28,33 +30,30 @@ impl Extensions { /// Check if container contains entry pub fn contains(&self) -> bool { - self.map.get(&TypeId::of::()).is_some() + self.map.contains_key(&TypeId::of::()) } /// Get a reference to a type previously inserted on this `Extensions`. pub fn get(&self) -> Option<&T> { self.map .get(&TypeId::of::()) - .and_then(|boxed| (&**boxed as &(dyn Any + 'static)).downcast_ref()) + .and_then(|boxed| boxed.downcast_ref()) } /// Get a mutable reference to a type previously inserted on this `Extensions`. pub fn get_mut(&mut self) -> Option<&mut T> { self.map .get_mut(&TypeId::of::()) - .and_then(|boxed| (&mut **boxed as &mut (dyn Any + 'static)).downcast_mut()) + .and_then(|boxed| boxed.downcast_mut()) } /// Remove a type from this `Extensions`. /// /// If a extension of this type existed, it will be returned. pub fn remove(&mut self) -> Option { - self.map.remove(&TypeId::of::()).and_then(|boxed| { - (boxed as Box) - .downcast() - .ok() - .map(|boxed| *boxed) - }) + self.map + .remove(&TypeId::of::()) + .and_then(|boxed| boxed.downcast().ok().map(|boxed| *boxed)) } /// Clear the `Extensions` of all inserted extensions. @@ -70,22 +69,113 @@ impl fmt::Debug for Extensions { } } -#[test] -fn test_extensions() { - #[derive(Debug, PartialEq)] - struct MyType(i32); +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_remove() { + let mut map = Extensions::new(); - let mut extensions = Extensions::new(); + map.insert::(123); + assert!(map.get::().is_some()); - extensions.insert(5i32); - extensions.insert(MyType(10)); + map.remove::(); + assert!(map.get::().is_none()); + } - assert_eq!(extensions.get(), Some(&5i32)); - assert_eq!(extensions.get_mut(), Some(&mut 5i32)); + #[test] + fn test_clear() { + let mut map = Extensions::new(); - assert_eq!(extensions.remove::(), Some(5i32)); - assert!(extensions.get::().is_none()); + map.insert::(8); + map.insert::(16); + map.insert::(32); - assert_eq!(extensions.get::(), None); - assert_eq!(extensions.get(), Some(&MyType(10))); + assert!(map.contains::()); + assert!(map.contains::()); + assert!(map.contains::()); + + map.clear(); + + assert!(!map.contains::()); + assert!(!map.contains::()); + assert!(!map.contains::()); + + map.insert::(10); + assert_eq!(*map.get::().unwrap(), 10); + } + + #[test] + fn test_integers() { + let mut map = Extensions::new(); + + map.insert::(8); + map.insert::(16); + map.insert::(32); + map.insert::(64); + map.insert::(128); + map.insert::(8); + map.insert::(16); + map.insert::(32); + map.insert::(64); + map.insert::(128); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + } + + #[test] + fn test_composition() { + struct Magi(pub T); + + struct Madoka { + pub god: bool, + } + + struct Homura { + pub attempts: usize, + } + + struct Mami { + pub guns: usize, + } + + let mut map = Extensions::new(); + + map.insert(Magi(Madoka { god: false })); + map.insert(Magi(Homura { attempts: 0 })); + map.insert(Magi(Mami { guns: 999 })); + + assert!(!map.get::>().unwrap().0.god); + assert_eq!(0, map.get::>().unwrap().0.attempts); + assert_eq!(999, map.get::>().unwrap().0.guns); + } + + #[test] + fn test_extensions() { + #[derive(Debug, PartialEq)] + struct MyType(i32); + + let mut extensions = Extensions::new(); + + extensions.insert(5i32); + extensions.insert(MyType(10)); + + assert_eq!(extensions.get(), Some(&5i32)); + assert_eq!(extensions.get_mut(), Some(&mut 5i32)); + + assert_eq!(extensions.remove::(), Some(5i32)); + assert!(extensions.get::().is_none()); + + assert_eq!(extensions.get::(), None); + assert_eq!(extensions.get(), Some(&MyType(10))); + } } diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index e113fd52d..c9d3edf33 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -8,7 +8,6 @@ use actix_codec::Decoder; use bytes::{Buf, Bytes, BytesMut}; use http::header::{HeaderName, HeaderValue}; use http::{header, Method, StatusCode, Uri, Version}; -use httparse; use log::{debug, error, trace}; use crate::error::ParseError; @@ -19,7 +18,7 @@ use crate::request::Request; const MAX_BUFFER_SIZE: usize = 131_072; const MAX_HEADERS: usize = 96; -/// Incoming messagd decoder +/// Incoming message decoder pub(crate) struct MessageDecoder(PhantomData); #[derive(Debug)] diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 6f4c09915..e16d3536f 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -10,6 +10,7 @@ use actix_service::Service; use bitflags::bitflags; use bytes::{Buf, BytesMut}; use log::{error, trace}; +use pin_project::pin_project; use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::cloneable::CloneableService; @@ -41,6 +42,7 @@ bitflags! { } } +#[pin_project::pin_project] /// Dispatcher for HTTP/1.1 protocol pub struct Dispatcher where @@ -52,9 +54,11 @@ where U: Service), Response = ()>, U::Error: fmt::Display, { + #[pin] inner: DispatcherState, } +#[pin_project(project = DispatcherStateProj)] enum DispatcherState where S: Service, @@ -65,11 +69,11 @@ where U: Service), Response = ()>, U::Error: fmt::Display, { - Normal(InnerDispatcher), - Upgrade(U::Future), - None, + Normal(#[pin] InnerDispatcher), + Upgrade(Pin>), } +#[pin_project(project = InnerDispatcherProj)] struct InnerDispatcher where S: Service, @@ -88,6 +92,7 @@ where peer_addr: Option, error: Option, + #[pin] state: State, payload: Option, messages: VecDeque, @@ -95,7 +100,7 @@ where ka_expire: Instant, ka_timer: Option, - io: T, + io: Option, read_buf: BytesMut, write_buf: BytesMut, codec: Codec, @@ -107,6 +112,7 @@ enum DispatcherMessage { Error(Response<()>), } +#[pin_project(project = StateProj)] enum State where S: Service, @@ -114,9 +120,9 @@ where B: MessageBody, { None, - ExpectCall(X::Future), - ServiceCall(S::Future), - SendPayload(ResponseBody), + ExpectCall(Pin>), + ServiceCall(Pin>), + SendPayload(#[pin] ResponseBody), } impl State @@ -141,7 +147,6 @@ where } } } - enum PollResponse { Upgrade(Request), DoNothing, @@ -236,7 +241,7 @@ where state: State::None, error: None, messages: VecDeque::new(), - io, + io: Some(io), codec, read_buf, service, @@ -278,29 +283,33 @@ where } // if checked is set to true, delay disconnect until all tasks have finished. - fn client_disconnected(&mut self) { - self.flags + fn client_disconnected(self: Pin<&mut Self>) { + let this = self.project(); + this.flags .insert(Flags::READ_DISCONNECT | Flags::WRITE_DISCONNECT); - if let Some(mut payload) = self.payload.take() { + if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::Incomplete(None)); } } /// Flush stream /// - /// true - got whouldblock - /// false - didnt get whouldblock - fn poll_flush(&mut self, cx: &mut Context<'_>) -> Result { + /// true - got WouldBlock + /// false - didn't get WouldBlock + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Result { if self.write_buf.is_empty() { return Ok(false); } let len = self.write_buf.len(); let mut written = 0; + let InnerDispatcherProj { io, write_buf, .. } = self.project(); + let mut io = Pin::new(io.as_mut().unwrap()); while written < len { - match unsafe { Pin::new_unchecked(&mut self.io) } - .poll_write(cx, &self.write_buf[written..]) - { + match io.as_mut().poll_write(cx, &write_buf[written..]) { Poll::Ready(Ok(0)) => { return Err(DispatchError::Io(io::Error::new( io::ErrorKind::WriteZero, @@ -312,112 +321,117 @@ where } Poll::Pending => { if written > 0 { - self.write_buf.advance(written); + write_buf.advance(written); } return Ok(true); } Poll::Ready(Err(err)) => return Err(DispatchError::Io(err)), } } - if written == self.write_buf.len() { - unsafe { self.write_buf.set_len(0) } + if written == write_buf.len() { + unsafe { write_buf.set_len(0) } } else { - self.write_buf.advance(written); + write_buf.advance(written); } Ok(false) } fn send_response( - &mut self, + self: Pin<&mut Self>, message: Response<()>, body: ResponseBody, ) -> Result, DispatchError> { - self.codec - .encode(Message::Item((message, body.size())), &mut self.write_buf) + let mut this = self.project(); + this.codec + .encode(Message::Item((message, body.size())), &mut this.write_buf) .map_err(|err| { - if let Some(mut payload) = self.payload.take() { + if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::Incomplete(None)); } DispatchError::Io(err) })?; - self.flags.set(Flags::KEEPALIVE, self.codec.keepalive()); + this.flags.set(Flags::KEEPALIVE, this.codec.keepalive()); match body.size() { BodySize::None | BodySize::Empty => Ok(State::None), _ => Ok(State::SendPayload(body)), } } - fn send_continue(&mut self) { - self.write_buf + fn send_continue(self: Pin<&mut Self>) { + self.project() + .write_buf .extend_from_slice(b"HTTP/1.1 100 Continue\r\n\r\n"); } fn poll_response( - &mut self, + mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Result { loop { - let state = match self.state { - State::None => match self.messages.pop_front() { + let mut this = self.as_mut().project(); + let state = match this.state.project() { + StateProj::None => match this.messages.pop_front() { Some(DispatcherMessage::Item(req)) => { - Some(self.handle_request(req, cx)?) - } - Some(DispatcherMessage::Error(res)) => { - Some(self.send_response(res, ResponseBody::Other(Body::Empty))?) + Some(self.as_mut().handle_request(req, cx)?) } + Some(DispatcherMessage::Error(res)) => Some( + self.as_mut() + .send_response(res, ResponseBody::Other(Body::Empty))?, + ), Some(DispatcherMessage::Upgrade(req)) => { return Ok(PollResponse::Upgrade(req)); } None => None, }, - State::ExpectCall(ref mut fut) => { - match unsafe { Pin::new_unchecked(fut) }.poll(cx) { - Poll::Ready(Ok(req)) => { - self.send_continue(); - self.state = State::ServiceCall(self.service.call(req)); - continue; - } - Poll::Ready(Err(e)) => { - let res: Response = e.into().into(); - let (res, body) = res.replace_body(()); - Some(self.send_response(res, body.into_body())?) - } - Poll::Pending => None, + StateProj::ExpectCall(fut) => match fut.as_mut().poll(cx) { + Poll::Ready(Ok(req)) => { + self.as_mut().send_continue(); + this = self.as_mut().project(); + this.state + .set(State::ServiceCall(Box::pin(this.service.call(req)))); + continue; } - } - State::ServiceCall(ref mut fut) => { - match unsafe { Pin::new_unchecked(fut) }.poll(cx) { - Poll::Ready(Ok(res)) => { - let (res, body) = res.into().replace_body(()); - self.state = self.send_response(res, body)?; - continue; - } - Poll::Ready(Err(e)) => { - let res: Response = e.into().into(); - let (res, body) = res.replace_body(()); - Some(self.send_response(res, body.into_body())?) - } - Poll::Pending => None, + Poll::Ready(Err(e)) => { + let res: Response = e.into().into(); + let (res, body) = res.replace_body(()); + Some(self.as_mut().send_response(res, body.into_body())?) } - } - State::SendPayload(ref mut stream) => { + Poll::Pending => None, + }, + StateProj::ServiceCall(fut) => match fut.as_mut().poll(cx) { + Poll::Ready(Ok(res)) => { + let (res, body) = res.into().replace_body(()); + let state = self.as_mut().send_response(res, body)?; + this = self.as_mut().project(); + this.state.set(state); + continue; + } + Poll::Ready(Err(e)) => { + let res: Response = e.into().into(); + let (res, body) = res.replace_body(()); + Some(self.as_mut().send_response(res, body.into_body())?) + } + Poll::Pending => None, + }, + StateProj::SendPayload(mut stream) => { loop { - if self.write_buf.len() < HW_BUFFER_SIZE { - match stream.poll_next(cx) { + if this.write_buf.len() < HW_BUFFER_SIZE { + match stream.as_mut().poll_next(cx) { Poll::Ready(Some(Ok(item))) => { - self.codec.encode( + this.codec.encode( Message::Chunk(Some(item)), - &mut self.write_buf, + &mut this.write_buf, )?; continue; } Poll::Ready(None) => { - self.codec.encode( + this.codec.encode( Message::Chunk(None), - &mut self.write_buf, + &mut this.write_buf, )?; - self.state = State::None; + this = self.as_mut().project(); + this.state.set(State::None); } Poll::Ready(Some(Err(_))) => { return Err(DispatchError::Unknown) @@ -433,9 +447,11 @@ where } }; + this = self.as_mut().project(); + // set new state if let Some(state) = state { - self.state = state; + this.state.set(state); if !self.state.is_empty() { continue; } @@ -443,7 +459,7 @@ where // if read-backpressure is enabled and we consumed some data. // we may read more data and retry if self.state.is_call() { - if self.poll_request(cx)? { + if self.as_mut().poll_request(cx)? { continue; } } else if !self.messages.is_empty() { @@ -457,16 +473,16 @@ where } fn handle_request( - &mut self, + mut self: Pin<&mut Self>, req: Request, cx: &mut Context<'_>, ) -> Result, DispatchError> { // Handle `EXPECT: 100-Continue` header let req = if req.head().expect() { - let mut task = self.expect.call(req); - match unsafe { Pin::new_unchecked(&mut task) }.poll(cx) { + let mut task = Box::pin(self.as_mut().project().expect.call(req)); + match task.as_mut().poll(cx) { Poll::Ready(Ok(req)) => { - self.send_continue(); + self.as_mut().send_continue(); req } Poll::Pending => return Ok(State::ExpectCall(task)), @@ -482,8 +498,8 @@ where }; // Call service - let mut task = self.service.call(req); - match unsafe { Pin::new_unchecked(&mut task) }.poll(cx) { + let mut task = Box::pin(self.as_mut().project().service.call(req)); + match task.as_mut().poll(cx) { Poll::Ready(Ok(res)) => { let (res, body) = res.into().replace_body(()); self.send_response(res, body) @@ -499,7 +515,7 @@ where /// Process one incoming requests pub(self) fn poll_request( - &mut self, + mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Result { // limit a mount of non processed requests @@ -508,24 +524,25 @@ where } let mut updated = false; + let mut this = self.as_mut().project(); loop { - match self.codec.decode(&mut self.read_buf) { + match this.codec.decode(&mut this.read_buf) { Ok(Some(msg)) => { updated = true; - self.flags.insert(Flags::STARTED); + this.flags.insert(Flags::STARTED); match msg { Message::Item(mut req) => { - let pl = self.codec.message_type(); - req.head_mut().peer_addr = self.peer_addr; + let pl = this.codec.message_type(); + req.head_mut().peer_addr = *this.peer_addr; // set on_connect data - if let Some(ref on_connect) = self.on_connect { + if let Some(ref on_connect) = this.on_connect { on_connect.set(&mut req.extensions_mut()); } - if pl == MessageType::Stream && self.upgrade.is_some() { - self.messages.push_back(DispatcherMessage::Upgrade(req)); + if pl == MessageType::Stream && this.upgrade.is_some() { + this.messages.push_back(DispatcherMessage::Upgrade(req)); break; } if pl == MessageType::Payload || pl == MessageType::Stream { @@ -533,41 +550,43 @@ where let (req1, _) = req.replace_payload(crate::Payload::H1(pl)); req = req1; - self.payload = Some(ps); + *this.payload = Some(ps); } // handle request early - if self.state.is_empty() { - self.state = self.handle_request(req, cx)?; + if this.state.is_empty() { + let state = self.as_mut().handle_request(req, cx)?; + this = self.as_mut().project(); + this.state.set(state); } else { - self.messages.push_back(DispatcherMessage::Item(req)); + this.messages.push_back(DispatcherMessage::Item(req)); } } Message::Chunk(Some(chunk)) => { - if let Some(ref mut payload) = self.payload { + if let Some(ref mut payload) = this.payload { payload.feed_data(chunk); } else { error!( "Internal server error: unexpected payload chunk" ); - self.flags.insert(Flags::READ_DISCONNECT); - self.messages.push_back(DispatcherMessage::Error( + this.flags.insert(Flags::READ_DISCONNECT); + this.messages.push_back(DispatcherMessage::Error( Response::InternalServerError().finish().drop_body(), )); - self.error = Some(DispatchError::InternalError); + *this.error = Some(DispatchError::InternalError); break; } } Message::Chunk(None) => { - if let Some(mut payload) = self.payload.take() { + if let Some(mut payload) = this.payload.take() { payload.feed_eof(); } else { error!("Internal server error: unexpected eof"); - self.flags.insert(Flags::READ_DISCONNECT); - self.messages.push_back(DispatcherMessage::Error( + this.flags.insert(Flags::READ_DISCONNECT); + this.messages.push_back(DispatcherMessage::Error( Response::InternalServerError().finish().drop_body(), )); - self.error = Some(DispatchError::InternalError); + *this.error = Some(DispatchError::InternalError); break; } } @@ -575,44 +594,49 @@ where } Ok(None) => break, Err(ParseError::Io(e)) => { - self.client_disconnected(); - self.error = Some(DispatchError::Io(e)); + self.as_mut().client_disconnected(); + this = self.as_mut().project(); + *this.error = Some(DispatchError::Io(e)); break; } Err(e) => { - if let Some(mut payload) = self.payload.take() { + if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::EncodingCorrupted); } // Malformed requests should be responded with 400 - self.messages.push_back(DispatcherMessage::Error( + this.messages.push_back(DispatcherMessage::Error( Response::BadRequest().finish().drop_body(), )); - self.flags.insert(Flags::READ_DISCONNECT); - self.error = Some(e.into()); + this.flags.insert(Flags::READ_DISCONNECT); + *this.error = Some(e.into()); break; } } } - if updated && self.ka_timer.is_some() { - if let Some(expire) = self.codec.config().keep_alive_expire() { - self.ka_expire = expire; + if updated && this.ka_timer.is_some() { + if let Some(expire) = this.codec.config().keep_alive_expire() { + *this.ka_expire = expire; } } Ok(updated) } /// keep-alive timer - fn poll_keepalive(&mut self, cx: &mut Context<'_>) -> Result<(), DispatchError> { - if self.ka_timer.is_none() { + fn poll_keepalive( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Result<(), DispatchError> { + let mut this = self.as_mut().project(); + if this.ka_timer.is_none() { // shutdown timeout - if self.flags.contains(Flags::SHUTDOWN) { - if let Some(interval) = self.codec.config().client_disconnect_timer() { - self.ka_timer = Some(delay_until(interval)); + if this.flags.contains(Flags::SHUTDOWN) { + if let Some(interval) = this.codec.config().client_disconnect_timer() { + *this.ka_timer = Some(delay_until(interval)); } else { - self.flags.insert(Flags::READ_DISCONNECT); - if let Some(mut payload) = self.payload.take() { + this.flags.insert(Flags::READ_DISCONNECT); + if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::Incomplete(None)); } return Ok(()); @@ -622,55 +646,56 @@ where } } - match Pin::new(&mut self.ka_timer.as_mut().unwrap()).poll(cx) { + match Pin::new(&mut this.ka_timer.as_mut().unwrap()).poll(cx) { Poll::Ready(()) => { // if we get timeout during shutdown, drop connection - if self.flags.contains(Flags::SHUTDOWN) { + if this.flags.contains(Flags::SHUTDOWN) { return Err(DispatchError::DisconnectTimeout); - } else if self.ka_timer.as_mut().unwrap().deadline() >= self.ka_expire { + } else if this.ka_timer.as_mut().unwrap().deadline() >= *this.ka_expire { // check for any outstanding tasks - if self.state.is_empty() && self.write_buf.is_empty() { - if self.flags.contains(Flags::STARTED) { + if this.state.is_empty() && this.write_buf.is_empty() { + if this.flags.contains(Flags::STARTED) { trace!("Keep-alive timeout, close connection"); - self.flags.insert(Flags::SHUTDOWN); + this.flags.insert(Flags::SHUTDOWN); // start shutdown timer if let Some(deadline) = - self.codec.config().client_disconnect_timer() + this.codec.config().client_disconnect_timer() { - if let Some(mut timer) = self.ka_timer.as_mut() { + if let Some(mut timer) = this.ka_timer.as_mut() { timer.reset(deadline); let _ = Pin::new(&mut timer).poll(cx); } } else { // no shutdown timeout, drop socket - self.flags.insert(Flags::WRITE_DISCONNECT); + this.flags.insert(Flags::WRITE_DISCONNECT); return Ok(()); } } else { // timeout on first request (slow request) return 408 - if !self.flags.contains(Flags::STARTED) { + if !this.flags.contains(Flags::STARTED) { trace!("Slow request timeout"); - let _ = self.send_response( + let _ = self.as_mut().send_response( Response::RequestTimeout().finish().drop_body(), ResponseBody::Other(Body::Empty), ); + this = self.as_mut().project(); } else { trace!("Keep-alive connection timeout"); } - self.flags.insert(Flags::STARTED | Flags::SHUTDOWN); - self.state = State::None; + this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); + this.state.set(State::None); } } else if let Some(deadline) = - self.codec.config().keep_alive_expire() + this.codec.config().keep_alive_expire() { - if let Some(mut timer) = self.ka_timer.as_mut() { + if let Some(mut timer) = this.ka_timer.as_mut() { timer.reset(deadline); let _ = Pin::new(&mut timer).poll(cx); } } - } else if let Some(mut timer) = self.ka_timer.as_mut() { - timer.reset(self.ka_expire); + } else if let Some(mut timer) = this.ka_timer.as_mut() { + timer.reset(*this.ka_expire); let _ = Pin::new(&mut timer).poll(cx); } } @@ -681,20 +706,6 @@ where } } -impl Unpin for Dispatcher -where - T: AsyncRead + AsyncWrite + Unpin, - S: Service, - S::Error: Into, - S::Response: Into>, - B: MessageBody, - X: Service, - X::Error: Into, - U: Service), Response = ()>, - U::Error: fmt::Display, -{ -} - impl Future for Dispatcher where T: AsyncRead + AsyncWrite + Unpin, @@ -711,20 +722,25 @@ where #[inline] fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - match self.as_mut().inner { - DispatcherState::Normal(ref mut inner) => { - inner.poll_keepalive(cx)?; + let this = self.as_mut().project(); + match this.inner.project() { + DispatcherStateProj::Normal(mut inner) => { + inner.as_mut().poll_keepalive(cx)?; if inner.flags.contains(Flags::SHUTDOWN) { if inner.flags.contains(Flags::WRITE_DISCONNECT) { Poll::Ready(Ok(())) } else { // flush buffer - inner.poll_flush(cx)?; - if !inner.write_buf.is_empty() { + inner.as_mut().poll_flush(cx)?; + if !inner.write_buf.is_empty() || inner.io.is_none() { Poll::Pending } else { - match Pin::new(&mut inner.io).poll_shutdown(cx) { + match Pin::new(inner.project().io) + .as_pin_mut() + .unwrap() + .poll_shutdown(cx) + { Poll::Ready(res) => { Poll::Ready(res.map_err(DispatchError::from)) } @@ -736,53 +752,58 @@ where // read socket into a buf let should_disconnect = if !inner.flags.contains(Flags::READ_DISCONNECT) { - read_available(cx, &mut inner.io, &mut inner.read_buf)? + let mut inner_p = inner.as_mut().project(); + read_available( + cx, + inner_p.io.as_mut().unwrap(), + &mut inner_p.read_buf, + )? } else { None }; - inner.poll_request(cx)?; + inner.as_mut().poll_request(cx)?; if let Some(true) = should_disconnect { - inner.flags.insert(Flags::READ_DISCONNECT); - if let Some(mut payload) = inner.payload.take() { + let inner_p = inner.as_mut().project(); + inner_p.flags.insert(Flags::READ_DISCONNECT); + if let Some(mut payload) = inner_p.payload.take() { payload.feed_eof(); } }; loop { + let inner_p = inner.as_mut().project(); let remaining = - inner.write_buf.capacity() - inner.write_buf.len(); + inner_p.write_buf.capacity() - inner_p.write_buf.len(); if remaining < LW_BUFFER_SIZE { - inner.write_buf.reserve(HW_BUFFER_SIZE - remaining); + inner_p.write_buf.reserve(HW_BUFFER_SIZE - remaining); } - let result = inner.poll_response(cx)?; + let result = inner.as_mut().poll_response(cx)?; let drain = result == PollResponse::DrainWriteBuf; // switch to upgrade handler if let PollResponse::Upgrade(req) = result { - if let DispatcherState::Normal(inner) = - std::mem::replace(&mut self.inner, DispatcherState::None) - { - let mut parts = FramedParts::with_read_buf( - inner.io, - inner.codec, - inner.read_buf, - ); - parts.write_buf = inner.write_buf; - let framed = Framed::from_parts(parts); - self.inner = DispatcherState::Upgrade( - inner.upgrade.unwrap().call((req, framed)), - ); - return self.poll(cx); - } else { - panic!() - } + let inner_p = inner.as_mut().project(); + let mut parts = FramedParts::with_read_buf( + inner_p.io.take().unwrap(), + std::mem::take(inner_p.codec), + std::mem::take(inner_p.read_buf), + ); + parts.write_buf = std::mem::take(inner_p.write_buf); + let framed = Framed::from_parts(parts); + let upgrade = + inner_p.upgrade.take().unwrap().call((req, framed)); + self.as_mut() + .project() + .inner + .set(DispatcherState::Upgrade(Box::pin(upgrade))); + return self.poll(cx); } - // we didnt get WouldBlock from write operation, + // we didn't get WouldBlock from write operation, // so data get written to kernel completely (OSX) // and we have to write again otherwise response can get stuck - if inner.poll_flush(cx)? || !drain { + if inner.as_mut().poll_flush(cx)? || !drain { break; } } @@ -794,25 +815,26 @@ where let is_empty = inner.state.is_empty(); + let inner_p = inner.as_mut().project(); // read half is closed and we do not processing any responses - if inner.flags.contains(Flags::READ_DISCONNECT) && is_empty { - inner.flags.insert(Flags::SHUTDOWN); + if inner_p.flags.contains(Flags::READ_DISCONNECT) && is_empty { + inner_p.flags.insert(Flags::SHUTDOWN); } // keep-alive and stream errors - if is_empty && inner.write_buf.is_empty() { - if let Some(err) = inner.error.take() { + if is_empty && inner_p.write_buf.is_empty() { + if let Some(err) = inner_p.error.take() { Poll::Ready(Err(err)) } // disconnect if keep-alive is not enabled - else if inner.flags.contains(Flags::STARTED) - && !inner.flags.intersects(Flags::KEEPALIVE) + else if inner_p.flags.contains(Flags::STARTED) + && !inner_p.flags.intersects(Flags::KEEPALIVE) { - inner.flags.insert(Flags::SHUTDOWN); + inner_p.flags.insert(Flags::SHUTDOWN); self.poll(cx) } // disconnect if shutdown - else if inner.flags.contains(Flags::SHUTDOWN) { + else if inner_p.flags.contains(Flags::SHUTDOWN) { self.poll(cx) } else { Poll::Pending @@ -822,13 +844,10 @@ where } } } - DispatcherState::Upgrade(ref mut fut) => { - unsafe { Pin::new_unchecked(fut) }.poll(cx).map_err(|e| { - error!("Upgrade handler error: {}", e); - DispatchError::Upgrade - }) - } - DispatcherState::None => panic!(), + DispatcherStateProj::Upgrade(fut) => fut.as_mut().poll(cx).map_err(|e| { + error!("Upgrade handler error: {}", e); + DispatchError::Upgrade + }), } } } @@ -918,9 +937,12 @@ mod tests { Poll::Ready(res) => assert!(res.is_err()), } - if let DispatcherState::Normal(ref inner) = h1.inner { + if let DispatcherState::Normal(ref mut inner) = h1.inner { assert!(inner.flags.contains(Flags::READ_DISCONNECT)); - assert_eq!(&inner.io.write_buf[..26], b"HTTP/1.1 400 Bad Request\r\n"); + assert_eq!( + &inner.io.take().unwrap().write_buf[..26], + b"HTTP/1.1 400 Bad Request\r\n" + ); } }) .await; diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index 4689906b4..eb8c337dd 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -4,7 +4,7 @@ use std::ptr::copy_nonoverlapping; use std::slice::from_raw_parts_mut; use std::{cmp, io}; -use bytes::{buf::BufMutExt, BufMut, BytesMut}; +use bytes::{BufMut, BytesMut}; use crate::body::BodySize; use crate::config::ServiceConfig; @@ -95,15 +95,6 @@ pub(crate) trait MessageType: Sized { } } BodySize::Sized(len) => helpers::write_content_length(len, dst), - BodySize::Sized64(len) => { - if camel_case { - dst.put_slice(b"\r\nContent-Length: "); - } else { - dst.put_slice(b"\r\ncontent-length: "); - } - #[allow(clippy::write_with_newline)] - write!(dst.writer(), "{}\r\n", len)?; - } BodySize::None => dst.put_slice(b"\r\n"), } @@ -338,8 +329,7 @@ impl MessageEncoder { if !head { self.te = match length { BodySize::Empty => TransferEncoding::empty(), - BodySize::Sized(len) => TransferEncoding::length(len as u64), - BodySize::Sized64(len) => TransferEncoding::length(len), + BodySize::Sized(len) => TransferEncoding::length(len), BodySize::Stream => { if message.chunked() && !stream { TransferEncoding::chunked() @@ -582,19 +572,6 @@ mod tests { assert!(data.contains("Content-Type: plain/text\r\n")); assert!(data.contains("Date: date\r\n")); - let _ = head.encode_headers( - &mut bytes, - Version::HTTP_11, - BodySize::Sized64(100), - ConnectionType::KeepAlive, - &ServiceConfig::default(), - ); - let data = - String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap(); - assert!(data.contains("Content-Length: 100\r\n")); - assert!(data.contains("Content-Type: plain/text\r\n")); - assert!(data.contains("Date: date\r\n")); - let mut head = RequestHead::default(); head.set_camel_case_headers(false); head.headers.insert(DATE, HeaderValue::from_static("date")); diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index 9ba4aa053..c44925c7a 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -13,6 +13,7 @@ use crate::response::Response; #[pin_project::pin_project] pub struct SendResponse { res: Option, BodySize)>>, + #[pin] body: Option>, framed: Option>, } @@ -35,24 +36,27 @@ where impl Future for SendResponse where T: AsyncRead + AsyncWrite, - B: MessageBody, + B: MessageBody + Unpin, { type Output = Result, Error>; + // TODO: rethink if we need loops in polls fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.get_mut(); + let mut this = self.project(); + let mut body_done = this.body.is_none(); loop { - let mut body_ready = this.body.is_some(); + let mut body_ready = !body_done; let framed = this.framed.as_mut().unwrap(); // send body - if this.res.is_none() && this.body.is_some() { - while body_ready && this.body.is_some() && !framed.is_write_buf_full() { - match this.body.as_mut().unwrap().poll_next(cx)? { + if this.res.is_none() && body_ready { + while body_ready && !body_done && !framed.is_write_buf_full() { + match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx)? { Poll::Ready(item) => { - // body is done - if item.is_none() { + // body is done when item is None + body_done = item.is_none(); + if body_done { let _ = this.body.take(); } framed.write(Message::Chunk(item))?; @@ -82,7 +86,7 @@ where continue; } - if this.body.is_some() { + if !body_done { if body_ready { continue; } else { diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 8b17e9479..33fb3a814 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -165,10 +165,10 @@ struct ServiceResponse { _t: PhantomData<(I, E)>, } -#[pin_project::pin_project] +#[pin_project::pin_project(project = ServiceResponseStateProj)] enum ServiceResponseState { ServiceCall(#[pin] F, Option>), - SendPayload(SendStream, ResponseBody), + SendPayload(SendStream, #[pin] ResponseBody), } impl ServiceResponse @@ -210,10 +210,6 @@ where CONTENT_LENGTH, HeaderValue::try_from(format!("{}", len)).unwrap(), ), - BodySize::Sized64(len) => res.headers_mut().insert( - CONTENT_LENGTH, - HeaderValue::try_from(format!("{}", len)).unwrap(), - ), }; // copy headers @@ -249,70 +245,65 @@ where { type Output = (); - #[pin_project::project] fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let mut this = self.as_mut().project(); - #[project] match this.state.project() { - ServiceResponseState::ServiceCall(call, send) => { - match call.poll(cx) { - Poll::Ready(Ok(res)) => { - let (res, body) = res.into().replace_body(()); + ServiceResponseStateProj::ServiceCall(call, send) => match call.poll(cx) { + Poll::Ready(Ok(res)) => { + let (res, body) = res.into().replace_body(()); - let mut send = send.take().unwrap(); - let mut size = body.size(); - let h2_res = - self.as_mut().prepare_response(res.head(), &mut size); - this = self.as_mut().project(); + let mut send = send.take().unwrap(); + let mut size = body.size(); + let h2_res = self.as_mut().prepare_response(res.head(), &mut size); + this = self.as_mut().project(); - let stream = match send.send_response(h2_res, size.is_eof()) { - Err(e) => { - trace!("Error sending h2 response: {:?}", e); - return Poll::Ready(()); - } - Ok(stream) => stream, - }; - - if size.is_eof() { - Poll::Ready(()) - } else { - this.state.set(ServiceResponseState::SendPayload(stream, body)); - self.poll(cx) + let stream = match send.send_response(h2_res, size.is_eof()) { + Err(e) => { + trace!("Error sending h2 response: {:?}", e); + return Poll::Ready(()); } - } - Poll::Pending => Poll::Pending, - Poll::Ready(Err(e)) => { - let res: Response = e.into().into(); - let (res, body) = res.replace_body(()); + Ok(stream) => stream, + }; - let mut send = send.take().unwrap(); - let mut size = body.size(); - let h2_res = - self.as_mut().prepare_response(res.head(), &mut size); - this = self.as_mut().project(); - - let stream = match send.send_response(h2_res, size.is_eof()) { - Err(e) => { - trace!("Error sending h2 response: {:?}", e); - return Poll::Ready(()); - } - Ok(stream) => stream, - }; - - if size.is_eof() { - Poll::Ready(()) - } else { - this.state.set(ServiceResponseState::SendPayload( - stream, - body.into_body(), - )); - self.poll(cx) - } + if size.is_eof() { + Poll::Ready(()) + } else { + this.state + .set(ServiceResponseState::SendPayload(stream, body)); + self.poll(cx) } } - } - ServiceResponseState::SendPayload(ref mut stream, ref mut body) => loop { + Poll::Pending => Poll::Pending, + Poll::Ready(Err(e)) => { + let res: Response = e.into().into(); + let (res, body) = res.replace_body(()); + + let mut send = send.take().unwrap(); + let mut size = body.size(); + let h2_res = self.as_mut().prepare_response(res.head(), &mut size); + this = self.as_mut().project(); + + let stream = match send.send_response(h2_res, size.is_eof()) { + Err(e) => { + trace!("Error sending h2 response: {:?}", e); + return Poll::Ready(()); + } + Ok(stream) => stream, + }; + + if size.is_eof() { + Poll::Ready(()) + } else { + this.state.set(ServiceResponseState::SendPayload( + stream, + body.into_body(), + )); + self.poll(cx) + } + } + }, + ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => loop { loop { if let Some(ref mut buffer) = this.buffer { match stream.poll_capacity(cx) { @@ -338,7 +329,7 @@ where } } } else { - match body.poll_next(cx) { + match body.as_mut().poll_next(cx) { Poll::Pending => return Poll::Pending, Poll::Ready(None) => { if let Err(e) = stream.send_data(Bytes::new(), true) { diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index ff3f69faf..eef5dd02c 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -83,13 +83,11 @@ where Error = DispatchError, InitError = S::InitError, > { - pipeline_factory(fn_factory(|| { - async { - Ok::<_, S::InitError>(fn_service(|io: TcpStream| { - let peer_addr = io.peer_addr().ok(); - ok::<_, DispatchError>((io, peer_addr)) - })) - } + pipeline_factory(fn_factory(|| async { + Ok::<_, S::InitError>(fn_service(|io: TcpStream| { + let peer_addr = io.peer_addr().ok(); + ok::<_, DispatchError>((io, peer_addr)) + })) })) .and_then(self) } diff --git a/actix-http/src/header/common/accept_charset.rs b/actix-http/src/header/common/accept_charset.rs index 117e2015d..291ca53b6 100644 --- a/actix-http/src/header/common/accept_charset.rs +++ b/actix-http/src/header/common/accept_charset.rs @@ -63,7 +63,7 @@ header! { (AcceptCharset, ACCEPT_CHARSET) => (QualityItem)+ test_accept_charset { - /// Test case from RFC + // Test case from RFC test_header!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); } } diff --git a/actix-http/src/header/common/content_disposition.rs b/actix-http/src/header/common/content_disposition.rs index d0d5af765..d65933901 100644 --- a/actix-http/src/header/common/content_disposition.rs +++ b/actix-http/src/header/common/content_disposition.rs @@ -90,40 +90,40 @@ pub enum DispositionParam { /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *token "=" value*. Recipients should /// ignore unrecognizable parameters. Unknown(String, String), - /// An unrecognized extended paramater as defined in + /// An unrecognized extended parameter as defined in /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *ext-parameter*, in /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *ext-token "=" ext-value*. The single - /// trailling asterisk is not included. Recipients should ignore unrecognizable parameters. + /// trailing asterisk is not included. Recipients should ignore unrecognizable parameters. UnknownExt(String, ExtendedValue), } impl DispositionParam { - /// Returns `true` if the paramater is [`Name`](DispositionParam::Name). + /// Returns `true` if the parameter is [`Name`](DispositionParam::Name). #[inline] pub fn is_name(&self) -> bool { self.as_name().is_some() } - /// Returns `true` if the paramater is [`Filename`](DispositionParam::Filename). + /// Returns `true` if the parameter is [`Filename`](DispositionParam::Filename). #[inline] pub fn is_filename(&self) -> bool { self.as_filename().is_some() } - /// Returns `true` if the paramater is [`FilenameExt`](DispositionParam::FilenameExt). + /// Returns `true` if the parameter is [`FilenameExt`](DispositionParam::FilenameExt). #[inline] pub fn is_filename_ext(&self) -> bool { self.as_filename_ext().is_some() } - /// Returns `true` if the paramater is [`Unknown`](DispositionParam::Unknown) and the `name` + /// Returns `true` if the parameter is [`Unknown`](DispositionParam::Unknown) and the `name` #[inline] /// matches. pub fn is_unknown>(&self, name: T) -> bool { self.as_unknown(name).is_some() } - /// Returns `true` if the paramater is [`UnknownExt`](DispositionParam::UnknownExt) and the + /// Returns `true` if the parameter is [`UnknownExt`](DispositionParam::UnknownExt) and the /// `name` matches. #[inline] pub fn is_unknown_ext>(&self, name: T) -> bool { @@ -373,7 +373,7 @@ impl ContentDisposition { let param = if param_name.eq_ignore_ascii_case("name") { DispositionParam::Name(value) } else if param_name.eq_ignore_ascii_case("filename") { - // See also comments in test_from_raw_uncessary_percent_decode. + // See also comments in test_from_raw_unnecessary_percent_decode. DispositionParam::Filename(value) } else { DispositionParam::Unknown(param_name.to_owned(), value) @@ -423,7 +423,7 @@ impl ContentDisposition { /// Return the value of *name* if exists. pub fn get_name(&self) -> Option<&str> { - self.parameters.iter().filter_map(|p| p.as_name()).nth(0) + self.parameters.iter().filter_map(|p| p.as_name()).next() } /// Return the value of *filename* if exists. @@ -431,7 +431,7 @@ impl ContentDisposition { self.parameters .iter() .filter_map(|p| p.as_filename()) - .nth(0) + .next() } /// Return the value of *filename\** if exists. @@ -439,7 +439,7 @@ impl ContentDisposition { self.parameters .iter() .filter_map(|p| p.as_filename_ext()) - .nth(0) + .next() } /// Return the value of the parameter which the `name` matches. @@ -448,7 +448,7 @@ impl ContentDisposition { self.parameters .iter() .filter_map(|p| p.as_unknown(name)) - .nth(0) + .next() } /// Return the value of the extended parameter which the `name` matches. @@ -457,7 +457,7 @@ impl ContentDisposition { self.parameters .iter() .filter_map(|p| p.as_unknown_ext(name)) - .nth(0) + .next() } } @@ -529,7 +529,7 @@ impl fmt::Display for DispositionParam { // ; to use within parameter values // // - // See also comments in test_from_raw_uncessary_percent_decode. + // See also comments in test_from_raw_unnecessary_percent_decode. lazy_static! { static ref RE: Regex = Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap(); } @@ -676,7 +676,7 @@ mod tests { fn test_from_raw_unordered() { let a = HeaderValue::from_static( "form-data; dummy=3; filename=\"sample.png\" ; name=upload;", - // Actually, a trailling semolocon is not compliant. But it is fine to accept. + // Actually, a trailing semicolon is not compliant. But it is fine to accept. ); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { @@ -833,7 +833,7 @@ mod tests { } #[test] - fn test_from_raw_uncessary_percent_decode() { + fn test_from_raw_unnecessary_percent_decode() { // In fact, RFC7578 (multipart/form-data) Section 2 and 4.2 suggests that filename with // non-ASCII characters MAY be percent-encoded. // On the contrary, RFC6266 or other RFCs related to Content-Disposition response header diff --git a/actix-http/src/header/common/range.rs b/actix-http/src/header/common/range.rs index 71718fc7a..f9e203bb2 100644 --- a/actix-http/src/header/common/range.rs +++ b/actix-http/src/header/common/range.rs @@ -7,7 +7,7 @@ use header::{Header, Raw}; /// `Range` header, defined in [RFC7233](https://tools.ietf.org/html/rfc7233#section-3.1) /// /// The "Range" header field on a GET request modifies the method -/// semantics to request transfer of only one or more subranges of the +/// semantics to request transfer of only one or more sub-ranges of the /// selected representation data, rather than the entire selected /// representation data. /// @@ -183,13 +183,13 @@ impl fmt::Display for Range { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Range::Bytes(ref ranges) => { - try!(write!(f, "bytes=")); + write!(f, "bytes=")?; for (i, range) in ranges.iter().enumerate() { if i != 0 { - try!(f.write_str(",")); + f.write_str(",")?; } - try!(Display::fmt(range, f)); + Display::fmt(range, f)?; } Ok(()) } @@ -214,9 +214,9 @@ impl FromStr for Range { } Ok(Range::Bytes(ranges)) } - (Some(unit), Some(range_str)) if unit != "" && range_str != "" => Ok( - Range::Unregistered(unit.to_owned(), range_str.to_owned()), - ), + (Some(unit), Some(range_str)) if unit != "" && range_str != "" => { + Ok(Range::Unregistered(unit.to_owned(), range_str.to_owned())) + } _ => Err(::Error::Header), } } @@ -229,7 +229,8 @@ impl FromStr for ByteRangeSpec { let mut parts = s.splitn(2, '-'); match (parts.next(), parts.next()) { - (Some(""), Some(end)) => end.parse() + (Some(""), Some(end)) => end + .parse() .or(Err(::Error::Header)) .map(ByteRangeSpec::Last), (Some(start), Some("")) => start @@ -272,163 +273,138 @@ impl Header for Range { } } -#[test] -fn test_parse_bytes_range_valid() { - let r: Range = Header::parse_header(&"bytes=1-100".into()).unwrap(); - let r2: Range = Header::parse_header(&"bytes=1-100,-".into()).unwrap(); - let r3 = Range::bytes(1, 100); - assert_eq!(r, r2); - assert_eq!(r2, r3); +#[cfg(test)] +mod tests { + use super::*; - let r: Range = Header::parse_header(&"bytes=1-100,200-".into()).unwrap(); - let r2: Range = - Header::parse_header(&"bytes= 1-100 , 101-xxx, 200- ".into()).unwrap(); - let r3 = Range::Bytes(vec![ - ByteRangeSpec::FromTo(1, 100), - ByteRangeSpec::AllFrom(200), - ]); - assert_eq!(r, r2); - assert_eq!(r2, r3); + #[test] + fn test_parse_bytes_range_valid() { + let r: Range = Header::parse_header(&"bytes=1-100".into()).unwrap(); + let r2: Range = Header::parse_header(&"bytes=1-100,-".into()).unwrap(); + let r3 = Range::bytes(1, 100); + assert_eq!(r, r2); + assert_eq!(r2, r3); - let r: Range = Header::parse_header(&"bytes=1-100,-100".into()).unwrap(); - let r2: Range = Header::parse_header(&"bytes=1-100, ,,-100".into()).unwrap(); - let r3 = Range::Bytes(vec![ - ByteRangeSpec::FromTo(1, 100), - ByteRangeSpec::Last(100), - ]); - assert_eq!(r, r2); - assert_eq!(r2, r3); + let r: Range = Header::parse_header(&"bytes=1-100,200-".into()).unwrap(); + let r2: Range = + Header::parse_header(&"bytes= 1-100 , 101-xxx, 200- ".into()).unwrap(); + let r3 = Range::Bytes(vec![ + ByteRangeSpec::FromTo(1, 100), + ByteRangeSpec::AllFrom(200), + ]); + assert_eq!(r, r2); + assert_eq!(r2, r3); - let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); - assert_eq!(r, r2); -} - -#[test] -fn test_parse_unregistered_range_valid() { - let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); - assert_eq!(r, r2); - - let r: Range = Header::parse_header(&"custom=abcd".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "abcd".to_owned()); - assert_eq!(r, r2); - - let r: Range = Header::parse_header(&"custom=xxx-yyy".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "xxx-yyy".to_owned()); - assert_eq!(r, r2); -} - -#[test] -fn test_parse_invalid() { - let r: ::Result = Header::parse_header(&"bytes=1-a,-".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"bytes=1-2-3".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"abc".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"bytes=1-100=".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"bytes=".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"custom=".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"=1-100".into()); - assert_eq!(r.ok(), None); -} - -#[test] -fn test_fmt() { - use header::Headers; - - let mut headers = Headers::new(); - - headers.set(Range::Bytes(vec![ - ByteRangeSpec::FromTo(0, 1000), - ByteRangeSpec::AllFrom(2000), - ])); - assert_eq!(&headers.to_string(), "Range: bytes=0-1000,2000-\r\n"); - - headers.clear(); - headers.set(Range::Bytes(vec![])); - - assert_eq!(&headers.to_string(), "Range: bytes=\r\n"); - - headers.clear(); - headers.set(Range::Unregistered( - "custom".to_owned(), - "1-xxx".to_owned(), - )); - - assert_eq!(&headers.to_string(), "Range: custom=1-xxx\r\n"); -} - -#[test] -fn test_byte_range_spec_to_satisfiable_range() { - assert_eq!( - Some((0, 0)), - ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(3) - ); - assert_eq!( - Some((1, 2)), - ByteRangeSpec::FromTo(1, 2).to_satisfiable_range(3) - ); - assert_eq!( - Some((1, 2)), - ByteRangeSpec::FromTo(1, 5).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::FromTo(3, 3).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::FromTo(2, 1).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(0) - ); - - assert_eq!( - Some((0, 2)), - ByteRangeSpec::AllFrom(0).to_satisfiable_range(3) - ); - assert_eq!( - Some((2, 2)), - ByteRangeSpec::AllFrom(2).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::AllFrom(3).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::AllFrom(5).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::AllFrom(0).to_satisfiable_range(0) - ); - - assert_eq!( - Some((1, 2)), - ByteRangeSpec::Last(2).to_satisfiable_range(3) - ); - assert_eq!( - Some((2, 2)), - ByteRangeSpec::Last(1).to_satisfiable_range(3) - ); - assert_eq!( - Some((0, 2)), - ByteRangeSpec::Last(5).to_satisfiable_range(3) - ); - assert_eq!(None, ByteRangeSpec::Last(0).to_satisfiable_range(3)); - assert_eq!(None, ByteRangeSpec::Last(2).to_satisfiable_range(0)); + let r: Range = Header::parse_header(&"bytes=1-100,-100".into()).unwrap(); + let r2: Range = Header::parse_header(&"bytes=1-100, ,,-100".into()).unwrap(); + let r3 = Range::Bytes(vec![ + ByteRangeSpec::FromTo(1, 100), + ByteRangeSpec::Last(100), + ]); + assert_eq!(r, r2); + assert_eq!(r2, r3); + + let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); + let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); + assert_eq!(r, r2); + } + + #[test] + fn test_parse_unregistered_range_valid() { + let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); + let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); + assert_eq!(r, r2); + + let r: Range = Header::parse_header(&"custom=abcd".into()).unwrap(); + let r2 = Range::Unregistered("custom".to_owned(), "abcd".to_owned()); + assert_eq!(r, r2); + + let r: Range = Header::parse_header(&"custom=xxx-yyy".into()).unwrap(); + let r2 = Range::Unregistered("custom".to_owned(), "xxx-yyy".to_owned()); + assert_eq!(r, r2); + } + + #[test] + fn test_parse_invalid() { + let r: ::Result = Header::parse_header(&"bytes=1-a,-".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"bytes=1-2-3".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"abc".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"bytes=1-100=".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"bytes=".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"custom=".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"=1-100".into()); + assert_eq!(r.ok(), None); + } + + #[test] + fn test_fmt() { + use header::Headers; + + let mut headers = Headers::new(); + + headers.set(Range::Bytes(vec![ + ByteRangeSpec::FromTo(0, 1000), + ByteRangeSpec::AllFrom(2000), + ])); + assert_eq!(&headers.to_string(), "Range: bytes=0-1000,2000-\r\n"); + + headers.clear(); + headers.set(Range::Bytes(vec![])); + + assert_eq!(&headers.to_string(), "Range: bytes=\r\n"); + + headers.clear(); + headers.set(Range::Unregistered("custom".to_owned(), "1-xxx".to_owned())); + + assert_eq!(&headers.to_string(), "Range: custom=1-xxx\r\n"); + } + + #[test] + fn test_byte_range_spec_to_satisfiable_range() { + assert_eq!( + Some((0, 0)), + ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(3) + ); + assert_eq!( + Some((1, 2)), + ByteRangeSpec::FromTo(1, 2).to_satisfiable_range(3) + ); + assert_eq!( + Some((1, 2)), + ByteRangeSpec::FromTo(1, 5).to_satisfiable_range(3) + ); + assert_eq!(None, ByteRangeSpec::FromTo(3, 3).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::FromTo(2, 1).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(0)); + + assert_eq!( + Some((0, 2)), + ByteRangeSpec::AllFrom(0).to_satisfiable_range(3) + ); + assert_eq!( + Some((2, 2)), + ByteRangeSpec::AllFrom(2).to_satisfiable_range(3) + ); + assert_eq!(None, ByteRangeSpec::AllFrom(3).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::AllFrom(5).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::AllFrom(0).to_satisfiable_range(0)); + + assert_eq!(Some((1, 2)), ByteRangeSpec::Last(2).to_satisfiable_range(3)); + assert_eq!(Some((2, 2)), ByteRangeSpec::Last(1).to_satisfiable_range(3)); + assert_eq!(Some((0, 2)), ByteRangeSpec::Last(5).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::Last(0).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::Last(2).to_satisfiable_range(0)); + } } diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index 132087b9e..36c050b8f 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -7,7 +7,7 @@ use http::header::{HeaderName, HeaderValue}; /// A set of HTTP headers /// -/// `HeaderMap` is an multimap of [`HeaderName`] to values. +/// `HeaderMap` is an multi-map of [`HeaderName`] to values. /// /// [`HeaderName`]: struct.HeaderName.html #[derive(Debug, Clone)] diff --git a/actix-http/src/header/shared/charset.rs b/actix-http/src/header/shared/charset.rs index 6ddfa03ea..00e7309d4 100644 --- a/actix-http/src/header/shared/charset.rs +++ b/actix-http/src/header/shared/charset.rs @@ -137,17 +137,22 @@ impl FromStr for Charset { } } -#[test] -fn test_parse() { - assert_eq!(Us_Ascii, "us-ascii".parse().unwrap()); - assert_eq!(Us_Ascii, "US-Ascii".parse().unwrap()); - assert_eq!(Us_Ascii, "US-ASCII".parse().unwrap()); - assert_eq!(Shift_Jis, "Shift-JIS".parse().unwrap()); - assert_eq!(Ext("ABCD".to_owned()), "abcd".parse().unwrap()); -} +#[cfg(test)] +mod tests { + use super::*; -#[test] -fn test_display() { - assert_eq!("US-ASCII", format!("{}", Us_Ascii)); - assert_eq!("ABCD", format!("{}", Ext("ABCD".to_owned()))); + #[test] + fn test_parse() { + assert_eq!(Us_Ascii, "us-ascii".parse().unwrap()); + assert_eq!(Us_Ascii, "US-Ascii".parse().unwrap()); + assert_eq!(Us_Ascii, "US-ASCII".parse().unwrap()); + assert_eq!(Shift_Jis, "Shift-JIS".parse().unwrap()); + assert_eq!(Ext("ABCD".to_owned()), "abcd".parse().unwrap()); + } + + #[test] + fn test_display() { + assert_eq!("US-ASCII", format!("{}", Us_Ascii)); + assert_eq!("ABCD", format!("{}", Ext("ABCD".to_owned()))); + } } diff --git a/actix-http/src/header/shared/httpdate.rs b/actix-http/src/header/shared/httpdate.rs index 1b52f0de4..81caf6d53 100644 --- a/actix-http/src/header/shared/httpdate.rs +++ b/actix-http/src/header/shared/httpdate.rs @@ -5,7 +5,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use bytes::{buf::BufMutExt, BytesMut}; use http::header::{HeaderValue, InvalidHeaderValue}; -use time::{PrimitiveDateTime, OffsetDateTime, offset}; +use time::{offset, OffsetDateTime, PrimitiveDateTime}; use crate::error::ParseError; use crate::header::IntoHeaderValue; @@ -20,8 +20,8 @@ impl FromStr for HttpDate { fn from_str(s: &str) -> Result { match time_parser::parse_http_date(s) { - Some(t) => Ok(HttpDate(t.using_offset(offset!(UTC)))), - None => Err(ParseError::Header) + Some(t) => Ok(HttpDate(t.assume_utc())), + None => Err(ParseError::Header), } } } @@ -40,7 +40,7 @@ impl From for HttpDate { impl From for HttpDate { fn from(sys: SystemTime) -> HttpDate { - HttpDate(PrimitiveDateTime::from(sys).using_offset(offset!(UTC))) + HttpDate(PrimitiveDateTime::from(sys).assume_utc()) } } @@ -49,7 +49,14 @@ impl IntoHeaderValue for HttpDate { fn try_into(self) -> Result { let mut wrt = BytesMut::with_capacity(29).writer(); - write!(wrt, "{}", self.0.to_offset(offset!(UTC)).format("%a, %d %b %Y %H:%M:%S GMT")).unwrap(); + write!( + wrt, + "{}", + self.0 + .to_offset(offset!(UTC)) + .format("%a, %d %b %Y %H:%M:%S GMT") + ) + .unwrap(); HeaderValue::from_maybe_shared(wrt.get_mut().split().freeze()) } } @@ -66,14 +73,13 @@ impl From for SystemTime { #[cfg(test)] mod tests { use super::HttpDate; - use time::{PrimitiveDateTime, date, time, offset}; + use time::{date, time, PrimitiveDateTime}; #[test] fn test_date() { - let nov_07 = HttpDate(PrimitiveDateTime::new( - date!(1994-11-07), - time!(8:48:37) - ).using_offset(offset!(UTC))); + let nov_07 = HttpDate( + PrimitiveDateTime::new(date!(1994 - 11 - 07), time!(8:48:37)).assume_utc(), + ); assert_eq!( "Sun, 07 Nov 1994 08:48:37 GMT".parse::().unwrap(), diff --git a/actix-http/src/helpers.rs b/actix-http/src/helpers.rs index 58ebff61f..bbf358b66 100644 --- a/actix-http/src/helpers.rs +++ b/actix-http/src/helpers.rs @@ -1,173 +1,46 @@ -use std::{io, mem, ptr, slice}; +use std::io; use bytes::{BufMut, BytesMut}; use http::Version; use crate::extensions::Extensions; -const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\ - 2021222324252627282930313233343536373839\ - 4041424344454647484950515253545556575859\ - 6061626364656667686970717273747576777879\ - 8081828384858687888990919293949596979899"; +const DIGITS_START: u8 = b'0'; -pub(crate) const STATUS_LINE_BUF_SIZE: usize = 13; - -pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesMut) { - let mut buf: [u8; STATUS_LINE_BUF_SIZE] = [ - b'H', b'T', b'T', b'P', b'/', b'1', b'.', b'1', b' ', b' ', b' ', b' ', b' ', - ]; +pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) { match version { - Version::HTTP_2 => buf[5] = b'2', - Version::HTTP_10 => buf[7] = b'0', - Version::HTTP_09 => { - buf[5] = b'0'; - buf[7] = b'9'; - } - _ => (), - } - - let mut curr: isize = 12; - let buf_ptr = buf.as_mut_ptr(); - let lut_ptr = DEC_DIGITS_LUT.as_ptr(); - let four = n > 999; - - // decode 2 more chars, if > 2 chars - let d1 = (n % 100) << 1; - n /= 100; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2); - } - - // decode last 1 or 2 chars - if n < 10 { - curr -= 1; - unsafe { - *buf_ptr.offset(curr) = (n as u8) + b'0'; - } - } else { - let d1 = n << 1; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping( - lut_ptr.offset(d1 as isize), - buf_ptr.offset(curr), - 2, - ); + Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "), + Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "), + Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "), + _ => { + // other HTTP version handlers do not use this method } } - bytes.put_slice(&buf); - if four { - bytes.put_u8(b' '); - } + let d100 = (n / 100) as u8; + let d10 = ((n / 10) % 10) as u8; + let d1 = (n % 10) as u8; + + bytes.put_u8(DIGITS_START + d100); + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); + + // trailing space before reason + bytes.put_u8(b' '); } /// NOTE: bytes object has to contain enough space -pub fn write_content_length(mut n: usize, bytes: &mut BytesMut) { - if n < 10 { - let mut buf: [u8; 21] = [ - b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', b'e', - b'n', b'g', b't', b'h', b':', b' ', b'0', b'\r', b'\n', - ]; - buf[18] = (n as u8) + b'0'; - bytes.put_slice(&buf); - } else if n < 100 { - let mut buf: [u8; 22] = [ - b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', b'e', - b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'\r', b'\n', - ]; - let d1 = n << 1; - unsafe { - ptr::copy_nonoverlapping( - DEC_DIGITS_LUT.as_ptr().add(d1), - buf.as_mut_ptr().offset(18), - 2, - ); - } - bytes.put_slice(&buf); - } else if n < 1000 { - let mut buf: [u8; 23] = [ - b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', b'e', - b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'0', b'\r', b'\n', - ]; - // decode 2 more chars, if > 2 chars - let d1 = (n % 100) << 1; - n /= 100; - unsafe { - ptr::copy_nonoverlapping( - DEC_DIGITS_LUT.as_ptr().add(d1), - buf.as_mut_ptr().offset(19), - 2, - ) - }; - - // decode last 1 - buf[18] = (n as u8) + b'0'; - - bytes.put_slice(&buf); - } else { - bytes.put_slice(b"\r\ncontent-length: "); - convert_usize(n, bytes); - } -} - -pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) { - let mut curr: isize = 39; - let mut buf: [u8; 41] = unsafe { mem::MaybeUninit::uninit().assume_init() }; - buf[39] = b'\r'; - buf[40] = b'\n'; - let buf_ptr = buf.as_mut_ptr(); - let lut_ptr = DEC_DIGITS_LUT.as_ptr(); - - // eagerly decode 4 characters at a time - while n >= 10_000 { - let rem = (n % 10_000) as isize; - n /= 10_000; - - let d1 = (rem / 100) << 1; - let d2 = (rem % 100) << 1; - curr -= 4; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); - ptr::copy_nonoverlapping(lut_ptr.offset(d2), buf_ptr.offset(curr + 2), 2); - } +pub fn write_content_length(n: u64, bytes: &mut BytesMut) { + if n == 0 { + bytes.put_slice(b"\r\ncontent-length: 0\r\n"); + return; } - // if we reach here numbers are <= 9999, so at most 4 chars long - let mut n = n as isize; // possibly reduce 64bit math + let mut buf = itoa::Buffer::new(); - // decode 2 more chars, if > 2 chars - if n >= 100 { - let d1 = (n % 100) << 1; - n /= 100; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); - } - } - - // decode last 1 or 2 chars - if n < 10 { - curr -= 1; - unsafe { - *buf_ptr.offset(curr) = (n as u8) + b'0'; - } - } else { - let d1 = n << 1; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); - } - } - - unsafe { - bytes.extend_from_slice(slice::from_raw_parts( - buf_ptr.offset(curr), - 41 - curr as usize, - )); - } + bytes.put_slice(b"\r\ncontent-length: "); + bytes.put_slice(buf.format(n).as_bytes()); + bytes.put_slice(b"\r\n"); } pub(crate) struct Writer<'a>(pub &'a mut BytesMut); @@ -196,8 +69,28 @@ impl DataFactory for Data { #[cfg(test)] mod tests { + use std::str::from_utf8; + use super::*; + #[test] + fn test_status_line() { + let mut bytes = BytesMut::new(); + bytes.reserve(50); + write_status_line(Version::HTTP_11, 200, &mut bytes); + assert_eq!(from_utf8(&bytes.split().freeze()).unwrap(), "HTTP/1.1 200 "); + + let mut bytes = BytesMut::new(); + bytes.reserve(50); + write_status_line(Version::HTTP_09, 404, &mut bytes); + assert_eq!(from_utf8(&bytes.split().freeze()).unwrap(), "HTTP/0.9 404 "); + + let mut bytes = BytesMut::new(); + bytes.reserve(50); + write_status_line(Version::HTTP_09, 515, &mut bytes); + assert_eq!(from_utf8(&bytes.split().freeze()).unwrap(), "HTTP/0.9 515 "); + } + #[test] fn test_write_content_length() { let mut bytes = BytesMut::new(); @@ -231,5 +124,48 @@ mod tests { bytes.reserve(50); write_content_length(5909, &mut bytes); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 5909\r\n"[..]); + bytes.reserve(50); + write_content_length(9999, &mut bytes); + assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 9999\r\n"[..]); + bytes.reserve(50); + write_content_length(10001, &mut bytes); + assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 10001\r\n"[..]); + bytes.reserve(50); + write_content_length(59094, &mut bytes); + assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 59094\r\n"[..]); + bytes.reserve(50); + write_content_length(99999, &mut bytes); + assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 99999\r\n"[..]); + + bytes.reserve(50); + write_content_length(590947, &mut bytes); + assert_eq!( + bytes.split().freeze(), + b"\r\ncontent-length: 590947\r\n"[..] + ); + bytes.reserve(50); + write_content_length(999999, &mut bytes); + assert_eq!( + bytes.split().freeze(), + b"\r\ncontent-length: 999999\r\n"[..] + ); + bytes.reserve(50); + write_content_length(5909471, &mut bytes); + assert_eq!( + bytes.split().freeze(), + b"\r\ncontent-length: 5909471\r\n"[..] + ); + bytes.reserve(50); + write_content_length(59094718, &mut bytes); + assert_eq!( + bytes.split().freeze(), + b"\r\ncontent-length: 59094718\r\n"[..] + ); + bytes.reserve(50); + write_content_length(4294973728, &mut bytes); + assert_eq!( + bytes.split().freeze(), + b"\r\ncontent-length: 4294973728\r\n"[..] + ); } } diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index a5ae4b447..9f615a129 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -1,5 +1,5 @@ //! Basic http primitives for actix-net framework. -#![deny(rust_2018_idioms, warnings)] +#![warn(rust_2018_idioms, warnings)] #![allow( clippy::type_complexity, clippy::too_many_arguments, @@ -10,6 +10,9 @@ #[macro_use] extern crate log; +#[macro_use] +mod macros; + pub mod body; mod builder; pub mod client; diff --git a/actix-http/src/macros.rs b/actix-http/src/macros.rs new file mode 100644 index 000000000..b970b14f2 --- /dev/null +++ b/actix-http/src/macros.rs @@ -0,0 +1,95 @@ +#[macro_export] +macro_rules! downcast_get_type_id { + () => { + /// A helper method to get the type ID of the type + /// this trait is implemented on. + /// This method is unsafe to *implement*, since `downcast_ref` relies + /// on the returned `TypeId` to perform a cast. + /// + /// Unfortunately, Rust has no notion of a trait method that is + /// unsafe to implement (marking it as `unsafe` makes it unsafe + /// to *call*). As a workaround, we require this method + /// to return a private type along with the `TypeId`. This + /// private type (`PrivateHelper`) has a private constructor, + /// making it impossible for safe code to construct outside of + /// this module. This ensures that safe code cannot violate + /// type-safety by implementing this method. + #[doc(hidden)] + fn __private_get_type_id__(&self) -> (std::any::TypeId, PrivateHelper) + where + Self: 'static, + { + (std::any::TypeId::of::(), PrivateHelper(())) + } + }; +} + +//Generate implementation for dyn $name +#[macro_export] +macro_rules! downcast { + ($name:ident) => { + /// A struct with a private constructor, for use with + /// `__private_get_type_id__`. Its single field is private, + /// ensuring that it can only be constructed from this module + #[doc(hidden)] + pub struct PrivateHelper(()); + + impl dyn $name + 'static { + /// Downcasts generic body to a specific type. + pub fn downcast_ref(&self) -> Option<&T> { + if self.__private_get_type_id__().0 == std::any::TypeId::of::() { + // Safety: external crates cannot override the default + // implementation of `__private_get_type_id__`, since + // it requires returning a private type. We can therefore + // rely on the returned `TypeId`, which ensures that this + // case is correct. + unsafe { Some(&*(self as *const dyn $name as *const T)) } + } else { + None + } + } + /// Downcasts a generic body to a mutable specific type. + pub fn downcast_mut(&mut self) -> Option<&mut T> { + if self.__private_get_type_id__().0 == std::any::TypeId::of::() { + // Safety: external crates cannot override the default + // implementation of `__private_get_type_id__`, since + // it requires returning a private type. We can therefore + // rely on the returned `TypeId`, which ensures that this + // case is correct. + unsafe { + Some(&mut *(self as *const dyn $name as *const T as *mut T)) + } + } else { + None + } + } + } + }; +} + +#[cfg(test)] +mod tests { + + trait MB { + downcast_get_type_id!(); + } + + downcast!(MB); + + impl MB for String {} + impl MB for () {} + + #[actix_rt::test] + async fn test_any_casting() { + let mut body = String::from("hello cast"); + let resp_body: &mut dyn MB = &mut body; + let body = resp_body.downcast_ref::().unwrap(); + assert_eq!(body, "hello cast"); + let body = &mut resp_body.downcast_mut::().unwrap(); + body.push_str("!"); + let body = resp_body.downcast_ref::().unwrap(); + assert_eq!(body, "hello cast!"); + let not_body = resp_body.downcast_ref::<()>(); + assert!(not_body.is_none()); + } +} diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index d005ad04a..5e53f73b6 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -99,13 +99,13 @@ impl RequestHead { } /// Is to uppercase headers with Camel-Case. - /// Befault is `false` + /// Default is `false` #[inline] pub fn camel_case_headers(&self) -> bool { self.flags.contains(Flags::CAMEL_CASE) } - /// Set `true` to send headers which are uppercased with Camel-Case. + /// Set `true` to send headers which are formatted as Camel-Case. #[inline] pub fn set_camel_case_headers(&mut self, val: bool) { if val { diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index fcdcd7cdf..9086212f1 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -9,7 +9,6 @@ use std::{fmt, str}; use bytes::{Bytes, BytesMut}; use futures_core::Stream; use serde::Serialize; -use serde_json; use crate::body::{Body, BodyStream, MessageBody, ResponseBody}; use crate::cookie::{Cookie, CookieJar}; @@ -473,7 +472,9 @@ impl ResponseBuilder { /// Disable chunked transfer encoding for HTTP/1.1 streaming responses. #[inline] - pub fn no_chunking(&mut self) -> &mut Self { + pub fn no_chunking(&mut self, len: u64) -> &mut Self { + self.header(header::CONTENT_LENGTH, len); + if let Some(parts) = parts(&mut self.head, &self.err) { parts.no_chunking(true); } @@ -498,12 +499,6 @@ impl ResponseBuilder { self } - /// Set content length - #[inline] - pub fn content_length(&mut self, len: u64) -> &mut Self { - self.header(header::CONTENT_LENGTH, len) - } - /// Set a cookie /// /// ```rust @@ -637,7 +632,7 @@ impl ResponseBuilder { /// `ResponseBuilder` can not be used after this call. pub fn streaming(&mut self, stream: S) -> Response where - S: Stream> + 'static, + S: Stream> + Unpin + 'static, E: Into + 'static, { self.body(Body::from_message(BodyStream::new(stream))) diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index 51de95135..94cdbc828 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -10,7 +10,7 @@ use bytes::Bytes; use futures_core::{ready, Future}; use futures_util::future::ok; use h2::server::{self, Handshake}; -use pin_project::{pin_project, project}; +use pin_project::pin_project; use crate::body::MessageBody; use crate::builder::HttpServiceBuilder; @@ -574,7 +574,7 @@ where } } -#[pin_project] +#[pin_project(project = StateProj)] enum State where S: Service, @@ -650,16 +650,14 @@ where U: Service), Response = ()>, U::Error: fmt::Display, { - #[project] fn poll( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { - #[project] match self.as_mut().project() { - State::H1(disp) => disp.poll(cx), - State::H2(disp) => disp.poll(cx), - State::H2Handshake(ref mut data) => { + StateProj::H1(disp) => disp.poll(cx), + StateProj::H2(disp) => disp.poll(cx), + StateProj::H2Handshake(ref mut data) => { let conn = if let Some(ref mut item) = data { match Pin::new(&mut item.0).poll(cx) { Poll::Ready(Ok(conn)) => conn, diff --git a/actix-http/src/time_parser.rs b/actix-http/src/time_parser.rs index f6623d24e..0d06a5867 100644 --- a/actix-http/src/time_parser.rs +++ b/actix-http/src/time_parser.rs @@ -1,4 +1,4 @@ -use time::{PrimitiveDateTime, Date}; +use time::{Date, OffsetDateTime, PrimitiveDateTime}; /// Attempt to parse a `time` string as one of either RFC 1123, RFC 850, or asctime. pub fn parse_http_date(time: &str) -> Option { @@ -19,7 +19,7 @@ fn try_parse_rfc_850(time: &str) -> Option { // If the `time` string contains a two-digit year, then as per RFC 2616 § 19.3, // we consider the year as part of this century if it's within the next 50 years, // otherwise we consider as part of the previous century. - let now = PrimitiveDateTime::now(); + let now = OffsetDateTime::now_utc(); let century_start_year = (now.year() / 100) * 100; let mut expanded_year = century_start_year + dt.year(); @@ -29,10 +29,10 @@ fn try_parse_rfc_850(time: &str) -> Option { match Date::try_from_ymd(expanded_year, dt.month(), dt.day()) { Ok(date) => Some(PrimitiveDateTime::new(date, dt.time())), - Err(_) => None + Err(_) => None, } } - Err(_) => None + Err(_) => None, } } diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index a37208a2b..733976a78 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -137,7 +137,7 @@ impl Encoder for Codec { Parser::write_message( dst, &data[..], - OpCode::Binary, + OpCode::Text, false, !self.flags.contains(Flags::SERVER), ) @@ -151,7 +151,7 @@ impl Encoder for Codec { Parser::write_message( dst, &data[..], - OpCode::Text, + OpCode::Binary, false, !self.flags.contains(Flags::SERVER), ) diff --git a/actix-http/src/ws/frame.rs b/actix-http/src/ws/frame.rs index 3c70eb2bd..8f7004f18 100644 --- a/actix-http/src/ws/frame.rs +++ b/actix-http/src/ws/frame.rs @@ -2,7 +2,6 @@ use std::convert::TryFrom; use bytes::{Buf, BufMut, BytesMut}; use log::debug; -use rand; use crate::ws::mask::apply_mask; use crate::ws::proto::{CloseCode, CloseReason, OpCode}; diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index ffa397979..6ffdecc35 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -58,6 +58,8 @@ pub enum ProtocolError { Io(io::Error), } +impl std::error::Error for ProtocolError {} + impl ResponseError for ProtocolError {} /// Websocket handshake errors @@ -108,7 +110,7 @@ impl ResponseError for HandshakeError { } } -/// Verify `WebSocket` handshake request and create handshake reponse. +/// Verify `WebSocket` handshake request and create handshake response. // /// `protocols` is a sequence of known protocols. On successful handshake, // /// the returned response headers contain the first protocol in this list // /// which the server also knows. @@ -168,7 +170,7 @@ pub fn verify_handshake(req: &RequestHead) -> Result<(), HandshakeError> { Ok(()) } -/// Create websocket's handshake response +/// Create websocket handshake response /// /// This function returns handshake `Response`, ready to send to peer. pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs index 60af6f08b..dd3078a6c 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -1,5 +1,3 @@ -use base64; -use sha1; use std::convert::{From, Into}; use std::fmt; @@ -205,7 +203,7 @@ impl> From<(CloseCode, T)> for CloseReason { static WS_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; -// TODO: hash is always same size, we dont need String +// TODO: hash is always same size, we don't need String pub fn hash_key(key: &[u8]) -> String { use sha1::Digest; let mut hasher = sha1::Sha1::new(); diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index 9da3b04a2..07104decc 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -1,6 +1,6 @@ use actix_service::ServiceFactory; use bytes::Bytes; -use futures::future::{self, ok}; +use futures_util::future::{self, ok}; use actix_http::{http, HttpService, Request, Response}; use actix_http_test::test_server; @@ -33,7 +33,8 @@ async fn test_h1_v2() { HttpService::build() .finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) .tcp() - }); + }) + .await; let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -61,7 +62,8 @@ async fn test_connection_close() { .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .tcp() .map(|_| ()) - }); + }) + .await; let response = srv.get("/").force_close().send().await.unwrap(); assert!(response.status().is_success()); @@ -80,7 +82,8 @@ async fn test_with_query_parameter() { }) .tcp() .map(|_| ()) - }); + }) + .await; let request = srv.request(http::Method::GET, srv.url("/?qp=5")); let response = request.send().await.unwrap(); diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index b25f05272..3a7bfa409 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -5,8 +5,8 @@ use actix_http_test::test_server; use actix_service::{fn_service, ServiceFactory}; use bytes::{Bytes, BytesMut}; -use futures::future::{err, ok, ready}; -use futures::stream::{once, Stream, StreamExt}; +use futures_util::future::{err, ok, ready}; +use futures_util::stream::{once, Stream, StreamExt}; use open_ssl::ssl::{AlpnError, SslAcceptor, SslFiletype, SslMethod}; use actix_http::error::{ErrorBadRequest, PayloadError}; @@ -67,7 +67,8 @@ async fn test_h2() -> io::Result<()> { .h2(|_| ok::<_, Error>(Response::Ok().finish())) .openssl(ssl_acceptor()) .map_err(|_| ()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -85,7 +86,8 @@ async fn test_h2_1() -> io::Result<()> { }) .openssl(ssl_acceptor()) .map_err(|_| ()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -97,15 +99,14 @@ async fn test_h2_body() -> io::Result<()> { let data = "HELLOWORLD".to_owned().repeat(64 * 1024); let mut srv = test_server(move || { HttpService::build() - .h2(|mut req: Request<_>| { - async move { - let body = load_body(req.take_payload()).await?; - Ok::<_, Error>(Response::Ok().body(body)) - } + .h2(|mut req: Request<_>| async move { + let body = load_body(req.take_payload()).await?; + Ok::<_, Error>(Response::Ok().body(body)) }) .openssl(ssl_acceptor()) .map_err(|_| ()) - }); + }) + .await; let response = srv.sget("/").send_body(data.clone()).await.unwrap(); assert!(response.status().is_success()); @@ -133,7 +134,8 @@ async fn test_h2_content_length() { }) .openssl(ssl_acceptor()) .map_err(|_| ()) - }); + }) + .await; let header = HeaderName::from_static("content-length"); let value = HeaderValue::from_static("0"); @@ -194,7 +196,7 @@ async fn test_h2_headers() { }) .openssl(ssl_acceptor()) .map_err(|_| ()) - }); + }).await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -233,7 +235,8 @@ async fn test_h2_body2() { .h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .openssl(ssl_acceptor()) .map_err(|_| ()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -250,7 +253,8 @@ async fn test_h2_head_empty() { .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .openssl(ssl_acceptor()) .map_err(|_| ()) - }); + }) + .await; let response = srv.shead("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -271,11 +275,12 @@ async fn test_h2_head_binary() { let mut srv = test_server(move || { HttpService::build() .h2(|_| { - ok::<_, ()>(Response::Ok().content_length(STR.len() as u64).body(STR)) + ok::<_, ()>(Response::Ok().body(STR)) }) .openssl(ssl_acceptor()) .map_err(|_| ()) - }); + }) + .await; let response = srv.shead("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -297,7 +302,8 @@ async fn test_h2_head_binary2() { .h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .openssl(ssl_acceptor()) .map_err(|_| ()) - }); + }) + .await; let response = srv.shead("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -320,7 +326,8 @@ async fn test_h2_body_length() { }) .openssl(ssl_acceptor()) .map_err(|_| ()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -344,7 +351,8 @@ async fn test_h2_body_chunked_explicit() { }) .openssl(ssl_acceptor()) .map_err(|_| ()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -371,7 +379,8 @@ async fn test_h2_response_http_error_handling() { })) .openssl(ssl_acceptor()) .map_err(|_| ()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); @@ -388,7 +397,8 @@ async fn test_h2_service_error() { .h2(|_| err::(ErrorBadRequest("error"))) .openssl(ssl_acceptor()) .map_err(|_| ()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); @@ -409,7 +419,8 @@ async fn test_h2_on_connect() { }) .openssl(ssl_acceptor()) .map_err(|_| ()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index bc0c91cc3..465cba6df 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -7,8 +7,8 @@ use actix_http_test::test_server; use actix_service::{fn_factory_with_config, fn_service}; use bytes::{Bytes, BytesMut}; -use futures::future::{self, err, ok}; -use futures::stream::{once, Stream, StreamExt}; +use futures_util::future::{self, err, ok}; +use futures_util::stream::{once, Stream, StreamExt}; use rust_tls::{ internal::pemfile::{certs, pkcs8_private_keys}, NoClientAuth, ServerConfig as RustlsServerConfig, @@ -45,7 +45,8 @@ async fn test_h1() -> io::Result<()> { HttpService::build() .h1(|_| future::ok::<_, Error>(Response::Ok().finish())) .rustls(ssl_acceptor()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -58,7 +59,8 @@ async fn test_h2() -> io::Result<()> { HttpService::build() .h2(|_| future::ok::<_, Error>(Response::Ok().finish())) .rustls(ssl_acceptor()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -75,7 +77,8 @@ async fn test_h1_1() -> io::Result<()> { future::ok::<_, Error>(Response::Ok().finish()) }) .rustls(ssl_acceptor()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -92,7 +95,8 @@ async fn test_h2_1() -> io::Result<()> { future::ok::<_, Error>(Response::Ok().finish()) }) .rustls(ssl_acceptor()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -104,14 +108,13 @@ async fn test_h2_body1() -> io::Result<()> { let data = "HELLOWORLD".to_owned().repeat(64 * 1024); let mut srv = test_server(move || { HttpService::build() - .h2(|mut req: Request<_>| { - async move { - let body = load_body(req.take_payload()).await?; - Ok::<_, Error>(Response::Ok().body(body)) - } + .h2(|mut req: Request<_>| async move { + let body = load_body(req.take_payload()).await?; + Ok::<_, Error>(Response::Ok().body(body)) }) .rustls(ssl_acceptor()) - }); + }) + .await; let response = srv.sget("/").send_body(data.clone()).await.unwrap(); assert!(response.status().is_success()); @@ -138,7 +141,8 @@ async fn test_h2_content_length() { future::ok::<_, ()>(Response::new(statuses[indx])) }) .rustls(ssl_acceptor()) - }); + }) + .await; let header = HeaderName::from_static("content-length"); let value = HeaderValue::from_static("0"); @@ -197,7 +201,7 @@ async fn test_h2_headers() { future::ok::<_, ()>(config.body(data.clone())) }) .rustls(ssl_acceptor()) - }); + }).await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -235,7 +239,8 @@ async fn test_h2_body2() { HttpService::build() .h2(|_| future::ok::<_, ()>(Response::Ok().body(STR))) .rustls(ssl_acceptor()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -251,7 +256,8 @@ async fn test_h2_head_empty() { HttpService::build() .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .rustls(ssl_acceptor()) - }); + }) + .await; let response = srv.shead("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -275,10 +281,11 @@ async fn test_h2_head_binary() { let mut srv = test_server(move || { HttpService::build() .h2(|_| { - ok::<_, ()>(Response::Ok().content_length(STR.len() as u64).body(STR)) + ok::<_, ()>(Response::Ok().body(STR)) }) .rustls(ssl_acceptor()) - }); + }) + .await; let response = srv.shead("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -302,7 +309,8 @@ async fn test_h2_head_binary2() { HttpService::build() .h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .rustls(ssl_acceptor()) - }); + }) + .await; let response = srv.shead("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -327,7 +335,8 @@ async fn test_h2_body_length() { ) }) .rustls(ssl_acceptor()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -350,7 +359,8 @@ async fn test_h2_body_chunked_explicit() { ) }) .rustls(ssl_acceptor()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -378,7 +388,8 @@ async fn test_h2_response_http_error_handling() { })) })) .rustls(ssl_acceptor()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); @@ -394,7 +405,8 @@ async fn test_h2_service_error() { HttpService::build() .h2(|_| err::(error::ErrorBadRequest("error"))) .rustls(ssl_acceptor()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert_eq!(response.status(), http::StatusCode::BAD_REQUEST); @@ -410,7 +422,8 @@ async fn test_h1_service_error() { HttpService::build() .h1(|_| err::(error::ErrorBadRequest("error"))) .rustls(ssl_acceptor()) - }); + }) + .await; let response = srv.sget("/").send().await.unwrap(); assert_eq!(response.status(), http::StatusCode::BAD_REQUEST); diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index a84692f9d..bee5ebef2 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -6,8 +6,8 @@ use actix_http_test::test_server; use actix_rt::time::delay_for; use actix_service::fn_service; use bytes::Bytes; -use futures::future::{self, err, ok, ready, FutureExt}; -use futures::stream::{once, StreamExt}; +use futures_util::future::{self, err, ok, ready, FutureExt}; +use futures_util::stream::{once, StreamExt}; use regex::Regex; use actix_http::httpmessage::HttpMessage; @@ -27,7 +27,8 @@ async fn test_h1() { future::ok::<_, ()>(Response::Ok().finish()) }) .tcp() - }); + }) + .await; let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -46,7 +47,8 @@ async fn test_h1_2() { future::ok::<_, ()>(Response::Ok().finish()) }) .tcp() - }); + }) + .await; let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -65,7 +67,8 @@ async fn test_expect_continue() { })) .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) .tcp() - }); + }) + .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); @@ -95,7 +98,8 @@ async fn test_expect_continue_h1() { })) .h1(fn_service(|_| future::ok::<_, ()>(Response::Ok().finish()))) .tcp() - }); + }) + .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); @@ -130,7 +134,8 @@ async fn test_chunked_payload() { }) })) .tcp() - }); + }) + .await; let returned_size = { let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); @@ -172,7 +177,8 @@ async fn test_slow_request() { .client_timeout(100) .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) .tcp() - }); + }) + .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n"); @@ -187,7 +193,8 @@ async fn test_http1_malformed_request() { HttpService::build() .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .tcp() - }); + }) + .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test/tests/test HTTP1.1\r\n"); @@ -202,7 +209,8 @@ async fn test_http1_keepalive() { HttpService::build() .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .tcp() - }); + }) + .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n\r\n"); @@ -223,7 +231,8 @@ async fn test_http1_keepalive_timeout() { .keep_alive(1) .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .tcp() - }); + }) + .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n\r\n"); @@ -243,7 +252,8 @@ async fn test_http1_keepalive_close() { HttpService::build() .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .tcp() - }); + }) + .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = @@ -263,7 +273,8 @@ async fn test_http10_keepalive_default_close() { HttpService::build() .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .tcp() - }); + }) + .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test/tests/test HTTP/1.0\r\n\r\n"); @@ -282,7 +293,8 @@ async fn test_http10_keepalive() { HttpService::build() .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .tcp() - }); + }) + .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream @@ -309,7 +321,8 @@ async fn test_http1_keepalive_disabled() { .keep_alive(KeepAlive::Disabled) .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .tcp() - }); + }) + .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n\r\n"); @@ -344,7 +357,8 @@ async fn test_content_length() { future::ok::<_, ()>(Response::new(statuses[indx])) }) .tcp() - }); + }) + .await; let header = HeaderName::from_static("content-length"); let value = HeaderValue::from_static("0"); @@ -397,7 +411,7 @@ async fn test_h1_headers() { } future::ok::<_, ()>(builder.body(data.clone())) }).tcp() - }); + }).await; let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -435,7 +449,8 @@ async fn test_h1_body() { HttpService::build() .h1(|_| ok::<_, ()>(Response::Ok().body(STR))) .tcp() - }); + }) + .await; let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -451,7 +466,8 @@ async fn test_h1_head_empty() { HttpService::build() .h1(|_| ok::<_, ()>(Response::Ok().body(STR))) .tcp() - }); + }) + .await; let response = srv.head("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -474,10 +490,11 @@ async fn test_h1_head_binary() { let mut srv = test_server(|| { HttpService::build() .h1(|_| { - ok::<_, ()>(Response::Ok().content_length(STR.len() as u64).body(STR)) + ok::<_, ()>(Response::Ok().body(STR)) }) .tcp() - }); + }) + .await; let response = srv.head("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -501,7 +518,8 @@ async fn test_h1_head_binary2() { HttpService::build() .h1(|_| ok::<_, ()>(Response::Ok().body(STR))) .tcp() - }); + }) + .await; let response = srv.head("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -526,7 +544,8 @@ async fn test_h1_body_length() { ) }) .tcp() - }); + }) + .await; let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -549,7 +568,8 @@ async fn test_h1_body_chunked_explicit() { ) }) .tcp() - }); + }) + .await; let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -579,7 +599,8 @@ async fn test_h1_body_chunked_implicit() { ok::<_, ()>(Response::Ok().streaming(body)) }) .tcp() - }); + }) + .await; let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); @@ -611,7 +632,8 @@ async fn test_h1_response_http_error_handling() { ) })) .tcp() - }); + }) + .await; let response = srv.get("/").send().await.unwrap(); assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); @@ -627,7 +649,8 @@ async fn test_h1_service_error() { HttpService::build() .h1(|_| future::err::(error::ErrorBadRequest("error"))) .tcp() - }); + }) + .await; let response = srv.get("/").send().await.unwrap(); assert_eq!(response.status(), http::StatusCode::BAD_REQUEST); @@ -647,7 +670,8 @@ async fn test_h1_on_connect() { future::ok::<_, ()>(Response::Ok().finish()) }) .tcp() - }); + }) + .await; let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs index 7152fee48..ff9def85b 100644 --- a/actix-http/tests/test_ws.rs +++ b/actix-http/tests/test_ws.rs @@ -1,4 +1,5 @@ use std::cell::Cell; +use std::future::Future; use std::marker::PhantomData; use std::pin::Pin; use std::sync::{Arc, Mutex}; @@ -9,9 +10,9 @@ use actix_http_test::test_server; use actix_service::{fn_factory, Service}; use actix_utils::framed::Dispatcher; use bytes::Bytes; -use futures::future; -use futures::task::{Context, Poll}; -use futures::{Future, SinkExt, StreamExt}; +use futures_util::future; +use futures_util::task::{Context, Poll}; +use futures_util::{SinkExt, StreamExt}; struct WsService(Arc, Cell)>>); @@ -93,7 +94,8 @@ async fn test_simple() { .finish(|_| future::ok::<_, ()>(Response::NotFound())) .tcp() } - }); + }) + .await; // client service let mut framed = srv.ws().await.unwrap(); diff --git a/actix-identity/CHANGES.md b/actix-identity/CHANGES.md deleted file mode 100644 index 0c9809ea1..000000000 --- a/actix-identity/CHANGES.md +++ /dev/null @@ -1,17 +0,0 @@ -# Changes - -## [Unreleased] - 2020-xx-xx - -* Update the `time` dependency to 0.2.5 - -## [0.2.1] - 2020-01-10 - -* Fix panic with already borrowed: BorrowMutError #1263 - -## [0.2.0] - 2019-12-20 - -* Use actix-web 2.0 - -## [0.1.0] - 2019-06-xx - -* Move identity middleware to separate crate diff --git a/actix-identity/Cargo.toml b/actix-identity/Cargo.toml deleted file mode 100644 index efeb24bda..000000000 --- a/actix-identity/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "actix-identity" -version = "0.2.1" -authors = ["Nikolay Kim "] -description = "Identity service for actix web framework." -readme = "README.md" -keywords = ["http", "web", "framework", "async", "futures"] -homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-identity/" -license = "MIT/Apache-2.0" -edition = "2018" - -[lib] -name = "actix_identity" -path = "src/lib.rs" - -[dependencies] -actix-web = { version = "2.0.0", default-features = false, features = ["secure-cookies"] } -actix-service = "1.0.2" -futures = "0.3.1" -serde = "1.0" -serde_json = "1.0" -time = { version = "0.2.5", default-features = false, features = ["std"] } - -[dev-dependencies] -actix-rt = "1.0.0" -actix-http = "1.0.1" -bytes = "0.5.3" diff --git a/actix-identity/LICENSE-APACHE b/actix-identity/LICENSE-APACHE deleted file mode 120000 index 965b606f3..000000000 --- a/actix-identity/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-APACHE \ No newline at end of file diff --git a/actix-identity/LICENSE-MIT b/actix-identity/LICENSE-MIT deleted file mode 120000 index 76219eb72..000000000 --- a/actix-identity/LICENSE-MIT +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-MIT \ No newline at end of file diff --git a/actix-identity/README.md b/actix-identity/README.md index 60b615c76..62a40137f 100644 --- a/actix-identity/README.md +++ b/actix-identity/README.md @@ -1,5 +1,7 @@ # Identity service 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-identity)](https://crates.io/crates/actix-identity) [![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) +**This crate moved to https://github.com/actix/actix-extras.** + ## Documentation & community resources * [User Guide](https://actix.rs/docs/) diff --git a/actix-identity/src/lib.rs b/actix-identity/src/lib.rs deleted file mode 100644 index b584b1af7..000000000 --- a/actix-identity/src/lib.rs +++ /dev/null @@ -1,1128 +0,0 @@ -//! Request identity service for Actix applications. -//! -//! [**IdentityService**](struct.IdentityService.html) middleware can be -//! used with different policies types to store identity information. -//! -//! By default, only cookie identity policy is implemented. Other backend -//! implementations can be added separately. -//! -//! [**CookieIdentityPolicy**](struct.CookieIdentityPolicy.html) -//! uses cookies as identity storage. -//! -//! To access current request identity -//! [**Identity**](struct.Identity.html) extractor should be used. -//! -//! ```rust -//! use actix_web::*; -//! use actix_identity::{Identity, CookieIdentityPolicy, IdentityService}; -//! -//! async fn index(id: Identity) -> String { -//! // access request identity -//! if let Some(id) = id.identity() { -//! format!("Welcome! {}", id) -//! } else { -//! "Welcome Anonymous!".to_owned() -//! } -//! } -//! -//! async fn login(id: Identity) -> HttpResponse { -//! id.remember("User1".to_owned()); // <- remember identity -//! HttpResponse::Ok().finish() -//! } -//! -//! async fn logout(id: Identity) -> HttpResponse { -//! id.forget(); // <- remove identity -//! HttpResponse::Ok().finish() -//! } -//! -//! fn main() { -//! let app = App::new().wrap(IdentityService::new( -//! // <- create identity middleware -//! CookieIdentityPolicy::new(&[0; 32]) // <- create cookie identity policy -//! .name("auth-cookie") -//! .secure(false))) -//! .service(web::resource("/index.html").to(index)) -//! .service(web::resource("/login.html").to(login)) -//! .service(web::resource("/logout.html").to(logout)); -//! } -//! ``` -use std::cell::RefCell; -use std::future::Future; -use std::rc::Rc; -use std::task::{Context, Poll}; -use std::time::SystemTime; - -use actix_service::{Service, Transform}; -use futures::future::{ok, FutureExt, LocalBoxFuture, Ready}; -use serde::{Deserialize, Serialize}; -use time::Duration; - -use actix_web::cookie::{Cookie, CookieJar, Key, SameSite}; -use actix_web::dev::{Extensions, Payload, ServiceRequest, ServiceResponse}; -use actix_web::error::{Error, Result}; -use actix_web::http::header::{self, HeaderValue}; -use actix_web::{FromRequest, HttpMessage, HttpRequest}; - -/// The extractor type to obtain your identity from a request. -/// -/// ```rust -/// use actix_web::*; -/// use actix_identity::Identity; -/// -/// fn index(id: Identity) -> Result { -/// // access request identity -/// if let Some(id) = id.identity() { -/// Ok(format!("Welcome! {}", id)) -/// } else { -/// Ok("Welcome Anonymous!".to_owned()) -/// } -/// } -/// -/// fn login(id: Identity) -> HttpResponse { -/// id.remember("User1".to_owned()); // <- remember identity -/// HttpResponse::Ok().finish() -/// } -/// -/// fn logout(id: Identity) -> HttpResponse { -/// id.forget(); // <- remove identity -/// HttpResponse::Ok().finish() -/// } -/// # fn main() {} -/// ``` -#[derive(Clone)] -pub struct Identity(HttpRequest); - -impl Identity { - /// Return the claimed identity of the user associated request or - /// ``None`` if no identity can be found associated with the request. - pub fn identity(&self) -> Option { - Identity::get_identity(&self.0.extensions()) - } - - /// Remember identity. - pub fn remember(&self, identity: String) { - if let Some(id) = self.0.extensions_mut().get_mut::() { - id.id = Some(identity); - id.changed = true; - } - } - - /// This method is used to 'forget' the current identity on subsequent - /// requests. - pub fn forget(&self) { - if let Some(id) = self.0.extensions_mut().get_mut::() { - id.id = None; - id.changed = true; - } - } - - fn get_identity(extensions: &Extensions) -> Option { - if let Some(id) = extensions.get::() { - id.id.clone() - } else { - None - } - } -} - -struct IdentityItem { - id: Option, - changed: bool, -} - -/// Helper trait that allows to get Identity. -/// -/// It could be used in middleware but identity policy must be set before any other middleware that needs identity -/// RequestIdentity is implemented both for `ServiceRequest` and `HttpRequest`. -pub trait RequestIdentity { - fn get_identity(&self) -> Option; -} - -impl RequestIdentity for T -where - T: HttpMessage, -{ - fn get_identity(&self) -> Option { - Identity::get_identity(&self.extensions()) - } -} - -/// Extractor implementation for Identity type. -/// -/// ```rust -/// # use actix_web::*; -/// use actix_identity::Identity; -/// -/// fn index(id: Identity) -> String { -/// // access request identity -/// if let Some(id) = id.identity() { -/// format!("Welcome! {}", id) -/// } else { -/// "Welcome Anonymous!".to_owned() -/// } -/// } -/// # fn main() {} -/// ``` -impl FromRequest for Identity { - type Config = (); - type Error = Error; - type Future = Ready>; - - #[inline] - fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - ok(Identity(req.clone())) - } -} - -/// Identity policy definition. -pub trait IdentityPolicy: Sized + 'static { - /// The return type of the middleware - type Future: Future, Error>>; - - /// The return type of the middleware - type ResponseFuture: Future>; - - /// Parse the session from request and load data from a service identity. - fn from_request(&self, request: &mut ServiceRequest) -> Self::Future; - - /// Write changes to response - fn to_response( - &self, - identity: Option, - changed: bool, - response: &mut ServiceResponse, - ) -> Self::ResponseFuture; -} - -/// Request identity middleware -/// -/// ```rust -/// use actix_web::App; -/// use actix_identity::{CookieIdentityPolicy, IdentityService}; -/// -/// fn main() { -/// let app = App::new().wrap(IdentityService::new( -/// // <- create identity middleware -/// CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend -/// .name("auth-cookie") -/// .secure(false), -/// )); -/// } -/// ``` -pub struct IdentityService { - backend: Rc, -} - -impl IdentityService { - /// Create new identity service with specified backend. - pub fn new(backend: T) -> Self { - IdentityService { - backend: Rc::new(backend), - } - } -} - -impl Transform for IdentityService -where - S: Service, Error = Error> - + 'static, - S::Future: 'static, - T: IdentityPolicy, - B: 'static, -{ - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; - type InitError = (); - type Transform = IdentityServiceMiddleware; - type Future = Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - ok(IdentityServiceMiddleware { - backend: self.backend.clone(), - service: Rc::new(RefCell::new(service)), - }) - } -} - -#[doc(hidden)] -pub struct IdentityServiceMiddleware { - backend: Rc, - service: Rc>, -} - -impl Clone for IdentityServiceMiddleware { - fn clone(&self) -> Self { - Self { - backend: self.backend.clone(), - service: self.service.clone(), - } - } -} - -impl Service for IdentityServiceMiddleware -where - B: 'static, - S: Service, Error = Error> - + 'static, - S::Future: 'static, - T: IdentityPolicy, -{ - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; - type Future = LocalBoxFuture<'static, Result>; - - fn poll_ready(&mut self, cx: &mut Context) -> Poll> { - self.service.borrow_mut().poll_ready(cx) - } - - fn call(&mut self, mut req: ServiceRequest) -> Self::Future { - let srv = self.service.clone(); - let backend = self.backend.clone(); - let fut = self.backend.from_request(&mut req); - - async move { - match fut.await { - Ok(id) => { - req.extensions_mut() - .insert(IdentityItem { id, changed: false }); - - // https://github.com/actix/actix-web/issues/1263 - let fut = { srv.borrow_mut().call(req) }; - let mut res = fut.await?; - let id = res.request().extensions_mut().remove::(); - - if let Some(id) = id { - match backend.to_response(id.id, id.changed, &mut res).await { - Ok(_) => Ok(res), - Err(e) => Ok(res.error_response(e)), - } - } else { - Ok(res) - } - } - Err(err) => Ok(req.error_response(err)), - } - } - .boxed_local() - } -} - -struct CookieIdentityInner { - key: Key, - key_v2: Key, - name: String, - path: String, - domain: Option, - secure: bool, - max_age: Option, - same_site: Option, - visit_deadline: Option, - login_deadline: Option, -} - -#[derive(Deserialize, Serialize, Debug)] -struct CookieValue { - identity: String, - #[serde(skip_serializing_if = "Option::is_none")] - login_timestamp: Option, - #[serde(skip_serializing_if = "Option::is_none")] - visit_timestamp: Option, -} - -#[derive(Debug)] -struct CookieIdentityExtention { - login_timestamp: Option, -} - -impl CookieIdentityInner { - fn new(key: &[u8]) -> CookieIdentityInner { - let key_v2: Vec = key.iter().chain([1, 0, 0, 0].iter()).cloned().collect(); - CookieIdentityInner { - key: Key::from_master(key), - key_v2: Key::from_master(&key_v2), - name: "actix-identity".to_owned(), - path: "/".to_owned(), - domain: None, - secure: true, - max_age: None, - same_site: None, - visit_deadline: None, - login_deadline: None, - } - } - - fn set_cookie( - &self, - resp: &mut ServiceResponse, - value: Option, - ) -> Result<()> { - let add_cookie = value.is_some(); - let val = value.map(|val| { - if !self.legacy_supported() { - serde_json::to_string(&val) - } else { - Ok(val.identity) - } - }); - let mut cookie = - Cookie::new(self.name.clone(), val.unwrap_or_else(|| Ok(String::new()))?); - cookie.set_path(self.path.clone()); - cookie.set_secure(self.secure); - cookie.set_http_only(true); - - if let Some(ref domain) = self.domain { - cookie.set_domain(domain.clone()); - } - - if let Some(max_age) = self.max_age { - cookie.set_max_age(max_age); - } - - if let Some(same_site) = self.same_site { - cookie.set_same_site(same_site); - } - - let mut jar = CookieJar::new(); - let key = if self.legacy_supported() { - &self.key - } else { - &self.key_v2 - }; - if add_cookie { - jar.private(&key).add(cookie); - } else { - jar.add_original(cookie.clone()); - jar.private(&key).remove(cookie); - } - for cookie in jar.delta() { - let val = HeaderValue::from_str(&cookie.to_string())?; - resp.headers_mut().append(header::SET_COOKIE, val); - } - Ok(()) - } - - fn load(&self, req: &ServiceRequest) -> Option { - let cookie = req.cookie(&self.name)?; - let mut jar = CookieJar::new(); - jar.add_original(cookie.clone()); - let res = if self.legacy_supported() { - jar.private(&self.key).get(&self.name).map(|n| CookieValue { - identity: n.value().to_string(), - login_timestamp: None, - visit_timestamp: None, - }) - } else { - None - }; - res.or_else(|| { - jar.private(&self.key_v2) - .get(&self.name) - .and_then(|c| self.parse(c)) - }) - } - - fn parse(&self, cookie: Cookie) -> Option { - let value: CookieValue = serde_json::from_str(cookie.value()).ok()?; - let now = SystemTime::now(); - if let Some(visit_deadline) = self.visit_deadline { - if now.duration_since(value.visit_timestamp?).ok()? - > visit_deadline - { - return None; - } - } - if let Some(login_deadline) = self.login_deadline { - if now.duration_since(value.login_timestamp?).ok()? - > login_deadline - { - return None; - } - } - Some(value) - } - - fn legacy_supported(&self) -> bool { - self.visit_deadline.is_none() && self.login_deadline.is_none() - } - - fn always_update_cookie(&self) -> bool { - self.visit_deadline.is_some() - } - - fn requires_oob_data(&self) -> bool { - self.login_deadline.is_some() - } -} - -/// Use cookies for request identity storage. -/// -/// The constructors take a key as an argument. -/// This is the private key for cookie - when this value is changed, -/// all identities are lost. The constructors will panic if the key is less -/// than 32 bytes in length. -/// -/// # Example -/// -/// ```rust -/// use actix_web::App; -/// use actix_identity::{CookieIdentityPolicy, IdentityService}; -/// -/// fn main() { -/// let app = App::new().wrap(IdentityService::new( -/// // <- create identity middleware -/// CookieIdentityPolicy::new(&[0; 32]) // <- construct cookie policy -/// .domain("www.rust-lang.org") -/// .name("actix_auth") -/// .path("/") -/// .secure(true), -/// )); -/// } -/// ``` -pub struct CookieIdentityPolicy(Rc); - -impl CookieIdentityPolicy { - /// Construct new `CookieIdentityPolicy` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn new(key: &[u8]) -> CookieIdentityPolicy { - CookieIdentityPolicy(Rc::new(CookieIdentityInner::new(key))) - } - - /// Sets the `path` field in the session cookie being built. - pub fn path>(mut self, value: S) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().path = value.into(); - self - } - - /// Sets the `name` field in the session cookie being built. - pub fn name>(mut self, value: S) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().name = value.into(); - self - } - - /// Sets the `domain` field in the session cookie being built. - pub fn domain>(mut self, value: S) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); - self - } - - /// Sets the `secure` field in the session cookie being built. - /// - /// If the `secure` field is set, a cookie will only be transmitted when the - /// connection is secure - i.e. `https` - pub fn secure(mut self, value: bool) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().secure = value; - self - } - - /// Sets the `max-age` field in the session cookie being built with given number of seconds. - pub fn max_age(self, seconds: i64) -> CookieIdentityPolicy { - self.max_age_time(Duration::seconds(seconds)) - } - - /// Sets the `max-age` field in the session cookie being built with `chrono::Duration`. - pub fn max_age_time(mut self, value: Duration) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); - self - } - - /// Sets the `same_site` field in the session cookie being built. - pub fn same_site(mut self, same_site: SameSite) -> Self { - Rc::get_mut(&mut self.0).unwrap().same_site = Some(same_site); - self - } - - /// Accepts only users whose cookie has been seen before the given deadline - /// - /// By default visit deadline is disabled. - pub fn visit_deadline(mut self, value: Duration) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().visit_deadline = Some(value); - self - } - - /// Accepts only users which has been authenticated before the given deadline - /// - /// By default login deadline is disabled. - pub fn login_deadline(mut self, value: Duration) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().login_deadline = Some(value); - self - } -} - -impl IdentityPolicy for CookieIdentityPolicy { - type Future = Ready, Error>>; - type ResponseFuture = Ready>; - - fn from_request(&self, req: &mut ServiceRequest) -> Self::Future { - ok(self.0.load(req).map( - |CookieValue { - identity, - login_timestamp, - .. - }| { - if self.0.requires_oob_data() { - req.extensions_mut() - .insert(CookieIdentityExtention { login_timestamp }); - } - identity - }, - )) - } - - fn to_response( - &self, - id: Option, - changed: bool, - res: &mut ServiceResponse, - ) -> Self::ResponseFuture { - let _ = if changed { - let login_timestamp = SystemTime::now(); - self.0.set_cookie( - res, - id.map(|identity| CookieValue { - identity, - login_timestamp: self.0.login_deadline.map(|_| login_timestamp), - visit_timestamp: self.0.visit_deadline.map(|_| login_timestamp), - }), - ) - } else if self.0.always_update_cookie() && id.is_some() { - let visit_timestamp = SystemTime::now(); - let login_timestamp = if self.0.requires_oob_data() { - let CookieIdentityExtention { - login_timestamp: lt, - } = res.request().extensions_mut().remove().unwrap(); - lt - } else { - None - }; - self.0.set_cookie( - res, - Some(CookieValue { - identity: id.unwrap(), - login_timestamp, - visit_timestamp: self.0.visit_deadline.map(|_| visit_timestamp), - }), - ) - } else { - Ok(()) - }; - ok(()) - } -} - -#[cfg(test)] -mod tests { - use std::borrow::Borrow; - - use super::*; - use actix_service::into_service; - use actix_web::http::StatusCode; - use actix_web::test::{self, TestRequest}; - use actix_web::{error, web, App, Error, HttpResponse}; - - const COOKIE_KEY_MASTER: [u8; 32] = [0; 32]; - const COOKIE_NAME: &'static str = "actix_auth"; - const COOKIE_LOGIN: &'static str = "test"; - - #[actix_rt::test] - async fn test_identity() { - let mut srv = test::init_service( - App::new() - .wrap(IdentityService::new( - CookieIdentityPolicy::new(&COOKIE_KEY_MASTER) - .domain("www.rust-lang.org") - .name(COOKIE_NAME) - .path("/") - .secure(true), - )) - .service(web::resource("/index").to(|id: Identity| { - if id.identity().is_some() { - HttpResponse::Created() - } else { - HttpResponse::Ok() - } - })) - .service(web::resource("/login").to(|id: Identity| { - id.remember(COOKIE_LOGIN.to_string()); - HttpResponse::Ok() - })) - .service(web::resource("/logout").to(|id: Identity| { - if id.identity().is_some() { - id.forget(); - HttpResponse::Ok() - } else { - HttpResponse::BadRequest() - } - })), - ) - .await; - let resp = - test::call_service(&mut srv, TestRequest::with_uri("/index").to_request()) - .await; - assert_eq!(resp.status(), StatusCode::OK); - - let resp = - test::call_service(&mut srv, TestRequest::with_uri("/login").to_request()) - .await; - assert_eq!(resp.status(), StatusCode::OK); - let c = resp.response().cookies().next().unwrap().to_owned(); - - let resp = test::call_service( - &mut srv, - TestRequest::with_uri("/index") - .cookie(c.clone()) - .to_request(), - ) - .await; - assert_eq!(resp.status(), StatusCode::CREATED); - - let resp = test::call_service( - &mut srv, - TestRequest::with_uri("/logout") - .cookie(c.clone()) - .to_request(), - ) - .await; - assert_eq!(resp.status(), StatusCode::OK); - assert!(resp.headers().contains_key(header::SET_COOKIE)) - } - - #[actix_rt::test] - async fn test_identity_max_age_time() { - let duration = Duration::days(1); - let mut srv = test::init_service( - App::new() - .wrap(IdentityService::new( - CookieIdentityPolicy::new(&COOKIE_KEY_MASTER) - .domain("www.rust-lang.org") - .name(COOKIE_NAME) - .path("/") - .max_age_time(duration) - .secure(true), - )) - .service(web::resource("/login").to(|id: Identity| { - id.remember("test".to_string()); - HttpResponse::Ok() - })), - ) - .await; - let resp = - test::call_service(&mut srv, TestRequest::with_uri("/login").to_request()) - .await; - assert_eq!(resp.status(), StatusCode::OK); - assert!(resp.headers().contains_key(header::SET_COOKIE)); - let c = resp.response().cookies().next().unwrap().to_owned(); - assert_eq!(duration, c.max_age().unwrap()); - } - - #[actix_rt::test] - async fn test_identity_max_age() { - let seconds = 60; - let mut srv = test::init_service( - App::new() - .wrap(IdentityService::new( - CookieIdentityPolicy::new(&COOKIE_KEY_MASTER) - .domain("www.rust-lang.org") - .name(COOKIE_NAME) - .path("/") - .max_age(seconds) - .secure(true), - )) - .service(web::resource("/login").to(|id: Identity| { - id.remember("test".to_string()); - HttpResponse::Ok() - })), - ) - .await; - let resp = - test::call_service(&mut srv, TestRequest::with_uri("/login").to_request()) - .await; - assert_eq!(resp.status(), StatusCode::OK); - assert!(resp.headers().contains_key(header::SET_COOKIE)); - let c = resp.response().cookies().next().unwrap().to_owned(); - assert_eq!(Duration::seconds(seconds as i64), c.max_age().unwrap()); - } - - async fn create_identity_server< - F: Fn(CookieIdentityPolicy) -> CookieIdentityPolicy + Sync + Send + Clone + 'static, - >( - f: F, - ) -> impl actix_service::Service< - Request = actix_http::Request, - Response = ServiceResponse, - Error = Error, - > { - test::init_service( - App::new() - .wrap(IdentityService::new(f(CookieIdentityPolicy::new( - &COOKIE_KEY_MASTER, - ) - .secure(false) - .name(COOKIE_NAME)))) - .service(web::resource("/").to(|id: Identity| { - async move { - let identity = id.identity(); - if identity.is_none() { - id.remember(COOKIE_LOGIN.to_string()) - } - web::Json(identity) - } - })), - ) - .await - } - - fn legacy_login_cookie(identity: &'static str) -> Cookie<'static> { - let mut jar = CookieJar::new(); - jar.private(&Key::from_master(&COOKIE_KEY_MASTER)) - .add(Cookie::new(COOKIE_NAME, identity)); - jar.get(COOKIE_NAME).unwrap().clone() - } - - fn login_cookie( - identity: &'static str, - login_timestamp: Option, - visit_timestamp: Option, - ) -> Cookie<'static> { - let mut jar = CookieJar::new(); - let key: Vec = COOKIE_KEY_MASTER - .iter() - .chain([1, 0, 0, 0].iter()) - .map(|e| *e) - .collect(); - jar.private(&Key::from_master(&key)).add(Cookie::new( - COOKIE_NAME, - serde_json::to_string(&CookieValue { - identity: identity.to_string(), - login_timestamp, - visit_timestamp, - }) - .unwrap(), - )); - jar.get(COOKIE_NAME).unwrap().clone() - } - - async fn assert_logged_in(response: ServiceResponse, identity: Option<&str>) { - let bytes = test::read_body(response).await; - let resp: Option = serde_json::from_slice(&bytes[..]).unwrap(); - assert_eq!(resp.as_ref().map(|s| s.borrow()), identity); - } - - fn assert_legacy_login_cookie(response: &mut ServiceResponse, identity: &str) { - let mut cookies = CookieJar::new(); - for cookie in response.headers().get_all(header::SET_COOKIE) { - cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap()); - } - let cookie = cookies - .private(&Key::from_master(&COOKIE_KEY_MASTER)) - .get(COOKIE_NAME) - .unwrap(); - assert_eq!(cookie.value(), identity); - } - - enum LoginTimestampCheck { - NoTimestamp, - NewTimestamp, - OldTimestamp(SystemTime), - } - - enum VisitTimeStampCheck { - NoTimestamp, - NewTimestamp, - } - - fn assert_login_cookie( - response: &mut ServiceResponse, - identity: &str, - login_timestamp: LoginTimestampCheck, - visit_timestamp: VisitTimeStampCheck, - ) { - let mut cookies = CookieJar::new(); - for cookie in response.headers().get_all(header::SET_COOKIE) { - cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap()); - } - let key: Vec = COOKIE_KEY_MASTER - .iter() - .chain([1, 0, 0, 0].iter()) - .map(|e| *e) - .collect(); - let cookie = cookies - .private(&Key::from_master(&key)) - .get(COOKIE_NAME) - .unwrap(); - let cv: CookieValue = serde_json::from_str(cookie.value()).unwrap(); - assert_eq!(cv.identity, identity); - let now = SystemTime::now(); - let t30sec_ago = now - Duration::seconds(30); - match login_timestamp { - LoginTimestampCheck::NoTimestamp => assert_eq!(cv.login_timestamp, None), - LoginTimestampCheck::NewTimestamp => assert!( - t30sec_ago <= cv.login_timestamp.unwrap() - && cv.login_timestamp.unwrap() <= now - ), - LoginTimestampCheck::OldTimestamp(old_timestamp) => { - assert_eq!(cv.login_timestamp, Some(old_timestamp)) - } - } - match visit_timestamp { - VisitTimeStampCheck::NoTimestamp => assert_eq!(cv.visit_timestamp, None), - VisitTimeStampCheck::NewTimestamp => assert!( - t30sec_ago <= cv.visit_timestamp.unwrap() - && cv.visit_timestamp.unwrap() <= now - ), - } - } - - fn assert_no_login_cookie(response: &mut ServiceResponse) { - let mut cookies = CookieJar::new(); - for cookie in response.headers().get_all(header::SET_COOKIE) { - cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap()); - } - assert!(cookies.get(COOKIE_NAME).is_none()); - } - - #[actix_rt::test] - async fn test_identity_legacy_cookie_is_set() { - let mut srv = create_identity_server(|c| c).await; - let mut resp = - test::call_service(&mut srv, TestRequest::with_uri("/").to_request()).await; - assert_legacy_login_cookie(&mut resp, COOKIE_LOGIN); - assert_logged_in(resp, None).await; - } - - #[actix_rt::test] - async fn test_identity_legacy_cookie_works() { - let mut srv = create_identity_server(|c| c).await; - let cookie = legacy_login_cookie(COOKIE_LOGIN); - let mut resp = test::call_service( - &mut srv, - TestRequest::with_uri("/") - .cookie(cookie.clone()) - .to_request(), - ) - .await; - assert_no_login_cookie(&mut resp); - assert_logged_in(resp, Some(COOKIE_LOGIN)).await; - } - - #[actix_rt::test] - async fn test_identity_legacy_cookie_rejected_if_visit_timestamp_needed() { - let mut srv = - create_identity_server(|c| c.visit_deadline(Duration::days(90))).await; - let cookie = legacy_login_cookie(COOKIE_LOGIN); - let mut resp = test::call_service( - &mut srv, - TestRequest::with_uri("/") - .cookie(cookie.clone()) - .to_request(), - ) - .await; - assert_login_cookie( - &mut resp, - COOKIE_LOGIN, - LoginTimestampCheck::NoTimestamp, - VisitTimeStampCheck::NewTimestamp, - ); - assert_logged_in(resp, None).await; - } - - #[actix_rt::test] - async fn test_identity_legacy_cookie_rejected_if_login_timestamp_needed() { - let mut srv = - create_identity_server(|c| c.login_deadline(Duration::days(90))).await; - let cookie = legacy_login_cookie(COOKIE_LOGIN); - let mut resp = test::call_service( - &mut srv, - TestRequest::with_uri("/") - .cookie(cookie.clone()) - .to_request(), - ) - .await; - assert_login_cookie( - &mut resp, - COOKIE_LOGIN, - LoginTimestampCheck::NewTimestamp, - VisitTimeStampCheck::NoTimestamp, - ); - assert_logged_in(resp, None).await; - } - - #[actix_rt::test] - async fn test_identity_cookie_rejected_if_login_timestamp_needed() { - let mut srv = - create_identity_server(|c| c.login_deadline(Duration::days(90))).await; - let cookie = login_cookie(COOKIE_LOGIN, None, Some(SystemTime::now())); - let mut resp = test::call_service( - &mut srv, - TestRequest::with_uri("/") - .cookie(cookie.clone()) - .to_request(), - ) - .await; - assert_login_cookie( - &mut resp, - COOKIE_LOGIN, - LoginTimestampCheck::NewTimestamp, - VisitTimeStampCheck::NoTimestamp, - ); - assert_logged_in(resp, None).await; - } - - #[actix_rt::test] - async fn test_identity_cookie_rejected_if_visit_timestamp_needed() { - let mut srv = - create_identity_server(|c| c.visit_deadline(Duration::days(90))).await; - let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None); - let mut resp = test::call_service( - &mut srv, - TestRequest::with_uri("/") - .cookie(cookie.clone()) - .to_request(), - ) - .await; - assert_login_cookie( - &mut resp, - COOKIE_LOGIN, - LoginTimestampCheck::NoTimestamp, - VisitTimeStampCheck::NewTimestamp, - ); - assert_logged_in(resp, None).await; - } - - #[actix_rt::test] - async fn test_identity_cookie_rejected_if_login_timestamp_too_old() { - let mut srv = - create_identity_server(|c| c.login_deadline(Duration::days(90))).await; - let cookie = login_cookie( - COOKIE_LOGIN, - Some(SystemTime::now() - Duration::days(180)), - None, - ); - let mut resp = test::call_service( - &mut srv, - TestRequest::with_uri("/") - .cookie(cookie.clone()) - .to_request(), - ) - .await; - assert_login_cookie( - &mut resp, - COOKIE_LOGIN, - LoginTimestampCheck::NewTimestamp, - VisitTimeStampCheck::NoTimestamp, - ); - assert_logged_in(resp, None).await; - } - - #[actix_rt::test] - async fn test_identity_cookie_rejected_if_visit_timestamp_too_old() { - let mut srv = - create_identity_server(|c| c.visit_deadline(Duration::days(90))).await; - let cookie = login_cookie( - COOKIE_LOGIN, - None, - Some(SystemTime::now() - Duration::days(180)), - ); - let mut resp = test::call_service( - &mut srv, - TestRequest::with_uri("/") - .cookie(cookie.clone()) - .to_request(), - ) - .await; - assert_login_cookie( - &mut resp, - COOKIE_LOGIN, - LoginTimestampCheck::NoTimestamp, - VisitTimeStampCheck::NewTimestamp, - ); - assert_logged_in(resp, None).await; - } - - #[actix_rt::test] - async fn test_identity_cookie_not_updated_on_login_deadline() { - let mut srv = - create_identity_server(|c| c.login_deadline(Duration::days(90))).await; - let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None); - let mut resp = test::call_service( - &mut srv, - TestRequest::with_uri("/") - .cookie(cookie.clone()) - .to_request(), - ) - .await; - assert_no_login_cookie(&mut resp); - assert_logged_in(resp, Some(COOKIE_LOGIN)).await; - } - - // https://github.com/actix/actix-web/issues/1263 - #[actix_rt::test] - async fn test_identity_cookie_updated_on_visit_deadline() { - let mut srv = create_identity_server(|c| { - c.visit_deadline(Duration::days(90)) - .login_deadline(Duration::days(90)) - }) - .await; - let timestamp = SystemTime::now() - Duration::days(1); - let cookie = login_cookie(COOKIE_LOGIN, Some(timestamp), Some(timestamp)); - let mut resp = test::call_service( - &mut srv, - TestRequest::with_uri("/") - .cookie(cookie.clone()) - .to_request(), - ) - .await; - assert_login_cookie( - &mut resp, - COOKIE_LOGIN, - LoginTimestampCheck::OldTimestamp(timestamp), - VisitTimeStampCheck::NewTimestamp, - ); - assert_logged_in(resp, Some(COOKIE_LOGIN)).await; - } - - #[actix_rt::test] - async fn test_borrowed_mut_error() { - use futures::future::{lazy, ok, Ready}; - - struct Ident; - impl IdentityPolicy for Ident { - type Future = Ready, Error>>; - type ResponseFuture = Ready>; - - fn from_request(&self, _: &mut ServiceRequest) -> Self::Future { - ok(Some("test".to_string())) - } - - fn to_response( - &self, - _: Option, - _: bool, - _: &mut ServiceResponse, - ) -> Self::ResponseFuture { - ok(()) - } - } - - let mut srv = IdentityServiceMiddleware { - backend: Rc::new(Ident), - service: Rc::new(RefCell::new(into_service(|_: ServiceRequest| { - async move { - actix_rt::time::delay_for(std::time::Duration::from_secs(100)).await; - Err::(error::ErrorBadRequest("error")) - } - }))), - }; - - let mut srv2 = srv.clone(); - let req = TestRequest::default().to_srv_request(); - actix_rt::spawn(async move { - let _ = srv2.call(req).await; - }); - actix_rt::time::delay_for(std::time::Duration::from_millis(50)).await; - - let _ = lazy(|cx| srv.poll_ready(cx)).await; - } -} diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index d73a69393..df3cecf71 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,9 +1,17 @@ # Changes -## [0.2.1] - 2020-01-xx +## [0.3.0-alpha.1] - 2020-05-25 + +* Update `actix-web` to 3.0.0-alpha.3 + +* Bump minimum supported Rust version to 1.40 + +* Minimize `futures` dependencies * Remove the unused `time` dependency +* Fix missing `std::error::Error` implement for `MultipartError`. + ## [0.2.0] - 2019-12-20 * Release diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index f9cd7cfd2..c5f315d75 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.2.0" +version = "0.3.0-alpha.1" authors = ["Nikolay Kim "] description = "Multipart support for actix web framework." readme = "README.md" @@ -16,17 +16,17 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "2.0.0-rc", default-features = false } +actix-web = { version = "3.0.0-alpha.3", default-features = false } actix-service = "1.0.1" actix-utils = "1.0.3" bytes = "0.5.3" derive_more = "0.99.2" httparse = "1.3" -futures = "0.3.1" +futures-util = { version = "0.3.5", default-features = false } log = "0.4" mime = "0.3" twoway = "0.2" [dev-dependencies] actix-rt = "1.0.0" -actix-http = "1.0.0" +actix-http = "2.0.0-alpha.4" diff --git a/actix-multipart/README.md b/actix-multipart/README.md index a453f489e..edb2e0020 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -5,4 +5,4 @@ * [API Documentation](https://docs.rs/actix-multipart/) * [Chat on gitter](https://gitter.im/actix/actix) * Cargo package: [actix-multipart](https://crates.io/crates/actix-multipart) -* Minimum supported Rust version: 1.39 or later +* Minimum supported Rust version: 1.40 or later diff --git a/actix-multipart/src/error.rs b/actix-multipart/src/error.rs index 6677f69c7..cdbb5d395 100644 --- a/actix-multipart/src/error.rs +++ b/actix-multipart/src/error.rs @@ -33,6 +33,8 @@ pub enum MultipartError { NotConsumed, } +impl std::error::Error for MultipartError {} + /// Return `BadRequest` for `MultipartError` impl ResponseError for MultipartError { fn status_code(&self) -> StatusCode { diff --git a/actix-multipart/src/extractor.rs b/actix-multipart/src/extractor.rs index 71c815227..4e4caee01 100644 --- a/actix-multipart/src/extractor.rs +++ b/actix-multipart/src/extractor.rs @@ -1,6 +1,6 @@ //! Multipart payload support use actix_web::{dev::Payload, Error, FromRequest, HttpRequest}; -use futures::future::{ok, Ready}; +use futures_util::future::{ok, Ready}; use crate::server::Multipart; @@ -11,7 +11,7 @@ use crate::server::Multipart; /// ## Server example /// /// ```rust -/// use futures::{Stream, StreamExt}; +/// use futures_util::stream::{Stream, StreamExt}; /// use actix_web::{web, HttpResponse, Error}; /// use actix_multipart as mp; /// diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index 2555cb7a3..f96a7821a 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -8,7 +8,7 @@ use std::task::{Context, Poll}; use std::{cmp, fmt}; use bytes::{Bytes, BytesMut}; -use futures::stream::{LocalBoxStream, Stream, StreamExt}; +use futures_util::stream::{LocalBoxStream, Stream, StreamExt}; use httparse; use mime; @@ -814,7 +814,7 @@ mod tests { use actix_utils::mpsc; use actix_web::http::header::{DispositionParam, DispositionType}; use bytes::Bytes; - use futures::future::lazy; + use futures_util::future::lazy; #[actix_rt::test] async fn test_boundary() { diff --git a/actix-session/CHANGES.md b/actix-session/CHANGES.md deleted file mode 100644 index f6753ae58..000000000 --- a/actix-session/CHANGES.md +++ /dev/null @@ -1,73 +0,0 @@ -# Changes - -## [Unreleased] - 2020-01-xx - -* Update the `time` dependency to 0.2.5 -* [#1292](https://github.com/actix/actix-web/pull/1292) Long lasting auto-prolonged session - -## [0.3.0] - 2019-12-20 - -* Release - -## [0.3.0-alpha.4] - 2019-12-xx - -* Allow access to sessions also from not mutable references to the request - -## [0.3.0-alpha.3] - 2019-12-xx - -* Add access to the session from RequestHead for use of session from guard methods - -* Migrate to `std::future` - -* Migrate to `actix-web` 2.0 - -## [0.2.0] - 2019-07-08 - -* Enhanced ``actix-session`` to facilitate state changes. Use ``Session.renew()`` - at successful login to cycle a session (new key/cookie but keeps state). - Use ``Session.purge()`` at logout to invalid a session cookie (and remove - from redis cache, if applicable). - -## [0.1.1] - 2019-06-03 - -* Fix optional cookie session support - -## [0.1.0] - 2019-05-18 - -* Use actix-web 1.0.0-rc - -## [0.1.0-beta.4] - 2019-05-12 - -* Use actix-web 1.0.0-beta.4 - -## [0.1.0-beta.2] - 2019-04-28 - -* Add helper trait `UserSession` which allows to get session for ServiceRequest and HttpRequest - -## [0.1.0-beta.1] - 2019-04-20 - -* Update actix-web to beta.1 - -* `CookieSession::max_age()` accepts value in seconds - -## [0.1.0-alpha.6] - 2019-04-14 - -* Update actix-web alpha.6 - -## [0.1.0-alpha.4] - 2019-04-08 - -* Update actix-web - -## [0.1.0-alpha.3] - 2019-04-02 - -* Update actix-web - -## [0.1.0-alpha.2] - 2019-03-29 - -* Update actix-web - -* Use new feature name for secure cookies - -## [0.1.0-alpha.1] - 2019-03-28 - -* Initial impl diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml deleted file mode 100644 index b279c9d89..000000000 --- a/actix-session/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "actix-session" -version = "0.3.0" -authors = ["Nikolay Kim "] -description = "Session for actix web framework." -readme = "README.md" -keywords = ["http", "web", "framework", "async", "futures"] -homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-session/" -license = "MIT/Apache-2.0" -edition = "2018" - -[lib] -name = "actix_session" -path = "src/lib.rs" - -[features] -default = ["cookie-session"] - -# sessions feature, session require "ring" crate and c compiler -cookie-session = ["actix-web/secure-cookies"] - -[dependencies] -actix-web = "2.0.0-rc" -actix-service = "1.0.1" -bytes = "0.5.3" -derive_more = "0.99.2" -futures = "0.3.1" -serde = "1.0" -serde_json = "1.0" -time = { version = "0.2.5", default-features = false, features = ["std"] } - -[dev-dependencies] -actix-rt = "1.0.0" diff --git a/actix-session/LICENSE-APACHE b/actix-session/LICENSE-APACHE deleted file mode 120000 index 965b606f3..000000000 --- a/actix-session/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-APACHE \ No newline at end of file diff --git a/actix-session/LICENSE-MIT b/actix-session/LICENSE-MIT deleted file mode 120000 index 76219eb72..000000000 --- a/actix-session/LICENSE-MIT +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-MIT \ No newline at end of file diff --git a/actix-session/README.md b/actix-session/README.md index 0aee756fd..00e580120 100644 --- a/actix-session/README.md +++ b/actix-session/README.md @@ -1,5 +1,7 @@ # Session 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-session)](https://crates.io/crates/actix-session) [![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) +**This crate moved to https://github.com/actix/actix-extras.** + ## Documentation & community resources * [User Guide](https://actix.rs/docs/) diff --git a/actix-session/src/cookie.rs b/actix-session/src/cookie.rs deleted file mode 100644 index b5297f561..000000000 --- a/actix-session/src/cookie.rs +++ /dev/null @@ -1,545 +0,0 @@ -//! Cookie session. -//! -//! [**CookieSession**](struct.CookieSession.html) -//! uses cookies as session storage. `CookieSession` creates sessions -//! which are limited to storing fewer than 4000 bytes of data, as the payload -//! must fit into a single cookie. An internal server error is generated if a -//! session contains more than 4000 bytes. -//! -//! A cookie may have a security policy of *signed* or *private*. Each has -//! a respective `CookieSession` constructor. -//! -//! A *signed* cookie may be viewed but not modified by the client. A *private* -//! cookie may neither be viewed nor modified by the client. -//! -//! The constructors take a key as an argument. This is the private key -//! for cookie session - when this value is changed, all session data is lost. - -use std::collections::HashMap; -use std::rc::Rc; -use std::task::{Context, Poll}; - -use actix_service::{Service, Transform}; -use actix_web::cookie::{Cookie, CookieJar, Key, SameSite}; -use actix_web::dev::{ServiceRequest, ServiceResponse}; -use actix_web::http::{header::SET_COOKIE, HeaderValue}; -use actix_web::{Error, HttpMessage, ResponseError}; -use derive_more::{Display, From}; -use futures::future::{ok, FutureExt, LocalBoxFuture, Ready}; -use serde_json::error::Error as JsonError; -use time::{Duration, OffsetDateTime}; - -use crate::{Session, SessionStatus}; - -/// Errors that can occur during handling cookie session -#[derive(Debug, From, Display)] -pub enum CookieSessionError { - /// Size of the serialized session is greater than 4000 bytes. - #[display(fmt = "Size of the serialized session is greater than 4000 bytes.")] - Overflow, - /// Fail to serialize session. - #[display(fmt = "Fail to serialize session")] - Serialize(JsonError), -} - -impl ResponseError for CookieSessionError {} - -enum CookieSecurity { - Signed, - Private, -} - -struct CookieSessionInner { - key: Key, - security: CookieSecurity, - name: String, - path: String, - domain: Option, - secure: bool, - http_only: bool, - max_age: Option, - expires_in: Option, - same_site: Option, -} - -impl CookieSessionInner { - fn new(key: &[u8], security: CookieSecurity) -> CookieSessionInner { - CookieSessionInner { - security, - key: Key::from_master(key), - name: "actix-session".to_owned(), - path: "/".to_owned(), - domain: None, - secure: true, - http_only: true, - max_age: None, - expires_in: None, - same_site: None, - } - } - - fn set_cookie( - &self, - res: &mut ServiceResponse, - state: impl Iterator, - ) -> Result<(), Error> { - let state: HashMap = state.collect(); - let value = - serde_json::to_string(&state).map_err(CookieSessionError::Serialize)?; - if value.len() > 4064 { - return Err(CookieSessionError::Overflow.into()); - } - - let mut cookie = Cookie::new(self.name.clone(), value); - cookie.set_path(self.path.clone()); - cookie.set_secure(self.secure); - cookie.set_http_only(self.http_only); - - if let Some(ref domain) = self.domain { - cookie.set_domain(domain.clone()); - } - - if let Some(expires_in) = self.expires_in { - cookie.set_expires(OffsetDateTime::now() + expires_in); - } - - if let Some(max_age) = self.max_age { - cookie.set_max_age(max_age); - } - - if let Some(same_site) = self.same_site { - cookie.set_same_site(same_site); - } - - let mut jar = CookieJar::new(); - - match self.security { - CookieSecurity::Signed => jar.signed(&self.key).add(cookie), - CookieSecurity::Private => jar.private(&self.key).add(cookie), - } - - for cookie in jar.delta() { - let val = HeaderValue::from_str(&cookie.encoded().to_string())?; - res.headers_mut().append(SET_COOKIE, val); - } - - Ok(()) - } - - /// invalidates session cookie - fn remove_cookie(&self, res: &mut ServiceResponse) -> Result<(), Error> { - let mut cookie = Cookie::named(self.name.clone()); - cookie.set_value(""); - cookie.set_max_age(Duration::zero()); - cookie.set_expires(OffsetDateTime::now() - Duration::days(365)); - - let val = HeaderValue::from_str(&cookie.to_string())?; - res.headers_mut().append(SET_COOKIE, val); - - Ok(()) - } - - fn load(&self, req: &ServiceRequest) -> (bool, HashMap) { - if let Ok(cookies) = req.cookies() { - for cookie in cookies.iter() { - if cookie.name() == self.name { - let mut jar = CookieJar::new(); - jar.add_original(cookie.clone()); - - let cookie_opt = match self.security { - CookieSecurity::Signed => jar.signed(&self.key).get(&self.name), - CookieSecurity::Private => { - jar.private(&self.key).get(&self.name) - } - }; - if let Some(cookie) = cookie_opt { - if let Ok(val) = serde_json::from_str(cookie.value()) { - return (false, val); - } - } - } - } - } - (true, HashMap::new()) - } -} - -/// Use cookies for session storage. -/// -/// `CookieSession` creates sessions which are limited to storing -/// fewer than 4000 bytes of data (as the payload must fit into a single -/// cookie). An Internal Server Error is generated if the session contains more -/// than 4000 bytes. -/// -/// A cookie may have a security policy of *signed* or *private*. Each has a -/// respective `CookieSessionBackend` constructor. -/// -/// A *signed* cookie is stored on the client as plaintext alongside -/// a signature such that the cookie may be viewed but not modified by the -/// client. -/// -/// A *private* cookie is stored on the client as encrypted text -/// such that it may neither be viewed nor modified by the client. -/// -/// The constructors take a key as an argument. -/// This is the private key for cookie session - when this value is changed, -/// all session data is lost. The constructors will panic if the key is less -/// than 32 bytes in length. -/// -/// The backend relies on `cookie` crate to create and read cookies. -/// By default all cookies are percent encoded, but certain symbols may -/// cause troubles when reading cookie, if they are not properly percent encoded. -/// -/// # Example -/// -/// ```rust -/// use actix_session::CookieSession; -/// use actix_web::{web, App, HttpResponse, HttpServer}; -/// -/// fn main() { -/// let app = App::new().wrap( -/// CookieSession::signed(&[0; 32]) -/// .domain("www.rust-lang.org") -/// .name("actix_session") -/// .path("/") -/// .secure(true)) -/// .service(web::resource("/").to(|| HttpResponse::Ok())); -/// } -/// ``` -pub struct CookieSession(Rc); - -impl CookieSession { - /// Construct new *signed* `CookieSessionBackend` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn signed(key: &[u8]) -> CookieSession { - CookieSession(Rc::new(CookieSessionInner::new( - key, - CookieSecurity::Signed, - ))) - } - - /// Construct new *private* `CookieSessionBackend` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn private(key: &[u8]) -> CookieSession { - CookieSession(Rc::new(CookieSessionInner::new( - key, - CookieSecurity::Private, - ))) - } - - /// Sets the `path` field in the session cookie being built. - pub fn path>(mut self, value: S) -> CookieSession { - Rc::get_mut(&mut self.0).unwrap().path = value.into(); - self - } - - /// Sets the `name` field in the session cookie being built. - pub fn name>(mut self, value: S) -> CookieSession { - Rc::get_mut(&mut self.0).unwrap().name = value.into(); - self - } - - /// Sets the `domain` field in the session cookie being built. - pub fn domain>(mut self, value: S) -> CookieSession { - Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); - self - } - - /// Sets the `secure` field in the session cookie being built. - /// - /// If the `secure` field is set, a cookie will only be transmitted when the - /// connection is secure - i.e. `https` - pub fn secure(mut self, value: bool) -> CookieSession { - Rc::get_mut(&mut self.0).unwrap().secure = value; - self - } - - /// Sets the `http_only` field in the session cookie being built. - pub fn http_only(mut self, value: bool) -> CookieSession { - Rc::get_mut(&mut self.0).unwrap().http_only = value; - self - } - - /// Sets the `same_site` field in the session cookie being built. - pub fn same_site(mut self, value: SameSite) -> CookieSession { - Rc::get_mut(&mut self.0).unwrap().same_site = Some(value); - self - } - - /// Sets the `max-age` field in the session cookie being built. - pub fn max_age(self, seconds: i64) -> CookieSession { - self.max_age_time(Duration::seconds(seconds)) - } - - /// Sets the `max-age` field in the session cookie being built. - pub fn max_age_time(mut self, value: time::Duration) -> CookieSession { - Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); - self - } - - /// Sets the `expires` field in the session cookie being built. - pub fn expires_in(self, seconds: i64) -> CookieSession { - self.expires_in_time(Duration::seconds(seconds)) - } - - /// Sets the `expires` field in the session cookie being built. - pub fn expires_in_time(mut self, value: Duration) -> CookieSession { - Rc::get_mut(&mut self.0).unwrap().expires_in = Some(value); - self - } -} - -impl Transform for CookieSession -where - S: Service>, - S::Future: 'static, - S::Error: 'static, -{ - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = S::Error; - type InitError = (); - type Transform = CookieSessionMiddleware; - type Future = Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - ok(CookieSessionMiddleware { - service, - inner: self.0.clone(), - }) - } -} - -/// Cookie session middleware -pub struct CookieSessionMiddleware { - service: S, - inner: Rc, -} - -impl Service for CookieSessionMiddleware -where - S: Service>, - S::Future: 'static, - S::Error: 'static, -{ - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = S::Error; - type Future = LocalBoxFuture<'static, Result>; - - fn poll_ready(&mut self, cx: &mut Context) -> Poll> { - self.service.poll_ready(cx) - } - - /// On first request, a new session cookie is returned in response, regardless - /// of whether any session state is set. With subsequent requests, if the - /// session state changes, then set-cookie is returned in response. As - /// a user logs out, call session.purge() to set SessionStatus accordingly - /// and this will trigger removal of the session cookie in the response. - fn call(&mut self, mut req: ServiceRequest) -> Self::Future { - let inner = self.inner.clone(); - let (is_new, state) = self.inner.load(&req); - let prolong_expiration = self.inner.expires_in.is_some(); - Session::set_session(state.into_iter(), &mut req); - - let fut = self.service.call(req); - - async move { - fut.await.map(|mut res| { - match Session::get_changes(&mut res) { - (SessionStatus::Changed, Some(state)) - | (SessionStatus::Renewed, Some(state)) => { - res.checked_expr(|res| inner.set_cookie(res, state)) - } - (SessionStatus::Unchanged, Some(state)) if prolong_expiration => { - res.checked_expr(|res| inner.set_cookie(res, state)) - } - (SessionStatus::Unchanged, _) => - // set a new session cookie upon first request (new client) - { - if is_new { - let state: HashMap = HashMap::new(); - res.checked_expr(|res| { - inner.set_cookie(res, state.into_iter()) - }) - } else { - res - } - } - (SessionStatus::Purged, _) => { - let _ = inner.remove_cookie(&mut res); - res - } - _ => res, - } - }) - } - .boxed_local() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use actix_web::{test, web, App}; - use bytes::Bytes; - - #[actix_rt::test] - async fn cookie_session() { - let mut app = test::init_service( - App::new() - .wrap(CookieSession::signed(&[0; 32]).secure(false)) - .service(web::resource("/").to(|ses: Session| { - async move { - let _ = ses.set("counter", 100); - "test" - } - })), - ) - .await; - - let request = test::TestRequest::get().to_request(); - let response = app.call(request).await.unwrap(); - assert!(response - .response() - .cookies() - .find(|c| c.name() == "actix-session") - .is_some()); - } - - #[actix_rt::test] - async fn private_cookie() { - let mut app = test::init_service( - App::new() - .wrap(CookieSession::private(&[0; 32]).secure(false)) - .service(web::resource("/").to(|ses: Session| { - async move { - let _ = ses.set("counter", 100); - "test" - } - })), - ) - .await; - - let request = test::TestRequest::get().to_request(); - let response = app.call(request).await.unwrap(); - assert!(response - .response() - .cookies() - .find(|c| c.name() == "actix-session") - .is_some()); - } - - #[actix_rt::test] - async fn cookie_session_extractor() { - let mut app = test::init_service( - App::new() - .wrap(CookieSession::signed(&[0; 32]).secure(false)) - .service(web::resource("/").to(|ses: Session| { - async move { - let _ = ses.set("counter", 100); - "test" - } - })), - ) - .await; - - let request = test::TestRequest::get().to_request(); - let response = app.call(request).await.unwrap(); - assert!(response - .response() - .cookies() - .find(|c| c.name() == "actix-session") - .is_some()); - } - - #[actix_rt::test] - async fn basics() { - let mut app = test::init_service( - App::new() - .wrap( - CookieSession::signed(&[0; 32]) - .path("/test/") - .name("actix-test") - .domain("localhost") - .http_only(true) - .same_site(SameSite::Lax) - .max_age(100), - ) - .service(web::resource("/").to(|ses: Session| { - async move { - let _ = ses.set("counter", 100); - "test" - } - })) - .service(web::resource("/test/").to(|ses: Session| { - async move { - let val: usize = ses.get("counter").unwrap().unwrap(); - format!("counter: {}", val) - } - })), - ) - .await; - - let request = test::TestRequest::get().to_request(); - let response = app.call(request).await.unwrap(); - let cookie = response - .response() - .cookies() - .find(|c| c.name() == "actix-test") - .unwrap() - .clone(); - assert_eq!(cookie.path().unwrap(), "/test/"); - - let request = test::TestRequest::with_uri("/test/") - .cookie(cookie) - .to_request(); - let body = test::read_response(&mut app, request).await; - assert_eq!(body, Bytes::from_static(b"counter: 100")); - } - - #[actix_rt::test] - async fn prolong_expiration() { - let mut app = test::init_service( - App::new() - .wrap(CookieSession::signed(&[0; 32]).secure(false).expires_in(60)) - .service(web::resource("/").to(|ses: Session| { - async move { - let _ = ses.set("counter", 100); - "test" - } - })) - .service( - web::resource("/test/") - .to(|| async move { "no-changes-in-session" }), - ), - ) - .await; - - let request = test::TestRequest::get().to_request(); - let response = app.call(request).await.unwrap(); - let expires_1 = response - .response() - .cookies() - .find(|c| c.name() == "actix-session") - .expect("Cookie is set") - .expires() - .expect("Expiration is set"); - - actix_rt::time::delay_for(std::time::Duration::from_secs(1)).await; - - let request = test::TestRequest::with_uri("/test/").to_request(); - let response = app.call(request).await.unwrap(); - let expires_2 = response - .response() - .cookies() - .find(|c| c.name() == "actix-session") - .expect("Cookie is set") - .expires() - .expect("Expiration is set"); - - assert!(expires_2 - expires_1 >= Duration::seconds(1)); - } -} diff --git a/actix-session/src/lib.rs b/actix-session/src/lib.rs deleted file mode 100644 index b6e5dd331..000000000 --- a/actix-session/src/lib.rs +++ /dev/null @@ -1,322 +0,0 @@ -//! User sessions. -//! -//! Actix provides a general solution for session management. Session -//! middlewares could provide different implementations which could -//! be accessed via general session api. -//! -//! By default, only cookie session backend is implemented. Other -//! backend implementations can be added. -//! -//! In general, you insert a *session* middleware and initialize it -//! , such as a `CookieSessionBackend`. To access session data, -//! [*Session*](struct.Session.html) extractor must be used. Session -//! extractor allows us to get or set session data. -//! -//! ```rust,no_run -//! use actix_web::{web, App, HttpServer, HttpResponse, Error}; -//! use actix_session::{Session, CookieSession}; -//! -//! fn index(session: Session) -> Result<&'static str, Error> { -//! // access session data -//! if let Some(count) = session.get::("counter")? { -//! println!("SESSION value: {}", count); -//! session.set("counter", count+1)?; -//! } else { -//! session.set("counter", 1)?; -//! } -//! -//! Ok("Welcome!") -//! } -//! -//! #[actix_rt::main] -//! async fn main() -> std::io::Result<()> { -//! HttpServer::new( -//! || App::new().wrap( -//! CookieSession::signed(&[0; 32]) // <- create cookie based session middleware -//! .secure(false) -//! ) -//! .service(web::resource("/").to(|| HttpResponse::Ok()))) -//! .bind("127.0.0.1:59880")? -//! .run() -//! .await -//! } -//! ``` -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; - -use actix_web::dev::{ - Extensions, Payload, RequestHead, ServiceRequest, ServiceResponse, -}; -use actix_web::{Error, FromRequest, HttpMessage, HttpRequest}; -use futures::future::{ok, Ready}; -use serde::de::DeserializeOwned; -use serde::Serialize; -use serde_json; - -#[cfg(feature = "cookie-session")] -mod cookie; -#[cfg(feature = "cookie-session")] -pub use crate::cookie::CookieSession; - -/// The high-level interface you use to modify session data. -/// -/// Session object could be obtained with -/// [`RequestSession::session`](trait.RequestSession.html#tymethod.session) -/// method. `RequestSession` trait is implemented for `HttpRequest`. -/// -/// ```rust -/// use actix_session::Session; -/// use actix_web::*; -/// -/// fn index(session: Session) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = session.get::("counter")? { -/// session.set("counter", count + 1)?; -/// } else { -/// session.set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -pub struct Session(Rc>); - -/// Helper trait that allows to get session -pub trait UserSession { - fn get_session(&self) -> Session; -} - -impl UserSession for HttpRequest { - fn get_session(&self) -> Session { - Session::get_session(&mut *self.extensions_mut()) - } -} - -impl UserSession for ServiceRequest { - fn get_session(&self) -> Session { - Session::get_session(&mut *self.extensions_mut()) - } -} - -impl UserSession for RequestHead { - fn get_session(&self) -> Session { - Session::get_session(&mut *self.extensions_mut()) - } -} - -#[derive(PartialEq, Clone, Debug)] -pub enum SessionStatus { - Changed, - Purged, - Renewed, - Unchanged, -} -impl Default for SessionStatus { - fn default() -> SessionStatus { - SessionStatus::Unchanged - } -} - -#[derive(Default)] -struct SessionInner { - state: HashMap, - pub status: SessionStatus, -} - -impl Session { - /// Get a `value` from the session. - pub fn get(&self, key: &str) -> Result, Error> { - if let Some(s) = self.0.borrow().state.get(key) { - Ok(Some(serde_json::from_str(s)?)) - } else { - Ok(None) - } - } - - /// Set a `value` from the session. - pub fn set(&self, key: &str, value: T) -> Result<(), Error> { - let mut inner = self.0.borrow_mut(); - if inner.status != SessionStatus::Purged { - inner.status = SessionStatus::Changed; - inner - .state - .insert(key.to_owned(), serde_json::to_string(&value)?); - } - Ok(()) - } - - /// Remove value from the session. - pub fn remove(&self, key: &str) { - let mut inner = self.0.borrow_mut(); - if inner.status != SessionStatus::Purged { - inner.status = SessionStatus::Changed; - inner.state.remove(key); - } - } - - /// Clear the session. - pub fn clear(&self) { - let mut inner = self.0.borrow_mut(); - if inner.status != SessionStatus::Purged { - inner.status = SessionStatus::Changed; - inner.state.clear() - } - } - - /// Removes session, both client and server side. - pub fn purge(&self) { - let mut inner = self.0.borrow_mut(); - inner.status = SessionStatus::Purged; - inner.state.clear(); - } - - /// Renews the session key, assigning existing session state to new key. - pub fn renew(&self) { - let mut inner = self.0.borrow_mut(); - if inner.status != SessionStatus::Purged { - inner.status = SessionStatus::Renewed; - } - } - - pub fn set_session( - data: impl Iterator, - req: &mut ServiceRequest, - ) { - let session = Session::get_session(&mut *req.extensions_mut()); - let mut inner = session.0.borrow_mut(); - inner.state.extend(data); - } - - pub fn get_changes( - res: &mut ServiceResponse, - ) -> ( - SessionStatus, - Option>, - ) { - if let Some(s_impl) = res - .request() - .extensions() - .get::>>() - { - let state = - std::mem::replace(&mut s_impl.borrow_mut().state, HashMap::new()); - (s_impl.borrow().status.clone(), Some(state.into_iter())) - } else { - (SessionStatus::Unchanged, None) - } - } - - fn get_session(extensions: &mut Extensions) -> Session { - if let Some(s_impl) = extensions.get::>>() { - return Session(Rc::clone(&s_impl)); - } - let inner = Rc::new(RefCell::new(SessionInner::default())); - extensions.insert(inner.clone()); - Session(inner) - } -} - -/// Extractor implementation for Session type. -/// -/// ```rust -/// # use actix_web::*; -/// use actix_session::Session; -/// -/// fn index(session: Session) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = session.get::("counter")? { -/// session.set("counter", count + 1)?; -/// } else { -/// session.set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -impl FromRequest for Session { - type Error = Error; - type Future = Ready>; - type Config = (); - - #[inline] - fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - ok(Session::get_session(&mut *req.extensions_mut())) - } -} - -#[cfg(test)] -mod tests { - use actix_web::{test, HttpResponse}; - - use super::*; - - #[test] - fn session() { - let mut req = test::TestRequest::default().to_srv_request(); - - Session::set_session( - vec![("key".to_string(), "\"value\"".to_string())].into_iter(), - &mut req, - ); - let session = Session::get_session(&mut *req.extensions_mut()); - let res = session.get::("key").unwrap(); - assert_eq!(res, Some("value".to_string())); - - session.set("key2", "value2".to_string()).unwrap(); - session.remove("key"); - - let mut res = req.into_response(HttpResponse::Ok().finish()); - let (_status, state) = Session::get_changes(&mut res); - let changes: Vec<_> = state.unwrap().collect(); - assert_eq!(changes, [("key2".to_string(), "\"value2\"".to_string())]); - } - - #[test] - fn get_session() { - let mut req = test::TestRequest::default().to_srv_request(); - - Session::set_session( - vec![("key".to_string(), "\"value\"".to_string())].into_iter(), - &mut req, - ); - - let session = req.get_session(); - let res = session.get::("key").unwrap(); - assert_eq!(res, Some("value".to_string())); - } - - #[test] - fn get_session_from_request_head() { - let mut req = test::TestRequest::default().to_srv_request(); - - Session::set_session( - vec![("key".to_string(), "\"value\"".to_string())].into_iter(), - &mut req, - ); - - let session = req.head_mut().get_session(); - let res = session.get::("key").unwrap(); - assert_eq!(res, Some("value".to_string())); - } - - #[test] - fn purge_session() { - let req = test::TestRequest::default().to_srv_request(); - let session = Session::get_session(&mut *req.extensions_mut()); - assert_eq!(session.0.borrow().status, SessionStatus::Unchanged); - session.purge(); - assert_eq!(session.0.borrow().status, SessionStatus::Purged); - } - - #[test] - fn renew_session() { - let req = test::TestRequest::default().to_srv_request(); - let session = Session::get_session(&mut *req.extensions_mut()); - assert_eq!(session.0.borrow().status, SessionStatus::Unchanged); - session.renew(); - assert_eq!(session.0.borrow().status, SessionStatus::Renewed); - } -} diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 66ff7ed6c..8fd48f77c 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,5 +1,15 @@ # Changes +## [Unreleased] - 2020-xx-xx + +* Bump minimum supported Rust version to 1.40 + +## [3.0.0-alpha.1] - 2020-05-08 + +* Update the actix-web dependency to 3.0.0-alpha.1 +* Update the actix dependency to 0.10.0-alpha.2 +* Update the actix-http dependency to 2.0.0-alpha.3 + ## [2.0.0] - 2019-12-20 * Release diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 6f573e442..8db7a35ef 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "2.0.0" +version = "3.0.0-alpha.1" authors = ["Nikolay Kim "] description = "Actix actors support for actix web framework." readme = "README.md" @@ -16,14 +16,16 @@ name = "actix_web_actors" path = "src/lib.rs" [dependencies] -actix = "0.9.0" -actix-web = "2.0.0-rc" -actix-http = "1.0.1" +actix = "0.10.0-alpha.2" +actix-web = "3.0.0-alpha.3" +actix-http = "2.0.0-alpha.4" actix-codec = "0.2.0" bytes = "0.5.2" -futures = "0.3.1" -pin-project = "0.4.6" +futures-channel = { version = "0.3.5", default-features = false } +futures-core = { version = "0.3.5", default-features = false } +pin-project = "0.4.17" [dev-dependencies] actix-rt = "1.0.0" -env_logger = "0.6" +env_logger = "0.7" +futures-util = { version = "0.3.5", default-features = false } diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index 6ff7ac67c..fb8c3a621 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -5,4 +5,4 @@ Actix actors support for actix web framework [![Build Status](https://travis-ci. * [API Documentation](https://docs.rs/actix-web-actors/) * [Chat on gitter](https://gitter.im/actix/actix) * Cargo package: [actix-web-actors](https://crates.io/crates/actix-web-actors) -* Minimum supported Rust version: 1.33 or later +* Minimum supported Rust version: 1.40 or later diff --git a/actix-web-actors/src/context.rs b/actix-web-actors/src/context.rs index 6a403de12..0839a4288 100644 --- a/actix-web-actors/src/context.rs +++ b/actix-web-actors/src/context.rs @@ -1,4 +1,5 @@ use std::collections::VecDeque; +use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; @@ -11,8 +12,8 @@ use actix::{ }; use actix_web::error::Error; use bytes::Bytes; -use futures::channel::oneshot::Sender; -use futures::{Future, Stream}; +use futures_channel::oneshot::Sender; +use futures_core::Stream; /// Execution context for http actors pub struct HttpContext @@ -174,7 +175,7 @@ where // frames if let Some(data) = self.fut.ctx().stream.pop_front() { - Poll::Ready(data.map(|b| Ok(b))) + Poll::Ready(data.map(Ok)) } else if self.fut.alive() { Poll::Pending } else { diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index b28aeade4..3f5972532 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -1,5 +1,6 @@ //! Websocket integration use std::collections::VecDeque; +use std::future::Future; use std::io; use std::pin::Pin; use std::task::{Context, Poll}; @@ -23,8 +24,8 @@ use actix_web::error::{Error, PayloadError}; use actix_web::http::{header, Method, StatusCode}; use actix_web::{HttpRequest, HttpResponse}; use bytes::{Bytes, BytesMut}; -use futures::channel::oneshot::Sender; -use futures::{Future, Stream}; +use futures_channel::oneshot::Sender; +use futures_core::Stream; /// Do websocket handshake and start ws actor. pub fn start(actor: A, req: &HttpRequest, stream: T) -> Result diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs index 076e375d3..25977c2c2 100644 --- a/actix-web-actors/tests/test_ws.rs +++ b/actix-web-actors/tests/test_ws.rs @@ -2,7 +2,7 @@ use actix::prelude::*; use actix_web::{test, web, App, HttpRequest}; use actix_web_actors::*; use bytes::Bytes; -use futures::{SinkExt, StreamExt}; +use futures_util::{SinkExt, StreamExt}; struct Ws; diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 95696abd3..b2e80591f 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,8 +1,18 @@ # Changes -## [0.2.NEXT] - 2020-xx-xx +## [0.2.2] - 2020-05-23 -* Allow the handler function to be named as `config` #1290 +* Add resource middleware on actix-web-codegen [#1467] + +[#1467]: https://github.com/actix/actix-web/pull/1467 + +## [0.2.1] - 2020-02-25 + +* Add `#[allow(missing_docs)]` attribute to generated structs [#1368] +* Allow the handler function to be named as `config` [#1290] + +[#1368]: https://github.com/actix/actix-web/issues/1368 +[#1290]: https://github.com/actix/actix-web/issues/1290 ## [0.2.0] - 2019-12-13 diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 3fe561deb..60480a7a1 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,8 +1,11 @@ [package] name = "actix-web-codegen" -version = "0.2.0" +version = "0.2.2" description = "Actix web proc macros" readme = "README.md" +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web" +documentation = "https://docs.rs/actix-web-codegen" authors = ["Nikolay Kim "] license = "MIT/Apache-2.0" edition = "2018" @@ -12,11 +15,11 @@ workspace = ".." proc-macro = true [dependencies] -quote = "^1" -syn = { version = "^1", features = ["full", "parsing"] } -proc-macro2 = "^1" +quote = "1" +syn = { version = "1", features = ["full", "parsing"] } +proc-macro2 = "1" [dev-dependencies] -actix-rt = { version = "1.0.0" } -actix-web = { version = "2.0.0-rc" } -futures = { version = "0.3.1" } +actix-rt = "1.0.0" +actix-web = "3.0.0-alpha.3" +futures-util = { version = "0.3.5", default-features = false } diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index c44a5fc7f..c482a6b36 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -1 +1,8 @@ # Macros 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-web-codegen)](https://crates.io/crates/actix-web-codegen) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +## Documentation & Resources + +* [API Documentation](https://docs.rs/actix-web-codegen/) +* [Chat on gitter](https://gitter.im/actix/actix) +* Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen) +* Minimum supported Rust version: 1.40 or later diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 0a727ed69..2a49b4714 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -21,6 +21,7 @@ //! //! - `"path"` - Raw literal string with path for which to register handle. Mandatory. //! - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` +//! - `wrap="Middleware"` - Registers a resource middleware. //! //! ## Notes //! @@ -32,7 +33,6 @@ //! ```rust //! use actix_web::HttpResponse; //! use actix_web_codegen::get; -//! use futures::{future, Future}; //! //! #[get("/test")] //! async fn async_test() -> Result { @@ -45,7 +45,6 @@ extern crate proc_macro; mod route; use proc_macro::TokenStream; -use syn::parse_macro_input; /// Creates route handler with `GET` method guard. /// @@ -55,14 +54,10 @@ use syn::parse_macro_input; /// /// - `"path"` - Raw literal string with path for which to register handler. Mandatory. /// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` +/// - `wrap="Middleware"` - Registers a resource middleware. #[proc_macro_attribute] pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as syn::AttributeArgs); - let gen = match route::Route::new(args, input, route::GuardType::Get) { - Ok(gen) => gen, - Err(err) => return err.to_compile_error().into(), - }; - gen.generate() + route::generate(args, input, route::GuardType::Get) } /// Creates route handler with `POST` method guard. @@ -72,12 +67,7 @@ pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { /// Attributes are the same as in [get](attr.get.html) #[proc_macro_attribute] pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as syn::AttributeArgs); - let gen = match route::Route::new(args, input, route::GuardType::Post) { - Ok(gen) => gen, - Err(err) => return err.to_compile_error().into(), - }; - gen.generate() + route::generate(args, input, route::GuardType::Post) } /// Creates route handler with `PUT` method guard. @@ -87,12 +77,7 @@ pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { /// Attributes are the same as in [get](attr.get.html) #[proc_macro_attribute] pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as syn::AttributeArgs); - let gen = match route::Route::new(args, input, route::GuardType::Put) { - Ok(gen) => gen, - Err(err) => return err.to_compile_error().into(), - }; - gen.generate() + route::generate(args, input, route::GuardType::Put) } /// Creates route handler with `DELETE` method guard. @@ -102,12 +87,7 @@ pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { /// Attributes are the same as in [get](attr.get.html) #[proc_macro_attribute] pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as syn::AttributeArgs); - let gen = match route::Route::new(args, input, route::GuardType::Delete) { - Ok(gen) => gen, - Err(err) => return err.to_compile_error().into(), - }; - gen.generate() + route::generate(args, input, route::GuardType::Delete) } /// Creates route handler with `HEAD` method guard. @@ -117,12 +97,7 @@ pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream { /// Attributes are the same as in [head](attr.head.html) #[proc_macro_attribute] pub fn head(args: TokenStream, input: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as syn::AttributeArgs); - let gen = match route::Route::new(args, input, route::GuardType::Head) { - Ok(gen) => gen, - Err(err) => return err.to_compile_error().into(), - }; - gen.generate() + route::generate(args, input, route::GuardType::Head) } /// Creates route handler with `CONNECT` method guard. @@ -132,12 +107,7 @@ pub fn head(args: TokenStream, input: TokenStream) -> TokenStream { /// Attributes are the same as in [connect](attr.connect.html) #[proc_macro_attribute] pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as syn::AttributeArgs); - let gen = match route::Route::new(args, input, route::GuardType::Connect) { - Ok(gen) => gen, - Err(err) => return err.to_compile_error().into(), - }; - gen.generate() + route::generate(args, input, route::GuardType::Connect) } /// Creates route handler with `OPTIONS` method guard. @@ -147,12 +117,7 @@ pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream { /// Attributes are the same as in [options](attr.options.html) #[proc_macro_attribute] pub fn options(args: TokenStream, input: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as syn::AttributeArgs); - let gen = match route::Route::new(args, input, route::GuardType::Options) { - Ok(gen) => gen, - Err(err) => return err.to_compile_error().into(), - }; - gen.generate() + route::generate(args, input, route::GuardType::Options) } /// Creates route handler with `TRACE` method guard. @@ -162,12 +127,7 @@ pub fn options(args: TokenStream, input: TokenStream) -> TokenStream { /// Attributes are the same as in [trace](attr.trace.html) #[proc_macro_attribute] pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as syn::AttributeArgs); - let gen = match route::Route::new(args, input, route::GuardType::Trace) { - Ok(gen) => gen, - Err(err) => return err.to_compile_error().into(), - }; - gen.generate() + route::generate(args, input, route::GuardType::Trace) } /// Creates route handler with `PATCH` method guard. @@ -177,10 +137,5 @@ pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream { /// Attributes are the same as in [patch](attr.patch.html) #[proc_macro_attribute] pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as syn::AttributeArgs); - let gen = match route::Route::new(args, input, route::GuardType::Patch) { - Ok(gen) => gen, - Err(err) => return err.to_compile_error().into(), - }; - gen.generate() + route::generate(args, input, route::GuardType::Patch) } diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index d48198484..7e3d43f1d 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -2,8 +2,8 @@ extern crate proc_macro; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{AttributeArgs, Ident, NestedMeta}; +use quote::{format_ident, quote, ToTokens, TokenStreamExt}; +use syn::{AttributeArgs, Ident, NestedMeta, parse_macro_input}; enum ResourceType { Async, @@ -12,11 +12,7 @@ enum ResourceType { impl ToTokens for ResourceType { fn to_tokens(&self, stream: &mut TokenStream2) { - let ident = match self { - ResourceType::Async => "to", - ResourceType::Sync => "to", - }; - let ident = Ident::new(ident, Span::call_site()); + let ident = format_ident!("to"); stream.append(ident); } } @@ -52,8 +48,7 @@ impl GuardType { impl ToTokens for GuardType { fn to_tokens(&self, stream: &mut TokenStream2) { - let ident = self.as_str(); - let ident = Ident::new(ident, Span::call_site()); + let ident = Ident::new(self.as_str(), Span::call_site()); stream.append(ident); } } @@ -61,12 +56,14 @@ impl ToTokens for GuardType { struct Args { path: syn::LitStr, guards: Vec, + wrappers: Vec, } impl Args { fn new(args: AttributeArgs) -> syn::Result { let mut path = None; let mut guards = Vec::new(); + let mut wrappers = Vec::new(); for arg in args { match arg { NestedMeta::Lit(syn::Lit::Str(lit)) => match path { @@ -90,21 +87,31 @@ impl Args { "Attribute guard expects literal string!", )); } + } else if nv.path.is_ident("wrap") { + if let syn::Lit::Str(lit) = nv.lit { + wrappers.push(lit.parse()?); + } else { + return Err(syn::Error::new_spanned( + nv.lit, + "Attribute wrap expects type", + )); + } } else { return Err(syn::Error::new_spanned( nv.path, - "Unknown attribute key is specified. Allowed: guard", + "Unknown attribute key is specified. Allowed: guard and wrap", )); } } arg => { - return Err(syn::Error::new_spanned(arg, "Unknown attribute")); + return Err(syn::Error::new_spanned(arg, "Unknown attribute.")); } } } Ok(Args { path: path.unwrap(), guards, + wrappers, }) } } @@ -181,17 +188,20 @@ impl Route { guard, }) } +} - pub fn generate(&self) -> TokenStream { - let name = &self.name; +impl ToTokens for Route { + fn to_tokens(&self, output: &mut TokenStream2) { + let Self { + name, + guard, + ast, + args: Args { path, guards, wrappers }, + resource_type, + } = self; let resource_name = name.to_string(); - let guard = &self.guard; - let ast = &self.ast; - let path = &self.args.path; - let extra_guards = &self.args.guards; - let resource_type = &self.resource_type; let stream = quote! { - #[allow(non_camel_case_types)] + #[allow(non_camel_case_types, missing_docs)] pub struct #name; impl actix_web::dev::HttpServiceFactory for #name { @@ -200,13 +210,27 @@ impl Route { let __resource = actix_web::Resource::new(#path) .name(#resource_name) .guard(actix_web::guard::#guard()) - #(.guard(actix_web::guard::fn_guard(#extra_guards)))* + #(.guard(actix_web::guard::fn_guard(#guards)))* + #(.wrap(#wrappers))* .#resource_type(#name); actix_web::dev::HttpServiceFactory::register(__resource, __config) } } }; - stream.into() + + output.extend(stream); + } +} + +pub(crate) fn generate( + args: TokenStream, + input: TokenStream, + guard: GuardType, +) -> TokenStream { + let args = parse_macro_input!(args as syn::AttributeArgs); + match Route::new(args, input, guard) { + Ok(route) => route.into_token_stream().into(), + Err(err) => err.to_compile_error().into(), } } diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index ffb50c11e..0ef7e1c75 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -1,6 +1,12 @@ -use actix_web::{http, test, web::Path, App, HttpResponse, Responder}; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use actix_web::{http, test, web::Path, App, HttpResponse, Responder, Error}; +use actix_web::dev::{Service, Transform, ServiceRequest, ServiceResponse}; use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, trace}; -use futures::{future, Future}; +use futures_util::future; +use actix_web::http::header::{HeaderName, HeaderValue}; // Make sure that we can name function as 'config' #[get("/config")] @@ -73,6 +79,65 @@ async fn get_param_test(_: Path) -> impl Responder { HttpResponse::Ok() } +pub struct ChangeStatusCode; + +impl Transform for ChangeStatusCode +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type InitError = (); + type Transform = ChangeStatusCodeMiddleware; + type Future = future::Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + future::ok(ChangeStatusCodeMiddleware { service }) + } +} + +pub struct ChangeStatusCodeMiddleware { + service: S, +} + +impl Service for ChangeStatusCodeMiddleware +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Future = Pin>>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.service.poll_ready(cx) + } + + fn call(&mut self, req: ServiceRequest) -> Self::Future { + + let fut = self.service.call(req); + + Box::pin(async move { + let mut res = fut.await?; + let headers = res.headers_mut(); + let header_name = HeaderName::from_lowercase(b"custom-header").unwrap(); + let header_value = HeaderValue::from_str("hello").unwrap(); + headers.insert(header_name, header_value); + Ok(res) + }) + } +} + +#[get("/test/wrap", wrap = "ChangeStatusCode")] +async fn get_wrap(_: Path) -> impl Responder { + HttpResponse::Ok() +} + #[actix_rt::test] async fn test_params() { let srv = test::start(|| { @@ -155,3 +220,15 @@ async fn test_auto_async() { let response = request.send().await.unwrap(); assert!(response.status().is_success()); } + +#[actix_rt::test] +async fn test_wrap() { + let srv = test::start(|| { + App::new() + .service(get_wrap) + }); + + let request = srv.request(http::Method::GET, srv.url("/test/wrap")); + let response = request.send().await.unwrap(); + assert!(response.headers().contains_key("custom-header")); +} diff --git a/awc/CHANGES.md b/awc/CHANGES.md index d9b26e453..6d5a81b5e 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,10 +1,26 @@ # Changes +## [2.0.0-alpha.2] - 2020-05-21 + +### Changed + +* Implement `std::error::Error` for our custom errors [#1422] +* Bump minimum supported Rust version to 1.40 +* Update `base64` dependency to 0.12 + +[#1422]: https://github.com/actix/actix-web/pull/1422 + +## [2.0.0-alpha.1] - 2020-03-11 + +* Update `actix-http` dependency to 2.0.0-alpha.2 +* Update `rustls` dependency to 0.17 +* ClientBuilder accepts initial_window_size and initial_connection_window_size HTTP2 configuration +* ClientBuilder allowing to set max_http_version to limit HTTP version to be used + ## [1.0.1] - 2019-12-15 * Fix compilation with default features off - ## [1.0.0] - 2019-12-13 * Release diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 67e0a3ee4..b36e735ca 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "1.0.1" +version = "2.0.0-alpha.2" authors = ["Nikolay Kim "] description = "Actix http client." readme = "README.md" @@ -36,13 +36,13 @@ compress = ["actix-http/compress"] [dependencies] actix-codec = "0.2.0" actix-service = "1.0.1" -actix-http = "1.0.0" +actix-http = "2.0.0-alpha.4" actix-rt = "1.0.0" -base64 = "0.11" +base64 = "0.12" bytes = "0.5.3" derive_more = "0.99.2" -futures-core = "0.3.1" +futures-core = { version = "0.3.5", default-features = false } log =" 0.4" mime = "0.3" percent-encoding = "2.1" @@ -50,19 +50,19 @@ rand = "0.7" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.6.1" -open-ssl = { version="0.10", package="openssl", optional = true } -rust-tls = { version = "0.16.0", package="rustls", optional = true, features = ["dangerous_configuration"] } +open-ssl = { version = "0.10", package = "openssl", optional = true } +rust-tls = { version = "0.17.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } [dev-dependencies] -actix-connect = { version = "1.0.1", features=["openssl"] } -actix-web = { version = "2.0.0-rc", features=["openssl"] } -actix-http = { version = "1.0.1", features=["openssl"] } -actix-http-test = { version = "1.0.0", features=["openssl"] } +actix-connect = { version = "2.0.0-alpha.2", features = ["openssl"] } +actix-web = { version = "3.0.0-alpha.3", features = ["openssl"] } +actix-http = { version = "2.0.0-alpha.4", features = ["openssl"] } +actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } actix-utils = "1.0.3" actix-server = "1.0.0" -actix-tls = { version = "1.0.0", features=["openssl", "rustls"] } +actix-tls = { version = "2.0.0-alpha.1", features = ["openssl", "rustls"] } brotli2 = "0.3.2" flate2 = "1.0.13" -futures = "0.3.1" -env_logger = "0.6" +futures-util = { version = "0.3.5", default-features = false } +env_logger = "0.7" webpki = "0.21" diff --git a/awc/README.md b/awc/README.md index 3b0034d76..2b6309c1d 100644 --- a/awc/README.md +++ b/awc/README.md @@ -8,7 +8,7 @@ An HTTP Client * [API Documentation](https://docs.rs/awc/) * [Chat on gitter](https://gitter.im/actix/actix) * Cargo package: [awc](https://crates.io/crates/awc) -* Minimum supported Rust version: 1.33 or later +* Minimum supported Rust version: 1.40 or later ## Example diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 7bd0171ec..7cd659c38 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -4,11 +4,11 @@ use std::fmt; use std::rc::Rc; use std::time::Duration; -use actix_http::client::{Connect, ConnectError, Connection, Connector}; -use actix_http::http::{header, Error as HttpError, HeaderMap, HeaderName}; +use actix_http::client::{Connect as HttpConnect, ConnectError, Connection, Connector}; +use actix_http::http::{self, header, Error as HttpError, HeaderMap, HeaderName}; use actix_service::Service; -use crate::connect::ConnectorWrapper; +use crate::connect::{Connect, ConnectorWrapper}; use crate::{Client, ClientConfig}; /// An HTTP Client builder @@ -16,10 +16,15 @@ use crate::{Client, ClientConfig}; /// This type can be used to construct an instance of `Client` through a /// builder-like pattern. pub struct ClientBuilder { - config: ClientConfig, default_headers: bool, allow_redirects: bool, max_redirects: usize, + max_http_version: Option, + stream_window_size: Option, + conn_window_size: Option, + headers: HeaderMap, + timeout: Option, + connector: Option>>, } impl Default for ClientBuilder { @@ -34,25 +39,24 @@ impl ClientBuilder { default_headers: true, allow_redirects: true, max_redirects: 10, - config: ClientConfig { - headers: HeaderMap::new(), - timeout: Some(Duration::from_secs(5)), - connector: RefCell::new(Box::new(ConnectorWrapper( - Connector::new().finish(), - ))), - }, + headers: HeaderMap::new(), + timeout: Some(Duration::from_secs(5)), + connector: None, + max_http_version: None, + stream_window_size: None, + conn_window_size: None, } } /// Use custom connector service. pub fn connector(mut self, connector: T) -> Self where - T: Service + 'static, + T: Service + 'static, T::Response: Connection, ::Future: 'static, T::Future: 'static, { - self.config.connector = RefCell::new(Box::new(ConnectorWrapper(connector))); + self.connector = Some(RefCell::new(Box::new(ConnectorWrapper(connector)))); self } @@ -61,13 +65,13 @@ impl ClientBuilder { /// Request timeout is the total time before a response must be received. /// Default value is 5 seconds. pub fn timeout(mut self, timeout: Duration) -> Self { - self.config.timeout = Some(timeout); + self.timeout = Some(timeout); self } /// Disable request timeout. pub fn disable_timeout(mut self) -> Self { - self.config.timeout = None; + self.timeout = None; self } @@ -79,6 +83,31 @@ impl ClientBuilder { self } + /// Maximum supported http major version + /// Supported versions http/1.1, http/2 + pub fn max_http_version(mut self, val: http::Version) -> Self { + self.max_http_version = Some(val); + self + } + + /// Indicates the initial window size (in octets) for + /// HTTP2 stream-level flow control for received data. + /// + /// The default value is 65,535 and is good for APIs, but not for big objects. + pub fn initial_window_size(mut self, size: u32) -> Self { + self.stream_window_size = Some(size); + self + } + + /// Indicates the initial window size (in octets) for + /// HTTP2 connection-level flow control for received data. + /// + /// The default value is 65,535 and is good for APIs, but not for big objects. + pub fn initial_connection_window_size(mut self, size: u32) -> Self { + self.conn_window_size = Some(size); + self + } + /// Set max number of redirects. /// /// Max redirects is set to 10 by default. @@ -106,7 +135,7 @@ impl ClientBuilder { match HeaderName::try_from(key) { Ok(key) => match value.try_into() { Ok(value) => { - self.config.headers.append(key, value); + self.headers.append(key, value); } Err(e) => log::error!("Header value error: {:?}", e), }, @@ -140,7 +169,29 @@ impl ClientBuilder { /// Finish build process and create `Client` instance. pub fn finish(self) -> Client { - Client(Rc::new(self.config)) + let connector = if let Some(connector) = self.connector { + connector + } else { + let mut connector = Connector::new(); + if let Some(val) = self.max_http_version { + connector = connector.max_http_version(val) + }; + if let Some(val) = self.conn_window_size { + connector = connector.initial_connection_window_size(val) + }; + if let Some(val) = self.stream_window_size { + connector = connector.initial_window_size(val) + }; + RefCell::new( + Box::new(ConnectorWrapper(connector.finish())) as Box + ) + }; + let config = ClientConfig { + headers: self.headers, + timeout: self.timeout, + connector, + }; + Client(Rc::new(config)) } } @@ -153,7 +204,6 @@ mod tests { let client = ClientBuilder::new().basic_auth("username", Some("password")); assert_eq!( client - .config .headers .get(header::AUTHORIZATION) .unwrap() @@ -165,7 +215,6 @@ mod tests { let client = ClientBuilder::new().basic_auth("username", None); assert_eq!( client - .config .headers .get(header::AUTHORIZATION) .unwrap() @@ -180,7 +229,6 @@ mod tests { let client = ClientBuilder::new().bearer_auth("someS3cr3tAutht0k3n"); assert_eq!( client - .config .headers .get(header::AUTHORIZATION) .unwrap() diff --git a/awc/src/error.rs b/awc/src/error.rs index 7fece74ee..d008166d9 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -42,6 +42,8 @@ pub enum WsClientError { SendRequest(SendRequestError), } +impl std::error::Error for WsClientError {} + impl From for WsClientError { fn from(err: InvalidUrl) -> Self { WsClientError::SendRequest(err.into()) @@ -68,5 +70,7 @@ pub enum JsonPayloadError { Payload(PayloadError), } +impl std::error::Error for JsonPayloadError {} + /// Return `InternalServerError` for `JsonPayloadError` impl ResponseError for JsonPayloadError {} diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 8944fe229..1cc31a194 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -1,4 +1,4 @@ -#![deny(rust_2018_idioms, warnings)] +#![warn(rust_2018_idioms, warnings)] #![allow( clippy::type_complexity, clippy::borrow_interior_mutable_const, @@ -7,7 +7,6 @@ //! An HTTP Client //! //! ```rust -//! use futures::future::{lazy, Future}; //! use actix_rt::System; //! use awc::Client; //! diff --git a/awc/src/request.rs b/awc/src/request.rs index 67b063a8e..21a7cd911 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -156,7 +156,7 @@ impl ClientRequest { /// /// ```rust /// fn main() { - /// # actix_rt::System::new("test").block_on(futures::future::lazy(|_| { + /// # actix_rt::System::new("test").block_on(futures_util::future::lazy(|_| { /// let req = awc::Client::new() /// .get("http://www.rust-lang.org") /// .set(awc::http::header::Date::now()) diff --git a/awc/src/response.rs b/awc/src/response.rs index 20093c72d..ffc8c5408 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -1,11 +1,12 @@ use std::cell::{Ref, RefMut}; use std::fmt; +use std::future::Future; use std::marker::PhantomData; use std::pin::Pin; use std::task::{Context, Poll}; use bytes::{Bytes, BytesMut}; -use futures_core::{ready, Future, Stream}; +use futures_core::{ready, Stream}; use actix_http::cookie::Cookie; use actix_http::error::{CookieParseError, PayloadError}; diff --git a/awc/src/sender.rs b/awc/src/sender.rs index ec18f12e3..5e0f5beec 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -1,3 +1,4 @@ +use std::future::Future; use std::net; use std::pin::Pin; use std::rc::Rc; @@ -7,9 +8,8 @@ use std::time::Duration; use actix_rt::time::{delay_for, Delay}; use bytes::Bytes; use derive_more::From; -use futures_core::{Future, Stream}; +use futures_core::Stream; use serde::Serialize; -use serde_json; use actix_http::body::{Body, BodyStream}; use actix_http::http::header::{self, IntoHeaderValue}; diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 8fb04b005..cc61f1006 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -9,7 +9,7 @@ use bytes::Bytes; use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; -use futures::future::ok; +use futures_util::future::ok; use rand::Rng; use actix_http::HttpService; @@ -107,17 +107,15 @@ async fn test_form() { #[actix_rt::test] async fn test_timeout() { let srv = test::start(|| { - App::new().service(web::resource("/").route(web::to(|| { - async { - actix_rt::time::delay_for(Duration::from_millis(200)).await; - Ok::<_, Error>(HttpResponse::Ok().body(STR)) - } + App::new().service(web::resource("/").route(web::to(|| async { + actix_rt::time::delay_for(Duration::from_millis(200)).await; + Ok::<_, Error>(HttpResponse::Ok().body(STR)) }))) }); let connector = awc::Connector::new() .connector(actix_connect::new_connector( - actix_connect::start_default_resolver(), + actix_connect::start_default_resolver().await.unwrap(), )) .timeout(Duration::from_secs(15)) .finish(); @@ -137,11 +135,9 @@ async fn test_timeout() { #[actix_rt::test] async fn test_timeout_override() { let srv = test::start(|| { - App::new().service(web::resource("/").route(web::to(|| { - async { - actix_rt::time::delay_for(Duration::from_millis(200)).await; - Ok::<_, Error>(HttpResponse::Ok().body(STR)) - } + App::new().service(web::resource("/").route(web::to(|| async { + actix_rt::time::delay_for(Duration::from_millis(200)).await; + Ok::<_, Error>(HttpResponse::Ok().body(STR)) }))) }); @@ -177,7 +173,8 @@ async fn test_connection_reuse() { )) .tcp(), ) - }); + }) + .await; let client = awc::Client::default(); @@ -214,7 +211,8 @@ async fn test_connection_force_close() { )) .tcp(), ) - }); + }) + .await; let client = awc::Client::default(); @@ -253,7 +251,8 @@ async fn test_connection_server_close() { )) .tcp(), ) - }); + }) + .await; let client = awc::Client::default(); @@ -291,7 +290,8 @@ async fn test_connection_wait_queue() { )) .tcp(), ) - }); + }) + .await; let client = awc::Client::build() .connector(awc::Connector::new().limit(1).finish()) @@ -339,7 +339,8 @@ async fn test_connection_wait_queue_force_close() { )) .tcp(), ) - }); + }) + .await; let client = awc::Client::build() .connector(awc::Connector::new().limit(1).finish()) diff --git a/awc/tests/test_connector.rs b/awc/tests/test_connector.rs new file mode 100644 index 000000000..b352eaab4 --- /dev/null +++ b/awc/tests/test_connector.rs @@ -0,0 +1,61 @@ +#![cfg(feature = "openssl")] +use actix_http::HttpService; +use actix_http_test::test_server; +use actix_service::{map_config, ServiceFactory}; +use actix_web::http::Version; +use actix_web::{dev::AppConfig, web, App, HttpResponse}; +use open_ssl::ssl::{SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode}; + +fn ssl_acceptor() -> SslAcceptor { + // load ssl keys + let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); + builder + .set_private_key_file("../tests/key.pem", SslFiletype::PEM) + .unwrap(); + builder + .set_certificate_chain_file("../tests/cert.pem") + .unwrap(); + builder.set_alpn_select_callback(|_, protos| { + const H2: &[u8] = b"\x02h2"; + if protos.windows(3).any(|window| window == H2) { + Ok(b"h2") + } else { + Err(open_ssl::ssl::AlpnError::NOACK) + } + }); + builder.set_alpn_protos(b"\x02h2").unwrap(); + builder.build() +} + +#[actix_rt::test] +async fn test_connection_window_size() { + let srv = test_server(move || { + HttpService::build() + .h2(map_config( + App::new() + .service(web::resource("/").route(web::to(|| HttpResponse::Ok()))), + |_| AppConfig::default(), + )) + .openssl(ssl_acceptor()) + .map_err(|_| ()) + }) + .await; + + // disable ssl verification + let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); + builder.set_verify(SslVerifyMode::NONE); + let _ = builder + .set_alpn_protos(b"\x02h2\x08http/1.1") + .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); + + let client = awc::Client::build() + .connector(awc::Connector::new().ssl(builder.build()).finish()) + .initial_window_size(100) + .initial_connection_window_size(100) + .finish(); + + let request = client.get(srv.surl("/")).send(); + let response = request.await.unwrap(); + assert!(response.status().is_success()); + assert_eq!(response.version(), Version::HTTP_2); +} diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index 1d7eb7bc5..0c6be76d4 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -7,10 +7,11 @@ use actix_http_test::test_server; use actix_service::{map_config, pipeline_factory, ServiceFactory}; use actix_web::http::Version; use actix_web::{dev::AppConfig, web, App, HttpResponse}; -use futures::future::ok; +use futures_util::future::ok; use open_ssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslVerifyMode}; use rust_tls::ClientConfig; +#[allow(unused)] fn ssl_acceptor() -> SslAcceptor { // load ssl keys let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); @@ -71,7 +72,8 @@ async fn _test_connection_reuse_h2() { .openssl(ssl_acceptor()) .map_err(|_| ()), ) - }); + }) + .await; // disable ssl verification let mut config = ClientConfig::new(); diff --git a/awc/tests/test_ssl_client.rs b/awc/tests/test_ssl_client.rs index d3995b4be..b2a2e1785 100644 --- a/awc/tests/test_ssl_client.rs +++ b/awc/tests/test_ssl_client.rs @@ -7,7 +7,7 @@ use actix_http_test::test_server; use actix_service::{map_config, pipeline_factory, ServiceFactory}; use actix_web::http::Version; use actix_web::{dev::AppConfig, web, App, HttpResponse}; -use futures::future::ok; +use futures_util::future::ok; use open_ssl::ssl::{SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode}; fn ssl_acceptor() -> SslAcceptor { @@ -53,7 +53,8 @@ async fn test_connection_reuse_h2() { .openssl(ssl_acceptor()) .map_err(|_| ()), ) - }); + }) + .await; // disable ssl verification let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); diff --git a/awc/tests/test_ws.rs b/awc/tests/test_ws.rs index ee937e43e..d3f66814f 100644 --- a/awc/tests/test_ws.rs +++ b/awc/tests/test_ws.rs @@ -4,8 +4,8 @@ use actix_codec::Framed; use actix_http::{body::BodySize, h1, ws, Error, HttpService, Request, Response}; use actix_http_test::test_server; use bytes::Bytes; -use futures::future::ok; -use futures::{SinkExt, StreamExt}; +use futures_util::future::ok; +use futures_util::{SinkExt, StreamExt}; async fn ws_service(req: ws::Frame) -> Result { match req { @@ -38,7 +38,8 @@ async fn test_simple() { }) .finish(|_| ok::<_, Error>(Response::NotFound())) .tcp() - }); + }) + .await; // client service let mut framed = srv.ws().await.unwrap(); diff --git a/benches/server.rs b/benches/server.rs index 93079a223..70531adf7 100644 --- a/benches/server.rs +++ b/benches/server.rs @@ -1,7 +1,7 @@ use actix_web::{test, web, App, HttpResponse}; use awc::Client; use criterion::{criterion_group, criterion_main, Criterion}; -use futures::future::join_all; +use futures_util::future::join_all; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ diff --git a/examples/uds.rs b/examples/uds.rs index 77f245d99..06aa87f4d 100644 --- a/examples/uds.rs +++ b/examples/uds.rs @@ -1,6 +1,6 @@ -use actix_web::{ - get, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer, -}; +use actix_web::{get, web, HttpRequest}; +#[cfg(unix)] +use actix_web::{middleware, App, Error, HttpResponse, HttpServer}; #[get("/resource1/{name}/index.html")] async fn index(req: HttpRequest, name: web::Path) -> String { @@ -8,6 +8,7 @@ async fn index(req: HttpRequest, name: web::Path) -> String { format!("Hello: {}!\r\n", name) } +#[cfg(unix)] async fn index_async(req: HttpRequest) -> Result<&'static str, Error> { println!("REQ: {:?}", req); Ok("Hello world!\r\n") diff --git a/src/app.rs b/src/app.rs index a060eb53e..8178d57fe 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,11 +10,11 @@ use actix_service::boxed::{self, BoxServiceFactory}; use actix_service::{ apply, apply_fn_factory, IntoServiceFactory, ServiceFactory, Transform, }; -use futures::future::{FutureExt, LocalBoxFuture}; +use futures_util::future::FutureExt; use crate::app_service::{AppEntry, AppInit, AppRoutingFactory}; use crate::config::ServiceConfig; -use crate::data::{Data, DataFactory}; +use crate::data::{Data, DataFactory, FnDataFactory}; use crate::dev::ResourceDef; use crate::error::Error; use crate::resource::Resource; @@ -25,8 +25,6 @@ use crate::service::{ }; type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; -type FnDataFactory = - Box LocalBoxFuture<'static, Result, ()>>>; /// Application builder - structure that follows the builder pattern /// for building application instances. @@ -320,7 +318,7 @@ where /// Registers middleware, in the form of a middleware component (type), /// that runs during inbound and/or outbound processing in the request - /// lifecycle (request -> response), modifying request/response as + /// life-cycle (request -> response), modifying request/response as /// necessary, across all requests managed by the *Application*. /// /// Use middleware when you need to read or modify *every* request or @@ -385,7 +383,7 @@ where } /// Registers middleware, in the form of a closure, that runs during inbound - /// and/or outbound processing in the request lifecycle (request -> response), + /// and/or outbound processing in the request life-cycle (request -> response), /// modifying request/response as necessary, across all requests managed by /// the *Application*. /// @@ -476,13 +474,15 @@ where mod tests { use actix_service::Service; use bytes::Bytes; - use futures::future::ok; + use futures_util::future::{err, ok}; use super::*; use crate::http::{header, HeaderValue, Method, StatusCode}; use crate::middleware::DefaultHeaders; use crate::service::ServiceRequest; - use crate::test::{call_service, init_service, read_body, TestRequest}; + use crate::test::{ + call_service, init_service, read_body, try_init_service, TestRequest, + }; use crate::{web, HttpRequest, HttpResponse}; #[actix_rt::test] @@ -551,6 +551,17 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + #[actix_rt::test] + async fn test_data_factory_errors() { + let srv = + try_init_service(App::new().data_factory(|| err::(())).service( + web::resource("/").to(|_: web::Data| HttpResponse::Ok()), + )) + .await; + + assert!(srv.is_err()); + } + #[actix_rt::test] async fn test_extension() { let mut srv = init_service(App::new().app_data(10usize).service( diff --git a/src/app_service.rs b/src/app_service.rs index ccfefbc68..233cfc0d5 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -9,10 +9,10 @@ use actix_http::{Extensions, Request, Response}; use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; use actix_service::boxed::{self, BoxService, BoxServiceFactory}; use actix_service::{fn_service, Service, ServiceFactory}; -use futures::future::{ok, FutureExt, LocalBoxFuture}; +use futures_util::future::{join_all, ok, FutureExt, LocalBoxFuture}; use crate::config::{AppConfig, AppService}; -use crate::data::DataFactory; +use crate::data::{DataFactory, FnDataFactory}; use crate::error::Error; use crate::guard::Guard; use crate::request::{HttpRequest, HttpRequestPool}; @@ -23,8 +23,6 @@ type Guards = Vec>; type HttpService = BoxService; type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; type BoxResponse = LocalBoxFuture<'static, Result>; -type FnDataFactory = - Box LocalBoxFuture<'static, Result, ()>>>; /// Service factory to convert `Request` to a `ServiceRequest`. /// It also executes data factories. @@ -78,7 +76,7 @@ where let mut config = AppService::new(config, default.clone(), self.data.clone()); // register services - std::mem::replace(&mut *self.services.borrow_mut(), Vec::new()) + std::mem::take(&mut *self.services.borrow_mut()) .into_iter() .for_each(|mut srv| srv.register(&mut config)); @@ -101,7 +99,7 @@ where }); // external resources - for mut rdef in std::mem::replace(&mut *self.external.borrow_mut(), Vec::new()) { + for mut rdef in std::mem::take(&mut *self.external.borrow_mut()) { rmap.add(&mut rdef, None); } @@ -109,12 +107,15 @@ where let rmap = Rc::new(rmap); rmap.finish(rmap.clone()); + // start all data factory futures + let factory_futs = join_all(self.data_factories.iter().map(|f| f())); + AppInitResult { endpoint: None, endpoint_fut: self.endpoint.new_service(()), data: self.data.clone(), - data_factories: Vec::new(), - data_factories_fut: self.data_factories.iter().map(|f| f()).collect(), + data_factories: None, + data_factories_fut: factory_futs.boxed_local(), extensions: Some( self.extensions .borrow_mut() @@ -133,15 +134,21 @@ pub struct AppInitResult where T: ServiceFactory, { - endpoint: Option, #[pin] endpoint_fut: T::Future, + // a Some signals completion of endpoint creation + endpoint: Option, + + #[pin] + data_factories_fut: LocalBoxFuture<'static, Vec, ()>>>, + // a Some signals completion of factory futures + data_factories: Option>>, + rmap: Rc, config: AppConfig, data: Rc>>, - data_factories: Vec>, - data_factories_fut: Vec, ()>>>, extensions: Option, + _t: PhantomData, } @@ -161,44 +168,46 @@ where let this = self.project(); // async data factories - let mut idx = 0; - while idx < this.data_factories_fut.len() { - match Pin::new(&mut this.data_factories_fut[idx]).poll(cx)? { - Poll::Ready(f) => { - this.data_factories.push(f); - let _ = this.data_factories_fut.remove(idx); - } - Poll::Pending => idx += 1, + if let Poll::Ready(factories) = this.data_factories_fut.poll(cx) { + let factories: Result, ()> = factories.into_iter().collect(); + + if let Ok(factories) = factories { + this.data_factories.replace(factories); + } else { + return Poll::Ready(Err(())); } } + // app service and middleware if this.endpoint.is_none() { if let Poll::Ready(srv) = this.endpoint_fut.poll(cx)? { *this.endpoint = Some(srv); } } - if this.endpoint.is_some() && this.data_factories_fut.is_empty() { + // not using if let so condition only needs shared ref + if this.endpoint.is_some() && this.data_factories.is_some() { // create app data container let mut data = this.extensions.take().unwrap(); + for f in this.data.iter() { f.create(&mut data); } - for f in this.data_factories.iter() { + for f in this.data_factories.take().unwrap().iter() { f.create(&mut data); } - Poll::Ready(Ok(AppInitService { + return Poll::Ready(Ok(AppInitService { service: this.endpoint.take().unwrap(), rmap: this.rmap.clone(), config: this.config.clone(), data: Rc::new(data), pool: HttpRequestPool::create(), - })) - } else { - Poll::Pending + })); } + + Poll::Pending } } @@ -236,7 +245,7 @@ where inner.path.reset(); inner.head = head; inner.payload = payload; - inner.app_data = self.data.clone(); + inner.app_data.push(self.data.clone()); req } else { HttpRequest::new( diff --git a/src/config.rs b/src/config.rs index 6ce96767d..19a5ccc7b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -50,7 +50,7 @@ impl AppService { } } - /// Check if root is beeing configured + /// Check if root is being configured pub fn is_root(&self) -> bool { self.root } @@ -123,6 +123,7 @@ impl AppService { } } +/// Application connection config #[derive(Clone)] pub struct AppConfig(Rc); diff --git a/src/data.rs b/src/data.rs index c36418942..34ada863d 100644 --- a/src/data.rs +++ b/src/data.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use actix_http::error::{Error, ErrorInternalServerError}; use actix_http::Extensions; -use futures::future::{err, ok, Ready}; +use futures_util::future::{err, ok, LocalBoxFuture, Ready}; use crate::dev::Payload; use crate::extract::FromRequest; @@ -14,6 +14,9 @@ pub(crate) trait DataFactory { fn create(&self, extensions: &mut Extensions) -> bool; } +pub(crate) type FnDataFactory = + Box LocalBoxFuture<'static, Result, ()>>>; + /// Application data. /// /// Application data is an arbitrary data attached to the app. @@ -100,6 +103,12 @@ impl Clone for Data { } } +impl From> for Data { + fn from(arc: Arc) -> Self { + Data(arc) + } +} + impl FromRequest for Data { type Config = (); type Error = Error; @@ -278,4 +287,11 @@ mod tests { assert_eq!(num.load(Ordering::SeqCst), 0); } + + #[actix_rt::test] + async fn test_data_from_arc() { + let data_new = Data::new(String::from("test-123")); + let data_from_arc = Data::from(Arc::new(String::from("test-123"))); + assert_eq!(data_new.0, data_from_arc.0) + } } diff --git a/src/error.rs b/src/error.rs index 31f6b9c5b..659ba05fd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,6 +21,8 @@ pub enum UrlGenerationError { ParseError(UrlParseError), } +impl std::error::Error for UrlGenerationError {} + /// `InternalServerError` for `UrlGeneratorError` impl ResponseError for UrlGenerationError {} @@ -51,6 +53,8 @@ pub enum UrlencodedError { Payload(PayloadError), } +impl std::error::Error for UrlencodedError {} + /// Return `BadRequest` for `UrlencodedError` impl ResponseError for UrlencodedError { fn status_code(&self) -> StatusCode { @@ -79,6 +83,8 @@ pub enum JsonPayloadError { Payload(PayloadError), } +impl std::error::Error for JsonPayloadError {} + /// Return `BadRequest` for `JsonPayloadError` impl ResponseError for JsonPayloadError { fn error_response(&self) -> HttpResponse { @@ -99,6 +105,8 @@ pub enum PathError { Deserialize(serde::de::value::Error), } +impl std::error::Error for PathError {} + /// Return `BadRequest` for `PathError` impl ResponseError for PathError { fn status_code(&self) -> StatusCode { @@ -114,6 +122,8 @@ pub enum QueryPayloadError { Deserialize(serde::de::value::Error), } +impl std::error::Error for QueryPayloadError {} + /// Return `BadRequest` for `QueryPayloadError` impl ResponseError for QueryPayloadError { fn status_code(&self) -> StatusCode { @@ -139,6 +149,8 @@ pub enum ReadlinesError { ContentTypeError(ContentTypeError), } +impl std::error::Error for ReadlinesError {} + /// Return `BadRequest` for `ReadlinesError` impl ResponseError for ReadlinesError { fn status_code(&self) -> StatusCode { diff --git a/src/extract.rs b/src/extract.rs index 5289bd7db..df9c34cb3 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -4,7 +4,7 @@ use std::pin::Pin; use std::task::{Context, Poll}; use actix_http::error::Error; -use futures::future::{ok, FutureExt, LocalBoxFuture, Ready}; +use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready}; use crate::dev::Payload; use crate::request::HttpRequest; @@ -50,7 +50,7 @@ pub trait FromRequest: Sized { /// ```rust /// use actix_web::{web, dev, App, Error, HttpRequest, FromRequest}; /// use actix_web::error::ErrorBadRequest; -/// use futures::future::{ok, err, Ready}; +/// use futures_util::future::{ok, err, Ready}; /// use serde_derive::Deserialize; /// use rand; /// @@ -122,7 +122,7 @@ where /// ```rust /// use actix_web::{web, dev, App, Result, Error, HttpRequest, FromRequest}; /// use actix_web::error::ErrorBadRequest; -/// use futures::future::{ok, err, Ready}; +/// use futures_util::future::{ok, err, Ready}; /// use serde_derive::Deserialize; /// use rand; /// diff --git a/src/guard.rs b/src/guard.rs index e6303e9c7..25284236a 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -6,7 +6,7 @@ //! It is possible to add guards to *scopes*, *resources* //! and *routes*. Actix provide several guards by default, like various //! http methods, header, etc. To become a guard, type must implement `Guard` -//! trait. Simple functions coulds guards as well. +//! trait. Simple functions could be guards as well. //! //! Guards can not modify the request object. But it is possible //! to store extra attributes on a request by using the `Extensions` container. @@ -100,7 +100,7 @@ pub fn Any(guard: F) -> AnyGuard { AnyGuard(vec![Box::new(guard)]) } -/// Matches if any of supplied guards matche. +/// Matches any of supplied guards. pub struct AnyGuard(Vec>); impl AnyGuard { diff --git a/src/handler.rs b/src/handler.rs index 33cd2408d..669512ab3 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -6,8 +6,8 @@ use std::task::{Context, Poll}; use actix_http::{Error, Response}; use actix_service::{Service, ServiceFactory}; -use futures::future::{ok, Ready}; -use futures::ready; +use futures_util::future::{ok, Ready}; +use futures_util::ready; use pin_project::pin_project; use crate::extract::FromRequest; diff --git a/src/info.rs b/src/info.rs index c9a642b36..5b506d85a 100644 --- a/src/info.rs +++ b/src/info.rs @@ -12,8 +12,8 @@ const X_FORWARDED_PROTO: &[u8] = b"x-forwarded-proto"; pub struct ConnectionInfo { scheme: String, host: String, - remote: Option, - peer: Option, + realip_remote_addr: Option, + remote_addr: Option, } impl ConnectionInfo { @@ -29,8 +29,7 @@ impl ConnectionInfo { fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { let mut host = None; let mut scheme = None; - let mut remote = None; - let mut peer = None; + let mut realip_remote_addr = None; // load forwarded header for hdr in req.headers.get_all(&header::FORWARDED) { @@ -42,8 +41,8 @@ impl ConnectionInfo { if let Some(val) = items.next() { match &name.to_lowercase() as &str { "for" => { - if remote.is_none() { - remote = Some(val.trim()); + if realip_remote_addr.is_none() { + realip_remote_addr = Some(val.trim()); } } "proto" => { @@ -106,27 +105,25 @@ impl ConnectionInfo { } } - // remote addr - if remote.is_none() { + // get remote_addraddr from socketaddr + let remote_addr = req.peer_addr.map(|addr| format!("{}", addr)); + + if realip_remote_addr.is_none() { if let Some(h) = req .headers .get(&HeaderName::from_lowercase(X_FORWARDED_FOR).unwrap()) { if let Ok(h) = h.to_str() { - remote = h.split(',').next().map(|v| v.trim()); + realip_remote_addr = h.split(',').next().map(|v| v.trim()); } } - if remote.is_none() { - // get peeraddr from socketaddr - peer = req.peer_addr.map(|addr| format!("{}", addr)); - } } ConnectionInfo { - peer, + remote_addr, scheme: scheme.unwrap_or("http").to_owned(), host: host.unwrap_or("localhost").to_owned(), - remote: remote.map(|s| s.to_owned()), + realip_remote_addr: realip_remote_addr.map(|s| s.to_owned()), } } @@ -155,13 +152,23 @@ impl ConnectionInfo { &self.host } - /// Remote socket addr of client initiated HTTP request. + /// remote_addr address of the request. + /// + /// Get remote_addr address from socket address + pub fn remote_addr(&self) -> Option<&str> { + if let Some(ref remote_addr) = self.remote_addr { + Some(remote_addr) + } else { + None + } + } + /// Real ip remote addr of client initiated HTTP request. /// /// The addr is resolved through the following headers, in this order: /// /// - Forwarded /// - X-Forwarded-For - /// - peer name of opened socket + /// - remote_addr name of opened socket /// /// # Security /// Do not use this function for security purposes, unless you can ensure the Forwarded and @@ -169,11 +176,11 @@ impl ConnectionInfo { /// address explicitly, use /// [`HttpRequest::peer_addr()`](../web/struct.HttpRequest.html#method.peer_addr) instead. #[inline] - pub fn remote(&self) -> Option<&str> { - if let Some(ref r) = self.remote { + pub fn realip_remote_addr(&self) -> Option<&str> { + if let Some(ref r) = self.realip_remote_addr { Some(r) - } else if let Some(ref peer) = self.peer { - Some(peer) + } else if let Some(ref remote_addr) = self.remote_addr { + Some(remote_addr) } else { None } @@ -202,7 +209,7 @@ mod tests { let info = req.connection_info(); assert_eq!(info.scheme(), "https"); assert_eq!(info.host(), "rust-lang.org"); - assert_eq!(info.remote(), Some("192.0.2.60")); + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); let req = TestRequest::default() .header(header::HOST, "rust-lang.org") @@ -211,20 +218,20 @@ mod tests { let info = req.connection_info(); assert_eq!(info.scheme(), "http"); assert_eq!(info.host(), "rust-lang.org"); - assert_eq!(info.remote(), None); + assert_eq!(info.realip_remote_addr(), None); let req = TestRequest::default() .header(X_FORWARDED_FOR, "192.0.2.60") .to_http_request(); let info = req.connection_info(); - assert_eq!(info.remote(), Some("192.0.2.60")); + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); let req = TestRequest::default() .header(X_FORWARDED_HOST, "192.0.2.60") .to_http_request(); let info = req.connection_info(); assert_eq!(info.host(), "192.0.2.60"); - assert_eq!(info.remote(), None); + assert_eq!(info.realip_remote_addr(), None); let req = TestRequest::default() .header(X_FORWARDED_PROTO, "https") diff --git a/src/lib.rs b/src/lib.rs index f1a074955..c7c569756 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![deny(rust_2018_idioms, warnings)] +#![warn(rust_2018_idioms, warnings)] #![allow( clippy::needless_doctest_main, clippy::type_complexity, @@ -7,6 +7,12 @@ //! Actix web is a small, pragmatic, and extremely fast web framework //! for Rust. //! +//! ## Example +//! +//! The `#[actix_rt::main]` macro in the example below is provided by the Actix runtime +//! crate, [`actix-rt`](https://crates.io/crates/actix-rt). You will need to include +//! `actix-rt` in your dependencies for it to run. +//! //! ```rust,no_run //! use actix_web::{web, App, Responder, HttpServer}; //! @@ -66,7 +72,7 @@ //! * SSL support with OpenSSL or `native-tls` //! * Middlewares (`Logger`, `Session`, `CORS`, `DefaultHeaders`) //! * Supports [Actix actor framework](https://github.com/actix/actix) -//! * Supported Rust version: 1.39 or later +//! * Supported Rust version: 1.40 or later //! //! ## Package feature //! diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 70006ab3c..6de451c84 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -11,7 +11,7 @@ use actix_http::encoding::Encoder; use actix_http::http::header::{ContentEncoding, ACCEPT_ENCODING}; use actix_http::Error; use actix_service::{Service, Transform}; -use futures::future::{ok, Ready}; +use futures_util::future::{ok, Ready}; use pin_project::pin_project; use crate::dev::BodyEncoding; @@ -133,7 +133,7 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); - match futures::ready!(this.fut.poll(cx)) { + match futures_util::ready!(this.fut.poll(cx)) { Ok(resp) => { let enc = if let Some(enc) = resp.response().get_encoding() { enc diff --git a/src/middleware/condition.rs b/src/middleware/condition.rs index 68d06837e..7ff81437b 100644 --- a/src/middleware/condition.rs +++ b/src/middleware/condition.rs @@ -2,10 +2,10 @@ use std::task::{Context, Poll}; use actix_service::{Service, Transform}; -use futures::future::{ok, Either, FutureExt, LocalBoxFuture}; +use futures_util::future::{ok, Either, FutureExt, LocalBoxFuture}; /// `Middleware` for conditionally enables another middleware. -/// The controled middleware must not change the `Service` interfaces. +/// The controlled middleware must not change the `Service` interfaces. /// This means you cannot control such middlewares like `Logger` or `Compress`. /// /// ## Usage diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index ba001c77b..ef2e56e69 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use std::task::{Context, Poll}; use actix_service::{Service, Transform}; -use futures::future::{ok, FutureExt, LocalBoxFuture, Ready}; +use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready}; use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; use crate::http::{Error as HttpError, HeaderMap}; @@ -157,7 +157,7 @@ where #[cfg(test)] mod tests { use actix_service::IntoService; - use futures::future::ok; + use futures_util::future::ok; use super::*; use crate::dev::ServiceRequest; diff --git a/src/middleware/errhandlers.rs b/src/middleware/errhandlers.rs index 71886af0b..93a5d3f22 100644 --- a/src/middleware/errhandlers.rs +++ b/src/middleware/errhandlers.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use std::task::{Context, Poll}; use actix_service::{Service, Transform}; -use futures::future::{ok, FutureExt, LocalBoxFuture, Ready}; +use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready}; use fxhash::FxHashMap; use crate::dev::{ServiceRequest, ServiceResponse}; @@ -147,7 +147,7 @@ where #[cfg(test)] mod tests { use actix_service::IntoService; - use futures::future::ok; + use futures_util::future::ok; use super::*; use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode}; diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index d692132ce..8b881c0a4 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -11,7 +11,7 @@ use std::task::{Context, Poll}; use actix_service::{Service, Transform}; use bytes::Bytes; -use futures::future::{ok, Ready}; +use futures_util::future::{ok, Ready}; use log::debug; use regex::Regex; use time::OffsetDateTime; @@ -72,12 +72,21 @@ use crate::HttpResponse; /// /// `%U` Request URL /// +/// `%{r}a` Real IP remote address **\*** +/// /// `%{FOO}i` request.headers['FOO'] /// /// `%{FOO}o` response.headers['FOO'] /// /// `%{FOO}e` os.environ['FOO'] /// +/// # Security +/// **\*** It is calculated using +/// [`ConnectionInfo::realip_remote_addr()`](../dev/struct.ConnectionInfo.html#method.realip_remote_addr) +/// +/// If you use this value ensure that all requests come from trusted hosts, since it is trivial +/// for the remote client to simulate been another client. +/// pub struct Logger(Rc); struct Inner { @@ -163,11 +172,11 @@ where LoggerResponse { fut: self.service.call(req), format: None, - time: OffsetDateTime::now(), + time: OffsetDateTime::now_utc(), _t: PhantomData, } } else { - let now = OffsetDateTime::now(); + let now = OffsetDateTime::now_utc(); let mut format = self.inner.format.clone(); for unit in &mut format.0 { @@ -207,7 +216,7 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); - let res = match futures::ready!(this.fut.poll(cx)) { + let res = match futures_util::ready!(this.fut.poll(cx)) { Ok(res) => res, Err(e) => return Poll::Ready(Err(e)), }; @@ -238,15 +247,20 @@ where } } +use pin_project::{pin_project, pinned_drop}; + +#[pin_project(PinnedDrop)] pub struct StreamLog { + #[pin] body: ResponseBody, format: Option, size: usize, time: OffsetDateTime, } -impl Drop for StreamLog { - fn drop(&mut self) { +#[pinned_drop] +impl PinnedDrop for StreamLog { + fn drop(self: Pin<&mut Self>) { if let Some(ref format) = self.format { let render = |fmt: &mut Formatter<'_>| { for unit in &format.0 { @@ -264,10 +278,14 @@ impl MessageBody for StreamLog { self.body.size() } - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { - match self.body.poll_next(cx) { + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let this = self.project(); + match this.body.poll_next(cx) { Poll::Ready(Some(Ok(chunk))) => { - self.size += chunk.len(); + *this.size += chunk.len(); Poll::Ready(Some(Ok(chunk))) } val => val, @@ -294,7 +312,7 @@ impl Format { /// Returns `None` if the format string syntax is incorrect. pub fn new(s: &str) -> Format { log::trace!("Access log format: {}", s); - let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrUsbTD]?)").unwrap(); + let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe])|[atPrUsbTD]?)").unwrap(); let mut idx = 0; let mut results = Vec::new(); @@ -308,6 +326,13 @@ impl Format { if let Some(key) = cap.get(2) { results.push(match cap.get(3).unwrap().as_str() { + "a" => { + if key.as_str() == "r" { + FormatText::RealIPRemoteAddr + } else { + unreachable!() + } + } "i" => FormatText::RequestHeader( HeaderName::try_from(key.as_str()).unwrap(), ), @@ -355,6 +380,7 @@ pub enum FormatText { Time, TimeMillis, RemoteAddr, + RealIPRemoteAddr, UrlPath, RequestHeader(HeaderName), ResponseHeader(HeaderName), @@ -373,12 +399,12 @@ impl FormatText { FormatText::Percent => "%".fmt(fmt), FormatText::ResponseSize => size.fmt(fmt), FormatText::Time => { - let rt = OffsetDateTime::now() - entry_time; + let rt = OffsetDateTime::now_utc() - entry_time; let rt = rt.as_seconds_f64(); fmt.write_fmt(format_args!("{:.6}", rt)) } FormatText::TimeMillis => { - let rt = OffsetDateTime::now() - entry_time; + let rt = OffsetDateTime::now_utc() - entry_time; let rt = (rt.whole_nanoseconds() as f64) / 1_000_000.0; fmt.write_fmt(format_args!("{:.6}", rt)) } @@ -451,7 +477,16 @@ impl FormatText { *self = FormatText::Str(s.to_string()); } FormatText::RemoteAddr => { - let s = if let Some(remote) = req.connection_info().remote() { + let s = if let Some(ref peer) = req.connection_info().remote_addr() { + FormatText::Str(peer.to_string()) + } else { + FormatText::Str("-".to_string()) + }; + *self = s; + } + FormatText::RealIPRemoteAddr => { + let s = if let Some(remote) = req.connection_info().realip_remote_addr() + { FormatText::Str(remote.to_string()) } else { FormatText::Str("-".to_string()) @@ -476,7 +511,7 @@ impl<'a> fmt::Display for FormatDisplay<'a> { #[cfg(test)] mod tests { use actix_service::{IntoService, Service, Transform}; - use futures::future::ok; + use futures_util::future::ok; use super::*; use crate::http::{header, StatusCode}; @@ -513,7 +548,7 @@ mod tests { .uri("/test/route/yeah") .to_srv_request(); - let now = OffsetDateTime::now(); + let now = OffsetDateTime::now_utc(); for unit in &mut format.0 { unit.render_request(now, &req); } @@ -542,9 +577,10 @@ mod tests { header::USER_AGENT, header::HeaderValue::from_static("ACTIX-WEB"), ) + .peer_addr("127.0.0.1:8081".parse().unwrap()) .to_srv_request(); - let now = OffsetDateTime::now(); + let now = OffsetDateTime::now_utc(); for unit in &mut format.0 { unit.render_request(now, &req); } @@ -554,7 +590,7 @@ mod tests { unit.render_response(&resp); } - let entry_time = OffsetDateTime::now(); + let entry_time = OffsetDateTime::now_utc(); let render = |fmt: &mut Formatter<'_>| { for unit in &format.0 { unit.render(fmt, 1024, entry_time)?; @@ -563,6 +599,7 @@ mod tests { }; let s = format!("{}", FormatDisplay(&render)); assert!(s.contains("GET / HTTP/1.1")); + assert!(s.contains("127.0.0.1")); assert!(s.contains("200 1024")); assert!(s.contains("ACTIX-WEB")); } @@ -572,7 +609,7 @@ mod tests { let mut format = Format::new("%t"); let req = TestRequest::default().to_srv_request(); - let now = OffsetDateTime::now(); + let now = OffsetDateTime::now_utc(); for unit in &mut format.0 { unit.render_request(now, &req); } @@ -591,4 +628,38 @@ mod tests { let s = format!("{}", FormatDisplay(&render)); assert!(s.contains(&format!("{}", now.format("%Y-%m-%dT%H:%M:%S")))); } + + #[actix_rt::test] + async fn test_remote_addr_format() { + let mut format = Format::new("%{r}a"); + + let req = TestRequest::with_header( + header::FORWARDED, + header::HeaderValue::from_static( + "for=192.0.2.60;proto=http;by=203.0.113.43", + ), + ) + .to_srv_request(); + + let now = OffsetDateTime::now_utc(); + for unit in &mut format.0 { + unit.render_request(now, &req); + } + + let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); + for unit in &mut format.0 { + unit.render_response(&resp); + } + + let entry_time = OffsetDateTime::now_utc(); + let render = |fmt: &mut Formatter<'_>| { + for unit in &format.0 { + unit.render(fmt, 1024, entry_time)?; + } + Ok(()) + }; + let s = format!("{}", FormatDisplay(&render)); + println!("{}", s); + assert!(s.contains("192.0.2.60")); + } } diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index f6b834bfe..d23f03445 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -4,7 +4,7 @@ use std::task::{Context, Poll}; use actix_http::http::{PathAndQuery, Uri}; use actix_service::{Service, Transform}; use bytes::Bytes; -use futures::future::{ok, Ready}; +use futures_util::future::{ok, Ready}; use regex::Regex; use crate::service::{ServiceRequest, ServiceResponse}; @@ -74,9 +74,13 @@ where fn call(&mut self, mut req: ServiceRequest) -> Self::Future { let head = req.head_mut(); - let path = head.uri.path(); + + // always add trailing slash, might be an extra one + let path = head.uri.path().to_string() + "/"; let original_len = path.len(); - let path = self.merge_slash.replace_all(path, "/"); + + // normalize multiple /'s to one / + let path = self.merge_slash.replace_all(&path, "/"); if original_len != path.len() { let mut parts = head.uri.clone().into_parts(); @@ -119,6 +123,14 @@ mod tests { let req = TestRequest::with_uri("/v1//something////").to_request(); let res = call_service(&mut app, req).await; assert!(res.status().is_success()); + + let req2 = TestRequest::with_uri("//v1/something").to_request(); + let res2 = call_service(&mut app, req2).await; + assert!(res2.status().is_success()); + + let req3 = TestRequest::with_uri("//v1//////something").to_request(); + let res3 = call_service(&mut app, req3).await; + assert!(res3.status().is_success()); } #[actix_rt::test] @@ -136,6 +148,14 @@ mod tests { let req = TestRequest::with_uri("/v1//something////").to_srv_request(); let res = normalize.call(req).await.unwrap(); assert!(res.status().is_success()); + + let req2 = TestRequest::with_uri("///v1/something").to_srv_request(); + let res2 = normalize.call(req2).await.unwrap(); + assert!(res2.status().is_success()); + + let req3 = TestRequest::with_uri("//v1///something").to_srv_request(); + let res3 = normalize.call(req3).await.unwrap(); + assert!(res3.status().is_success()); } #[actix_rt::test] @@ -156,4 +176,23 @@ mod tests { let res = normalize.call(req).await.unwrap(); assert!(res.status().is_success()); } + + #[actix_rt::test] + async fn should_normalize_nothing_notrail() { + const URI: &str = "/v1/something"; + + let srv = |req: ServiceRequest| { + assert_eq!(URI, req.path()); + ok(req.into_response(HttpResponse::Ok().finish())) + }; + + let mut normalize = NormalizePath + .new_transform(srv.into_service()) + .await + .unwrap(); + + let req = TestRequest::with_uri(URI).to_srv_request(); + let res = normalize.call(req).await.unwrap(); + assert!(res.status().is_success()); + } } diff --git a/src/request.rs b/src/request.rs index cd9c72313..f8abeb1bb 100644 --- a/src/request.rs +++ b/src/request.rs @@ -5,7 +5,8 @@ use std::{fmt, net}; use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; use actix_router::{Path, Url}; -use futures::future::{ok, Ready}; +use futures_util::future::{ok, Ready}; +use tinyvec::TinyVec; use crate::config::AppConfig; use crate::error::UrlGenerationError; @@ -21,7 +22,7 @@ pub(crate) struct HttpRequestInner { pub(crate) head: Message, pub(crate) path: Path, pub(crate) payload: Payload, - pub(crate) app_data: Rc, + pub(crate) app_data: TinyVec<[Rc; 4]>, rmap: Rc, config: AppConfig, pool: &'static HttpRequestPool, @@ -38,13 +39,16 @@ impl HttpRequest { app_data: Rc, pool: &'static HttpRequestPool, ) -> HttpRequest { + let mut data = TinyVec::<[Rc; 4]>::new(); + data.push(app_data); + HttpRequest(Rc::new(HttpRequestInner { head, path, payload, rmap, config, - app_data, + app_data: data, pool, })) } @@ -57,7 +61,7 @@ impl HttpRequest { &self.0.head } - /// This method returns muttable reference to the request head. + /// This method returns mutable reference to the request head. /// panics if multiple references of http request exists. #[inline] pub(crate) fn head_mut(&mut self) -> &mut RequestHead { @@ -215,11 +219,13 @@ impl HttpRequest { /// let opt_t = req.app_data::>(); /// ``` pub fn app_data(&self) -> Option<&T> { - if let Some(st) = self.0.app_data.get::() { - Some(&st) - } else { - None + for container in self.0.app_data.iter().rev() { + if let Some(data) = container.get::() { + return Some(data); + } } + + None } } @@ -342,10 +348,13 @@ impl HttpRequestPool { #[cfg(test)] mod tests { + use actix_service::Service; + use bytes::Bytes; + use super::*; use crate::dev::{ResourceDef, ResourceMap}; use crate::http::{header, StatusCode}; - use crate::test::{call_service, init_service, TestRequest}; + use crate::test::{call_service, init_service, read_body, TestRequest}; use crate::{web, App, HttpResponse}; #[test] @@ -494,6 +503,68 @@ mod tests { assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } + #[actix_rt::test] + async fn test_cascading_data() { + #[allow(dead_code)] + fn echo_usize(req: HttpRequest) -> HttpResponse { + let num = req.app_data::().unwrap(); + HttpResponse::Ok().body(num.to_string()) + } + + let mut srv = init_service( + App::new() + .app_data(88usize) + .service(web::resource("/").route(web::get().to(echo_usize))) + .service( + web::resource("/one") + .app_data(1u32) + .route(web::get().to(echo_usize)), + ), + ) + .await; + + let req = TestRequest::get().uri("/").to_request(); + let resp = srv.call(req).await.unwrap(); + let body = read_body(resp).await; + assert_eq!(body, Bytes::from_static(b"88")); + + let req = TestRequest::get().uri("/one").to_request(); + let resp = srv.call(req).await.unwrap(); + let body = read_body(resp).await; + assert_eq!(body, Bytes::from_static(b"88")); + } + + #[actix_rt::test] + async fn test_overwrite_data() { + #[allow(dead_code)] + fn echo_usize(req: HttpRequest) -> HttpResponse { + let num = req.app_data::().unwrap(); + HttpResponse::Ok().body(num.to_string()) + } + + let mut srv = init_service( + App::new() + .app_data(88usize) + .service(web::resource("/").route(web::get().to(echo_usize))) + .service( + web::resource("/one") + .app_data(1usize) + .route(web::get().to(echo_usize)), + ), + ) + .await; + + let req = TestRequest::get().uri("/").to_request(); + let resp = srv.call(req).await.unwrap(); + let body = read_body(resp).await; + assert_eq!(body, Bytes::from_static(b"88")); + + let req = TestRequest::get().uri("/one").to_request(); + let resp = srv.call(req).await.unwrap(); + let body = read_body(resp).await; + assert_eq!(body, Bytes::from_static(b"1")); + } + #[actix_rt::test] async fn test_extensions_dropped() { struct Tracker { diff --git a/src/resource.rs b/src/resource.rs index d03024a07..5da1de62f 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -11,7 +11,7 @@ use actix_service::boxed::{self, BoxService, BoxServiceFactory}; use actix_service::{ apply, apply_fn_factory, IntoServiceFactory, Service, ServiceFactory, Transform, }; -use futures::future::{ok, Either, LocalBoxFuture, Ready}; +use futures_util::future::{ok, Either, LocalBoxFuture, Ready}; use crate::data::Data; use crate::dev::{insert_slash, AppService, HttpServiceFactory, ResourceDef}; @@ -46,7 +46,7 @@ type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Err /// ``` /// /// If no matching route could be found, *405* response code get returned. -/// Default behavior could be overriden with `default_resource()` method. +/// Default behavior could be overridden with `default_resource()` method. pub struct Resource { endpoint: T, rdef: Vec, @@ -196,9 +196,9 @@ where self.app_data(Data::new(data)) } - /// Set or override application data. + /// Add resource data. /// - /// This method overrides data stored with [`App::app_data()`](#method.app_data) + /// Data of different types from parent contexts will still be accessible. pub fn app_data(mut self, data: U) -> Self { if self.data.is_none() { self.data = Some(Extensions::new()); @@ -379,7 +379,7 @@ where let guards = if self.guards.is_empty() { None } else { - Some(std::mem::replace(&mut self.guards, Vec::new())) + Some(std::mem::take(&mut self.guards)) }; let mut rdef = if config.is_root() || !self.rdef.is_empty() { ResourceDef::new(insert_slash(self.rdef.clone())) @@ -393,6 +393,7 @@ where if let Some(ref mut ext) = self.data { config.set_service_data(ext); } + config.register_service(rdef, guards, self, None) } } @@ -536,12 +537,15 @@ impl Service for ResourceService { for route in self.routes.iter_mut() { if route.check(&mut req) { if let Some(ref data) = self.data { - req.set_data_container(data.clone()); + req.add_data_container(data.clone()); } return Either::Right(route.call(req)); } } if let Some(ref mut default) = self.default { + if let Some(ref data) = self.data { + req.add_data_container(data.clone()); + } Either::Right(default.call(req)) } else { let req = req.into_parts().0; @@ -584,7 +588,7 @@ mod tests { use actix_rt::time::delay_for; use actix_service::Service; - use futures::future::ok; + use futures_util::future::ok; use crate::http::{header, HeaderValue, Method, StatusCode}; use crate::middleware::DefaultHeaders; @@ -649,11 +653,9 @@ mod tests { #[actix_rt::test] async fn test_to() { let mut srv = - init_service(App::new().service(web::resource("/test").to(|| { - async { - delay_for(Duration::from_millis(100)).await; - Ok::<_, Error>(HttpResponse::Ok()) - } + init_service(App::new().service(web::resource("/test").to(|| async { + delay_for(Duration::from_millis(100)).await; + Ok::<_, Error>(HttpResponse::Ok()) }))) .await; let req = TestRequest::with_uri("/test").to_request(); @@ -793,4 +795,23 @@ mod tests { let resp = call_service(&mut srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } + + #[actix_rt::test] + async fn test_data_default_service() { + let mut srv = init_service( + App::new().data(1usize).service( + web::resource("/test") + .data(10usize) + .default_service(web::to(|data: web::Data| { + assert_eq!(**data, 10); + HttpResponse::Ok() + })), + ), + ) + .await; + + let req = TestRequest::get().uri("/test").to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + } } diff --git a/src/responder.rs b/src/responder.rs index 7189eecf1..e102d23e1 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -10,9 +10,9 @@ use actix_http::http::{ }; use actix_http::{Error, Response, ResponseBuilder}; use bytes::{Bytes, BytesMut}; -use futures::future::{err, ok, Either as EitherFuture, Ready}; -use futures::ready; -use pin_project::{pin_project, project}; +use futures_util::future::{err, ok, Either as EitherFuture, Ready}; +use futures_util::ready; +use pin_project::pin_project; use crate::request::HttpRequest; @@ -379,7 +379,7 @@ where } } -#[pin_project] +#[pin_project(project = EitherResponderProj)] pub enum EitherResponder where A: Responder, @@ -396,14 +396,12 @@ where { type Output = Result; - #[project] fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - #[project] match self.project() { - EitherResponder::A(fut) => { + EitherResponderProj::A(fut) => { Poll::Ready(ready!(fut.poll(cx)).map_err(|e| e.into())) } - EitherResponder::B(fut) => { + EitherResponderProj::B(fut) => { Poll::Ready(ready!(fut.poll(cx).map_err(|e| e.into()))) } } diff --git a/src/route.rs b/src/route.rs index f7e391746..2763f3b1a 100644 --- a/src/route.rs +++ b/src/route.rs @@ -5,7 +5,7 @@ use std::task::{Context, Poll}; use actix_http::{http::Method, Error}; use actix_service::{Service, ServiceFactory}; -use futures::future::{ready, FutureExt, LocalBoxFuture}; +use futures_util::future::{ready, FutureExt, LocalBoxFuture}; use crate::extract::FromRequest; use crate::guard::{self, Guard}; @@ -56,7 +56,7 @@ impl Route { } pub(crate) fn take_guards(&mut self) -> Vec> { - std::mem::replace(Rc::get_mut(&mut self.guards).unwrap(), Vec::new()) + std::mem::take(Rc::get_mut(&mut self.guards).unwrap()) } } @@ -362,31 +362,23 @@ mod tests { .service( web::resource("/test") .route(web::get().to(|| HttpResponse::Ok())) - .route(web::put().to(|| { - async { - Err::(error::ErrorBadRequest("err")) - } + .route(web::put().to(|| async { + Err::(error::ErrorBadRequest("err")) })) - .route(web::post().to(|| { - async { - delay_for(Duration::from_millis(100)).await; - HttpResponse::Created() - } + .route(web::post().to(|| async { + delay_for(Duration::from_millis(100)).await; + HttpResponse::Created() })) - .route(web::delete().to(|| { - async { - delay_for(Duration::from_millis(100)).await; - Err::(error::ErrorBadRequest("err")) - } + .route(web::delete().to(|| async { + delay_for(Duration::from_millis(100)).await; + Err::(error::ErrorBadRequest("err")) })), ) - .service(web::resource("/json").route(web::get().to(|| { - async { - delay_for(Duration::from_millis(25)).await; - web::Json(MyObject { - name: "test".to_string(), - }) - } + .service(web::resource("/json").route(web::get().to(|| async { + delay_for(Duration::from_millis(25)).await; + web::Json(MyObject { + name: "test".to_string(), + }) }))), ) .await; diff --git a/src/scope.rs b/src/scope.rs index 18e775e61..b9166a4bf 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,5 +1,6 @@ use std::cell::RefCell; use std::fmt; +use std::future::Future; use std::pin::Pin; use std::rc::Rc; use std::task::{Context, Poll}; @@ -10,7 +11,7 @@ use actix_service::boxed::{self, BoxService, BoxServiceFactory}; use actix_service::{ apply, apply_fn_factory, IntoServiceFactory, Service, ServiceFactory, Transform, }; -use futures::future::{ok, Either, Future, LocalBoxFuture, Ready}; +use futures_util::future::{ok, Either, LocalBoxFuture, Ready}; use crate::config::ServiceConfig; use crate::data::Data; @@ -54,7 +55,7 @@ type BoxedResponse = LocalBoxFuture<'static, Result>; /// ``` /// /// In the above example three routes get registered: -/// * /{project_id}/path1 - reponds to all http method +/// * /{project_id}/path1 - responds to all http method /// * /{project_id}/path2 - `GET` requests /// * /{project_id}/path3 - `HEAD` requests /// @@ -151,9 +152,9 @@ where self.app_data(Data::new(data)) } - /// Set or override application data. + /// Add scope data. /// - /// This method overrides data stored with [`App::app_data()`](#method.app_data) + /// Data of different types from parent contexts will still be accessible. pub fn app_data(mut self, data: U) -> Self { if self.data.is_none() { self.data = Some(Extensions::new()); @@ -303,7 +304,7 @@ where /// Registers middleware, in the form of a middleware component (type), /// that runs during inbound processing in the request - /// lifecycle (request -> response), modifying request as + /// life-cycle (request -> response), modifying request as /// necessary, across all requests managed by the *Scope*. Scope-level /// middleware is more limited in what it can modify, relative to Route or /// Application level middleware, in that Scope-level middleware can not modify @@ -344,7 +345,7 @@ where } /// Registers middleware, in the form of a closure, that runs during inbound - /// processing in the request lifecycle (request -> response), modifying + /// processing in the request life-cycle (request -> response), modifying /// request as necessary, across all requests managed by the *Scope*. /// Scope-level middleware is more limited in what it can modify, relative /// to Route or Application level middleware, in that Scope-level middleware @@ -429,7 +430,7 @@ where let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef)); // external resources - for mut rdef in std::mem::replace(&mut self.external, Vec::new()) { + for mut rdef in std::mem::take(&mut self.external) { rmap.add(&mut rdef, None); } @@ -622,10 +623,13 @@ impl Service for ScopeService { if let Some((srv, _info)) = res { if let Some(ref data) = self.data { - req.set_data_container(data.clone()); + req.add_data_container(data.clone()); } Either::Left(srv.call(req)) } else if let Some(ref mut default) = self.default { + if let Some(ref data) = self.data { + req.add_data_container(data.clone()); + } Either::Left(default.call(req)) } else { let req = req.into_parts().0; @@ -663,7 +667,7 @@ impl ServiceFactory for ScopeEndpoint { mod tests { use actix_service::Service; use bytes::Bytes; - use futures::future::ok; + use futures_util::future::ok; use crate::dev::{Body, ResponseBody}; use crate::http::{header, HeaderValue, Method, StatusCode}; @@ -825,11 +829,9 @@ mod tests { async fn test_scope_variable_segment() { let mut srv = init_service(App::new().service(web::scope("/ab-{project}").service( - web::resource("/path1").to(|r: HttpRequest| { - async move { - HttpResponse::Ok() - .body(format!("project: {}", &r.match_info()["project"])) - } + web::resource("/path1").to(|r: HttpRequest| async move { + HttpResponse::Ok() + .body(format!("project: {}", &r.match_info()["project"])) }), ))) .await; @@ -937,11 +939,9 @@ mod tests { async fn test_nested_scope_with_variable_segment() { let mut srv = init_service(App::new().service(web::scope("/app").service( web::scope("/{project_id}").service(web::resource("/path1").to( - |r: HttpRequest| { - async move { - HttpResponse::Created() - .body(format!("project: {}", &r.match_info()["project_id"])) - } + |r: HttpRequest| async move { + HttpResponse::Created() + .body(format!("project: {}", &r.match_info()["project_id"])) }, )), ))) @@ -964,14 +964,12 @@ mod tests { async fn test_nested2_scope_with_variable_segment() { let mut srv = init_service(App::new().service(web::scope("/app").service( web::scope("/{project}").service(web::scope("/{id}").service( - web::resource("/path1").to(|r: HttpRequest| { - async move { - HttpResponse::Created().body(format!( - "project: {} - {}", - &r.match_info()["project"], - &r.match_info()["id"], - )) - } + web::resource("/path1").to(|r: HttpRequest| async move { + HttpResponse::Created().body(format!( + "project: {} - {}", + &r.match_info()["project"], + &r.match_info()["id"], + )) }), )), ))) @@ -1119,6 +1117,23 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } + #[actix_rt::test] + async fn test_override_data_default_service() { + let mut srv = init_service(App::new().data(1usize).service( + web::scope("app").data(10usize).default_service(web::to( + |data: web::Data| { + assert_eq!(**data, 10); + HttpResponse::Ok() + }, + )), + )) + .await; + + let req = TestRequest::with_uri("/app/t").to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + } + #[actix_rt::test] async fn test_override_app_data() { let mut srv = init_service(App::new().app_data(web::Data::new(1usize)).service( @@ -1177,15 +1192,11 @@ mod tests { ); s.route( "/", - web::get().to(|req: HttpRequest| { - async move { - HttpResponse::Ok().body(format!( - "{}", - req.url_for("youtube", &["xxxxxx"]) - .unwrap() - .as_str() - )) - } + web::get().to(|req: HttpRequest| async move { + HttpResponse::Ok().body(format!( + "{}", + req.url_for("youtube", &["xxxxxx"]).unwrap().as_str() + )) }), ); })); @@ -1203,11 +1214,9 @@ mod tests { async fn test_url_for_nested() { let mut srv = init_service(App::new().service(web::scope("/a").service( web::scope("/b").service(web::resource("/c/{stuff}").name("c").route( - web::get().to(|req: HttpRequest| { - async move { - HttpResponse::Ok() - .body(format!("{}", req.url_for("c", &["12345"]).unwrap())) - } + web::get().to(|req: HttpRequest| async move { + HttpResponse::Ok() + .body(format!("{}", req.url_for("c", &["12345"]).unwrap())) }), )), ))) diff --git a/src/server.rs b/src/server.rs index 11cfbb6bc..b2695b004 100644 --- a/src/server.rs +++ b/src/server.rs @@ -6,14 +6,12 @@ use actix_http::{body::MessageBody, Error, HttpService, KeepAlive, Request, Resp use actix_server::{Server, ServerBuilder}; use actix_service::{map_config, IntoServiceFactory, Service, ServiceFactory}; -use net2::TcpBuilder; - #[cfg(unix)] use actix_http::Protocol; #[cfg(unix)] use actix_service::pipeline_factory; #[cfg(unix)] -use futures::future::ok; +use futures_util::future::ok; #[cfg(feature = "openssl")] use actix_tls::openssl::{AlpnError, SslAcceptor, SslAcceptorBuilder}; @@ -443,8 +441,6 @@ where #[cfg(unix)] /// Start listening for unix domain connections on existing listener. - /// - /// This method is available with `uds` feature. pub fn listen_uds( mut self, lst: std::os::unix::net::UnixListener, @@ -483,8 +479,6 @@ where #[cfg(unix)] /// Start listening for incoming unix domain connections. - /// - /// This method is available with `uds` feature. pub fn bind_uds(mut self, addr: A) -> io::Result where A: AsRef, @@ -566,13 +560,16 @@ fn create_tcp_listener( addr: net::SocketAddr, backlog: i32, ) -> io::Result { - let builder = match addr { - net::SocketAddr::V4(_) => TcpBuilder::new_v4()?, - net::SocketAddr::V6(_) => TcpBuilder::new_v6()?, + use socket2::{Domain, Protocol, Socket, Type}; + let domain = match addr { + net::SocketAddr::V4(_) => Domain::ipv4(), + net::SocketAddr::V6(_) => Domain::ipv6(), }; - builder.reuse_address(true)?; - builder.bind(addr)?; - Ok(builder.listen(backlog)?) + let socket = Socket::new(domain, Type::stream(), Some(Protocol::tcp()))?; + socket.set_reuse_address(true)?; + socket.bind(&addr.into())?; + socket.listen(backlog)?; + Ok(socket.into_tcp_listener()) } #[cfg(feature = "openssl")] diff --git a/src/service.rs b/src/service.rs index e51be9964..232a2f132 100644 --- a/src/service.rs +++ b/src/service.rs @@ -69,7 +69,7 @@ impl ServiceRequest { /// Construct request from parts. /// - /// `ServiceRequest` can be re-constructed only if `req` hasnt been cloned. + /// `ServiceRequest` can be re-constructed only if `req` hasn't been cloned. pub fn from_parts( mut req: HttpRequest, pl: Payload, @@ -217,11 +217,13 @@ impl ServiceRequest { /// Get an application data stored with `App::data()` method during /// application configuration. pub fn app_data(&self) -> Option> { - if let Some(st) = (self.0).0.app_data.get::>() { - Some(st.clone()) - } else { - None + for container in (self.0).0.app_data.iter().rev() { + if let Some(data) = container.get::>() { + return Some(Data::clone(&data)); + } } + + None } /// Set request payload. @@ -230,9 +232,12 @@ impl ServiceRequest { } #[doc(hidden)] - /// Set new app data container - pub fn set_data_container(&mut self, extensions: Rc) { - Rc::get_mut(&mut (self.0).0).unwrap().app_data = extensions; + /// Add app data container to request's resolution set. + pub fn add_data_container(&mut self, extensions: Rc) { + Rc::get_mut(&mut (self.0).0) + .unwrap() + .app_data + .push(extensions); } } @@ -510,7 +515,7 @@ where let guards = if self.guards.is_empty() { None } else { - Some(std::mem::replace(&mut self.guards, Vec::new())) + Some(std::mem::take(&mut self.guards)) }; let mut rdef = if config.is_root() || !self.rdef.is_empty() { @@ -531,7 +536,7 @@ mod tests { use crate::test::{init_service, TestRequest}; use crate::{guard, http, web, App, HttpResponse}; use actix_service::Service; - use futures::future::ok; + use futures_util::future::ok; #[test] fn test_service_request() { @@ -578,7 +583,6 @@ mod tests { let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::NOT_FOUND); } - #[test] fn test_fmt_debug() { let req = TestRequest::get() diff --git a/src/test.rs b/src/test.rs index 956980530..a64ec3a73 100644 --- a/src/test.rs +++ b/src/test.rs @@ -18,12 +18,13 @@ use actix_service::{ use awc::error::PayloadError; use awc::{Client, ClientRequest, ClientResponse, Connector}; use bytes::{Bytes, BytesMut}; -use futures::future::ok; -use futures::stream::{Stream, StreamExt}; -use net2::TcpBuilder; +use futures_core::Stream; +use futures_util::future::ok; +use futures_util::StreamExt; use serde::de::DeserializeOwned; use serde::Serialize; use serde_json; +use socket2::{Domain, Protocol, Socket, Type}; pub use actix_http::test::TestBuffer; @@ -78,6 +79,28 @@ pub fn default_service( pub async fn init_service( app: R, ) -> impl Service, Error = E> +where + R: IntoServiceFactory, + S: ServiceFactory< + Config = AppConfig, + Request = Request, + Response = ServiceResponse, + Error = E, + >, + S::InitError: std::fmt::Debug, +{ + try_init_service(app) + .await + .expect("service initilization failed") +} + +/// Fallible version of init_service that allows testing data factory errors. +pub(crate) async fn try_init_service( + app: R, +) -> Result< + impl Service, Error = E>, + S::InitError, +> where R: IntoServiceFactory, S: ServiceFactory< @@ -89,7 +112,7 @@ where S::InitError: std::fmt::Debug, { let srv = app.into_factory(); - srv.new_service(AppConfig::default()).await.unwrap() + srv.new_service(AppConfig::default()).await } /// Calls service and waits for response future completion. @@ -150,7 +173,7 @@ where pub async fn read_response(app: &mut S, req: Request) -> Bytes where S: Service, Error = Error>, - B: MessageBody, + B: MessageBody + Unpin, { let mut resp = app .call(req) @@ -187,13 +210,13 @@ where /// .to_request(); /// /// let resp = test::call_service(&mut app, req).await; -/// let result = test::read_body(resp); +/// let result = test::read_body(resp).await; /// assert_eq!(result, Bytes::from_static(b"welcome!")); /// } /// ``` pub async fn read_body(mut res: ServiceResponse) -> Bytes where - B: MessageBody, + B: MessageBody + Unpin, { let mut body = res.take_body(); let mut bytes = BytesMut::new(); @@ -203,6 +226,54 @@ where bytes.freeze() } +/// Helper function that returns a deserialized response body of a ServiceResponse. +/// +/// ```rust +/// use actix_web::{App, test, web, HttpResponse, http::header}; +/// use serde::{Serialize, Deserialize}; +/// +/// #[derive(Serialize, Deserialize)] +/// pub struct Person { +/// id: String, +/// name: String, +/// } +/// +/// #[actix_rt::test] +/// async fn test_post_person() { +/// let mut app = test::init_service( +/// App::new().service( +/// web::resource("/people") +/// .route(web::post().to(|person: web::Json| async { +/// HttpResponse::Ok() +/// .json(person.into_inner())}) +/// )) +/// ).await; +/// +/// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); +/// +/// let resp = test::TestRequest::post() +/// .uri("/people") +/// .header(header::CONTENT_TYPE, "application/json") +/// .set_payload(payload) +/// .send_request(&mut app) +/// .await; +/// +/// assert!(resp.status().is_success()); +/// +/// let result: Person = test::read_body_json(resp).await; +/// } +/// ``` +pub async fn read_body_json(res: ServiceResponse) -> T +where + B: MessageBody + Unpin, + T: DeserializeOwned, +{ + let body = read_body(res).await; + + serde_json::from_slice(&body) + .unwrap_or_else(|_| panic!("read_response_json failed during deserialization")) +} + pub async fn load_stream(mut stream: S) -> Result where S: Stream> + Unpin, @@ -251,7 +322,7 @@ where pub async fn read_response_json(app: &mut S, req: Request) -> T where S: Service, Error = Error>, - B: MessageBody, + B: MessageBody + Unpin, T: DeserializeOwned, { let body = read_response(app, req).await; @@ -527,6 +598,16 @@ impl TestRequest { (req, payload) } + + /// Complete request creation, calls service and waits for response future completion. + pub async fn send_request(self, app: &mut S) -> S::Response + where + S: Service, Error = E>, + E: std::fmt::Debug, + { + let req = self.to_request(); + call_service(app, req).await + } } /// Start test server with default configuration @@ -834,10 +915,11 @@ impl TestServerConfig { /// Get first available unused address pub fn unused_addr() -> net::SocketAddr { let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); - let socket = TcpBuilder::new_v4().unwrap(); - socket.bind(&addr).unwrap(); - socket.reuse_address(true).unwrap(); - let tcp = socket.to_tcp_listener().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() } @@ -953,7 +1035,6 @@ impl Drop for TestServer { #[cfg(test)] mod tests { use actix_http::httpmessage::HttpMessage; - use futures::FutureExt; use serde::{Deserialize, Serialize}; use std::time::SystemTime; @@ -1042,6 +1123,23 @@ mod tests { assert_eq!(result, Bytes::from_static(b"welcome!")); } + #[actix_rt::test] + async fn test_send_request() { + let mut app = + init_service(App::new().service(web::resource("/index.html").route( + web::get().to(|| async { HttpResponse::Ok().body("welcome!") }), + ))) + .await; + + let resp = TestRequest::get() + .uri("/index.html") + .send_request(&mut app) + .await; + + let result = read_body(resp).await; + assert_eq!(result, Bytes::from_static(b"welcome!")); + } + #[derive(Serialize, Deserialize)] pub struct Person { id: String, @@ -1051,8 +1149,8 @@ mod tests { #[actix_rt::test] async fn test_response_json() { let mut app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Json| { - async { HttpResponse::Ok().json(person.into_inner()) } + web::post().to(|person: web::Json| async { + HttpResponse::Ok().json(person.into_inner()) }), ))) .await; @@ -1069,11 +1167,33 @@ mod tests { assert_eq!(&result.id, "12345"); } + #[actix_rt::test] + async fn test_body_json() { + let mut app = init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| async { + HttpResponse::Ok().json(person.into_inner()) + }), + ))) + .await; + + let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); + + let resp = TestRequest::post() + .uri("/people") + .header(header::CONTENT_TYPE, "application/json") + .set_payload(payload) + .send_request(&mut app) + .await; + + let result: Person = read_body_json(resp).await; + assert_eq!(&result.name, "User name"); + } + #[actix_rt::test] async fn test_request_response_form() { let mut app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Form| { - async { HttpResponse::Ok().json(person.into_inner()) } + web::post().to(|person: web::Form| async { + HttpResponse::Ok().json(person.into_inner()) }), ))) .await; @@ -1098,8 +1218,8 @@ mod tests { #[actix_rt::test] async fn test_request_response_json() { let mut app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Json| { - async { HttpResponse::Ok().json(person.into_inner()) } + web::post().to(|person: web::Json| async { + HttpResponse::Ok().json(person.into_inner()) }), ))) .await; @@ -1163,46 +1283,53 @@ mod tests { assert!(res.status().is_success()); } - #[actix_rt::test] - async fn test_actor() { - use actix::Actor; + /* - struct MyActor; + Comment out until actix decoupled of actix-http: + https://github.com/actix/actix/issues/321 - struct Num(usize); - impl actix::Message for Num { - type Result = usize; - } - impl actix::Actor for MyActor { - type Context = actix::Context; - } - impl actix::Handler for MyActor { - type Result = usize; - fn handle(&mut self, msg: Num, _: &mut Self::Context) -> Self::Result { - msg.0 + use futures::FutureExt; + + #[actix_rt::test] + async fn test_actor() { + use actix::Actor; + + struct MyActor; + + struct Num(usize); + impl actix::Message for Num { + type Result = usize; + } + impl actix::Actor for MyActor { + type Context = actix::Context; + } + impl actix::Handler for MyActor { + type Result = usize; + fn handle(&mut self, msg: Num, _: &mut Self::Context) -> Self::Result { + msg.0 + } } - } - let addr = MyActor.start(); - let mut app = init_service(App::new().service(web::resource("/index.html").to( - move || { - addr.send(Num(1)).map(|res| match res { - Ok(res) => { - if res == 1 { - Ok(HttpResponse::Ok()) - } else { - Ok(HttpResponse::BadRequest()) + let mut app = init_service(App::new().service(web::resource("/index.html").to( + move || { + addr.send(Num(1)).map(|res| match res { + Ok(res) => { + if res == 1 { + Ok(HttpResponse::Ok()) + } else { + Ok(HttpResponse::BadRequest()) + } } - } - Err(err) => Err(err), - }) - }, - ))) - .await; + Err(err) => Err(err), + }) + }, + ))) + .await; - let req = TestRequest::post().uri("/index.html").to_request(); - let res = app.call(req).await.unwrap(); - assert!(res.status().is_success()); - } + let req = TestRequest::post().uri("/index.html").to_request(); + let res = app.call(req).await.unwrap(); + assert!(res.status().is_success()); + } + */ } diff --git a/src/types/form.rs b/src/types/form.rs index d917345e1..ca1a4b103 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -9,8 +9,8 @@ use std::{fmt, ops}; use actix_http::{Error, HttpMessage, Payload, Response}; use bytes::BytesMut; use encoding_rs::{Encoding, UTF_8}; -use futures::future::{err, ok, FutureExt, LocalBoxFuture, Ready}; -use futures::StreamExt; +use futures_util::future::{err, ok, FutureExt, LocalBoxFuture, Ready}; +use futures_util::StreamExt; use serde::de::DeserializeOwned; use serde::Serialize; diff --git a/src/types/json.rs b/src/types/json.rs index fb00bf7a6..f746fd432 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -7,11 +7,10 @@ use std::task::{Context, Poll}; use std::{fmt, ops}; use bytes::BytesMut; -use futures::future::{err, ok, FutureExt, LocalBoxFuture, Ready}; -use futures::StreamExt; +use futures_util::future::{err, ok, FutureExt, LocalBoxFuture, Ready}; +use futures_util::StreamExt; use serde::de::DeserializeOwned; use serde::Serialize; -use serde_json; use actix_http::http::{header::CONTENT_LENGTH, StatusCode}; use actix_http::{HttpMessage, Payload, Response}; @@ -208,8 +207,10 @@ where /// Json extractor configuration /// +/// # Examples +/// /// ```rust -/// use actix_web::{error, web, App, FromRequest, HttpResponse}; +/// use actix_web::{error, web, App, FromRequest, HttpRequest, HttpResponse}; /// use serde_derive::Deserialize; /// /// #[derive(Deserialize)] @@ -222,6 +223,19 @@ where /// format!("Welcome {}!", info.username) /// } /// +/// /// Return either a 400 or 415, and include the error message from serde +/// /// in the response body +/// fn json_error_handler(err: error::JsonPayloadError, _req: &HttpRequest) -> error::Error { +/// let detail = err.to_string(); +/// let response = match &err { +/// error::JsonPayloadError::ContentType => { +/// HttpResponse::UnsupportedMediaType().content_type("text/plain").body(detail) +/// } +/// _ => HttpResponse::BadRequest().content_type("text/plain").body(detail), +/// }; +/// error::InternalError::from_response(err, response).into() +/// } +/// /// fn main() { /// let app = App::new().service( /// web::resource("/index.html") @@ -232,10 +246,7 @@ where /// .content_type(|mime| { // <- accept text/plain content type /// mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN /// }) -/// .error_handler(|err, req| { // <- create custom error response -/// error::InternalError::from_response( -/// err, HttpResponse::Conflict().finish()).into() -/// }) +/// .error_handler(json_error_handler) // Use our custom error response /// })) /// .route(web::post().to(index)) /// ); diff --git a/src/types/path.rs b/src/types/path.rs index a37cb8f12..82050171c 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -4,7 +4,7 @@ use std::{fmt, ops}; use actix_http::error::{Error, ErrorNotFound}; use actix_router::PathDeserializer; -use futures::future::{ready, Ready}; +use futures_util::future::{ready, Ready}; use serde::de; use crate::dev::Payload; diff --git a/src/types/payload.rs b/src/types/payload.rs index 449e6c5b0..bad33bfc6 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -8,8 +8,9 @@ use actix_http::error::{Error, ErrorBadRequest, PayloadError}; use actix_http::HttpMessage; use bytes::{Bytes, BytesMut}; use encoding_rs::UTF_8; -use futures::future::{err, ok, Either, FutureExt, LocalBoxFuture, Ready}; -use futures::{Stream, StreamExt}; +use futures_core::stream::Stream; +use futures_util::future::{err, ok, Either, FutureExt, LocalBoxFuture, Ready}; +use futures_util::StreamExt; use mime::Mime; use crate::dev; @@ -22,9 +23,10 @@ use crate::request::HttpRequest; /// ## Example /// /// ```rust -/// use futures::{Future, Stream, StreamExt}; /// use actix_web::{web, error, App, Error, HttpResponse}; -/// +/// use std::future::Future; +/// use futures_core::stream::Stream; +/// use futures_util::StreamExt; /// /// extract binary data from request /// async fn index(mut body: web::Payload) -> Result /// { @@ -70,8 +72,10 @@ impl Stream for Payload { /// ## Example /// /// ```rust -/// use futures::{Future, Stream, StreamExt}; /// use actix_web::{web, error, App, Error, HttpResponse}; +/// use std::future::Future; +/// use futures_core::stream::Stream; +/// use futures_util::StreamExt; /// /// /// extract binary data from request /// async fn index(mut body: web::Payload) -> Result diff --git a/src/types/query.rs b/src/types/query.rs index a6c18d9be..cf1a8930d 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -4,9 +4,8 @@ use std::sync::Arc; use std::{fmt, ops}; use actix_http::error::Error; -use futures::future::{err, ok, Ready}; +use futures_util::future::{err, ok, Ready}; use serde::de; -use serde_urlencoded; use crate::dev::Payload; use crate::error::QueryPayloadError; diff --git a/src/types/readlines.rs b/src/types/readlines.rs index 82853381b..f03235377 100644 --- a/src/types/readlines.rs +++ b/src/types/readlines.rs @@ -5,7 +5,7 @@ use std::task::{Context, Poll}; use bytes::{Bytes, BytesMut}; use encoding_rs::{Encoding, UTF_8}; -use futures::Stream; +use futures_util::stream::Stream; use crate::dev::Payload; use crate::error::{PayloadError, ReadlinesError}; @@ -172,7 +172,7 @@ where #[cfg(test)] mod tests { - use futures::stream::StreamExt; + use futures_util::stream::StreamExt; use super::*; use crate::test::TestRequest; diff --git a/src/web.rs b/src/web.rs index 50d99479a..1d1174f41 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,11 +1,11 @@ //! Essentials helper functions and types for application registration. use actix_http::http::Method; use actix_router::IntoPattern; -use futures::Future; +use std::future::Future; pub use actix_http::Response as HttpResponse; pub use bytes::{Bytes, BytesMut}; -pub use futures::channel::oneshot::Canceled; +pub use futures_channel::oneshot::Canceled; use crate::error::BlockingError; use crate::extract::FromRequest; @@ -96,7 +96,7 @@ pub fn route() -> Route { /// ); /// ``` /// -/// In the above example, one `GET` route get added: +/// In the above example, one `GET` route gets added: /// * /{project_id} /// pub fn get() -> Route { @@ -114,7 +114,7 @@ pub fn get() -> Route { /// ); /// ``` /// -/// In the above example, one `POST` route get added: +/// In the above example, one `POST` route gets added: /// * /{project_id} /// pub fn post() -> Route { @@ -132,7 +132,7 @@ pub fn post() -> Route { /// ); /// ``` /// -/// In the above example, one `PUT` route get added: +/// In the above example, one `PUT` route gets added: /// * /{project_id} /// pub fn put() -> Route { @@ -150,7 +150,7 @@ pub fn put() -> Route { /// ); /// ``` /// -/// In the above example, one `PATCH` route get added: +/// In the above example, one `PATCH` route gets added: /// * /{project_id} /// pub fn patch() -> Route { @@ -168,7 +168,7 @@ pub fn patch() -> Route { /// ); /// ``` /// -/// In the above example, one `DELETE` route get added: +/// In the above example, one `DELETE` route gets added: /// * /{project_id} /// pub fn delete() -> Route { @@ -186,13 +186,31 @@ pub fn delete() -> Route { /// ); /// ``` /// -/// In the above example, one `HEAD` route get added: +/// In the above example, one `HEAD` route gets added: /// * /{project_id} /// pub fn head() -> Route { method(Method::HEAD) } +/// Create *route* with `TRACE` method guard. +/// +/// ```rust +/// use actix_web::{web, App, HttpResponse}; +/// +/// let app = App::new().service( +/// web::resource("/{project_id}") +/// .route(web::trace().to(|| HttpResponse::Ok())) +/// ); +/// ``` +/// +/// In the above example, one `HEAD` route gets added: +/// * /{project_id} +/// +pub fn trace() -> Route { + method(Method::TRACE) +} + /// Create *route* and add method guard. /// /// ```rust @@ -204,7 +222,7 @@ pub fn head() -> Route { /// ); /// ``` /// -/// In the above example, one `GET` route get added: +/// In the above example, one `GET` route gets added: /// * /{project_id} /// pub fn method(method: Method) -> Route { diff --git a/test-server/CHANGES.md b/test-server/CHANGES.md index 617b8092f..079cad74a 100644 --- a/test-server/CHANGES.md +++ b/test-server/CHANGES.md @@ -1,9 +1,14 @@ # Changes -## [Unreleased] - 2020-xx-xx - -* Update the `time` dependency to 0.2.5 +## [2.0.0-alpha.1] - 2020-05-23 +* Update the `time` dependency to 0.2.7 +* Update `actix-connect` dependency to 2.0.0-alpha.2 +* Make `test_server` `async` fn. +* Bump minimum supported Rust version to 1.40 +* Replace deprecated `net2` crate with `socket2` +* Update `base64` dependency to 0.12 +* Update `env_logger` dependency to 0.7 ## [1.0.0] - 2019-12-13 diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index b22414e29..f90cef0dd 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "1.0.0" +version = "2.0.0-alpha.1" authors = ["Nikolay Kim "] description = "Actix http test server" readme = "README.md" @@ -12,7 +12,7 @@ categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] license = "MIT/Apache-2.0" -exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] +exclude = [".gitignore", ".cargo/config"] edition = "2018" workspace = ".." @@ -32,28 +32,28 @@ openssl = ["open-ssl", "awc/openssl"] [dependencies] actix-service = "1.0.1" actix-codec = "0.2.0" -actix-connect = "1.0.0" +actix-connect = "2.0.0-alpha.2" actix-utils = "1.0.3" actix-rt = "1.0.0" actix-server = "1.0.0" actix-testing = "1.0.0" -awc = "1.0.0" +awc = "2.0.0-alpha.2" -base64 = "0.11" +base64 = "0.12" bytes = "0.5.3" -futures = "0.3.1" +futures-core = { version = "0.3.5", default-features = false } http = "0.2.0" log = "0.4" -env_logger = "0.6" -net2 = "0.2" +env_logger = "0.7" +socket2 = "0.3" serde = "1.0" serde_json = "1.0" sha1 = "0.6" slab = "0.4" serde_urlencoded = "0.6.1" -time = { version = "0.2.5", default-features = false, features = ["std"] } -open-ssl = { version="0.10", package="openssl", optional = true } +time = { version = "0.2.7", default-features = false, features = ["std"] } +open-ssl = { version = "0.10", package = "openssl", optional = true } [dev-dependencies] -actix-web = "2.0.0-rc" -actix-http = "1.0.1" +actix-web = "3.0.0-alpha.3" +actix-http = "2.0.0-alpha.4" diff --git a/test-server/README.md b/test-server/README.md index e40650124..db0791db7 100644 --- a/test-server/README.md +++ b/test-server/README.md @@ -6,4 +6,4 @@ * [API Documentation](https://docs.rs/actix-http-test/) * [Chat on gitter](https://gitter.im/actix/actix) * Cargo package: [actix-http-test](https://crates.io/crates/actix-http-test) -* Minimum supported Rust version: 1.33 or later +* Minimum supported Rust version: 1.40 or later diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 27326c67a..f6c1183b4 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -7,9 +7,9 @@ use actix_rt::{net::TcpStream, System}; use actix_server::{Server, ServiceFactory}; use awc::{error::PayloadError, ws, Client, ClientRequest, ClientResponse, Connector}; use bytes::Bytes; -use futures::Stream; +use futures_core::stream::Stream; use http::Method; -use net2::TcpBuilder; +use socket2::{Domain, Protocol, Socket, Type}; pub use actix_testing::*; @@ -43,7 +43,7 @@ pub use actix_testing::*; /// assert!(response.status().is_success()); /// } /// ``` -pub fn test_server>(factory: F) -> TestServer { +pub async fn test_server>(factory: F) -> TestServer { let (tx, rx) = mpsc::channel(); // run server in separate thread @@ -92,7 +92,7 @@ pub fn test_server>(factory: F) -> TestServer { Client::build().connector(connector).finish() }; - actix_connect::start_default_resolver(); + actix_connect::start_default_resolver().await.unwrap(); TestServer { addr, @@ -104,10 +104,11 @@ pub fn test_server>(factory: F) -> TestServer { /// Get first available unused address pub fn unused_addr() -> net::SocketAddr { let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); - let socket = TcpBuilder::new_v4().unwrap(); - socket.bind(&addr).unwrap(); - socket.reuse_address(true).unwrap(); - let tcp = socket.to_tcp_listener().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() } diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index ecd5c9ffb..750084fdc 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -1,25 +1,15 @@ -use net2::TcpBuilder; use std::sync::mpsc; -use std::{net, thread, time::Duration}; +use std::{thread, time::Duration}; #[cfg(feature = "openssl")] use open_ssl::ssl::SslAcceptorBuilder; -use actix_web::{web, App, HttpResponse, HttpServer}; - -fn unused_addr() -> net::SocketAddr { - let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); - let socket = TcpBuilder::new_v4().unwrap(); - socket.bind(&addr).unwrap(); - socket.reuse_address(true).unwrap(); - let tcp = socket.to_tcp_listener().unwrap(); - tcp.local_addr().unwrap() -} +use actix_web::{test, web, App, HttpResponse, HttpServer}; #[cfg(unix)] #[actix_rt::test] async fn test_start() { - let addr = unused_addr(); + let addr = test::unused_addr(); let (tx, rx) = mpsc::channel(); thread::spawn(move || { @@ -92,7 +82,7 @@ fn ssl_acceptor() -> std::io::Result { async fn test_start_ssl() { use actix_web::HttpRequest; - let addr = unused_addr(); + let addr = test::unused_addr(); let (tx, rx) = mpsc::channel(); thread::spawn(move || { diff --git a/tests/test_server.rs b/tests/test_server.rs index 1916b372c..0ac4b0232 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1,3 +1,4 @@ +use std::future::Future; use std::io::{Read, Write}; use std::pin::Pin; use std::task::{Context, Poll}; @@ -11,7 +12,7 @@ use bytes::Bytes; use flate2::read::GzDecoder; use flate2::write::{GzEncoder, ZlibDecoder, ZlibEncoder}; use flate2::Compression; -use futures::{ready, Future}; +use futures_util::ready; use rand::{distributions::Alphanumeric, Rng}; use actix_web::dev::BodyEncoding; @@ -56,7 +57,7 @@ impl TestBody { } } -impl futures::Stream for TestBody { +impl futures_core::stream::Stream for TestBody { type Item = Result; fn poll_next( @@ -348,9 +349,10 @@ async fn test_body_br_streaming() { #[actix_rt::test] async fn test_head_binary() { let srv = test::start_with(test::config().h1(), || { - App::new().service(web::resource("/").route( - web::head().to(move || HttpResponse::Ok().content_length(100).body(STR)), - )) + App::new().service( + web::resource("/") + .route(web::head().to(move || HttpResponse::Ok().body(STR))), + ) }); let mut response = srv.head("/").send().await.unwrap(); @@ -371,8 +373,7 @@ async fn test_no_chunking() { let srv = test::start_with(test::config().h1(), || { App::new().service(web::resource("/").route(web::to(move || { HttpResponse::Ok() - .no_chunking() - .content_length(STR.len() as u64) + .no_chunking(STR.len() as u64) .streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) }))) }); diff --git a/tests/test_weird_poll.rs b/tests/test_weird_poll.rs index 21d1d611a..7e4300901 100644 --- a/tests/test_weird_poll.rs +++ b/tests/test_weird_poll.rs @@ -1,10 +1,13 @@ // Regression test for #/1321 +/* use futures::task::{noop_waker, Context}; use futures::stream::once; use actix_http::body::{MessageBody, BodyStream}; use bytes::Bytes; +Disable weird poll until actix-web is based on actix-http 2.0.0 + #[test] fn weird_poll() { let (sender, receiver) = futures::channel::oneshot::channel(); @@ -24,3 +27,4 @@ fn weird_poll() { let _ = std::mem::replace(&mut body_stream, Err([0; 32])).unwrap().poll_next(&mut context); } +*/