diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 128f51ffd..2df863ae8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- -name: bug report -about: create a bug report +name: Bug Report +about: Create a bug report. --- Your issue may already be reported! diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..b779b33fa --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ + + + +## PR Type + + +INSERT_PR_TYPE + + +## PR Checklist +Check your PR fulfills the following: + + + +- [ ] Tests for the changes have been added / updated. +- [ ] Documentation comments have been added / updated. +- [ ] A changelog entry has been made for the appropriate packages. +- [ ] Format code with the latest stable rustfmt + + +## Overview + + + + + + diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index ce8a7da7e..d19471a16 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -1,13 +1,18 @@ name: Benchmark (Linux) -on: [push, pull_request] +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - master jobs: check_benchmark: runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 - name: Install Rust uses: actions-rs/toolchain@v1 diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml new file mode 100644 index 000000000..fb1ed7f32 --- /dev/null +++ b/.github/workflows/clippy-fmt.yml @@ -0,0 +1,32 @@ +on: + pull_request: + types: [opened, synchronize, reopened] + +name: Clippy and rustfmt Check +jobs: + clippy_check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: rustfmt + override: true + - name: Check with rustfmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + components: clippy + override: true + - name: Check with Clippy + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features --all --tests diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index ae804cc53..a068070ff 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -1,6 +1,11 @@ name: CI (Linux) -on: [push, pull_request] +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - master jobs: build_and_test: @@ -8,7 +13,7 @@ jobs: fail-fast: false matrix: version: - - 1.40.0 # MSRV + - 1.42.0 # MSRV - stable - nightly @@ -16,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 - name: Install ${{ matrix.version }} uses: actions-rs/toolchain@v1 @@ -55,7 +60,7 @@ jobs: - name: Generate coverage file if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request') run: | - cargo install cargo-tarpaulin + cargo install cargo-tarpaulin --vers "^0.13" cargo tarpaulin --out Xml - name: Upload to Codecov if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request') diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 6c360bacc..dc8558ac1 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,6 +1,11 @@ name: CI (macOS) -on: [push, pull_request] +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - master jobs: build_and_test: @@ -15,7 +20,7 @@ jobs: runs-on: macOS-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 - name: Install ${{ matrix.version }} uses: actions-rs/toolchain@v1 diff --git a/.github/workflows/upload-doc.yml b/.github/workflows/upload-doc.yml index 75d534b28..99fa663cc 100644 --- a/.github/workflows/upload-doc.yml +++ b/.github/workflows/upload-doc.yml @@ -11,7 +11,7 @@ jobs: if: github.repository == 'actix/actix-web' steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 - name: Install Rust uses: actions-rs/toolchain@v1 @@ -29,7 +29,9 @@ jobs: - 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 + - name: Deploy to GitHub Pages + uses: JamesIves/github-pages-deploy-action@3.5.8 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: gh-pages + FOLDER: target/doc diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 5fd785fad..d53d50a61 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,6 +1,11 @@ name: CI (Windows) -on: [push, pull_request] +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - master env: VCPKGRS_DYNAMIC: 1 @@ -18,7 +23,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 - name: Install ${{ matrix.version }} uses: actions-rs/toolchain@v1 diff --git a/.gitignore b/.gitignore index 42d0755dd..638a4397a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,10 @@ guide/build/ *.pid *.sock *~ +.DS_Store # These are backup files generated by rustfmt **/*.rs.bk + +# Configuration directory generated by CLion +.idea diff --git a/CHANGES.md b/CHANGES.md index 739f1b13c..5fd3869f9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,19 +1,109 @@ # Changes -## [3.0.0-alpha.3] - 2020-05-21 +## Unreleased - 2020-xx-xx + +## 3.1.0 - 2020-09-29 +### Changed +* Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allows `NormalizePath` + to retain any trailing slashes. [#1695] +* Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc` + via `web::Data::from` [#1710] + +### Fixed +* `ResourceMap` debug printing is no longer infinitely recursive. [#1708] + +[#1695]: https://github.com/actix/actix-web/pull/1695 +[#1708]: https://github.com/actix/actix-web/pull/1708 +[#1710]: https://github.com/actix/actix-web/pull/1710 + + +## 3.0.2 - 2020-09-15 +### Fixed +* `NormalizePath` when used with `TrailingSlash::Trim` no longer trims the root path "/". [#1678] + +[#1678]: https://github.com/actix/actix-web/pull/1678 + + +## 3.0.1 - 2020-09-13 +### Changed +* `middleware::normalize::TrailingSlash` enum is now accessible. [#1673] + +[#1673]: https://github.com/actix/actix-web/pull/1673 + + +## 3.0.0 - 2020-09-11 +* No significant changes from `3.0.0-beta.4`. + + +## 3.0.0-beta.4 - 2020-09-09 ### Added +* `middleware::NormalizePath` now has configurable behaviour for either always having a trailing + slash, or as the new addition, always trimming trailing slashes. [#1639] +### Changed +* Update actix-codec and actix-utils dependencies. [#1634] +* `FormConfig` and `JsonConfig` configurations are now also considered when set + using `App::data`. [#1641] +* `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. [#1655] +* `HttpServer::maxconnrate` is renamed to the more expressive + `HttpServer::max_connection_rate`. [#1655] + +[#1639]: https://github.com/actix/actix-web/pull/1639 +[#1641]: https://github.com/actix/actix-web/pull/1641 +[#1634]: https://github.com/actix/actix-web/pull/1634 +[#1655]: https://github.com/actix/actix-web/pull/1655 + +## 3.0.0-beta.3 - 2020-08-17 +### Changed +* Update `rustls` to 0.18 + + +## 3.0.0-beta.2 - 2020-08-17 +### Changed +* `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set + using `App::data`. [#1610] +* `web::Path` now has a public representation: `web::Path(pub T)` that enables + destructuring. [#1594] +* `ServiceRequest::app_data` allows retrieval of non-Data data without splitting into parts to + access `HttpRequest` which already allows this. [#1618] +* Re-export all error types from `awc`. [#1621] +* MSRV is now 1.42.0. + +### Fixed +* Memory leak of app data in pooled requests. [#1609] + +[#1594]: https://github.com/actix/actix-web/pull/1594 +[#1609]: https://github.com/actix/actix-web/pull/1609 +[#1610]: https://github.com/actix/actix-web/pull/1610 +[#1618]: https://github.com/actix/actix-web/pull/1618 +[#1621]: https://github.com/actix/actix-web/pull/1621 + + +## 3.0.0-beta.1 - 2020-07-13 +### Added +* Re-export `actix_rt::main` as `actix_web::main`. +* `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched + resource pattern. +* `HttpRequest::match_name` and `ServiceRequest::match_name` for extracting matched resource name. + +### Changed +* Fix actix_http::h1::dispatcher so it returns when HW_BUFFER_SIZE is reached. Should reduce peak memory consumption during large uploads. [#1550] +* Migrate cookie handling to `cookie` crate. Actix-web no longer requires `ring` dependency. +* MSRV is now 1.41.1 + +### Fixed +* `NormalizePath` improved consistency when path needs slashes added _and_ removed. + + +## 3.0.0-alpha.3 - 2020-05-21 +### 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 @@ -96,7 +186,7 @@ ### Deleted -* Delete HttpServer::run(), it is not useful witht async/await +* Delete HttpServer::run(), it is not useful with async/await ## [2.0.0-alpha.3] - 2019-12-07 @@ -141,7 +231,7 @@ ### Changed -* Make UrlEncodedError::Overflow more informativve +* Make UrlEncodedError::Overflow more informative * Use actix-testing for testing utils @@ -159,7 +249,7 @@ * Re-implement Host predicate (#989) -* Form immplements Responder, returning a `application/x-www-form-urlencoded` response +* Form implements Responder, returning a `application/x-www-form-urlencoded` response * Add `into_inner` to `Data` diff --git a/Cargo.toml b/Cargo.toml index 8c6d461d6..56158389c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "actix-web" -version = "3.0.0-alpha.3" +version = "3.1.0" authors = ["Nikolay Kim "] -description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." +description = "Actix web is a powerful, pragmatic, and extremely fast web framework for Rust." readme = "README.md" keywords = ["actix", "http", "web", "framework", "async"] homepage = "https://actix.rs" @@ -11,7 +11,7 @@ documentation = "https://docs.rs/actix-web/" categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" edition = "2018" [package.metadata.docs.rs] @@ -43,7 +43,7 @@ default = ["compress"] # content-encoding support compress = ["actix-http/compress", "awc/compress"] -# sessions feature, session require "ring" crate and c compiler +# sessions feature secure-cookies = ["actix-http/secure-cookies"] # openssl @@ -65,20 +65,20 @@ name = "test_server" required-features = ["compress"] [dependencies] -actix-codec = "0.2.0" -actix-service = "1.0.2" -actix-utils = "1.0.6" +actix-codec = "0.3.0" +actix-service = "1.0.6" +actix-utils = "2.0.0" actix-router = "0.2.4" -actix-rt = "1.0.0" +actix-rt = "1.1.1" actix-server = "1.0.0" actix-testing = "1.0.0" actix-macros = "0.1.0" actix-threadpool = "0.3.1" -actix-tls = "2.0.0-alpha.1" +actix-tls = "2.0.0" -actix-web-codegen = "0.2.2" -actix-http = "2.0.0-alpha.4" -awc = { version = "2.0.0-alpha.2", default-features = false } +actix-web-codegen = "0.3.0" +actix-http = "2.0.0" +awc = { version = "2.0.0", default-features = false } bytes = "0.5.3" derive_more = "0.99.2" @@ -92,17 +92,18 @@ mime = "0.3" socket2 = "0.3" pin-project = "0.4.17" regex = "1.3" -serde = { version = "1.0", features=["derive"] } +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_urlencoded = "0.6.1" 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.17.0", package = "rustls", optional = true } -tinyvec = { version = "0.3", features = ["alloc"] } +open-ssl = { package = "openssl", version = "0.10", optional = true } +rust-tls = { package = "rustls", version = "0.18.0", optional = true } +tinyvec = { version = "1", features = ["alloc"] } [dev-dependencies] -actix = "0.10.0-alpha.1" +actix = "0.10.0" +actix-http = { version = "2.0.0", features = ["actors"] } rand = "0.7" env_logger = "0.7" serde_derive = "1.0" @@ -124,6 +125,10 @@ actix-files = { path = "actix-files" } actix-multipart = { path = "actix-multipart" } awc = { path = "awc" } +[[example]] +name = "client" +required-features = ["rustls"] + [[bench]] name = "server" harness = false diff --git a/LICENSE-APACHE b/LICENSE-APACHE index 6cdf2d16c..8f5ba39b8 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2017-NOW Nikolay Kim + Copyright 2017-NOW Actix Team Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/LICENSE-MIT b/LICENSE-MIT index 0f80296ae..95938ef15 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,4 +1,4 @@ -Copyright (c) 2017 Nikolay Kim +Copyright (c) 2017 Actix Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated diff --git a/MIGRATION.md b/MIGRATION.md index d2e9735fb..5c4650194 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,17 +1,60 @@ ## Unreleased + +## 3.0.0 + +* The return type for `ServiceRequest::app_data::()` was changed from returning a `Data` to + simply a `T`. To access a `Data` use `ServiceRequest::app_data::>()`. + +* Cookie handling has been offloaded to the `cookie` crate: + * `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs. + * Some types now require lifetime parameters. + +* The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects + any `actix-web` method previously expecting a time v0.1 input. + * Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now result in `SameSite=None` being sent with the response Set-Cookie header. To create a cookie without a SameSite attribute, remove any calls setting same_site. * actix-http support for Actors messages was moved to actix-http crate and is enabled with feature `actors` + * content_length function is removed from actix-http. You can set Content-Length by normally setting the response body or calling no_chunking function. * `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a `u64` instead of a `usize`. +* Code that was using `path.` to access a `web::Path<(A, B, C)>`s elements now needs to use + destructuring or `.into_inner()`. For example: + + ```rust + // Previously: + async fn some_route(path: web::Path<(String, String)>) -> String { + format!("Hello, {} {}", path.0, path.1) + } + + // Now (this also worked before): + async fn some_route(path: web::Path<(String, String)>) -> String { + let (first_name, last_name) = path.into_inner(); + format!("Hello, {} {}", first_name, last_name) + } + // Or (this wasn't previously supported): + async fn some_route(web::Path((first_name, last_name)): web::Path<(String, String)>) -> String { + format!("Hello, {} {}", first_name, last_name) + } + ``` + +* `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one. + It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`, + or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`. + +* `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. + +* `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`. + + ## 2.0.0 * `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to diff --git a/README.md b/README.md index 97e3ceeae..3e3ce8bf1 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,45 @@
-

Actix web

-

Actix web is a small, pragmatic, and extremely fast rust web framework

+

Actix web

+

+ Actix web is a powerful, pragmatic, and extremely fast web framework for Rust +

+[![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) +[![Documentation](https://docs.rs/actix-web/badge.svg)](https://docs.rs/actix-web) +[![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html) +![License](https://img.shields.io/crates/l/actix-web.svg) +
[![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) -[![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) -[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![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.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) +[![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)

- -

- Website - | - Chat - | - Examples -

-
-Actix web is a simple, pragmatic and extremely fast web framework for Rust. +## Features -* Supported *HTTP/1.x* and *HTTP/2.0* protocols +* Supports *HTTP/1.x* and *HTTP/2* * Streaming and pipelining * Keep-alive and slow requests handling * Client/server [WebSockets](https://actix.rs/docs/websockets/) support * Transparent content compression/decompression (br, gzip, deflate) -* Configurable [request routing](https://actix.rs/docs/url-dispatch/) +* Powerful [request routing](https://actix.rs/docs/url-dispatch/) * Multipart streams * Static assets -* SSL support with OpenSSL or Rustls +* SSL support using OpenSSL or Rustls * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) -* Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) +* Includes an async [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+ +* Runs on stable Rust 1.42+ -## Docs +## Documentation -* [API documentation (master)](https://actix.rs/actix-web/actix_web) -* [API documentation (docs.rs)](https://docs.rs/actix-web) -* [User guide](https://actix.rs) +* [Website & User Guide](https://actix.rs) +* [Examples Repository](https://github.com/actix/examples) +* [API Documentation](https://docs.rs/actix-web) +* [API Documentation (master branch)](https://actix.rs/actix-web/actix_web) ## Example @@ -52,8 +47,7 @@ Dependencies: ```toml [dependencies] -actix-web = "2" -actix-rt = "1" +actix-web = "3" ``` Code: @@ -62,11 +56,11 @@ Code: use actix_web::{get, web, App, HttpServer, Responder}; #[get("/{id}/{name}/index.html")] -async fn index(info: web::Path<(u32, String)>) -> impl Responder { - format!("Hello {}! id:{}", info.1, info.0) +async fn index(web::Path((id, name)): web::Path<(u32, String)>) -> impl Responder { + format!("Hello {}! id:{}", name, id) } -#[actix_rt::main] +#[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| App::new().service(index)) .bind("127.0.0.1:8080")? @@ -77,37 +71,39 @@ async fn main() -> std::io::Result<()> { ### More examples -* [Basics](https://github.com/actix/examples/tree/master/basics/) -* [Stateful](https://github.com/actix/examples/tree/master/state/) -* [Multipart streams](https://github.com/actix/examples/tree/master/multipart/) -* [Simple websocket](https://github.com/actix/examples/tree/master/websocket/) -* [Tera](https://github.com/actix/examples/tree/master/template_tera/) -* [Askama](https://github.com/actix/examples/tree/master/template_askama/) templates -* [Diesel integration](https://github.com/actix/examples/tree/master/diesel/) -* [r2d2](https://github.com/actix/examples/tree/master/r2d2/) -* [OpenSSL](https://github.com/actix/examples/tree/master/openssl/) -* [Rustls](https://github.com/actix/examples/tree/master/rustls/) -* [Tcp/Websocket chat](https://github.com/actix/examples/tree/master/websocket-chat/) -* [Json](https://github.com/actix/examples/tree/master/json/) +* [Basic Setup](https://github.com/actix/examples/tree/master/basics/) +* [Application State](https://github.com/actix/examples/tree/master/state/) +* [JSON Handling](https://github.com/actix/examples/tree/master/json/) +* [Multipart Streams](https://github.com/actix/examples/tree/master/multipart/) +* [Diesel Integration](https://github.com/actix/examples/tree/master/diesel/) +* [r2d2 Integration](https://github.com/actix/examples/tree/master/r2d2/) +* [Simple WebSocket](https://github.com/actix/examples/tree/master/websocket/) +* [Tera Templates](https://github.com/actix/examples/tree/master/template_tera/) +* [Askama Templates](https://github.com/actix/examples/tree/master/template_askama/) +* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/rustls/) +* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/openssl/) +* [WebSocket Chat](https://github.com/actix/examples/tree/master/websocket-chat/) You may consider checking out [this directory](https://github.com/actix/examples/tree/master/) for more examples. ## Benchmarks -* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r18) +One of the fastest web frameworks available according to the +[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r19). ## License This project is licensed under either of -* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) -* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or + [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) at your option. ## Code of Conduct -Contribution to the actix-web crate is organized under the terms of the -Contributor Covenant, the maintainer of actix-web, @fafhrd91, promises to -intervene to uphold that code of conduct. +Contribution to the actix-web crate is organized under the terms of the Contributor Covenant, the +maintainers of Actix web, promises to intervene to uphold that code of conduct. diff --git a/actix-cors/README.md b/actix-cors/README.md deleted file mode 100644 index c860ec5ae..000000000 --- a/actix-cors/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# 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/) -* [API Documentation](https://docs.rs/actix-cors/) -* [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-cors](https://crates.io/crates/actix-cors) -* Minimum supported Rust version: 1.34 or later diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index abf143997..75d616ff9 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,7 +1,12 @@ # Changes -## [0.3.0-alpha.1] - 2020-05-23 +## [Unreleased] - 2020-xx-xx +## [0.3.0-beta.1] - 2020-07-15 +* Update `v_htmlescape` to 0.10 +* Update `actix-web` and `actix-http` dependencies to beta.1 + +## [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 diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 356c7a413..634296c56 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.3.0-alpha.1" +version = "0.3.0" authors = ["Nikolay Kim "] description = "Static files support for actix web." readme = "README.md" @@ -9,18 +9,16 @@ homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" documentation = "https://docs.rs/actix-files/" categories = ["asynchronous", "web-programming::http-server"] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" edition = "2018" -workspace = ".." [lib] name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "3.0.0-alpha.3", default-features = false } -actix-http = "2.0.0-alpha.4" -actix-service = "1.0.1" +actix-web = { version = "3.0.0", default-features = false } +actix-service = "1.0.6" bitflags = "1" bytes = "0.5.3" futures-core = { version = "0.3.5", default-features = false } @@ -30,8 +28,8 @@ log = "0.4" mime = "0.3" mime_guess = "2.0.1" percent-encoding = "2.1" -v_htmlescape = "0.4" +v_htmlescape = "0.10" [dev-dependencies] actix-rt = "1.0.0" -actix-web = { version = "3.0.0-alpha.3", features = ["openssl"] } +actix-web = { version = "3.0.0", features = ["openssl"] } diff --git a/actix-files/src/chunked.rs b/actix-files/src/chunked.rs new file mode 100644 index 000000000..580b06787 --- /dev/null +++ b/actix-files/src/chunked.rs @@ -0,0 +1,94 @@ +use std::{ + cmp, fmt, + fs::File, + future::Future, + io::{self, Read, Seek}, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_web::{ + error::{BlockingError, Error}, + web, +}; +use bytes::Bytes; +use futures_core::{ready, Stream}; +use futures_util::future::{FutureExt, LocalBoxFuture}; + +use crate::handle_error; + +type ChunkedBoxFuture = + LocalBoxFuture<'static, Result<(File, Bytes), BlockingError>>; + +#[doc(hidden)] +/// A helper created from a `std::fs::File` which reads the file +/// chunk-by-chunk on a `ThreadPool`. +pub struct ChunkedReadFile { + pub(crate) size: u64, + pub(crate) offset: u64, + pub(crate) file: Option, + pub(crate) fut: Option, + pub(crate) counter: u64, +} + +impl fmt::Debug for ChunkedReadFile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("ChunkedReadFile") + } +} + +impl Stream for ChunkedReadFile { + type Item = Result; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + if let Some(ref mut fut) = self.fut { + return match ready!(Pin::new(fut).poll(cx)) { + Ok((file, bytes)) => { + self.fut.take(); + self.file = Some(file); + + self.offset += bytes.len() as u64; + self.counter += bytes.len() as u64; + + Poll::Ready(Some(Ok(bytes))) + } + Err(e) => Poll::Ready(Some(Err(handle_error(e)))), + }; + } + + let size = self.size; + let offset = self.offset; + let counter = self.counter; + + if size == counter { + Poll::Ready(None) + } else { + let mut file = self.file.take().expect("Use after completion"); + + self.fut = Some( + web::block(move || { + let max_bytes = + cmp::min(size.saturating_sub(counter), 65_536) as usize; + + let mut buf = Vec::with_capacity(max_bytes); + file.seek(io::SeekFrom::Start(offset))?; + + let n_bytes = + file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; + + if n_bytes == 0 { + return Err(io::ErrorKind::UnexpectedEof.into()); + } + + Ok((file, Bytes::from(buf))) + }) + .boxed_local(), + ); + + self.poll_next(cx) + } + } +} diff --git a/actix-files/src/directory.rs b/actix-files/src/directory.rs new file mode 100644 index 000000000..3717985d3 --- /dev/null +++ b/actix-files/src/directory.rs @@ -0,0 +1,114 @@ +use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf}; + +use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse}; +use percent_encoding::{utf8_percent_encode, CONTROLS}; +use v_htmlescape::escape as escape_html_entity; + +/// A directory; responds with the generated directory listing. +#[derive(Debug)] +pub struct Directory { + /// Base directory. + pub base: PathBuf, + + /// Path of subdirectory to generate listing for. + pub path: PathBuf, +} + +impl Directory { + /// Create a new directory + pub fn new(base: PathBuf, path: PathBuf) -> Directory { + Directory { base, path } + } + + /// Is this entry visible from this directory? + pub fn is_visible(&self, entry: &io::Result) -> bool { + if let Ok(ref entry) = *entry { + if let Some(name) = entry.file_name().to_str() { + if name.starts_with('.') { + return false; + } + } + if let Ok(ref md) = entry.metadata() { + let ft = md.file_type(); + return ft.is_dir() || ft.is_file() || ft.is_symlink(); + } + } + false + } +} + +pub(crate) type DirectoryRenderer = + dyn Fn(&Directory, &HttpRequest) -> Result; + +// show file url as relative to static path +macro_rules! encode_file_url { + ($path:ident) => { + utf8_percent_encode(&$path, CONTROLS) + }; +} + +// " -- " & -- & ' -- ' < -- < > -- > / -- / +macro_rules! encode_file_name { + ($entry:ident) => { + escape_html_entity(&$entry.file_name().to_string_lossy()) + }; +} + +pub(crate) fn directory_listing( + dir: &Directory, + req: &HttpRequest, +) -> Result { + let index_of = format!("Index of {}", req.path()); + let mut body = String::new(); + let base = Path::new(req.path()); + + for entry in dir.path.read_dir()? { + if dir.is_visible(&entry) { + let entry = entry.unwrap(); + let p = match entry.path().strip_prefix(&dir.path) { + Ok(p) if cfg!(windows) => { + base.join(p).to_string_lossy().replace("\\", "/") + } + Ok(p) => base.join(p).to_string_lossy().into_owned(), + Err(_) => continue, + }; + + // if file is a directory, add '/' to the end of the name + if let Ok(metadata) = entry.metadata() { + if metadata.is_dir() { + let _ = write!( + body, + "
  • {}/
  • ", + encode_file_url!(p), + encode_file_name!(entry), + ); + } else { + let _ = write!( + body, + "
  • {}
  • ", + encode_file_url!(p), + encode_file_name!(entry), + ); + } + } else { + continue; + } + } + } + + let html = format!( + "\ + {}\ +

    {}

    \ +
      \ + {}\ +
    \n", + index_of, index_of, body + ); + Ok(ServiceResponse::new( + req.clone(), + HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(html), + )) +} diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs new file mode 100644 index 000000000..2b55e1aa9 --- /dev/null +++ b/actix-files/src/files.rs @@ -0,0 +1,250 @@ +use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc}; + +use actix_service::{boxed, IntoServiceFactory, ServiceFactory}; +use actix_web::{ + dev::{ + AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse, + }, + error::Error, + guard::Guard, + http::header::DispositionType, + HttpRequest, +}; +use futures_util::future::{ok, FutureExt, LocalBoxFuture}; + +use crate::{ + directory_listing, named, Directory, DirectoryRenderer, FilesService, + HttpNewService, MimeOverride, +}; + +/// Static files handling service. +/// +/// `Files` service must be registered with `App::service()` method. +/// +/// ```rust +/// use actix_web::App; +/// use actix_files::Files; +/// +/// let app = App::new() +/// .service(Files::new("/static", ".")); +/// ``` +pub struct Files { + path: String, + directory: PathBuf, + index: Option, + show_index: bool, + redirect_to_slash: bool, + default: Rc>>>, + renderer: Rc, + mime_override: Option>, + file_flags: named::Flags, + guards: Option>, +} + +impl fmt::Debug for Files { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Files") + } +} + +impl Clone for Files { + fn clone(&self) -> Self { + Self { + directory: self.directory.clone(), + index: self.index.clone(), + show_index: self.show_index, + redirect_to_slash: self.redirect_to_slash, + default: self.default.clone(), + renderer: self.renderer.clone(), + file_flags: self.file_flags, + path: self.path.clone(), + mime_override: self.mime_override.clone(), + guards: self.guards.clone(), + } + } +} + +impl Files { + /// Create new `Files` instance for specified base directory. + /// + /// `File` uses `ThreadPool` for blocking filesystem operations. + /// By default pool with 5x threads of available cpus is used. + /// Pool size can be changed by setting ACTIX_THREADPOOL environment variable. + pub fn new>(path: &str, dir: T) -> Files { + let orig_dir = dir.into(); + let dir = match orig_dir.canonicalize() { + Ok(canon_dir) => canon_dir, + Err(_) => { + log::error!("Specified path is not a directory: {:?}", orig_dir); + PathBuf::new() + } + }; + + Files { + path: path.to_string(), + directory: dir, + index: None, + show_index: false, + redirect_to_slash: false, + default: Rc::new(RefCell::new(None)), + renderer: Rc::new(directory_listing), + mime_override: None, + file_flags: named::Flags::default(), + guards: None, + } + } + + /// Show files listing for directories. + /// + /// By default show files listing is disabled. + pub fn show_files_listing(mut self) -> Self { + self.show_index = true; + self + } + + /// Redirects to a slash-ended path when browsing a directory. + /// + /// By default never redirect. + pub fn redirect_to_slash_directory(mut self) -> Self { + self.redirect_to_slash = true; + self + } + + /// Set custom directory renderer + pub fn files_listing_renderer(mut self, f: F) -> Self + where + for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result + + 'static, + { + self.renderer = Rc::new(f); + self + } + + /// Specifies mime override callback + pub fn mime_override(mut self, f: F) -> Self + where + F: Fn(&mime::Name<'_>) -> DispositionType + 'static, + { + self.mime_override = Some(Rc::new(f)); + self + } + + /// Set index file + /// + /// Shows specific index file for directory "/" instead of + /// showing files listing. + pub fn index_file>(mut self, index: T) -> Self { + self.index = Some(index.into()); + self + } + + #[inline] + /// Specifies whether to use ETag or not. + /// + /// Default is true. + pub fn use_etag(mut self, value: bool) -> Self { + self.file_flags.set(named::Flags::ETAG, value); + self + } + + #[inline] + /// Specifies whether to use Last-Modified or not. + /// + /// Default is true. + pub fn use_last_modified(mut self, value: bool) -> Self { + self.file_flags.set(named::Flags::LAST_MD, value); + self + } + + /// Specifies custom guards to use for directory listings and files. + /// + /// Default behaviour allows GET and HEAD. + #[inline] + pub fn use_guards(mut self, guards: G) -> Self { + self.guards = Some(Rc::new(guards)); + self + } + + /// Disable `Content-Disposition` header. + /// + /// By default Content-Disposition` header is enabled. + #[inline] + pub fn disable_content_disposition(mut self) -> Self { + self.file_flags.remove(named::Flags::CONTENT_DISPOSITION); + self + } + + /// Sets default handler which is used when no matched file could be found. + pub fn default_handler(mut self, f: F) -> Self + where + F: IntoServiceFactory, + U: ServiceFactory< + Config = (), + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, + > + 'static, + { + // create and configure default resource + self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory( + f.into_factory().map_init_err(|_| ()), + ))))); + + self + } +} + +impl HttpServiceFactory for Files { + fn register(self, config: &mut AppService) { + if self.default.borrow().is_none() { + *self.default.borrow_mut() = Some(config.default_service()); + } + + let rdef = if config.is_root() { + ResourceDef::root_prefix(&self.path) + } else { + ResourceDef::prefix(&self.path) + }; + + config.register_service(rdef, None, self, None) + } +} + +impl ServiceFactory for Files { + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Config = (); + type Service = FilesService; + type InitError = (); + type Future = LocalBoxFuture<'static, Result>; + + fn new_service(&self, _: ()) -> Self::Future { + let mut srv = FilesService { + directory: self.directory.clone(), + index: self.index.clone(), + show_index: self.show_index, + redirect_to_slash: self.redirect_to_slash, + default: None, + renderer: self.renderer.clone(), + mime_override: self.mime_override.clone(), + file_flags: self.file_flags, + guards: self.guards.clone(), + }; + + if let Some(ref default) = *self.default.borrow() { + default + .new_service(()) + .map(move |result| match result { + Ok(default) => { + srv.default = Some(default); + Ok(srv) + } + Err(_) => Err(()), + }) + .boxed_local() + } else { + ok(srv).boxed_local() + } + } +} diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 76c68ce25..1fc7cb3f3 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -1,43 +1,52 @@ -#![allow(clippy::borrow_interior_mutable_const, clippy::type_complexity)] +//! Static files support for Actix Web. +//! +//! Provides a non-blocking service for serving static files from disk. +//! +//! # Example +//! ```rust +//! use actix_web::App; +//! use actix_files::Files; +//! +//! let app = App::new() +//! .service(Files::new("/static", ".")); +//! ``` +//! +//! # Implementation Quirks +//! - If a filename contains non-ascii characters, that file will be served with the `charset=utf-8` +//! extension on the Content-Type header. -//! Static files support -use std::cell::RefCell; -use std::fmt::Write; -use std::fs::{DirEntry, File}; -use std::future::Future; -use std::io::{Read, Seek}; -use std::path::{Path, PathBuf}; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; -use std::{cmp, io}; +#![deny(rust_2018_idioms)] +#![warn(missing_docs, missing_debug_implementations)] -use actix_service::boxed::{self, BoxService, BoxServiceFactory}; -use actix_service::{IntoServiceFactory, Service, ServiceFactory}; -use actix_web::dev::{ - AppService, HttpServiceFactory, Payload, ResourceDef, ServiceRequest, - ServiceResponse, +use std::io; + +use actix_service::boxed::{BoxService, BoxServiceFactory}; +use actix_web::{ + dev::{ServiceRequest, ServiceResponse}, + error::{BlockingError, Error, ErrorInternalServerError}, + http::header::DispositionType, }; -use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; -use actix_web::guard::Guard; -use actix_web::http::header::{self, DispositionType}; -use actix_web::http::Method; -use actix_web::{web, FromRequest, HttpRequest, HttpResponse}; -use bytes::Bytes; -use futures_core::Stream; -use futures_util::future::{ok, ready, Either, FutureExt, LocalBoxFuture, Ready}; -use mime; use mime_guess::from_ext; -use percent_encoding::{utf8_percent_encode, CONTROLS}; -use v_htmlescape::escape as escape_html_entity; +mod chunked; +mod directory; mod error; +mod files; mod named; +mod path_buf; mod range; +mod service; -use self::error::{FilesError, UriSegmentError}; +pub use crate::chunked::ChunkedReadFile; +pub use crate::directory::Directory; +pub use crate::files::Files; pub use crate::named::NamedFile; pub use crate::range::HttpRange; +pub use crate::service::FilesService; + +use self::directory::{directory_listing, DirectoryRenderer}; +use self::error::FilesError; +use self::path_buf::PathBufWrap; type HttpService = BoxService; type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; @@ -50,610 +59,37 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime { from_ext(ext).first_or_octet_stream() } -fn handle_error(err: BlockingError) -> Error { +pub(crate) fn handle_error(err: BlockingError) -> Error { match err { BlockingError::Error(err) => err.into(), BlockingError::Canceled => ErrorInternalServerError("Unexpected error"), } } -#[doc(hidden)] -/// A helper created from a `std::fs::File` which reads the file -/// chunk-by-chunk on a `ThreadPool`. -pub struct ChunkedReadFile { - size: u64, - offset: u64, - file: Option, - fut: - Option>>>, - counter: u64, -} -impl Stream for ChunkedReadFile { - type Item = Result; - - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context, - ) -> Poll> { - if let Some(ref mut fut) = self.fut { - return match Pin::new(fut).poll(cx) { - Poll::Ready(Ok((file, bytes))) => { - self.fut.take(); - self.file = Some(file); - self.offset += bytes.len() as u64; - self.counter += bytes.len() as u64; - Poll::Ready(Some(Ok(bytes))) - } - Poll::Ready(Err(e)) => Poll::Ready(Some(Err(handle_error(e)))), - Poll::Pending => Poll::Pending, - }; - } - - let size = self.size; - let offset = self.offset; - let counter = self.counter; - - if size == counter { - Poll::Ready(None) - } else { - let mut file = self.file.take().expect("Use after completion"); - self.fut = Some( - web::block(move || { - let max_bytes: usize; - max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; - let mut buf = Vec::with_capacity(max_bytes); - file.seek(io::SeekFrom::Start(offset))?; - let nbytes = - file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; - if nbytes == 0 { - return Err(io::ErrorKind::UnexpectedEof.into()); - } - Ok((file, Bytes::from(buf))) - }) - .boxed_local(), - ); - self.poll_next(cx) - } - } -} - -type DirectoryRenderer = - dyn Fn(&Directory, &HttpRequest) -> Result; - -/// A directory; responds with the generated directory listing. -#[derive(Debug)] -pub struct Directory { - /// Base directory - pub base: PathBuf, - /// Path of subdirectory to generate listing for - pub path: PathBuf, -} - -impl Directory { - /// Create a new directory - pub fn new(base: PathBuf, path: PathBuf) -> Directory { - Directory { base, path } - } - - /// Is this entry visible from this directory? - pub fn is_visible(&self, entry: &io::Result) -> bool { - if let Ok(ref entry) = *entry { - if let Some(name) = entry.file_name().to_str() { - if name.starts_with('.') { - return false; - } - } - if let Ok(ref md) = entry.metadata() { - let ft = md.file_type(); - return ft.is_dir() || ft.is_file() || ft.is_symlink(); - } - } - false - } -} - -// show file url as relative to static path -macro_rules! encode_file_url { - ($path:ident) => { - utf8_percent_encode(&$path, CONTROLS) - }; -} - -// " -- " & -- & ' -- ' < -- < > -- > / -- / -macro_rules! encode_file_name { - ($entry:ident) => { - escape_html_entity(&$entry.file_name().to_string_lossy()) - }; -} - -fn directory_listing( - dir: &Directory, - req: &HttpRequest, -) -> Result { - let index_of = format!("Index of {}", req.path()); - let mut body = String::new(); - let base = Path::new(req.path()); - - for entry in dir.path.read_dir()? { - if dir.is_visible(&entry) { - let entry = entry.unwrap(); - let p = match entry.path().strip_prefix(&dir.path) { - Ok(p) if cfg!(windows) => { - base.join(p).to_string_lossy().replace("\\", "/") - } - Ok(p) => base.join(p).to_string_lossy().into_owned(), - Err(_) => continue, - }; - - // if file is a directory, add '/' to the end of the name - if let Ok(metadata) = entry.metadata() { - if metadata.is_dir() { - let _ = write!( - body, - "
  • {}/
  • ", - encode_file_url!(p), - encode_file_name!(entry), - ); - } else { - let _ = write!( - body, - "
  • {}
  • ", - encode_file_url!(p), - encode_file_name!(entry), - ); - } - } else { - continue; - } - } - } - - let html = format!( - "\ - {}\ -

    {}

    \ -
      \ - {}\ -
    \n", - index_of, index_of, body - ); - Ok(ServiceResponse::new( - req.clone(), - HttpResponse::Ok() - .content_type("text/html; charset=utf-8") - .body(html), - )) -} - -type MimeOverride = dyn Fn(&mime::Name) -> DispositionType; - -/// Static files handling -/// -/// `Files` service must be registered with `App::service()` method. -/// -/// ```rust -/// use actix_web::App; -/// use actix_files as fs; -/// -/// fn main() { -/// let app = App::new() -/// .service(fs::Files::new("/static", ".")); -/// } -/// ``` -pub struct Files { - path: String, - directory: PathBuf, - index: Option, - show_index: bool, - redirect_to_slash: bool, - default: Rc>>>, - renderer: Rc, - mime_override: Option>, - file_flags: named::Flags, - guards: Option>>, -} - -impl Clone for Files { - fn clone(&self) -> Self { - Self { - directory: self.directory.clone(), - index: self.index.clone(), - show_index: self.show_index, - redirect_to_slash: self.redirect_to_slash, - default: self.default.clone(), - renderer: self.renderer.clone(), - file_flags: self.file_flags, - path: self.path.clone(), - mime_override: self.mime_override.clone(), - guards: self.guards.clone(), - } - } -} - -impl Files { - /// Create new `Files` instance for specified base directory. - /// - /// `File` uses `ThreadPool` for blocking filesystem operations. - /// By default pool with 5x threads of available cpus is used. - /// Pool size can be changed by setting ACTIX_THREADPOOL environment variable. - pub fn new>(path: &str, dir: T) -> Files { - let orig_dir = dir.into(); - let dir = match orig_dir.canonicalize() { - Ok(canon_dir) => canon_dir, - Err(_) => { - log::error!("Specified path is not a directory: {:?}", orig_dir); - PathBuf::new() - } - }; - - Files { - path: path.to_string(), - directory: dir, - index: None, - show_index: false, - redirect_to_slash: false, - default: Rc::new(RefCell::new(None)), - renderer: Rc::new(directory_listing), - mime_override: None, - file_flags: named::Flags::default(), - guards: None, - } - } - - /// Show files listing for directories. - /// - /// By default show files listing is disabled. - pub fn show_files_listing(mut self) -> Self { - self.show_index = true; - self - } - - /// Redirects to a slash-ended path when browsing a directory. - /// - /// By default never redirect. - pub fn redirect_to_slash_directory(mut self) -> Self { - self.redirect_to_slash = true; - self - } - - /// Set custom directory renderer - pub fn files_listing_renderer(mut self, f: F) -> Self - where - for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result - + 'static, - { - self.renderer = Rc::new(f); - self - } - - /// Specifies mime override callback - pub fn mime_override(mut self, f: F) -> Self - where - F: Fn(&mime::Name) -> DispositionType + 'static, - { - self.mime_override = Some(Rc::new(f)); - self - } - - /// Set index file - /// - /// Shows specific index file for directory "/" instead of - /// showing files listing. - pub fn index_file>(mut self, index: T) -> Self { - self.index = Some(index.into()); - self - } - - #[inline] - /// Specifies whether to use ETag or not. - /// - /// Default is true. - pub fn use_etag(mut self, value: bool) -> Self { - self.file_flags.set(named::Flags::ETAG, value); - self - } - - #[inline] - /// Specifies whether to use Last-Modified or not. - /// - /// Default is true. - pub fn use_last_modified(mut self, value: bool) -> Self { - self.file_flags.set(named::Flags::LAST_MD, value); - self - } - - /// Specifies custom guards to use for directory listings and files. - /// - /// Default behaviour allows GET and HEAD. - #[inline] - pub fn use_guards(mut self, guards: G) -> Self { - self.guards = Some(Rc::new(Box::new(guards))); - self - } - - /// Disable `Content-Disposition` header. - /// - /// By default Content-Disposition` header is enabled. - #[inline] - pub fn disable_content_disposition(mut self) -> Self { - self.file_flags.remove(named::Flags::CONTENT_DISPOSITION); - self - } - - /// Sets default handler which is used when no matched file could be found. - pub fn default_handler(mut self, f: F) -> Self - where - F: IntoServiceFactory, - U: ServiceFactory< - Config = (), - Request = ServiceRequest, - Response = ServiceResponse, - Error = Error, - > + 'static, - { - // create and configure default resource - self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory( - f.into_factory().map_init_err(|_| ()), - ))))); - - self - } -} - -impl HttpServiceFactory for Files { - fn register(self, config: &mut AppService) { - if self.default.borrow().is_none() { - *self.default.borrow_mut() = Some(config.default_service()); - } - let rdef = if config.is_root() { - ResourceDef::root_prefix(&self.path) - } else { - ResourceDef::prefix(&self.path) - }; - config.register_service(rdef, None, self, None) - } -} - -impl ServiceFactory for Files { - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; - type Config = (); - type Service = FilesService; - type InitError = (); - type Future = LocalBoxFuture<'static, Result>; - - fn new_service(&self, _: ()) -> Self::Future { - let mut srv = FilesService { - directory: self.directory.clone(), - index: self.index.clone(), - show_index: self.show_index, - redirect_to_slash: self.redirect_to_slash, - default: None, - renderer: self.renderer.clone(), - mime_override: self.mime_override.clone(), - file_flags: self.file_flags, - guards: self.guards.clone(), - }; - - if let Some(ref default) = *self.default.borrow() { - default - .new_service(()) - .map(move |result| match result { - Ok(default) => { - srv.default = Some(default); - Ok(srv) - } - Err(_) => Err(()), - }) - .boxed_local() - } else { - ok(srv).boxed_local() - } - } -} - -pub struct FilesService { - directory: PathBuf, - index: Option, - show_index: bool, - redirect_to_slash: bool, - default: Option, - renderer: Rc, - mime_override: Option>, - file_flags: named::Flags, - guards: Option>>, -} - -impl FilesService { - fn handle_err( - &mut self, - e: io::Error, - req: ServiceRequest, - ) -> Either< - Ready>, - LocalBoxFuture<'static, Result>, - > { - log::debug!("Files: Failed to handle {}: {}", req.path(), e); - if let Some(ref mut default) = self.default { - Either::Right(default.call(req)) - } else { - Either::Left(ok(req.error_response(e))) - } - } -} - -impl Service for FilesService { - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; - type Future = Either< - Ready>, - LocalBoxFuture<'static, Result>, - >; - - fn poll_ready(&mut self, _: &mut Context) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: ServiceRequest) -> Self::Future { - let is_method_valid = if let Some(guard) = &self.guards { - // execute user defined guards - (**guard).check(req.head()) - } else { - // default behaviour - match *req.method() { - Method::HEAD | Method::GET => true, - _ => false, - } - }; - - if !is_method_valid { - return Either::Left(ok(req.into_response( - actix_web::HttpResponse::MethodNotAllowed() - .header(header::CONTENT_TYPE, "text/plain") - .body("Request did not meet this resource's requirements."), - ))); - } - - let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) { - Ok(item) => item, - Err(e) => return Either::Left(ok(req.error_response(e))), - }; - - // full file path - let path = match self.directory.join(&real_path.0).canonicalize() { - Ok(path) => path, - Err(e) => return self.handle_err(e, req), - }; - - if path.is_dir() { - if let Some(ref redir_index) = self.index { - if self.redirect_to_slash && !req.path().ends_with('/') { - let redirect_to = format!("{}/", req.path()); - return Either::Left(ok(req.into_response( - HttpResponse::Found() - .header(header::LOCATION, redirect_to) - .body("") - .into_body(), - ))); - } - - let path = path.join(redir_index); - - match NamedFile::open(path) { - Ok(mut named_file) => { - if let Some(ref mime_override) = self.mime_override { - let new_disposition = - mime_override(&named_file.content_type.type_()); - named_file.content_disposition.disposition = new_disposition; - } - - named_file.flags = self.file_flags; - let (req, _) = req.into_parts(); - Either::Left(ok(match named_file.into_response(&req) { - Ok(item) => ServiceResponse::new(req, item), - Err(e) => ServiceResponse::from_err(e, req), - })) - } - Err(e) => self.handle_err(e, req), - } - } else if self.show_index { - let dir = Directory::new(self.directory.clone(), path); - let (req, _) = req.into_parts(); - let x = (self.renderer)(&dir, &req); - match x { - Ok(resp) => Either::Left(ok(resp)), - Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), - } - } else { - Either::Left(ok(ServiceResponse::from_err( - FilesError::IsDirectory, - req.into_parts().0, - ))) - } - } else { - match NamedFile::open(path) { - Ok(mut named_file) => { - if let Some(ref mime_override) = self.mime_override { - let new_disposition = - mime_override(&named_file.content_type.type_()); - named_file.content_disposition.disposition = new_disposition; - } - - named_file.flags = self.file_flags; - let (req, _) = req.into_parts(); - match named_file.into_response(&req) { - Ok(item) => { - Either::Left(ok(ServiceResponse::new(req.clone(), item))) - } - Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), - } - } - Err(e) => self.handle_err(e, req), - } - } - } -} - -#[derive(Debug)] -struct PathBufWrp(PathBuf); - -impl PathBufWrp { - fn get_pathbuf(path: &str) -> Result { - let mut buf = PathBuf::new(); - for segment in path.split('/') { - if segment == ".." { - buf.pop(); - } else if segment.starts_with('.') { - return Err(UriSegmentError::BadStart('.')); - } else if segment.starts_with('*') { - return Err(UriSegmentError::BadStart('*')); - } else if segment.ends_with(':') { - return Err(UriSegmentError::BadEnd(':')); - } else if segment.ends_with('>') { - return Err(UriSegmentError::BadEnd('>')); - } else if segment.ends_with('<') { - return Err(UriSegmentError::BadEnd('<')); - } else if segment.is_empty() { - continue; - } else if cfg!(windows) && segment.contains('\\') { - return Err(UriSegmentError::BadChar('\\')); - } else { - buf.push(segment) - } - } - - Ok(PathBufWrp(buf)) - } -} - -impl FromRequest for PathBufWrp { - type Error = UriSegmentError; - type Future = Ready>; - type Config = (); - - fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - ready(PathBufWrp::get_pathbuf(req.match_info().path())) - } -} +type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType; #[cfg(test)] mod tests { - use std::fs; - use std::iter::FromIterator; - use std::ops::Add; - use std::time::{Duration, SystemTime}; + use std::{ + fs::{self, File}, + ops::Add, + time::{Duration, SystemTime}, + }; + + use actix_service::ServiceFactory; + use actix_web::{ + guard, + http::{ + header::{self, ContentDisposition, DispositionParam, DispositionType}, + Method, StatusCode, + }, + middleware::Compress, + test::{self, TestRequest}, + web, App, HttpResponse, Responder, + }; + use futures_util::future::ok; use super::*; - use actix_web::guard; - use actix_web::http::header::{ - self, ContentDisposition, DispositionParam, DispositionType, - }; - use actix_web::http::{Method, StatusCode}; - use actix_web::middleware::Compress; - use actix_web::test::{self, TestRequest}; - use actix_web::{App, Responder}; #[actix_rt::test] async fn test_file_extension_to_mime() { @@ -898,7 +334,7 @@ mod tests { #[actix_rt::test] async fn test_mime_override() { - fn all_attachment(_: &mime::Name) -> DispositionType { + fn all_attachment(_: &mime::Name<'_>) -> DispositionType { DispositionType::Attachment } @@ -952,9 +388,7 @@ mod tests { #[actix_rt::test] async fn test_named_file_content_range_headers() { - let srv = test::start(|| { - App::new().service(Files::new("/", ".")) - }); + let srv = test::start(|| App::new().service(Files::new("/", "."))); // Valid range header let response = srv @@ -979,9 +413,7 @@ mod tests { #[actix_rt::test] async fn test_named_file_content_length_headers() { - let srv = test::start(|| { - App::new().service(Files::new("/", ".")) - }); + let srv = test::start(|| App::new().service(Files::new("/", "."))); // Valid range header let response = srv @@ -1014,21 +446,15 @@ mod tests { // Check file contents let bytes = response.body().await.unwrap(); - let data = Bytes::from(fs::read("tests/test.binary").unwrap()); + let data = web::Bytes::from(fs::read("tests/test.binary").unwrap()); assert_eq!(bytes, data); } #[actix_rt::test] async fn test_head_content_length_headers() { - let srv = test::start(|| { - App::new().service(Files::new("/", ".")) - }); + let srv = test::start(|| App::new().service(Files::new("/", "."))); - let response = srv - .head("/tests/test.binary") - .send() - .await - .unwrap(); + let response = srv.head("/tests/test.binary").send().await.unwrap(); let content_length = response .headers() @@ -1053,7 +479,7 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); let bytes = test::read_body(response).await; - let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); + let data = web::Bytes::from(fs::read("tests/test space.binary").unwrap()); assert_eq!(bytes, data); } @@ -1097,12 +523,10 @@ mod tests { #[actix_rt::test] async fn test_named_file_content_encoding() { let mut srv = test::init_service(App::new().wrap(Compress::default()).service( - web::resource("/").to(|| { - async { - NamedFile::open("Cargo.toml") - .unwrap() - .set_content_encoding(header::ContentEncoding::Identity) - } + web::resource("/").to(|| async { + NamedFile::open("Cargo.toml") + .unwrap() + .set_content_encoding(header::ContentEncoding::Identity) }), )) .await; @@ -1119,12 +543,10 @@ mod tests { #[actix_rt::test] async fn test_named_file_content_encoding_gzip() { let mut srv = test::init_service(App::new().wrap(Compress::default()).service( - web::resource("/").to(|| { - async { - NamedFile::open("Cargo.toml") - .unwrap() - .set_content_encoding(header::ContentEncoding::Gzip) - } + web::resource("/").to(|| async { + NamedFile::open("Cargo.toml") + .unwrap() + .set_content_encoding(header::ContentEncoding::Gzip) }), )) .await; @@ -1235,7 +657,7 @@ mod tests { let resp = test::call_service(&mut st, req).await; assert_eq!(resp.status(), StatusCode::OK); let bytes = test::read_body(resp).await; - assert_eq!(bytes, Bytes::from_static(b"default content")); + assert_eq!(bytes, web::Bytes::from_static(b"default content")); } // #[actix_rt::test] @@ -1351,36 +773,4 @@ mod tests { // let response = srv.execute(request.send()).unwrap(); // assert_eq!(response.status(), StatusCode::OK); // } - - #[actix_rt::test] - async fn test_path_buf() { - assert_eq!( - PathBufWrp::get_pathbuf("/test/.tt").map(|t| t.0), - Err(UriSegmentError::BadStart('.')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/test/*tt").map(|t| t.0), - Err(UriSegmentError::BadStart('*')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/test/tt:").map(|t| t.0), - Err(UriSegmentError::BadEnd(':')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/test/tt<").map(|t| t.0), - Err(UriSegmentError::BadEnd('<')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/test/tt>").map(|t| t.0), - Err(UriSegmentError::BadEnd('>')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/seg1/seg2/").unwrap().0, - PathBuf::from_iter(vec!["seg1", "seg2"]) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/seg1/../seg2/").unwrap().0, - PathBuf::from_iter(vec!["seg2"]) - ); - } } diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 6ee561a4b..3caa4a809 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -7,18 +7,20 @@ use std::time::{SystemTime, UNIX_EPOCH}; #[cfg(unix)] use std::os::unix::fs::MetadataExt; -use bitflags::bitflags; -use mime; -use mime_guess::from_path; - -use actix_http::body::SizedStream; -use actix_web::dev::BodyEncoding; -use actix_web::http::header::{ - self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue, +use actix_web::{ + dev::{BodyEncoding, SizedStream}, + http::{ + header::{ + self, Charset, ContentDisposition, DispositionParam, DispositionType, + ExtendedValue, + }, + ContentEncoding, StatusCode, + }, + Error, HttpMessage, HttpRequest, HttpResponse, Responder, }; -use actix_web::http::{ContentEncoding, StatusCode}; -use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder}; +use bitflags::bitflags; use futures_util::future::{ready, Ready}; +use mime_guess::from_path; use crate::range::HttpRange; use crate::ChunkedReadFile; @@ -90,12 +92,14 @@ impl NamedFile { }; let ct = from_path(&path).first_or_octet_stream(); - let disposition_type = match ct.type_() { + let disposition = match ct.type_() { mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, _ => DispositionType::Attachment, }; + let mut parameters = vec![DispositionParam::Filename(String::from(filename.as_ref()))]; + if !filename.is_ascii() { parameters.push(DispositionParam::FilenameExt(ExtendedValue { charset: Charset::Ext(String::from("UTF-8")), @@ -103,16 +107,19 @@ impl NamedFile { value: filename.into_owned().into_bytes(), })) } + let cd = ContentDisposition { - disposition: disposition_type, - parameters: parameters, + disposition, + parameters, }; + (ct, cd) }; let md = file.metadata()?; let modified = md.modified().ok(); let encoding = None; + Ok(NamedFile { path, file, @@ -243,6 +250,7 @@ impl NamedFile { let dur = mtime .duration_since(UNIX_EPOCH) .expect("modification time must be after epoch"); + header::EntityTag::strong(format!( "{:x}:{:x}:{:x}:{:x}", ino, @@ -257,9 +265,11 @@ impl NamedFile { self.modified.map(|mtime| mtime.into()) } + /// Creates an `HttpResponse` with file as a streaming body. pub fn into_response(self, req: &HttpRequest) -> Result { if self.status_code != StatusCode::OK { let mut resp = HttpResponse::build(self.status_code); + resp.set(header::ContentType(self.content_type.clone())) .if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| { res.header( @@ -267,9 +277,11 @@ impl NamedFile { self.content_disposition.to_string(), ); }); + if let Some(current_encoding) = self.encoding { resp.encoding(current_encoding); } + let reader = ChunkedReadFile { size: self.md.len(), offset: 0, @@ -277,6 +289,7 @@ impl NamedFile { fut: None, counter: 0, }; + return Ok(resp.streaming(reader)); } @@ -285,6 +298,7 @@ impl NamedFile { } else { None }; + let last_modified = if self.flags.contains(Flags::LAST_MD) { self.last_modified() } else { @@ -299,6 +313,7 @@ impl NamedFile { { let t1: SystemTime = m.clone().into(); let t2: SystemTime = since.clone().into(); + match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { (Ok(t1), Ok(t2)) => t1 > t2, _ => false, @@ -310,13 +325,14 @@ impl NamedFile { // check last modified let not_modified = if !none_match(etag.as_ref(), req) { true - } else if req.headers().contains_key(&header::IF_NONE_MATCH) { + } else if req.headers().contains_key(header::IF_NONE_MATCH) { false } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = (last_modified, req.get_header()) { let t1: SystemTime = m.clone().into(); let t2: SystemTime = since.clone().into(); + match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { (Ok(t1), Ok(t2)) => t1 <= t2, _ => false, @@ -333,6 +349,7 @@ impl NamedFile { self.content_disposition.to_string(), ); }); + // default compressing if let Some(current_encoding) = self.encoding { resp.encoding(current_encoding); @@ -351,11 +368,12 @@ impl NamedFile { let mut offset = 0; // check for range header - if let Some(ranges) = req.headers().get(&header::RANGE) { - if let Ok(rangesheader) = ranges.to_str() { - if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) { - length = rangesvec[0].length; - offset = rangesvec[0].start; + if let Some(ranges) = req.headers().get(header::RANGE) { + if let Ok(ranges_header) = ranges.to_str() { + if let Ok(ranges) = HttpRange::parse(ranges_header, length) { + length = ranges[0].length; + offset = ranges[0].start; + resp.encoding(ContentEncoding::Identity); resp.header( header::CONTENT_RANGE, @@ -415,6 +433,7 @@ impl DerefMut for NamedFile { fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { match req.get_header::() { None | Some(header::IfMatch::Any) => true, + Some(header::IfMatch::Items(ref items)) => { if let Some(some_etag) = etag { for item in items { @@ -423,6 +442,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { } } } + false } } @@ -432,6 +452,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { match req.get_header::() { Some(header::IfNoneMatch::Any) => false, + Some(header::IfNoneMatch::Items(ref items)) => { if let Some(some_etag) = etag { for item in items { @@ -440,8 +461,10 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { } } } + true } + None => true, } } diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs new file mode 100644 index 000000000..2f3ae84d4 --- /dev/null +++ b/actix-files/src/path_buf.rs @@ -0,0 +1,99 @@ +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; + +use actix_web::{dev::Payload, FromRequest, HttpRequest}; +use futures_util::future::{ready, Ready}; + +use crate::error::UriSegmentError; + +#[derive(Debug)] +pub(crate) struct PathBufWrap(PathBuf); + +impl FromStr for PathBufWrap { + type Err = UriSegmentError; + + fn from_str(path: &str) -> Result { + let mut buf = PathBuf::new(); + + for segment in path.split('/') { + if segment == ".." { + buf.pop(); + } else if segment.starts_with('.') { + return Err(UriSegmentError::BadStart('.')); + } else if segment.starts_with('*') { + return Err(UriSegmentError::BadStart('*')); + } else if segment.ends_with(':') { + return Err(UriSegmentError::BadEnd(':')); + } else if segment.ends_with('>') { + return Err(UriSegmentError::BadEnd('>')); + } else if segment.ends_with('<') { + return Err(UriSegmentError::BadEnd('<')); + } else if segment.is_empty() { + continue; + } else if cfg!(windows) && segment.contains('\\') { + return Err(UriSegmentError::BadChar('\\')); + } else { + buf.push(segment) + } + } + + Ok(PathBufWrap(buf)) + } +} + +impl AsRef for PathBufWrap { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl FromRequest for PathBufWrap { + type Error = UriSegmentError; + type Future = Ready>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ready(req.match_info().path().parse()) + } +} + +#[cfg(test)] +mod tests { + use std::iter::FromIterator; + + use super::*; + + #[test] + fn test_path_buf() { + assert_eq!( + PathBufWrap::from_str("/test/.tt").map(|t| t.0), + Err(UriSegmentError::BadStart('.')) + ); + assert_eq!( + PathBufWrap::from_str("/test/*tt").map(|t| t.0), + Err(UriSegmentError::BadStart('*')) + ); + assert_eq!( + PathBufWrap::from_str("/test/tt:").map(|t| t.0), + Err(UriSegmentError::BadEnd(':')) + ); + assert_eq!( + PathBufWrap::from_str("/test/tt<").map(|t| t.0), + Err(UriSegmentError::BadEnd('<')) + ); + assert_eq!( + PathBufWrap::from_str("/test/tt>").map(|t| t.0), + Err(UriSegmentError::BadEnd('>')) + ); + assert_eq!( + PathBufWrap::from_str("/seg1/seg2/").unwrap().0, + PathBuf::from_iter(vec!["seg1", "seg2"]) + ); + assert_eq!( + PathBufWrap::from_str("/seg1/../seg2/").unwrap().0, + PathBuf::from_iter(vec!["seg2"]) + ); + } +} diff --git a/actix-files/src/range.rs b/actix-files/src/range.rs index 47673b0b0..e891ca7ec 100644 --- a/actix-files/src/range.rs +++ b/actix-files/src/range.rs @@ -1,11 +1,14 @@ /// HTTP Range header representation. #[derive(Debug, Clone, Copy)] pub struct HttpRange { + /// Start of range. pub start: u64, + + /// Length of range. pub length: u64, } -static PREFIX: &str = "bytes="; +const PREFIX: &str = "bytes="; const PREFIX_LEN: usize = 6; impl HttpRange { diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs new file mode 100644 index 000000000..cbf4c2d3b --- /dev/null +++ b/actix-files/src/service.rs @@ -0,0 +1,167 @@ +use std::{ + fmt, io, + path::PathBuf, + rc::Rc, + task::{Context, Poll}, +}; + +use actix_service::Service; +use actix_web::{ + dev::{ServiceRequest, ServiceResponse}, + error::Error, + guard::Guard, + http::{header, Method}, + HttpResponse, +}; +use futures_util::future::{ok, Either, LocalBoxFuture, Ready}; + +use crate::{ + named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, + NamedFile, PathBufWrap, +}; + +/// Assembled file serving service. +pub struct FilesService { + pub(crate) directory: PathBuf, + pub(crate) index: Option, + pub(crate) show_index: bool, + pub(crate) redirect_to_slash: bool, + pub(crate) default: Option, + pub(crate) renderer: Rc, + pub(crate) mime_override: Option>, + pub(crate) file_flags: named::Flags, + pub(crate) guards: Option>, +} + +type FilesServiceFuture = Either< + Ready>, + LocalBoxFuture<'static, Result>, +>; + +impl FilesService { + fn handle_err(&mut self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture { + log::debug!("Failed to handle {}: {}", req.path(), e); + + if let Some(ref mut default) = self.default { + Either::Right(default.call(req)) + } else { + Either::Left(ok(req.error_response(e))) + } + } +} + +impl fmt::Debug for FilesService { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("FilesService") + } +} + +impl Service for FilesService { + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Future = FilesServiceFuture; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: ServiceRequest) -> Self::Future { + let is_method_valid = if let Some(guard) = &self.guards { + // execute user defined guards + (**guard).check(req.head()) + } else { + // default behavior + matches!(*req.method(), Method::HEAD | Method::GET) + }; + + if !is_method_valid { + return Either::Left(ok(req.into_response( + actix_web::HttpResponse::MethodNotAllowed() + .header(header::CONTENT_TYPE, "text/plain") + .body("Request did not meet this resource's requirements."), + ))); + } + + let real_path: PathBufWrap = match req.match_info().path().parse() { + Ok(item) => item, + Err(e) => return Either::Left(ok(req.error_response(e))), + }; + + // full file path + let path = match self.directory.join(&real_path).canonicalize() { + Ok(path) => path, + Err(e) => return self.handle_err(e, req), + }; + + if path.is_dir() { + if let Some(ref redir_index) = self.index { + if self.redirect_to_slash && !req.path().ends_with('/') { + let redirect_to = format!("{}/", req.path()); + + return Either::Left(ok(req.into_response( + HttpResponse::Found() + .header(header::LOCATION, redirect_to) + .body("") + .into_body(), + ))); + } + + let path = path.join(redir_index); + + match NamedFile::open(path) { + Ok(mut named_file) => { + if let Some(ref mime_override) = self.mime_override { + let new_disposition = + mime_override(&named_file.content_type.type_()); + named_file.content_disposition.disposition = new_disposition; + } + named_file.flags = self.file_flags; + + let (req, _) = req.into_parts(); + Either::Left(ok(match named_file.into_response(&req) { + Ok(item) => ServiceResponse::new(req, item), + Err(e) => ServiceResponse::from_err(e, req), + })) + } + Err(e) => self.handle_err(e, req), + } + } else if self.show_index { + let dir = Directory::new(self.directory.clone(), path); + + let (req, _) = req.into_parts(); + let x = (self.renderer)(&dir, &req); + + match x { + Ok(resp) => Either::Left(ok(resp)), + Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), + } + } else { + Either::Left(ok(ServiceResponse::from_err( + FilesError::IsDirectory, + req.into_parts().0, + ))) + } + } else { + match NamedFile::open(path) { + Ok(mut named_file) => { + if let Some(ref mime_override) = self.mime_override { + let new_disposition = + mime_override(&named_file.content_type.type_()); + named_file.content_disposition.disposition = new_disposition; + } + named_file.flags = self.file_flags; + + let (req, _) = req.into_parts(); + match named_file.into_response(&req) { + Ok(item) => { + Either::Left(ok(ServiceResponse::new(req.clone(), item))) + } + Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), + } + } + Err(e) => self.handle_err(e, req), + } + } + } +} diff --git a/actix-framed/README.md b/actix-framed/README.md deleted file mode 100644 index a4eaadf21..000000000 --- a/actix-framed/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Framed app for actix web - -**This crate has been deprecated and removed.** diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index c19e40e4c..6a98c4ca7 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,5 +1,50 @@ # Changes +## Unreleased - 2020-xx-xx + + +## 2.0.0 - 2020-09-11 +* No significant changes from `2.0.0-beta.4`. + + +## 2.0.0-beta.4 - 2020-09-09 +### Changed +* Update actix-codec and actix-utils dependencies. +* Update actix-connect and actix-tls dependencies. + + +## [2.0.0-beta.3] - 2020-08-14 + +### Fixed +* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626] + +[#1626]: https://github.com/actix/actix-web/pull/1626 + + +## [2.0.0-beta.2] - 2020-07-21 +### Fixed +* Potential UB in h1 decoder using uninitialized memory. [#1614] + +### Changed +* Fix illegal chunked encoding. [#1615] + +[#1614]: https://github.com/actix/actix-web/pull/1614 +[#1615]: https://github.com/actix/actix-web/pull/1615 + + +## [2.0.0-beta.1] - 2020-07-11 + +### Changed + +* Migrate cookie handling to `cookie` crate. [#1558] +* Update `sha-1` to 0.9. [#1586] +* Fix leak in client pool. [#1580] +* MSRV is now 1.41.1. + +[#1558]: https://github.com/actix/actix-web/pull/1558 +[#1586]: https://github.com/actix/actix-web/pull/1586 +[#1580]: https://github.com/actix/actix-web/pull/1580 + ## [2.0.0-alpha.4] - 2020-05-21 ### Changed diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index d2ae7698e..0bbde881d 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "actix-http" -version = "2.0.0-alpha.4" +version = "2.0.0" authors = ["Nikolay Kim "] -description = "Actix http primitives" +description = "Actix HTTP primitives" readme = "README.md" keywords = ["actix", "http", "framework", "async", "futures"] homepage = "https://actix.rs" @@ -11,7 +11,7 @@ documentation = "https://docs.rs/actix-http/" categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" edition = "2018" [package.metadata.docs.rs] @@ -34,24 +34,25 @@ rustls = ["actix-tls/rustls", "actix-connect/rustls"] compress = ["flate2", "brotli2"] # support for secure cookies -secure-cookies = ["ring"] +secure-cookies = ["cookie/secure"] # support for actix Actor messages actors = ["actix"] [dependencies] -actix-service = "1.0.5" -actix-codec = "0.2.0" -actix-connect = "2.0.0-alpha.3" -actix-utils = "1.0.6" +actix-service = "1.0.6" +actix-codec = "0.3.0" +actix-connect = "2.0.0" +actix-utils = "2.0.0" actix-rt = "1.0.0" actix-threadpool = "0.3.1" -actix-tls = { version = "2.0.0-alpha.1", optional = true } -actix = { version = "0.10.0-alpha.1", optional = true } +actix-tls = { version = "2.0.0", optional = true } +actix = { version = "0.10.0", optional = true } base64 = "0.12" bitflags = "1.2" bytes = "0.5.3" +cookie = { version = "0.14.1", features = ["percent-encode"] } copyless = "0.1.4" derive_more = "0.99.2" either = "1.5.3" @@ -75,28 +76,25 @@ rand = "0.7" regex = "1.3" serde = "1.0" serde_json = "1.0" -sha-1 = "0.8" +sha-1 = "0.9" slab = "0.4" serde_urlencoded = "0.6.1" time = { version = "0.2.7", default-features = false, features = ["std"] } -# for secure cookie -ring = { version = "0.16.9", optional = true } - # compression brotli2 = { version="0.3.2", optional = true } flate2 = { version = "1.0.13", optional = true } [dev-dependencies] 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"] } +actix-connect = { version = "2.0.0", features = ["openssl"] } +actix-http-test = { version = "2.0.0", features = ["openssl"] } +actix-tls = { version = "2.0.0", features = ["openssl"] } criterion = "0.3" env_logger = "0.7" serde_derive = "1.0" open-ssl = { version="0.10", package = "openssl" } -rust-tls = { version="0.17", package = "rustls" } +rust-tls = { version="0.18", package = "rustls" } [[bench]] name = "content-length" @@ -105,3 +103,7 @@ harness = false [[bench]] name = "status-line" harness = false + +[[bench]] +name = "uninit-headers" +harness = false diff --git a/actix-http/LICENSE-APACHE b/actix-http/LICENSE-APACHE deleted file mode 100644 index 6cdf2d16c..000000000 --- a/actix-http/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-http/LICENSE-APACHE b/actix-http/LICENSE-APACHE new file mode 120000 index 000000000..965b606f3 --- /dev/null +++ b/actix-http/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/actix-http/LICENSE-MIT b/actix-http/LICENSE-MIT deleted file mode 100644 index 0f80296ae..000000000 --- a/actix-http/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-http/LICENSE-MIT b/actix-http/LICENSE-MIT new file mode 120000 index 000000000..76219eb72 --- /dev/null +++ b/actix-http/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/actix-http/README.md b/actix-http/README.md index d4c96f2a7..96fc54d2e 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -13,12 +13,11 @@ Actix http ## Example ```rust -// see examples/framed_hello.rs for complete list of used crates. use std::{env, io}; use actix_http::{HttpService, Response}; use actix_server::Server; -use futures::future; +use futures_util::future; use http::header::HeaderValue; use log::info; diff --git a/actix-http/benches/uninit-headers.rs b/actix-http/benches/uninit-headers.rs new file mode 100644 index 000000000..83e74171c --- /dev/null +++ b/actix-http/benches/uninit-headers.rs @@ -0,0 +1,137 @@ +use criterion::{criterion_group, criterion_main, Criterion}; + +use bytes::BytesMut; + +// A Miri run detects UB, seen on this playground: +// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f5d9aa166aa48df8dca05fce2b6c3915 + +fn bench_header_parsing(c: &mut Criterion) { + c.bench_function("Original (Unsound) [short]", |b| { + b.iter(|| { + let mut buf = BytesMut::from(REQ_SHORT); + _original::parse_headers(&mut buf); + }) + }); + + c.bench_function("New (safe) [short]", |b| { + b.iter(|| { + let mut buf = BytesMut::from(REQ_SHORT); + _new::parse_headers(&mut buf); + }) + }); + + c.bench_function("Original (Unsound) [realistic]", |b| { + b.iter(|| { + let mut buf = BytesMut::from(REQ); + _original::parse_headers(&mut buf); + }) + }); + + c.bench_function("New (safe) [realistic]", |b| { + b.iter(|| { + let mut buf = BytesMut::from(REQ); + _new::parse_headers(&mut buf); + }) + }); +} + +criterion_group!(benches, bench_header_parsing); +criterion_main!(benches); + +const MAX_HEADERS: usize = 96; + +const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] = + [httparse::EMPTY_HEADER; MAX_HEADERS]; + +#[derive(Clone, Copy)] +struct HeaderIndex { + name: (usize, usize), + value: (usize, usize), +} + +const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex { + name: (0, 0), + value: (0, 0), +}; + +const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = + [EMPTY_HEADER_INDEX; MAX_HEADERS]; + +impl HeaderIndex { + fn record( + bytes: &[u8], + headers: &[httparse::Header<'_>], + indices: &mut [HeaderIndex], + ) { + let bytes_ptr = bytes.as_ptr() as usize; + for (header, indices) in headers.iter().zip(indices.iter_mut()) { + let name_start = header.name.as_ptr() as usize - bytes_ptr; + let name_end = name_start + header.name.len(); + indices.name = (name_start, name_end); + let value_start = header.value.as_ptr() as usize - bytes_ptr; + let value_end = value_start + header.value.len(); + indices.value = (value_start, value_end); + } + } +} + +// test cases taken from: +// https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs + +const REQ_SHORT: &'static [u8] = b"\ +GET / HTTP/1.0\r\n\ +Host: example.com\r\n\ +Cookie: session=60; user_id=1\r\n\r\n"; + +const REQ: &'static [u8] = b"\ +GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\ +Host: www.kittyhell.com\r\n\ +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n\ +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n\ +Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n\ +Accept-Encoding: gzip,deflate\r\n\ +Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n\ +Keep-Alive: 115\r\n\ +Connection: keep-alive\r\n\ +Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral|padding=under256\r\n\r\n"; + +mod _new { + use super::*; + + pub fn parse_headers(src: &mut BytesMut) -> usize { + let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; + let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; + + let mut req = httparse::Request::new(&mut parsed); + match req.parse(src).unwrap() { + httparse::Status::Complete(_len) => { + HeaderIndex::record(src, req.headers, &mut headers); + req.headers.len() + } + _ => unreachable!(), + } + } +} + +mod _original { + use super::*; + + use std::mem::MaybeUninit; + + pub fn parse_headers(src: &mut BytesMut) -> usize { + let mut headers: [HeaderIndex; MAX_HEADERS] = + unsafe { MaybeUninit::uninit().assume_init() }; + + let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = + unsafe { MaybeUninit::uninit().assume_init() }; + + let mut req = httparse::Request::new(&mut parsed); + match req.parse(src).unwrap() { + httparse::Status::Complete(_len) => { + HeaderIndex::record(src, req.headers, &mut headers); + req.headers.len() + } + _ => unreachable!(), + } + } +} diff --git a/actix-http/src/body.rs b/actix-http/src/body.rs index 0b01aa8ce..c5d831c45 100644 --- a/actix-http/src/body.rs +++ b/actix-http/src/body.rs @@ -21,12 +21,7 @@ pub enum BodySize { impl BodySize { pub fn is_eof(&self) -> bool { - match self { - BodySize::None - | BodySize::Empty - | BodySize::Sized(0) => true, - _ => false, - } + matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0)) } } @@ -192,14 +187,8 @@ impl MessageBody for Body { impl PartialEq for Body { fn eq(&self, other: &Body) -> bool { match *self { - Body::None => match *other { - Body::None => true, - _ => false, - }, - Body::Empty => match *other { - Body::Empty => true, - _ => false, - }, + Body::None => matches!(*other, Body::None), + Body::Empty => matches!(*other, Body::Empty), Body::Bytes(ref b) => match *other { Body::Bytes(ref b2) => b == b2, _ => false, @@ -476,9 +465,9 @@ where #[cfg(test)] mod tests { use super::*; - use futures_util::stream; use futures_util::future::poll_fn; use futures_util::pin_mut; + use futures_util::stream; impl Body { pub(crate) fn get_ref(&self) -> &[u8] { @@ -612,10 +601,6 @@ mod tests { #[actix_rt::test] async fn test_body_eq() { - assert!(Body::None == Body::None); - assert!(Body::None != Body::Empty); - assert!(Body::Empty == Body::Empty); - assert!(Body::Empty != Body::None); assert!( Body::Bytes(Bytes::from_static(b"1")) == Body::Bytes(Bytes::from_static(b"1")) @@ -627,7 +612,7 @@ mod tests { async fn test_body_debug() { assert!(format!("{:?}", Body::None).contains("Body::None")); assert!(format!("{:?}", Body::Empty).contains("Body::Empty")); - assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains("1")); + assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1')); } #[actix_rt::test] @@ -729,7 +714,7 @@ mod tests { let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast"); let body = &mut resp_body.downcast_mut::().unwrap(); - body.push_str("!"); + body.push('!'); let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast!"); let not_body = resp_body.downcast_ref::<()>(); diff --git a/actix-http/src/client/connection.rs b/actix-http/src/client/connection.rs index eecf2ee6f..ec86dabb0 100644 --- a/actix-http/src/client/connection.rs +++ b/actix-http/src/client/connection.rs @@ -46,10 +46,10 @@ pub trait Connection { pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static { /// Close connection - fn close(&mut self); + fn close(self: Pin<&mut Self>); /// Release connection to the connection pool - fn release(&mut self); + fn release(self: Pin<&mut Self>); } #[doc(hidden)] @@ -195,11 +195,15 @@ where match self { EitherConnection::A(con) => con .open_tunnel(head) - .map(|res| res.map(|(head, framed)| (head, framed.map_io(EitherIo::A)))) + .map(|res| { + res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::A))) + }) .boxed_local(), EitherConnection::B(con) => con .open_tunnel(head) - .map(|res| res.map(|(head, framed)| (head, framed.map_io(EitherIo::B)))) + .map(|res| { + res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::B))) + }) .boxed_local(), } } diff --git a/actix-http/src/client/h1proto.rs b/actix-http/src/client/h1proto.rs index 51e853b3d..06cc05404 100644 --- a/actix-http/src/client/h1proto.rs +++ b/actix-http/src/client/h1proto.rs @@ -67,17 +67,17 @@ where }; // create Framed and send request - let mut framed = Framed::new(io, h1::ClientCodec::default()); - framed.send((head, body.size()).into()).await?; + let mut framed_inner = Framed::new(io, h1::ClientCodec::default()); + framed_inner.send((head, body.size()).into()).await?; // send request body match body.size() { BodySize::None | BodySize::Empty | BodySize::Sized(0) => (), - _ => send_body(body, &mut framed).await?, + _ => send_body(body, Pin::new(&mut framed_inner)).await?, }; // read response and init read body - let res = framed.into_future().await; + let res = Pin::new(&mut framed_inner).into_future().await; let (head, framed) = if let (Some(result), framed) = res { let item = result.map_err(SendRequestError::from)?; (item, framed) @@ -85,14 +85,14 @@ where return Err(SendRequestError::from(ConnectError::Disconnected)); }; - match framed.get_codec().message_type() { + match framed.codec_ref().message_type() { h1::MessageType::None => { - let force_close = !framed.get_codec().keepalive(); + let force_close = !framed.codec_ref().keepalive(); release_connection(framed, force_close); Ok((head, Payload::None)) } _ => { - let pl: PayloadStream = PlStream::new(framed).boxed_local(); + let pl: PayloadStream = PlStream::new(framed_inner).boxed_local(); Ok((head, pl.into())) } } @@ -119,35 +119,36 @@ where } /// send request body to the peer -pub(crate) async fn send_body( +pub(crate) async fn send_body( body: B, - framed: &mut Framed, + mut framed: Pin<&mut Framed>, ) -> Result<(), SendRequestError> where - I: ConnectionLifetime, + T: ConnectionLifetime + Unpin, B: MessageBody, { - let mut eof = false; pin_mut!(body); + + let mut eof = false; while !eof { - while !eof && !framed.is_write_buf_full() { + while !eof && !framed.as_ref().is_write_buf_full() { match poll_fn(|cx| body.as_mut().poll_next(cx)).await { Some(result) => { - framed.write(h1::Message::Chunk(Some(result?)))?; + framed.as_mut().write(h1::Message::Chunk(Some(result?)))?; } None => { eof = true; - framed.write(h1::Message::Chunk(None))?; + framed.as_mut().write(h1::Message::Chunk(None))?; } } } - if !framed.is_write_buf_empty() { - poll_fn(|cx| match framed.flush(cx) { + if !framed.as_ref().is_write_buf_empty() { + poll_fn(|cx| match framed.as_mut().flush(cx) { Poll::Ready(Ok(_)) => Poll::Ready(Ok(())), Poll::Ready(Err(err)) => Poll::Ready(Err(err)), Poll::Pending => { - if !framed.is_write_buf_full() { + if !framed.as_ref().is_write_buf_full() { Poll::Ready(Ok(())) } else { Poll::Pending @@ -158,13 +159,14 @@ where } } - SinkExt::flush(framed).await?; + SinkExt::flush(Pin::into_inner(framed)).await?; Ok(()) } #[doc(hidden)] /// HTTP client connection pub struct H1Connection { + /// T should be `Unpin` io: Option, created: time::Instant, pool: Option>, @@ -175,7 +177,7 @@ where T: AsyncRead + AsyncWrite + Unpin + 'static, { /// Close connection - fn close(&mut self) { + fn close(mut self: Pin<&mut Self>) { if let Some(mut pool) = self.pool.take() { if let Some(io) = self.io.take() { pool.close(IoConnection::new( @@ -188,7 +190,7 @@ where } /// Release this connection to the connection pool - fn release(&mut self) { + fn release(mut self: Pin<&mut Self>) { if let Some(mut pool) = self.pool.take() { if let Some(io) = self.io.take() { pool.release(IoConnection::new( @@ -242,14 +244,18 @@ impl AsyncWrite for H1Connection } } +#[pin_project::pin_project] pub(crate) struct PlStream { + #[pin] framed: Option>, } impl PlStream { fn new(framed: Framed) -> Self { + let framed = framed.into_map_codec(|codec| codec.into_payload_codec()); + PlStream { - framed: Some(framed.map_codec(|codec| codec.into_payload_codec())), + framed: Some(framed), } } } @@ -261,16 +267,16 @@ impl Stream for PlStream { self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { - let this = self.get_mut(); + let mut this = self.project(); - match this.framed.as_mut().unwrap().next_item(cx)? { + match this.framed.as_mut().as_pin_mut().unwrap().next_item(cx)? { Poll::Pending => Poll::Pending, Poll::Ready(Some(chunk)) => { if let Some(chunk) = chunk { Poll::Ready(Some(Ok(chunk))) } else { - let framed = this.framed.take().unwrap(); - let force_close = !framed.get_codec().keepalive(); + let framed = this.framed.as_mut().as_pin_mut().unwrap(); + let force_close = !framed.codec_ref().keepalive(); release_connection(framed, force_close); Poll::Ready(None) } @@ -280,14 +286,13 @@ impl Stream for PlStream { } } -fn release_connection(framed: Framed, force_close: bool) +fn release_connection(framed: Pin<&mut Framed>, force_close: bool) where T: ConnectionLifetime, { - let mut parts = framed.into_parts(); - if !force_close && parts.read_buf.is_empty() && parts.write_buf.is_empty() { - parts.io.release() + if !force_close && framed.is_read_buf_empty() && framed.is_write_buf_empty() { + framed.io_pin().release() } else { - parts.io.close() + framed.io_pin().close() } } diff --git a/actix-http/src/client/h2proto.rs b/actix-http/src/client/h2proto.rs index 48ab9fe4a..3f9a981f4 100644 --- a/actix-http/src/client/h2proto.rs +++ b/actix-http/src/client/h2proto.rs @@ -37,10 +37,10 @@ where trace!("Sending client request: {:?} {:?}", head, body.size()); let head_req = head.as_ref().method == Method::HEAD; let length = body.size(); - let eof = match length { - BodySize::None | BodySize::Empty | BodySize::Sized(0) => true, - _ => false, - }; + let eof = matches!( + length, + BodySize::None | BodySize::Empty | BodySize::Sized(0) + ); let mut req = Request::new(()); *req.uri_mut() = head.as_ref().uri.clone(); diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs index 5a10725b0..08abc6277 100644 --- a/actix-http/src/client/pool.rs +++ b/actix-http/src/client/pool.rs @@ -53,17 +53,23 @@ where + 'static, { pub(crate) fn new(connector: T, config: ConnectorConfig) -> Self { - ConnectionPool( - Rc::new(RefCell::new(connector)), - Rc::new(RefCell::new(Inner { - config, - acquired: 0, - waiters: Slab::new(), - waiters_queue: IndexSet::new(), - available: FxHashMap::default(), - waker: LocalWaker::new(), - })), - ) + let connector_rc = Rc::new(RefCell::new(connector)); + let inner_rc = Rc::new(RefCell::new(Inner { + config, + acquired: 0, + waiters: Slab::new(), + waiters_queue: IndexSet::new(), + available: FxHashMap::default(), + waker: LocalWaker::new(), + })); + + // start support future + actix_rt::spawn(ConnectorPoolSupport { + connector: Rc::clone(&connector_rc), + inner: Rc::clone(&inner_rc), + }); + + ConnectionPool(connector_rc, inner_rc) } } @@ -76,6 +82,13 @@ where } } +impl Drop for ConnectionPool { + fn drop(&mut self) { + // wake up the ConnectorPoolSupport when dropping so it can exit properly. + self.1.borrow().waker.wake(); + } +} + impl Service for ConnectionPool where Io: AsyncRead + AsyncWrite + Unpin + 'static, @@ -92,12 +105,6 @@ where } fn call(&mut self, req: Connect) -> Self::Future { - // start support future - actix_rt::spawn(ConnectorPoolSupport { - connector: self.0.clone(), - inner: self.1.clone(), - }); - let mut connector = self.0.clone(); let inner = self.1.clone(); @@ -112,11 +119,11 @@ where match poll_fn(|cx| Poll::Ready(inner.borrow_mut().acquire(&key, cx))).await { Acquire::Acquired(io, created) => { // use existing connection - return Ok(IoConnection::new( + Ok(IoConnection::new( io, created, Some(Acquired(key, Some(inner))), - )); + )) } Acquire::Available => { // open tcp connection @@ -435,7 +442,13 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); - let mut inner = this.inner.as_ref().borrow_mut(); + if Rc::strong_count(this.inner) == 1 { + // If we are last copy of Inner it means the ConnectionPool is already gone + // and we are safe to exit. + return Poll::Ready(()); + } + + let mut inner = this.inner.borrow_mut(); inner.waker.register(cx.waker()); // check waiters diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index abf3d8ff9..b314d4c99 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -17,7 +17,7 @@ const DATE_VALUE_LENGTH: usize = 29; pub enum KeepAlive { /// Keep alive in seconds Timeout(usize), - /// Relay on OS to shutdown tcp connection + /// Rely on OS to shutdown tcp connection Os, /// Disabled Disabled, @@ -209,6 +209,7 @@ impl Date { date.update(); date } + fn update(&mut self) { self.pos = 0; write!( diff --git a/actix-http/src/cookie/builder.rs b/actix-http/src/cookie/builder.rs deleted file mode 100644 index b64352e35..000000000 --- a/actix-http/src/cookie/builder.rs +++ /dev/null @@ -1,252 +0,0 @@ -use std::borrow::Cow; - -use time::{Duration, OffsetDateTime}; - -use super::{Cookie, SameSite}; - -/// Structure that follows the builder pattern for building `Cookie` structs. -/// -/// To construct a cookie: -/// -/// 1. Call [`Cookie::build`](struct.Cookie.html#method.build) to start building. -/// 2. Use any of the builder methods to set fields in the cookie. -/// 3. Call [finish](#method.finish) to retrieve the built cookie. -/// -/// # Example -/// -/// ```rust -/// use actix_http::cookie::Cookie; -/// -/// let cookie: Cookie = Cookie::build("name", "value") -/// .domain("www.rust-lang.org") -/// .path("/") -/// .secure(true) -/// .http_only(true) -/// .max_age(84600) -/// .finish(); -/// ``` -#[derive(Debug, Clone)] -pub struct CookieBuilder { - /// The cookie being built. - cookie: Cookie<'static>, -} - -impl CookieBuilder { - /// Creates a new `CookieBuilder` instance from the given name and value. - /// - /// This method is typically called indirectly via - /// [Cookie::build](struct.Cookie.html#method.build). - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar").finish(); - /// assert_eq!(c.name_value(), ("foo", "bar")); - /// ``` - pub fn new(name: N, value: V) -> CookieBuilder - where - N: Into>, - V: Into>, - { - CookieBuilder { - cookie: Cookie::new(name, value), - } - } - - /// Sets the `expires` field in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .expires(time::OffsetDateTime::now_utc()) - /// .finish(); - /// - /// assert!(c.expires().is_some()); - /// ``` - #[inline] - pub fn expires(mut self, when: OffsetDateTime) -> CookieBuilder { - self.cookie.set_expires(when); - self - } - - /// Sets the `max_age` field in seconds in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .max_age(1800) - /// .finish(); - /// - /// assert_eq!(c.max_age(), Some(time::Duration::seconds(30 * 60))); - /// ``` - #[inline] - pub fn max_age(self, seconds: i64) -> CookieBuilder { - self.max_age_time(Duration::seconds(seconds)) - } - - /// Sets the `max_age` field in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .max_age_time(time::Duration::minutes(30)) - /// .finish(); - /// - /// assert_eq!(c.max_age(), Some(time::Duration::seconds(30 * 60))); - /// ``` - #[inline] - 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 - } - - /// Sets the `domain` field in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .domain("www.rust-lang.org") - /// .finish(); - /// - /// assert_eq!(c.domain(), Some("www.rust-lang.org")); - /// ``` - pub fn domain>>(mut self, value: D) -> CookieBuilder { - self.cookie.set_domain(value); - self - } - - /// Sets the `path` field in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .path("/") - /// .finish(); - /// - /// assert_eq!(c.path(), Some("/")); - /// ``` - pub fn path>>(mut self, path: P) -> CookieBuilder { - self.cookie.set_path(path); - self - } - - /// Sets the `secure` field in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .secure(true) - /// .finish(); - /// - /// assert_eq!(c.secure(), Some(true)); - /// ``` - #[inline] - pub fn secure(mut self, value: bool) -> CookieBuilder { - self.cookie.set_secure(value); - self - } - - /// Sets the `http_only` field in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .http_only(true) - /// .finish(); - /// - /// assert_eq!(c.http_only(), Some(true)); - /// ``` - #[inline] - pub fn http_only(mut self, value: bool) -> CookieBuilder { - self.cookie.set_http_only(value); - self - } - - /// Sets the `same_site` field in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{Cookie, SameSite}; - /// - /// let c = Cookie::build("foo", "bar") - /// .same_site(SameSite::Strict) - /// .finish(); - /// - /// assert_eq!(c.same_site(), Some(SameSite::Strict)); - /// ``` - #[inline] - pub fn same_site(mut self, value: SameSite) -> CookieBuilder { - self.cookie.set_same_site(value); - self - } - - /// Makes the cookie being built 'permanent' by extending its expiration and - /// max age 20 years into the future. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// use time::Duration; - /// - /// let c = Cookie::build("foo", "bar") - /// .permanent() - /// .finish(); - /// - /// assert_eq!(c.max_age(), Some(Duration::days(365 * 20))); - /// # assert!(c.expires().is_some()); - /// ``` - #[inline] - pub fn permanent(mut self) -> CookieBuilder { - self.cookie.make_permanent(); - self - } - - /// Finishes building and returns the built `Cookie`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .domain("crates.io") - /// .path("/") - /// .finish(); - /// - /// assert_eq!(c.name_value(), ("foo", "bar")); - /// assert_eq!(c.domain(), Some("crates.io")); - /// assert_eq!(c.path(), Some("/")); - /// ``` - #[inline] - pub fn finish(self) -> Cookie<'static> { - self.cookie - } -} diff --git a/actix-http/src/cookie/delta.rs b/actix-http/src/cookie/delta.rs deleted file mode 100644 index a001a5bb8..000000000 --- a/actix-http/src/cookie/delta.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::borrow::Borrow; -use std::hash::{Hash, Hasher}; -use std::ops::{Deref, DerefMut}; - -use super::Cookie; - -/// A `DeltaCookie` is a helper structure used in a cookie jar. It wraps a -/// `Cookie` so that it can be hashed and compared purely by name. It further -/// records whether the wrapped cookie is a "removal" cookie, that is, a cookie -/// that when sent to the client removes the named cookie on the client's -/// machine. -#[derive(Clone, Debug)] -pub struct DeltaCookie { - pub cookie: Cookie<'static>, - pub removed: bool, -} - -impl DeltaCookie { - /// Create a new `DeltaCookie` that is being added to a jar. - #[inline] - pub fn added(cookie: Cookie<'static>) -> DeltaCookie { - DeltaCookie { - cookie, - removed: false, - } - } - - /// Create a new `DeltaCookie` that is being removed from a jar. The - /// `cookie` should be a "removal" cookie. - #[inline] - pub fn removed(cookie: Cookie<'static>) -> DeltaCookie { - DeltaCookie { - cookie, - removed: true, - } - } -} - -impl Deref for DeltaCookie { - type Target = Cookie<'static>; - - fn deref(&self) -> &Cookie<'static> { - &self.cookie - } -} - -impl DerefMut for DeltaCookie { - fn deref_mut(&mut self) -> &mut Cookie<'static> { - &mut self.cookie - } -} - -impl PartialEq for DeltaCookie { - fn eq(&self, other: &DeltaCookie) -> bool { - self.name() == other.name() - } -} - -impl Eq for DeltaCookie {} - -impl Hash for DeltaCookie { - fn hash(&self, state: &mut H) { - self.name().hash(state); - } -} - -impl Borrow for DeltaCookie { - fn borrow(&self) -> &str { - self.name() - } -} diff --git a/actix-http/src/cookie/draft.rs b/actix-http/src/cookie/draft.rs deleted file mode 100644 index a6525a605..000000000 --- a/actix-http/src/cookie/draft.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! This module contains types that represent cookie properties that are not yet -//! standardized. That is, _draft_ features. - -use std::fmt; - -/// The `SameSite` cookie attribute. -/// -/// A cookie with a `SameSite` attribute is imposed restrictions on when it is -/// sent to the origin server in a cross-site request. If the `SameSite` -/// attribute is "Strict", then the cookie is never sent in cross-site requests. -/// If the `SameSite` attribute is "Lax", the cookie is only sent in cross-site -/// requests with "safe" HTTP methods, i.e, `GET`, `HEAD`, `OPTIONS`, `TRACE`. -/// If the `SameSite` attribute is not present then the cookie will be sent as -/// normal. In some browsers, this will implicitly handle the cookie as if "Lax" -/// and in others, "None". It's best to explicitly set the `SameSite` attribute -/// to avoid inconsistent behavior. -/// -/// **Note:** Depending on browser, the `Secure` attribute may be required for -/// `SameSite` "None" cookies to be accepted. -/// -/// **Note:** This cookie attribute is an HTTP draft! Its meaning and definition -/// are subject to change. -/// -/// More info about these draft changes can be found in the draft spec: -/// - https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum SameSite { - /// The "Strict" `SameSite` attribute. - Strict, - /// The "Lax" `SameSite` attribute. - Lax, - /// The "None" `SameSite` attribute. - None, -} - -impl SameSite { - /// Returns `true` if `self` is `SameSite::Strict` and `false` otherwise. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::SameSite; - /// - /// let strict = SameSite::Strict; - /// assert!(strict.is_strict()); - /// assert!(!strict.is_lax()); - /// assert!(!strict.is_none()); - /// ``` - #[inline] - pub fn is_strict(self) -> bool { - match self { - SameSite::Strict => true, - SameSite::Lax | SameSite::None => false, - } - } - - /// Returns `true` if `self` is `SameSite::Lax` and `false` otherwise. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::SameSite; - /// - /// let lax = SameSite::Lax; - /// assert!(lax.is_lax()); - /// assert!(!lax.is_strict()); - /// assert!(!lax.is_none()); - /// ``` - #[inline] - pub fn is_lax(self) -> bool { - match self { - SameSite::Lax => true, - SameSite::Strict | SameSite::None => false, - } - } - - /// Returns `true` if `self` is `SameSite::None` and `false` otherwise. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::SameSite; - /// - /// let none = SameSite::None; - /// assert!(none.is_none()); - /// assert!(!none.is_lax()); - /// assert!(!none.is_strict()); - /// ``` - #[inline] - pub fn is_none(self) -> bool { - match self { - SameSite::None => true, - SameSite::Lax | SameSite::Strict => false, - } - } -} - -impl fmt::Display for SameSite { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - SameSite::Strict => write!(f, "Strict"), - SameSite::Lax => write!(f, "Lax"), - SameSite::None => write!(f, "None"), - } - } -} diff --git a/actix-http/src/cookie/jar.rs b/actix-http/src/cookie/jar.rs deleted file mode 100644 index fbefa1bbf..000000000 --- a/actix-http/src/cookie/jar.rs +++ /dev/null @@ -1,651 +0,0 @@ -use std::collections::HashSet; -use std::mem; - -use time::{Duration, OffsetDateTime}; - -use super::delta::DeltaCookie; -use super::Cookie; - -#[cfg(feature = "secure-cookies")] -use super::secure::{Key, PrivateJar, SignedJar}; - -/// A collection of cookies that tracks its modifications. -/// -/// 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 iterator over the changes. -/// -/// # Usage -/// -/// A jar's life begins via [new](#method.new) and calls to -/// [`add_original`](#method.add_original): -/// -/// ```rust -/// use actix_http::cookie::{Cookie, CookieJar}; -/// -/// let mut jar = CookieJar::new(); -/// jar.add_original(Cookie::new("name", "value")); -/// jar.add_original(Cookie::new("second", "another")); -/// ``` -/// -/// Cookies can be added via [add](#method.add) and removed via -/// [remove](#method.remove). Finally, cookies can be looked up via -/// [get](#method.get): -/// -/// ```rust -/// # use actix_http::cookie::{Cookie, CookieJar}; -/// let mut jar = CookieJar::new(); -/// jar.add(Cookie::new("a", "one")); -/// jar.add(Cookie::new("b", "two")); -/// -/// assert_eq!(jar.get("a").map(|c| c.value()), Some("one")); -/// assert_eq!(jar.get("b").map(|c| c.value()), Some("two")); -/// -/// jar.remove(Cookie::named("b")); -/// assert!(jar.get("b").is_none()); -/// ``` -/// -/// # Deltas -/// -/// A jar keeps track of any modifications made to it over time. The -/// modifications are recorded as cookies. The modifications can be retrieved -/// via [delta](#method.delta). Any new `Cookie` added to a jar via `add` -/// results in the same `Cookie` appearing in the `delta`; cookies added via -/// `add_original` do not count towards the delta. Any _original_ cookie that is -/// removed from a jar results in a "removal" cookie appearing in the delta. A -/// "removal" cookie is a cookie that a server sends so that the cookie is -/// removed from the client's machine. -/// -/// Deltas are typically used to create `Set-Cookie` headers corresponding to -/// the changes made to a cookie jar over a period of time. -/// -/// ```rust -/// # use actix_http::cookie::{Cookie, CookieJar}; -/// let mut jar = CookieJar::new(); -/// -/// // original cookies don't affect the delta -/// jar.add_original(Cookie::new("original", "value")); -/// assert_eq!(jar.delta().count(), 0); -/// -/// // new cookies result in an equivalent `Cookie` in the delta -/// jar.add(Cookie::new("a", "one")); -/// jar.add(Cookie::new("b", "two")); -/// assert_eq!(jar.delta().count(), 2); -/// -/// // removing an original cookie adds a "removal" cookie to the delta -/// jar.remove(Cookie::named("original")); -/// assert_eq!(jar.delta().count(), 3); -/// -/// // removing a new cookie that was added removes that `Cookie` from the delta -/// jar.remove(Cookie::named("a")); -/// assert_eq!(jar.delta().count(), 2); -/// ``` -#[derive(Default, Debug, Clone)] -pub struct CookieJar { - original_cookies: HashSet, - delta_cookies: HashSet, -} - -impl CookieJar { - /// Creates an empty cookie jar. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::CookieJar; - /// - /// let jar = CookieJar::new(); - /// assert_eq!(jar.iter().count(), 0); - /// ``` - pub fn new() -> CookieJar { - CookieJar::default() - } - - /// Returns a reference to the `Cookie` inside this jar with the name - /// `name`. If no such cookie exists, returns `None`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// - /// let mut jar = CookieJar::new(); - /// assert!(jar.get("name").is_none()); - /// - /// jar.add(Cookie::new("name", "value")); - /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value")); - /// ``` - pub fn get(&self, name: &str) -> Option<&Cookie<'static>> { - self.delta_cookies - .get(name) - .or_else(|| self.original_cookies.get(name)) - .and_then(|c| if !c.removed { Some(&c.cookie) } else { None }) - } - - /// Adds an "original" `cookie` to this jar. If an original cookie with the - /// same name already exists, it is replaced with `cookie`. Cookies added - /// with `add` take precedence and are not replaced by this method. - /// - /// Adding an original cookie does not affect the [delta](#method.delta) - /// computation. This method is intended to be used to seed the cookie jar - /// with cookies received from a client's HTTP message. - /// - /// For accurate `delta` computations, this method should not be called - /// after calling `remove`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// - /// let mut jar = CookieJar::new(); - /// jar.add_original(Cookie::new("name", "value")); - /// jar.add_original(Cookie::new("second", "two")); - /// - /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value")); - /// assert_eq!(jar.get("second").map(|c| c.value()), Some("two")); - /// assert_eq!(jar.iter().count(), 2); - /// assert_eq!(jar.delta().count(), 0); - /// ``` - pub fn add_original(&mut self, cookie: Cookie<'static>) { - self.original_cookies.replace(DeltaCookie::added(cookie)); - } - - /// Adds `cookie` to this jar. If a cookie with the same name already - /// exists, it is replaced with `cookie`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// - /// let mut jar = CookieJar::new(); - /// jar.add(Cookie::new("name", "value")); - /// jar.add(Cookie::new("second", "two")); - /// - /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value")); - /// assert_eq!(jar.get("second").map(|c| c.value()), Some("two")); - /// assert_eq!(jar.iter().count(), 2); - /// assert_eq!(jar.delta().count(), 2); - /// ``` - pub fn add(&mut self, cookie: Cookie<'static>) { - self.delta_cookies.replace(DeltaCookie::added(cookie)); - } - - /// Removes `cookie` from this jar. If an _original_ cookie with the same - /// name as `cookie` is present in the jar, a _removal_ cookie will be - /// present in the `delta` computation. To properly generate the removal - /// cookie, `cookie` must contain the same `path` and `domain` as the cookie - /// that was initially set. - /// - /// A "removal" cookie is a cookie that has the same name as the original - /// cookie but has an empty value, a max-age of 0, and an expiration date - /// far in the past. - /// - /// # Example - /// - /// Removing an _original_ cookie results in a _removal_ cookie: - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// use time::Duration; - /// - /// let mut jar = CookieJar::new(); - /// - /// // Assume this cookie originally had a path of "/" and domain of "a.b". - /// jar.add_original(Cookie::new("name", "value")); - /// - /// // If the path and domain were set, they must be provided to `remove`. - /// jar.remove(Cookie::build("name", "").path("/").domain("a.b").finish()); - /// - /// // The delta will contain the removal cookie. - /// let delta: Vec<_> = jar.delta().collect(); - /// assert_eq!(delta.len(), 1); - /// assert_eq!(delta[0].name(), "name"); - /// assert_eq!(delta[0].max_age(), Some(Duration::zero())); - /// ``` - /// - /// Removing a new cookie does not result in a _removal_ cookie: - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// - /// let mut jar = CookieJar::new(); - /// jar.add(Cookie::new("name", "value")); - /// assert_eq!(jar.delta().count(), 1); - /// - /// jar.remove(Cookie::named("name")); - /// assert_eq!(jar.delta().count(), 0); - /// ``` - pub fn remove(&mut self, mut cookie: Cookie<'static>) { - if self.original_cookies.contains(cookie.name()) { - cookie.set_value(""); - cookie.set_max_age(Duration::zero()); - cookie.set_expires(OffsetDateTime::now_utc() - Duration::days(365)); - self.delta_cookies.replace(DeltaCookie::removed(cookie)); - } else { - self.delta_cookies.remove(cookie.name()); - } - } - - /// Removes `cookie` from this jar completely. This method differs from - /// `remove` in that no delta cookie is created under any condition. Neither - /// the `delta` nor `iter` methods will return a cookie that is removed - /// using this method. - /// - /// # Example - /// - /// Removing an _original_ cookie; no _removal_ cookie is generated: - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// use time::Duration; - /// - /// let mut jar = CookieJar::new(); - /// - /// // Add an original cookie and a new cookie. - /// jar.add_original(Cookie::new("name", "value")); - /// jar.add(Cookie::new("key", "value")); - /// assert_eq!(jar.delta().count(), 1); - /// assert_eq!(jar.iter().count(), 2); - /// - /// // Now force remove the original cookie. - /// jar.force_remove(Cookie::new("name", "value")); - /// assert_eq!(jar.delta().count(), 1); - /// assert_eq!(jar.iter().count(), 1); - /// - /// // Now force remove the new cookie. - /// jar.force_remove(Cookie::new("key", "value")); - /// assert_eq!(jar.delta().count(), 0); - /// assert_eq!(jar.iter().count(), 0); - /// ``` - pub fn force_remove<'a>(&mut self, cookie: Cookie<'a>) { - self.original_cookies.remove(cookie.name()); - self.delta_cookies.remove(cookie.name()); - } - - /// Removes all cookies from this cookie jar. - #[deprecated( - since = "0.7.0", - note = "calling this method may not remove \ - all cookies since the path and domain are not specified; use \ - `remove` instead" - )] - pub fn clear(&mut self) { - self.delta_cookies.clear(); - for delta in mem::take(&mut self.original_cookies) { - self.remove(delta.cookie); - } - } - - /// Returns an iterator over cookies that represent the changes to this jar - /// over time. These cookies can be rendered directly as `Set-Cookie` header - /// values to affect the changes made to this jar on the client. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// - /// let mut jar = CookieJar::new(); - /// jar.add_original(Cookie::new("name", "value")); - /// jar.add_original(Cookie::new("second", "two")); - /// - /// // Add new cookies. - /// jar.add(Cookie::new("new", "third")); - /// jar.add(Cookie::new("another", "fourth")); - /// jar.add(Cookie::new("yac", "fifth")); - /// - /// // Remove some cookies. - /// jar.remove(Cookie::named("name")); - /// jar.remove(Cookie::named("another")); - /// - /// // Delta contains two new cookies ("new", "yac") and a removal ("name"). - /// assert_eq!(jar.delta().count(), 3); - /// ``` - pub fn delta(&self) -> Delta<'_> { - Delta { - iter: self.delta_cookies.iter(), - } - } - - /// Returns an iterator over all of the cookies present in this jar. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// - /// let mut jar = CookieJar::new(); - /// - /// jar.add_original(Cookie::new("name", "value")); - /// jar.add_original(Cookie::new("second", "two")); - /// - /// jar.add(Cookie::new("new", "third")); - /// jar.add(Cookie::new("another", "fourth")); - /// jar.add(Cookie::new("yac", "fifth")); - /// - /// jar.remove(Cookie::named("name")); - /// jar.remove(Cookie::named("another")); - /// - /// // There are three cookies in the jar: "second", "new", and "yac". - /// # assert_eq!(jar.iter().count(), 3); - /// for cookie in jar.iter() { - /// match cookie.name() { - /// "second" => assert_eq!(cookie.value(), "two"), - /// "new" => assert_eq!(cookie.value(), "third"), - /// "yac" => assert_eq!(cookie.value(), "fifth"), - /// _ => unreachable!("there are only three cookies in the jar") - /// } - /// } - /// ``` - pub fn iter(&self) -> Iter<'_> { - Iter { - delta_cookies: self - .delta_cookies - .iter() - .chain(self.original_cookies.difference(&self.delta_cookies)), - } - } - - /// Returns a `PrivateJar` with `self` as its parent jar using the key `key` - /// to sign/encrypt and verify/decrypt cookies added/retrieved from the - /// child jar. - /// - /// Any modifications to the child jar will be reflected on the parent jar, - /// and any retrievals from the child jar will be made from the parent jar. - /// - /// This method is only available when the `secure` feature is enabled. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{Cookie, CookieJar, Key}; - /// - /// // Generate a secure key. - /// let key = Key::generate(); - /// - /// // Add a private (signed + encrypted) cookie. - /// let mut jar = CookieJar::new(); - /// jar.private(&key).add(Cookie::new("private", "text")); - /// - /// // The cookie's contents are encrypted. - /// assert_ne!(jar.get("private").unwrap().value(), "text"); - /// - /// // They can be decrypted and verified through the child jar. - /// assert_eq!(jar.private(&key).get("private").unwrap().value(), "text"); - /// - /// // A tampered with cookie does not validate but still exists. - /// let mut cookie = jar.get("private").unwrap().clone(); - /// jar.add(Cookie::new("private", cookie.value().to_string() + "!")); - /// assert!(jar.private(&key).get("private").is_none()); - /// assert!(jar.get("private").is_some()); - /// ``` - #[cfg(feature = "secure-cookies")] - pub fn private(&mut self, key: &Key) -> PrivateJar<'_> { - PrivateJar::new(self, key) - } - - /// Returns a `SignedJar` with `self` as its parent jar using the key `key` - /// to sign/verify cookies added/retrieved from the child jar. - /// - /// Any modifications to the child jar will be reflected on the parent jar, - /// and any retrievals from the child jar will be made from the parent jar. - /// - /// This method is only available when the `secure` feature is enabled. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{Cookie, CookieJar, Key}; - /// - /// // Generate a secure key. - /// let key = Key::generate(); - /// - /// // Add a signed cookie. - /// let mut jar = CookieJar::new(); - /// jar.signed(&key).add(Cookie::new("signed", "text")); - /// - /// // The cookie's contents are signed but still in plaintext. - /// assert_ne!(jar.get("signed").unwrap().value(), "text"); - /// assert!(jar.get("signed").unwrap().value().contains("text")); - /// - /// // They can be verified through the child jar. - /// assert_eq!(jar.signed(&key).get("signed").unwrap().value(), "text"); - /// - /// // A tampered with cookie does not validate but still exists. - /// let mut cookie = jar.get("signed").unwrap().clone(); - /// jar.add(Cookie::new("signed", cookie.value().to_string() + "!")); - /// assert!(jar.signed(&key).get("signed").is_none()); - /// assert!(jar.get("signed").is_some()); - /// ``` - #[cfg(feature = "secure-cookies")] - pub fn signed(&mut self, key: &Key) -> SignedJar<'_> { - SignedJar::new(self, key) - } -} - -use std::collections::hash_set::Iter as HashSetIter; - -/// Iterator over the changes to a cookie jar. -pub struct Delta<'a> { - iter: HashSetIter<'a, DeltaCookie>, -} - -impl<'a> Iterator for Delta<'a> { - type Item = &'a Cookie<'static>; - - fn next(&mut self) -> Option<&'a Cookie<'static>> { - self.iter.next().map(|c| &c.cookie) - } -} - -use std::collections::hash_map::RandomState; -use std::collections::hash_set::Difference; -use std::iter::Chain; - -/// Iterator over all of the cookies in a jar. -pub struct Iter<'a> { - delta_cookies: - Chain, Difference<'a, DeltaCookie, RandomState>>, -} - -impl<'a> Iterator for Iter<'a> { - type Item = &'a Cookie<'static>; - - fn next(&mut self) -> Option<&'a Cookie<'static>> { - for cookie in self.delta_cookies.by_ref() { - if !cookie.removed { - return Some(&*cookie); - } - } - - None - } -} - -#[cfg(test)] -mod test { - #[cfg(feature = "secure-cookies")] - use super::Key; - use super::{Cookie, CookieJar}; - - #[test] - #[allow(deprecated)] - fn simple() { - let mut c = CookieJar::new(); - - c.add(Cookie::new("test", "")); - c.add(Cookie::new("test2", "")); - c.remove(Cookie::named("test")); - - assert!(c.get("test").is_none()); - assert!(c.get("test2").is_some()); - - c.add(Cookie::new("test3", "")); - c.clear(); - - assert!(c.get("test").is_none()); - assert!(c.get("test2").is_none()); - assert!(c.get("test3").is_none()); - } - - #[test] - fn jar_is_send() { - fn is_send(_: T) -> bool { - true - } - - assert!(is_send(CookieJar::new())) - } - - #[test] - #[cfg(feature = "secure-cookies")] - fn iter() { - let key = Key::generate(); - let mut c = CookieJar::new(); - - c.add_original(Cookie::new("original", "original")); - - c.add(Cookie::new("test", "test")); - c.add(Cookie::new("test2", "test2")); - c.add(Cookie::new("test3", "test3")); - assert_eq!(c.iter().count(), 4); - - c.signed(&key).add(Cookie::new("signed", "signed")); - c.private(&key).add(Cookie::new("encrypted", "encrypted")); - assert_eq!(c.iter().count(), 6); - - c.remove(Cookie::named("test")); - assert_eq!(c.iter().count(), 5); - - c.remove(Cookie::named("signed")); - c.remove(Cookie::named("test2")); - assert_eq!(c.iter().count(), 3); - - c.add(Cookie::new("test2", "test2")); - assert_eq!(c.iter().count(), 4); - - c.remove(Cookie::named("test2")); - assert_eq!(c.iter().count(), 3); - } - - #[test] - #[cfg(feature = "secure-cookies")] - fn delta() { - use std::collections::HashMap; - use time::Duration; - - let mut c = CookieJar::new(); - - c.add_original(Cookie::new("original", "original")); - c.add_original(Cookie::new("original1", "original1")); - - c.add(Cookie::new("test", "test")); - c.add(Cookie::new("test2", "test2")); - c.add(Cookie::new("test3", "test3")); - c.add(Cookie::new("test4", "test4")); - - c.remove(Cookie::named("test")); - c.remove(Cookie::named("original")); - - assert_eq!(c.delta().count(), 4); - - let names: HashMap<_, _> = c.delta().map(|c| (c.name(), c.max_age())).collect(); - - assert!(names.get("test2").unwrap().is_none()); - assert!(names.get("test3").unwrap().is_none()); - assert!(names.get("test4").unwrap().is_none()); - assert_eq!(names.get("original").unwrap(), &Some(Duration::zero())); - } - - #[test] - fn replace_original() { - let mut jar = CookieJar::new(); - jar.add_original(Cookie::new("original_a", "a")); - jar.add_original(Cookie::new("original_b", "b")); - assert_eq!(jar.get("original_a").unwrap().value(), "a"); - - jar.add(Cookie::new("original_a", "av2")); - assert_eq!(jar.get("original_a").unwrap().value(), "av2"); - } - - #[test] - fn empty_delta() { - let mut jar = CookieJar::new(); - jar.add(Cookie::new("name", "val")); - assert_eq!(jar.delta().count(), 1); - - jar.remove(Cookie::named("name")); - assert_eq!(jar.delta().count(), 0); - - jar.add_original(Cookie::new("name", "val")); - assert_eq!(jar.delta().count(), 0); - - jar.remove(Cookie::named("name")); - assert_eq!(jar.delta().count(), 1); - - jar.add(Cookie::new("name", "val")); - assert_eq!(jar.delta().count(), 1); - - jar.remove(Cookie::named("name")); - assert_eq!(jar.delta().count(), 1); - } - - #[test] - fn add_remove_add() { - let mut jar = CookieJar::new(); - jar.add_original(Cookie::new("name", "val")); - assert_eq!(jar.delta().count(), 0); - - jar.remove(Cookie::named("name")); - assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); - assert_eq!(jar.delta().count(), 1); - - // The cookie's been deleted. Another original doesn't change that. - jar.add_original(Cookie::new("name", "val")); - assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); - assert_eq!(jar.delta().count(), 1); - - jar.remove(Cookie::named("name")); - assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); - assert_eq!(jar.delta().count(), 1); - - jar.add(Cookie::new("name", "val")); - assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1); - assert_eq!(jar.delta().count(), 1); - - jar.remove(Cookie::named("name")); - assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); - assert_eq!(jar.delta().count(), 1); - } - - #[test] - fn replace_remove() { - let mut jar = CookieJar::new(); - jar.add_original(Cookie::new("name", "val")); - assert_eq!(jar.delta().count(), 0); - - jar.add(Cookie::new("name", "val")); - assert_eq!(jar.delta().count(), 1); - assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1); - - jar.remove(Cookie::named("name")); - assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); - } - - #[test] - fn remove_with_path() { - let mut jar = CookieJar::new(); - jar.add_original(Cookie::build("name", "val").finish()); - assert_eq!(jar.iter().count(), 1); - assert_eq!(jar.delta().count(), 0); - assert_eq!(jar.iter().filter(|c| c.path().is_none()).count(), 1); - - jar.remove(Cookie::build("name", "").path("/").finish()); - assert_eq!(jar.iter().count(), 0); - assert_eq!(jar.delta().count(), 1); - assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); - assert_eq!(jar.delta().filter(|c| c.path() == Some("/")).count(), 1); - } -} diff --git a/actix-http/src/cookie/mod.rs b/actix-http/src/cookie/mod.rs deleted file mode 100644 index b94e0fe0f..000000000 --- a/actix-http/src/cookie/mod.rs +++ /dev/null @@ -1,1100 +0,0 @@ -//! https://github.com/alexcrichton/cookie-rs fork -//! -//! HTTP cookie parsing and cookie jar management. -//! -//! This crates provides the [`Cookie`](struct.Cookie.html) type, which directly -//! maps to an HTTP cookie, and the [`CookieJar`](struct.CookieJar.html) type, -//! which allows for simple management of many cookies as well as encryption and -//! signing of cookies for session management. -//! -//! # Features -//! -//! This crates can be configured at compile-time through the following Cargo -//! features: -//! -//! -//! * **secure** (disabled by default) -//! -//! Enables signed and private (signed + encrypted) cookie jars. -//! -//! When this feature is enabled, the -//! [signed](struct.CookieJar.html#method.signed) and -//! [private](struct.CookieJar.html#method.private) method of `CookieJar` and -//! [`SignedJar`](struct.SignedJar.html) and -//! [`PrivateJar`](struct.PrivateJar.html) structures are available. The jars -//! act as "children jars", allowing for easy retrieval and addition of signed -//! and/or encrypted cookies to a cookie jar. When this feature is disabled, -//! none of the types are available. -//! -//! * **percent-encode** (disabled by default) -//! -//! Enables percent encoding and decoding of names and values in cookies. -//! -//! When this feature is enabled, the -//! [encoded](struct.Cookie.html#method.encoded) and -//! [`parse_encoded`](struct.Cookie.html#method.parse_encoded) methods of -//! `Cookie` become available. The `encoded` method returns a wrapper around a -//! `Cookie` whose `Display` implementation percent-encodes the name and value -//! of the cookie. The `parse_encoded` method percent-decodes the name and -//! value of a `Cookie` during parsing. When this feature is disabled, the -//! `encoded` and `parse_encoded` methods are not available. -//! -//! You can enable features via the `Cargo.toml` file: -//! -//! ```ignore -//! [dependencies.cookie] -//! features = ["secure", "percent-encode"] -//! ``` - -#![doc(html_root_url = "https://docs.rs/cookie/0.11")] -#![warn(missing_docs)] - -mod builder; -mod delta; -mod draft; -mod jar; -mod parse; - -#[cfg(feature = "secure-cookies")] -#[macro_use] -mod secure; -#[cfg(feature = "secure-cookies")] -pub use self::secure::*; - -use std::borrow::Cow; -use std::fmt; -use std::str::FromStr; - -use percent_encoding::{percent_encode, AsciiSet, CONTROLS}; -use time::{Duration, OffsetDateTime}; - -pub use self::builder::CookieBuilder; -pub use self::draft::*; -pub use self::jar::{CookieJar, Delta, Iter}; -use self::parse::parse_cookie; -pub use self::parse::ParseError; - -/// https://url.spec.whatwg.org/#fragment-percent-encode-set -const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); - -/// https://url.spec.whatwg.org/#path-percent-encode-set -const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}'); - -/// https://url.spec.whatwg.org/#userinfo-percent-encode-set -pub const USERINFO: &AsciiSet = &PATH - .add(b'/') - .add(b':') - .add(b';') - .add(b'=') - .add(b'@') - .add(b'[') - .add(b'\\') - .add(b']') - .add(b'^') - .add(b'|'); - -#[derive(Debug, Clone)] -enum CookieStr { - /// An string derived from indexes (start, end). - Indexed(usize, usize), - /// A string derived from a concrete string. - Concrete(Cow<'static, str>), -} - -impl CookieStr { - /// Retrieves the string `self` corresponds to. If `self` is derived from - /// indexes, the corresponding sub-slice of `string` is returned. Otherwise, - /// the concrete string is returned. - /// - /// # Panics - /// - /// Panics if `self` is an indexed string and `string` is None. - fn to_str<'s>(&'s self, string: Option<&'s Cow<'_, str>>) -> &'s str { - match *self { - CookieStr::Indexed(i, j) => { - let s = string.expect( - "`Some` base string must exist when \ - converting indexed str to str! (This is a module invariant.)", - ); - &s[i..j] - } - CookieStr::Concrete(ref cstr) => &*cstr, - } - } - - #[allow(clippy::ptr_arg)] - fn to_raw_str<'s, 'c: 's>(&'s self, string: &'s Cow<'c, str>) -> Option<&'c str> { - match *self { - CookieStr::Indexed(i, j) => match *string { - Cow::Borrowed(s) => Some(&s[i..j]), - Cow::Owned(_) => None, - }, - CookieStr::Concrete(_) => None, - } - } -} - -/// Representation of an HTTP cookie. -/// -/// # Constructing a `Cookie` -/// -/// To construct a cookie with only a name/value, use the [new](#method.new) -/// method: -/// -/// ```rust -/// use actix_http::cookie::Cookie; -/// -/// let cookie = Cookie::new("name", "value"); -/// assert_eq!(&cookie.to_string(), "name=value"); -/// ``` -/// -/// To construct more elaborate cookies, use the [build](#method.build) method -/// and [`CookieBuilder`](struct.CookieBuilder.html) methods: -/// -/// ```rust -/// use actix_http::cookie::Cookie; -/// -/// let cookie = Cookie::build("name", "value") -/// .domain("www.rust-lang.org") -/// .path("/") -/// .secure(true) -/// .http_only(true) -/// .finish(); -/// ``` -#[derive(Debug, Clone)] -pub struct Cookie<'c> { - /// Storage for the cookie string. Only used if this structure was derived - /// from a string that was subsequently parsed. - cookie_string: Option>, - /// The cookie's name. - name: CookieStr, - /// The cookie's value. - value: CookieStr, - /// The cookie's expiration, if any. - expires: Option, - /// The cookie's maximum age, if any. - max_age: Option, - /// The cookie's domain, if any. - domain: Option, - /// The cookie's path domain, if any. - path: Option, - /// Whether this cookie was marked Secure. - secure: Option, - /// Whether this cookie was marked HttpOnly. - http_only: Option, - /// The draft `SameSite` attribute. - same_site: Option, -} - -impl Cookie<'static> { - /// Creates a new `Cookie` with the given name and value. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let cookie = Cookie::new("name", "value"); - /// assert_eq!(cookie.name_value(), ("name", "value")); - /// ``` - pub fn new(name: N, value: V) -> Cookie<'static> - where - N: Into>, - V: Into>, - { - Cookie { - cookie_string: None, - name: CookieStr::Concrete(name.into()), - value: CookieStr::Concrete(value.into()), - expires: None, - max_age: None, - domain: None, - path: None, - secure: None, - http_only: None, - same_site: None, - } - } - - /// Creates a new `Cookie` with the given name and an empty value. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let cookie = Cookie::named("name"); - /// assert_eq!(cookie.name(), "name"); - /// assert!(cookie.value().is_empty()); - /// ``` - pub fn named(name: N) -> Cookie<'static> - where - N: Into>, - { - Cookie::new(name, "") - } - - /// Creates a new `CookieBuilder` instance from the given key and value - /// strings. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar").finish(); - /// assert_eq!(c.name_value(), ("foo", "bar")); - /// ``` - pub fn build(name: N, value: V) -> CookieBuilder - where - N: Into>, - V: Into>, - { - CookieBuilder::new(name, value) - } -} - -impl<'c> Cookie<'c> { - /// Parses a `Cookie` from the given HTTP cookie header value string. Does - /// not perform any percent-decoding. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse("foo=bar%20baz; HttpOnly").unwrap(); - /// assert_eq!(c.name_value(), ("foo", "bar%20baz")); - /// assert_eq!(c.http_only(), Some(true)); - /// ``` - pub fn parse(s: S) -> Result, ParseError> - where - S: Into>, - { - parse_cookie(s, false) - } - - /// Parses a `Cookie` from the given HTTP cookie header value string where - /// the name and value fields are percent-encoded. Percent-decodes the - /// name/value fields. - /// - /// This API requires the `percent-encode` feature to be enabled on this - /// crate. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse_encoded("foo=bar%20baz; HttpOnly").unwrap(); - /// assert_eq!(c.name_value(), ("foo", "bar baz")); - /// assert_eq!(c.http_only(), Some(true)); - /// ``` - pub fn parse_encoded(s: S) -> Result, ParseError> - where - S: Into>, - { - parse_cookie(s, true) - } - - /// Wraps `self` in an `EncodedCookie`: a cost-free wrapper around `Cookie` - /// whose `Display` implementation percent-encodes the name and value of the - /// wrapped `Cookie`. - /// - /// This method is only available when the `percent-encode` feature is - /// enabled. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut c = Cookie::new("my name", "this; value?"); - /// assert_eq!(&c.encoded().to_string(), "my%20name=this%3B%20value%3F"); - /// ``` - pub fn encoded<'a>(&'a self) -> EncodedCookie<'a, 'c> { - EncodedCookie(self) - } - - /// Converts `self` into a `Cookie` with a static lifetime. This method - /// results in at most one allocation. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::new("a", "b"); - /// let owned_cookie = c.into_owned(); - /// assert_eq!(owned_cookie.name_value(), ("a", "b")); - /// ``` - pub fn into_owned(self) -> Cookie<'static> { - Cookie { - cookie_string: self.cookie_string.map(|s| s.into_owned().into()), - name: self.name, - value: self.value, - expires: self.expires, - max_age: self.max_age, - domain: self.domain, - path: self.path, - secure: self.secure, - http_only: self.http_only, - same_site: self.same_site, - } - } - - /// Returns the name of `self`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::new("name", "value"); - /// assert_eq!(c.name(), "name"); - /// ``` - #[inline] - pub fn name(&self) -> &str { - self.name.to_str(self.cookie_string.as_ref()) - } - - /// Returns the value of `self`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::new("name", "value"); - /// assert_eq!(c.value(), "value"); - /// ``` - #[inline] - pub fn value(&self) -> &str { - self.value.to_str(self.cookie_string.as_ref()) - } - - /// Returns the name and value of `self` as a tuple of `(name, value)`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::new("name", "value"); - /// assert_eq!(c.name_value(), ("name", "value")); - /// ``` - #[inline] - pub fn name_value(&self) -> (&str, &str) { - (self.name(), self.value()) - } - - /// Returns whether this cookie was marked `HttpOnly` or not. Returns - /// `Some(true)` when the cookie was explicitly set (manually or parsed) as - /// `HttpOnly`, `Some(false)` when `http_only` was manually set to `false`, - /// and `None` otherwise. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse("name=value; httponly").unwrap(); - /// assert_eq!(c.http_only(), Some(true)); - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.http_only(), None); - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.http_only(), None); - /// - /// // An explicitly set "false" value. - /// c.set_http_only(false); - /// assert_eq!(c.http_only(), Some(false)); - /// - /// // An explicitly set "true" value. - /// c.set_http_only(true); - /// assert_eq!(c.http_only(), Some(true)); - /// ``` - #[inline] - pub fn http_only(&self) -> Option { - self.http_only - } - - /// Returns whether this cookie was marked `Secure` or not. Returns - /// `Some(true)` when the cookie was explicitly set (manually or parsed) as - /// `Secure`, `Some(false)` when `secure` was manually set to `false`, and - /// `None` otherwise. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse("name=value; Secure").unwrap(); - /// assert_eq!(c.secure(), Some(true)); - /// - /// let mut c = Cookie::parse("name=value").unwrap(); - /// assert_eq!(c.secure(), None); - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.secure(), None); - /// - /// // An explicitly set "false" value. - /// c.set_secure(false); - /// assert_eq!(c.secure(), Some(false)); - /// - /// // An explicitly set "true" value. - /// c.set_secure(true); - /// assert_eq!(c.secure(), Some(true)); - /// ``` - #[inline] - pub fn secure(&self) -> Option { - self.secure - } - - /// Returns the `SameSite` attribute of this cookie if one was specified. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{Cookie, SameSite}; - /// - /// let c = Cookie::parse("name=value; SameSite=Lax").unwrap(); - /// assert_eq!(c.same_site(), Some(SameSite::Lax)); - /// ``` - #[inline] - pub fn same_site(&self) -> Option { - self.same_site - } - - /// Returns the specified max-age of the cookie if one was specified. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse("name=value").unwrap(); - /// assert_eq!(c.max_age(), None); - /// - /// let c = Cookie::parse("name=value; Max-Age=3600").unwrap(); - /// assert_eq!(c.max_age().map(|age| age.whole_hours()), Some(1)); - /// ``` - #[inline] - pub fn max_age(&self) -> Option { - self.max_age - } - - /// Returns the `Path` of the cookie if one was specified. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse("name=value").unwrap(); - /// assert_eq!(c.path(), None); - /// - /// let c = Cookie::parse("name=value; Path=/").unwrap(); - /// assert_eq!(c.path(), Some("/")); - /// - /// let c = Cookie::parse("name=value; path=/sub").unwrap(); - /// assert_eq!(c.path(), Some("/sub")); - /// ``` - #[inline] - pub fn path(&self) -> Option<&str> { - match self.path { - Some(ref c) => Some(c.to_str(self.cookie_string.as_ref())), - None => None, - } - } - - /// Returns the `Domain` of the cookie if one was specified. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse("name=value").unwrap(); - /// assert_eq!(c.domain(), None); - /// - /// let c = Cookie::parse("name=value; Domain=crates.io").unwrap(); - /// assert_eq!(c.domain(), Some("crates.io")); - /// ``` - #[inline] - pub fn domain(&self) -> Option<&str> { - match self.domain { - Some(ref c) => Some(c.to_str(self.cookie_string.as_ref())), - None => None, - } - } - - /// Returns the `Expires` time of the cookie if one was specified. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse("name=value").unwrap(); - /// assert_eq!(c.expires(), None); - /// - /// let expire_time = "Wed, 21 Oct 2017 07:28:00 GMT"; - /// let cookie_str = format!("name=value; Expires={}", expire_time); - /// let c = Cookie::parse(cookie_str).unwrap(); - /// assert_eq!(c.expires().map(|t| t.year()), Some(2017)); - /// ``` - #[inline] - pub fn expires(&self) -> Option { - self.expires - } - - /// Sets the name of `self` to `name`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.name(), "name"); - /// - /// c.set_name("foo"); - /// assert_eq!(c.name(), "foo"); - /// ``` - pub fn set_name>>(&mut self, name: N) { - self.name = CookieStr::Concrete(name.into()) - } - - /// Sets the value of `self` to `value`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.value(), "value"); - /// - /// c.set_value("bar"); - /// assert_eq!(c.value(), "bar"); - /// ``` - pub fn set_value>>(&mut self, value: V) { - self.value = CookieStr::Concrete(value.into()) - } - - /// Sets the value of `http_only` in `self` to `value`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.http_only(), None); - /// - /// c.set_http_only(true); - /// assert_eq!(c.http_only(), Some(true)); - /// ``` - #[inline] - pub fn set_http_only(&mut self, value: bool) { - self.http_only = Some(value); - } - - /// Sets the value of `secure` in `self` to `value`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.secure(), None); - /// - /// c.set_secure(true); - /// assert_eq!(c.secure(), Some(true)); - /// ``` - #[inline] - pub fn set_secure(&mut self, value: bool) { - self.secure = Some(value); - } - - /// Sets the value of `same_site` in `self` to `value`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{Cookie, SameSite}; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert!(c.same_site().is_none()); - /// - /// c.set_same_site(SameSite::Strict); - /// assert_eq!(c.same_site(), Some(SameSite::Strict)); - /// ``` - #[inline] - pub fn set_same_site(&mut self, value: SameSite) { - self.same_site = Some(value); - } - - /// Sets the value of `max_age` in `self` to `value`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// use time::Duration; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.max_age(), None); - /// - /// c.set_max_age(Duration::hours(10)); - /// assert_eq!(c.max_age(), Some(Duration::hours(10))); - /// ``` - #[inline] - pub fn set_max_age(&mut self, value: Duration) { - self.max_age = Some(value); - } - - /// Sets the `path` of `self` to `path`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.path(), None); - /// - /// c.set_path("/"); - /// assert_eq!(c.path(), Some("/")); - /// ``` - pub fn set_path>>(&mut self, path: P) { - self.path = Some(CookieStr::Concrete(path.into())); - } - - /// Sets the `domain` of `self` to `domain`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.domain(), None); - /// - /// c.set_domain("rust-lang.org"); - /// assert_eq!(c.domain(), Some("rust-lang.org")); - /// ``` - pub fn set_domain>>(&mut self, domain: D) { - self.domain = Some(CookieStr::Concrete(domain.into())); - } - - /// Sets the expires field of `self` to `time`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// use time::{Duration, OffsetDateTime}; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.expires(), None); - /// - /// let mut now = OffsetDateTime::now(); - /// now += Duration::week(); - /// - /// c.set_expires(now); - /// assert!(c.expires().is_some()) - /// ``` - #[inline] - pub fn set_expires(&mut self, time: OffsetDateTime) { - self.expires = Some(time); - } - - /// Makes `self` a "permanent" cookie by extending its expiration and max - /// age 20 years into the future. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// use time::Duration; - /// - /// let mut c = Cookie::new("foo", "bar"); - /// assert!(c.expires().is_none()); - /// assert!(c.max_age().is_none()); - /// - /// c.make_permanent(); - /// assert!(c.expires().is_some()); - /// assert_eq!(c.max_age(), Some(Duration::days(365 * 20))); - /// ``` - pub fn make_permanent(&mut self) { - let twenty_years = Duration::days(365 * 20); - self.set_max_age(twenty_years); - self.set_expires(OffsetDateTime::now_utc() + twenty_years); - } - - fn fmt_parameters(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(true) = self.http_only() { - write!(f, "; HttpOnly")?; - } - - if let Some(true) = self.secure() { - write!(f, "; Secure")?; - } - - if let Some(same_site) = self.same_site() { - write!(f, "; SameSite={}", same_site)?; - } - - if let Some(path) = self.path() { - write!(f, "; Path={}", path)?; - } - - if let Some(domain) = self.domain() { - write!(f, "; Domain={}", domain)?; - } - - if let Some(max_age) = self.max_age() { - write!(f, "; Max-Age={}", max_age.whole_seconds())?; - } - - if let Some(time) = self.expires() { - write!(f, "; Expires={}", time.format("%a, %d %b %Y %H:%M:%S GMT"))?; - } - - Ok(()) - } - - /// Returns the name of `self` as a string slice of the raw string `self` - /// was originally parsed from. If `self` was not originally parsed from a - /// raw string, returns `None`. - /// - /// This method differs from [name](#method.name) in that it returns a - /// string with the same lifetime as the originally parsed string. This - /// lifetime may outlive `self`. If a longer lifetime is not required, or - /// you're unsure if you need a longer lifetime, use [name](#method.name). - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let cookie_string = format!("{}={}", "foo", "bar"); - /// - /// // `c` will be dropped at the end of the scope, but `name` will live on - /// let name = { - /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); - /// c.name_raw() - /// }; - /// - /// assert_eq!(name, Some("foo")); - /// ``` - #[inline] - pub fn name_raw(&self) -> Option<&'c str> { - self.cookie_string - .as_ref() - .and_then(|s| self.name.to_raw_str(s)) - } - - /// Returns the value of `self` as a string slice of the raw string `self` - /// was originally parsed from. If `self` was not originally parsed from a - /// raw string, returns `None`. - /// - /// This method differs from [value](#method.value) in that it returns a - /// string with the same lifetime as the originally parsed string. This - /// lifetime may outlive `self`. If a longer lifetime is not required, or - /// you're unsure if you need a longer lifetime, use [value](#method.value). - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let cookie_string = format!("{}={}", "foo", "bar"); - /// - /// // `c` will be dropped at the end of the scope, but `value` will live on - /// let value = { - /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); - /// c.value_raw() - /// }; - /// - /// assert_eq!(value, Some("bar")); - /// ``` - #[inline] - pub fn value_raw(&self) -> Option<&'c str> { - self.cookie_string - .as_ref() - .and_then(|s| self.value.to_raw_str(s)) - } - - /// Returns the `Path` of `self` as a string slice of the raw string `self` - /// was originally parsed from. If `self` was not originally parsed from a - /// raw string, or if `self` doesn't contain a `Path`, or if the `Path` has - /// changed since parsing, returns `None`. - /// - /// This method differs from [path](#method.path) in that it returns a - /// string with the same lifetime as the originally parsed string. This - /// lifetime may outlive `self`. If a longer lifetime is not required, or - /// you're unsure if you need a longer lifetime, use [path](#method.path). - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let cookie_string = format!("{}={}; Path=/", "foo", "bar"); - /// - /// // `c` will be dropped at the end of the scope, but `path` will live on - /// let path = { - /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); - /// c.path_raw() - /// }; - /// - /// assert_eq!(path, Some("/")); - /// ``` - #[inline] - pub fn path_raw(&self) -> Option<&'c str> { - match (self.path.as_ref(), self.cookie_string.as_ref()) { - (Some(path), Some(string)) => path.to_raw_str(string), - _ => None, - } - } - - /// Returns the `Domain` of `self` as a string slice of the raw string - /// `self` was originally parsed from. If `self` was not originally parsed - /// from a raw string, or if `self` doesn't contain a `Domain`, or if the - /// `Domain` has changed since parsing, returns `None`. - /// - /// This method differs from [domain](#method.domain) in that it returns a - /// string with the same lifetime as the originally parsed string. This - /// lifetime may outlive `self` struct. If a longer lifetime is not - /// required, or you're unsure if you need a longer lifetime, use - /// [domain](#method.domain). - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let cookie_string = format!("{}={}; Domain=crates.io", "foo", "bar"); - /// - /// //`c` will be dropped at the end of the scope, but `domain` will live on - /// let domain = { - /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); - /// c.domain_raw() - /// }; - /// - /// assert_eq!(domain, Some("crates.io")); - /// ``` - #[inline] - pub fn domain_raw(&self) -> Option<&'c str> { - match (self.domain.as_ref(), self.cookie_string.as_ref()) { - (Some(domain), Some(string)) => domain.to_raw_str(string), - _ => None, - } - } -} - -/// Wrapper around `Cookie` whose `Display` implementation percent-encodes the -/// cookie's name and value. -/// -/// A value of this type can be obtained via the -/// [encoded](struct.Cookie.html#method.encoded) method on -/// [Cookie](struct.Cookie.html). This type should only be used for its -/// `Display` implementation. -/// -/// This type is only available when the `percent-encode` feature is enabled. -/// -/// # Example -/// -/// ```rust -/// use actix_http::cookie::Cookie; -/// -/// let mut c = Cookie::new("my name", "this; value?"); -/// assert_eq!(&c.encoded().to_string(), "my%20name=this%3B%20value%3F"); -/// ``` -pub struct EncodedCookie<'a, 'c>(&'a Cookie<'c>); - -impl<'a, 'c: 'a> fmt::Display for EncodedCookie<'a, 'c> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Percent-encode the name and value. - let name = percent_encode(self.0.name().as_bytes(), USERINFO); - let value = percent_encode(self.0.value().as_bytes(), USERINFO); - - // Write out the name/value pair and the cookie's parameters. - write!(f, "{}={}", name, value)?; - self.0.fmt_parameters(f) - } -} - -impl<'c> fmt::Display for Cookie<'c> { - /// Formats the cookie `self` as a `Set-Cookie` header value. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut cookie = Cookie::build("foo", "bar") - /// .path("/") - /// .finish(); - /// - /// assert_eq!(&cookie.to_string(), "foo=bar; Path=/"); - /// ``` - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}={}", self.name(), self.value())?; - self.fmt_parameters(f) - } -} - -impl FromStr for Cookie<'static> { - type Err = ParseError; - - fn from_str(s: &str) -> Result, ParseError> { - Cookie::parse(s).map(|c| c.into_owned()) - } -} - -impl<'a, 'b> PartialEq> for Cookie<'a> { - fn eq(&self, other: &Cookie<'b>) -> bool { - let so_far_so_good = self.name() == other.name() - && self.value() == other.value() - && self.http_only() == other.http_only() - && self.secure() == other.secure() - && self.max_age() == other.max_age() - && self.expires() == other.expires(); - - if !so_far_so_good { - return false; - } - - match (self.path(), other.path()) { - (Some(a), Some(b)) if a.eq_ignore_ascii_case(b) => {} - (None, None) => {} - _ => return false, - }; - - match (self.domain(), other.domain()) { - (Some(a), Some(b)) if a.eq_ignore_ascii_case(b) => {} - (None, None) => {} - _ => return false, - }; - - true - } -} - -#[cfg(test)] -mod tests { - use super::{Cookie, SameSite}; - use time::PrimitiveDateTime; - - #[test] - fn format() { - let cookie = Cookie::new("foo", "bar"); - assert_eq!(&cookie.to_string(), "foo=bar"); - - let cookie = Cookie::build("foo", "bar").http_only(true).finish(); - assert_eq!(&cookie.to_string(), "foo=bar; HttpOnly"); - - let cookie = Cookie::build("foo", "bar").max_age(10).finish(); - assert_eq!(&cookie.to_string(), "foo=bar; Max-Age=10"); - - let cookie = Cookie::build("foo", "bar").secure(true).finish(); - assert_eq!(&cookie.to_string(), "foo=bar; Secure"); - - let cookie = Cookie::build("foo", "bar").path("/").finish(); - assert_eq!(&cookie.to_string(), "foo=bar; Path=/"); - - let cookie = Cookie::build("foo", "bar") - .domain("www.rust-lang.org") - .finish(); - 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() - .assume_utc(); - let cookie = Cookie::build("foo", "bar").expires(expires).finish(); - assert_eq!( - &cookie.to_string(), - "foo=bar; Expires=Wed, 21 Oct 2015 07:28:00 GMT" - ); - - let cookie = Cookie::build("foo", "bar") - .same_site(SameSite::Strict) - .finish(); - assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Strict"); - - let cookie = Cookie::build("foo", "bar") - .same_site(SameSite::Lax) - .finish(); - assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Lax"); - - let cookie = Cookie::build("foo", "bar") - .same_site(SameSite::None) - .finish(); - assert_eq!(&cookie.to_string(), "foo=bar; SameSite=None"); - } - - #[test] - fn cookie_string_long_lifetimes() { - let cookie_string = - "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io".to_owned(); - let (name, value, path, domain) = { - // Create a cookie passing a slice - let c = Cookie::parse(cookie_string.as_str()).unwrap(); - (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) - }; - - assert_eq!(name, Some("bar")); - assert_eq!(value, Some("baz")); - assert_eq!(path, Some("/subdir")); - assert_eq!(domain, Some("crates.io")); - } - - #[test] - fn owned_cookie_string() { - let cookie_string = - "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io".to_owned(); - let (name, value, path, domain) = { - // Create a cookie passing an owned string - let c = Cookie::parse(cookie_string).unwrap(); - (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) - }; - - assert_eq!(name, None); - assert_eq!(value, None); - assert_eq!(path, None); - assert_eq!(domain, None); - } - - #[test] - fn owned_cookie_struct() { - let cookie_string = "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io"; - let (name, value, path, domain) = { - // Create an owned cookie - let c = Cookie::parse(cookie_string).unwrap().into_owned(); - - (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) - }; - - assert_eq!(name, None); - assert_eq!(value, None); - assert_eq!(path, None); - assert_eq!(domain, None); - } - - #[test] - fn format_encoded() { - let cookie = Cookie::build("foo !?=", "bar;; a").finish(); - let cookie_str = cookie.encoded().to_string(); - assert_eq!(&cookie_str, "foo%20!%3F%3D=bar%3B%3B%20a"); - - let cookie = Cookie::parse_encoded(cookie_str).unwrap(); - assert_eq!(cookie.name_value(), ("foo !?=", "bar;; a")); - } -} diff --git a/actix-http/src/cookie/parse.rs b/actix-http/src/cookie/parse.rs deleted file mode 100644 index d472b32b6..000000000 --- a/actix-http/src/cookie/parse.rs +++ /dev/null @@ -1,467 +0,0 @@ -use std::borrow::Cow; -use std::cmp; -use std::convert::From; -use std::error::Error; -use std::fmt; -use std::str::Utf8Error; - -use percent_encoding::percent_decode; -use time::Duration; - -use super::{Cookie, CookieStr, SameSite}; - -use crate::time_parser; - -/// Enum corresponding to a parsing error. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum ParseError { - /// The cookie did not contain a name/value pair. - MissingPair, - /// The cookie's name was empty. - EmptyName, - /// Decoding the cookie's name or value resulted in invalid UTF-8. - Utf8Error(Utf8Error), - /// It is discouraged to exhaustively match on this enum as its variants may - /// grow without a breaking-change bump in version numbers. - #[doc(hidden)] - __Nonexhasutive, -} - -impl ParseError { - /// Returns a description of this error as a string - pub fn as_str(&self) -> &'static str { - match *self { - ParseError::MissingPair => "the cookie is missing a name/value pair", - ParseError::EmptyName => "the cookie's name is empty", - ParseError::Utf8Error(_) => { - "decoding the cookie's name or value resulted in invalid UTF-8" - } - ParseError::__Nonexhasutive => unreachable!("__Nonexhasutive ParseError"), - } - } -} - -impl fmt::Display for ParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.as_str()) - } -} - -impl From for ParseError { - fn from(error: Utf8Error) -> ParseError { - ParseError::Utf8Error(error) - } -} - -impl Error for ParseError {} - -fn indexes_of(needle: &str, haystack: &str) -> Option<(usize, usize)> { - let haystack_start = haystack.as_ptr() as usize; - let needle_start = needle.as_ptr() as usize; - - if needle_start < haystack_start { - return None; - } - - if (needle_start + needle.len()) > (haystack_start + haystack.len()) { - return None; - } - - let start = needle_start - haystack_start; - let end = start + needle.len(); - Some((start, end)) -} - -fn name_val_decoded( - name: &str, - val: &str, -) -> Result<(CookieStr, CookieStr), ParseError> { - let decoded_name = percent_decode(name.as_bytes()).decode_utf8()?; - let decoded_value = percent_decode(val.as_bytes()).decode_utf8()?; - let name = CookieStr::Concrete(Cow::Owned(decoded_name.into_owned())); - let val = CookieStr::Concrete(Cow::Owned(decoded_value.into_owned())); - - Ok((name, val)) -} - -// This function does the real parsing but _does not_ set the `cookie_string` in -// the returned cookie object. This only exists so that the borrow to `s` is -// returned at the end of the call, allowing the `cookie_string` field to be -// set in the outer `parse` function. -fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { - let mut attributes = s.split(';'); - let key_value = match attributes.next() { - Some(s) => s, - _ => panic!(), - }; - - // Determine the name = val. - let (name, value) = match key_value.find('=') { - Some(i) => (key_value[..i].trim(), key_value[(i + 1)..].trim()), - None => return Err(ParseError::MissingPair), - }; - - if name.is_empty() { - return Err(ParseError::EmptyName); - } - - // Create a cookie with all of the defaults. We'll fill things in while we - // iterate through the parameters below. - let (name, value) = if decode { - name_val_decoded(name, value)? - } else { - let name_indexes = indexes_of(name, s).expect("name sub"); - let value_indexes = indexes_of(value, s).expect("value sub"); - let name = CookieStr::Indexed(name_indexes.0, name_indexes.1); - let value = CookieStr::Indexed(value_indexes.0, value_indexes.1); - - (name, value) - }; - - let mut cookie = Cookie { - name, - value, - cookie_string: None, - expires: None, - max_age: None, - domain: None, - path: None, - secure: None, - http_only: None, - same_site: None, - }; - - for attr in attributes { - let (key, value) = match attr.find('=') { - Some(i) => (attr[..i].trim(), Some(attr[(i + 1)..].trim())), - None => (attr.trim(), None), - }; - - match (&*key.to_ascii_lowercase(), value) { - ("secure", _) => cookie.secure = Some(true), - ("httponly", _) => cookie.http_only = Some(true), - ("max-age", Some(v)) => { - // See RFC 6265 Section 5.2.2, negative values indicate that the - // earliest possible expiration time should be used, so set the - // max age as 0 seconds. - cookie.max_age = match v.parse() { - Ok(val) if val <= 0 => Some(Duration::zero()), - Ok(val) => { - // Don't panic if the max age seconds is greater than what's supported by - // `Duration`. - let val = cmp::min(val, Duration::max_value().whole_seconds()); - Some(Duration::seconds(val)) - } - Err(_) => continue, - }; - } - ("domain", Some(mut domain)) if !domain.is_empty() => { - if domain.starts_with('.') { - domain = &domain[1..]; - } - - let (i, j) = indexes_of(domain, s).expect("domain sub"); - cookie.domain = Some(CookieStr::Indexed(i, j)); - } - ("path", Some(v)) => { - let (i, j) = indexes_of(v, s).expect("path sub"); - cookie.path = Some(CookieStr::Indexed(i, j)); - } - ("samesite", Some(v)) => { - if v.eq_ignore_ascii_case("strict") { - 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 - // ignore this cookie, i.e, fail to parse it, when an - // invalid value is passed in. The draft is at - // http://httpwg.org/http-extensions/draft-ietf-httpbis-cookie-same-site.html. - } - } - ("expires", Some(v)) => { - // Try parsing with three date formats according to - // http://tools.ietf.org/html/rfc2616#section-3.3.1. Try - // additional ones as encountered in the real world. - let tm = time_parser::parse_http_date(v) - .or_else(|| time::parse(v, "%a, %d-%b-%Y %H:%M:%S").ok()); - - if let Some(time) = tm { - cookie.expires = Some(time.assume_utc()) - } - } - _ => { - // We're going to be permissive here. If we have no idea what - // this is, then it's something nonstandard. We're not going to - // store it (because it's not compliant), but we're also not - // going to emit an error. - } - } - } - - Ok(cookie) -} - -pub fn parse_cookie<'c, S>(cow: S, decode: bool) -> Result, ParseError> -where - S: Into>, -{ - let s = cow.into(); - let mut cookie = parse_inner(&s, decode)?; - cookie.cookie_string = Some(s); - Ok(cookie) -} - -#[cfg(test)] -mod tests { - use super::{Cookie, SameSite}; - use time::{Duration, PrimitiveDateTime}; - - macro_rules! assert_eq_parse { - ($string:expr, $expected:expr) => { - let cookie = match Cookie::parse($string) { - Ok(cookie) => cookie, - Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e), - }; - - assert_eq!(cookie, $expected); - }; - } - - macro_rules! assert_ne_parse { - ($string:expr, $expected:expr) => { - let cookie = match Cookie::parse($string) { - Ok(cookie) => cookie, - Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e), - }; - - assert_ne!(cookie, $expected); - }; - } - - #[test] - fn parse_same_site() { - let expected = Cookie::build("foo", "bar") - .same_site(SameSite::Lax) - .finish(); - - assert_eq_parse!("foo=bar; SameSite=Lax", expected); - assert_eq_parse!("foo=bar; SameSite=lax", expected); - assert_eq_parse!("foo=bar; SameSite=LAX", expected); - assert_eq_parse!("foo=bar; samesite=Lax", expected); - assert_eq_parse!("foo=bar; SAMESITE=Lax", expected); - - let expected = Cookie::build("foo", "bar") - .same_site(SameSite::Strict) - .finish(); - - assert_eq_parse!("foo=bar; SameSite=Strict", expected); - assert_eq_parse!("foo=bar; SameSITE=Strict", expected); - 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] - fn parse() { - assert!(Cookie::parse("bar").is_err()); - assert!(Cookie::parse("=bar").is_err()); - assert!(Cookie::parse(" =bar").is_err()); - assert!(Cookie::parse("foo=").is_ok()); - - let expected = Cookie::build("foo", "bar=baz").finish(); - assert_eq_parse!("foo=bar=baz", expected); - - let mut expected = Cookie::build("foo", "bar").finish(); - assert_eq_parse!("foo=bar", expected); - assert_eq_parse!("foo = bar", expected); - assert_eq_parse!(" foo=bar ", expected); - assert_eq_parse!(" foo=bar ;Domain=", expected); - assert_eq_parse!(" foo=bar ;Domain= ", expected); - assert_eq_parse!(" foo=bar ;Ignored", expected); - - let mut unexpected = Cookie::build("foo", "bar").http_only(false).finish(); - assert_ne_parse!(" foo=bar ;HttpOnly", unexpected); - assert_ne_parse!(" foo=bar; httponly", unexpected); - - expected.set_http_only(true); - assert_eq_parse!(" foo=bar ;HttpOnly", expected); - assert_eq_parse!(" foo=bar ;httponly", expected); - assert_eq_parse!(" foo=bar ;HTTPONLY=whatever", expected); - assert_eq_parse!(" foo=bar ; sekure; HTTPONLY", expected); - - expected.set_secure(true); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure=aaaa", expected); - - unexpected.set_http_only(true); - unexpected.set_secure(true); - assert_ne_parse!(" foo=bar ;HttpOnly; skeure", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly; =secure", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly;", unexpected); - - unexpected.set_secure(false); - assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected); - - expected.set_max_age(Duration::zero()); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=0", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0 ", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=-1", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", expected); - - expected.set_max_age(Duration::minutes(1)); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=60", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 60 ", expected); - - expected.set_max_age(Duration::seconds(4)); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 4 ", expected); - - unexpected.set_secure(true); - unexpected.set_max_age(Duration::minutes(1)); - assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=122", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 38 ", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=51", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0", unexpected); - - expected.set_path("/"); - assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/", expected); - assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/", expected); - - expected.set_path("/foo"); - assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", expected); - assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/foo", expected); - assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path=/foo", expected); - assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path = /foo", expected); - - unexpected.set_max_age(Duration::seconds(4)); - unexpected.set_path("/bar"); - assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", unexpected); - assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/baz", unexpected); - - expected.set_domain("www.foo.com"); - assert_eq_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=www.foo.com", - expected - ); - - expected.set_domain("foo.com"); - assert_eq_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=foo.com", - expected - ); - assert_eq_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=FOO.COM", - expected - ); - - unexpected.set_path("/foo"); - unexpected.set_domain("bar.com"); - assert_ne_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=foo.com", - unexpected - ); - assert_ne_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=FOO.COM", - unexpected - ); - - 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() - .assume_utc(); - expected.set_expires(expires); - assert_eq_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", - expected - ); - - unexpected.set_domain("foo.com"); - 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] - fn odd_characters() { - let expected = Cookie::new("foo", "b%2Fr"); - assert_eq_parse!("foo=b%2Fr", expected); - } - - #[test] - fn odd_characters_encoded() { - let expected = Cookie::new("foo", "b/r"); - let cookie = match Cookie::parse_encoded("foo=b%2Fr") { - Ok(cookie) => cookie, - Err(e) => panic!("Failed to parse: {:?}", e), - }; - - assert_eq!(cookie, expected); - } - - #[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 - ); - } -} diff --git a/actix-http/src/cookie/secure/key.rs b/actix-http/src/cookie/secure/key.rs deleted file mode 100644 index 41413921f..000000000 --- a/actix-http/src/cookie/secure/key.rs +++ /dev/null @@ -1,190 +0,0 @@ -use ring::hkdf::{Algorithm, KeyType, Prk, HKDF_SHA256}; -use ring::rand::{SecureRandom, SystemRandom}; - -use super::private::KEY_LEN as PRIVATE_KEY_LEN; -use super::signed::KEY_LEN as SIGNED_KEY_LEN; - -static HKDF_DIGEST: Algorithm = HKDF_SHA256; -const KEYS_INFO: &[&[u8]] = &[b"COOKIE;SIGNED:HMAC-SHA256;PRIVATE:AEAD-AES-256-GCM"]; - -/// A cryptographic master key for use with `Signed` and/or `Private` jars. -/// -/// This structure encapsulates secure, cryptographic keys for use with both -/// [PrivateJar](struct.PrivateJar.html) and [SignedJar](struct.SignedJar.html). -/// It can be derived from a single master key via -/// [from_master](#method.from_master) or generated from a secure random source -/// via [generate](#method.generate). A single instance of `Key` can be used for -/// both a `PrivateJar` and a `SignedJar`. -/// -/// This type is only available when the `secure` feature is enabled. -#[derive(Clone)] -pub struct Key { - signing_key: [u8; SIGNED_KEY_LEN], - encryption_key: [u8; PRIVATE_KEY_LEN], -} - -impl KeyType for &Key { - #[inline] - fn len(&self) -> usize { - SIGNED_KEY_LEN + PRIVATE_KEY_LEN - } -} - -impl Key { - /// Derives new signing/encryption keys from a master key. - /// - /// The master key must be at least 256-bits (32 bytes). For security, the - /// master key _must_ be cryptographically random. The keys are derived - /// deterministically from the master key. - /// - /// # Panics - /// - /// Panics if `key` is less than 32 bytes in length. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Key; - /// - /// # /* - /// let master_key = { /* a cryptographically random key >= 32 bytes */ }; - /// # */ - /// # let master_key: &Vec = &(0..32).collect(); - /// - /// let key = Key::from_master(master_key); - /// ``` - pub fn from_master(key: &[u8]) -> Key { - if key.len() < 32 { - panic!( - "bad master key length: expected at least 32 bytes, found {}", - key.len() - ); - } - - // An empty `Key` structure; will be filled in with HKDF derived keys. - let mut output_key = Key { - signing_key: [0; SIGNED_KEY_LEN], - encryption_key: [0; PRIVATE_KEY_LEN], - }; - - // Expand the master key into two HKDF generated keys. - let mut both_keys = [0; SIGNED_KEY_LEN + PRIVATE_KEY_LEN]; - let prk = Prk::new_less_safe(HKDF_DIGEST, key); - let okm = prk.expand(KEYS_INFO, &output_key).expect("okm expand"); - okm.fill(&mut both_keys).expect("fill keys"); - - // Copy the key parts into their respective fields. - output_key - .signing_key - .copy_from_slice(&both_keys[..SIGNED_KEY_LEN]); - output_key - .encryption_key - .copy_from_slice(&both_keys[SIGNED_KEY_LEN..]); - output_key - } - - /// Generates signing/encryption keys from a secure, random source. Keys are - /// generated non-deterministically. - /// - /// # Panics - /// - /// Panics if randomness cannot be retrieved from the operating system. See - /// [try_generate](#method.try_generate) for a non-panicking version. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Key; - /// - /// let key = Key::generate(); - /// ``` - pub fn generate() -> Key { - Self::try_generate().expect("failed to generate `Key` from randomness") - } - - /// Attempts to generate signing/encryption keys from a secure, random - /// source. Keys are generated non-deterministically. If randomness cannot be - /// retrieved from the underlying operating system, returns `None`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Key; - /// - /// let key = Key::try_generate(); - /// ``` - pub fn try_generate() -> Option { - let mut sign_key = [0; SIGNED_KEY_LEN]; - let mut enc_key = [0; PRIVATE_KEY_LEN]; - - let rng = SystemRandom::new(); - if rng.fill(&mut sign_key).is_err() || rng.fill(&mut enc_key).is_err() { - return None; - } - - Some(Key { - signing_key: sign_key, - encryption_key: enc_key, - }) - } - - /// Returns the raw bytes of a key suitable for signing cookies. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Key; - /// - /// let key = Key::generate(); - /// let signing_key = key.signing(); - /// ``` - pub fn signing(&self) -> &[u8] { - &self.signing_key[..] - } - - /// Returns the raw bytes of a key suitable for encrypting cookies. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Key; - /// - /// let key = Key::generate(); - /// let encryption_key = key.encryption(); - /// ``` - pub fn encryption(&self) -> &[u8] { - &self.encryption_key[..] - } -} - -#[cfg(test)] -mod test { - use super::Key; - - #[test] - fn deterministic_from_master() { - let master_key: Vec = (0..32).collect(); - - let key_a = Key::from_master(&master_key); - let key_b = Key::from_master(&master_key); - - assert_eq!(key_a.signing(), key_b.signing()); - assert_eq!(key_a.encryption(), key_b.encryption()); - assert_ne!(key_a.encryption(), key_a.signing()); - - let master_key_2: Vec = (32..64).collect(); - let key_2 = Key::from_master(&master_key_2); - - assert_ne!(key_2.signing(), key_a.signing()); - assert_ne!(key_2.encryption(), key_a.encryption()); - } - - #[test] - fn non_deterministic_generate() { - let key_a = Key::generate(); - let key_b = Key::generate(); - - assert_ne!(key_a.signing(), key_b.signing()); - assert_ne!(key_a.encryption(), key_b.encryption()); - } -} diff --git a/actix-http/src/cookie/secure/macros.rs b/actix-http/src/cookie/secure/macros.rs deleted file mode 100644 index 089047c4e..000000000 --- a/actix-http/src/cookie/secure/macros.rs +++ /dev/null @@ -1,40 +0,0 @@ -#[cfg(test)] -macro_rules! assert_simple_behaviour { - ($clear:expr, $secure:expr) => {{ - assert_eq!($clear.iter().count(), 0); - - $secure.add(Cookie::new("name", "val")); - assert_eq!($clear.iter().count(), 1); - assert_eq!($secure.get("name").unwrap().value(), "val"); - assert_ne!($clear.get("name").unwrap().value(), "val"); - - $secure.add(Cookie::new("another", "two")); - assert_eq!($clear.iter().count(), 2); - - $clear.remove(Cookie::named("another")); - assert_eq!($clear.iter().count(), 1); - - $secure.remove(Cookie::named("name")); - assert_eq!($clear.iter().count(), 0); - }}; -} - -#[cfg(test)] -macro_rules! assert_secure_behaviour { - ($clear:expr, $secure:expr) => {{ - $secure.add(Cookie::new("secure", "secure")); - assert!($clear.get("secure").unwrap().value() != "secure"); - assert!($secure.get("secure").unwrap().value() == "secure"); - - let mut cookie = $clear.get("secure").unwrap().clone(); - let new_val = format!("{}l", cookie.value()); - cookie.set_value(new_val); - $clear.add(cookie); - assert!($secure.get("secure").is_none()); - - let mut cookie = $clear.get("secure").unwrap().clone(); - cookie.set_value("foobar"); - $clear.add(cookie); - assert!($secure.get("secure").is_none()); - }}; -} diff --git a/actix-http/src/cookie/secure/mod.rs b/actix-http/src/cookie/secure/mod.rs deleted file mode 100644 index e0fba9733..000000000 --- a/actix-http/src/cookie/secure/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Fork of https://github.com/alexcrichton/cookie-rs -#[macro_use] -mod macros; -mod key; -mod private; -mod signed; - -pub use self::key::*; -pub use self::private::*; -pub use self::signed::*; diff --git a/actix-http/src/cookie/secure/private.rs b/actix-http/src/cookie/secure/private.rs deleted file mode 100644 index f05e23800..000000000 --- a/actix-http/src/cookie/secure/private.rs +++ /dev/null @@ -1,275 +0,0 @@ -use std::str; - -use log::warn; -use ring::aead::{Aad, Algorithm, Nonce, AES_256_GCM}; -use ring::aead::{LessSafeKey, UnboundKey}; -use ring::rand::{SecureRandom, SystemRandom}; - -use super::Key; -use crate::cookie::{Cookie, CookieJar}; - -// Keep these in sync, and keep the key len synced with the `private` docs as -// well as the `KEYS_INFO` const in secure::Key. -static ALGO: &Algorithm = &AES_256_GCM; -const NONCE_LEN: usize = 12; -pub const KEY_LEN: usize = 32; - -/// A child cookie jar that provides authenticated encryption for its cookies. -/// -/// A _private_ child jar signs and encrypts all the cookies added to it and -/// verifies and decrypts cookies retrieved from it. Any cookies stored in a -/// `PrivateJar` are simultaneously assured confidentiality, integrity, and -/// authenticity. In other words, clients cannot discover nor tamper with the -/// contents of a cookie, nor can they fabricate cookie data. -/// -/// This type is only available when the `secure` feature is enabled. -pub struct PrivateJar<'a> { - parent: &'a mut CookieJar, - key: [u8; KEY_LEN], -} - -impl<'a> PrivateJar<'a> { - /// Creates a new child `PrivateJar` with parent `parent` and key `key`. - /// This method is typically called indirectly via the `signed` method of - /// `CookieJar`. - #[doc(hidden)] - pub fn new(parent: &'a mut CookieJar, key: &Key) -> PrivateJar<'a> { - let mut key_array = [0u8; KEY_LEN]; - key_array.copy_from_slice(key.encryption()); - PrivateJar { - parent, - key: key_array, - } - } - - /// Given a sealed value `str` and a key name `name`, where the nonce is - /// prepended to the original value and then both are Base64 encoded, - /// verifies and decrypts the sealed value and returns it. If there's a - /// problem, returns an `Err` with a string describing the issue. - fn unseal(&self, name: &str, value: &str) -> Result { - let mut data = base64::decode(value).map_err(|_| "bad base64 value")?; - if data.len() <= NONCE_LEN { - return Err("length of decoded data is <= NONCE_LEN"); - } - - let ad = Aad::from(name.as_bytes()); - let key = LessSafeKey::new( - UnboundKey::new(&ALGO, &self.key).expect("matching key length"), - ); - let (nonce, mut sealed) = data.split_at_mut(NONCE_LEN); - let nonce = - Nonce::try_assume_unique_for_key(nonce).expect("invalid length of `nonce`"); - let unsealed = key - .open_in_place(nonce, ad, &mut sealed) - .map_err(|_| "invalid key/nonce/value: bad seal")?; - - if let Ok(unsealed_utf8) = str::from_utf8(unsealed) { - Ok(unsealed_utf8.to_string()) - } else { - warn!( - "Private cookie does not have utf8 content! -It is likely the secret key used to encrypt them has been leaked. -Please change it as soon as possible." - ); - Err("bad unsealed utf8") - } - } - - /// Returns a reference to the `Cookie` inside this jar with the name `name` - /// and authenticates and decrypts the cookie's value, returning a `Cookie` - /// with the decrypted value. If the cookie cannot be found, or the cookie - /// fails to authenticate or decrypt, `None` is returned. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// let mut private_jar = jar.private(&key); - /// assert!(private_jar.get("name").is_none()); - /// - /// private_jar.add(Cookie::new("name", "value")); - /// assert_eq!(private_jar.get("name").unwrap().value(), "value"); - /// ``` - pub fn get(&self, name: &str) -> Option> { - if let Some(cookie_ref) = self.parent.get(name) { - let mut cookie = cookie_ref.clone(); - if let Ok(value) = self.unseal(name, cookie.value()) { - cookie.set_value(value); - return Some(cookie); - } - } - - None - } - - /// Adds `cookie` to the parent jar. The cookie's value is encrypted with - /// authenticated encryption assuring confidentiality, integrity, and - /// authenticity. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// jar.private(&key).add(Cookie::new("name", "value")); - /// - /// assert_ne!(jar.get("name").unwrap().value(), "value"); - /// assert_eq!(jar.private(&key).get("name").unwrap().value(), "value"); - /// ``` - pub fn add(&mut self, mut cookie: Cookie<'static>) { - self.encrypt_cookie(&mut cookie); - - // Add the sealed cookie to the parent. - self.parent.add(cookie); - } - - /// Adds an "original" `cookie` to parent jar. The cookie's value is - /// encrypted with authenticated encryption assuring confidentiality, - /// integrity, and authenticity. Adding an original cookie does not affect - /// the [`CookieJar::delta()`](struct.CookieJar.html#method.delta) - /// computation. This method is intended to be used to seed the cookie jar - /// with cookies received from a client's HTTP message. - /// - /// For accurate `delta` computations, this method should not be called - /// after calling `remove`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// jar.private(&key).add_original(Cookie::new("name", "value")); - /// - /// assert_eq!(jar.iter().count(), 1); - /// assert_eq!(jar.delta().count(), 0); - /// ``` - pub fn add_original(&mut self, mut cookie: Cookie<'static>) { - self.encrypt_cookie(&mut cookie); - - // Add the sealed cookie to the parent. - self.parent.add_original(cookie); - } - - /// Encrypts the cookie's value with - /// authenticated encryption assuring confidentiality, integrity, and authenticity. - fn encrypt_cookie(&self, cookie: &mut Cookie<'_>) { - let name = cookie.name().as_bytes(); - let value = cookie.value().as_bytes(); - let data = encrypt_name_value(name, value, &self.key); - - // Base64 encode the nonce and encrypted value. - let sealed_value = base64::encode(&data); - cookie.set_value(sealed_value); - } - - /// Removes `cookie` from the parent jar. - /// - /// For correct removal, the passed in `cookie` must contain the same `path` - /// and `domain` as the cookie that was initially set. - /// - /// See [CookieJar::remove](struct.CookieJar.html#method.remove) for more - /// details. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// let mut private_jar = jar.private(&key); - /// - /// private_jar.add(Cookie::new("name", "value")); - /// assert!(private_jar.get("name").is_some()); - /// - /// private_jar.remove(Cookie::named("name")); - /// assert!(private_jar.get("name").is_none()); - /// ``` - pub fn remove(&mut self, cookie: Cookie<'static>) { - self.parent.remove(cookie); - } -} - -fn encrypt_name_value(name: &[u8], value: &[u8], key: &[u8]) -> Vec { - // Create the `SealingKey` structure. - let unbound = UnboundKey::new(&ALGO, key).expect("matching key length"); - let key = LessSafeKey::new(unbound); - - // Create a vec to hold the [nonce | cookie value | overhead]. - let mut data = vec![0; NONCE_LEN + value.len() + ALGO.tag_len()]; - - // Randomly generate the nonce, then copy the cookie value as input. - let (nonce, in_out) = data.split_at_mut(NONCE_LEN); - let (in_out, tag) = in_out.split_at_mut(value.len()); - in_out.copy_from_slice(value); - - // Randomly generate the nonce into the nonce piece. - SystemRandom::new() - .fill(nonce) - .expect("couldn't random fill nonce"); - let nonce = Nonce::try_assume_unique_for_key(nonce).expect("invalid `nonce` length"); - - // Use cookie's name as associated data to prevent value swapping. - let ad = Aad::from(name); - let ad_tag = key - .seal_in_place_separate_tag(nonce, ad, in_out) - .expect("in-place seal"); - - // Copy the tag into the tag piece. - tag.copy_from_slice(ad_tag.as_ref()); - - // Remove the overhead and return the sealed content. - data -} - -#[cfg(test)] -mod test { - use super::{encrypt_name_value, Cookie, CookieJar, Key}; - - #[test] - fn simple() { - let key = Key::generate(); - let mut jar = CookieJar::new(); - assert_simple_behaviour!(jar, jar.private(&key)); - } - - #[test] - fn private() { - let key = Key::generate(); - let mut jar = CookieJar::new(); - assert_secure_behaviour!(jar, jar.private(&key)); - } - - #[test] - fn non_utf8() { - let key = Key::generate(); - let mut jar = CookieJar::new(); - - let name = "malicious"; - let mut assert_non_utf8 = |value: &[u8]| { - let sealed = encrypt_name_value(name.as_bytes(), value, &key.encryption()); - let encoded = base64::encode(&sealed); - assert_eq!( - jar.private(&key).unseal(name, &encoded), - Err("bad unsealed utf8") - ); - jar.add(Cookie::new(name, encoded)); - assert_eq!(jar.private(&key).get(name), None); - }; - - assert_non_utf8(&[0x72, 0xfb, 0xdf, 0x74]); // rûst in ISO/IEC 8859-1 - - let mut malicious = - String::from(r#"{"id":"abc123??%X","admin":true}"#).into_bytes(); - malicious[8] |= 0b1100_0000; - malicious[9] |= 0b1100_0000; - assert_non_utf8(&malicious); - } -} diff --git a/actix-http/src/cookie/secure/signed.rs b/actix-http/src/cookie/secure/signed.rs deleted file mode 100644 index 64e8d5dda..000000000 --- a/actix-http/src/cookie/secure/signed.rs +++ /dev/null @@ -1,184 +0,0 @@ -use ring::hmac::{self, sign, verify}; - -use super::Key; -use crate::cookie::{Cookie, CookieJar}; - -// Keep these in sync, and keep the key len synced with the `signed` docs as -// well as the `KEYS_INFO` const in secure::Key. -static HMAC_DIGEST: hmac::Algorithm = hmac::HMAC_SHA256; -const BASE64_DIGEST_LEN: usize = 44; -pub const KEY_LEN: usize = 32; - -/// A child cookie jar that authenticates its cookies. -/// -/// A _signed_ child jar signs all the cookies added to it and verifies cookies -/// retrieved from it. Any cookies stored in a `SignedJar` are assured integrity -/// and authenticity. In other words, clients cannot tamper with the contents of -/// a cookie nor can they fabricate cookie values, but the data is visible in -/// plaintext. -/// -/// This type is only available when the `secure` feature is enabled. -pub struct SignedJar<'a> { - parent: &'a mut CookieJar, - key: hmac::Key, -} - -impl<'a> SignedJar<'a> { - /// Creates a new child `SignedJar` with parent `parent` and key `key`. This - /// method is typically called indirectly via the `signed` method of - /// `CookieJar`. - #[doc(hidden)] - pub fn new(parent: &'a mut CookieJar, key: &Key) -> SignedJar<'a> { - SignedJar { - parent, - key: hmac::Key::new(HMAC_DIGEST, key.signing()), - } - } - - /// Given a signed value `str` where the signature is prepended to `value`, - /// verifies the signed value and returns it. If there's a problem, returns - /// an `Err` with a string describing the issue. - fn verify(&self, cookie_value: &str) -> Result { - if cookie_value.len() < BASE64_DIGEST_LEN { - return Err("length of value is <= BASE64_DIGEST_LEN"); - } - - let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN); - let sig = base64::decode(digest_str).map_err(|_| "bad base64 digest")?; - - verify(&self.key, value.as_bytes(), &sig) - .map(|_| value.to_string()) - .map_err(|_| "value did not verify") - } - - /// Returns a reference to the `Cookie` inside this jar with the name `name` - /// and verifies the authenticity and integrity of the cookie's value, - /// returning a `Cookie` with the authenticated value. If the cookie cannot - /// be found, or the cookie fails to verify, `None` is returned. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// let mut signed_jar = jar.signed(&key); - /// assert!(signed_jar.get("name").is_none()); - /// - /// signed_jar.add(Cookie::new("name", "value")); - /// assert_eq!(signed_jar.get("name").unwrap().value(), "value"); - /// ``` - pub fn get(&self, name: &str) -> Option> { - if let Some(cookie_ref) = self.parent.get(name) { - let mut cookie = cookie_ref.clone(); - if let Ok(value) = self.verify(cookie.value()) { - cookie.set_value(value); - return Some(cookie); - } - } - - None - } - - /// Adds `cookie` to the parent jar. The cookie's value is signed assuring - /// integrity and authenticity. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// jar.signed(&key).add(Cookie::new("name", "value")); - /// - /// assert_ne!(jar.get("name").unwrap().value(), "value"); - /// assert!(jar.get("name").unwrap().value().contains("value")); - /// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value"); - /// ``` - pub fn add(&mut self, mut cookie: Cookie<'static>) { - self.sign_cookie(&mut cookie); - self.parent.add(cookie); - } - - /// Adds an "original" `cookie` to this jar. The cookie's value is signed - /// assuring integrity and authenticity. Adding an original cookie does not - /// affect the [`CookieJar::delta()`](struct.CookieJar.html#method.delta) - /// computation. This method is intended to be used to seed the cookie jar - /// with cookies received from a client's HTTP message. - /// - /// For accurate `delta` computations, this method should not be called - /// after calling `remove`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// jar.signed(&key).add_original(Cookie::new("name", "value")); - /// - /// assert_eq!(jar.iter().count(), 1); - /// assert_eq!(jar.delta().count(), 0); - /// ``` - pub fn add_original(&mut self, mut cookie: Cookie<'static>) { - self.sign_cookie(&mut cookie); - self.parent.add_original(cookie); - } - - /// Signs the cookie's value assuring integrity and authenticity. - fn sign_cookie(&self, cookie: &mut Cookie<'_>) { - let digest = sign(&self.key, cookie.value().as_bytes()); - let mut new_value = base64::encode(digest.as_ref()); - new_value.push_str(cookie.value()); - cookie.set_value(new_value); - } - - /// Removes `cookie` from the parent jar. - /// - /// For correct removal, the passed in `cookie` must contain the same `path` - /// and `domain` as the cookie that was initially set. - /// - /// See [CookieJar::remove](struct.CookieJar.html#method.remove) for more - /// details. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// let mut signed_jar = jar.signed(&key); - /// - /// signed_jar.add(Cookie::new("name", "value")); - /// assert!(signed_jar.get("name").is_some()); - /// - /// signed_jar.remove(Cookie::named("name")); - /// assert!(signed_jar.get("name").is_none()); - /// ``` - pub fn remove(&mut self, cookie: Cookie<'static>) { - self.parent.remove(cookie); - } -} - -#[cfg(test)] -mod test { - use super::{Cookie, CookieJar, Key}; - - #[test] - fn simple() { - let key = Key::generate(); - let mut jar = CookieJar::new(); - assert_simple_behaviour!(jar, jar.signed(&key)); - } - - #[test] - fn private() { - let key = Key::generate(); - let mut jar = CookieJar::new(); - assert_secure_behaviour!(jar, jar.signed(&key)); - } -} diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index f0a4b70bc..e93c077af 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -1,4 +1,5 @@ //! Error and Result module + use std::cell::RefCell; use std::io::Write; use std::str::Utf8Error; @@ -7,7 +8,7 @@ use std::{fmt, io, result}; use actix_codec::{Decoder, Encoder}; pub use actix_threadpool::BlockingError; -use actix_utils::framed::DispatcherError as FramedDispatcherError; +use actix_utils::dispatcher::DispatcherError as FramedDispatcherError; use actix_utils::timeout::TimeoutError; use bytes::BytesMut; use derive_more::{Display, From}; @@ -452,10 +453,10 @@ impl ResponseError for ContentTypeError { } } -impl ResponseError for FramedDispatcherError +impl + Decoder, I> ResponseError for FramedDispatcherError where E: fmt::Debug + fmt::Display, - ::Error: fmt::Debug, + >::Error: fmt::Debug, ::Error: fmt::Debug, { } @@ -964,7 +965,6 @@ impl ResponseError for actix::actors::resolver::ResolverError {} mod tests { use super::*; use http::{Error as HttpError, StatusCode}; - use httparse; use std::io; #[test] diff --git a/actix-http/src/extensions.rs b/actix-http/src/extensions.rs index 4e3918537..96e01767b 100644 --- a/actix-http/src/extensions.rs +++ b/actix-http/src/extensions.rs @@ -72,7 +72,7 @@ impl fmt::Debug for Extensions { #[cfg(test)] mod tests { use super::*; - + #[test] fn test_remove() { let mut map = Extensions::new(); diff --git a/actix-http/src/h1/client.rs b/actix-http/src/h1/client.rs index bcfc18cde..2e0103409 100644 --- a/actix-http/src/h1/client.rs +++ b/actix-http/src/h1/client.rs @@ -173,13 +173,12 @@ impl Decoder for ClientPayloadCodec { } } -impl Encoder for ClientCodec { - type Item = Message<(RequestHeadType, BodySize)>; +impl Encoder> for ClientCodec { type Error = io::Error; fn encode( &mut self, - item: Self::Item, + item: Message<(RequestHeadType, BodySize)>, dst: &mut BytesMut, ) -> Result<(), Self::Error> { match item { diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index de2af9ee7..036f16670 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -144,13 +144,12 @@ impl Decoder for Codec { } } -impl Encoder for Codec { - type Item = Message<(Response<()>, BodySize)>; +impl Encoder, BodySize)>> for Codec { type Error = io::Error; fn encode( &mut self, - item: Self::Item, + item: Message<(Response<()>, BodySize)>, dst: &mut BytesMut, ) -> Result<(), Self::Error> { match item { diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index c9d3edf33..8e891dc5c 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -1,7 +1,6 @@ use std::convert::TryFrom; use std::io; use std::marker::PhantomData; -use std::mem::MaybeUninit; use std::task::Poll; use actix_codec::Decoder; @@ -46,7 +45,7 @@ impl Decoder for MessageDecoder { pub(crate) enum PayloadLength { Payload(PayloadType), - Upgrade, + UpgradeWebSocket, None, } @@ -65,7 +64,7 @@ pub(crate) trait MessageType: Sized { raw_headers: &[HeaderIndex], ) -> Result { let mut ka = None; - let mut has_upgrade = false; + let mut has_upgrade_websocket = false; let mut expect = false; let mut chunked = false; let mut content_length = None; @@ -77,12 +76,14 @@ pub(crate) trait MessageType: Sized { let name = HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap(); - // Unsafe: httparse check header value for valid utf-8 + // SAFETY: httparse already checks header value is only visible ASCII bytes + // from_maybe_shared_unchecked contains debug assertions so they are omitted here let value = unsafe { HeaderValue::from_maybe_shared_unchecked( slice.slice(idx.value.0..idx.value.1), ) }; + match name { header::CONTENT_LENGTH => { if let Ok(s) = value.to_str() { @@ -124,12 +125,9 @@ pub(crate) trait MessageType: Sized { }; } header::UPGRADE => { - has_upgrade = true; - // check content-length, some clients (dart) - // sends "content-length: 0" with websocket upgrade if let Ok(val) = value.to_str().map(|val| val.trim()) { if val.eq_ignore_ascii_case("websocket") { - content_length = None; + has_upgrade_websocket = true; } } } @@ -156,13 +154,13 @@ pub(crate) trait MessageType: Sized { Ok(PayloadLength::Payload(PayloadType::Payload( PayloadDecoder::chunked(), ))) + } else if has_upgrade_websocket { + Ok(PayloadLength::UpgradeWebSocket) } else if let Some(len) = content_length { // Content-Length Ok(PayloadLength::Payload(PayloadType::Payload( PayloadDecoder::length(len), ))) - } else if has_upgrade { - Ok(PayloadLength::Upgrade) } else { Ok(PayloadLength::None) } @@ -184,16 +182,11 @@ impl MessageType for Request { &mut self.head_mut().headers } - #[allow(clippy::uninit_assumed_init)] fn decode(src: &mut BytesMut) -> Result, ParseError> { - // Unsafe: we read only this data only after httparse parses headers into. - // performance bump for pipeline benchmarks. - let mut headers: [HeaderIndex; MAX_HEADERS] = - unsafe { MaybeUninit::uninit().assume_init() }; + let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; let (len, method, uri, ver, h_len) = { - let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = - unsafe { MaybeUninit::uninit().assume_init() }; + let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; let mut req = httparse::Request::new(&mut parsed); match req.parse(src)? { @@ -222,7 +215,7 @@ impl MessageType for Request { // payload decoder let decoder = match length { PayloadLength::Payload(pl) => pl, - PayloadLength::Upgrade => { + PayloadLength::UpgradeWebSocket => { // upgrade(websocket) PayloadType::Stream(PayloadDecoder::eof()) } @@ -260,16 +253,11 @@ impl MessageType for ResponseHead { &mut self.headers } - #[allow(clippy::uninit_assumed_init)] fn decode(src: &mut BytesMut) -> Result, ParseError> { - // Unsafe: we read only this data only after httparse parses headers into. - // performance bump for pipeline benchmarks. - let mut headers: [HeaderIndex; MAX_HEADERS] = - unsafe { MaybeUninit::uninit().assume_init() }; + let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; let (len, ver, status, h_len) = { - let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = - unsafe { MaybeUninit::uninit().assume_init() }; + let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; let mut res = httparse::Response::new(&mut parsed); match res.parse(src)? { @@ -324,6 +312,17 @@ pub(crate) struct HeaderIndex { pub(crate) value: (usize, usize), } +pub(crate) const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex { + name: (0, 0), + value: (0, 0), +}; + +pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = + [EMPTY_HEADER_INDEX; MAX_HEADERS]; + +pub(crate) const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] = + [httparse::EMPTY_HEADER; MAX_HEADERS]; + impl HeaderIndex { pub(crate) fn record( bytes: &[u8], @@ -655,10 +654,7 @@ mod tests { } fn is_unhandled(&self) -> bool { - match self { - PayloadType::Stream(_) => true, - _ => false, - } + matches!(self, PayloadType::Stream(_)) } } @@ -670,10 +666,7 @@ mod tests { } } fn eof(&self) -> bool { - match *self { - PayloadItem::Eof => true, - _ => false, - } + matches!(*self, PayloadItem::Eof) } } @@ -979,7 +972,7 @@ mod tests { unreachable!("Error"); } - // type in chunked + // intentional typo in "chunked" let mut buf = BytesMut::from( "GET /test HTTP/1.1\r\n\ transfer-encoding: chnked\r\n\r\n", @@ -1040,7 +1033,7 @@ mod tests { } #[test] - fn test_http_request_upgrade() { + fn test_http_request_upgrade_websocket() { let mut buf = BytesMut::from( "GET /test HTTP/1.1\r\n\ connection: upgrade\r\n\ @@ -1054,6 +1047,26 @@ mod tests { assert!(pl.is_unhandled()); } + #[test] + fn test_http_request_upgrade_h2c() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + connection: upgrade, http2-settings\r\n\ + upgrade: h2c\r\n\ + http2-settings: dummy\r\n\r\n", + ); + let mut reader = MessageDecoder::::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + // `connection: upgrade, http2-settings` doesn't work properly.. + // see MessageType::set_headers(). + // + // The line below should be: + // assert_eq!(req.head().connection_type(), ConnectionType::Upgrade); + assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); + assert!(req.upgrade()); + assert!(!pl.is_unhandled()); + } + #[test] fn test_http_request_parser_utf8() { let mut buf = BytesMut::from( diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index e16d3536f..7c4de9707 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -132,19 +132,11 @@ where B: MessageBody, { fn is_empty(&self) -> bool { - if let State::None = self { - true - } else { - false - } + matches!(self, State::None) } fn is_call(&self) -> bool { - if let State::ServiceCall(_) = self { - true - } else { - false - } + matches!(self, State::ServiceCall(_)) } } enum PollResponse { @@ -156,14 +148,8 @@ enum PollResponse { impl PartialEq for PollResponse { fn eq(&self, other: &PollResponse) -> bool { match self { - PollResponse::DrainWriteBuf => match other { - PollResponse::DrainWriteBuf => true, - _ => false, - }, - PollResponse::DoNothing => match other { - PollResponse::DoNothing => true, - _ => false, - }, + PollResponse::DrainWriteBuf => matches!(other, PollResponse::DrainWriteBuf), + PollResponse::DoNothing => matches!(other, PollResponse::DoNothing), _ => false, } } @@ -328,11 +314,15 @@ where Poll::Ready(Err(err)) => return Err(DispatchError::Io(err)), } } + if written == write_buf.len() { + // SAFETY: setting length to 0 is safe + // skips one length check vs truncate unsafe { write_buf.set_len(0) } } else { write_buf.advance(written); } + Ok(false) } @@ -861,7 +851,14 @@ where T: AsyncRead + Unpin, { let mut read_some = false; + loop { + // If buf is full return but do not disconnect since + // there is more reading to be done + if buf.len() >= HW_BUFFER_SIZE { + return Ok(Some(false)); + } + let remaining = buf.capacity() - buf.len(); if remaining < LW_BUFFER_SIZE { buf.reserve(HW_BUFFER_SIZE - remaining); diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index eb8c337dd..e16b4c3dc 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -129,89 +129,133 @@ pub(crate) trait MessageType: Sized { .chain(extra_headers.inner.iter()); // write headers - let mut pos = 0; + let mut has_date = false; - let mut remaining = dst.capacity() - dst.len(); + let mut buf = dst.bytes_mut().as_mut_ptr() as *mut u8; + let mut remaining = dst.capacity() - dst.len(); + + // tracks bytes written since last buffer resize + // since buf is a raw pointer to a bytes container storage but is written to without the + // container's knowledge, this is used to sync the containers cursor after data is written + let mut pos = 0; + for (key, value) in headers { match *key { CONNECTION => continue, TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue, - DATE => { - has_date = true; - } + DATE => has_date = true, _ => (), } + let k = key.as_str().as_bytes(); + let k_len = k.len(); + match value { map::Value::One(ref val) => { let v = val.as_ref(); let v_len = v.len(); - let k_len = k.len(); + + // key length + value length + colon + space + \r\n let len = k_len + v_len + 4; + if len > remaining { + // not enough room in buffer for this header; reserve more space + + // SAFETY: all the bytes written up to position "pos" are initialized + // the written byte count and pointer advancement are kept in sync unsafe { dst.advance_mut(pos); } + pos = 0; dst.reserve(len * 2); remaining = dst.capacity() - dst.len(); + + // re-assign buf raw pointer since it's possible that the buffer was + // reallocated and/or resized buf = dst.bytes_mut().as_mut_ptr() as *mut u8; } - // use upper Camel-Case + + // SAFETY: on each write, it is enough to ensure that the advancement of the + // cursor matches the number of bytes written unsafe { + // use upper Camel-Case if camel_case { write_camel_case(k, from_raw_parts_mut(buf, k_len)) } else { write_data(k, buf, k_len) } + buf = buf.add(k_len); + write_data(b": ", buf, 2); buf = buf.add(2); + write_data(v, buf, v_len); buf = buf.add(v_len); + write_data(b"\r\n", buf, 2); buf = buf.add(2); - pos += len; - remaining -= len; } + + pos += len; + remaining -= len; } + map::Value::Multi(ref vec) => { for val in vec { let v = val.as_ref(); let v_len = v.len(); - let k_len = k.len(); let len = k_len + v_len + 4; + if len > remaining { + // SAFETY: all the bytes written up to position "pos" are initialized + // the written byte count and pointer advancement are kept in sync unsafe { dst.advance_mut(pos); } pos = 0; dst.reserve(len * 2); remaining = dst.capacity() - dst.len(); + + // re-assign buf raw pointer since it's possible that the buffer was + // reallocated and/or resized buf = dst.bytes_mut().as_mut_ptr() as *mut u8; } - // use upper Camel-Case + + // SAFETY: on each write, it is enough to ensure that the advancement of + // the cursor matches the number of bytes written unsafe { if camel_case { write_camel_case(k, from_raw_parts_mut(buf, k_len)); } else { write_data(k, buf, k_len); } + buf = buf.add(k_len); + write_data(b": ", buf, 2); buf = buf.add(2); + write_data(v, buf, v_len); buf = buf.add(v_len); + write_data(b"\r\n", buf, 2); buf = buf.add(2); }; + pos += len; remaining -= len; } } } } + + // final cursor synchronization with the bytes container + // + // SAFETY: all the bytes written up to position "pos" are initialized + // the written byte count and pointer advancement are kept in sync unsafe { dst.advance_mut(pos); } @@ -477,7 +521,10 @@ impl<'a> io::Write for Writer<'a> { } } +/// # Safety +/// Callers must ensure that the given length matches given value length. unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) { + debug_assert_eq!(value.len(), len); copy_nonoverlapping(value.as_ptr(), buf, len); } diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index 4d1a1dc1b..6aafd4089 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -98,7 +98,7 @@ mod openssl { use super::*; use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream}; - use actix_tls::{openssl::HandshakeError, SslError}; + use actix_tls::{openssl::HandshakeError, TlsError}; impl H1Service, S, B, X, U> where @@ -126,19 +126,19 @@ mod openssl { Config = (), Request = TcpStream, Response = (), - Error = SslError, DispatchError>, + Error = TlsError, DispatchError>, InitError = (), > { pipeline_factory( Acceptor::new(acceptor) - .map_err(SslError::Ssl) + .map_err(TlsError::Tls) .map_init_err(|_| panic!()), ) .and_then(|io: SslStream| { let peer_addr = io.get_ref().peer_addr().ok(); ok((io, peer_addr)) }) - .and_then(self.map_err(SslError::Service)) + .and_then(self.map_err(TlsError::Service)) } } } @@ -147,7 +147,7 @@ mod openssl { mod rustls { use super::*; use actix_tls::rustls::{Acceptor, ServerConfig, TlsStream}; - use actix_tls::SslError; + use actix_tls::TlsError; use std::{fmt, io}; impl H1Service, S, B, X, U> @@ -176,19 +176,19 @@ mod rustls { Config = (), Request = TcpStream, Response = (), - Error = SslError, + Error = TlsError, InitError = (), > { pipeline_factory( Acceptor::new(config) - .map_err(SslError::Ssl) + .map_err(TlsError::Tls) .map_init_err(|_| panic!()), ) .and_then(|io: TlsStream| { let peer_addr = io.get_ref().0.peer_addr().ok(); ok((io, peer_addr)) }) - .and_then(self.map_err(SslError::Service)) + .and_then(self.map_err(TlsError::Service)) } } } @@ -548,10 +548,12 @@ where } #[doc(hidden)] +#[pin_project::pin_project] pub struct OneRequestServiceResponse where T: AsyncRead + AsyncWrite + Unpin, { + #[pin] framed: Option>, } @@ -562,16 +564,18 @@ where type Output = Result<(Request, Framed), ParseError>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - match self.framed.as_mut().unwrap().next_item(cx) { - Poll::Ready(Some(Ok(req))) => match req { + let this = self.as_mut().project(); + + match ready!(this.framed.as_pin_mut().unwrap().next_item(cx)) { + Some(Ok(req)) => match req { Message::Item(req) => { - Poll::Ready(Ok((req, self.framed.take().unwrap()))) + let mut this = self.as_mut().project(); + Poll::Ready(Ok((req, this.framed.take().unwrap()))) } Message::Chunk(_) => unreachable!("Something is wrong"), }, - Poll::Ready(Some(Err(err))) => Poll::Ready(Err(err)), - Poll::Ready(None) => Poll::Ready(Err(ParseError::Incomplete)), - Poll::Pending => Poll::Pending, + Some(Err(err)) => Poll::Ready(Err(err)), + None => Poll::Ready(Err(ParseError::Incomplete)), } } } diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index c44925c7a..9e9c57137 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -9,12 +9,13 @@ use crate::error::Error; use crate::h1::{Codec, Message}; use crate::response::Response; -/// Send http/1 response +/// Send HTTP/1 response #[pin_project::pin_project] pub struct SendResponse { res: Option, BodySize)>>, #[pin] body: Option>, + #[pin] framed: Option>, } @@ -35,23 +36,30 @@ where impl Future for SendResponse where - T: AsyncRead + AsyncWrite, + T: AsyncRead + AsyncWrite + Unpin, 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 mut this = self.project(); + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.as_mut().project(); let mut body_done = this.body.is_none(); loop { let mut body_ready = !body_done; - let framed = this.framed.as_mut().unwrap(); // send body if this.res.is_none() && body_ready { - while body_ready && !body_done && !framed.is_write_buf_full() { + while body_ready + && !body_done + && !this + .framed + .as_ref() + .as_pin_ref() + .unwrap() + .is_write_buf_full() + { match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx)? { Poll::Ready(item) => { // body is done when item is None @@ -59,6 +67,7 @@ where if body_done { let _ = this.body.take(); } + let framed = this.framed.as_mut().as_pin_mut().unwrap(); framed.write(Message::Chunk(item))?; } Poll::Pending => body_ready = false, @@ -66,6 +75,8 @@ where } } + let framed = this.framed.as_mut().as_pin_mut().unwrap(); + // flush write buffer if !framed.is_write_buf_empty() { match framed.flush(cx)? { @@ -96,6 +107,9 @@ where break; } } - Poll::Ready(Ok(this.framed.take().unwrap())) + + let framed = this.framed.take().unwrap(); + + Poll::Ready(Ok(framed)) } } diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 33fb3a814..daa651f4d 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -227,9 +227,11 @@ where if !has_date { let mut bytes = BytesMut::with_capacity(29); self.config.set_date_header(&mut bytes); - res.headers_mut().insert(DATE, unsafe { - HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) - }); + res.headers_mut().insert( + DATE, + // SAFETY: serialized date-times are known ASCII strings + unsafe { HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) }, + ); } res @@ -303,55 +305,59 @@ where } } }, - ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => loop { + ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => { loop { - if let Some(ref mut buffer) = this.buffer { - match stream.poll_capacity(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(None) => return Poll::Ready(()), - Poll::Ready(Some(Ok(cap))) => { - let len = buffer.len(); - let bytes = buffer.split_to(std::cmp::min(cap, len)); + loop { + if let Some(ref mut buffer) = this.buffer { + match stream.poll_capacity(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(None) => return Poll::Ready(()), + Poll::Ready(Some(Ok(cap))) => { + let len = buffer.len(); + let bytes = buffer.split_to(std::cmp::min(cap, len)); - if let Err(e) = stream.send_data(bytes, false) { + if let Err(e) = stream.send_data(bytes, false) { + warn!("{:?}", e); + return Poll::Ready(()); + } else if !buffer.is_empty() { + let cap = + std::cmp::min(buffer.len(), CHUNK_SIZE); + stream.reserve_capacity(cap); + } else { + this.buffer.take(); + } + } + Poll::Ready(Some(Err(e))) => { warn!("{:?}", e); return Poll::Ready(()); - } else if !buffer.is_empty() { - let cap = std::cmp::min(buffer.len(), CHUNK_SIZE); - stream.reserve_capacity(cap); - } else { - this.buffer.take(); } } - Poll::Ready(Some(Err(e))) => { - warn!("{:?}", e); - return Poll::Ready(()); - } - } - } else { - 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) { - warn!("{:?}", e); + } else { + 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) + { + warn!("{:?}", e); + } + return Poll::Ready(()); + } + Poll::Ready(Some(Ok(chunk))) => { + stream.reserve_capacity(std::cmp::min( + chunk.len(), + CHUNK_SIZE, + )); + *this.buffer = Some(chunk); + } + Poll::Ready(Some(Err(e))) => { + error!("Response payload stream error: {:?}", e); + return Poll::Ready(()); } - return Poll::Ready(()); - } - Poll::Ready(Some(Ok(chunk))) => { - stream.reserve_capacity(std::cmp::min( - chunk.len(), - CHUNK_SIZE, - )); - *this.buffer = Some(chunk); - } - Poll::Ready(Some(Err(e))) => { - error!("Response payload stream error: {:?}", e); - return Poll::Ready(()); } } } } - }, + } } } } diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index eef5dd02c..6b5620e02 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -97,7 +97,7 @@ where mod openssl { use actix_service::{fn_factory, fn_service}; use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream}; - use actix_tls::{openssl::HandshakeError, SslError}; + use actix_tls::{openssl::HandshakeError, TlsError}; use super::*; @@ -117,12 +117,12 @@ mod openssl { Config = (), Request = TcpStream, Response = (), - Error = SslError, DispatchError>, + Error = TlsError, DispatchError>, InitError = S::InitError, > { pipeline_factory( Acceptor::new(acceptor) - .map_err(SslError::Ssl) + .map_err(TlsError::Tls) .map_init_err(|_| panic!()), ) .and_then(fn_factory(|| { @@ -131,7 +131,7 @@ mod openssl { ok((io, peer_addr)) })) })) - .and_then(self.map_err(SslError::Service)) + .and_then(self.map_err(TlsError::Service)) } } } @@ -140,7 +140,7 @@ mod openssl { mod rustls { use super::*; use actix_tls::rustls::{Acceptor, ServerConfig, TlsStream}; - use actix_tls::SslError; + use actix_tls::TlsError; use std::io; impl H2Service, S, B> @@ -159,7 +159,7 @@ mod rustls { Config = (), Request = TcpStream, Response = (), - Error = SslError, + Error = TlsError, InitError = S::InitError, > { let protos = vec!["h2".to_string().into()]; @@ -167,7 +167,7 @@ mod rustls { pipeline_factory( Acceptor::new(config) - .map_err(SslError::Ssl) + .map_err(TlsError::Tls) .map_init_err(|_| panic!()), ) .and_then(fn_factory(|| { @@ -176,7 +176,7 @@ mod rustls { ok((io, peer_addr)) })) })) - .and_then(self.map_err(SslError::Service)) + .and_then(self.map_err(TlsError::Service)) } } } diff --git a/actix-http/src/header/common/allow.rs b/actix-http/src/header/common/allow.rs index 432cc00d5..88c21763c 100644 --- a/actix-http/src/header/common/allow.rs +++ b/actix-http/src/header/common/allow.rs @@ -1,5 +1,5 @@ -use http::Method; use http::header; +use http::Method; header! { /// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1) diff --git a/actix-http/src/header/common/content_disposition.rs b/actix-http/src/header/common/content_disposition.rs index d65933901..37da830ca 100644 --- a/actix-http/src/header/common/content_disposition.rs +++ b/actix-http/src/header/common/content_disposition.rs @@ -283,11 +283,11 @@ impl DispositionParam { /// Some("\u{1f600}.svg".as_bytes())); /// ``` /// -/// # WARN +/// # Security Note +/// /// If "filename" parameter is supplied, do not use the file name blindly, check and possibly /// change to match local file system conventions if applicable, and do not use directory path -/// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3) -/// . +/// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3). #[derive(Clone, Debug, PartialEq)] pub struct ContentDisposition { /// The disposition type @@ -387,26 +387,17 @@ impl ContentDisposition { /// Returns `true` if it is [`Inline`](DispositionType::Inline). pub fn is_inline(&self) -> bool { - match self.disposition { - DispositionType::Inline => true, - _ => false, - } + matches!(self.disposition, DispositionType::Inline) } /// Returns `true` if it is [`Attachment`](DispositionType::Attachment). pub fn is_attachment(&self) -> bool { - match self.disposition { - DispositionType::Attachment => true, - _ => false, - } + matches!(self.disposition, DispositionType::Attachment) } /// Returns `true` if it is [`FormData`](DispositionType::FormData). pub fn is_form_data(&self) -> bool { - match self.disposition { - DispositionType::FormData => true, - _ => false, - } + matches!(self.disposition, DispositionType::FormData) } /// Returns `true` if it is [`Ext`](DispositionType::Ext) and the `disp_type` matches. diff --git a/actix-http/src/header/common/mod.rs b/actix-http/src/header/common/mod.rs index 08950ea8b..83489b864 100644 --- a/actix-http/src/header/common/mod.rs +++ b/actix-http/src/header/common/mod.rs @@ -9,11 +9,13 @@ pub use self::accept_charset::AcceptCharset; //pub use self::accept_encoding::AcceptEncoding; -pub use self::accept_language::AcceptLanguage; pub use self::accept::Accept; +pub use self::accept_language::AcceptLanguage; pub use self::allow::Allow; pub use self::cache_control::{CacheControl, CacheDirective}; -pub use self::content_disposition::{ContentDisposition, DispositionType, DispositionParam}; +pub use self::content_disposition::{ + ContentDisposition, DispositionParam, DispositionType, +}; pub use self::content_language::ContentLanguage; pub use self::content_range::{ContentRange, ContentRangeSpec}; pub use self::content_type::ContentType; @@ -47,7 +49,7 @@ macro_rules! __hyper__deref { &mut self.0 } } - } + }; } #[doc(hidden)] @@ -74,8 +76,8 @@ macro_rules! test_header { ($id:ident, $raw:expr) => { #[test] fn $id() { - use $crate::test; use super::*; + use $crate::test; let raw = $raw; let a: Vec> = raw.iter().map(|x| x.to_vec()).collect(); @@ -118,7 +120,7 @@ macro_rules! test_header { // Test formatting if typed.is_some() { let raw = &($raw)[..]; - let mut iter = raw.iter().map(|b|str::from_utf8(&b[..]).unwrap()); + let mut iter = raw.iter().map(|b| str::from_utf8(&b[..]).unwrap()); let mut joined = String::new(); joined.push_str(iter.next().unwrap()); for s in iter { @@ -128,7 +130,7 @@ macro_rules! test_header { assert_eq!(format!("{}", typed.unwrap()), joined); } } - } + }; } #[macro_export] @@ -330,11 +332,10 @@ macro_rules! header { }; } - mod accept_charset; //mod accept_encoding; -mod accept_language; mod accept; +mod accept_language; mod allow; mod cache_control; mod content_disposition; diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index 0db26ceb0..46fb31a62 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -148,10 +148,7 @@ impl ContentEncoding { #[inline] /// Is the content compressed? pub fn is_compression(self) -> bool { - match self { - ContentEncoding::Identity | ContentEncoding::Auto => false, - _ => true, - } + matches!(self, ContentEncoding::Identity | ContentEncoding::Auto) } #[inline] diff --git a/actix-http/src/httpmessage.rs b/actix-http/src/httpmessage.rs index e1c4136b0..471fbbcdc 100644 --- a/actix-http/src/httpmessage.rs +++ b/actix-http/src/httpmessage.rs @@ -167,7 +167,6 @@ where mod tests { use bytes::Bytes; use encoding_rs::ISO_8859_2; - use mime; use super::*; use crate::test::TestRequest; diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 9f615a129..fab91be2b 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -1,11 +1,13 @@ //! Basic http primitives for actix-net framework. -#![warn(rust_2018_idioms, warnings)] + +#![deny(rust_2018_idioms)] #![allow( clippy::type_complexity, clippy::too_many_arguments, clippy::new_without_default, clippy::borrow_interior_mutable_const )] +#![allow(clippy::manual_strip)] // Allow this to keep MSRV(1.42). #[macro_use] extern crate log; @@ -32,7 +34,7 @@ mod response; mod service; mod time_parser; -pub mod cookie; +pub use cookie; pub mod error; pub mod h1; pub mod h2; diff --git a/actix-http/src/macros.rs b/actix-http/src/macros.rs index b970b14f2..8973aa39b 100644 --- a/actix-http/src/macros.rs +++ b/actix-http/src/macros.rs @@ -38,7 +38,7 @@ macro_rules! downcast { /// 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 + // 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 @@ -48,10 +48,11 @@ macro_rules! downcast { 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 + // 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 @@ -86,7 +87,7 @@ mod tests { let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast"); let body = &mut resp_body.downcast_mut::().unwrap(); - body.push_str("!"); + body.push('!'); let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast!"); let not_body = resp_body.downcast_ref::<()>(); diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 9086212f1..2def67168 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -877,7 +877,7 @@ mod tests { .domain("www.rust-lang.org") .path("/test") .http_only(true) - .max_age_time(time::Duration::days(1)) + .max_age(time::Duration::days(1)) .finish(), ) .del_cookie(&cookies[1]) diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index 94cdbc828..9ee579702 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -195,7 +195,7 @@ where mod openssl { use super::*; use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream}; - use actix_tls::{openssl::HandshakeError, SslError}; + use actix_tls::{openssl::HandshakeError, TlsError}; impl HttpService, S, B, X, U> where @@ -226,12 +226,12 @@ mod openssl { Config = (), Request = TcpStream, Response = (), - Error = SslError, DispatchError>, + Error = TlsError, DispatchError>, InitError = (), > { pipeline_factory( Acceptor::new(acceptor) - .map_err(SslError::Ssl) + .map_err(TlsError::Tls) .map_init_err(|_| panic!()), ) .and_then(|io: SslStream| { @@ -247,7 +247,7 @@ mod openssl { let peer_addr = io.get_ref().peer_addr().ok(); ok((io, proto, peer_addr)) }) - .and_then(self.map_err(SslError::Service)) + .and_then(self.map_err(TlsError::Service)) } } } @@ -256,7 +256,7 @@ mod openssl { mod rustls { use super::*; use actix_tls::rustls::{Acceptor, ServerConfig, Session, TlsStream}; - use actix_tls::SslError; + use actix_tls::TlsError; use std::io; impl HttpService, S, B, X, U> @@ -288,7 +288,7 @@ mod rustls { Config = (), Request = TcpStream, Response = (), - Error = SslError, + Error = TlsError, InitError = (), > { let protos = vec!["h2".to_string().into(), "http/1.1".to_string().into()]; @@ -296,7 +296,7 @@ mod rustls { pipeline_factory( Acceptor::new(config) - .map_err(SslError::Ssl) + .map_err(TlsError::Tls) .map_init_err(|_| panic!()), ) .and_then(|io: TlsStream| { @@ -312,7 +312,7 @@ mod rustls { let peer_addr = io.get_ref().0.peer_addr().ok(); ok((io, proto, peer_addr)) }) - .and_then(self.map_err(SslError::Service)) + .and_then(self.map_err(TlsError::Service)) } } } diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs index 061ba610f..b79f5a73c 100644 --- a/actix-http/src/test.rs +++ b/actix-http/src/test.rs @@ -1,6 +1,5 @@ //! Test Various helpers for Actix applications to use during testing. use std::convert::TryFrom; -use std::fmt::Write as FmtWrite; use std::io::{self, Read, Write}; use std::pin::Pin; use std::str::FromStr; @@ -10,9 +9,8 @@ use actix_codec::{AsyncRead, AsyncWrite}; use bytes::{Bytes, BytesMut}; use http::header::{self, HeaderName, HeaderValue}; use http::{Error as HttpError, Method, Uri, Version}; -use percent_encoding::percent_encode; -use crate::cookie::{Cookie, CookieJar, USERINFO}; +use crate::cookie::{Cookie, CookieJar}; use crate::header::HeaderMap; use crate::header::{Header, IntoHeaderValue}; use crate::payload::Payload; @@ -163,17 +161,17 @@ impl TestRequest { head.version = inner.version; head.headers = inner.headers; - let mut cookie = String::new(); - for c in inner.cookies.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO); - let value = percent_encode(c.value().as_bytes(), USERINFO); - let _ = write!(&mut cookie, "; {}={}", name, value); - } + let cookie: String = inner + .cookies + .delta() + // ensure only name=value is written to cookie header + .map(|c| Cookie::new(c.name(), c.value()).encoded().to_string()) + .collect::>() + .join("; "); + if !cookie.is_empty() { - head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); + head.headers + .insert(header::COOKIE, HeaderValue::from_str(&cookie).unwrap()); } req diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index 733976a78..7c9628b1a 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -91,8 +91,7 @@ impl Codec { } } -impl Encoder for Codec { - type Item = Message; +impl Encoder for Codec { type Error = ProtocolError; fn encode(&mut self, item: Message, dst: &mut BytesMut) -> Result<(), Self::Error> { diff --git a/actix-http/src/ws/dispatcher.rs b/actix-http/src/ws/dispatcher.rs index 7a6b11b18..b114217a0 100644 --- a/actix-http/src/ws/dispatcher.rs +++ b/actix-http/src/ws/dispatcher.rs @@ -4,16 +4,18 @@ use std::task::{Context, Poll}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_service::{IntoService, Service}; -use actix_utils::framed; +use actix_utils::dispatcher::{Dispatcher as InnerDispatcher, DispatcherError}; use super::{Codec, Frame, Message}; +#[pin_project::pin_project] pub struct Dispatcher where S: Service + 'static, T: AsyncRead + AsyncWrite, { - inner: framed::Dispatcher, + #[pin] + inner: InnerDispatcher, } impl Dispatcher @@ -25,13 +27,13 @@ where { pub fn new>(io: T, service: F) -> Self { Dispatcher { - inner: framed::Dispatcher::new(Framed::new(io, Codec::new()), service), + inner: InnerDispatcher::new(Framed::new(io, Codec::new()), service), } } pub fn with>(framed: Framed, service: F) -> Self { Dispatcher { - inner: framed::Dispatcher::new(framed, service), + inner: InnerDispatcher::new(framed, service), } } } @@ -43,9 +45,9 @@ where S::Future: 'static, S::Error: 'static, { - type Output = Result<(), framed::DispatcherError>; + type Output = Result<(), DispatcherError>; - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - Pin::new(&mut self.inner).poll(cx) + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.project().inner.poll(cx) } } diff --git a/actix-http/src/ws/frame.rs b/actix-http/src/ws/frame.rs index 8f7004f18..0598a9b4e 100644 --- a/actix-http/src/ws/frame.rs +++ b/actix-http/src/ws/frame.rs @@ -229,10 +229,7 @@ mod tests { fn is_none( frm: &Result)>, ProtocolError>, ) -> bool { - match *frm { - Ok(None) => true, - _ => false, - } + matches!(*frm, Ok(None)) } fn extract( diff --git a/actix-http/src/ws/mask.rs b/actix-http/src/ws/mask.rs index 7eb5d148f..726b1a4a1 100644 --- a/actix-http/src/ws/mask.rs +++ b/actix-http/src/ws/mask.rs @@ -7,6 +7,8 @@ use std::slice; struct ShortSlice<'a>(&'a mut [u8]); impl<'a> ShortSlice<'a> { + /// # Safety + /// Given slice must be shorter than 8 bytes. unsafe fn new(slice: &'a mut [u8]) -> Self { // Sanity check for debug builds debug_assert!(slice.len() < 8); @@ -46,13 +48,13 @@ pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) { } } -#[inline] // TODO: copy_nonoverlapping here compiles to call memcpy. While it is not so // inefficient, it could be done better. The compiler does not understand that // a `ShortSlice` must be smaller than a u64. +#[inline] #[allow(clippy::needless_pass_by_value)] fn xor_short(buf: ShortSlice<'_>, mask: u64) { - // Unsafe: we know that a `ShortSlice` fits in a u64 + // SAFETY: we know that a `ShortSlice` fits in a u64 unsafe { let (ptr, len) = (buf.0.as_mut_ptr(), buf.0.len()); let mut b: u64 = 0; @@ -64,8 +66,9 @@ fn xor_short(buf: ShortSlice<'_>, mask: u64) { } } +/// # Safety +/// Caller must ensure the buffer has the correct size and alignment. #[inline] -// Unsafe: caller must ensure the buffer has the correct size and alignment unsafe fn cast_slice(buf: &mut [u8]) -> &mut [u64] { // Assert correct size and alignment in debug builds debug_assert!(buf.len().trailing_zeros() >= 3); @@ -74,9 +77,9 @@ unsafe fn cast_slice(buf: &mut [u8]) -> &mut [u64] { slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u64, buf.len() >> 3) } -#[inline] // Splits a slice into three parts: an unaligned short head and tail, plus an aligned // u64 mid section. +#[inline] fn align_buf(buf: &mut [u8]) -> (ShortSlice<'_>, &mut [u64], ShortSlice<'_>) { let start_ptr = buf.as_ptr() as usize; let end_ptr = start_ptr + buf.len(); @@ -91,13 +94,13 @@ fn align_buf(buf: &mut [u8]) -> (ShortSlice<'_>, &mut [u64], ShortSlice<'_>) { let (tmp, tail) = buf.split_at_mut(end_aligned - start_ptr); let (head, mid) = tmp.split_at_mut(start_aligned - start_ptr); - // Unsafe: we know the middle section is correctly aligned, and the outer + // SAFETY: we know the middle section is correctly aligned, and the outer // sections are smaller than 8 bytes unsafe { (ShortSlice::new(head), cast_slice(mid), ShortSlice(tail)) } } else { // We didn't cross even one aligned boundary! - // Unsafe: The outer sections are smaller than 8 bytes + // SAFETY: The outer sections are smaller than 8 bytes unsafe { (ShortSlice::new(buf), &mut [], ShortSlice::new(&mut [])) } } } @@ -139,7 +142,7 @@ mod tests { let mut masked = unmasked.clone(); apply_mask_fallback(&mut masked[1..], &mask); - let mut masked_fast = unmasked.clone(); + let mut masked_fast = unmasked; apply_mask(&mut masked_fast[1..], mask_u32); assert_eq!(masked, masked_fast); diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs index dd3078a6c..fc271a8f5 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -208,10 +208,10 @@ pub fn hash_key(key: &[u8]) -> String { use sha1::Digest; let mut hasher = sha1::Sha1::new(); - hasher.input(key); - hasher.input(WS_GUID.as_bytes()); + hasher.update(key); + hasher.update(WS_GUID.as_bytes()); - base64::encode(hasher.result().as_ref()) + base64::encode(&hasher.finalize()) } #[cfg(test)] diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index 3a7bfa409..795deacdc 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -274,9 +274,7 @@ async fn test_h2_head_empty() { async fn test_h2_head_binary() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| { - ok::<_, ()>(Response::Ok().body(STR)) - }) + .h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .openssl(ssl_acceptor()) .map_err(|_| ()) }) diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 465cba6df..beae359d9 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -280,9 +280,7 @@ async fn test_h2_head_empty() { async fn test_h2_head_binary() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| { - ok::<_, ()>(Response::Ok().body(STR)) - }) + .h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .rustls(ssl_acceptor()) }) .await; diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index bee5ebef2..0375b6f66 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -489,9 +489,7 @@ async fn test_h1_head_empty() { async fn test_h1_head_binary() { let mut srv = test_server(|| { HttpService::build() - .h1(|_| { - ok::<_, ()>(Response::Ok().body(STR)) - }) + .h1(|_| ok::<_, ()>(Response::Ok().body(STR))) .tcp() }) .await; diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs index ff9def85b..5d86605f4 100644 --- a/actix-http/tests/test_ws.rs +++ b/actix-http/tests/test_ws.rs @@ -8,7 +8,7 @@ use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_http::{body, h1, ws, Error, HttpService, Request, Response}; use actix_http_test::test_server; use actix_service::{fn_factory, Service}; -use actix_utils::framed::Dispatcher; +use actix_utils::dispatcher::Dispatcher; use bytes::Bytes; use futures_util::future; use futures_util::task::{Context, Poll}; @@ -59,7 +59,7 @@ where .await .unwrap(); - Dispatcher::new(framed.into_framed(ws::Codec::new()), service) + Dispatcher::new(framed.replace_codec(ws::Codec::new()), service) .await .map_err(|_| panic!()) }; diff --git a/actix-identity/README.md b/actix-identity/README.md deleted file mode 100644 index 62a40137f..000000000 --- a/actix-identity/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# 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/) -* [API Documentation](https://docs.rs/actix-identity/) -* [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-session](https://crates.io/crates/actix-identity) -* Minimum supported Rust version: 1.34 or later diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index df3cecf71..446ca5ad2 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,15 +1,26 @@ # Changes -## [0.3.0-alpha.1] - 2020-05-25 +## Unreleased - 2020-xx-xx +* Fix multipart consuming payload before header checks #1513 + +## 3.0.0 - 2020-09-11 +* No significant changes from `3.0.0-beta.2`. + + +## 3.0.0-beta.2 - 2020-09-10 +* Update `actix-*` dependencies to latest versions. + + +## 0.3.0-beta.1 - 2020-07-15 +* Update `actix-web` to 3.0.0-beta.1 + + +## 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 diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index c5f315d75..e2e9dbf14 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.3.0-alpha.1" +version = "0.3.0" authors = ["Nikolay Kim "] description = "Multipart support for actix web framework." readme = "README.md" @@ -8,7 +8,7 @@ keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" documentation = "https://docs.rs/actix-multipart/" -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" edition = "2018" [lib] @@ -16,9 +16,9 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "3.0.0-alpha.3", default-features = false } -actix-service = "1.0.1" -actix-utils = "1.0.3" +actix-web = { version = "3.0.0", default-features = false } +actix-service = "1.0.6" +actix-utils = "2.0.0" bytes = "0.5.3" derive_more = "0.99.2" httparse = "1.3" @@ -29,4 +29,4 @@ twoway = "0.2" [dev-dependencies] actix-rt = "1.0.0" -actix-http = "2.0.0-alpha.4" +actix-http = "2.0.0" diff --git a/actix-multipart/src/extractor.rs b/actix-multipart/src/extractor.rs index 4e4caee01..6aaa415c4 100644 --- a/actix-multipart/src/extractor.rs +++ b/actix-multipart/src/extractor.rs @@ -36,6 +36,9 @@ impl FromRequest for Multipart { #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - ok(Multipart::new(req.headers(), payload.take())) + ok(match Multipart::boundary(req.headers()) { + Ok(boundary) => Multipart::from_boundary(boundary, payload.take()), + Err(err) => Multipart::from_error(err), + }) } } diff --git a/actix-multipart/src/lib.rs b/actix-multipart/src/lib.rs index 43eb048ca..46dd0ee9b 100644 --- a/actix-multipart/src/lib.rs +++ b/actix-multipart/src/lib.rs @@ -1,3 +1,6 @@ +//! Multipart form support for Actix web. + +#![deny(rust_2018_idioms)] #![allow(clippy::borrow_interior_mutable_const)] mod error; diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index f96a7821a..b9ebf97cc 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -1,4 +1,5 @@ //! Multipart payload support + use std::cell::{Cell, RefCell, RefMut}; use std::convert::TryFrom; use std::marker::PhantomData; @@ -9,8 +10,6 @@ use std::{cmp, fmt}; use bytes::{Bytes, BytesMut}; use futures_util::stream::{LocalBoxStream, Stream, StreamExt}; -use httparse; -use mime; use actix_utils::task::LocalWaker; use actix_web::error::{ParseError, PayloadError}; @@ -65,26 +64,13 @@ impl Multipart { S: Stream> + Unpin + 'static, { match Self::boundary(headers) { - Ok(boundary) => Multipart { - error: None, - safety: Safety::new(), - inner: Some(Rc::new(RefCell::new(InnerMultipart { - boundary, - payload: PayloadRef::new(PayloadBuffer::new(Box::new(stream))), - state: InnerState::FirstBoundary, - item: InnerMultipartItem::None, - }))), - }, - Err(err) => Multipart { - error: Some(err), - safety: Safety::new(), - inner: None, - }, + Ok(boundary) => Multipart::from_boundary(boundary, stream), + Err(err) => Multipart::from_error(err), } } /// Extract boundary info from headers. - fn boundary(headers: &HeaderMap) -> Result { + pub(crate) fn boundary(headers: &HeaderMap) -> Result { if let Some(content_type) = headers.get(&header::CONTENT_TYPE) { if let Ok(content_type) = content_type.to_str() { if let Ok(ct) = content_type.parse::() { @@ -103,6 +89,32 @@ impl Multipart { Err(MultipartError::NoContentType) } } + + /// Create multipart instance for given boundary and stream + pub(crate) fn from_boundary(boundary: String, stream: S) -> Multipart + where + S: Stream> + Unpin + 'static, + { + Multipart { + error: None, + safety: Safety::new(), + inner: Some(Rc::new(RefCell::new(InnerMultipart { + boundary, + payload: PayloadRef::new(PayloadBuffer::new(Box::new(stream))), + state: InnerState::FirstBoundary, + item: InnerMultipartItem::None, + }))), + } + } + + /// Create Multipart instance from MultipartError + pub(crate) fn from_error(err: MultipartError) -> Multipart { + Multipart { + error: Some(err), + safety: Safety::new(), + inner: None, + } + } } impl Stream for Multipart { @@ -110,7 +122,7 @@ impl Stream for Multipart { fn poll_next( mut self: Pin<&mut Self>, - cx: &mut Context, + cx: &mut Context<'_>, ) -> Poll> { if let Some(err) = self.error.take() { Poll::Ready(Some(Err(err))) @@ -246,7 +258,7 @@ impl InnerMultipart { fn poll( &mut self, safety: &Safety, - cx: &mut Context, + cx: &mut Context<'_>, ) -> Poll>> { if self.state == InnerState::Eof { Poll::Ready(None) @@ -418,7 +430,10 @@ impl Field { impl Stream for Field { type Item = Result; - fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { if self.safety.current() { let mut inner = self.inner.borrow_mut(); if let Some(mut payload) = @@ -436,7 +451,7 @@ impl Stream for Field { } impl fmt::Debug for Field { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "\nField: {}", self.ct)?; writeln!(f, " boundary: {}", self.inner.borrow().boundary)?; writeln!(f, " headers:")?; @@ -691,7 +706,7 @@ impl Safety { self.clean.get() } - fn clone(&self, cx: &mut Context) -> Safety { + fn clone(&self, cx: &mut Context<'_>) -> Safety { let payload = Rc::clone(&self.payload); let s = Safety { task: LocalWaker::new(), @@ -736,7 +751,7 @@ impl PayloadBuffer { } } - fn poll_stream(&mut self, cx: &mut Context) -> Result<(), PayloadError> { + fn poll_stream(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> { loop { match Pin::new(&mut self.stream).poll_next(cx) { Poll::Ready(Some(Ok(data))) => self.buf.extend_from_slice(&data), @@ -813,6 +828,8 @@ mod tests { use actix_http::h1::Payload; use actix_utils::mpsc; use actix_web::http::header::{DispositionParam, DispositionType}; + use actix_web::test::TestRequest; + use actix_web::FromRequest; use bytes::Bytes; use futures_util::future::lazy; @@ -876,11 +893,11 @@ mod tests { impl SlowStream { fn new(bytes: Bytes) -> SlowStream { - return SlowStream { - bytes: bytes, + SlowStream { + bytes, pos: 0, ready: false, - }; + } } } @@ -889,7 +906,7 @@ mod tests { fn poll_next( self: Pin<&mut Self>, - cx: &mut Context, + cx: &mut Context<'_>, ) -> Poll> { let this = self.get_mut(); if !this.ready { @@ -1149,4 +1166,38 @@ mod tests { ); assert_eq!(payload.buf.len(), 0); } + + #[actix_rt::test] + async fn test_multipart_from_error() { + let err = MultipartError::NoContentType; + let mut multipart = Multipart::from_error(err); + assert!(multipart.next().await.unwrap().is_err()) + } + + #[actix_rt::test] + async fn test_multipart_from_boundary() { + let (_, payload) = create_stream(); + let (_, headers) = create_simple_request_with_header(); + let boundary = Multipart::boundary(&headers); + assert!(boundary.is_ok()); + let _ = Multipart::from_boundary(boundary.unwrap(), payload); + } + + #[actix_rt::test] + async fn test_multipart_payload_consumption() { + // with sample payload and HttpRequest with no headers + let (_, inner_payload) = Payload::create(false); + let mut payload = actix_web::dev::Payload::from(inner_payload); + let req = TestRequest::default().to_http_request(); + + // multipart should generate an error + let mut mp = Multipart::from_request(&req, &mut payload).await.unwrap(); + assert!(mp.next().await.unwrap().is_err()); + + // and should not consume the payload + match payload { + actix_web::dev::Payload::H1(_) => {} //expected + _ => unreachable!(), + } + } } diff --git a/actix-session/README.md b/actix-session/README.md deleted file mode 100644 index 00e580120..000000000 --- a/actix-session/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# 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/) -* [API Documentation](https://docs.rs/actix-session/) -* [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-session](https://crates.io/crates/actix-session) -* Minimum supported Rust version: 1.34 or later diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 8fd48f77c..4b9381a33 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,11 +1,22 @@ # Changes -## [Unreleased] - 2020-xx-xx +## Unreleased - 2020-xx-xx + +## 3.0.0 - 2020-09-11 +* No significant changes from `3.0.0-beta.2`. + + +## 3.0.0-beta.2 - 2020-09-10 +* Update `actix-*` dependencies to latest versions. + + +## [3.0.0-beta.1] - 2020-xx-xx +* Update `actix-web` & `actix-http` dependencies to beta.1 * Bump minimum supported Rust version to 1.40 -## [3.0.0-alpha.1] - 2020-05-08 +## [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 diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 8db7a35ef..2f3c63022 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "3.0.0-alpha.1" +version = "3.0.0" authors = ["Nikolay Kim "] description = "Actix actors support for actix web framework." readme = "README.md" @@ -8,7 +8,7 @@ keywords = ["actix", "http", "web", "framework", "async"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" documentation = "https://docs.rs/actix-web-actors/" -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" edition = "2018" [lib] @@ -16,16 +16,16 @@ name = "actix_web_actors" path = "src/lib.rs" [dependencies] -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" +actix = "0.10.0" +actix-web = { version = "3.0.0", default-features = false } +actix-http = "2.0.0" +actix-codec = "0.3.0" bytes = "0.5.2" 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" +actix-rt = "1.1.1" env_logger = "0.7" futures-util = { version = "0.3.5", default-features = false } diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs index 6360917cd..0421f05fb 100644 --- a/actix-web-actors/src/lib.rs +++ b/actix-web-actors/src/lib.rs @@ -1,5 +1,8 @@ -#![allow(clippy::borrow_interior_mutable_const)] //! Actix actors integration for Actix web framework + +#![deny(rust_2018_idioms)] +#![allow(clippy::borrow_interior_mutable_const)] + mod context; pub mod ws; diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs index 25977c2c2..dda9f6f0b 100644 --- a/actix-web-actors/tests/test_ws.rs +++ b/actix-web-actors/tests/test_ws.rs @@ -30,8 +30,8 @@ impl StreamHandler> for Ws { async fn test_simple() { let mut srv = test::start(|| { App::new().service(web::resource("/").to( - |req: HttpRequest, stream: web::Payload| { - async move { ws::start(Ws, &req, stream) } + |req: HttpRequest, stream: web::Payload| async move { + ws::start(Ws, &req, stream) }, )) }); @@ -51,7 +51,7 @@ async fn test_simple() { .await .unwrap(); let item = framed.next().await.unwrap().unwrap(); - assert_eq!(item, ws::Frame::Binary(Bytes::from_static(b"text").into())); + assert_eq!(item, ws::Frame::Binary(Bytes::from_static(b"text"))); framed.send(ws::Message::Ping("text".into())).await.unwrap(); let item = framed.next().await.unwrap().unwrap(); diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index b2e80591f..1ab51f924 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,49 +1,68 @@ # Changes -## [0.2.2] - 2020-05-23 +## Unreleased - 2020-xx-xx + +## 0.4.0 - 2020-09-20 +* Added compile success and failure testing. [#1677] +* Add `route` macro for supporting multiple HTTP methods guards. [#1674] + +[#1677]: https://github.com/actix/actix-web/pull/1677 +[#1674]: https://github.com/actix/actix-web/pull/1674 + + +## 0.3.0 - 2020-09-11 +* No significant changes from `0.3.0-beta.1`. + + +## 0.3.0-beta.1 - 2020-07-14 +* Add main entry-point macro that uses re-exported runtime. [#1559] + +[#1559]: https://github.com/actix/actix-web/pull/1559 + + +## 0.2.2 - 2020-05-23 * Add resource middleware on actix-web-codegen [#1467] [#1467]: https://github.com/actix/actix-web/pull/1467 -## [0.2.1] - 2020-02-25 +## 0.2.1 - 2020-02-25 * Add `#[allow(missing_docs)]` attribute to generated structs [#1368] * Allow the handler function to be named as `config` [#1290] [#1368]: https://github.com/actix/actix-web/issues/1368 [#1290]: https://github.com/actix/actix-web/issues/1290 -## [0.2.0] - 2019-12-13 +## 0.2.0 - 2019-12-13 * Generate code for actix-web 2.0 -## [0.1.3] - 2019-10-14 +## 0.1.3 - 2019-10-14 * Bump up `syn` & `quote` to 1.0 - * Provide better error message -## [0.1.2] - 2019-06-04 +## 0.1.2 - 2019-06-04 * Add macros for head, options, trace, connect and patch http methods -## [0.1.1] - 2019-06-01 +## 0.1.1 - 2019-06-01 * Add syn "extra-traits" feature -## [0.1.0] - 2019-05-18 +## 0.1.0 - 2019-05-18 * Release -## [0.1.0-beta.1] - 2019-04-20 +## 0.1.0-beta.1 - 2019-04-20 * Gen code for actix-web 1.0.0-beta.1 -## [0.1.0-alpha.6] - 2019-04-14 +## 0.1.0-alpha.6 - 2019-04-14 * Gen code for actix-web 1.0.0-alpha.6 -## [0.1.0-alpha.1] - 2019-03-28 +## 0.1.0-alpha.1 - 2019-03-28 * Initial impl diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 60480a7a1..fd99a8376 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,15 +1,14 @@ [package] name = "actix-web-codegen" -version = "0.2.2" +version = "0.4.0" 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" +license = "MIT OR Apache-2.0" edition = "2018" -workspace = ".." [lib] proc-macro = true @@ -20,6 +19,8 @@ syn = { version = "1", features = ["full", "parsing"] } proc-macro2 = "1" [dev-dependencies] -actix-rt = "1.0.0" -actix-web = "3.0.0-alpha.3" +actix-rt = "1.1.1" +actix-web = "3.0.0" futures-util = { version = "0.3.5", default-features = false } +trybuild = "1" +rustversion = "1" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index c482a6b36..6eca847b8 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -1,8 +1,22 @@ -# 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) +# actix-web-codegen + +> Helper and convenience macros for Actix Web + +[![crates.io](https://meritbadge.herokuapp.com/actix-web-codegen)](https://crates.io/crates/actix-web-codegen) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg)](https://docs.rs/actix-web) +[![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html) +[![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) +[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) +[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Documentation & Resources -* [API Documentation](https://docs.rs/actix-web-codegen/) -* [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen) -* Minimum supported Rust version: 1.40 or later +- [API Documentation](https://docs.rs/actix-web-codegen) +- [Chat on Gitter](https://gitter.im/actix/actix-web) +- Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen) +- Minimum supported Rust version: 1.42 or later. + +## Compile Testing +Uses the [`trybuild`] crate. All compile fail tests should include a stderr file generated by `trybuild`. See the [workflow section](https://github.com/dtolnay/trybuild#workflow) of the trybuild docs for info on how to do this. + +[`trybuild`]: https://github.com/dtolnay/trybuild diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 2a49b4714..af2bc7f18 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -1,141 +1,198 @@ -#![recursion_limit = "512"] -//! Actix-web codegen module +//! Macros for reducing boilerplate code in Actix Web applications. //! -//! Generators for routes and scopes +//! ## Actix Web Re-exports +//! Actix Web re-exports a version of this crate in it's entirety so you usually don't have to +//! specify a dependency on this crate explicitly. Sometimes, however, updates are made to this +//! crate before the actix-web dependency is updated. Therefore, code examples here will show +//! explicit imports. Check the latest [actix-web attributes docs] to see which macros +//! are re-exported. //! -//! ## Route -//! -//! Macros: -//! -//! - [get](attr.get.html) -//! - [post](attr.post.html) -//! - [put](attr.put.html) -//! - [delete](attr.delete.html) -//! - [head](attr.head.html) -//! - [connect](attr.connect.html) -//! - [options](attr.options.html) -//! - [trace](attr.trace.html) -//! - [patch](attr.patch.html) -//! -//! ### Attributes: -//! -//! - `"path"` - Raw literal string with path for which to register handle. Mandatory. -//! - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` -//! - `wrap="Middleware"` - Registers a resource middleware. -//! -//! ## Notes -//! -//! Function name can be specified as any expression that is going to be accessible to the generate -//! code (e.g `my_guard` or `my_module::my_guard`) -//! -//! ## Example: +//! # Runtime Setup +//! Used for setting up the actix async runtime. See [main] macro docs. //! //! ```rust -//! use actix_web::HttpResponse; -//! use actix_web_codegen::get; -//! -//! #[get("/test")] -//! async fn async_test() -> Result { -//! Ok(HttpResponse::Ok().finish()) +//! #[actix_web_codegen::main] // or `#[actix_web::main]` in Actix Web apps +//! async fn main() { +//! async { println!("Hello world"); }.await //! } //! ``` +//! +//! # Single Method Handler +//! There is a macro to set up a handler for each of the most common HTTP methods that also define +//! additional guards and route-specific middleware. +//! +//! See docs for: [GET], [POST], [PATCH], [PUT], [DELETE], [HEAD], [CONNECT], [OPTIONS], [TRACE] +//! +//! ```rust +//! # use actix_web::HttpResponse; +//! # use actix_web_codegen::get; +//! #[get("/test")] +//! async fn get_handler() -> HttpResponse { +//! HttpResponse::Ok().finish() +//! } +//! ``` +//! +//! # Multiple Method Handlers +//! Similar to the single method handler macro but takes one or more arguments for the HTTP methods +//! it should respond to. See [route] macro docs. +//! +//! ```rust +//! # use actix_web::HttpResponse; +//! # use actix_web_codegen::route; +//! #[route("/test", method="GET", method="HEAD")] +//! async fn get_and_head_handler() -> HttpResponse { +//! HttpResponse::Ok().finish() +//! } +//! ``` +//! +//! [actix-web attributes docs]: https://docs.rs/actix-web/*/actix_web/#attributes +//! [main]: attr.main.html +//! [route]: attr.route.html +//! [GET]: attr.get.html +//! [POST]: attr.post.html +//! [PUT]: attr.put.html +//! [DELETE]: attr.delete.html +//! [HEAD]: attr.head.html +//! [CONNECT]: attr.connect.html +//! [OPTIONS]: attr.options.html +//! [TRACE]: attr.trace.html +//! [PATCH]: attr.patch.html -extern crate proc_macro; - -mod route; +#![recursion_limit = "512"] use proc_macro::TokenStream; -/// Creates route handler with `GET` method guard. +mod route; + +/// Creates resource handler, allowing multiple HTTP method guards. /// -/// Syntax: `#[get("path"[, attributes])]` +/// # Syntax +/// ```text +/// #[route("path", method="HTTP_METHOD"[, attributes])] +/// ``` /// -/// ## Attributes: -/// -/// - `"path"` - Raw literal string with path for which to register handler. Mandatory. +/// # Attributes +/// - `"path"` - Raw literal string with path for which to register handler. +/// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. Upper-case string, "GET", "POST" for example. /// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` /// - `wrap="Middleware"` - Registers a resource middleware. +/// +/// # Notes +/// Function name can be specified as any expression that is going to be accessible to the generate +/// code, e.g `my_guard` or `my_module::my_guard`. +/// +/// # Example +/// +/// ```rust +/// # use actix_web::HttpResponse; +/// # use actix_web_codegen::route; +/// #[route("/test", method="GET", method="HEAD")] +/// async fn example() -> HttpResponse { +/// HttpResponse::Ok().finish() +/// } +/// ``` #[proc_macro_attribute] -pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Get) +pub fn route(args: TokenStream, input: TokenStream) -> TokenStream { + route::with_method(None, args, input) } -/// Creates route handler with `POST` method guard. -/// -/// Syntax: `#[post("path"[, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html) -#[proc_macro_attribute] -pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Post) +macro_rules! doc_comment { + ($x:expr; $($tt:tt)*) => { + #[doc = $x] + $($tt)* + }; } -/// Creates route handler with `PUT` method guard. -/// -/// Syntax: `#[put("path"[, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html) -#[proc_macro_attribute] -pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Put) +macro_rules! method_macro { + ( + $($variant:ident, $method:ident,)+ + ) => { + $(doc_comment! { +concat!(" +Creates route handler with `actix_web::guard::", stringify!($variant), "`. + +# Syntax +```text +#[", stringify!($method), r#"("path"[, attributes])] +``` + +# Attributes +- `"path"` - Raw literal string with path for which to register handler. +- `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`. +- `wrap="Middleware"` - Registers a resource middleware. + +# Notes +Function name can be specified as any expression that is going to be accessible to the generate +code, e.g `my_guard` or `my_module::my_guard`. + +# Example + +```rust +# use actix_web::HttpResponse; +# use actix_web_codegen::"#, stringify!($method), "; +#[", stringify!($method), r#"("/")] +async fn example() -> HttpResponse { + HttpResponse::Ok().finish() +} +``` +"#); + #[proc_macro_attribute] + pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream { + route::with_method(Some(route::MethodType::$variant), args, input) + } + })+ + }; } -/// Creates route handler with `DELETE` method guard. -/// -/// Syntax: `#[delete("path"[, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html) -#[proc_macro_attribute] -pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Delete) +method_macro! { + Get, get, + Post, post, + Put, put, + Delete, delete, + Head, head, + Connect, connect, + Options, options, + Trace, trace, + Patch, patch, } -/// Creates route handler with `HEAD` method guard. +/// Marks async main function as the actix system entry-point. /// -/// Syntax: `#[head("path"[, attributes])]` +/// # Actix Web Re-export +/// This macro can be applied with `#[actix_web::main]` when used in Actix Web applications. /// -/// Attributes are the same as in [head](attr.head.html) +/// # Usage +/// ```rust +/// #[actix_web_codegen::main] +/// async fn main() { +/// async { println!("Hello world"); }.await +/// } +/// ``` #[proc_macro_attribute] -pub fn head(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Head) -} +pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { + use quote::quote; -/// Creates route handler with `CONNECT` method guard. -/// -/// Syntax: `#[connect("path"[, attributes])]` -/// -/// Attributes are the same as in [connect](attr.connect.html) -#[proc_macro_attribute] -pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Connect) -} + let mut input = syn::parse_macro_input!(item as syn::ItemFn); + let attrs = &input.attrs; + let vis = &input.vis; + let sig = &mut input.sig; + let body = &input.block; + let name = &sig.ident; -/// Creates route handler with `OPTIONS` method guard. -/// -/// Syntax: `#[options("path"[, attributes])]` -/// -/// Attributes are the same as in [options](attr.options.html) -#[proc_macro_attribute] -pub fn options(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Options) -} + if sig.asyncness.is_none() { + return syn::Error::new_spanned(sig.fn_token, "only async fn is supported") + .to_compile_error() + .into(); + } -/// Creates route handler with `TRACE` method guard. -/// -/// Syntax: `#[trace("path"[, attributes])]` -/// -/// Attributes are the same as in [trace](attr.trace.html) -#[proc_macro_attribute] -pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Trace) -} + sig.asyncness = None; -/// Creates route handler with `PATCH` method guard. -/// -/// Syntax: `#[patch("path"[, attributes])]` -/// -/// Attributes are the same as in [patch](attr.patch.html) -#[proc_macro_attribute] -pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Patch) + (quote! { + #(#attrs)* + #vis #sig { + actix_web::rt::System::new(stringify!(#name)) + .block_on(async move { #body }) + } + }) + .into() } diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 7e3d43f1d..ddbd42454 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -1,9 +1,12 @@ extern crate proc_macro; +use std::collections::HashSet; +use std::convert::TryFrom; + use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{format_ident, quote, ToTokens, TokenStreamExt}; -use syn::{AttributeArgs, Ident, NestedMeta, parse_macro_input}; +use syn::{parse_macro_input, AttributeArgs, Ident, NestedMeta}; enum ResourceType { Async, @@ -17,53 +20,81 @@ impl ToTokens for ResourceType { } } -#[derive(PartialEq)] -pub enum GuardType { - Get, - Post, - Put, - Delete, - Head, - Connect, - Options, - Trace, - Patch, -} - -impl GuardType { - fn as_str(&self) -> &'static str { - match self { - GuardType::Get => "Get", - GuardType::Post => "Post", - GuardType::Put => "Put", - GuardType::Delete => "Delete", - GuardType::Head => "Head", - GuardType::Connect => "Connect", - GuardType::Options => "Options", - GuardType::Trace => "Trace", - GuardType::Patch => "Patch", +macro_rules! method_type { + ( + $($variant:ident, $upper:ident,)+ + ) => { + #[derive(Debug, PartialEq, Eq, Hash)] + pub enum MethodType { + $( + $variant, + )+ } - } + + impl MethodType { + fn as_str(&self) -> &'static str { + match self { + $(Self::$variant => stringify!($variant),)+ + } + } + + fn parse(method: &str) -> Result { + match method { + $(stringify!($upper) => Ok(Self::$variant),)+ + _ => Err(format!("Unexpected HTTP method: `{}`", method)), + } + } + } + }; } -impl ToTokens for GuardType { +method_type! { + Get, GET, + Post, POST, + Put, PUT, + Delete, DELETE, + Head, HEAD, + Connect, CONNECT, + Options, OPTIONS, + Trace, TRACE, + Patch, PATCH, +} + +impl ToTokens for MethodType { fn to_tokens(&self, stream: &mut TokenStream2) { let ident = Ident::new(self.as_str(), Span::call_site()); stream.append(ident); } } +impl TryFrom<&syn::LitStr> for MethodType { + type Error = syn::Error; + + fn try_from(value: &syn::LitStr) -> Result { + Self::parse(value.value().as_str()) + .map_err(|message| syn::Error::new_spanned(value, message)) + } +} + struct Args { path: syn::LitStr, guards: Vec, wrappers: Vec, + methods: HashSet, } impl Args { - fn new(args: AttributeArgs) -> syn::Result { + fn new(args: AttributeArgs, method: Option) -> syn::Result { let mut path = None; let mut guards = Vec::new(); let mut wrappers = Vec::new(); + let mut methods = HashSet::new(); + + let is_route_macro = method.is_none(); + if let Some(method) = method { + methods.insert(method); + } + for arg in args { match arg { NestedMeta::Lit(syn::Lit::Str(lit)) => match path { @@ -96,10 +127,33 @@ impl Args { "Attribute wrap expects type", )); } + } else if nv.path.is_ident("method") { + if !is_route_macro { + return Err(syn::Error::new_spanned( + &nv, + "HTTP method forbidden here. To handle multiple methods, use `route` instead", + )); + } else if let syn::Lit::Str(ref lit) = nv.lit { + let method = MethodType::try_from(lit)?; + if !methods.insert(method) { + return Err(syn::Error::new_spanned( + &nv.lit, + &format!( + "HTTP method defined more than once: `{}`", + lit.value() + ), + )); + } + } else { + return Err(syn::Error::new_spanned( + nv.lit, + "Attribute method expects literal string!", + )); + } } else { return Err(syn::Error::new_spanned( nv.path, - "Unknown attribute key is specified. Allowed: guard and wrap", + "Unknown attribute key is specified. Allowed: guard, method and wrap", )); } } @@ -112,6 +166,7 @@ impl Args { path: path.unwrap(), guards, wrappers, + methods, }) } } @@ -121,7 +176,6 @@ pub struct Route { args: Args, ast: syn::ItemFn, resource_type: ResourceType, - guard: GuardType, } fn guess_resource_type(typ: &syn::Type) -> ResourceType { @@ -150,21 +204,30 @@ impl Route { pub fn new( args: AttributeArgs, input: TokenStream, - guard: GuardType, + method: Option, ) -> syn::Result { if args.is_empty() { return Err(syn::Error::new( Span::call_site(), format!( - r#"invalid server definition, expected #[{}("")]"#, - guard.as_str().to_ascii_lowercase() + r#"invalid service definition, expected #[{}("")]"#, + method + .map(|it| it.as_str()) + .unwrap_or("route") + .to_ascii_lowercase() ), )); } let ast: syn::ItemFn = syn::parse(input)?; let name = ast.sig.ident.clone(); - let args = Args::new(args)?; + let args = Args::new(args, method)?; + if args.methods.is_empty() { + return Err(syn::Error::new( + Span::call_site(), + "The #[route(..)] macro requires at least one `method` attribute", + )); + } let resource_type = if ast.sig.asyncness.is_some() { ResourceType::Async @@ -185,7 +248,6 @@ impl Route { args, ast, resource_type, - guard, }) } } @@ -194,12 +256,36 @@ impl ToTokens for Route { fn to_tokens(&self, output: &mut TokenStream2) { let Self { name, - guard, ast, - args: Args { path, guards, wrappers }, + args: + Args { + path, + guards, + wrappers, + methods, + }, resource_type, } = self; let resource_name = name.to_string(); + let method_guards = { + let mut others = methods.iter(); + // unwrapping since length is checked to be at least one + let first = others.next().unwrap(); + + if methods.len() > 1 { + quote! { + .guard( + actix_web::guard::Any(actix_web::guard::#first()) + #(.or(actix_web::guard::#others()))* + ) + } + } else { + quote! { + .guard(actix_web::guard::#first()) + } + } + }; + let stream = quote! { #[allow(non_camel_case_types, missing_docs)] pub struct #name; @@ -209,7 +295,7 @@ impl ToTokens for Route { #ast let __resource = actix_web::Resource::new(#path) .name(#resource_name) - .guard(actix_web::guard::#guard()) + #method_guards #(.guard(actix_web::guard::fn_guard(#guards)))* #(.wrap(#wrappers))* .#resource_type(#name); @@ -223,13 +309,13 @@ impl ToTokens for Route { } } -pub(crate) fn generate( +pub(crate) fn with_method( + method: Option, args: TokenStream, input: TokenStream, - guard: GuardType, ) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); - match Route::new(args, input, guard) { + match Route::new(args, input, method) { Ok(route) => route.into_token_stream().into(), Err(err) => err.to_compile_error().into(), } diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 0ef7e1c75..dd2bccd7f 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -2,11 +2,13 @@ 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_util::future; +use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; use actix_web::http::header::{HeaderName, HeaderValue}; +use actix_web::{http, test, web::Path, App, Error, HttpResponse, Responder}; +use actix_web_codegen::{ + connect, delete, get, head, options, patch, post, put, route, trace, +}; +use futures_util::future; // Make sure that we can name function as 'config' #[get("/config")] @@ -79,6 +81,11 @@ async fn get_param_test(_: Path) -> impl Responder { HttpResponse::Ok() } +#[route("/multi", method = "GET", method = "POST", method = "HEAD")] +async fn route_test() -> impl Responder { + HttpResponse::Ok() +} + pub struct ChangeStatusCode; impl Transform for ChangeStatusCode @@ -112,6 +119,7 @@ where type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; + #[allow(clippy::type_complexity)] type Future = Pin>>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { @@ -119,7 +127,6 @@ where } fn call(&mut self, req: ServiceRequest) -> Self::Future { - let fut = self.service.call(req); Box::pin(async move { @@ -172,6 +179,7 @@ async fn test_body() { .service(trace_test) .service(patch_test) .service(test_handler) + .service(route_test) }); let request = srv.request(http::Method::GET, srv.url("/test")); let response = request.send().await.unwrap(); @@ -210,6 +218,22 @@ async fn test_body() { let request = srv.request(http::Method::GET, srv.url("/test")); let response = request.send().await.unwrap(); assert!(response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url("/multi")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::POST, srv.url("/multi")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::HEAD, srv.url("/multi")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::PATCH, srv.url("/multi")); + let response = request.send().await.unwrap(); + assert!(!response.status().is_success()); } #[actix_rt::test] @@ -223,10 +247,7 @@ async fn test_auto_async() { #[actix_rt::test] async fn test_wrap() { - let srv = test::start(|| { - App::new() - .service(get_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(); diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs new file mode 100644 index 000000000..1bc2bd25e --- /dev/null +++ b/actix-web-codegen/tests/trybuild.rs @@ -0,0 +1,27 @@ +#[test] +fn compile_macros() { + let t = trybuild::TestCases::new(); + + t.pass("tests/trybuild/simple.rs"); + t.compile_fail("tests/trybuild/simple-fail.rs"); + + t.pass("tests/trybuild/route-ok.rs"); + t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs"); + t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs"); + + test_route_missing_method(&t) +} + +#[rustversion::stable(1.42)] +fn test_route_missing_method(t: &trybuild::TestCases) { + t.compile_fail("tests/trybuild/route-missing-method-fail-msrv.rs"); +} + +#[rustversion::not(stable(1.42))] +#[rustversion::not(nightly)] +fn test_route_missing_method(t: &trybuild::TestCases) { + t.compile_fail("tests/trybuild/route-missing-method-fail.rs"); +} + +#[rustversion::nightly] +fn test_route_missing_method(_t: &trybuild::TestCases) {} diff --git a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs new file mode 100644 index 000000000..9a38050f7 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs @@ -0,0 +1,17 @@ +use actix_web_codegen::*; + +#[route("/", method="GET", method="GET")] +async fn index() -> String { + "Hello World!".to_owned() +} + +#[actix_web::main] +async fn main() { + use actix_web::{App, test}; + + let srv = test::start(|| App::new().service(index)); + + let request = srv.get("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr new file mode 100644 index 000000000..f3eda68af --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr @@ -0,0 +1,11 @@ +error: HTTP method defined more than once: `GET` + --> $DIR/route-duplicate-method-fail.rs:3:35 + | +3 | #[route("/", method="GET", method="GET")] + | ^^^^^ + +error[E0425]: cannot find value `index` in this scope + --> $DIR/route-duplicate-method-fail.rs:12:49 + | +12 | let srv = test::start(|| App::new().service(index)); + | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs new file mode 120000 index 000000000..70a5c0e33 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs @@ -0,0 +1 @@ +route-missing-method-fail.rs \ No newline at end of file diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr new file mode 100644 index 000000000..d3e2b60ae --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr @@ -0,0 +1,11 @@ +error: The #[route(..)] macro requires at least one `method` attribute + --> $DIR/route-missing-method-fail-msrv.rs:3:1 + | +3 | #[route("/")] + | ^^^^^^^^^^^^^ + +error[E0425]: cannot find value `index` in this scope + --> $DIR/route-missing-method-fail-msrv.rs:12:49 + | +12 | let srv = test::start(|| App::new().service(index)); + | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs b/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs new file mode 100644 index 000000000..ce87a55a4 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs @@ -0,0 +1,17 @@ +use actix_web_codegen::*; + +#[route("/")] +async fn index() -> String { + "Hello World!".to_owned() +} + +#[actix_web::main] +async fn main() { + use actix_web::{App, test}; + + let srv = test::start(|| App::new().service(index)); + + let request = srv.get("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr new file mode 100644 index 000000000..0518a61ed --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr @@ -0,0 +1,13 @@ +error: The #[route(..)] macro requires at least one `method` attribute + --> $DIR/route-missing-method-fail.rs:3:1 + | +3 | #[route("/")] + | ^^^^^^^^^^^^^ + | + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0425]: cannot find value `index` in this scope + --> $DIR/route-missing-method-fail.rs:12:49 + | +12 | let srv = test::start(|| App::new().service(index)); + | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/route-ok.rs b/actix-web-codegen/tests/trybuild/route-ok.rs new file mode 100644 index 000000000..c4f679604 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-ok.rs @@ -0,0 +1,17 @@ +use actix_web_codegen::*; + +#[route("/", method="GET", method="HEAD")] +async fn index() -> String { + "Hello World!".to_owned() +} + +#[actix_web::main] +async fn main() { + use actix_web::{App, test}; + + let srv = test::start(|| App::new().service(index)); + + let request = srv.get("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs new file mode 100644 index 000000000..28cd1344c --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs @@ -0,0 +1,17 @@ +use actix_web_codegen::*; + +#[route("/", method="UNEXPECTED")] +async fn index() -> String { + "Hello World!".to_owned() +} + +#[actix_web::main] +async fn main() { + use actix_web::{App, test}; + + let srv = test::start(|| App::new().service(index)); + + let request = srv.get("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr new file mode 100644 index 000000000..9d87f310b --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr @@ -0,0 +1,11 @@ +error: Unexpected HTTP method: `UNEXPECTED` + --> $DIR/route-unexpected-method-fail.rs:3:21 + | +3 | #[route("/", method="UNEXPECTED")] + | ^^^^^^^^^^^^ + +error[E0425]: cannot find value `index` in this scope + --> $DIR/route-unexpected-method-fail.rs:12:49 + | +12 | let srv = test::start(|| App::new().service(index)); + | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/simple-fail.rs b/actix-web-codegen/tests/trybuild/simple-fail.rs new file mode 100644 index 000000000..a57fdc16d --- /dev/null +++ b/actix-web-codegen/tests/trybuild/simple-fail.rs @@ -0,0 +1,30 @@ +use actix_web_codegen::*; + +#[get("/one", other)] +async fn one() -> String { + "Hello World!".to_owned() +} + +#[post(/two)] +async fn two() -> String { + "Hello World!".to_owned() +} + +static PATCH_PATH: &str = "/three"; + +#[patch(PATCH_PATH)] +async fn three() -> String { + "Hello World!".to_owned() +} + +#[delete("/four", "/five")] +async fn four() -> String { + "Hello World!".to_owned() +} + +#[delete("/five", method="GET")] +async fn five() -> String { + "Hello World!".to_owned() +} + +fn main() {} diff --git a/actix-web-codegen/tests/trybuild/simple-fail.stderr b/actix-web-codegen/tests/trybuild/simple-fail.stderr new file mode 100644 index 000000000..cffc81ff8 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/simple-fail.stderr @@ -0,0 +1,29 @@ +error: Unknown attribute. + --> $DIR/simple-fail.rs:3:15 + | +3 | #[get("/one", other)] + | ^^^^^ + +error: expected identifier or literal + --> $DIR/simple-fail.rs:8:8 + | +8 | #[post(/two)] + | ^ + +error: Unknown attribute. + --> $DIR/simple-fail.rs:15:9 + | +15 | #[patch(PATCH_PATH)] + | ^^^^^^^^^^ + +error: Multiple paths specified! Should be only one! + --> $DIR/simple-fail.rs:20:19 + | +20 | #[delete("/four", "/five")] + | ^^^^^^^ + +error: HTTP method forbidden here. To handle multiple methods, use `route` instead + --> $DIR/simple-fail.rs:25:19 + | +25 | #[delete("/five", method="GET")] + | ^^^^^^^^^^^^ diff --git a/actix-web-codegen/tests/trybuild/simple.rs b/actix-web-codegen/tests/trybuild/simple.rs new file mode 100644 index 000000000..761b04905 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/simple.rs @@ -0,0 +1,16 @@ +use actix_web::{Responder, HttpResponse, App, test}; +use actix_web_codegen::*; + +#[get("/config")] +async fn config() -> impl Responder { + HttpResponse::Ok() +} + +#[actix_web::main] +async fn main() { + let srv = test::start(|| App::new().service(config)); + + let request = srv.get("/config"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 6d5a81b5e..07a469746 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,5 +1,32 @@ # Changes +## Unreleased - 2020-xx-xx + + +## 2.0.0 - 2020-09-11 +### Changed +* `Client::build` was renamed to `Client::builder`. + + +## 2.0.0-beta.4 - 2020-09-09 +### Changed +* Update actix-codec & actix-tls dependencies. + + +## 2.0.0-beta.3 - 2020-08-17 +### Changed +* Update `rustls` to 0.18 + + +## 2.0.0-beta.2 - 2020-07-21 +### Changed +* Update `actix-http` dependency to 2.0.0-beta.2 + + +## [2.0.0-beta.1] - 2020-07-14 +### Changed +* Update `actix-http` dependency to 2.0.0-beta.1 + ## [2.0.0-alpha.2] - 2020-05-21 ### Changed diff --git a/awc/Cargo.toml b/awc/Cargo.toml index b36e735ca..c67b6ba6f 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,17 +1,20 @@ [package] name = "awc" -version = "2.0.0-alpha.2" +version = "2.0.0" authors = ["Nikolay Kim "] -description = "Actix http client." +description = "Async HTTP client library that uses the Actix runtime." readme = "README.md" keywords = ["actix", "http", "framework", "async", "web"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" documentation = "https://docs.rs/awc/" -categories = ["network-programming", "asynchronous", - "web-programming::http-client", - "web-programming::websocket"] -license = "MIT/Apache-2.0" +categories = [ + "network-programming", + "asynchronous", + "web-programming::http-client", + "web-programming::websocket", +] +license = "MIT OR Apache-2.0" edition = "2018" [lib] @@ -34,9 +37,9 @@ rustls = ["rust-tls", "actix-http/rustls"] compress = ["actix-http/compress"] [dependencies] -actix-codec = "0.2.0" -actix-service = "1.0.1" -actix-http = "2.0.0-alpha.4" +actix-codec = "0.3.0" +actix-service = "1.0.6" +actix-http = "2.0.0" actix-rt = "1.0.0" base64 = "0.12" @@ -51,16 +54,16 @@ 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.17.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } +rust-tls = { version = "0.18.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } [dev-dependencies] -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-connect = { version = "2.0.0", features = ["openssl"] } +actix-web = { version = "3.0.0", features = ["openssl"] } +actix-http = { version = "2.0.0", features = ["openssl"] } +actix-http-test = { version = "2.0.0", features = ["openssl"] } +actix-utils = "2.0.0" actix-server = "1.0.0" -actix-tls = { version = "2.0.0-alpha.1", features = ["openssl", "rustls"] } +actix-tls = { version = "2.0.0", features = ["openssl", "rustls"] } brotli2 = "0.3.2" flate2 = "1.0.13" futures-util = { version = "0.3.5", default-features = false } diff --git a/awc/src/connect.rs b/awc/src/connect.rs index 618d653f5..7fbe1543a 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -152,7 +152,7 @@ where let (head, framed) = connection.open_tunnel(RequestHeadType::from(head)).await?; - let framed = framed.map_io(|io| BoxedSocket(Box::new(Socket(io)))); + let framed = framed.into_map_io(|io| BoxedSocket(Box::new(Socket(io)))); Ok((head, framed)) }) } @@ -186,7 +186,7 @@ where .open_tunnel(RequestHeadType::Rc(head, extra_headers)) .await?; - let framed = framed.map_io(|io| BoxedSocket(Box::new(Socket(io)))); + let framed = framed.into_map_io(|io| BoxedSocket(Box::new(Socket(io)))); Ok((head, framed)) }) } diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 1cc31a194..45c52092a 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -1,27 +1,96 @@ -#![warn(rust_2018_idioms, warnings)] +#![deny(rust_2018_idioms)] #![allow( clippy::type_complexity, clippy::borrow_interior_mutable_const, clippy::needless_doctest_main )] -//! An HTTP Client + +//! `awc` is a HTTP and WebSocket client library built using the Actix ecosystem. +//! +//! ## Making a GET request //! //! ```rust -//! use actix_rt::System; -//! use awc::Client; +//! # #[actix_rt::main] +//! # async fn main() -> Result<(), awc::error::SendRequestError> { +//! let mut client = awc::Client::default(); +//! let response = client.get("http://www.rust-lang.org") // <- Create request builder +//! .header("User-Agent", "Actix-web") +//! .send() // <- Send http request +//! .await?; //! -//! #[actix_rt::main] -//! async fn main() { -//! let mut client = Client::default(); -//! -//! let response = client.get("http://www.rust-lang.org") // <- Create request builder -//! .header("User-Agent", "Actix-web") -//! .send() // <- Send http request -//! .await; -//! -//! println!("Response: {:?}", response); -//! } +//! println!("Response: {:?}", response); +//! # Ok(()) +//! # } //! ``` +//! +//! ## Making POST requests +//! +//! ### Raw body contents +//! +//! ```rust +//! # #[actix_rt::main] +//! # async fn main() -> Result<(), awc::error::SendRequestError> { +//! let mut client = awc::Client::default(); +//! let response = client.post("http://httpbin.org/post") +//! .send_body("Raw body contents") +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Forms +//! +//! ```rust +//! # #[actix_rt::main] +//! # async fn main() -> Result<(), awc::error::SendRequestError> { +//! let params = [("foo", "bar"), ("baz", "quux")]; +//! +//! let mut client = awc::Client::default(); +//! let response = client.post("http://httpbin.org/post") +//! .send_form(¶ms) +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ### JSON +//! +//! ```rust +//! # #[actix_rt::main] +//! # async fn main() -> Result<(), awc::error::SendRequestError> { +//! let request = serde_json::json!({ +//! "lang": "rust", +//! "body": "json" +//! }); +//! +//! let mut client = awc::Client::default(); +//! let response = client.post("http://httpbin.org/post") +//! .send_json(&request) +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## WebSocket support +//! +//! ``` +//! # #[actix_rt::main] +//! # async fn main() -> Result<(), Box> { +//! use futures_util::{sink::SinkExt, stream::StreamExt}; +//! let (_resp, mut connection) = awc::Client::new() +//! .ws("ws://echo.websocket.org") +//! .connect() +//! .await?; +//! +//! connection +//! .send(awc::ws::Message::Text("Echo".to_string())) +//! .await?; +//! let response = connection.next().await.unwrap()?; +//! # assert_eq!(response, awc::ws::Frame::Text("Echo".as_bytes().into())); +//! # Ok(()) +//! # } +//! ``` + use std::cell::RefCell; use std::convert::TryFrom; use std::rc::Rc; @@ -51,7 +120,9 @@ pub use self::sender::SendClientRequest; use self::connect::{Connect, ConnectorWrapper}; -/// An HTTP Client +/// An asynchronous HTTP and WebSocket client. +/// +/// ## Examples /// /// ```rust /// use awc::Client; @@ -95,8 +166,9 @@ impl Client { Client::default() } - /// Build client instance. - pub fn build() -> ClientBuilder { + /// Create `Client` builder. + /// This function is equivalent of `ClientBuilder::new()`. + pub fn builder() -> ClientBuilder { ClientBuilder::new() } @@ -193,7 +265,8 @@ impl Client { self.request(Method::OPTIONS, url) } - /// Construct WebSockets request. + /// Initialize a WebSocket connection. + /// Returns a WebSocket connection builder. pub fn ws(&self, url: U) -> ws::WebsocketsRequest where Uri: TryFrom, diff --git a/awc/src/request.rs b/awc/src/request.rs index 21a7cd911..dcada2c6d 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -1,16 +1,14 @@ use std::convert::TryFrom; -use std::fmt::Write as FmtWrite; use std::rc::Rc; use std::time::Duration; use std::{fmt, net}; use bytes::Bytes; use futures_core::Stream; -use percent_encoding::percent_encode; use serde::Serialize; use actix_http::body::Body; -use actix_http::cookie::{Cookie, CookieJar, USERINFO}; +use actix_http::cookie::{Cookie, CookieJar}; use actix_http::http::header::{self, Header, IntoHeaderValue}; use actix_http::http::{ uri, ConnectionType, Error as HttpError, HeaderMap, HeaderName, HeaderValue, Method, @@ -527,16 +525,18 @@ impl ClientRequest { // set cookies if let Some(ref mut jar) = self.cookies { - let mut cookie = String::new(); - for c in jar.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO); - let value = percent_encode(c.value().as_bytes(), USERINFO); - let _ = write!(&mut cookie, "; {}={}", name, value); + let cookie: String = jar + .delta() + // ensure only name=value is written to cookie header + .map(|c| Cookie::new(c.name(), c.value()).encoded().to_string()) + .collect::>() + .join("; "); + + if !cookie.is_empty() { + self.head + .headers + .insert(header::COOKIE, HeaderValue::from_str(&cookie).unwrap()); } - self.head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); } let mut slf = self; @@ -586,16 +586,16 @@ mod tests { use super::*; use crate::Client; - #[test] - fn test_debug() { + #[actix_rt::test] + async fn test_debug() { let request = Client::new().get("/").header("x-test", "111"); let repr = format!("{:?}", request); assert!(repr.contains("ClientRequest")); assert!(repr.contains("x-test")); } - #[test] - fn test_basics() { + #[actix_rt::test] + async fn test_basics() { let mut req = Client::new() .put("/") .version(Version::HTTP_2) @@ -621,9 +621,9 @@ mod tests { let _ = req.send_body(""); } - #[test] - fn test_client_header() { - let req = Client::build() + #[actix_rt::test] + async fn test_client_header() { + let req = Client::builder() .header(header::CONTENT_TYPE, "111") .finish() .get("/"); @@ -639,9 +639,9 @@ mod tests { ); } - #[test] - fn test_client_header_override() { - let req = Client::build() + #[actix_rt::test] + async fn test_client_header_override() { + let req = Client::builder() .header(header::CONTENT_TYPE, "111") .finish() .get("/") @@ -658,8 +658,8 @@ mod tests { ); } - #[test] - fn client_basic_auth() { + #[actix_rt::test] + async fn client_basic_auth() { let req = Client::new() .get("/") .basic_auth("username", Some("password")); @@ -685,8 +685,8 @@ mod tests { ); } - #[test] - fn client_bearer_auth() { + #[actix_rt::test] + async fn client_bearer_auth() { let req = Client::new().get("/").bearer_auth("someS3cr3tAutht0k3n"); assert_eq!( req.head @@ -699,8 +699,8 @@ mod tests { ); } - #[test] - fn client_query() { + #[actix_rt::test] + async fn client_query() { let req = Client::new() .get("/") .query(&[("key1", "val1"), ("key2", "val2")]) diff --git a/awc/src/response.rs b/awc/src/response.rs index ffc8c5408..8364aa556 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -402,14 +402,12 @@ mod tests { fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { match err { - JsonPayloadError::Payload(PayloadError::Overflow) => match other { - JsonPayloadError::Payload(PayloadError::Overflow) => true, - _ => false, - }, - JsonPayloadError::ContentType => match other { - JsonPayloadError::ContentType => true, - _ => false, - }, + JsonPayloadError::Payload(PayloadError::Overflow) => { + matches!(other, JsonPayloadError::Payload(PayloadError::Overflow)) + } + JsonPayloadError::ContentType => { + matches!(other, JsonPayloadError::ContentType) + } _ => false, } } diff --git a/awc/src/sender.rs b/awc/src/sender.rs index 5e0f5beec..0bcdf4307 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -193,11 +193,7 @@ impl RequestSender { } }; - SendClientRequest::new( - fut, - response_decompress, - timeout.or_else(|| config.timeout), - ) + SendClientRequest::new(fut, response_decompress, timeout.or(config.timeout)) } pub(crate) fn send_json( diff --git a/awc/src/test.rs b/awc/src/test.rs index a6cbd03e6..68e5c9dc5 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -1,13 +1,11 @@ //! Test helpers for actix http client to use during testing. use std::convert::TryFrom; -use std::fmt::Write as FmtWrite; -use actix_http::cookie::{Cookie, CookieJar, USERINFO}; +use actix_http::cookie::{Cookie, CookieJar}; use actix_http::http::header::{self, Header, HeaderValue, IntoHeaderValue}; use actix_http::http::{Error as HttpError, HeaderName, StatusCode, Version}; use actix_http::{h1, Payload, ResponseHead}; use bytes::Bytes; -use percent_encoding::percent_encode; use crate::ClientResponse; @@ -88,16 +86,10 @@ impl TestResponse { pub fn finish(self) -> ClientResponse { let mut head = self.head; - let mut cookie = String::new(); - for c in self.cookies.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO); - let value = percent_encode(c.value().as_bytes(), USERINFO); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - if !cookie.is_empty() { + for cookie in self.cookies.delta() { head.headers.insert( header::SET_COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + HeaderValue::from_str(&cookie.encoded().to_string()).unwrap(), ); } diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 89ca50b59..57e80bd46 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -1,6 +1,32 @@ //! Websockets client +//! +//! Type definitions required to use [`awc::Client`](../struct.Client.html) as a WebSocket client. +//! +//! # Example +//! +//! ``` +//! use awc::{Client, ws}; +//! use futures_util::{sink::SinkExt, stream::StreamExt}; +//! +//! #[actix_rt::main] +//! async fn main() { +//! let (_resp, mut connection) = Client::new() +//! .ws("ws://echo.websocket.org") +//! .connect() +//! .await +//! .unwrap(); +//! +//! connection +//! .send(ws::Message::Text("Echo".to_string())) +//! .await +//! .unwrap(); +//! let response = connection.next().await.unwrap().unwrap(); +//! +//! assert_eq!(response, ws::Frame::Text("Echo".as_bytes().into())); +//! } +//! ``` + use std::convert::TryFrom; -use std::fmt::Write as FmtWrite; use std::net::SocketAddr; use std::rc::Rc; use std::{fmt, str}; @@ -9,9 +35,7 @@ use actix_codec::Framed; use actix_http::cookie::{Cookie, CookieJar}; use actix_http::{ws, Payload, RequestHead}; use actix_rt::time::timeout; -use percent_encoding::percent_encode; -use actix_http::cookie::USERINFO; pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message}; use crate::connect::BoxedSocket; @@ -246,16 +270,18 @@ impl WebsocketsRequest { // set cookies if let Some(ref mut jar) = self.cookies { - let mut cookie = String::new(); - for c in jar.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO); - let value = percent_encode(c.value().as_bytes(), USERINFO); - let _ = write!(&mut cookie, "; {}={}", name, value); + let cookie: String = jar + .delta() + // ensure only name=value is written to cookie header + .map(|c| Cookie::new(c.name(), c.value()).encoded().to_string()) + .collect::>() + .join("; "); + + if !cookie.is_empty() { + self.head + .headers + .insert(header::COOKIE, HeaderValue::from_str(&cookie).unwrap()); } - self.head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); } // origin @@ -367,7 +393,7 @@ impl WebsocketsRequest { // response and ws framed Ok(( ClientResponse::new(head, Payload::None), - framed.map_codec(|_| { + framed.into_map_codec(|_| { if server_mode { ws::Codec::new().max_size(max_size) } else { @@ -408,7 +434,7 @@ mod tests { #[actix_rt::test] async fn test_header_override() { - let req = Client::build() + let req = Client::builder() .header(header::CONTENT_TYPE, "111") .finish() .ws("/") diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index cc61f1006..a9552d0d5 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -120,7 +120,7 @@ async fn test_timeout() { .timeout(Duration::from_secs(15)) .finish(); - let client = awc::Client::build() + let client = awc::Client::builder() .connector(connector) .timeout(Duration::from_millis(50)) .finish(); @@ -141,7 +141,7 @@ async fn test_timeout_override() { }))) }); - let client = awc::Client::build() + let client = awc::Client::builder() .timeout(Duration::from_millis(50000)) .finish(); let request = client @@ -167,8 +167,7 @@ async fn test_connection_reuse() { }) .and_then( HttpService::new(map_config( - App::new() - .service(web::resource("/").route(web::to(|| HttpResponse::Ok()))), + App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) .tcp(), @@ -205,8 +204,7 @@ async fn test_connection_force_close() { }) .and_then( HttpService::new(map_config( - App::new() - .service(web::resource("/").route(web::to(|| HttpResponse::Ok()))), + App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) .tcp(), @@ -293,7 +291,7 @@ async fn test_connection_wait_queue() { }) .await; - let client = awc::Client::build() + let client = awc::Client::builder() .connector(awc::Connector::new().limit(1).finish()) .finish(); @@ -342,7 +340,7 @@ async fn test_connection_wait_queue_force_close() { }) .await; - let client = awc::Client::build() + let client = awc::Client::builder() .connector(awc::Connector::new().limit(1).finish()) .finish(); diff --git a/awc/tests/test_connector.rs b/awc/tests/test_connector.rs index b352eaab4..888f7a900 100644 --- a/awc/tests/test_connector.rs +++ b/awc/tests/test_connector.rs @@ -32,8 +32,7 @@ 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()))), + App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) .openssl(ssl_acceptor()) @@ -48,7 +47,7 @@ async fn test_connection_window_size() { .set_alpn_protos(b"\x02h2\x08http/1.1") .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); - let client = awc::Client::build() + let client = awc::Client::builder() .connector(awc::Connector::new().ssl(builder.build()).finish()) .initial_window_size(100) .initial_connection_window_size(100) diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index 0c6be76d4..0df6b154c 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -64,9 +64,8 @@ async fn _test_connection_reuse_h2() { .and_then( HttpService::build() .h2(map_config( - App::new().service( - web::resource("/").route(web::to(|| HttpResponse::Ok())), - ), + App::new() + .service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) .openssl(ssl_acceptor()) @@ -83,7 +82,7 @@ async fn _test_connection_reuse_h2() { .dangerous() .set_certificate_verifier(Arc::new(danger::NoCertificateVerification {})); - let client = awc::Client::build() + let client = awc::Client::builder() .connector(awc::Connector::new().rustls(Arc::new(config)).finish()) .finish(); diff --git a/awc/tests/test_ssl_client.rs b/awc/tests/test_ssl_client.rs index b2a2e1785..eced5f14b 100644 --- a/awc/tests/test_ssl_client.rs +++ b/awc/tests/test_ssl_client.rs @@ -45,9 +45,8 @@ async fn test_connection_reuse_h2() { .and_then( HttpService::build() .h2(map_config( - App::new().service( - web::resource("/").route(web::to(|| HttpResponse::Ok())), - ), + App::new() + .service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) .openssl(ssl_acceptor()) @@ -63,7 +62,7 @@ async fn test_connection_reuse_h2() { .set_alpn_protos(b"\x02h2\x08http/1.1") .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); - let client = awc::Client::build() + let client = awc::Client::builder() .connector(awc::Connector::new().ssl(builder.build()).finish()) .finish(); diff --git a/awc/tests/test_ws.rs b/awc/tests/test_ws.rs index d3f66814f..1c1068668 100644 --- a/awc/tests/test_ws.rs +++ b/awc/tests/test_ws.rs @@ -32,7 +32,7 @@ async fn test_simple() { .await?; // start websocket service - let framed = framed.into_framed(ws::Codec::new()); + let framed = framed.replace_codec(ws::Codec::new()); ws::Dispatcher::with(framed, ws_service).await } }) diff --git a/benches/server.rs b/benches/server.rs index 70531adf7..041d0fa57 100644 --- a/benches/server.rs +++ b/benches/server.rs @@ -27,15 +27,16 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ // benchmark sending all requests at the same time fn bench_async_burst(c: &mut Criterion) { + // We are using System here, since Runtime requires preinitialized tokio + // Maybe add to actix_rt docs + let mut rt = actix_rt::System::new("test"); + let srv = test::start(|| { App::new() .service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) }); - // We are using System here, since Runtime requires preinitialized tokio - // Maybe add to actix_rt docs let url = srv.url("/"); - let mut rt = actix_rt::System::new("test"); c.bench_function("get_body_async_burst", move |b| { b.iter_custom(|iters| { diff --git a/docs/graphs/.gitignore b/docs/graphs/.gitignore new file mode 100644 index 000000000..284a286c9 --- /dev/null +++ b/docs/graphs/.gitignore @@ -0,0 +1,2 @@ +# do not track rendered graphs +*.png diff --git a/docs/graphs/dependency-graphs.md b/docs/graphs/dependency-graphs.md new file mode 100644 index 000000000..6a4392d6b --- /dev/null +++ b/docs/graphs/dependency-graphs.md @@ -0,0 +1,11 @@ +# Actix Ecosystem Dependency Graphs + +See rendered versions of these dot graphs [on the wiki](https://github.com/actix/actix-web/wiki/Dependency-Graph). + +## Rendering + +Dot graphs were rendered using the `dot` command from [GraphViz](https://www.graphviz.org/doc/info/command.html): + +```sh +for f in $(ls docs/graphs/*.dot | xargs); do dot $f -Tpng -o${f:r}.png; done +``` diff --git a/docs/graphs/net-only.dot b/docs/graphs/net-only.dot new file mode 100644 index 000000000..0eebf2a6f --- /dev/null +++ b/docs/graphs/net-only.dot @@ -0,0 +1,25 @@ +digraph { + subgraph cluster_net { + label="actix/actix-net"; + "actix-codec" + "actix-connect" + "actix-macros" + "actix-rt" + "actix-server" + "actix-service" + "actix-testing" + "actix-threadpool" + "actix-tls" + "actix-tracing" + "actix-utils" + "actix-router" + } + + "actix-utils" -> { "actix-service" "actix-rt" "actix-codec" } + "actix-tracing" -> { "actix-service" } + "actix-tls" -> { "actix-service" "actix-codec" "actix-utils" } + "actix-testing" -> { "actix-rt" "actix-macros" "actix-server" "actix-service" } + "actix-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" } + "actix-rt" -> { "actix-macros" "actix-threadpool" } + "actix-connect" -> { "actix-service" "actix-codec" "actix-utils" "actix-rt" } +} diff --git a/docs/graphs/web-focus.dot b/docs/graphs/web-focus.dot new file mode 100644 index 000000000..7abd51268 --- /dev/null +++ b/docs/graphs/web-focus.dot @@ -0,0 +1,30 @@ +digraph { + subgraph cluster_web { + label="actix/actix-web" + "awc" + "actix-web" + "actix-files" + "actix-http" + "actix-multipart" + "actix-web-actors" + "actix-web-codegen" + } + + "actix-web" -> { "actix-codec" "actix-service" "actix-utils" "actix-router" "actix-rt" "actix-server" "actix-testing" "actix-macros" "actix-threadpool" "actix-tls" "actix-web-codegen" "actix-http" "awc" } + "awc" -> { "actix-codec" "actix-service" "actix-http" "actix-rt" } + "actix-web-actors" -> { "actix" "actix-web" "actix-http" "actix-codec" } + "actix-multipart" -> { "actix-web" "actix-service" "actix-utils" } + "actix-http" -> { "actix-service" "actix-codec" "actix-connect" "actix-utils" "actix-rt" "actix-threadpool" } + "actix-http" -> { "actix" "actix-tls" }[color=blue] // optional + "actix-files" -> { "actix-web" "actix-http" } + + // net + + "actix-utils" -> { "actix-service" "actix-rt" "actix-codec" } + "actix-tracing" -> { "actix-service" } + "actix-tls" -> { "actix-service" "actix-codec" "actix-utils" } + "actix-testing" -> { "actix-rt" "actix-macros" "actix-server" "actix-service" } + "actix-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" } + "actix-rt" -> { "actix-macros" "actix-threadpool" } + "actix-connect" -> { "actix-service" "actix-codec" "actix-utils" "actix-rt" } +} diff --git a/docs/graphs/web-only.dot b/docs/graphs/web-only.dot new file mode 100644 index 000000000..6e41fdc27 --- /dev/null +++ b/docs/graphs/web-only.dot @@ -0,0 +1,19 @@ +digraph { + subgraph cluster_web { + label="actix/actix-web" + "awc" + "actix-web" + "actix-files" + "actix-http" + "actix-multipart" + "actix-web-actors" + "actix-web-codegen" + } + + "actix-web" -> { "actix-web-codegen" "actix-http" "awc" } + "awc" -> { "actix-http" } + "actix-web-actors" -> { "actix" "actix-web" "actix-http" } + "actix-multipart" -> { "actix-web" } + "actix-http" -> { "actix" }[color=blue] // optional + "actix-files" -> { "actix-web" "actix-http" } +} diff --git a/examples/basic.rs b/examples/basic.rs index bd6f8146f..8b2bf2319 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -16,7 +16,7 @@ async fn no_params() -> &'static str { "Hello world!\r\n" } -#[actix_rt::main] +#[actix_web::main] async fn main() -> std::io::Result<()> { std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info"); env_logger::init(); diff --git a/examples/client.rs b/examples/client.rs index 874e08e1b..15cf24fa8 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -1,6 +1,6 @@ use actix_http::Error; -#[actix_rt::main] +#[actix_web::main] async fn main() -> Result<(), Error> { std::env::set_var("RUST_LOG", "actix_http=trace"); env_logger::init(); diff --git a/examples/uds.rs b/examples/uds.rs index 06aa87f4d..e34fa5ac9 100644 --- a/examples/uds.rs +++ b/examples/uds.rs @@ -20,7 +20,7 @@ async fn no_params() -> &'static str { } #[cfg(unix)] -#[actix_rt::main] +#[actix_web::main] async fn main() -> std::io::Result<()> { std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info"); env_logger::init(); diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 000000000..a50908ca3 --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +1.42.0 diff --git a/src/app.rs b/src/app.rs index 8178d57fe..fdedb0a75 100644 --- a/src/app.rs +++ b/src/app.rs @@ -42,6 +42,7 @@ pub struct App { impl App { /// Create application builder. Application can be configured with a builder-like pattern. + #[allow(clippy::new_without_default)] pub fn new() -> Self { let fref = Rc::new(RefCell::new(None)); App { @@ -488,7 +489,7 @@ mod tests { #[actix_rt::test] async fn test_default_resource() { let mut srv = init_service( - App::new().service(web::resource("/test").to(|| HttpResponse::Ok())), + App::new().service(web::resource("/test").to(HttpResponse::Ok)), ) .await; let req = TestRequest::with_uri("/test").to_request(); @@ -501,13 +502,13 @@ mod tests { let mut srv = init_service( App::new() - .service(web::resource("/test").to(|| HttpResponse::Ok())) + .service(web::resource("/test").to(HttpResponse::Ok)) .service( web::resource("/test2") .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::Created())) }) - .route(web::get().to(|| HttpResponse::Ok())), + .route(web::get().to(HttpResponse::Ok)), ) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::MethodNotAllowed())) @@ -584,7 +585,7 @@ mod tests { DefaultHeaders::new() .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), ) - .route("/test", web::get().to(|| HttpResponse::Ok())), + .route("/test", web::get().to(HttpResponse::Ok)), ) .await; let req = TestRequest::with_uri("/test").to_request(); @@ -600,7 +601,7 @@ mod tests { async fn test_router_wrap() { let mut srv = init_service( App::new() - .route("/test", web::get().to(|| HttpResponse::Ok())) + .route("/test", web::get().to(HttpResponse::Ok)) .wrap( DefaultHeaders::new() .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), @@ -631,7 +632,7 @@ mod tests { Ok(res) } }) - .service(web::resource("/test").to(|| HttpResponse::Ok())), + .service(web::resource("/test").to(HttpResponse::Ok)), ) .await; let req = TestRequest::with_uri("/test").to_request(); @@ -647,7 +648,7 @@ mod tests { async fn test_router_wrap_fn() { let mut srv = init_service( App::new() - .route("/test", web::get().to(|| HttpResponse::Ok())) + .route("/test", web::get().to(HttpResponse::Ok)) .wrap_fn(|req, srv| { let fut = srv.call(req); async { @@ -678,10 +679,9 @@ mod tests { .route( "/test", web::get().to(|req: HttpRequest| { - HttpResponse::Ok().body(format!( - "{}", - req.url_for("youtube", &["12345"]).unwrap() - )) + HttpResponse::Ok().body( + req.url_for("youtube", &["12345"]).unwrap().to_string(), + ) }), ), ) diff --git a/src/app_service.rs b/src/app_service.rs index 233cfc0d5..98d8c8a8d 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -10,6 +10,7 @@ use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; use actix_service::boxed::{self, BoxService, BoxServiceFactory}; use actix_service::{fn_service, Service, ServiceFactory}; use futures_util::future::{join_all, ok, FutureExt, LocalBoxFuture}; +use tinyvec::tiny_vec; use crate::config::{AppConfig, AppService}; use crate::data::{DataFactory, FnDataFactory}; @@ -245,7 +246,7 @@ where inner.path.reset(); inner.head = head; inner.payload = payload; - inner.app_data.push(self.data.clone()); + inner.app_data = tiny_vec![self.data.clone()]; req } else { HttpRequest::new( @@ -474,7 +475,7 @@ mod tests { let mut app = init_service( App::new() .data(DropData(data.clone())) - .service(web::resource("/test").to(|| HttpResponse::Ok())), + .service(web::resource("/test").to(HttpResponse::Ok)), ) .await; let req = TestRequest::with_uri("/test").to_request(); diff --git a/src/config.rs b/src/config.rs index 19a5ccc7b..0f49288ec 100644 --- a/src/config.rs +++ b/src/config.rs @@ -311,10 +311,9 @@ mod tests { .route( "/test", web::get().to(|req: HttpRequest| { - HttpResponse::Ok().body(format!( - "{}", - req.url_for("youtube", &["12345"]).unwrap() - )) + HttpResponse::Ok().body( + req.url_for("youtube", &["12345"]).unwrap().to_string(), + ) }), ), ) @@ -330,9 +329,9 @@ mod tests { async fn test_service() { let mut srv = init_service(App::new().configure(|cfg| { cfg.service( - web::resource("/test").route(web::get().to(|| HttpResponse::Created())), + web::resource("/test").route(web::get().to(HttpResponse::Created)), ) - .route("/index.html", web::get().to(|| HttpResponse::Ok())); + .route("/index.html", web::get().to(HttpResponse::Ok)); })) .await; diff --git a/src/data.rs b/src/data.rs index 34ada863d..6405fd901 100644 --- a/src/data.rs +++ b/src/data.rs @@ -66,7 +66,7 @@ pub(crate) type FnDataFactory = /// } /// ``` #[derive(Debug)] -pub struct Data(Arc); +pub struct Data(Arc); impl Data { /// Create new `Data` instance. @@ -89,7 +89,7 @@ impl Data { } } -impl Deref for Data { +impl Deref for Data { type Target = Arc; fn deref(&self) -> &Arc { @@ -97,19 +97,19 @@ impl Deref for Data { } } -impl Clone for Data { +impl Clone for Data { fn clone(&self) -> Data { Data(self.0.clone()) } } -impl From> for Data { +impl From> for Data { fn from(arc: Arc) -> Self { Data(arc) } } -impl FromRequest for Data { +impl FromRequest for Data { type Config = (); type Error = Error; type Future = Ready>; @@ -131,7 +131,7 @@ impl FromRequest for Data { } } -impl DataFactory for Data { +impl DataFactory for Data { fn create(&self, extensions: &mut Extensions) -> bool { if !extensions.contains::>() { extensions.insert(Data(self.0.clone())); @@ -200,14 +200,14 @@ mod tests { #[actix_rt::test] async fn test_route_data_extractor() { - let mut srv = - init_service(App::new().service(web::resource("/").data(10usize).route( - web::get().to(|data: web::Data| { - let _ = data.clone(); - HttpResponse::Ok() - }), - ))) - .await; + let mut srv = init_service( + App::new().service( + web::resource("/") + .data(10usize) + .route(web::get().to(|_data: web::Data| HttpResponse::Ok())), + ), + ) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); @@ -233,7 +233,6 @@ mod tests { web::resource("/").data(10usize).route(web::get().to( |data: web::Data| { assert_eq!(**data, 10); - let _ = data.clone(); HttpResponse::Ok() }, )), @@ -294,4 +293,24 @@ mod tests { let data_from_arc = Data::from(Arc::new(String::from("test-123"))); assert_eq!(data_new.0, data_from_arc.0) } + + #[actix_rt::test] + async fn test_data_from_dyn_arc() { + trait TestTrait { + fn get_num(&self) -> i32; + } + struct A {} + impl TestTrait for A { + fn get_num(&self) -> i32 { + 42 + } + } + // This works when Sized is required + let dyn_arc_box: Arc> = Arc::new(Box::new(A {})); + let data_arc_box = Data::from(dyn_arc_box); + // This works when Data Sized Bound is removed + let dyn_arc: Arc = Arc::new(A {}); + let data_arc = Data::from(dyn_arc); + assert_eq!(data_arc_box.get_num(), data_arc.get_num()) + } } diff --git a/src/info.rs b/src/info.rs index 5b506d85a..1d9b402a7 100644 --- a/src/info.rs +++ b/src/info.rs @@ -25,7 +25,7 @@ impl ConnectionInfo { Ref::map(req.extensions(), |e| e.get().unwrap()) } - #[allow(clippy::cognitive_complexity)] + #[allow(clippy::cognitive_complexity, clippy::borrow_interior_mutable_const)] fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { let mut host = None; let mut scheme = None; diff --git a/src/lib.rs b/src/lib.rs index c7c569756..07f988ab1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,88 +1,75 @@ -#![warn(rust_2018_idioms, warnings)] -#![allow( - clippy::needless_doctest_main, - clippy::type_complexity, - clippy::borrow_interior_mutable_const -)] -//! Actix web is a small, pragmatic, and extremely fast web framework -//! for Rust. +//! Actix web is a powerful, 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}; +//! use actix_web::{get, web, App, HttpServer, Responder}; //! -//! async fn index(info: web::Path<(String, u32)>) -> impl Responder { -//! format!("Hello {}! id:{}", info.0, info.1) +//! #[get("/{id}/{name}/index.html")] +//! async fn index(web::Path((id, name)): web::Path<(u32, String)>) -> impl Responder { +//! format!("Hello {}! id:{}", name, id) //! } //! -//! #[actix_rt::main] +//! #[actix_web::main] //! async fn main() -> std::io::Result<()> { -//! HttpServer::new(|| App::new().service( -//! web::resource("/{name}/{id}/index.html").to(index)) -//! ) +//! HttpServer::new(|| App::new().service(index)) //! .bind("127.0.0.1:8080")? //! .run() //! .await //! } //! ``` //! -//! ## Documentation & community resources +//! ## Documentation & Community Resources //! -//! Besides the API documentation (which you are currently looking -//! at!), several other resources are available: +//! In addition to this API documentation, several other resources are available: //! -//! * [User Guide](https://actix.rs/docs/) -//! * [Chat on gitter](https://gitter.im/actix/actix) -//! * [GitHub repository](https://github.com/actix/actix-web) -//! * [Cargo package](https://crates.io/crates/actix-web) +//! * [Website & User Guide](https://actix.rs/) +//! * [Examples Repository](https://github.com/actix/examples) +//! * [Community Chat on Gitter](https://gitter.im/actix/actix-web) //! -//! To get started navigating the API documentation you may want to -//! consider looking at the following pages: +//! To get started navigating the API docs, you may consider looking at the following pages first: //! -//! * [App](struct.App.html): This struct represents an actix-web -//! application and is used to configure routes and other common -//! settings. +//! * [App](struct.App.html): This struct represents an Actix web application and is used to +//! configure routes and other common application settings. //! -//! * [HttpServer](struct.HttpServer.html): This struct -//! represents an HTTP server instance and is used to instantiate and -//! configure servers. +//! * [HttpServer](struct.HttpServer.html): This struct represents an HTTP server instance and is +//! used to instantiate and configure servers. //! -//! * [web](web/index.html): This module -//! provides essential helper functions and types for application registration. +//! * [web](web/index.html): This module provides essential types for route registration as well as +//! common utilities for request handlers. //! -//! * [HttpRequest](struct.HttpRequest.html) and -//! [HttpResponse](struct.HttpResponse.html): These structs -//! represent HTTP requests and responses and expose various methods -//! for inspecting, creating and otherwise utilizing them. +//! * [HttpRequest](struct.HttpRequest.html) and [HttpResponse](struct.HttpResponse.html): These +//! structs represent HTTP requests and responses and expose methods for creating, inspecting, +//! and otherwise utilizing them. //! //! ## Features //! -//! * Supported *HTTP/1.x* and *HTTP/2.0* protocols +//! * Supports *HTTP/1.x* and *HTTP/2* //! * Streaming and pipelining //! * Keep-alive and slow requests handling -//! * `WebSockets` server/client +//! * Client/server [WebSockets](https://actix.rs/docs/websockets/) support //! * Transparent content compression/decompression (br, gzip, deflate) -//! * Configurable request routing +//! * Powerful [request routing](https://actix.rs/docs/url-dispatch/) //! * Multipart streams -//! * SSL support with OpenSSL or `native-tls` -//! * Middlewares (`Logger`, `Session`, `CORS`, `DefaultHeaders`) +//! * Static assets +//! * SSL support using OpenSSL or Rustls +//! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) +//! * Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) //! * Supports [Actix actor framework](https://github.com/actix/actix) -//! * Supported Rust version: 1.40 or later +//! * Runs on stable Rust 1.42+ //! -//! ## Package feature +//! ## Crate Features //! -//! * `client` - enables http client (default enabled) -//! * `compress` - enables content encoding compression support (default enabled) -//! * `openssl` - enables ssl support via `openssl` crate, supports `http/2` -//! * `rustls` - enables ssl support via `rustls` crate, supports `http/2` -//! * `secure-cookies` - enables secure cookies support, includes `ring` crate as -//! dependency -#![allow(clippy::type_complexity, clippy::new_without_default)] +//! * `compress` - content encoding compression support (enabled by default) +//! * `openssl` - HTTPS support via `openssl` crate, supports `HTTP/2` +//! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2` +//! * `secure-cookies` - secure cookies support + +#![deny(rust_2018_idioms)] +#![allow(clippy::needless_doctest_main, clippy::type_complexity)] +#![allow(clippy::rc_buffer)] // FXIME: We should take a closer look for the warnings at some point. +#![doc(html_logo_url = "https://actix.rs/img/logo.png")] +#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] mod app; mod app_service; @@ -106,12 +93,10 @@ pub mod test; mod types; pub mod web; -#[doc(hidden)] -pub use actix_web_codegen::*; - -// re-export for convenience pub use actix_http::Response as HttpResponse; pub use actix_http::{body, cookie, http, Error, HttpMessage, ResponseError, Result}; +pub use actix_rt as rt; +pub use actix_web_codegen::*; pub use crate::app::App; pub use crate::extract::FromRequest; @@ -212,26 +197,26 @@ pub mod dev { } pub mod client { - //! An HTTP Client + //! Actix web async HTTP client. //! //! ```rust //! use actix_web::client::Client; //! - //! #[actix_rt::main] + //! #[actix_web::main] //! async fn main() { //! let mut client = Client::default(); //! //! // Create request builder and send request //! let response = client.get("http://www.rust-lang.org") - //! .header("User-Agent", "Actix-web") - //! .send().await; // <- Send http request + //! .header("User-Agent", "actix-web/3.0") + //! .send() // <- Send request + //! .await; // <- Wait for response //! //! println!("Response: {:?}", response); //! } //! ``` - pub use awc::error::{ - ConnectError, InvalidUrl, PayloadError, SendRequestError, WsClientError, - }; + + pub use awc::error::*; pub use awc::{ test, Client, ClientBuilder, ClientRequest, ClientResponse, Connector, }; diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 6de451c84..fe3ba841c 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -90,6 +90,7 @@ where self.service.poll_ready(cx) } + #[allow(clippy::borrow_interior_mutable_const)] fn call(&mut self, req: ServiceRequest) -> Self::Future { // negotiate content-encoding let encoding = if let Some(val) = req.headers().get(&ACCEPT_ENCODING) { diff --git a/src/middleware/condition.rs b/src/middleware/condition.rs index 7ff81437b..ab1c69746 100644 --- a/src/middleware/condition.rs +++ b/src/middleware/condition.rs @@ -17,7 +17,7 @@ use futures_util::future::{ok, Either, FutureExt, LocalBoxFuture}; /// # fn main() { /// let enable_normalize = std::env::var("NORMALIZE_PATH") == Ok("true".into()); /// let app = App::new() -/// .wrap(Condition::new(enable_normalize, NormalizePath)); +/// .wrap(Condition::new(enable_normalize, NormalizePath::default())); /// # } /// ``` pub struct Condition { diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index ef2e56e69..6d43aba95 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -128,6 +128,7 @@ where self.service.poll_ready(cx) } + #[allow(clippy::borrow_interior_mutable_const)] fn call(&mut self, req: ServiceRequest) -> Self::Future { let inner = self.inner.clone(); let fut = self.service.call(req); diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 8b881c0a4..51d4722d7 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -85,7 +85,7 @@ use crate::HttpResponse; /// [`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. +/// for the remote client to simulate being another client. /// pub struct Logger(Rc); @@ -478,7 +478,7 @@ impl FormatText { } FormatText::RemoteAddr => { let s = if let Some(ref peer) = req.connection_info().remote_addr() { - FormatText::Str(peer.to_string()) + FormatText::Str((*peer).to_string()) } else { FormatText::Str("-".to_string()) }; @@ -626,7 +626,7 @@ mod tests { Ok(()) }; let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains(&format!("{}", now.format("%Y-%m-%dT%H:%M:%S")))); + assert!(s.contains(&now.format("%Y-%m-%dT%H:%M:%S"))); } #[actix_rt::test] diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index f0d42cc2a..12c12a98c 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -9,7 +9,7 @@ mod condition; mod defaultheaders; pub mod errhandlers; mod logger; -mod normalize; +pub mod normalize; pub use self::condition::Condition; pub use self::defaultheaders::DefaultHeaders; diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index d23f03445..e0ecd90dc 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -10,19 +10,42 @@ use regex::Regex; use crate::service::{ServiceRequest, ServiceResponse}; use crate::Error; +/// To be used when constructing `NormalizePath` to define it's behavior. +#[non_exhaustive] +#[derive(Clone, Copy)] +pub enum TrailingSlash { + /// Always add a trailing slash to the end of the path. + /// This will require all routes to end in a trailing slash for them to be accessible. + Always, + /// Only merge any present multiple trailing slashes. + /// + /// Note: This option provides the best compatibility with the v2 version of this middlware. + MergeOnly, + /// Trim trailing slashes from the end of the path. + Trim, +} + +impl Default for TrailingSlash { + fn default() -> Self { + TrailingSlash::Always + } +} + #[derive(Default, Clone, Copy)] /// `Middleware` to normalize request's URI in place /// /// Performs following: /// /// - Merges multiple slashes into one. +/// - Appends a trailing slash if one is not present, removes one if present, or keeps trailing +/// slashes as-is, depending on the supplied `TrailingSlash` variant. /// /// ```rust /// use actix_web::{web, http, middleware, App, HttpResponse}; /// /// # fn main() { /// let app = App::new() -/// .wrap(middleware::NormalizePath) +/// .wrap(middleware::NormalizePath::default()) /// .service( /// web::resource("/test") /// .route(web::get().to(|| HttpResponse::Ok())) @@ -31,7 +54,14 @@ use crate::Error; /// # } /// ``` -pub struct NormalizePath; +pub struct NormalizePath(TrailingSlash); + +impl NormalizePath { + /// Create new `NormalizePath` middleware with the specified trailing slash style. + pub fn new(trailing_slash_style: TrailingSlash) -> Self { + NormalizePath(trailing_slash_style) + } +} impl Transform for NormalizePath where @@ -49,13 +79,16 @@ where ok(NormalizePathNormalization { service, merge_slash: Regex::new("//+").unwrap(), + trailing_slash_behavior: self.0, }) } } +#[doc(hidden)] pub struct NormalizePathNormalization { service: S, merge_slash: Regex, + trailing_slash_behavior: TrailingSlash, } impl Service for NormalizePathNormalization @@ -75,14 +108,34 @@ where fn call(&mut self, mut req: ServiceRequest) -> Self::Future { let head = req.head_mut(); - // always add trailing slash, might be an extra one - let path = head.uri.path().to_string() + "/"; - let original_len = path.len(); + let original_path = head.uri.path(); + + // Either adds a string to the end (duplicates will be removed anyways) or trims all slashes from the end + let path = match self.trailing_slash_behavior { + TrailingSlash::Always => original_path.to_string() + "/", + TrailingSlash::MergeOnly => original_path.to_string(), + TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(), + }; // normalize multiple /'s to one / let path = self.merge_slash.replace_all(&path, "/"); - if original_len != path.len() { + // Ensure root paths are still resolvable. If resulting path is blank after previous step + // it means the path was one or more slashes. Reduce to single slash. + let path = if path.is_empty() { "/" } else { path.as_ref() }; + + // Check whether the path has been changed + // + // This check was previously implemented as string length comparison + // + // That approach fails when a trailing slash is added, + // and a duplicate slash is removed, + // since the length of the strings remains the same + // + // For example, the path "/v1//s" will be normalized to "/v1/s/" + // Both of the paths have the same length, + // so the change can not be deduced from the length comparison + if path != original_path { let mut parts = head.uri.clone().into_parts(); let pq = parts.path_and_query.as_ref().unwrap(); @@ -116,10 +169,23 @@ mod tests { let mut app = init_service( App::new() .wrap(NormalizePath::default()) - .service(web::resource("/v1/something/").to(|| HttpResponse::Ok())), + .service(web::resource("/").to(HttpResponse::Ok)) + .service(web::resource("/v1/something/").to(HttpResponse::Ok)), ) .await; + let req = TestRequest::with_uri("/").to_request(); + let res = call_service(&mut app, req).await; + assert!(res.status().is_success()); + + let req = TestRequest::with_uri("/?query=test").to_request(); + let res = call_service(&mut app, req).await; + assert!(res.status().is_success()); + + let req = TestRequest::with_uri("///").to_request(); + let res = call_service(&mut app, req).await; + assert!(res.status().is_success()); + let req = TestRequest::with_uri("/v1//something////").to_request(); let res = call_service(&mut app, req).await; assert!(res.status().is_success()); @@ -131,6 +197,82 @@ mod tests { let req3 = TestRequest::with_uri("//v1//////something").to_request(); let res3 = call_service(&mut app, req3).await; assert!(res3.status().is_success()); + + let req4 = TestRequest::with_uri("/v1//something").to_request(); + let res4 = call_service(&mut app, req4).await; + assert!(res4.status().is_success()); + } + + #[actix_rt::test] + async fn trim_trailing_slashes() { + let mut app = init_service( + App::new() + .wrap(NormalizePath(TrailingSlash::Trim)) + .service(web::resource("/").to(HttpResponse::Ok)) + .service(web::resource("/v1/something").to(HttpResponse::Ok)), + ) + .await; + + // root paths should still work + let req = TestRequest::with_uri("/").to_request(); + let res = call_service(&mut app, req).await; + assert!(res.status().is_success()); + + let req = TestRequest::with_uri("/?query=test").to_request(); + let res = call_service(&mut app, req).await; + assert!(res.status().is_success()); + + let req = TestRequest::with_uri("///").to_request(); + let res = call_service(&mut app, req).await; + assert!(res.status().is_success()); + + let req = TestRequest::with_uri("/v1/something////").to_request(); + let res = call_service(&mut app, req).await; + assert!(res.status().is_success()); + + 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()); + + let req4 = TestRequest::with_uri("//v1//something").to_request(); + let res4 = call_service(&mut app, req4).await; + assert!(res4.status().is_success()); + } + + #[actix_rt::test] + async fn keep_trailing_slash_unchange() { + let mut app = init_service( + App::new() + .wrap(NormalizePath(TrailingSlash::MergeOnly)) + .service(web::resource("/").to(HttpResponse::Ok)) + .service(web::resource("/v1/something").to(HttpResponse::Ok)) + .service(web::resource("/v1/").to(HttpResponse::Ok)), + ) + .await; + + let tests = vec![ + ("/", true), // root paths should still work + ("/?query=test", true), + ("///", true), + ("/v1/something////", false), + ("/v1/something/", false), + ("//v1//something", true), + ("/v1/", true), + ("/v1", false), + ("/v1////", true), + ("//v1//", true), + ("///v1", false), + ]; + + for (path, success) in tests { + let req = TestRequest::with_uri(path).to_request(); + let res = call_service(&mut app, req).await; + assert_eq!(res.status().is_success(), success); + } } #[actix_rt::test] @@ -140,7 +282,7 @@ mod tests { ok(req.into_response(HttpResponse::Ok().finish())) }; - let mut normalize = NormalizePath + let mut normalize = NormalizePath::default() .new_transform(srv.into_service()) .await .unwrap(); @@ -156,6 +298,10 @@ mod tests { let req3 = TestRequest::with_uri("//v1///something").to_srv_request(); let res3 = normalize.call(req3).await.unwrap(); assert!(res3.status().is_success()); + + let req4 = TestRequest::with_uri("/v1//something").to_srv_request(); + let res4 = normalize.call(req4).await.unwrap(); + assert!(res4.status().is_success()); } #[actix_rt::test] @@ -167,7 +313,7 @@ mod tests { ok(req.into_response(HttpResponse::Ok().finish())) }; - let mut normalize = NormalizePath + let mut normalize = NormalizePath::default() .new_transform(srv.into_service()) .await .unwrap(); @@ -178,15 +324,15 @@ mod tests { } #[actix_rt::test] - async fn should_normalize_nothing_notrail() { + async fn should_normalize_notrail() { const URI: &str = "/v1/something"; let srv = |req: ServiceRequest| { - assert_eq!(URI, req.path()); + assert_eq!(URI.to_string() + "/", req.path()); ok(req.into_response(HttpResponse::Ok().finish())) }; - let mut normalize = NormalizePath + let mut normalize = NormalizePath::default() .new_transform(srv.into_service()) .await .unwrap(); diff --git a/src/request.rs b/src/request.rs index f8abeb1bb..a1b42f926 100644 --- a/src/request.rs +++ b/src/request.rs @@ -126,6 +126,25 @@ impl HttpRequest { &mut Rc::get_mut(&mut self.0).unwrap().path } + /// The resource definition pattern that matched the path. Useful for logging and metrics. + /// + /// For example, when a resource with pattern `/user/{id}/profile` is defined and a call is made + /// to `/user/123/profile` this function would return `Some("/user/{id}/profile")`. + /// + /// Returns a None when no resource is fully matched, including default services. + #[inline] + pub fn match_pattern(&self) -> Option { + self.0.rmap.match_pattern(self.path()) + } + + /// The resource name that matched the path. Useful for logging and metrics. + /// + /// Returns a None when no resource is fully matched, including default services. + #[inline] + pub fn match_name(&self) -> Option<&str> { + self.0.rmap.match_name(self.path()) + } + /// Request extensions #[inline] pub fn extensions(&self) -> Ref<'_, Extensions> { @@ -141,7 +160,6 @@ impl HttpRequest { /// Generate url for named resource /// /// ```rust - /// # extern crate actix_web; /// # use actix_web::{web, App, HttpRequest, HttpResponse}; /// # /// fn index(req: HttpRequest) -> HttpResponse { @@ -258,6 +276,7 @@ impl HttpMessage for HttpRequest { impl Drop for HttpRequest { fn drop(&mut self) { + // if possible, contribute to current worker's HttpRequest allocation pool if Rc::strong_count(&self.0) == 1 { let v = &mut self.0.pool.0.borrow_mut(); if v.len() < 128 { @@ -322,25 +341,32 @@ impl fmt::Debug for HttpRequest { } } -/// Request's objects pool +/// Slab-allocated `HttpRequest` Pool +/// +/// Since request processing may yield for asynchronous events to complete, a worker may have many +/// requests in-flight at any time. Pooling requests like this amortizes the performance and memory +/// costs of allocating and de-allocating HttpRequest objects as frequently as they otherwise would. +/// +/// Request objects are added when they are dropped (see `::drop`) and re-used +/// in `::call` when there are available objects in the list. +/// +/// The pool's initial capacity is 128 items. pub(crate) struct HttpRequestPool(RefCell>>); impl HttpRequestPool { + /// Allocates a slab of memory for pool use. pub(crate) fn create() -> &'static HttpRequestPool { let pool = HttpRequestPool(RefCell::new(Vec::with_capacity(128))); Box::leak(Box::new(pool)) } - /// Get message from the pool + /// Re-use a previously allocated (but now completed/discarded) HttpRequest object. #[inline] pub(crate) fn get_request(&self) -> Option { - if let Some(inner) = self.0.borrow_mut().pop() { - Some(HttpRequest(inner)) - } else { - None - } + self.0.borrow_mut().pop().map(HttpRequest) } + /// Clears all allocated HttpRequest objects. pub(crate) fn clear(&self) { self.0.borrow_mut().clear() } @@ -452,6 +478,24 @@ mod tests { ); } + #[test] + fn test_match_name() { + let mut rdef = ResourceDef::new("/index.html"); + *rdef.name_mut() = "index".to_string(); + + let mut rmap = ResourceMap::new(ResourceDef::new("")); + rmap.add(&mut rdef, None); + + assert!(rmap.has_resource("/index.html")); + + let req = TestRequest::default() + .uri("/index.html") + .rmap(rmap) + .to_http_request(); + + assert_eq!(req.match_name(), Some("index")); + } + #[test] fn test_url_for_external() { let mut rdef = ResourceDef::new("https://youtube.com/watch/{video_id}"); @@ -599,4 +643,36 @@ mod tests { assert!(tracker.borrow().dropped); } + + #[actix_rt::test] + async fn extract_path_pattern() { + let mut srv = init_service( + App::new().service( + web::scope("/user/{id}") + .service(web::resource("/profile").route(web::get().to( + move |req: HttpRequest| { + assert_eq!( + req.match_pattern(), + Some("/user/{id}/profile".to_owned()) + ); + + HttpResponse::Ok().finish() + }, + ))) + .default_service(web::to(move |req: HttpRequest| { + assert!(req.match_pattern().is_none()); + HttpResponse::Ok().finish() + })), + ), + ) + .await; + + let req = TestRequest::get().uri("/user/22/profile").to_request(); + let res = call_service(&mut srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + + let req = TestRequest::get().uri("/user/22/not-exist").to_request(); + let res = call_service(&mut srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + } } diff --git a/src/resource.rs b/src/resource.rs index 5da1de62f..dd9b23012 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -607,7 +607,7 @@ mod tests { header::CONTENT_TYPE, HeaderValue::from_static("0001"), )) - .route(web::get().to(|| HttpResponse::Ok())), + .route(web::get().to(HttpResponse::Ok)), ), ) .await; @@ -637,7 +637,7 @@ mod tests { }) } }) - .route(web::get().to(|| HttpResponse::Ok())), + .route(web::get().to(HttpResponse::Ok)), ), ) .await; @@ -684,9 +684,7 @@ mod tests { async fn test_default_resource() { let mut srv = init_service( App::new() - .service( - web::resource("/test").route(web::get().to(|| HttpResponse::Ok())), - ) + .service(web::resource("/test").route(web::get().to(HttpResponse::Ok))) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::BadRequest())) }), @@ -705,7 +703,7 @@ mod tests { let mut srv = init_service( App::new().service( web::resource("/test") - .route(web::get().to(|| HttpResponse::Ok())) + .route(web::get().to(HttpResponse::Ok)) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::BadRequest())) }), @@ -731,17 +729,17 @@ mod tests { .service( web::resource("/test/{p}") .guard(guard::Get()) - .to(|| HttpResponse::Ok()), + .to(HttpResponse::Ok), ) .service( web::resource("/test/{p}") .guard(guard::Put()) - .to(|| HttpResponse::Created()), + .to(HttpResponse::Created), ) .service( web::resource("/test/{p}") .guard(guard::Delete()) - .to(|| HttpResponse::NoContent()), + .to(HttpResponse::NoContent), ), ) .await; @@ -783,7 +781,8 @@ mod tests { data3: web::Data| { assert_eq!(**data1, 10); assert_eq!(**data2, '*'); - assert_eq!(**data3, 1.0); + let error = std::f64::EPSILON; + assert!((**data3 - 1.0).abs() < error); HttpResponse::Ok() }, ), diff --git a/src/responder.rs b/src/responder.rs index e102d23e1..fc80831b8 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -480,7 +480,7 @@ pub(crate) mod tests { assert_eq!(resp.status(), StatusCode::OK); match resp.response().body() { ResponseBody::Body(Body::Bytes(ref b)) => { - let bytes: Bytes = b.clone().into(); + let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"some")); } _ => panic!(), diff --git a/src/rmap.rs b/src/rmap.rs index 47092608c..05c1f3f15 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -1,5 +1,5 @@ use std::cell::RefCell; -use std::rc::Rc; +use std::rc::{Rc, Weak}; use actix_router::ResourceDef; use fxhash::FxHashMap; @@ -11,7 +11,7 @@ use crate::request::HttpRequest; #[derive(Clone, Debug)] pub struct ResourceMap { root: ResourceDef, - parent: RefCell>>, + parent: RefCell>, named: FxHashMap, patterns: Vec<(ResourceDef, Option>)>, } @@ -20,7 +20,7 @@ impl ResourceMap { pub fn new(root: ResourceDef) -> Self { ResourceMap { root, - parent: RefCell::new(None), + parent: RefCell::new(Weak::new()), named: FxHashMap::default(), patterns: Vec::new(), } @@ -38,14 +38,12 @@ impl ResourceMap { pub(crate) fn finish(&self, current: Rc) { for (_, nested) in &self.patterns { if let Some(ref nested) = nested { - *nested.parent.borrow_mut() = Some(current.clone()); + *nested.parent.borrow_mut() = Rc::downgrade(¤t); nested.finish(nested.clone()); } } } -} -impl ResourceMap { /// Generate url for named resource /// /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. @@ -95,6 +93,66 @@ impl ResourceMap { false } + /// Returns the name of the route that matches the given path or None if no full match + /// is possible. + pub fn match_name(&self, path: &str) -> Option<&str> { + let path = if path.is_empty() { "/" } else { path }; + + for (pattern, rmap) in &self.patterns { + if let Some(ref rmap) = rmap { + if let Some(plen) = pattern.is_prefix_match(path) { + return rmap.match_name(&path[plen..]); + } + } else if pattern.is_match(path) { + return match pattern.name() { + "" => None, + s => Some(s), + }; + } + } + + None + } + + /// Returns the full resource pattern matched against a path or None if no full match + /// is possible. + pub fn match_pattern(&self, path: &str) -> Option { + let path = if path.is_empty() { "/" } else { path }; + + // ensure a full match exists + if !self.has_resource(path) { + return None; + } + + Some(self.traverse_resource_pattern(path)) + } + + /// Takes remaining path and tries to match it up against a resource definition within the + /// current resource map recursively, returning a concatenation of all resource prefixes and + /// patterns matched in the tree. + /// + /// Should only be used after checking the resource exists in the map so that partial match + /// patterns are not returned. + fn traverse_resource_pattern(&self, remaining: &str) -> String { + for (pattern, rmap) in &self.patterns { + if let Some(ref rmap) = rmap { + if let Some(prefix_len) = pattern.is_prefix_match(remaining) { + let prefix = pattern.pattern().to_owned(); + + return [ + prefix, + rmap.traverse_resource_pattern(&remaining[prefix_len..]), + ] + .concat(); + } + } else if pattern.is_match(remaining) { + return pattern.pattern().to_owned(); + } + } + + String::new() + } + fn patterns_for( &self, name: &str, @@ -152,7 +210,7 @@ impl ResourceMap { U: Iterator, I: AsRef, { - if let Some(ref parent) = *self.parent.borrow() { + if let Some(ref parent) = self.parent.borrow().upgrade() { parent.fill_root(path, elements)?; } if self.root.resource_path(path, elements) { @@ -172,7 +230,7 @@ impl ResourceMap { U: Iterator, I: AsRef, { - if let Some(ref parent) = *self.parent.borrow() { + if let Some(ref parent) = self.parent.borrow().upgrade() { if let Some(pattern) = parent.named.get(name) { self.fill_root(path, elements)?; if pattern.resource_path(path, elements) { @@ -188,3 +246,163 @@ impl ResourceMap { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extract_matched_pattern() { + let mut root = ResourceMap::new(ResourceDef::root_prefix("")); + + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + user_map.add(&mut ResourceDef::new("/"), None); + user_map.add(&mut ResourceDef::new("/profile"), None); + user_map.add(&mut ResourceDef::new("/article/{id}"), None); + user_map.add(&mut ResourceDef::new("/post/{post_id}"), None); + user_map.add( + &mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"), + None, + ); + + root.add(&mut ResourceDef::new("/info"), None); + root.add(&mut ResourceDef::new("/v{version:[[:digit:]]{1}}"), None); + root.add( + &mut ResourceDef::root_prefix("/user/{id}"), + Some(Rc::new(user_map)), + ); + + let root = Rc::new(root); + root.finish(Rc::clone(&root)); + + // sanity check resource map setup + + assert!(root.has_resource("/info")); + assert!(!root.has_resource("/bar")); + + assert!(root.has_resource("/v1")); + assert!(root.has_resource("/v2")); + assert!(!root.has_resource("/v33")); + + assert!(root.has_resource("/user/22")); + assert!(root.has_resource("/user/22/")); + assert!(root.has_resource("/user/22/profile")); + + // extract patterns from paths + + assert!(root.match_pattern("/bar").is_none()); + assert!(root.match_pattern("/v44").is_none()); + + assert_eq!(root.match_pattern("/info"), Some("/info".to_owned())); + assert_eq!( + root.match_pattern("/v1"), + Some("/v{version:[[:digit:]]{1}}".to_owned()) + ); + assert_eq!( + root.match_pattern("/v2"), + Some("/v{version:[[:digit:]]{1}}".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/profile"), + Some("/user/{id}/profile".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/602CFB82-7709-4B17-ADCF-4C347B6F2203/profile"), + Some("/user/{id}/profile".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/article/44"), + Some("/user/{id}/article/{id}".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/post/my-post"), + Some("/user/{id}/post/{post_id}".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/post/other-post/comment/42"), + Some("/user/{id}/post/{post_id}/comment/{comment_id}".to_owned()) + ); + } + + #[test] + fn extract_matched_name() { + let mut root = ResourceMap::new(ResourceDef::root_prefix("")); + + let mut rdef = ResourceDef::new("/info"); + *rdef.name_mut() = "root_info".to_owned(); + root.add(&mut rdef, None); + + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + let mut rdef = ResourceDef::new("/"); + user_map.add(&mut rdef, None); + + let mut rdef = ResourceDef::new("/post/{post_id}"); + *rdef.name_mut() = "user_post".to_owned(); + user_map.add(&mut rdef, None); + + root.add( + &mut ResourceDef::root_prefix("/user/{id}"), + Some(Rc::new(user_map)), + ); + + let root = Rc::new(root); + root.finish(Rc::clone(&root)); + + // sanity check resource map setup + + assert!(root.has_resource("/info")); + assert!(!root.has_resource("/bar")); + + assert!(root.has_resource("/user/22")); + assert!(root.has_resource("/user/22/")); + assert!(root.has_resource("/user/22/post/55")); + + // extract patterns from paths + + assert!(root.match_name("/bar").is_none()); + assert!(root.match_name("/v44").is_none()); + + assert_eq!(root.match_name("/info"), Some("root_info")); + assert_eq!(root.match_name("/user/22"), None); + assert_eq!(root.match_name("/user/22/"), None); + assert_eq!(root.match_name("/user/22/post/55"), Some("user_post")); + } + + #[test] + fn bug_fix_issue_1582_debug_print_exits() { + // ref: https://github.com/actix/actix-web/issues/1582 + let mut root = ResourceMap::new(ResourceDef::root_prefix("")); + + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + user_map.add(&mut ResourceDef::new("/"), None); + user_map.add(&mut ResourceDef::new("/profile"), None); + user_map.add(&mut ResourceDef::new("/article/{id}"), None); + user_map.add(&mut ResourceDef::new("/post/{post_id}"), None); + user_map.add( + &mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"), + None, + ); + + root.add( + &mut ResourceDef::root_prefix("/user/{id}"), + Some(Rc::new(user_map)), + ); + + let root = Rc::new(root); + root.finish(Rc::clone(&root)); + + // check root has no parent + assert!(root.parent.borrow().upgrade().is_none()); + // check child has parent reference + assert!(root.patterns[0].1.is_some()); + // check child's parent root id matches root's root id + assert_eq!( + root.patterns[0].1.as_ref().unwrap().root.id(), + root.root.id() + ); + + let output = format!("{:?}", root); + assert!(output.starts_with("ResourceMap {")); + assert!(output.ends_with(" }")); + } +} diff --git a/src/route.rs b/src/route.rs index 2763f3b1a..129a67332 100644 --- a/src/route.rs +++ b/src/route.rs @@ -46,6 +46,7 @@ pub struct Route { impl Route { /// Create new route which matches any request. + #[allow(clippy::new_without_default)] pub fn new() -> Route { Route { service: Box::new(RouteNewService::new(Extract::new(Handler::new(|| { @@ -361,13 +362,13 @@ mod tests { App::new() .service( web::resource("/test") - .route(web::get().to(|| HttpResponse::Ok())) + .route(web::get().to(HttpResponse::Ok)) .route(web::put().to(|| async { Err::(error::ErrorBadRequest("err")) })) .route(web::post().to(|| async { delay_for(Duration::from_millis(100)).await; - HttpResponse::Created() + Ok::<_, ()>(HttpResponse::Created()) })) .route(web::delete().to(|| async { delay_for(Duration::from_millis(100)).await; diff --git a/src/scope.rs b/src/scope.rs index b9166a4bf..2520fd7ae 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -58,7 +58,6 @@ type BoxedResponse = LocalBoxFuture<'static, Result>; /// * /{project_id}/path1 - responds to all http method /// * /{project_id}/path2 - `GET` requests /// * /{project_id}/path3 - `HEAD` requests -/// pub struct Scope { endpoint: T, rdef: String, @@ -678,12 +677,9 @@ mod tests { #[actix_rt::test] async fn test_scope() { - let mut srv = init_service( - App::new().service( - web::scope("/app") - .service(web::resource("/path1").to(|| HttpResponse::Ok())), - ), - ) + let mut srv = init_service(App::new().service( + web::scope("/app").service(web::resource("/path1").to(HttpResponse::Ok)), + )) .await; let req = TestRequest::with_uri("/app/path1").to_request(); @@ -696,8 +692,8 @@ mod tests { let mut srv = init_service( App::new().service( web::scope("/app") - .service(web::resource("").to(|| HttpResponse::Ok())) - .service(web::resource("/").to(|| HttpResponse::Created())), + .service(web::resource("").to(HttpResponse::Ok)) + .service(web::resource("/").to(HttpResponse::Created)), ), ) .await; @@ -714,7 +710,7 @@ mod tests { #[actix_rt::test] async fn test_scope_root2() { let mut srv = init_service(App::new().service( - web::scope("/app/").service(web::resource("").to(|| HttpResponse::Ok())), + web::scope("/app/").service(web::resource("").to(HttpResponse::Ok)), )) .await; @@ -730,7 +726,7 @@ mod tests { #[actix_rt::test] async fn test_scope_root3() { let mut srv = init_service(App::new().service( - web::scope("/app/").service(web::resource("/").to(|| HttpResponse::Ok())), + web::scope("/app/").service(web::resource("/").to(HttpResponse::Ok)), )) .await; @@ -748,8 +744,8 @@ mod tests { let mut srv = init_service( App::new().service( web::scope("app") - .route("/path1", web::get().to(|| HttpResponse::Ok())) - .route("/path1", web::delete().to(|| HttpResponse::Ok())), + .route("/path1", web::get().to(HttpResponse::Ok)) + .route("/path1", web::delete().to(HttpResponse::Ok)), ), ) .await; @@ -777,8 +773,8 @@ mod tests { App::new().service( web::scope("app").service( web::resource("path1") - .route(web::get().to(|| HttpResponse::Ok())) - .route(web::delete().to(|| HttpResponse::Ok())), + .route(web::get().to(HttpResponse::Ok)) + .route(web::delete().to(HttpResponse::Ok)), ), ), ) @@ -807,7 +803,7 @@ mod tests { App::new().service( web::scope("/app") .guard(guard::Get()) - .service(web::resource("/path1").to(|| HttpResponse::Ok())), + .service(web::resource("/path1").to(HttpResponse::Ok)), ), ) .await; @@ -829,7 +825,7 @@ 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 { + web::resource("/path1").to(|r: HttpRequest| { HttpResponse::Ok() .body(format!("project: {}", &r.match_info()["project"])) }), @@ -842,7 +838,7 @@ mod tests { match resp.response().body() { ResponseBody::Body(Body::Bytes(ref b)) => { - let bytes: Bytes = b.clone().into(); + let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"project: project1")); } _ => panic!(), @@ -855,14 +851,9 @@ mod tests { #[actix_rt::test] async fn test_nested_scope() { - let mut srv = init_service( - App::new().service( - web::scope("/app") - .service(web::scope("/t1").service( - web::resource("/path1").to(|| HttpResponse::Created()), - )), - ), - ) + let mut srv = init_service(App::new().service(web::scope("/app").service( + web::scope("/t1").service(web::resource("/path1").to(HttpResponse::Created)), + ))) .await; let req = TestRequest::with_uri("/app/t1/path1").to_request(); @@ -872,14 +863,9 @@ mod tests { #[actix_rt::test] async fn test_nested_scope_no_slash() { - let mut srv = init_service( - App::new().service( - web::scope("/app") - .service(web::scope("t1").service( - web::resource("/path1").to(|| HttpResponse::Created()), - )), - ), - ) + let mut srv = init_service(App::new().service(web::scope("/app").service( + web::scope("t1").service(web::resource("/path1").to(HttpResponse::Created)), + ))) .await; let req = TestRequest::with_uri("/app/t1/path1").to_request(); @@ -893,8 +879,8 @@ mod tests { App::new().service( web::scope("/app").service( web::scope("/t1") - .service(web::resource("").to(|| HttpResponse::Ok())) - .service(web::resource("/").to(|| HttpResponse::Created())), + .service(web::resource("").to(HttpResponse::Ok)) + .service(web::resource("/").to(HttpResponse::Created)), ), ), ) @@ -916,7 +902,7 @@ mod tests { web::scope("/app").service( web::scope("/t1") .guard(guard::Get()) - .service(web::resource("/path1").to(|| HttpResponse::Ok())), + .service(web::resource("/path1").to(HttpResponse::Ok)), ), ), ) @@ -939,7 +925,7 @@ 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 { + |r: HttpRequest| { HttpResponse::Created() .body(format!("project: {}", &r.match_info()["project_id"])) }, @@ -953,7 +939,7 @@ mod tests { match resp.response().body() { ResponseBody::Body(Body::Bytes(ref b)) => { - let bytes: Bytes = b.clone().into(); + let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"project: project_1")); } _ => panic!(), @@ -964,7 +950,7 @@ 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 { + web::resource("/path1").to(|r: HttpRequest| { HttpResponse::Created().body(format!( "project: {} - {}", &r.match_info()["project"], @@ -981,7 +967,7 @@ mod tests { match resp.response().body() { ResponseBody::Body(Body::Bytes(ref b)) => { - let bytes: Bytes = b.clone().into(); + let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"project: test - 1")); } _ => panic!(), @@ -997,7 +983,7 @@ mod tests { let mut srv = init_service( App::new().service( web::scope("/app") - .service(web::resource("/path1").to(|| HttpResponse::Ok())) + .service(web::resource("/path1").to(HttpResponse::Ok)) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::BadRequest())) }), @@ -1018,9 +1004,10 @@ mod tests { async fn test_default_resource_propagation() { let mut srv = init_service( App::new() - .service(web::scope("/app1").default_service( - web::resource("").to(|| HttpResponse::BadRequest()), - )) + .service( + web::scope("/app1") + .default_service(web::resource("").to(HttpResponse::BadRequest)), + ) .service(web::scope("/app2")) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::MethodNotAllowed())) @@ -1043,21 +1030,21 @@ mod tests { #[actix_rt::test] async fn test_middleware() { - let mut srv = - init_service( - App::new().service( - web::scope("app") - .wrap(DefaultHeaders::new().header( + let mut srv = init_service( + App::new().service( + web::scope("app") + .wrap( + DefaultHeaders::new().header( header::CONTENT_TYPE, HeaderValue::from_static("0001"), - )) - .service( - web::resource("/test") - .route(web::get().to(|| HttpResponse::Ok())), ), - ), - ) - .await; + ) + .service( + web::resource("/test").route(web::get().to(HttpResponse::Ok)), + ), + ), + ) + .await; let req = TestRequest::with_uri("/app/test").to_request(); let resp = call_service(&mut srv, req).await; @@ -1084,7 +1071,7 @@ mod tests { Ok(res) } }) - .route("/test", web::get().to(|| HttpResponse::Ok())), + .route("/test", web::get().to(HttpResponse::Ok)), ), ) .await; @@ -1105,7 +1092,6 @@ mod tests { "/t", web::get().to(|data: web::Data| { assert_eq!(**data, 10); - let _ = data.clone(); HttpResponse::Ok() }), ), @@ -1141,7 +1127,6 @@ mod tests { "/t", web::get().to(|data: web::Data| { assert_eq!(**data, 10); - let _ = data.clone(); HttpResponse::Ok() }), ), @@ -1157,7 +1142,7 @@ mod tests { async fn test_scope_config() { let mut srv = init_service(App::new().service(web::scope("/app").configure(|s| { - s.route("/path1", web::get().to(|| HttpResponse::Ok())); + s.route("/path1", web::get().to(HttpResponse::Ok)); }))) .await; @@ -1171,7 +1156,7 @@ mod tests { let mut srv = init_service(App::new().service(web::scope("/app").configure(|s| { s.service(web::scope("/v1").configure(|s| { - s.route("/", web::get().to(|| HttpResponse::Ok())); + s.route("/", web::get().to(HttpResponse::Ok)); })); }))) .await; @@ -1192,11 +1177,10 @@ 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| { + HttpResponse::Ok().body( + req.url_for("youtube", &["xxxxxx"]).unwrap().to_string(), + ) }), ); })); @@ -1214,7 +1198,7 @@ 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 { + web::get().to(|req: HttpRequest| { HttpResponse::Ok() .body(format!("{}", req.url_for("c", &["12345"]).unwrap())) }), diff --git a/src/server.rs b/src/server.rs index b2695b004..2b86f7416 100644 --- a/src/server.rs +++ b/src/server.rs @@ -122,23 +122,23 @@ where /// Sets the maximum per-worker number of concurrent connections. /// - /// All socket listeners will stop accepting connections when this limit is reached - /// for each worker. + /// All socket listeners will stop accepting connections when this limit is reached for + /// each worker. /// /// By default max connections is set to a 25k. - pub fn maxconn(mut self, num: usize) -> Self { + pub fn max_connections(mut self, num: usize) -> Self { self.builder = self.builder.maxconn(num); self } /// Sets the maximum per-worker concurrent connection establish process. /// - /// All listeners will stop accepting connections when this limit is reached. It - /// can be used to limit the global SSL CPU usage. + /// All listeners will stop accepting connections when this limit is reached. It can be used to + /// limit the global TLS CPU usage. /// /// By default max connections is set to a 256. - pub fn maxconnrate(self, num: usize) -> Self { - actix_tls::max_concurrent_ssl_connect(num); + pub fn max_connection_rate(self, num: usize) -> Self { + actix_tls::max_concurrent_tls_connect(num); self } @@ -375,19 +375,20 @@ where addr: A, ) -> io::Result> { let mut err = None; - let mut succ = false; + let mut success = false; let mut sockets = Vec::new(); + for addr in addr.to_socket_addrs()? { match create_tcp_listener(addr, self.backlog) { Ok(lst) => { - succ = true; + success = true; sockets.push(lst); } Err(e) => err = Some(e), } } - if !succ { + if !success { if let Some(e) = err.take() { Err(e) } else { @@ -575,17 +576,19 @@ fn create_tcp_listener( #[cfg(feature = "openssl")] /// Configure `SslAcceptorBuilder` with custom server flags. fn openssl_acceptor(mut builder: SslAcceptorBuilder) -> io::Result { - builder.set_alpn_select_callback(|_, protos| { + builder.set_alpn_select_callback(|_, protocols| { const H2: &[u8] = b"\x02h2"; const H11: &[u8] = b"\x08http/1.1"; - if protos.windows(3).any(|window| window == H2) { + + if protocols.windows(3).any(|window| window == H2) { Ok(b"h2") - } else if protos.windows(9).any(|window| window == H11) { + } else if protocols.windows(9).any(|window| window == H11) { Ok(b"http/1.1") } else { Err(AlpnError::NOACK) } }); + builder.set_alpn_protos(b"\x08http/1.1\x02h2")?; Ok(builder.build()) diff --git a/src/service.rs b/src/service.rs index 232a2f132..a861ba38c 100644 --- a/src/service.rs +++ b/src/service.rs @@ -12,7 +12,6 @@ use actix_router::{IntoPattern, Path, Resource, ResourceDef, Url}; use actix_service::{IntoServiceFactory, ServiceFactory}; use crate::config::{AppConfig, AppService}; -use crate::data::Data; use crate::dev::insert_slash; use crate::guard::Guard; use crate::info::ConnectionInfo; @@ -196,6 +195,18 @@ impl ServiceRequest { self.0.match_info() } + /// Counterpart to [`HttpRequest::match_name`](../struct.HttpRequest.html#method.match_name). + #[inline] + pub fn match_name(&self) -> Option<&str> { + self.0.match_name() + } + + /// Counterpart to [`HttpRequest::match_pattern`](../struct.HttpRequest.html#method.match_pattern). + #[inline] + pub fn match_pattern(&self) -> Option { + self.0.match_pattern() + } + #[inline] /// Get a mutable reference to the Path parameters. pub fn match_info_mut(&mut self) -> &mut Path { @@ -214,12 +225,11 @@ impl ServiceRequest { self.0.app_config() } - /// Get an application data stored with `App::data()` method during - /// application configuration. - pub fn app_data(&self) -> Option> { + /// Counterpart to [`HttpRequest::app_data`](../struct.HttpRequest.html#method.app_data). + pub fn app_data(&self) -> Option<&T> { for container in (self.0).0.app_data.iter().rev() { - if let Some(data) = container.get::>() { - return Some(Data::clone(&data)); + if let Some(data) = container.get::() { + return Some(data); } } @@ -583,6 +593,28 @@ mod tests { let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::NOT_FOUND); } + + #[actix_rt::test] + async fn test_service_data() { + let mut srv = init_service( + App::new() + .data(42u32) + .service(web::service("/test").name("test").finish( + |req: ServiceRequest| { + assert_eq!( + req.app_data::>().unwrap().as_ref(), + &42 + ); + ok(req.into_response(HttpResponse::Ok().finish())) + }, + )), + ) + .await; + let req = TestRequest::with_uri("/test").to_request(); + let resp = srv.call(req).await.unwrap(); + assert_eq!(resp.status(), http::StatusCode::OK); + } + #[test] fn test_fmt_debug() { let req = TestRequest::get() diff --git a/src/test.rs b/src/test.rs index a64ec3a73..ee51b71ee 100644 --- a/src/test.rs +++ b/src/test.rs @@ -23,7 +23,6 @@ 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; @@ -823,7 +822,7 @@ where } }; - Client::build().connector(connector).finish() + Client::builder().connector(connector).finish() }; TestServer { @@ -1073,14 +1072,9 @@ mod tests { let mut app = init_service( App::new().service( web::resource("/index.html") - .route(web::put().to(|| async { HttpResponse::Ok().body("put!") })) - .route( - web::patch().to(|| async { HttpResponse::Ok().body("patch!") }), - ) - .route( - web::delete() - .to(|| async { HttpResponse::Ok().body("delete!") }), - ), + .route(web::put().to(|| HttpResponse::Ok().body("put!"))) + .route(web::patch().to(|| HttpResponse::Ok().body("patch!"))) + .route(web::delete().to(|| HttpResponse::Ok().body("delete!"))), ), ) .await; @@ -1108,11 +1102,13 @@ mod tests { #[actix_rt::test] async fn test_response() { - let mut app = - init_service(App::new().service(web::resource("/index.html").route( - web::post().to(|| async { HttpResponse::Ok().body("welcome!") }), - ))) - .await; + let mut app = init_service( + App::new().service( + web::resource("/index.html") + .route(web::post().to(|| HttpResponse::Ok().body("welcome!"))), + ), + ) + .await; let req = TestRequest::post() .uri("/index.html") @@ -1125,11 +1121,13 @@ mod tests { #[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 mut app = init_service( + App::new().service( + web::resource("/index.html") + .route(web::get().to(|| HttpResponse::Ok().body("welcome!"))), + ), + ) + .await; let resp = TestRequest::get() .uri("/index.html") @@ -1149,7 +1147,7 @@ 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 { + web::post().to(|person: web::Json| { HttpResponse::Ok().json(person.into_inner()) }), ))) @@ -1170,7 +1168,7 @@ mod tests { #[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 { + web::post().to(|person: web::Json| { HttpResponse::Ok().json(person.into_inner()) }), ))) @@ -1192,7 +1190,7 @@ mod tests { #[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 { + web::post().to(|person: web::Form| { HttpResponse::Ok().json(person.into_inner()) }), ))) @@ -1218,7 +1216,7 @@ 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 { + web::post().to(|person: web::Json| { HttpResponse::Ok().json(person.into_inner()) }), ))) @@ -1283,53 +1281,54 @@ mod tests { assert!(res.status().is_success()); } - /* + #[actix_rt::test] + async fn test_actor() { + use crate::Error; + use actix::prelude::*; - Comment out until actix decoupled of actix-http: - https://github.com/actix/actix/issues/321 + struct MyActor; - 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 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; - - let req = TestRequest::post().uri("/index.html").to_request(); - let res = app.call(req).await.unwrap(); - assert!(res.status().is_success()); + impl Actor for MyActor { + type Context = Context; } - */ + + struct Num(usize); + + impl Message for Num { + type Result = usize; + } + + impl Handler for MyActor { + type Result = usize; + + fn handle(&mut self, msg: Num, _: &mut Self::Context) -> Self::Result { + msg.0 + } + } + + let addr = MyActor.start(); + + async fn actor_handler( + addr: Data>, + ) -> Result { + // `?` operator tests "actors" feature flag on actix-http + let res = addr.send(Num(1)).await?; + + if res == 1 { + Ok(HttpResponse::Ok()) + } else { + Ok(HttpResponse::BadRequest()) + } + } + + let srv = App::new() + .data(addr.clone()) + .service(web::resource("/").to(actor_handler)); + + let mut app = init_service(srv).await; + + let req = TestRequest::post().uri("/").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 ca1a4b103..2a7101287 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -23,7 +23,7 @@ use crate::http::{ StatusCode, }; use crate::request::HttpRequest; -use crate::responder::Responder; +use crate::{responder::Responder, web}; /// Form data helper (`application/x-www-form-urlencoded`) /// @@ -121,8 +121,12 @@ where fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { let req2 = req.clone(); let (limit, err) = req - .app_data::() - .map(|c| (c.limit, c.ehandler.clone())) + .app_data::() + .or_else(|| { + req.app_data::>() + .map(|d| d.as_ref()) + }) + .map(|c| (c.limit, c.err_handler.clone())) .unwrap_or((16384, None)); UrlEncoded::new(req, payload) @@ -191,7 +195,7 @@ impl Responder for Form { /// web::resource("/index.html") /// // change `Form` extractor configuration /// .app_data( -/// web::Form::::configure(|cfg| cfg.limit(4097)) +/// web::FormConfig::default().limit(4097) /// ) /// .route(web::get().to(index)) /// ); @@ -200,7 +204,7 @@ impl Responder for Form { #[derive(Clone)] pub struct FormConfig { limit: usize, - ehandler: Option Error>>, + err_handler: Option Error>>, } impl FormConfig { @@ -215,7 +219,7 @@ impl FormConfig { where F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static, { - self.ehandler = Some(Rc::new(f)); + self.err_handler = Some(Rc::new(f)); self } } @@ -223,8 +227,8 @@ impl FormConfig { impl Default for FormConfig { fn default() -> Self { FormConfig { - limit: 16384, - ehandler: None, + limit: 16_384, // 2^14 bytes (~16kB) + err_handler: None, } } } @@ -252,6 +256,7 @@ pub struct UrlEncoded { fut: Option>>, } +#[allow(clippy::borrow_interior_mutable_const)] impl UrlEncoded { /// Create a new future to URL encode a request pub fn new(req: &HttpRequest, payload: &mut Payload) -> UrlEncoded { @@ -377,7 +382,7 @@ mod tests { use serde::{Deserialize, Serialize}; use super::*; - use crate::http::header::{HeaderValue, CONTENT_TYPE}; + use crate::http::header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}; use crate::test::TestRequest; #[derive(Deserialize, Serialize, Debug, PartialEq)] @@ -406,18 +411,15 @@ mod tests { fn eq(err: UrlencodedError, other: UrlencodedError) -> bool { match err { - UrlencodedError::Overflow { .. } => match other { - UrlencodedError::Overflow { .. } => true, - _ => false, - }, - UrlencodedError::UnknownLength => match other { - UrlencodedError::UnknownLength => true, - _ => false, - }, - UrlencodedError::ContentType => match other { - UrlencodedError::ContentType => true, - _ => false, - }, + UrlencodedError::Overflow { .. } => { + matches!(other, UrlencodedError::Overflow { .. }) + } + UrlencodedError::UnknownLength => { + matches!(other, UrlencodedError::UnknownLength) + } + UrlencodedError::ContentType => { + matches!(other, UrlencodedError::ContentType) + } _ => false, } } @@ -501,4 +503,22 @@ mod tests { use crate::responder::tests::BodyTest; assert_eq!(resp.body().bin_ref(), b"hello=world&counter=123"); } + + #[actix_rt::test] + async fn test_with_config_in_data_wrapper() { + let ctype = HeaderValue::from_static("application/x-www-form-urlencoded"); + + let (req, mut pl) = TestRequest::default() + .header(CONTENT_TYPE, ctype) + .header(CONTENT_LENGTH, HeaderValue::from_static("20")) + .set_payload(Bytes::from_static(b"hello=test&counter=4")) + .app_data(web::Data::new(FormConfig::default().limit(10))) + .to_http_parts(); + + let s = Form::::from_request(&req, &mut pl).await; + assert!(s.is_err()); + + let err_str = s.err().unwrap().to_string(); + assert!(err_str.contains("Urlencoded payload size is bigger")); + } } diff --git a/src/types/json.rs b/src/types/json.rs index f746fd432..081a022e8 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -20,7 +20,7 @@ use crate::dev::Decompress; use crate::error::{Error, JsonPayloadError}; use crate::extract::FromRequest; use crate::request::HttpRequest; -use crate::responder::Responder; +use crate::{responder::Responder, web}; /// Json helper /// @@ -179,10 +179,11 @@ where #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { let req2 = req.clone(); - let (limit, err, ctype) = req - .app_data::() - .map(|c| (c.limit, c.ehandler.clone(), c.content_type.clone())) - .unwrap_or((32768, None, None)); + let config = JsonConfig::from_req(req); + + let limit = config.limit; + let ctype = config.content_type.clone(); + let err_handler = config.err_handler.clone(); JsonBody::new(req, payload, ctype) .limit(limit) @@ -193,7 +194,8 @@ where Request path: {}", req2.path() ); - if let Some(err) = err { + + if let Some(err) = err_handler { Err((*err)(e, &req2)) } else { Err(e.into()) @@ -207,10 +209,10 @@ where /// Json extractor configuration /// -/// # Examples +/// # Example /// /// ```rust -/// use actix_web::{error, web, App, FromRequest, HttpRequest, HttpResponse}; +/// use actix_web::{error, web, App, FromRequest, HttpResponse}; /// use serde_derive::Deserialize; /// /// #[derive(Deserialize)] @@ -223,39 +225,31 @@ 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") /// .app_data( -/// // change json extractor configuration -/// web::Json::::configure(|cfg| { -/// cfg.limit(4096) -/// .content_type(|mime| { // <- accept text/plain content type -/// mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN -/// }) -/// .error_handler(json_error_handler) // Use our custom error response -/// })) +/// // Json extractor configuration for this resource. +/// web::JsonConfig::default() +/// .limit(4096) // Limit request payload size +/// .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() +/// }) +/// ) /// .route(web::post().to(index)) /// ); /// } /// ``` +/// #[derive(Clone)] pub struct JsonConfig { limit: usize, - ehandler: Option Error + Send + Sync>>, + err_handler: + Option Error + Send + Sync>>, content_type: Option bool + Send + Sync>>, } @@ -271,7 +265,7 @@ impl JsonConfig { where F: Fn(JsonPayloadError, &HttpRequest) -> Error + Send + Sync + 'static, { - self.ehandler = Some(Arc::new(f)); + self.err_handler = Some(Arc::new(f)); self } @@ -283,15 +277,26 @@ impl JsonConfig { self.content_type = Some(Arc::new(predicate)); self } + + /// Extract payload config from app data. Check both `T` and `Data`, in that order, and fall + /// back to the default payload config. + fn from_req(req: &HttpRequest) -> &Self { + req.app_data::() + .or_else(|| req.app_data::>().map(|d| d.as_ref())) + .unwrap_or(&DEFAULT_CONFIG) + } } +// Allow shared refs to default. +const DEFAULT_CONFIG: JsonConfig = JsonConfig { + limit: 32_768, // 2^15 bytes, (~32kB) + err_handler: None, + content_type: None, +}; + impl Default for JsonConfig { fn default() -> Self { - JsonConfig { - limit: 32768, - ehandler: None, - content_type: None, - } + DEFAULT_CONFIG.clone() } } @@ -319,6 +324,7 @@ where U: DeserializeOwned + 'static, { /// Create `JsonBody` for request. + #[allow(clippy::borrow_interior_mutable_const)] pub fn new( req: &HttpRequest, payload: &mut Payload, @@ -421,7 +427,7 @@ mod tests { use super::*; use crate::error::InternalError; - use crate::http::header; + use crate::http::header::{self, HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}; use crate::test::{load_stream, TestRequest}; use crate::HttpResponse; @@ -432,14 +438,10 @@ mod tests { fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { match err { - JsonPayloadError::Overflow => match other { - JsonPayloadError::Overflow => true, - _ => false, - }, - JsonPayloadError::ContentType => match other { - JsonPayloadError::ContentType => true, - _ => false, - }, + JsonPayloadError::Overflow => matches!(other, JsonPayloadError::Overflow), + JsonPayloadError::ContentType => { + matches!(other, JsonPayloadError::ContentType) + } _ => false, } } @@ -485,7 +487,7 @@ mod tests { .to_http_parts(); let s = Json::::from_request(&req, &mut pl).await; - let mut resp = Response::from_error(s.err().unwrap().into()); + let mut resp = Response::from_error(s.err().unwrap()); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let body = load_stream(resp.take_body()).await.unwrap(); @@ -662,4 +664,20 @@ mod tests { let s = Json::::from_request(&req, &mut pl).await; assert!(s.is_err()) } + + #[actix_rt::test] + async fn test_with_config_in_data_wrapper() { + let (req, mut pl) = TestRequest::default() + .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) + .header(CONTENT_LENGTH, HeaderValue::from_static("16")) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .app_data(web::Data::new(JsonConfig::default().limit(10))) + .to_http_parts(); + + let s = Json::::from_request(&req, &mut pl).await; + assert!(s.is_err()); + + let err_str = s.err().unwrap().to_string(); + assert!(err_str.contains("Json payload size is bigger than allowed")); + } } diff --git a/src/types/path.rs b/src/types/path.rs index 82050171c..dbb5f3ee0 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -25,8 +25,8 @@ use crate::FromRequest; /// /// extract path info from "/{username}/{count}/index.html" url /// /// {username} - deserializes to a String /// /// {count} - - deserializes to a u32 -/// async fn index(info: web::Path<(String, u32)>) -> String { -/// format!("Welcome {}! {}", info.0, info.1) +/// async fn index(web::Path((username, count)): web::Path<(String, u32)>) -> String { +/// format!("Welcome {}! {}", username, count) /// } /// /// fn main() { @@ -61,20 +61,18 @@ use crate::FromRequest; /// ); /// } /// ``` -pub struct Path { - inner: T, -} +pub struct Path(pub T); impl Path { /// Deconstruct to an inner value pub fn into_inner(self) -> T { - self.inner + self.0 } } impl AsRef for Path { fn as_ref(&self) -> &T { - &self.inner + &self.0 } } @@ -82,31 +80,31 @@ impl ops::Deref for Path { type Target = T; fn deref(&self) -> &T { - &self.inner + &self.0 } } impl ops::DerefMut for Path { fn deref_mut(&mut self) -> &mut T { - &mut self.inner + &mut self.0 } } impl From for Path { fn from(inner: T) -> Path { - Path { inner } + Path(inner) } } impl fmt::Debug for Path { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.inner.fmt(f) + self.0.fmt(f) } } impl fmt::Display for Path { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.inner.fmt(f) + self.0.fmt(f) } } @@ -120,8 +118,8 @@ impl fmt::Display for Path { /// /// extract path info from "/{username}/{count}/index.html" url /// /// {username} - deserializes to a String /// /// {count} - - deserializes to a u32 -/// async fn index(info: web::Path<(String, u32)>) -> String { -/// format!("Welcome {}! {}", info.0, info.1) +/// async fn index(web::Path((username, count)): web::Path<(String, u32)>) -> String { +/// format!("Welcome {}! {}", username, count) /// } /// /// fn main() { @@ -173,7 +171,7 @@ where ready( de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) - .map(|inner| Path { inner }) + .map(Path) .map_err(move |e| { log::debug!( "Failed during Path extractor deserialization. \ @@ -290,21 +288,22 @@ mod tests { resource.match_path(req.match_info_mut()); let (req, mut pl) = req.into_parts(); - let res = <(Path<(String, String)>,)>::from_request(&req, &mut pl) + let (Path(res),) = <(Path<(String, String)>,)>::from_request(&req, &mut pl) .await .unwrap(); - assert_eq!((res.0).0, "name"); - assert_eq!((res.0).1, "user1"); + assert_eq!(res.0, "name"); + assert_eq!(res.1, "user1"); - let res = <(Path<(String, String)>, Path<(String, String)>)>::from_request( - &req, &mut pl, - ) - .await - .unwrap(); - assert_eq!((res.0).0, "name"); - assert_eq!((res.0).1, "user1"); - assert_eq!((res.1).0, "name"); - assert_eq!((res.1).1, "user1"); + let (Path(a), Path(b)) = + <(Path<(String, String)>, Path<(String, String)>)>::from_request( + &req, &mut pl, + ) + .await + .unwrap(); + assert_eq!(a.0, "name"); + assert_eq!(a.1, "user1"); + assert_eq!(b.0, "name"); + assert_eq!(b.1, "user1"); let () = <()>::from_request(&req, &mut pl).await.unwrap(); } @@ -329,7 +328,7 @@ mod tests { let s = s.into_inner(); assert_eq!(s.value, "user2"); - let s = Path::<(String, String)>::from_request(&req, &mut pl) + let Path(s) = Path::<(String, String)>::from_request(&req, &mut pl) .await .unwrap(); assert_eq!(s.0, "name"); @@ -344,7 +343,7 @@ mod tests { assert_eq!(s.as_ref().key, "name"); assert_eq!(s.value, 32); - let s = Path::<(String, u8)>::from_request(&req, &mut pl) + let Path(s) = Path::<(String, u8)>::from_request(&req, &mut pl) .await .unwrap(); assert_eq!(s.0, "name"); diff --git a/src/types/payload.rs b/src/types/payload.rs index bad33bfc6..4ff5ef4b4 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -13,10 +13,10 @@ use futures_util::future::{err, ok, Either, FutureExt, LocalBoxFuture, Ready}; use futures_util::StreamExt; use mime::Mime; -use crate::dev; use crate::extract::FromRequest; use crate::http::header; use crate::request::HttpRequest; +use crate::{dev, web}; /// Payload extractor returns request 's payload stream. /// @@ -142,13 +142,8 @@ impl FromRequest for Bytes { #[inline] fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { - let tmp; - let cfg = if let Some(cfg) = req.app_data::() { - cfg - } else { - tmp = PayloadConfig::default(); - &tmp - }; + // allow both Config and Data + let cfg = PayloadConfig::from_req(req); if let Err(e) = cfg.check_mimetype(req) { return Either::Right(err(e)); @@ -197,13 +192,7 @@ impl FromRequest for String { #[inline] fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { - let tmp; - let cfg = if let Some(cfg) = req.app_data::() { - cfg - } else { - tmp = PayloadConfig::default(); - &tmp - }; + let cfg = PayloadConfig::from_req(req); // check content-type if let Err(e) = cfg.check_mimetype(req) { @@ -237,7 +226,12 @@ impl FromRequest for String { ) } } -/// Payload configuration for request's payload. + +/// Configuration for request's payload. +/// +/// Applies to the built-in `Bytes` and `String` extractors. Note that the Payload extractor does +/// not automatically check conformance with this configuration to allow more flexibility when +/// building extractors on top of `Payload`. #[derive(Clone)] pub struct PayloadConfig { limit: usize, @@ -284,14 +278,25 @@ impl PayloadConfig { } Ok(()) } + + /// Extract payload config from app data. Check both `T` and `Data`, in that order, and fall + /// back to the default payload config. + fn from_req(req: &HttpRequest) -> &Self { + req.app_data::() + .or_else(|| req.app_data::>().map(|d| d.as_ref())) + .unwrap_or(&DEFAULT_CONFIG) + } } +// Allow shared refs to default. +const DEFAULT_CONFIG: PayloadConfig = PayloadConfig { + limit: 262_144, // 2^18 bytes (~256kB) + mimetype: None, +}; + impl Default for PayloadConfig { fn default() -> Self { - PayloadConfig { - limit: 262_144, - mimetype: None, - } + DEFAULT_CONFIG.clone() } } @@ -315,6 +320,7 @@ pub struct HttpMessageBody { impl HttpMessageBody { /// Create `MessageBody` for request. + #[allow(clippy::borrow_interior_mutable_const)] pub fn new(req: &HttpRequest, payload: &mut dev::Payload) -> HttpMessageBody { let mut len = None; if let Some(l) = req.headers().get(&header::CONTENT_LENGTH) { @@ -406,8 +412,9 @@ mod tests { use bytes::Bytes; use super::*; - use crate::http::header; - use crate::test::TestRequest; + use crate::http::{header, StatusCode}; + use crate::test::{call_service, init_service, TestRequest}; + use crate::{web, App, Responder}; #[actix_rt::test] async fn test_payload_config() { @@ -427,6 +434,86 @@ mod tests { assert!(cfg.check_mimetype(&req).is_ok()); } + #[actix_rt::test] + async fn test_config_recall_locations() { + async fn bytes_handler(_: Bytes) -> impl Responder { + "payload is probably json bytes" + } + + async fn string_handler(_: String) -> impl Responder { + "payload is probably json string" + } + + let mut srv = init_service( + App::new() + .service( + web::resource("/bytes-app-data") + .app_data( + PayloadConfig::default().mimetype(mime::APPLICATION_JSON), + ) + .route(web::get().to(bytes_handler)), + ) + .service( + web::resource("/bytes-data") + .data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON)) + .route(web::get().to(bytes_handler)), + ) + .service( + web::resource("/string-app-data") + .app_data( + PayloadConfig::default().mimetype(mime::APPLICATION_JSON), + ) + .route(web::get().to(string_handler)), + ) + .service( + web::resource("/string-data") + .data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON)) + .route(web::get().to(string_handler)), + ), + ) + .await; + + let req = TestRequest::with_uri("/bytes-app-data").to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/bytes-data").to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/string-app-data").to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/string-data").to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/bytes-app-data") + .header(header::CONTENT_TYPE, mime::APPLICATION_JSON) + .to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/bytes-data") + .header(header::CONTENT_TYPE, mime::APPLICATION_JSON) + .to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/string-app-data") + .header(header::CONTENT_TYPE, mime::APPLICATION_JSON) + .to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/string-data") + .header(header::CONTENT_TYPE, mime::APPLICATION_JSON) + .to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + } + #[actix_rt::test] async fn test_bytes() { let (req, mut pl) = TestRequest::with_header(header::CONTENT_LENGTH, "11") diff --git a/src/types/query.rs b/src/types/query.rs index cf1a8930d..f9440e1b4 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -188,12 +188,12 @@ where /// let app = App::new().service( /// web::resource("/index.html").app_data( /// // change query extractor configuration -/// web::Query::::configure(|cfg| { -/// cfg.error_handler(|err, req| { // <- create custom error response +/// web::QueryConfig::default() +/// .error_handler(|err, req| { // <- create custom error response /// error::InternalError::from_response( /// err, HttpResponse::Conflict().finish()).into() /// }) -/// })) +/// ) /// .route(web::post().to(index)) /// ); /// } diff --git a/test-server/CHANGES.md b/test-server/CHANGES.md index 079cad74a..0a11e2cae 100644 --- a/test-server/CHANGES.md +++ b/test-server/CHANGES.md @@ -1,7 +1,15 @@ # Changes -## [2.0.0-alpha.1] - 2020-05-23 +## Unreleased - 2020-xx-xx +* add ability to set address for `TestServer` [#1645] + +[#1645]: https://github.com/actix/actix-web/pull/1645 + +## 2.0.0 - 2020-09-11 +* Update actix-codec and actix-utils dependencies. + +## 2.0.0-alpha.1 - 2020-05-23 * Update the `time` dependency to 0.2.7 * Update `actix-connect` dependency to 2.0.0-alpha.2 * Make `test_server` `async` fn. diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index f90cef0dd..d06bd5dec 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "actix-http-test" -version = "2.0.0-alpha.1" +version = "2.0.0" authors = ["Nikolay Kim "] -description = "Actix http test server" +description = "Actix HTTP test server" readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" @@ -11,10 +11,9 @@ documentation = "https://docs.rs/actix-http-test/" categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" exclude = [".gitignore", ".cargo/config"] edition = "2018" -workspace = ".." [package.metadata.docs.rs] features = [] @@ -30,30 +29,28 @@ default = [] openssl = ["open-ssl", "awc/openssl"] [dependencies] -actix-service = "1.0.1" -actix-codec = "0.2.0" -actix-connect = "2.0.0-alpha.2" -actix-utils = "1.0.3" -actix-rt = "1.0.0" +actix-service = "1.0.6" +actix-codec = "0.3.0" +actix-connect = "2.0.0" +actix-utils = "2.0.0" +actix-rt = "1.1.1" actix-server = "1.0.0" actix-testing = "1.0.0" -awc = "2.0.0-alpha.2" +awc = "2.0.0" base64 = "0.12" bytes = "0.5.3" futures-core = { version = "0.3.5", default-features = false } http = "0.2.0" log = "0.4" -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.7", default-features = false, features = ["std"] } open-ssl = { version = "0.10", package = "openssl", optional = true } [dev-dependencies] -actix-web = "3.0.0-alpha.3" -actix-http = "2.0.0-alpha.4" +actix-web = "3.0.0" +actix-http = "2.0.0" diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index f6c1183b4..4159c8d86 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -44,12 +44,20 @@ pub use actix_testing::*; /// } /// ``` pub async fn test_server>(factory: F) -> TestServer { + let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); + test_server_with_addr(tcp, factory).await +} + +/// Start [`test server`](./fn.test_server.html) on a concrete Address +pub async fn test_server_with_addr>( + tcp: net::TcpListener, + factory: F, +) -> TestServer { let (tx, rx) = mpsc::channel(); // run server in separate thread thread::spawn(move || { let sys = System::new("actix-test-server"); - let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); let local_addr = tcp.local_addr().unwrap(); Server::build() @@ -90,7 +98,7 @@ pub async fn test_server>(factory: F) -> TestServer } }; - Client::build().connector(connector).finish() + Client::builder().connector(connector).finish() }; actix_connect::start_default_resolver().await.unwrap(); diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index 750084fdc..118640aca 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -22,8 +22,8 @@ async fn test_start() { }) .workers(1) .backlog(1) - .maxconn(10) - .maxconnrate(10) + .max_connections(10) + .max_connection_rate(10) .keep_alive(10) .client_timeout(5000) .client_shutdown(0) @@ -43,7 +43,7 @@ async fn test_start() { { use actix_http::client; - let client = awc::Client::build() + let client = awc::Client::builder() .connector( client::Connector::new() .timeout(Duration::from_millis(100)) @@ -115,7 +115,7 @@ async fn test_start_ssl() { .set_alpn_protos(b"\x02h2\x08http/1.1") .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); - let client = awc::Client::build() + let client = awc::Client::builder() .connector( awc::Connector::new() .ssl(builder.build()) diff --git a/tests/test_server.rs b/tests/test_server.rs index 0ac4b0232..f8a9ab86d 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -16,7 +16,8 @@ use futures_util::ready; use rand::{distributions::Alphanumeric, Rng}; use actix_web::dev::BodyEncoding; -use actix_web::middleware::Compress; +use actix_web::middleware::normalize::TrailingSlash; +use actix_web::middleware::{Compress, NormalizePath}; use actix_web::{dev, test, web, App, Error, HttpResponse}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ @@ -851,7 +852,7 @@ async fn test_slow_request() { use std::net; let srv = test::start_with(test::config().client_timeout(200), || { - App::new().service(web::resource("/").route(web::to(|| HttpResponse::Ok()))) + App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))) }); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); @@ -866,6 +867,20 @@ async fn test_slow_request() { assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); } +#[actix_rt::test] +async fn test_normalize() { + let srv = test::start_with(test::config().h1(), || { + App::new() + .wrap(NormalizePath::new(TrailingSlash::Trim)) + .service( + web::resource("/one").route(web::to(|| HttpResponse::Ok().finish())), + ) + }); + + let response = srv.get("/one/").send().await.unwrap(); + assert!(response.status().is_success()); +} + // #[cfg(feature = "openssl")] // #[actix_rt::test] // async fn test_ssl_handshake_timeout() {