1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-13 05:43:57 +02:00

Compare commits

...

71 Commits

Author SHA1 Message Date
b373e1370d prepare files 0.5.0 release 2020-12-26 04:05:45 +00:00
404b5a7709 Add optional support for hidden files/directories (#1811) 2020-12-26 03:36:15 +00:00
ecf08d5156 Remove boxed future from h1 Dispatcher (#1836) 2020-12-24 19:15:17 +00:00
87655b3028 reduce one clone on Arc. (#1850) 2020-12-23 23:58:25 +00:00
3a192400a6 Simplify handler (#1843) 2020-12-23 15:47:07 +00:00
2a7f2c1d59 dispatcher internals testing (#1840) 2020-12-23 01:28:17 +00:00
05f104c240 improve NormalizePath docs (#1839) 2020-12-23 00:19:20 +00:00
4dccd092f3 Bump rand from 0.7.x to 0.8.x (#1845) 2020-12-22 23:45:31 +00:00
95ccf1c9bc replace actix_utils::oneshot with futures_channle::oneshot (#1844) 2020-12-21 16:42:20 +00:00
6cbf27508a simplify ExtractService's return type (#1842) 2020-12-20 02:20:29 +00:00
79de04d862 optimise Extract service (#1841) 2020-12-19 16:33:34 +00:00
a4dbaa8ed1 remove boxed future in DefaultHeaders middleware (#1838) 2020-12-18 23:08:59 +00:00
c7b4c6edfa Disable PR comment from codecov 2020-12-17 21:38:52 +09:00
2a5215c1d6 Remove boxed future from HttpMessage (#1834) 2020-12-17 11:40:49 +00:00
97f615c245 remove boxed futures on Json extract type (#1832) 2020-12-16 23:34:33 +00:00
1a361273e7 optimize bytes and string payload extractors (#1831) 2020-12-16 22:40:26 +00:00
d7ce648445 remove boxed future for Option<T> and Result<T, E> extract type (#1829)
* remove boxed future for Option<T> and Result<T, E> extract type

* use ready macro

* fix fmt
2020-12-16 18:34:10 +00:00
fabc68659b Intradoc links conversion (#1827)
* switching to nightly for intra-doc links

* actix-files intra-doc conversion

* more specific Result

* intradoc conversion complete

* rm blank comments and readme doc link fixes

* macros and broken links
2020-12-13 13:28:39 +00:00
542db82282 Simplify wake up of task (#1826) 2020-12-12 20:07:06 +00:00
ae63eb8bb2 fix clippy warnings (#1806)
* fix clippy warnings

* prevent CI fail status caused by codecov
2020-12-09 11:22:19 +00:00
7a3776b770 remove two unused generics on BoxedRouteFuture types. (#1820) 2020-12-09 10:47:59 +00:00
ff79c33fd4 remove a box (#1814) 2020-12-06 11:42:15 +00:00
b75a9b7a20 add error to message in test helper func (#1812) 2020-12-05 04:57:56 +09:00
d0c6ca7671 test-server => actix-http-test (#1807) 2020-12-02 17:23:30 +00:00
24d525d978 prepare web 3.3.2 release 2020-12-01 22:22:46 +00:00
1f70ef155d Fix match_pattern() returning None for scope with resource of empty path (#1798)
* fix match_pattern function not returning pattern where scope has resource of path ""

* remove print in test

* make comparison on existing else if block

* add fix to changelog
2020-12-01 13:39:41 +00:00
7981e0068a Remove a panic in normalize middleware (#1762)
Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
2020-12-01 10:22:15 +09:00
32d59ca904 Upgrade socket2 dependency (#1803)
Upgrades to a version not making invalid assumptions about
the memory layout of std::net::SocketAddr
2020-12-01 04:18:02 +09:00
ea8bf36104 update web and awc changelogs 2020-11-29 16:35:35 +00:00
0b5b463cfa prepare web and awc releases
closes #1799
2020-11-29 16:33:45 +00:00
fe6ad816cc update dotgraphs 2020-11-25 00:54:00 +00:00
e72b787ba7 prepare actix-web and actix-http-test releases 2020-11-25 00:53:48 +00:00
efc317d3b0 prepare actix-http and awc releases 2020-11-25 00:07:56 +00:00
31057becca prepare actix-files release 0.4.1 2020-11-24 20:33:23 +00:00
f1a9b45437 improve docs for Files::new 2020-11-24 20:23:09 +00:00
5af46775b8 refactor quality and use TryFrom instead of custom trait (#1797) 2020-11-24 11:37:05 +00:00
70f4747a23 add method for getting accept type preference (#1793) 2020-11-24 10:08:57 +00:00
2f11ef089b fix rustdoc uploads 2020-11-24 00:29:13 +00:00
4100c50c70 add either extractor (#1788) 2020-11-20 18:02:41 +00:00
a929209967 actix-files intra-doc migration (#1785) 2020-11-10 23:54:38 +00:00
49e945c88f switching to nightly for intra-doc links (#1783) 2020-11-09 14:01:36 +00:00
9b42333fac Fix typo in Query extractor docs (#1777) 2020-11-06 13:34:42 +00:00
e5b86d189c Fix typo in request_data.rs (#1774) 2020-11-05 17:46:17 +00:00
4bfd5c2781 Upgrade serde_urlencoded to 0.7 (#1773) 2020-11-06 01:36:15 +09:00
9b6a089b36 fix awc doc example (#1772)
* fix awc readme example

Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-11-05 06:20:01 +08:00
ceac97bb8d Update config.yml 2020-11-04 15:08:12 +00:00
61b65aa64a add common 1xx http response builders (#1768)
Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
2020-11-02 18:23:18 +09:00
5468c3c410 Drop content length headers from 101 responses (#1767)
Co-authored-by: Sebastian Mayr <smayr@atlassian.com>
2020-11-02 17:44:14 +09:00
b6385c2b4e Remove CoC on actix-http as duplicated 2020-10-31 12:12:19 +09:00
5135c1e3a0 Update CoC contact information 2020-10-31 12:06:51 +09:00
22b451cf2d fix deps.rs badge 2020-10-31 02:39:54 +00:00
42f51eb962 prepare web release 3.2.0 2020-10-30 03:15:22 +00:00
156c97cef2 prepare awc release 2.0.1 2020-10-30 02:50:53 +00:00
798d744eef prepare http release 2.1.0 2020-10-30 02:19:56 +00:00
4cb833616a deprecate builder if-x methods (#1760) 2020-10-30 02:10:05 +00:00
9963a5ef54 expose on_connect v2 (#1754)
Co-authored-by: Mikail Bagishov <bagishov.mikail@yandex.ru>
2020-10-30 02:03:26 +00:00
4519db36b2 register fns for custom request-derived logging units (#1749)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-10-29 18:38:49 +00:00
7030bf5fe8 Adding app_data to ServiceConfig (#1758)
Co-authored-by: Rob Ede <robjtede@icloud.com>
Co-authored-by: Augusto <augusto@flowciety.de>
2020-10-26 17:02:45 +00:00
20078fe603 Bump pin-project to 1.0 (#1733) 2020-10-25 19:41:44 +09:00
06e5042b94 use idenity encoding on client if no compression features are enabled (#1737)
Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-10-24 21:15:01 +01:00
41e7cec72f Re-export bytes::Buf and bytes::BufMut as well (#1750)
Co-authored-by: Daniel Egger <daniel.egger@axiros.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-10-24 20:31:23 +01:00
d45a1aa6b6 Add web::ReqData<T> extractor (#1748)
Co-authored-by: Jonas Platte <jonas@lumeo.com>
2020-10-24 18:49:50 +01:00
98243db9f1 Print unconfigured Data<T> type when attempting extraction (#1743)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-10-20 17:35:34 +01:00
f92742bdac Bump base64 to 0.13 (#1744) 2020-10-19 18:24:22 +01:00
e563025b16 always construct shortslice using debug checked new constructor (#1741) 2020-10-19 12:51:30 +01:00
cfd5b381f1 Implement Logger middleware regex exclude pattern (#1723)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-10-19 07:18:16 +01:00
2f84914146 Skip some tests that cause ICE on nightly (#1740) 2020-10-19 11:52:05 +09:00
d765e9099d Fix clippy::rc_buffer (#1728) 2020-10-10 09:26:05 +09:00
34b23f31c9 prepare files release 0.4.0 2020-10-06 22:08:33 +01:00
26c1a901d9 add files preference for utf8 text responses (#1714) 2020-10-06 21:56:28 +01:00
c2c71cc626 Fix/suppress clippy warnings (#1720) 2020-10-01 18:19:09 +09:00
112 changed files with 3316 additions and 1347 deletions

View File

@ -1,8 +1,15 @@
blank_issues_enabled: true blank_issues_enabled: true
contact_links: contact_links:
- name: Gitter channel (actix-web) - name: GitHub Discussions
url: https://github.com/actix/actix-web/discussions
about: Actix Web Q&A
- name: Gitter chat (actix-web)
url: https://gitter.im/actix/actix-web url: https://gitter.im/actix/actix-web
about: Please ask and answer questions about the actix-web here. about: Actix Web Q&A
- name: Gitter channel (actix) - name: Gitter chat (actix)
url: https://gitter.im/actix/actix url: https://gitter.im/actix/actix
about: Please ask and answer questions about the actix here. about: Actix (actor framework) Q&A
- name: Actix Discord
url: https://discord.gg/NWpN5mmg3x
about: Actix developer discussion and community chat

View File

@ -16,7 +16,7 @@ jobs:
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: stable-x86_64-unknown-linux-gnu toolchain: nightly-x86_64-unknown-linux-gnu
profile: minimal profile: minimal
override: true override: true
@ -30,7 +30,7 @@ jobs:
run: echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html run: echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@3.5.8 uses: JamesIves/github-pages-deploy-action@3.7.1
with: with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages BRANCH: gh-pages

View File

@ -1,6 +1,62 @@
# Changes # Changes
## Unreleased - 2020-xx-xx ## Unreleased - 2020-xx-xx
### Changed
* Bumped `rand` to `0.8`
### Fixed
* added the actual parsing error to `test::read_body_json` [#1812]
[#1812]: https://github.com/actix/actix-web/pull/1812
## 3.3.2 - 2020-12-01
### Fixed
* Removed an occasional `unwrap` on `None` panic in `NormalizePathNormalization`. [#1762]
* Fix `match_pattern()` returning `None` for scope with empty path resource. [#1798]
* Increase minimum `socket2` version. [#1803]
[#1762]: https://github.com/actix/actix-web/pull/1762
[#1798]: https://github.com/actix/actix-web/pull/1798
[#1803]: https://github.com/actix/actix-web/pull/1803
## 3.3.1 - 2020-11-29
* Ensure `actix-http` dependency uses same `serde_urlencoded`.
## 3.3.0 - 2020-11-25
### Added
* Add `Either<A, B>` extractor helper. [#1788]
### Changed
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
[#1773]: https://github.com/actix/actix-web/pull/1773
[#1788]: https://github.com/actix/actix-web/pull/1788
## 3.2.0 - 2020-10-30
### Added
* Implement `exclude_regex` for Logger middleware. [#1723]
* Add request-local data extractor `web::ReqData`. [#1748]
* Add ability to register closure for request middleware logging. [#1749]
* Add `app_data` to `ServiceConfig`. [#1757]
* Expose `on_connect` for access to the connection stream before request is handled. [#1754]
### Changed
* Updated actix-web-codegen dependency for access to new `#[route(...)]` multi-method macro.
* Print non-configured `Data<T>` type when attempting extraction. [#1743]
* Re-export bytes::Buf{Mut} in web module. [#1750]
* Upgrade `pin-project` to `1.0`.
[#1723]: https://github.com/actix/actix-web/pull/1723
[#1743]: https://github.com/actix/actix-web/pull/1743
[#1748]: https://github.com/actix/actix-web/pull/1748
[#1750]: https://github.com/actix/actix-web/pull/1750
[#1754]: https://github.com/actix/actix-web/pull/1754
[#1749]: https://github.com/actix/actix-web/pull/1749
## 3.1.0 - 2020-09-29 ## 3.1.0 - 2020-09-29

View File

@ -34,10 +34,13 @@ This Code of Conduct applies both within project spaces and in public spaces whe
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at fafhrd91@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at robjtede@icloud.com ([@robjtede]) or huyuumi@neet.club ([@JohnTitor]). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
[@robjtede]: https://github.com/robjtede
[@JohnTitor]: https://github.com/JohnTitor
## Attribution ## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]

View File

@ -1,8 +1,8 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "3.1.0" version = "3.3.2"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix web is a powerful, pragmatic, and extremely fast web framework for Rust." description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
readme = "README.md" readme = "README.md"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -34,7 +34,7 @@ members = [
"actix-multipart", "actix-multipart",
"actix-web-actors", "actix-web-actors",
"actix-web-codegen", "actix-web-codegen",
"test-server", "actix-http-test",
] ]
[features] [features]
@ -64,6 +64,14 @@ required-features = ["compress"]
name = "test_server" name = "test_server"
required-features = ["compress"] required-features = ["compress"]
[[example]]
name = "on_connect"
required-features = []
[[example]]
name = "client"
required-features = ["rustls"]
[dependencies] [dependencies]
actix-codec = "0.3.0" actix-codec = "0.3.0"
actix-service = "1.0.6" actix-service = "1.0.6"
@ -76,12 +84,12 @@ actix-macros = "0.1.0"
actix-threadpool = "0.3.1" actix-threadpool = "0.3.1"
actix-tls = "2.0.0" actix-tls = "2.0.0"
actix-web-codegen = "0.3.0" actix-web-codegen = "0.4.0"
actix-http = "2.0.0" actix-http = "2.2.0"
awc = { version = "2.0.0", default-features = false } awc = { version = "2.0.3", default-features = false }
bytes = "0.5.3" bytes = "0.5.3"
derive_more = "0.99.2" derive_more = "0.99.5"
encoding_rs = "0.8" encoding_rs = "0.8"
futures-channel = { version = "0.3.5", default-features = false } futures-channel = { version = "0.3.5", default-features = false }
futures-core = { version = "0.3.5", default-features = false } futures-core = { version = "0.3.5", default-features = false }
@ -89,12 +97,12 @@ futures-util = { version = "0.3.5", default-features = false }
fxhash = "0.2.1" fxhash = "0.2.1"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
socket2 = "0.3" socket2 = "0.3.16"
pin-project = "0.4.17" pin-project = "1.0.0"
regex = "1.3" regex = "1.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.6.1" serde_urlencoded = "0.7"
time = { version = "0.2.7", default-features = false, features = ["std"] } time = { version = "0.2.7", default-features = false, features = ["std"] }
url = "2.1" url = "2.1"
open-ssl = { package = "openssl", version = "0.10", optional = true } open-ssl = { package = "openssl", version = "0.10", optional = true }
@ -103,9 +111,9 @@ tinyvec = { version = "1", features = ["alloc"] }
[dev-dependencies] [dev-dependencies]
actix = "0.10.0" actix = "0.10.0"
actix-http = { version = "2.0.0", features = ["actors"] } actix-http = { version = "2.1.0", features = ["actors"] }
rand = "0.7" rand = "0.8"
env_logger = "0.7" env_logger = "0.8"
serde_derive = "1.0" serde_derive = "1.0"
brotli2 = "0.3.2" brotli2 = "0.3.2"
flate2 = "1.0.13" flate2 = "1.0.13"
@ -119,16 +127,12 @@ codegen-units = 1
[patch.crates-io] [patch.crates-io]
actix-web = { path = "." } actix-web = { path = "." }
actix-http = { path = "actix-http" } actix-http = { path = "actix-http" }
actix-http-test = { path = "test-server" } actix-http-test = { path = "actix-http-test" }
actix-web-codegen = { path = "actix-web-codegen" } actix-web-codegen = { path = "actix-web-codegen" }
actix-files = { path = "actix-files" }
actix-multipart = { path = "actix-multipart" } actix-multipart = { path = "actix-multipart" }
actix-files = { path = "actix-files" }
awc = { path = "awc" } awc = { path = "awc" }
[[example]]
name = "client"
required-features = ["rustls"]
[[bench]] [[bench]]
name = "server" name = "server"
harness = false harness = false

View File

@ -1,19 +1,21 @@
<div align="center"> <div align="center">
<h1>Actix web</h1> <h1>Actix web</h1>
<p> <p>
<strong>Actix web is a powerful, pragmatic, and extremely fast web framework for Rust</strong> <strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
</p> </p>
<p> <p>
[![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg)](https://docs.rs/actix-web) [![Documentation](https://docs.rs/actix-web/badge.svg?version=3.3.2)](https://docs.rs/actix-web/3.3.2)
[![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html) [![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) ![License](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/3.3.2/status.svg)](https://deps.rs/crate/actix-web/3.3.2)
<br /> <br />
[![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)
[![Download](https://img.shields.io/crates/d/actix-web.svg)](https://crates.io/crates/actix-web) [![Download](https://img.shields.io/crates/d/actix-web.svg)](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) [![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)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
</p> </p>
</div> </div>

View File

@ -1,12 +1,34 @@
# Changes # Changes
## [Unreleased] - 2020-xx-xx ## Unreleased - 2020-xx-xx
## [0.3.0-beta.1] - 2020-07-15
## 0.5.0 - 2020-12-26
* Optionally support hidden files/directories. [#1811]
[#1811]: https://github.com/actix/actix-web/pull/1811
## 0.4.1 - 2020-11-24
* Clarify order of parameters in `Files::new` and improve docs.
## 0.4.0 - 2020-10-06
* Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
[#1714]: https://github.com/actix/actix-web/pull/1714
## 0.3.0 - 2020-09-11
* No significant changes from 0.3.0-beta.1.
## 0.3.0-beta.1 - 2020-07-15
* Update `v_htmlescape` to 0.10 * Update `v_htmlescape` to 0.10
* Update `actix-web` and `actix-http` dependencies to beta.1 * Update `actix-web` and `actix-http` dependencies to beta.1
## [0.3.0-alpha.1] - 2020-05-23
## 0.3.0-alpha.1 - 2020-05-23
* Update `actix-web` and `actix-http` dependencies to alpha * Update `actix-web` and `actix-http` dependencies to alpha
* Fix some typos in the docs * Fix some typos in the docs
* Bump minimum supported Rust version to 1.40 * Bump minimum supported Rust version to 1.40
@ -14,77 +36,73 @@
[#1384]: https://github.com/actix/actix-web/pull/1384 [#1384]: https://github.com/actix/actix-web/pull/1384
## [0.2.1] - 2019-12-22
## 0.2.1 - 2019-12-22
* Use the same format for file URLs regardless of platforms * Use the same format for file URLs regardless of platforms
## [0.2.0] - 2019-12-20
## 0.2.0 - 2019-12-20
* Fix BodyEncoding trait import #1220 * Fix BodyEncoding trait import #1220
## [0.2.0-alpha.1] - 2019-12-07
## 0.2.0-alpha.1 - 2019-12-07
* Migrate to `std::future` * Migrate to `std::future`
## [0.1.7] - 2019-11-06
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151) ## 0.1.7 - 2019-11-06
* Add an additional `filename*` param in the `Content-Disposition` header of
## [0.1.6] - 2019-10-14 `actix_files::NamedFile` to be more compatible. (#1151)
## 0.1.6 - 2019-10-14
* Add option to redirect to a slash-ended path `Files` #1132 * Add option to redirect to a slash-ended path `Files` #1132
## [0.1.5] - 2019-10-08
## 0.1.5 - 2019-10-08
* Bump up `mime_guess` crate version to 2.0.1 * Bump up `mime_guess` crate version to 2.0.1
* Bump up `percent-encoding` crate version to 2.1 * Bump up `percent-encoding` crate version to 2.1
* Allow user defined request guards for `Files` #1113 * Allow user defined request guards for `Files` #1113
## [0.1.4] - 2019-07-20
## 0.1.4 - 2019-07-20
* Allow to disable `Content-Disposition` header #686 * Allow to disable `Content-Disposition` header #686
## [0.1.3] - 2019-06-28
## 0.1.3 - 2019-06-28
* Do not set `Content-Length` header, let actix-http set it #930 * Do not set `Content-Length` header, let actix-http set it #930
## [0.1.2] - 2019-06-13
## 0.1.2 - 2019-06-13
* Content-Length is 0 for NamedFile HEAD request #914 * Content-Length is 0 for NamedFile HEAD request #914
* Fix ring dependency from actix-web default features for #741 * Fix ring dependency from actix-web default features for #741
## [0.1.1] - 2019-06-01
## 0.1.1 - 2019-06-01
* Static files are incorrectly served as both chunked and with length #812 * Static files are incorrectly served as both chunked and with length #812
## [0.1.0] - 2019-05-25
* NamedFile last-modified check always fails due to nano-seconds ## 0.1.0 - 2019-05-25
in file modified date #820 * NamedFile last-modified check always fails due to nano-seconds in file modified date #820
## [0.1.0-beta.4] - 2019-05-12
## 0.1.0-beta.4 - 2019-05-12
* Update actix-web to beta.4 * Update actix-web to beta.4
## [0.1.0-beta.1] - 2019-04-20
## 0.1.0-beta.1 - 2019-04-20
* Update actix-web to beta.1 * Update actix-web to beta.1
## [0.1.0-alpha.6] - 2019-04-14
## 0.1.0-alpha.6 - 2019-04-14
* Update actix-web to alpha6 * Update actix-web to alpha6
## [0.1.0-alpha.4] - 2019-04-08
## 0.1.0-alpha.4 - 2019-04-08
* Update actix-web to alpha4 * Update actix-web to alpha4
## [0.1.0-alpha.2] - 2019-04-02
## 0.1.0-alpha.2 - 2019-04-02
* Add default handler support * Add default handler support
## [0.1.0-alpha.1] - 2019-03-28
## 0.1.0-alpha.1 - 2019-03-28
* Initial impl * Initial impl

View File

@ -1,8 +1,8 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.3.0" version = "0.5.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Static files support for actix web." description = "Static file serving for Actix Web"
readme = "README.md" readme = "README.md"
keywords = ["actix", "http", "async", "futures"] keywords = ["actix", "http", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -21,15 +21,15 @@ actix-web = { version = "3.0.0", default-features = false }
actix-service = "1.0.6" actix-service = "1.0.6"
bitflags = "1" bitflags = "1"
bytes = "0.5.3" bytes = "0.5.3"
futures-core = { version = "0.3.5", default-features = false } futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.5", default-features = false } futures-util = { version = "0.3.7", default-features = false }
derive_more = "0.99.2" derive_more = "0.99.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
mime_guess = "2.0.1" mime_guess = "2.0.1"
percent-encoding = "2.1" percent-encoding = "2.1"
v_htmlescape = "0.10" v_htmlescape = "0.12"
[dev-dependencies] [dev-dependencies]
actix-rt = "1.0.0" actix-rt = "1.0.0"
actix-web = { version = "3.0.0", features = ["openssl"] } actix-web = "3.0.0"

View File

@ -1,9 +1,19 @@
# Static files support for actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-files)](https://crates.io/crates/actix-files) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) # actix-files
## Documentation & community resources > Static file serving for Actix Web
* [User Guide](https://actix.rs/docs/) [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
* [API Documentation](https://docs.rs/actix-files/) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.5.0)](https://docs.rs/actix-files/0.5.0)
* [Chat on gitter](https://gitter.im/actix/actix) [![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
* Cargo package: [actix-files](https://crates.io/crates/actix-files) ![License](https://img.shields.io/crates/l/actix-files.svg)
* Minimum supported Rust version: 1.40 or later <br />
[![dependency status](https://deps.rs/crate/actix-files/0.5.0/status.svg)](https://deps.rs/crate/actix-files/0.5.0)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-files/)
- [Example Project](https://github.com/actix/examples/tree/master/static_index)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum supported Rust version: 1.42 or later

View File

@ -0,0 +1,52 @@
use mime::Mime;
/// Transforms MIME `text/*` types into their UTF-8 equivalent, if supported.
///
/// MIME types that are converted
/// - application/javascript
/// - text/html
/// - text/css
/// - text/plain
/// - text/csv
/// - text/tab-separated-values
pub(crate) fn equiv_utf8_text(ct: Mime) -> Mime {
// use (roughly) order of file-type popularity for a web server
if ct == mime::APPLICATION_JAVASCRIPT {
return mime::APPLICATION_JAVASCRIPT_UTF_8;
}
if ct == mime::TEXT_HTML {
return mime::TEXT_HTML_UTF_8;
}
if ct == mime::TEXT_CSS {
return mime::TEXT_CSS_UTF_8;
}
if ct == mime::TEXT_PLAIN {
return mime::TEXT_PLAIN_UTF_8;
}
if ct == mime::TEXT_CSV {
return mime::TEXT_CSV_UTF_8;
}
if ct == mime::TEXT_TAB_SEPARATED_VALUES {
return mime::TEXT_TAB_SEPARATED_VALUES_UTF_8;
}
ct
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_equiv_utf8_text() {
assert_eq!(equiv_utf8_text(mime::TEXT_PLAIN), mime::TEXT_PLAIN_UTF_8);
assert_eq!(equiv_utf8_text(mime::TEXT_XML), mime::TEXT_XML);
assert_eq!(equiv_utf8_text(mime::IMAGE_PNG), mime::IMAGE_PNG);
}
}

View File

@ -39,6 +39,7 @@ pub struct Files {
mime_override: Option<Rc<MimeOverride>>, mime_override: Option<Rc<MimeOverride>>,
file_flags: named::Flags, file_flags: named::Flags,
guards: Option<Rc<dyn Guard>>, guards: Option<Rc<dyn Guard>>,
hidden_files: bool,
} }
impl fmt::Debug for Files { impl fmt::Debug for Files {
@ -60,18 +61,31 @@ impl Clone for Files {
path: self.path.clone(), path: self.path.clone(),
mime_override: self.mime_override.clone(), mime_override: self.mime_override.clone(),
guards: self.guards.clone(), guards: self.guards.clone(),
hidden_files: self.hidden_files,
} }
} }
} }
impl Files { impl Files {
/// Create new `Files` instance for specified base directory. /// Create new `Files` instance for a specified base directory.
/// ///
/// `File` uses `ThreadPool` for blocking filesystem operations. /// # Argument Order
/// By default pool with 5x threads of available cpus is used. /// The first argument (`mount_path`) is the root URL at which the static files are served.
/// Pool size can be changed by setting ACTIX_THREADPOOL environment variable. /// For example, `/assets` will serve files at `example.com/assets/...`.
pub fn new<T: Into<PathBuf>>(path: &str, dir: T) -> Files { ///
let orig_dir = dir.into(); /// The second argument (`serve_from`) is the location on disk at which files are loaded.
/// This can be a relative path. For example, `./` would serve files from the current
/// working directory.
///
/// # Implementation Notes
/// If the mount path is set as the root path `/`, services registered after this one will
/// be inaccessible. Register more specific handlers and services first.
///
/// `Files` uses a threadpool for blocking filesystem operations. By default, the pool uses a
/// number of threads equal to 5x the number of available logical CPUs. Pool size can be changed
/// by setting ACTIX_THREADPOOL environment variable.
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
let orig_dir = serve_from.into();
let dir = match orig_dir.canonicalize() { let dir = match orig_dir.canonicalize() {
Ok(canon_dir) => canon_dir, Ok(canon_dir) => canon_dir,
Err(_) => { Err(_) => {
@ -81,7 +95,7 @@ impl Files {
}; };
Files { Files {
path: path.to_string(), path: mount_path.to_owned(),
directory: dir, directory: dir,
index: None, index: None,
show_index: false, show_index: false,
@ -91,6 +105,7 @@ impl Files {
mime_override: None, mime_override: None,
file_flags: named::Flags::default(), file_flags: named::Flags::default(),
guards: None, guards: None,
hidden_files: false,
} }
} }
@ -138,24 +153,33 @@ impl Files {
self self
} }
#[inline]
/// Specifies whether to use ETag or not. /// Specifies whether to use ETag or not.
/// ///
/// Default is true. /// Default is true.
#[inline]
pub fn use_etag(mut self, value: bool) -> Self { pub fn use_etag(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::ETAG, value); self.file_flags.set(named::Flags::ETAG, value);
self self
} }
#[inline]
/// Specifies whether to use Last-Modified or not. /// Specifies whether to use Last-Modified or not.
/// ///
/// Default is true. /// Default is true.
#[inline]
pub fn use_last_modified(mut self, value: bool) -> Self { pub fn use_last_modified(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::LAST_MD, value); self.file_flags.set(named::Flags::LAST_MD, value);
self self
} }
/// Specifies whether text responses should signal a UTF-8 encoding.
///
/// Default is false (but will default to true in a future version).
#[inline]
pub fn prefer_utf8(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::PREFER_UTF8, value);
self
}
/// Specifies custom guards to use for directory listings and files. /// Specifies custom guards to use for directory listings and files.
/// ///
/// Default behaviour allows GET and HEAD. /// Default behaviour allows GET and HEAD.
@ -192,6 +216,13 @@ impl Files {
self self
} }
/// Enables serving hidden files and directories, allowing a leading dots in url fragments.
#[inline]
pub fn use_hidden_files(mut self) -> Self {
self.hidden_files = true;
self
}
} }
impl HttpServiceFactory for Files { impl HttpServiceFactory for Files {
@ -230,6 +261,7 @@ impl ServiceFactory for Files {
mime_override: self.mime_override.clone(), mime_override: self.mime_override.clone(),
file_flags: self.file_flags, file_flags: self.file_flags,
guards: self.guards.clone(), guards: self.guards.clone(),
hidden_files: self.hidden_files,
}; };
if let Some(ref default) = *self.default.borrow() { if let Some(ref default) = *self.default.borrow() {

View File

@ -1,4 +1,4 @@
//! Static files support for Actix Web. //! Static file serving for Actix Web.
//! //!
//! Provides a non-blocking service for serving static files from disk. //! Provides a non-blocking service for serving static files from disk.
//! //!
@ -8,12 +8,8 @@
//! use actix_files::Files; //! use actix_files::Files;
//! //!
//! let app = App::new() //! let app = App::new()
//! .service(Files::new("/static", ".")); //! .service(Files::new("/static", ".").prefer_utf8(true));
//! ``` //! ```
//!
//! # Implementation Quirks
//! - If a filename contains non-ascii characters, that file will be served with the `charset=utf-8`
//! extension on the Content-Type header.
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
#![warn(missing_docs, missing_debug_implementations)] #![warn(missing_docs, missing_debug_implementations)]
@ -30,6 +26,7 @@ use mime_guess::from_ext;
mod chunked; mod chunked;
mod directory; mod directory;
mod encoding;
mod error; mod error;
mod files; mod files;
mod named; mod named;
@ -93,6 +90,9 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_file_extension_to_mime() { async fn test_file_extension_to_mime() {
let m = file_extension_to_mime("");
assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
let m = file_extension_to_mime("jpg"); let m = file_extension_to_mime("jpg");
assert_eq!(m, mime::IMAGE_JPEG); assert_eq!(m, mime::IMAGE_JPEG);

View File

@ -22,20 +22,21 @@ use bitflags::bitflags;
use futures_util::future::{ready, Ready}; use futures_util::future::{ready, Ready};
use mime_guess::from_path; use mime_guess::from_path;
use crate::range::HttpRange;
use crate::ChunkedReadFile; use crate::ChunkedReadFile;
use crate::{encoding::equiv_utf8_text, range::HttpRange};
bitflags! { bitflags! {
pub(crate) struct Flags: u8 { pub(crate) struct Flags: u8 {
const ETAG = 0b0000_0001; const ETAG = 0b0000_0001;
const LAST_MD = 0b0000_0010; const LAST_MD = 0b0000_0010;
const CONTENT_DISPOSITION = 0b0000_0100; const CONTENT_DISPOSITION = 0b0000_0100;
const PREFER_UTF8 = 0b0000_1000;
} }
} }
impl Default for Flags { impl Default for Flags {
fn default() -> Self { fn default() -> Self {
Flags::all() Flags::from_bits_truncate(0b0000_0111)
} }
} }
@ -92,6 +93,7 @@ impl NamedFile {
}; };
let ct = from_path(&path).first_or_octet_stream(); let ct = from_path(&path).first_or_octet_stream();
let disposition = match ct.type_() { let disposition = match ct.type_() {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
_ => DispositionType::Attachment, _ => DispositionType::Attachment,
@ -191,7 +193,7 @@ impl NamedFile {
/// image, and video content types, and `attachment` otherwise, and /// image, and video content types, and `attachment` otherwise, and
/// the filename is taken from the path provided in the `open` method /// the filename is taken from the path provided in the `open` method
/// after converting it to UTF-8 using. /// after converting it to UTF-8 using.
/// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy). /// [`std::ffi::OsStr::to_string_lossy`]
#[inline] #[inline]
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self { pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
self.content_disposition = cd; self.content_disposition = cd;
@ -215,24 +217,33 @@ impl NamedFile {
self self
} }
#[inline] /// Specifies whether to use ETag or not.
///Specifies whether to use ETag or not.
/// ///
///Default is true. /// Default is true.
#[inline]
pub fn use_etag(mut self, value: bool) -> Self { pub fn use_etag(mut self, value: bool) -> Self {
self.flags.set(Flags::ETAG, value); self.flags.set(Flags::ETAG, value);
self self
} }
#[inline] /// Specifies whether to use Last-Modified or not.
///Specifies whether to use Last-Modified or not.
/// ///
///Default is true. /// Default is true.
#[inline]
pub fn use_last_modified(mut self, value: bool) -> Self { pub fn use_last_modified(mut self, value: bool) -> Self {
self.flags.set(Flags::LAST_MD, value); self.flags.set(Flags::LAST_MD, value);
self self
} }
/// Specifies whether text responses should signal a UTF-8 encoding.
///
/// Default is false (but will default to true in a future version).
#[inline]
pub fn prefer_utf8(mut self, value: bool) -> Self {
self.flags.set(Flags::PREFER_UTF8, value);
self
}
pub(crate) fn etag(&self) -> Option<header::EntityTag> { pub(crate) fn etag(&self) -> Option<header::EntityTag> {
// This etag format is similar to Apache's. // This etag format is similar to Apache's.
self.modified.as_ref().map(|mtime| { self.modified.as_ref().map(|mtime| {
@ -268,18 +279,24 @@ impl NamedFile {
/// Creates an `HttpResponse` with file as a streaming body. /// Creates an `HttpResponse` with file as a streaming body.
pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> { pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> {
if self.status_code != StatusCode::OK { if self.status_code != StatusCode::OK {
let mut resp = HttpResponse::build(self.status_code); let mut res = HttpResponse::build(self.status_code);
resp.set(header::ContentType(self.content_type.clone())) if self.flags.contains(Flags::PREFER_UTF8) {
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| { let ct = equiv_utf8_text(self.content_type.clone());
res.header( res.header(header::CONTENT_TYPE, ct.to_string());
header::CONTENT_DISPOSITION, } else {
self.content_disposition.to_string(), res.header(header::CONTENT_TYPE, self.content_type.to_string());
); }
});
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
res.header(
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
);
}
if let Some(current_encoding) = self.encoding { if let Some(current_encoding) = self.encoding {
resp.encoding(current_encoding); res.encoding(current_encoding);
} }
let reader = ChunkedReadFile { let reader = ChunkedReadFile {
@ -290,7 +307,7 @@ impl NamedFile {
counter: 0, counter: 0,
}; };
return Ok(resp.streaming(reader)); return Ok(res.streaming(reader));
} }
let etag = if self.flags.contains(Flags::ETAG) { let etag = if self.flags.contains(Flags::ETAG) {
@ -342,25 +359,33 @@ impl NamedFile {
}; };
let mut resp = HttpResponse::build(self.status_code); 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| { if self.flags.contains(Flags::PREFER_UTF8) {
res.header( let ct = equiv_utf8_text(self.content_type.clone());
header::CONTENT_DISPOSITION, resp.header(header::CONTENT_TYPE, ct.to_string());
self.content_disposition.to_string(), } else {
); resp.header(header::CONTENT_TYPE, self.content_type.to_string());
}); }
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
resp.header(
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
);
}
// default compressing // default compressing
if let Some(current_encoding) = self.encoding { if let Some(current_encoding) = self.encoding {
resp.encoding(current_encoding); resp.encoding(current_encoding);
} }
resp.if_some(last_modified, |lm, resp| { if let Some(lm) = last_modified {
resp.set(header::LastModified(lm)); resp.header(header::LAST_MODIFIED, lm.to_string());
}) }
.if_some(etag, |etag, resp| {
resp.set(header::ETag(etag)); if let Some(etag) = etag {
}); resp.header(header::ETAG, etag.to_string());
}
resp.header(header::ACCEPT_RANGES, "bytes"); resp.header(header::ACCEPT_RANGES, "bytes");

View File

@ -15,12 +15,19 @@ impl FromStr for PathBufWrap {
type Err = UriSegmentError; type Err = UriSegmentError;
fn from_str(path: &str) -> Result<Self, Self::Err> { fn from_str(path: &str) -> Result<Self, Self::Err> {
Self::parse_path(path, false)
}
}
impl PathBufWrap {
/// Parse a path, giving the choice of allowing hidden files to be considered valid segments.
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
let mut buf = PathBuf::new(); let mut buf = PathBuf::new();
for segment in path.split('/') { for segment in path.split('/') {
if segment == ".." { if segment == ".." {
buf.pop(); buf.pop();
} else if segment.starts_with('.') { } else if !hidden_files && segment.starts_with('.') {
return Err(UriSegmentError::BadStart('.')); return Err(UriSegmentError::BadStart('.'));
} else if segment.starts_with('*') { } else if segment.starts_with('*') {
return Err(UriSegmentError::BadStart('*')); return Err(UriSegmentError::BadStart('*'));
@ -96,4 +103,17 @@ mod tests {
PathBuf::from_iter(vec!["seg2"]) PathBuf::from_iter(vec!["seg2"])
); );
} }
#[test]
fn test_parse_path() {
assert_eq!(
PathBufWrap::parse_path("/test/.tt", false).map(|t| t.0),
Err(UriSegmentError::BadStart('.'))
);
assert_eq!(
PathBufWrap::parse_path("/test/.tt", true).unwrap().0,
PathBuf::from_iter(vec!["test", ".tt"])
);
}
} }

View File

@ -31,6 +31,7 @@ pub struct FilesService {
pub(crate) mime_override: Option<Rc<MimeOverride>>, pub(crate) mime_override: Option<Rc<MimeOverride>>,
pub(crate) file_flags: named::Flags, pub(crate) file_flags: named::Flags,
pub(crate) guards: Option<Rc<dyn Guard>>, pub(crate) guards: Option<Rc<dyn Guard>>,
pub(crate) hidden_files: bool,
} }
type FilesServiceFuture = Either< type FilesServiceFuture = Either<
@ -83,10 +84,11 @@ impl Service for FilesService {
))); )));
} }
let real_path: PathBufWrap = match req.match_info().path().parse() { let real_path =
Ok(item) => item, match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) {
Err(e) => return Either::Left(ok(req.error_response(e))), Ok(item) => item,
}; Err(e) => return Either::Left(ok(req.error_response(e))),
};
// full file path // full file path
let path = match self.directory.join(&real_path).canonicalize() { let path = match self.directory.join(&real_path).canonicalize() {

View File

@ -0,0 +1,40 @@
use actix_files::Files;
use actix_web::{
http::{
header::{self, HeaderValue},
StatusCode,
},
test::{self, TestRequest},
App,
};
#[actix_rt::test]
async fn test_utf8_file_contents() {
// use default ISO-8859-1 encoding
let mut srv =
test::init_service(App::new().service(Files::new("/", "./tests"))).await;
let req = TestRequest::with_uri("/utf8.txt").to_request();
let res = test::call_service(&mut srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("text/plain")),
);
// prefer UTF-8 encoding
let mut srv = test::init_service(
App::new().service(Files::new("/", "./tests").prefer_utf8(true)),
)
.await;
let req = TestRequest::with_uri("/utf8.txt").to_request();
let res = test::call_service(&mut srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
);
}

View File

@ -0,0 +1,3 @@
中文内容显示正确。
English is OK.

View File

@ -2,13 +2,20 @@
## Unreleased - 2020-xx-xx ## Unreleased - 2020-xx-xx
* add ability to set address for `TestServer` [#1645]
## 2.1.0 - 2020-11-25
* Add ability to set address for `TestServer`. [#1645]
* Upgrade `base64` to `0.13`.
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
[#1773]: https://github.com/actix/actix-web/pull/1773
[#1645]: https://github.com/actix/actix-web/pull/1645 [#1645]: https://github.com/actix/actix-web/pull/1645
## 2.0.0 - 2020-09-11 ## 2.0.0 - 2020-09-11
* Update actix-codec and actix-utils dependencies. * Update actix-codec and actix-utils dependencies.
## 2.0.0-alpha.1 - 2020-05-23 ## 2.0.0-alpha.1 - 2020-05-23
* Update the `time` dependency to 0.2.7 * Update the `time` dependency to 0.2.7
* Update `actix-connect` dependency to 2.0.0-alpha.2 * Update `actix-connect` dependency to 2.0.0-alpha.2
@ -18,74 +25,56 @@
* Update `base64` dependency to 0.12 * Update `base64` dependency to 0.12
* Update `env_logger` dependency to 0.7 * Update `env_logger` dependency to 0.7
## [1.0.0] - 2019-12-13 ## 1.0.0 - 2019-12-13
### Changed
* Replaced `TestServer::start()` with `test_server()` * Replaced `TestServer::start()` with `test_server()`
## [1.0.0-alpha.3] - 2019-12-07 ## 1.0.0-alpha.3 - 2019-12-07
### Changed
* Migrate to `std::future` * Migrate to `std::future`
## [0.2.5] - 2019-09-17 ## 0.2.5 - 2019-09-17
### Changed
* Update serde_urlencoded to "0.6.1" * Update serde_urlencoded to "0.6.1"
* Increase TestServerRuntime timeouts from 500ms to 3000ms * Increase TestServerRuntime timeouts from 500ms to 3000ms
### Fixed
* Do not override current `System` * Do not override current `System`
## [0.2.4] - 2019-07-18 ## 0.2.4 - 2019-07-18
* Update actix-server to 0.6 * Update actix-server to 0.6
## [0.2.3] - 2019-07-16
## 0.2.3 - 2019-07-16
* Add `delete`, `options`, `patch` methods to `TestServerRunner` * Add `delete`, `options`, `patch` methods to `TestServerRunner`
## [0.2.2] - 2019-06-16
## 0.2.2 - 2019-06-16
* Add .put() and .sput() methods * Add .put() and .sput() methods
## [0.2.1] - 2019-06-05
## 0.2.1 - 2019-06-05
* Add license files * Add license files
## [0.2.0] - 2019-05-12
## 0.2.0 - 2019-05-12
* Update awc and actix-http deps * Update awc and actix-http deps
## [0.1.1] - 2019-04-24
## 0.1.1 - 2019-04-24
* Always make new connection for http client * Always make new connection for http client
## [0.1.0] - 2019-04-16 ## 0.1.0 - 2019-04-16
* No changes * No changes
## [0.1.0-alpha.3] - 2019-04-02 ## 0.1.0-alpha.3 - 2019-04-02
* Request functions accept path #743 * Request functions accept path #743
## [0.1.0-alpha.2] - 2019-03-29 ## 0.1.0-alpha.2 - 2019-03-29
* Added TestServerRuntime::load_body() method * Added TestServerRuntime::load_body() method
* Update actix-http and awc libraries * Update actix-http and awc libraries
## [0.1.0-alpha.1] - 2019-03-28 ## 0.1.0-alpha.1 - 2019-03-28
* Initial impl * Initial impl

View File

@ -1,8 +1,8 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "2.0.0" version = "2.1.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix HTTP test server" description = "Various helpers for Actix applications to use during testing"
readme = "README.md" readme = "README.md"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -38,7 +38,7 @@ actix-server = "1.0.0"
actix-testing = "1.0.0" actix-testing = "1.0.0"
awc = "2.0.0" awc = "2.0.0"
base64 = "0.12" base64 = "0.13"
bytes = "0.5.3" bytes = "0.5.3"
futures-core = { version = "0.3.5", default-features = false } futures-core = { version = "0.3.5", default-features = false }
http = "0.2.0" http = "0.2.0"
@ -47,7 +47,7 @@ socket2 = "0.3"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
slab = "0.4" slab = "0.4"
serde_urlencoded = "0.6.1" serde_urlencoded = "0.7"
time = { version = "0.2.7", default-features = false, features = ["std"] } time = { version = "0.2.7", default-features = false, features = ["std"] }
open-ssl = { version = "0.10", package = "openssl", optional = true } open-ssl = { version = "0.10", package = "openssl", optional = true }

15
actix-http-test/README.md Normal file
View File

@ -0,0 +1,15 @@
# actix-http-test
> Various helpers for Actix applications to use during testing.
[![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=2.1.0)](https://docs.rs/actix-http-test/2.1.0)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-http-test)
[![Dependency Status](https://deps.rs/crate/actix-http-test/2.1.0/status.svg)](https://deps.rs/crate/actix-http-test/2.1.0)
[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-http-test)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum Supported Rust Version (MSRV): 1.42.0

View File

@ -1,4 +1,9 @@
//! Various helpers for Actix applications to use during testing. //! Various helpers for Actix applications to use during testing.
#![deny(rust_2018_idioms)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
use std::sync::mpsc; use std::sync::mpsc;
use std::{net, thread, time}; use std::{net, thread, time};
@ -48,7 +53,7 @@ pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer
test_server_with_addr(tcp, factory).await test_server_with_addr(tcp, factory).await
} }
/// Start [`test server`](./fn.test_server.html) on a concrete Address /// Start [`test server`](test_server()) on a concrete Address
pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>( pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
tcp: net::TcpListener, tcp: net::TcpListener,
factory: F, factory: F,

View File

@ -1,6 +1,41 @@
# Changes # Changes
## Unreleased - 2020-xx-xx ## Unreleased - 2020-xx-xx
### Changed
* Bumped `rand` to `0.8`
## 2.2.0 - 2020-11-25
### Added
* HttpResponse builders for 1xx status codes. [#1768]
* `Accept::mime_precedence` and `Accept::mime_preference`. [#1793]
* `TryFrom<u16>` and `TryFrom<f32>` for `http::header::Quality`. [#1797]
### Fixed
* Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767]
### Changed
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
[#1773]: https://github.com/actix/actix-web/pull/1773
[#1767]: https://github.com/actix/actix-web/pull/1767
[#1768]: https://github.com/actix/actix-web/pull/1768
[#1793]: https://github.com/actix/actix-web/pull/1793
[#1797]: https://github.com/actix/actix-web/pull/1797
## 2.1.0 - 2020-10-30
### Added
* Added more flexible `on_connect_ext` methods for on-connect handling. [#1754]
### Changed
* Upgrade `base64` to `0.13`. [#1744]
* Upgrade `pin-project` to `1.0`. [#1733]
* Deprecate `ResponseBuilder::{if_some, if_true}`. [#1760]
[#1760]: https://github.com/actix/actix-web/pull/1760
[#1754]: https://github.com/actix/actix-web/pull/1754
[#1733]: https://github.com/actix/actix-web/pull/1733
[#1744]: https://github.com/actix/actix-web/pull/1744
## 2.0.0 - 2020-09-11 ## 2.0.0 - 2020-09-11

View File

@ -1,46 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at fafhrd91@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -1,8 +1,8 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "2.0.0" version = "2.2.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix HTTP primitives" description = "HTTP primitives for the Actix ecosystem"
readme = "README.md" readme = "README.md"
keywords = ["actix", "http", "framework", "async", "futures"] keywords = ["actix", "http", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -49,7 +49,7 @@ actix-threadpool = "0.3.1"
actix-tls = { version = "2.0.0", optional = true } actix-tls = { version = "2.0.0", optional = true }
actix = { version = "0.10.0", optional = true } actix = { version = "0.10.0", optional = true }
base64 = "0.12" base64 = "0.13"
bitflags = "1.2" bitflags = "1.2"
bytes = "0.5.3" bytes = "0.5.3"
cookie = { version = "0.14.1", features = ["percent-encode"] } cookie = { version = "0.14.1", features = ["percent-encode"] }
@ -71,14 +71,14 @@ language-tags = "0.2"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project = "0.4.17" pin-project = "1.0.0"
rand = "0.7" rand = "0.8"
regex = "1.3" regex = "1.3"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
sha-1 = "0.9" sha-1 = "0.9"
slab = "0.4" slab = "0.4"
serde_urlencoded = "0.6.1" serde_urlencoded = "0.7"
time = { version = "0.2.7", default-features = false, features = ["std"] } time = { version = "0.2.7", default-features = false, features = ["std"] }
# compression # compression

View File

@ -1,14 +1,18 @@
# Actix http [![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-http)](https://crates.io/crates/actix-http) [![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-http
Actix http > HTTP primitives for the Actix ecosystem.
## Documentation & community resources [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=2.2.0)](https://docs.rs/actix-http/2.2.0)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-http)
[![Dependency Status](https://deps.rs/crate/actix-http/2.2.0/status.svg)](https://deps.rs/crate/actix-http/2.2.0)
[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
* [User Guide](https://actix.rs/docs/) ## Documentation & Resources
* [API Documentation](https://docs.rs/actix-http/)
* [Chat on gitter](https://gitter.im/actix/actix) - [API Documentation](https://docs.rs/actix-http)
* Cargo package: [actix-http](https://crates.io/crates/actix-http) - [Chat on Gitter](https://gitter.im/actix/actix-web)
* Minimum supported Rust version: 1.40 or later - Minimum Supported Rust Version (MSRV): 1.42.0
## Example ## Example

View File

@ -14,10 +14,11 @@ use crate::helpers::{Data, DataFactory};
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
use crate::service::HttpService; use crate::service::HttpService;
use crate::{ConnectCallback, Extensions};
/// A http service builder /// A HTTP service builder
/// ///
/// This type can be used to construct an instance of `http service` through a /// This type can be used to construct an instance of [`HttpService`] through a
/// builder-like pattern. /// builder-like pattern.
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> { pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> {
keep_alive: KeepAlive, keep_alive: KeepAlive,
@ -27,7 +28,9 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> {
local_addr: Option<net::SocketAddr>, local_addr: Option<net::SocketAddr>,
expect: X, expect: X,
upgrade: Option<U>, upgrade: Option<U>,
// DEPRECATED: in favor of on_connect_ext
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, S)>, _t: PhantomData<(T, S)>,
} }
@ -49,6 +52,7 @@ where
expect: ExpectHandler, expect: ExpectHandler,
upgrade: None, upgrade: None,
on_connect: None, on_connect: None,
on_connect_ext: None,
_t: PhantomData, _t: PhantomData,
} }
} }
@ -138,6 +142,7 @@ where
expect: expect.into_factory(), expect: expect.into_factory(),
upgrade: self.upgrade, upgrade: self.upgrade,
on_connect: self.on_connect, on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData, _t: PhantomData,
} }
} }
@ -167,14 +172,16 @@ where
expect: self.expect, expect: self.expect,
upgrade: Some(upgrade.into_factory()), upgrade: Some(upgrade.into_factory()),
on_connect: self.on_connect, on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData, _t: PhantomData,
} }
} }
/// Set on-connect callback. /// Set on-connect callback.
/// ///
/// It get called once per connection and result of the call /// Called once per connection. Return value of the call is stored in request extensions.
/// get stored to the request's extensions. ///
/// *SOFT DEPRECATED*: Prefer the `on_connect_ext` style callback.
pub fn on_connect<F, I>(mut self, f: F) -> Self pub fn on_connect<F, I>(mut self, f: F) -> Self
where where
F: Fn(&T) -> I + 'static, F: Fn(&T) -> I + 'static,
@ -184,7 +191,20 @@ where
self self
} }
/// Finish service configuration and create *http service* for HTTP/1 protocol. /// Sets the callback to be run on connection establishment.
///
/// Has mutable access to a data container that will be merged into request extensions.
/// This enables transport layer data (like client certificates) to be accessed in middleware
/// and handlers.
pub fn on_connect_ext<F>(mut self, f: F) -> Self
where
F: Fn(&T, &mut Extensions) + 'static,
{
self.on_connect_ext = Some(Rc::new(f));
self
}
/// Finish service configuration and create a HTTP Service for HTTP/1 protocol.
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U> pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
where where
B: MessageBody, B: MessageBody,
@ -200,13 +220,15 @@ where
self.secure, self.secure,
self.local_addr, self.local_addr,
); );
H1Service::with_config(cfg, service.into_factory()) H1Service::with_config(cfg, service.into_factory())
.expect(self.expect) .expect(self.expect)
.upgrade(self.upgrade) .upgrade(self.upgrade)
.on_connect(self.on_connect) .on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext)
} }
/// Finish service configuration and create *http service* for HTTP/2 protocol. /// Finish service configuration and create a HTTP service for HTTP/2 protocol.
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B> pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
where where
B: MessageBody + 'static, B: MessageBody + 'static,
@ -223,7 +245,10 @@ where
self.secure, self.secure,
self.local_addr, self.local_addr,
); );
H2Service::with_config(cfg, service.into_factory()).on_connect(self.on_connect)
H2Service::with_config(cfg, service.into_factory())
.on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext)
} }
/// Finish service configuration and create `HttpService` instance. /// Finish service configuration and create `HttpService` instance.
@ -243,9 +268,11 @@ where
self.secure, self.secure,
self.local_addr, self.local_addr,
); );
HttpService::with_config(cfg, service.into_factory()) HttpService::with_config(cfg, service.into_factory())
.expect(self.expect) .expect(self.expect)
.upgrade(self.upgrade) .upgrade(self.upgrade)
.on_connect(self.on_connect) .on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext)
} }
} }

View File

@ -9,8 +9,9 @@ use std::time::{Duration, Instant};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::time::{delay_for, Delay}; use actix_rt::time::{delay_for, Delay};
use actix_service::Service; use actix_service::Service;
use actix_utils::{oneshot, task::LocalWaker}; use actix_utils::task::LocalWaker;
use bytes::Bytes; use bytes::Bytes;
use futures_channel::oneshot;
use futures_util::future::{poll_fn, FutureExt, LocalBoxFuture}; use futures_util::future::{poll_fn, FutureExt, LocalBoxFuture};
use fxhash::FxHashMap; use fxhash::FxHashMap;
use h2::client::{Connection, SendRequest}; use h2::client::{Connection, SendRequest};

View File

@ -4,12 +4,12 @@ use std::task::{Context, Poll};
use actix_service::Service; use actix_service::Service;
#[doc(hidden)]
/// Service that allows to turn non-clone service to a service with `Clone` impl /// Service that allows to turn non-clone service to a service with `Clone` impl
/// ///
/// # Panics /// # Panics
/// CloneableService might panic with some creative use of thread local storage. /// CloneableService might panic with some creative use of thread local storage.
/// See https://github.com/actix/actix-web/issues/1295 for example /// See https://github.com/actix/actix-web/issues/1295 for example
#[doc(hidden)]
pub(crate) struct CloneableService<T: Service>(Rc<RefCell<T>>); pub(crate) struct CloneableService<T: Service>(Rc<RefCell<T>>);
impl<T: Service> CloneableService<T> { impl<T: Service> CloneableService<T> {

View File

@ -25,7 +25,7 @@ pub use crate::cookie::ParseError as CookieParseError;
use crate::helpers::Writer; use crate::helpers::Writer;
use crate::response::{Response, ResponseBuilder}; use crate::response::{Response, ResponseBuilder};
/// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html) /// A specialized [`std::result::Result`]
/// for actix web operations /// for actix web operations
/// ///
/// This typedef is generally used to avoid writing out /// This typedef is generally used to avoid writing out

View File

@ -1,10 +1,10 @@
use std::any::{Any, TypeId}; use std::any::{Any, TypeId};
use std::fmt; use std::{fmt, mem};
use fxhash::FxHashMap; use fxhash::FxHashMap;
#[derive(Default)]
/// A type map of request extensions. /// A type map of request extensions.
#[derive(Default)]
pub struct Extensions { pub struct Extensions {
/// Use FxHasher with a std HashMap with for faster /// Use FxHasher with a std HashMap with for faster
/// lookups on the small `TypeId` (u64 equivalent) keys. /// lookups on the small `TypeId` (u64 equivalent) keys.
@ -61,6 +61,16 @@ impl Extensions {
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.map.clear(); self.map.clear();
} }
/// Extends self with the items from another `Extensions`.
pub fn extend(&mut self, other: Extensions) {
self.map.extend(other.map);
}
/// Sets (or overrides) items from `other` into this map.
pub(crate) fn drain_from(&mut self, other: &mut Self) {
self.map.extend(mem::take(&mut other.map));
}
} }
impl fmt::Debug for Extensions { impl fmt::Debug for Extensions {
@ -178,4 +188,57 @@ mod tests {
assert_eq!(extensions.get::<bool>(), None); assert_eq!(extensions.get::<bool>(), None);
assert_eq!(extensions.get(), Some(&MyType(10))); assert_eq!(extensions.get(), Some(&MyType(10)));
} }
#[test]
fn test_extend() {
#[derive(Debug, PartialEq)]
struct MyType(i32);
let mut extensions = Extensions::new();
extensions.insert(5i32);
extensions.insert(MyType(10));
let mut other = Extensions::new();
other.insert(15i32);
other.insert(20u8);
extensions.extend(other);
assert_eq!(extensions.get(), Some(&15i32));
assert_eq!(extensions.get_mut(), Some(&mut 15i32));
assert_eq!(extensions.remove::<i32>(), Some(15i32));
assert!(extensions.get::<i32>().is_none());
assert_eq!(extensions.get::<bool>(), None);
assert_eq!(extensions.get(), Some(&MyType(10)));
assert_eq!(extensions.get(), Some(&20u8));
assert_eq!(extensions.get_mut(), Some(&mut 20u8));
}
#[test]
fn test_drain_from() {
let mut ext = Extensions::new();
ext.insert(2isize);
let mut more_ext = Extensions::new();
more_ext.insert(5isize);
more_ext.insert(5usize);
assert_eq!(ext.get::<isize>(), Some(&2isize));
assert_eq!(ext.get::<usize>(), None);
assert_eq!(more_ext.get::<isize>(), Some(&5isize));
assert_eq!(more_ext.get::<usize>(), Some(&5usize));
ext.drain_from(&mut more_ext);
assert_eq!(ext.get::<isize>(), Some(&5isize));
assert_eq!(ext.get::<usize>(), Some(&5usize));
assert_eq!(more_ext.get::<isize>(), None);
assert_eq!(more_ext.get::<usize>(), None);
}
} }

View File

@ -58,6 +58,7 @@ impl Codec {
} else { } else {
Flags::empty() Flags::empty()
}; };
Codec { Codec {
config, config,
flags, flags,
@ -69,26 +70,26 @@ impl Codec {
} }
} }
/// Check if request is upgrade.
#[inline] #[inline]
/// Check if request is upgrade
pub fn upgrade(&self) -> bool { pub fn upgrade(&self) -> bool {
self.ctype == ConnectionType::Upgrade self.ctype == ConnectionType::Upgrade
} }
/// Check if last response is keep-alive.
#[inline] #[inline]
/// Check if last response is keep-alive
pub fn keepalive(&self) -> bool { pub fn keepalive(&self) -> bool {
self.ctype == ConnectionType::KeepAlive self.ctype == ConnectionType::KeepAlive
} }
/// Check if keep-alive enabled on server level.
#[inline] #[inline]
/// Check if keep-alive enabled on server level
pub fn keepalive_enabled(&self) -> bool { pub fn keepalive_enabled(&self) -> bool {
self.flags.contains(Flags::KEEPALIVE_ENABLED) self.flags.contains(Flags::KEEPALIVE_ENABLED)
} }
/// Check last request's message type.
#[inline] #[inline]
/// Check last request's message type
pub fn message_type(&self) -> MessageType { pub fn message_type(&self) -> MessageType {
if self.flags.contains(Flags::STREAM) { if self.flags.contains(Flags::STREAM) {
MessageType::Stream MessageType::Stream

View File

@ -1,8 +1,11 @@
use std::collections::VecDeque; use std::{
use std::future::Future; collections::VecDeque,
use std::pin::Pin; fmt,
use std::task::{Context, Poll}; future::Future,
use std::{fmt, io, net}; io, mem, net,
pin::Pin,
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed, FramedParts}; use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed, FramedParts};
use actix_rt::time::{delay_until, Delay, Instant}; use actix_rt::time::{delay_until, Delay, Instant};
@ -12,7 +15,6 @@ use bytes::{Buf, BytesMut};
use log::{error, trace}; use log::{error, trace};
use pin_project::pin_project; use pin_project::pin_project;
use crate::body::{Body, BodySize, MessageBody, ResponseBody};
use crate::cloneable::CloneableService; use crate::cloneable::CloneableService;
use crate::config::ServiceConfig; use crate::config::ServiceConfig;
use crate::error::{DispatchError, Error}; use crate::error::{DispatchError, Error};
@ -21,6 +23,10 @@ use crate::helpers::DataFactory;
use crate::httpmessage::HttpMessage; use crate::httpmessage::HttpMessage;
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
use crate::{
body::{Body, BodySize, MessageBody, ResponseBody},
Extensions,
};
use super::codec::Codec; use super::codec::Codec;
use super::payload::{Payload, PayloadSender, PayloadStatus}; use super::payload::{Payload, PayloadSender, PayloadStatus};
@ -56,6 +62,9 @@ where
{ {
#[pin] #[pin]
inner: DispatcherState<T, S, B, X, U>, inner: DispatcherState<T, S, B, X, U>,
#[cfg(test)]
poll_count: u64,
} }
#[pin_project(project = DispatcherStateProj)] #[pin_project(project = DispatcherStateProj)]
@ -88,6 +97,7 @@ where
expect: CloneableService<X>, expect: CloneableService<X>,
upgrade: Option<CloneableService<U>>, upgrade: Option<CloneableService<U>>,
on_connect: Option<Box<dyn DataFactory>>, on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
flags: Flags, flags: Flags,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
error: Option<DispatchError>, error: Option<DispatchError>,
@ -120,8 +130,8 @@ where
B: MessageBody, B: MessageBody,
{ {
None, None,
ExpectCall(Pin<Box<X::Future>>), ExpectCall(#[pin] X::Future),
ServiceCall(Pin<Box<S::Future>>), ServiceCall(#[pin] S::Future),
SendPayload(#[pin] ResponseBody<B>), SendPayload(#[pin] ResponseBody<B>),
} }
@ -167,7 +177,7 @@ where
U: Service<Request = (Request, Framed<T, Codec>), Response = ()>, U: Service<Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
/// Create http/1 dispatcher. /// Create HTTP/1 dispatcher.
pub(crate) fn new( pub(crate) fn new(
stream: T, stream: T,
config: ServiceConfig, config: ServiceConfig,
@ -175,6 +185,7 @@ where
expect: CloneableService<X>, expect: CloneableService<X>,
upgrade: Option<CloneableService<U>>, upgrade: Option<CloneableService<U>>,
on_connect: Option<Box<dyn DataFactory>>, on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
) -> Self { ) -> Self {
Dispatcher::with_timeout( Dispatcher::with_timeout(
@ -187,6 +198,7 @@ where
expect, expect,
upgrade, upgrade,
on_connect, on_connect,
on_connect_data,
peer_addr, peer_addr,
) )
} }
@ -202,6 +214,7 @@ where
expect: CloneableService<X>, expect: CloneableService<X>,
upgrade: Option<CloneableService<U>>, upgrade: Option<CloneableService<U>>,
on_connect: Option<Box<dyn DataFactory>>, on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
) -> Self { ) -> Self {
let keepalive = config.keep_alive_enabled(); let keepalive = config.keep_alive_enabled();
@ -234,11 +247,15 @@ where
expect, expect,
upgrade, upgrade,
on_connect, on_connect,
on_connect_data,
flags, flags,
peer_addr, peer_addr,
ka_expire, ka_expire,
ka_timer, ka_timer,
}), }),
#[cfg(test)]
poll_count: 0,
} }
} }
} }
@ -330,7 +347,7 @@ where
self: Pin<&mut Self>, self: Pin<&mut Self>,
message: Response<()>, message: Response<()>,
body: ResponseBody<B>, body: ResponseBody<B>,
) -> Result<State<S, B, X>, DispatchError> { ) -> Result<(), DispatchError> {
let mut this = self.project(); let mut this = self.project();
this.codec this.codec
.encode(Message::Item((message, body.size())), &mut this.write_buf) .encode(Message::Item((message, body.size())), &mut this.write_buf)
@ -343,9 +360,10 @@ where
this.flags.set(Flags::KEEPALIVE, this.codec.keepalive()); this.flags.set(Flags::KEEPALIVE, this.codec.keepalive());
match body.size() { match body.size() {
BodySize::None | BodySize::Empty => Ok(State::None), BodySize::None | BodySize::Empty => this.state.set(State::None),
_ => Ok(State::SendPayload(body)), _ => this.state.set(State::SendPayload(body)),
} };
Ok(())
} }
fn send_continue(self: Pin<&mut Self>) { fn send_continue(self: Pin<&mut Self>) {
@ -360,49 +378,52 @@ where
) -> Result<PollResponse, DispatchError> { ) -> Result<PollResponse, DispatchError> {
loop { loop {
let mut this = self.as_mut().project(); let mut this = self.as_mut().project();
let state = match this.state.project() { // state is not changed on Poll::Pending.
// other variant and conditions always trigger a state change(or an error).
let state_change = match this.state.project() {
StateProj::None => match this.messages.pop_front() { StateProj::None => match this.messages.pop_front() {
Some(DispatcherMessage::Item(req)) => { Some(DispatcherMessage::Item(req)) => {
Some(self.as_mut().handle_request(req, cx)?) self.as_mut().handle_request(req, cx)?;
true
} }
Some(DispatcherMessage::Error(res)) => Some( Some(DispatcherMessage::Error(res)) => {
self.as_mut() self.as_mut()
.send_response(res, ResponseBody::Other(Body::Empty))?, .send_response(res, ResponseBody::Other(Body::Empty))?;
), true
}
Some(DispatcherMessage::Upgrade(req)) => { Some(DispatcherMessage::Upgrade(req)) => {
return Ok(PollResponse::Upgrade(req)); return Ok(PollResponse::Upgrade(req));
} }
None => None, None => false,
}, },
StateProj::ExpectCall(fut) => match fut.as_mut().poll(cx) { StateProj::ExpectCall(fut) => match fut.poll(cx) {
Poll::Ready(Ok(req)) => { Poll::Ready(Ok(req)) => {
self.as_mut().send_continue(); self.as_mut().send_continue();
this = self.as_mut().project(); this = self.as_mut().project();
this.state this.state.set(State::ServiceCall(this.service.call(req)));
.set(State::ServiceCall(Box::pin(this.service.call(req))));
continue; continue;
} }
Poll::Ready(Err(e)) => { Poll::Ready(Err(e)) => {
let res: Response = e.into().into(); let res: Response = e.into().into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
Some(self.as_mut().send_response(res, body.into_body())?) self.as_mut().send_response(res, body.into_body())?;
true
} }
Poll::Pending => None, Poll::Pending => false,
}, },
StateProj::ServiceCall(fut) => match fut.as_mut().poll(cx) { StateProj::ServiceCall(fut) => match fut.poll(cx) {
Poll::Ready(Ok(res)) => { Poll::Ready(Ok(res)) => {
let (res, body) = res.into().replace_body(()); let (res, body) = res.into().replace_body(());
let state = self.as_mut().send_response(res, body)?; self.as_mut().send_response(res, body)?;
this = self.as_mut().project();
this.state.set(state);
continue; continue;
} }
Poll::Ready(Err(e)) => { Poll::Ready(Err(e)) => {
let res: Response = e.into().into(); let res: Response = e.into().into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
Some(self.as_mut().send_response(res, body.into_body())?) self.as_mut().send_response(res, body.into_body())?;
true
} }
Poll::Pending => None, Poll::Pending => false,
}, },
StateProj::SendPayload(mut stream) => { StateProj::SendPayload(mut stream) => {
loop { loop {
@ -437,11 +458,8 @@ where
} }
}; };
this = self.as_mut().project(); // state is changed and continue when the state is not Empty
if state_change {
// set new state
if let Some(state) = state {
this.state.set(state);
if !self.state.is_empty() { if !self.state.is_empty() {
continue; continue;
} }
@ -466,49 +484,77 @@ where
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
req: Request, req: Request,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Result<State<S, B, X>, DispatchError> { ) -> Result<(), DispatchError> {
// Handle `EXPECT: 100-Continue` header // Handle `EXPECT: 100-Continue` header
let req = if req.head().expect() { if req.head().expect() {
let mut task = Box::pin(self.as_mut().project().expect.call(req)); // set dispatcher state so the future is pinned.
match task.as_mut().poll(cx) { let task = self.as_mut().project().expect.call(req);
Poll::Ready(Ok(req)) => { self.as_mut().project().state.set(State::ExpectCall(task));
self.as_mut().send_continue();
req
}
Poll::Pending => return Ok(State::ExpectCall(task)),
Poll::Ready(Err(e)) => {
let e = e.into();
let res: Response = e.into();
let (res, body) = res.replace_body(());
return self.send_response(res, body.into_body());
}
}
} else { } else {
req // the same as above.
let task = self.as_mut().project().service.call(req);
self.as_mut().project().state.set(State::ServiceCall(task));
}; };
// Call service // eagerly poll the future for once(or twice if expect is resolved immediately).
let mut task = Box::pin(self.as_mut().project().service.call(req)); loop {
match task.as_mut().poll(cx) { match self.as_mut().project().state.project() {
Poll::Ready(Ok(res)) => { StateProj::ExpectCall(fut) => {
let (res, body) = res.into().replace_body(()); match fut.poll(cx) {
self.send_response(res, body) // expect is resolved. continue loop and poll the service call branch.
} Poll::Ready(Ok(req)) => {
Poll::Pending => Ok(State::ServiceCall(task)), self.as_mut().send_continue();
Poll::Ready(Err(e)) => { let task = self.as_mut().project().service.call(req);
let res: Response = e.into().into(); self.as_mut().project().state.set(State::ServiceCall(task));
let (res, body) = res.replace_body(()); continue;
self.send_response(res, body.into_body()) }
// future is pending. return Ok(()) to notify that a new state is
// set and the outer loop should be continue.
Poll::Pending => return Ok(()),
// future is error. send response and return a result. On success
// to notify the dispatcher a new state is set and the outer loop
// should be continue.
Poll::Ready(Err(e)) => {
let e = e.into();
let res: Response = e.into();
let (res, body) = res.replace_body(());
return self.send_response(res, body.into_body());
}
}
}
StateProj::ServiceCall(fut) => {
// return no matter the service call future's result.
return match fut.poll(cx) {
// future is resolved. send response and return a result. On success
// to notify the dispatcher a new state is set and the outer loop
// should be continue.
Poll::Ready(Ok(res)) => {
let (res, body) = res.into().replace_body(());
self.send_response(res, body)
}
// see the comment on ExpectCall state branch's Pending.
Poll::Pending => Ok(()),
// see the comment on ExpectCall state branch's Ready(Err(e)).
Poll::Ready(Err(e)) => {
let res: Response = e.into().into();
let (res, body) = res.replace_body(());
self.send_response(res, body.into_body())
}
};
}
_ => unreachable!(
"State must be set to ServiceCall or ExceptCall in handle_request"
),
} }
} }
} }
/// Process one incoming requests /// Process one incoming request.
pub(self) fn poll_request( pub(self) fn poll_request(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Result<bool, DispatchError> { ) -> Result<bool, DispatchError> {
// limit a mount of non processed requests // limit amount of non-processed requests
if self.messages.len() >= MAX_PIPELINED_MESSAGES || !self.can_read(cx) { if self.messages.len() >= MAX_PIPELINED_MESSAGES || !self.can_read(cx) {
return Ok(false); return Ok(false);
} }
@ -526,11 +572,15 @@ where
let pl = this.codec.message_type(); let pl = this.codec.message_type();
req.head_mut().peer_addr = *this.peer_addr; req.head_mut().peer_addr = *this.peer_addr;
// DEPRECATED
// set on_connect data // set on_connect data
if let Some(ref on_connect) = this.on_connect { if let Some(ref on_connect) = this.on_connect {
on_connect.set(&mut req.extensions_mut()); on_connect.set(&mut req.extensions_mut());
} }
// merge on_connect_ext data into request extensions
req.extensions_mut().drain_from(this.on_connect_data);
if pl == MessageType::Stream && this.upgrade.is_some() { if pl == MessageType::Stream && this.upgrade.is_some() {
this.messages.push_back(DispatcherMessage::Upgrade(req)); this.messages.push_back(DispatcherMessage::Upgrade(req));
break; break;
@ -545,9 +595,8 @@ where
// handle request early // handle request early
if this.state.is_empty() { if this.state.is_empty() {
let state = self.as_mut().handle_request(req, cx)?; self.as_mut().handle_request(req, cx)?;
this = self.as_mut().project(); this = self.as_mut().project();
this.state.set(state);
} else { } else {
this.messages.push_back(DispatcherMessage::Item(req)); this.messages.push_back(DispatcherMessage::Item(req));
} }
@ -713,6 +762,12 @@ where
#[inline] #[inline]
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.as_mut().project(); let this = self.as_mut().project();
#[cfg(test)]
{
*this.poll_count += 1;
}
match this.inner.project() { match this.inner.project() {
DispatcherStateProj::Normal(mut inner) => { DispatcherStateProj::Normal(mut inner) => {
inner.as_mut().poll_keepalive(cx)?; inner.as_mut().poll_keepalive(cx)?;
@ -776,10 +831,10 @@ where
let inner_p = inner.as_mut().project(); let inner_p = inner.as_mut().project();
let mut parts = FramedParts::with_read_buf( let mut parts = FramedParts::with_read_buf(
inner_p.io.take().unwrap(), inner_p.io.take().unwrap(),
std::mem::take(inner_p.codec), mem::take(inner_p.codec),
std::mem::take(inner_p.read_buf), mem::take(inner_p.read_buf),
); );
parts.write_buf = std::mem::take(inner_p.write_buf); parts.write_buf = mem::take(inner_p.write_buf);
let framed = Framed::from_parts(parts); let framed = Framed::from_parts(parts);
let upgrade = let upgrade =
inner_p.upgrade.take().unwrap().call((req, framed)); inner_p.upgrade.take().unwrap().call((req, framed));
@ -791,8 +846,11 @@ where
} }
// we didn't get WouldBlock from write operation, // we didn't get WouldBlock from write operation,
// so data get written to kernel completely (OSX) // so data get written to kernel completely (macOS)
// and we have to write again otherwise response can get stuck // and we have to write again otherwise response can get stuck
//
// TODO: what? is WouldBlock good or bad?
// want to find a reference for this macOS behavior
if inner.as_mut().poll_flush(cx)? || !drain { if inner.as_mut().poll_flush(cx)? || !drain {
break; break;
} }
@ -842,6 +900,11 @@ where
} }
} }
/// Returns either:
/// - `Ok(Some(true))` - data was read and done reading all data.
/// - `Ok(Some(false))` - data was read but there should be more to read.
/// - `Ok(None)` - no data was read but there should be more to read later.
/// - Unhandled Errors
fn read_available<T>( fn read_available<T>(
cx: &mut Context<'_>, cx: &mut Context<'_>,
io: &mut T, io: &mut T,
@ -875,17 +938,17 @@ where
read_some = true; read_some = true;
} }
} }
Poll::Ready(Err(e)) => { Poll::Ready(Err(err)) => {
return if e.kind() == io::ErrorKind::WouldBlock { return if err.kind() == io::ErrorKind::WouldBlock {
if read_some { if read_some {
Ok(Some(false)) Ok(Some(false))
} else { } else {
Ok(None) Ok(None)
} }
} else if e.kind() == io::ErrorKind::ConnectionReset && read_some { } else if err.kind() == io::ErrorKind::ConnectionReset && read_some {
Ok(Some(true)) Ok(Some(true))
} else { } else {
Err(e) Err(err)
} }
} }
} }
@ -905,43 +968,376 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_service::IntoService; use std::{marker::PhantomData, str};
use futures_util::future::{lazy, ok};
use actix_service::fn_service;
use futures_util::future::{lazy, ready};
use super::*; use super::*;
use crate::error::Error;
use crate::h1::{ExpectHandler, UpgradeHandler};
use crate::test::TestBuffer; use crate::test::TestBuffer;
use crate::{error::Error, KeepAlive};
use crate::{
h1::{ExpectHandler, UpgradeHandler},
test::TestSeqBuffer,
};
fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
haystack[from..]
.windows(needle.len())
.position(|window| window == needle)
}
fn stabilize_date_header(payload: &mut [u8]) {
let mut from = 0;
while let Some(pos) = find_slice(&payload, b"date", from) {
payload[(from + pos)..(from + pos + 35)]
.copy_from_slice(b"date: Thu, 01 Jan 1970 12:34:56 UTC");
from += 35;
}
}
fn ok_service() -> impl Service<Request = Request, Response = Response, Error = Error>
{
fn_service(|_req: Request| ready(Ok::<_, Error>(Response::Ok().finish())))
}
fn echo_path_service(
) -> impl Service<Request = Request, Response = Response, Error = Error> {
fn_service(|req: Request| {
let path = req.path().as_bytes();
ready(Ok::<_, Error>(Response::Ok().body(Body::from_slice(path))))
})
}
fn echo_payload_service(
) -> impl Service<Request = Request, Response = Response, Error = Error> {
fn_service(|mut req: Request| {
Box::pin(async move {
use futures_util::stream::StreamExt as _;
let mut pl = req.take_payload();
let mut body = BytesMut::new();
while let Some(chunk) = pl.next().await {
body.extend_from_slice(chunk.unwrap().bytes())
}
Ok::<_, Error>(Response::Ok().body(body))
})
})
}
#[actix_rt::test] #[actix_rt::test]
async fn test_req_parse_err() { async fn test_req_parse_err() {
lazy(|cx| { lazy(|cx| {
let buf = TestBuffer::new("GET /test HTTP/1\r\n\r\n"); let buf = TestBuffer::new("GET /test HTTP/1\r\n\r\n");
let mut h1 = Dispatcher::<_, _, _, _, UpgradeHandler<TestBuffer>>::new( let h1 = Dispatcher::<_, _, _, _, UpgradeHandler<TestBuffer>>::new(
buf, buf,
ServiceConfig::default(), ServiceConfig::default(),
CloneableService::new( CloneableService::new(ok_service()),
(|_| ok::<_, Error>(Response::Ok().finish())).into_service(),
),
CloneableService::new(ExpectHandler), CloneableService::new(ExpectHandler),
None, None,
None, None,
Extensions::new(),
None, None,
); );
match Pin::new(&mut h1).poll(cx) {
futures_util::pin_mut!(h1);
match h1.as_mut().poll(cx) {
Poll::Pending => panic!(), Poll::Pending => panic!(),
Poll::Ready(res) => assert!(res.is_err()), Poll::Ready(res) => assert!(res.is_err()),
} }
if let DispatcherState::Normal(ref mut inner) = h1.inner { if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() {
assert!(inner.flags.contains(Flags::READ_DISCONNECT)); assert!(inner.flags.contains(Flags::READ_DISCONNECT));
assert_eq!( assert_eq!(
&inner.io.take().unwrap().write_buf[..26], &inner.project().io.take().unwrap().write_buf[..26],
b"HTTP/1.1 400 Bad Request\r\n" b"HTTP/1.1 400 Bad Request\r\n"
); );
} }
}) })
.await; .await;
} }
#[actix_rt::test]
async fn test_pipelining() {
lazy(|cx| {
let buf = TestBuffer::new(
"\
GET /abcd HTTP/1.1\r\n\r\n\
GET /def HTTP/1.1\r\n\r\n\
",
);
let cfg = ServiceConfig::new(KeepAlive::Disabled, 1, 1, false, None);
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler<TestBuffer>>::new(
buf,
cfg,
CloneableService::new(echo_path_service()),
CloneableService::new(ExpectHandler),
None,
None,
Extensions::new(),
None,
);
futures_util::pin_mut!(h1);
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
match h1.as_mut().poll(cx) {
Poll::Pending => panic!("first poll should not be pending"),
Poll::Ready(res) => assert!(res.is_ok()),
}
// polls: initial => shutdown
assert_eq!(h1.poll_count, 2);
if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() {
let res = &mut inner.project().io.take().unwrap().write_buf[..];
stabilize_date_header(res);
let exp = b"\
HTTP/1.1 200 OK\r\n\
content-length: 5\r\n\
connection: close\r\n\
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
/abcd\
HTTP/1.1 200 OK\r\n\
content-length: 4\r\n\
connection: close\r\n\
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
/def\
";
assert_eq!(res.to_vec(), exp.to_vec());
}
})
.await;
lazy(|cx| {
let buf = TestBuffer::new(
"\
GET /abcd HTTP/1.1\r\n\r\n\
GET /def HTTP/1\r\n\r\n\
",
);
let cfg = ServiceConfig::new(KeepAlive::Disabled, 1, 1, false, None);
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler<TestBuffer>>::new(
buf,
cfg,
CloneableService::new(echo_path_service()),
CloneableService::new(ExpectHandler),
None,
None,
Extensions::new(),
None,
);
futures_util::pin_mut!(h1);
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
match h1.as_mut().poll(cx) {
Poll::Pending => panic!("first poll should not be pending"),
Poll::Ready(res) => assert!(res.is_err()),
}
// polls: initial => shutdown
assert_eq!(h1.poll_count, 1);
if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() {
let res = &mut inner.project().io.take().unwrap().write_buf[..];
stabilize_date_header(res);
let exp = b"\
HTTP/1.1 200 OK\r\n\
content-length: 5\r\n\
connection: close\r\n\
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
/abcd\
HTTP/1.1 400 Bad Request\r\n\
content-length: 0\r\n\
connection: close\r\n\
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\r\n\
";
assert_eq!(res.to_vec(), exp.to_vec());
}
})
.await;
}
#[actix_rt::test]
async fn test_expect() {
lazy(|cx| {
let mut buf = TestSeqBuffer::empty();
let cfg = ServiceConfig::new(KeepAlive::Disabled, 0, 0, false, None);
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler<_>>::new(
buf.clone(),
cfg,
CloneableService::new(echo_payload_service()),
CloneableService::new(ExpectHandler),
None,
None,
Extensions::new(),
None,
);
buf.extend_read_buf(
"\
POST /upload HTTP/1.1\r\n\
Content-Length: 5\r\n\
Expect: 100-continue\r\n\
\r\n\
",
);
futures_util::pin_mut!(h1);
assert!(h1.as_mut().poll(cx).is_pending());
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
// polls: manual
assert_eq!(h1.poll_count, 1);
eprintln!("poll count: {}", h1.poll_count);
if let DispatcherState::Normal(ref inner) = h1.inner {
let io = inner.io.as_ref().unwrap();
let res = &io.write_buf()[..];
assert_eq!(
str::from_utf8(res).unwrap(),
"HTTP/1.1 100 Continue\r\n\r\n"
);
}
buf.extend_read_buf("12345");
assert!(h1.as_mut().poll(cx).is_ready());
// polls: manual manual shutdown
assert_eq!(h1.poll_count, 3);
if let DispatcherState::Normal(ref inner) = h1.inner {
let io = inner.io.as_ref().unwrap();
let mut res = (&io.write_buf()[..]).to_owned();
stabilize_date_header(&mut res);
assert_eq!(
str::from_utf8(&res).unwrap(),
"\
HTTP/1.1 100 Continue\r\n\
\r\n\
HTTP/1.1 200 OK\r\n\
content-length: 5\r\n\
connection: close\r\n\
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\
\r\n\
12345\
"
);
}
})
.await;
}
#[actix_rt::test]
async fn test_eager_expect() {
lazy(|cx| {
let mut buf = TestSeqBuffer::empty();
let cfg = ServiceConfig::new(KeepAlive::Disabled, 0, 0, false, None);
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler<_>>::new(
buf.clone(),
cfg,
CloneableService::new(echo_path_service()),
CloneableService::new(ExpectHandler),
None,
None,
Extensions::new(),
None,
);
buf.extend_read_buf(
"\
POST /upload HTTP/1.1\r\n\
Content-Length: 5\r\n\
Expect: 100-continue\r\n\
\r\n\
",
);
futures_util::pin_mut!(h1);
assert!(h1.as_mut().poll(cx).is_ready());
assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
// polls: manual shutdown
assert_eq!(h1.poll_count, 2);
if let DispatcherState::Normal(ref inner) = h1.inner {
let io = inner.io.as_ref().unwrap();
let mut res = (&io.write_buf()[..]).to_owned();
stabilize_date_header(&mut res);
// Despite the content-length header and even though the request payload has not
// been sent, this test expects a complete service response since the payload
// is not used at all. The service passed to dispatcher is path echo and doesn't
// consume payload bytes.
assert_eq!(
str::from_utf8(&res).unwrap(),
"\
HTTP/1.1 100 Continue\r\n\
\r\n\
HTTP/1.1 200 OK\r\n\
content-length: 7\r\n\
connection: close\r\n\
date: Thu, 01 Jan 1970 12:34:56 UTC\r\n\
\r\n\
/upload\
"
);
}
})
.await;
}
#[actix_rt::test]
async fn test_upgrade() {
lazy(|cx| {
let mut buf = TestSeqBuffer::empty();
let cfg = ServiceConfig::new(KeepAlive::Disabled, 0, 0, false, None);
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler<_>>::new(
buf.clone(),
cfg,
CloneableService::new(ok_service()),
CloneableService::new(ExpectHandler),
Some(CloneableService::new(UpgradeHandler(PhantomData))),
None,
Extensions::new(),
None,
);
buf.extend_read_buf(
"\
GET /ws HTTP/1.1\r\n\
Connection: Upgrade\r\n\
Upgrade: websocket\r\n\
\r\n\
",
);
futures_util::pin_mut!(h1);
assert!(h1.as_mut().poll(cx).is_ready());
assert!(matches!(&h1.inner, DispatcherState::Upgrade(_)));
// polls: manual shutdown
assert_eq!(h1.poll_count, 2);
})
.await;
}
} }

View File

@ -64,14 +64,17 @@ pub(crate) trait MessageType: Sized {
// Content length // Content length
if let Some(status) = self.status() { if let Some(status) = self.status() {
match status { match status {
StatusCode::NO_CONTENT StatusCode::CONTINUE
| StatusCode::CONTINUE | StatusCode::SWITCHING_PROTOCOLS
| StatusCode::PROCESSING => length = BodySize::None, | StatusCode::PROCESSING
StatusCode::SWITCHING_PROTOCOLS => { | StatusCode::NO_CONTENT => {
// skip content-length and transfer-encoding headers
// See https://tools.ietf.org/html/rfc7230#section-3.3.1
// and https://tools.ietf.org/html/rfc7230#section-3.3.2
skip_len = true; skip_len = true;
length = BodySize::Stream; length = BodySize::None
} }
_ => (), _ => {}
} }
} }
match length { match length {
@ -676,4 +679,28 @@ mod tests {
assert!(data.contains("authorization: another authorization\r\n")); assert!(data.contains("authorization: another authorization\r\n"));
assert!(data.contains("date: date\r\n")); assert!(data.contains("date: date\r\n"));
} }
#[test]
fn test_no_content_length() {
let mut bytes = BytesMut::with_capacity(2048);
let mut res: Response<()> =
Response::new(StatusCode::SWITCHING_PROTOCOLS).into_body::<()>();
res.headers_mut()
.insert(DATE, HeaderValue::from_static(&""));
res.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static(&"0"));
let _ = res.encode_headers(
&mut bytes,
Version::HTTP_11,
BodySize::Stream,
ConnectionType::Upgrade,
&ServiceConfig::default(),
);
let data =
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
assert!(!data.contains("content-length: 0\r\n"));
assert!(!data.contains("transfer-encoding: chunked\r\n"));
}
} }

View File

@ -1,7 +1,7 @@
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use actix_service::{Service, ServiceFactory}; use actix_service::{Service, ServiceFactory};
use futures_util::future::{ok, Ready}; use futures_util::future::{ready, Ready};
use crate::error::Error; use crate::error::Error;
use crate::request::Request; use crate::request::Request;
@ -17,8 +17,8 @@ impl ServiceFactory for ExpectHandler {
type InitError = Error; type InitError = Error;
type Future = Ready<Result<Self::Service, Self::InitError>>; type Future = Ready<Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: Self::Config) -> Self::Future {
ok(ExpectHandler) ready(Ok(ExpectHandler))
} }
} }
@ -33,6 +33,8 @@ impl Service for ExpectHandler {
} }
fn call(&mut self, req: Request) -> Self::Future { fn call(&mut self, req: Request) -> Self::Future {
ok(req) ready(Ok(req))
// TODO: add some way to trigger error
// Err(error::ErrorExpectationFailed("test"))
} }
} }

View File

@ -182,9 +182,7 @@ impl Inner {
self.len += data.len(); self.len += data.len();
self.items.push_back(data); self.items.push_back(data);
self.need_read = self.len < MAX_BUFFER_SIZE; self.need_read = self.len < MAX_BUFFER_SIZE;
if let Some(task) = self.task.take() { self.task.wake();
task.wake()
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -18,6 +18,7 @@ use crate::error::{DispatchError, Error, ParseError};
use crate::helpers::DataFactory; use crate::helpers::DataFactory;
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
use crate::{ConnectCallback, Extensions};
use super::codec::Codec; use super::codec::Codec;
use super::dispatcher::Dispatcher; use super::dispatcher::Dispatcher;
@ -30,6 +31,7 @@ pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler<T>> {
expect: X, expect: X,
upgrade: Option<U>, upgrade: Option<U>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B)>, _t: PhantomData<(T, B)>,
} }
@ -52,6 +54,7 @@ where
expect: ExpectHandler, expect: ExpectHandler,
upgrade: None, upgrade: None,
on_connect: None, on_connect: None,
on_connect_ext: None,
_t: PhantomData, _t: PhantomData,
} }
} }
@ -213,6 +216,7 @@ where
srv: self.srv, srv: self.srv,
upgrade: self.upgrade, upgrade: self.upgrade,
on_connect: self.on_connect, on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData, _t: PhantomData,
} }
} }
@ -229,6 +233,7 @@ where
srv: self.srv, srv: self.srv,
expect: self.expect, expect: self.expect,
on_connect: self.on_connect, on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData, _t: PhantomData,
} }
} }
@ -241,6 +246,12 @@ where
self.on_connect = f; self.on_connect = f;
self self
} }
/// Set on connect callback.
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
self.on_connect_ext = f;
self
}
} }
impl<T, S, B, X, U> ServiceFactory for H1Service<T, S, B, X, U> impl<T, S, B, X, U> ServiceFactory for H1Service<T, S, B, X, U>
@ -274,6 +285,7 @@ where
expect: None, expect: None,
upgrade: None, upgrade: None,
on_connect: self.on_connect.clone(), on_connect: self.on_connect.clone(),
on_connect_ext: self.on_connect_ext.clone(),
cfg: Some(self.cfg.clone()), cfg: Some(self.cfg.clone()),
_t: PhantomData, _t: PhantomData,
} }
@ -303,6 +315,7 @@ where
expect: Option<X::Service>, expect: Option<X::Service>,
upgrade: Option<U::Service>, upgrade: Option<U::Service>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: Option<ServiceConfig>, cfg: Option<ServiceConfig>,
_t: PhantomData<(T, B)>, _t: PhantomData<(T, B)>,
} }
@ -352,23 +365,26 @@ where
Poll::Ready(result.map(|service| { Poll::Ready(result.map(|service| {
let this = self.as_mut().project(); let this = self.as_mut().project();
H1ServiceHandler::new( H1ServiceHandler::new(
this.cfg.take().unwrap(), this.cfg.take().unwrap(),
service, service,
this.expect.take().unwrap(), this.expect.take().unwrap(),
this.upgrade.take(), this.upgrade.take(),
this.on_connect.clone(), this.on_connect.clone(),
this.on_connect_ext.clone(),
) )
})) }))
} }
} }
/// `Service` implementation for HTTP1 transport /// `Service` implementation for HTTP/1 transport
pub struct H1ServiceHandler<T, S: Service, B, X: Service, U: Service> { pub struct H1ServiceHandler<T, S: Service, B, X: Service, U: Service> {
srv: CloneableService<S>, srv: CloneableService<S>,
expect: CloneableService<X>, expect: CloneableService<X>,
upgrade: Option<CloneableService<U>>, upgrade: Option<CloneableService<U>>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: ServiceConfig, cfg: ServiceConfig,
_t: PhantomData<(T, B)>, _t: PhantomData<(T, B)>,
} }
@ -390,6 +406,7 @@ where
expect: X, expect: X,
upgrade: Option<U>, upgrade: Option<U>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
) -> H1ServiceHandler<T, S, B, X, U> { ) -> H1ServiceHandler<T, S, B, X, U> {
H1ServiceHandler { H1ServiceHandler {
srv: CloneableService::new(srv), srv: CloneableService::new(srv),
@ -397,6 +414,7 @@ where
upgrade: upgrade.map(CloneableService::new), upgrade: upgrade.map(CloneableService::new),
cfg, cfg,
on_connect, on_connect,
on_connect_ext,
_t: PhantomData, _t: PhantomData,
} }
} }
@ -462,11 +480,13 @@ where
} }
fn call(&mut self, (io, addr): Self::Request) -> Self::Future { fn call(&mut self, (io, addr): Self::Request) -> Self::Future {
let on_connect = if let Some(ref on_connect) = self.on_connect { let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
Some(on_connect(&io))
} else { let mut connect_extensions = Extensions::new();
None if let Some(ref handler) = self.on_connect_ext {
}; // run on_connect_ext callback, populating connect extensions
handler(&io, &mut connect_extensions);
}
Dispatcher::new( Dispatcher::new(
io, io,
@ -474,7 +494,8 @@ where
self.srv.clone(), self.srv.clone(),
self.expect.clone(), self.expect.clone(),
self.upgrade.clone(), self.upgrade.clone(),
on_connect, deprecated_on_connect,
connect_extensions,
addr, addr,
) )
} }

View File

@ -3,13 +3,13 @@ use std::task::{Context, Poll};
use actix_codec::Framed; use actix_codec::Framed;
use actix_service::{Service, ServiceFactory}; use actix_service::{Service, ServiceFactory};
use futures_util::future::Ready; use futures_util::future::{ready, Ready};
use crate::error::Error; use crate::error::Error;
use crate::h1::Codec; use crate::h1::Codec;
use crate::request::Request; use crate::request::Request;
pub struct UpgradeHandler<T>(PhantomData<T>); pub struct UpgradeHandler<T>(pub(crate) PhantomData<T>);
impl<T> ServiceFactory for UpgradeHandler<T> { impl<T> ServiceFactory for UpgradeHandler<T> {
type Config = (); type Config = ();
@ -36,6 +36,6 @@ impl<T> Service for UpgradeHandler<T> {
} }
fn call(&mut self, _: Self::Request) -> Self::Future { fn call(&mut self, _: Self::Request) -> Self::Future {
unimplemented!() ready(Ok(()))
} }
} }

View File

@ -24,6 +24,7 @@ use crate::message::ResponseHead;
use crate::payload::Payload; use crate::payload::Payload;
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
use crate::Extensions;
const CHUNK_SIZE: usize = 16_384; const CHUNK_SIZE: usize = 16_384;
@ -36,6 +37,7 @@ where
service: CloneableService<S>, service: CloneableService<S>,
connection: Connection<T, Bytes>, connection: Connection<T, Bytes>,
on_connect: Option<Box<dyn DataFactory>>, on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
config: ServiceConfig, config: ServiceConfig,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
ka_expire: Instant, ka_expire: Instant,
@ -56,6 +58,7 @@ where
service: CloneableService<S>, service: CloneableService<S>,
connection: Connection<T, Bytes>, connection: Connection<T, Bytes>,
on_connect: Option<Box<dyn DataFactory>>, on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
config: ServiceConfig, config: ServiceConfig,
timeout: Option<Delay>, timeout: Option<Delay>,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
@ -82,6 +85,7 @@ where
peer_addr, peer_addr,
connection, connection,
on_connect, on_connect,
on_connect_data,
ka_expire, ka_expire,
ka_timer, ka_timer,
_t: PhantomData, _t: PhantomData,
@ -130,11 +134,15 @@ where
head.headers = parts.headers.into(); head.headers = parts.headers.into();
head.peer_addr = this.peer_addr; head.peer_addr = this.peer_addr;
// DEPRECATED
// set on_connect data // set on_connect data
if let Some(ref on_connect) = this.on_connect { if let Some(ref on_connect) = this.on_connect {
on_connect.set(&mut req.extensions_mut()); on_connect.set(&mut req.extensions_mut());
} }
// merge on_connect_ext data into request extensions
req.extensions_mut().drain_from(&mut this.on_connect_data);
actix_rt::spawn(ServiceResponse::< actix_rt::spawn(ServiceResponse::<
S::Future, S::Future,
S::Response, S::Response,

View File

@ -2,7 +2,7 @@ use std::future::Future;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::{net, rc}; use std::{net, rc::Rc};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
@ -23,6 +23,7 @@ use crate::error::{DispatchError, Error};
use crate::helpers::DataFactory; use crate::helpers::DataFactory;
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
use crate::{ConnectCallback, Extensions};
use super::dispatcher::Dispatcher; use super::dispatcher::Dispatcher;
@ -30,7 +31,8 @@ use super::dispatcher::Dispatcher;
pub struct H2Service<T, S, B> { pub struct H2Service<T, S, B> {
srv: S, srv: S,
cfg: ServiceConfig, cfg: ServiceConfig,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B)>, _t: PhantomData<(T, B)>,
} }
@ -50,19 +52,27 @@ where
H2Service { H2Service {
cfg, cfg,
on_connect: None, on_connect: None,
on_connect_ext: None,
srv: service.into_factory(), srv: service.into_factory(),
_t: PhantomData, _t: PhantomData,
} }
} }
/// Set on connect callback. /// Set on connect callback.
pub(crate) fn on_connect( pub(crate) fn on_connect(
mut self, mut self,
f: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, f: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
) -> Self { ) -> Self {
self.on_connect = f; self.on_connect = f;
self self
} }
/// Set on connect callback.
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
self.on_connect_ext = f;
self
}
} }
impl<S, B> H2Service<TcpStream, S, B> impl<S, B> H2Service<TcpStream, S, B>
@ -203,6 +213,7 @@ where
fut: self.srv.new_service(()), fut: self.srv.new_service(()),
cfg: Some(self.cfg.clone()), cfg: Some(self.cfg.clone()),
on_connect: self.on_connect.clone(), on_connect: self.on_connect.clone(),
on_connect_ext: self.on_connect_ext.clone(),
_t: PhantomData, _t: PhantomData,
} }
} }
@ -214,7 +225,8 @@ pub struct H2ServiceResponse<T, S: ServiceFactory, B> {
#[pin] #[pin]
fut: S::Future, fut: S::Future,
cfg: Option<ServiceConfig>, cfg: Option<ServiceConfig>,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B)>, _t: PhantomData<(T, B)>,
} }
@ -237,6 +249,7 @@ where
H2ServiceHandler::new( H2ServiceHandler::new(
this.cfg.take().unwrap(), this.cfg.take().unwrap(),
this.on_connect.clone(), this.on_connect.clone(),
this.on_connect_ext.clone(),
service, service,
) )
})) }))
@ -247,7 +260,8 @@ where
pub struct H2ServiceHandler<T, S: Service, B> { pub struct H2ServiceHandler<T, S: Service, B> {
srv: CloneableService<S>, srv: CloneableService<S>,
cfg: ServiceConfig, cfg: ServiceConfig,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B)>, _t: PhantomData<(T, B)>,
} }
@ -261,12 +275,14 @@ where
{ {
fn new( fn new(
cfg: ServiceConfig, cfg: ServiceConfig,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
srv: S, srv: S,
) -> H2ServiceHandler<T, S, B> { ) -> H2ServiceHandler<T, S, B> {
H2ServiceHandler { H2ServiceHandler {
cfg, cfg,
on_connect, on_connect,
on_connect_ext,
srv: CloneableService::new(srv), srv: CloneableService::new(srv),
_t: PhantomData, _t: PhantomData,
} }
@ -296,18 +312,21 @@ where
} }
fn call(&mut self, (io, addr): Self::Request) -> Self::Future { fn call(&mut self, (io, addr): Self::Request) -> Self::Future {
let on_connect = if let Some(ref on_connect) = self.on_connect { let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
Some(on_connect(&io))
} else { let mut connect_extensions = Extensions::new();
None if let Some(ref handler) = self.on_connect_ext {
}; // run on_connect_ext callback, populating connect extensions
handler(&io, &mut connect_extensions);
}
H2ServiceHandlerResponse { H2ServiceHandlerResponse {
state: State::Handshake( state: State::Handshake(
Some(self.srv.clone()), Some(self.srv.clone()),
Some(self.cfg.clone()), Some(self.cfg.clone()),
addr, addr,
on_connect, deprecated_on_connect,
Some(connect_extensions),
server::handshake(io), server::handshake(io),
), ),
} }
@ -325,6 +344,7 @@ where
Option<ServiceConfig>, Option<ServiceConfig>,
Option<net::SocketAddr>, Option<net::SocketAddr>,
Option<Box<dyn DataFactory>>, Option<Box<dyn DataFactory>>,
Option<Extensions>,
Handshake<T, Bytes>, Handshake<T, Bytes>,
), ),
} }
@ -360,6 +380,7 @@ where
ref mut config, ref mut config,
ref peer_addr, ref peer_addr,
ref mut on_connect, ref mut on_connect,
ref mut on_connect_data,
ref mut handshake, ref mut handshake,
) => match Pin::new(handshake).poll(cx) { ) => match Pin::new(handshake).poll(cx) {
Poll::Ready(Ok(conn)) => { Poll::Ready(Ok(conn)) => {
@ -367,6 +388,7 @@ where
srv.take().unwrap(), srv.take().unwrap(),
conn, conn,
on_connect.take(), on_connect.take(),
on_connect_data.take().unwrap(),
config.take().unwrap(), config.take().unwrap(),
None, None,
*peer_addr, *peer_addr,

View File

@ -1,3 +1,5 @@
use std::cmp::Ordering;
use mime::Mime; use mime::Mime;
use crate::header::{qitem, QualityItem}; use crate::header::{qitem, QualityItem};
@ -7,7 +9,7 @@ header! {
/// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2) /// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2)
/// ///
/// The `Accept` header field can be used by user agents to specify /// The `Accept` header field can be used by user agents to specify
/// response media types that are acceptable. Accept header fields can /// response media types that are acceptable. Accept header fields can
/// be used to indicate that the request is specifically limited to a /// be used to indicate that the request is specifically limited to a
/// small set of desired types, as in the case of a request for an /// small set of desired types, as in the case of a request for an
/// in-line image /// in-line image
@ -97,14 +99,14 @@ header! {
test_header!( test_header!(
test1, test1,
vec![b"audio/*; q=0.2, audio/basic"], vec![b"audio/*; q=0.2, audio/basic"],
Some(HeaderField(vec![ Some(Accept(vec![
QualityItem::new("audio/*".parse().unwrap(), q(200)), QualityItem::new("audio/*".parse().unwrap(), q(200)),
qitem("audio/basic".parse().unwrap()), qitem("audio/basic".parse().unwrap()),
]))); ])));
test_header!( test_header!(
test2, test2,
vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"],
Some(HeaderField(vec![ Some(Accept(vec![
QualityItem::new(mime::TEXT_PLAIN, q(500)), QualityItem::new(mime::TEXT_PLAIN, q(500)),
qitem(mime::TEXT_HTML), qitem(mime::TEXT_HTML),
QualityItem::new( QualityItem::new(
@ -138,23 +140,148 @@ header! {
} }
impl Accept { impl Accept {
/// A constructor to easily create `Accept: */*`. /// Construct `Accept: */*`.
pub fn star() -> Accept { pub fn star() -> Accept {
Accept(vec![qitem(mime::STAR_STAR)]) Accept(vec![qitem(mime::STAR_STAR)])
} }
/// A constructor to easily create `Accept: application/json`. /// Construct `Accept: application/json`.
pub fn json() -> Accept { pub fn json() -> Accept {
Accept(vec![qitem(mime::APPLICATION_JSON)]) Accept(vec![qitem(mime::APPLICATION_JSON)])
} }
/// A constructor to easily create `Accept: text/*`. /// Construct `Accept: text/*`.
pub fn text() -> Accept { pub fn text() -> Accept {
Accept(vec![qitem(mime::TEXT_STAR)]) Accept(vec![qitem(mime::TEXT_STAR)])
} }
/// A constructor to easily create `Accept: image/*`. /// Construct `Accept: image/*`.
pub fn image() -> Accept { pub fn image() -> Accept {
Accept(vec![qitem(mime::IMAGE_STAR)]) Accept(vec![qitem(mime::IMAGE_STAR)])
} }
/// Construct `Accept: text/html`.
pub fn html() -> Accept {
Accept(vec![qitem(mime::TEXT_HTML)])
}
/// Returns a sorted list of mime types from highest to lowest preference, accounting for
/// [q-factor weighting] and specificity.
///
/// [q-factor weighting]: https://tools.ietf.org/html/rfc7231#section-5.3.2
pub fn mime_precedence(&self) -> Vec<Mime> {
let mut types = self.0.clone();
// use stable sort so items with equal q-factor and specificity retain listed order
types.sort_by(|a, b| {
// sort by q-factor descending
b.quality.cmp(&a.quality).then_with(|| {
// use specificity rules on mime types with
// same q-factor (eg. text/html > text/* > */*)
// subtypes are not comparable if main type is star, so return
match (a.item.type_(), b.item.type_()) {
(mime::STAR, mime::STAR) => return Ordering::Equal,
// a is sorted after b
(mime::STAR, _) => return Ordering::Greater,
// a is sorted before b
(_, mime::STAR) => return Ordering::Less,
_ => {}
}
// in both these match expressions, the returned ordering appears
// inverted because sort is high-to-low ("descending") precedence
match (a.item.subtype(), b.item.subtype()) {
(mime::STAR, mime::STAR) => Ordering::Equal,
// a is sorted after b
(mime::STAR, _) => Ordering::Greater,
// a is sorted before b
(_, mime::STAR) => Ordering::Less,
_ => Ordering::Equal,
}
})
});
types.into_iter().map(|qitem| qitem.item).collect()
}
/// Extracts the most preferable mime type, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first mime type is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// Returns `None` if contained list is empty.
///
/// [q-factor weighting]: https://tools.ietf.org/html/rfc7231#section-5.3.2
pub fn mime_preference(&self) -> Option<Mime> {
let types = self.mime_precedence();
types.first().cloned()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::header::q;
#[test]
fn test_mime_precedence() {
let test = Accept(vec![]);
assert!(test.mime_precedence().is_empty());
let test = Accept(vec![qitem(mime::APPLICATION_JSON)]);
assert_eq!(test.mime_precedence(), vec!(mime::APPLICATION_JSON));
let test = Accept(vec![
qitem(mime::TEXT_HTML),
"application/xhtml+xml".parse().unwrap(),
QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
QualityItem::new(mime::STAR_STAR, q(0.8)),
]);
assert_eq!(
test.mime_precedence(),
vec![
mime::TEXT_HTML,
"application/xhtml+xml".parse().unwrap(),
"application/xml".parse().unwrap(),
mime::STAR_STAR,
]
);
let test = Accept(vec![
qitem(mime::STAR_STAR),
qitem(mime::IMAGE_STAR),
qitem(mime::IMAGE_PNG),
]);
assert_eq!(
test.mime_precedence(),
vec![mime::IMAGE_PNG, mime::IMAGE_STAR, mime::STAR_STAR]
);
}
#[test]
fn test_mime_preference() {
let test = Accept(vec![
qitem(mime::TEXT_HTML),
"application/xhtml+xml".parse().unwrap(),
QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
QualityItem::new(mime::STAR_STAR, q(0.8)),
]);
assert_eq!(test.mime_preference(), Some(mime::TEXT_HTML));
let test = Accept(vec![
QualityItem::new("video/*".parse().unwrap(), q(0.8)),
qitem(mime::IMAGE_PNG),
QualityItem::new(mime::STAR_STAR, q(0.5)),
qitem(mime::IMAGE_SVG),
QualityItem::new(mime::IMAGE_STAR, q(0.8)),
]);
assert_eq!(test.mime_preference(), Some(mime::IMAGE_PNG));
}
} }

View File

@ -550,8 +550,7 @@ impl fmt::Display for ContentDisposition {
write!(f, "{}", self.disposition)?; write!(f, "{}", self.disposition)?;
self.parameters self.parameters
.iter() .iter()
.map(|param| write!(f, "; {}", param)) .try_for_each(|param| write!(f, "; {}", param))
.collect()
} }
} }

View File

@ -3,7 +3,7 @@
//! ## Mime //! ## Mime
//! //!
//! Several header fields use MIME values for their contents. Keeping with the //! Several header fields use MIME values for their contents. Keeping with the
//! strongly-typed theme, the [mime](https://docs.rs/mime) crate //! strongly-typed theme, the [mime] crate
//! is used, such as `ContentType(pub Mime)`. //! is used, such as `ContentType(pub Mime)`.
#![cfg_attr(rustfmt, rustfmt_skip)] #![cfg_attr(rustfmt, rustfmt_skip)]

View File

@ -8,8 +8,6 @@ use http::header::{HeaderName, HeaderValue};
/// A set of HTTP headers /// A set of HTTP headers
/// ///
/// `HeaderMap` is an multi-map of [`HeaderName`] to values. /// `HeaderMap` is an multi-map of [`HeaderName`] to values.
///
/// [`HeaderName`]: struct.HeaderName.html
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct HeaderMap { pub struct HeaderMap {
pub(crate) inner: FxHashMap<HeaderName, Value>, pub(crate) inner: FxHashMap<HeaderName, Value>,
@ -141,8 +139,6 @@ impl HeaderMap {
/// The returned view does not incur any allocations and allows iterating /// The returned view does not incur any allocations and allows iterating
/// the values associated with the key. See [`GetAll`] for more details. /// the values associated with the key. See [`GetAll`] for more details.
/// Returns `None` if there are no values associated with the key. /// Returns `None` if there are no values associated with the key.
///
/// [`GetAll`]: struct.GetAll.html
pub fn get_all<N: AsName>(&self, name: N) -> GetAll<'_> { pub fn get_all<N: AsName>(&self, name: N) -> GetAll<'_> {
GetAll { GetAll {
idx: 0, idx: 0,

View File

@ -370,9 +370,7 @@ impl fmt::Display for ExtendedValue {
} }
/// Percent encode a sequence of bytes with a character set defined in /// Percent encode a sequence of bytes with a character set defined in
/// [https://tools.ietf.org/html/rfc5987#section-3.2][url] /// <https://tools.ietf.org/html/rfc5987#section-3.2>
///
/// [url]: https://tools.ietf.org/html/rfc5987#section-3.2
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result { pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE); let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
fmt::Display::fmt(&encoded, f) fmt::Display::fmt(&encoded, f)

View File

@ -7,9 +7,7 @@ use self::Charset::*;
/// ///
/// The string representation is normalized to upper case. /// The string representation is normalized to upper case.
/// ///
/// See [http://www.iana.org/assignments/character-sets/character-sets.xhtml][url]. /// See <http://www.iana.org/assignments/character-sets/character-sets.xhtml>.
///
/// [url]: http://www.iana.org/assignments/character-sets/character-sets.xhtml
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum Charset { pub enum Charset {

View File

@ -7,10 +7,12 @@ use crate::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer};
/// 1. `%x21`, or /// 1. `%x21`, or
/// 2. in the range `%x23` to `%x7E`, or /// 2. in the range `%x23` to `%x7E`, or
/// 3. above `%x80` /// 3. above `%x80`
fn entity_validate_char(c: u8) -> bool {
c == 0x21 || (0x23..=0x7e).contains(&c) || (c >= 0x80)
}
fn check_slice_validity(slice: &str) -> bool { fn check_slice_validity(slice: &str) -> bool {
slice slice.bytes().all(entity_validate_char)
.bytes()
.all(|c| c == b'\x21' || (c >= b'\x23' && c <= b'\x7e') | (c >= b'\x80'))
} }
/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3) /// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3)

View File

@ -1,10 +1,17 @@
use std::{cmp, fmt, str}; use std::{
cmp,
convert::{TryFrom, TryInto},
fmt, str,
};
use self::internal::IntoQuality; use derive_more::{Display, Error};
const MAX_QUALITY: u16 = 1000;
const MAX_FLOAT_QUALITY: f32 = 1.0;
/// Represents a quality used in quality values. /// Represents a quality used in quality values.
/// ///
/// Can be created with the `q` function. /// Can be created with the [`q`] function.
/// ///
/// # Implementation notes /// # Implementation notes
/// ///
@ -18,12 +25,54 @@ use self::internal::IntoQuality;
/// ///
/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1) /// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1)
/// gives more information on quality values in HTTP header fields. /// gives more information on quality values in HTTP header fields.
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Quality(u16); pub struct Quality(u16);
impl Quality {
/// # Panics
/// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
fn from_f32(value: f32) -> Self {
// Check that `value` is within range should be done before calling this method.
// Just in case, this debug_assert should catch if we were forgetful.
debug_assert!(
(0.0f32..=1.0f32).contains(&value),
"q value must be between 0.0 and 1.0"
);
Quality((value * MAX_QUALITY as f32) as u16)
}
}
impl Default for Quality { impl Default for Quality {
fn default() -> Quality { fn default() -> Quality {
Quality(1000) Quality(MAX_QUALITY)
}
}
#[derive(Debug, Clone, Display, Error)]
pub struct QualityOutOfBounds;
impl TryFrom<u16> for Quality {
type Error = QualityOutOfBounds;
fn try_from(value: u16) -> Result<Self, Self::Error> {
if (0..=MAX_QUALITY).contains(&value) {
Ok(Quality(value))
} else {
Err(QualityOutOfBounds)
}
}
}
impl TryFrom<f32> for Quality {
type Error = QualityOutOfBounds;
fn try_from(value: f32) -> Result<Self, Self::Error> {
if (0.0..=MAX_FLOAT_QUALITY).contains(&value) {
Ok(Quality::from_f32(value))
} else {
Err(QualityOutOfBounds)
}
} }
} }
@ -55,8 +104,9 @@ impl<T: PartialEq> cmp::PartialOrd for QualityItem<T> {
impl<T: fmt::Display> fmt::Display for QualityItem<T> { impl<T: fmt::Display> fmt::Display for QualityItem<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.item, f)?; fmt::Display::fmt(&self.item, f)?;
match self.quality.0 { match self.quality.0 {
1000 => Ok(()), MAX_QUALITY => Ok(()),
0 => f.write_str("; q=0"), 0 => f.write_str("; q=0"),
x => write!(f, "; q=0.{}", format!("{:03}", x).trim_end_matches('0')), x => write!(f, "; q=0.{}", format!("{:03}", x).trim_end_matches('0')),
} }
@ -66,105 +116,79 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
impl<T: str::FromStr> str::FromStr for QualityItem<T> { impl<T: str::FromStr> str::FromStr for QualityItem<T> {
type Err = crate::error::ParseError; type Err = crate::error::ParseError;
fn from_str(s: &str) -> Result<QualityItem<T>, crate::error::ParseError> { fn from_str(qitem_str: &str) -> Result<QualityItem<T>, crate::error::ParseError> {
if !s.is_ascii() { if !qitem_str.is_ascii() {
return Err(crate::error::ParseError::Header); return Err(crate::error::ParseError::Header);
} }
// Set defaults used if parsing fails. // Set defaults used if parsing fails.
let mut raw_item = s; let mut raw_item = qitem_str;
let mut quality = 1f32; let mut quality = 1f32;
let parts: Vec<&str> = s.rsplitn(2, ';').map(|x| x.trim()).collect(); let parts: Vec<_> = qitem_str.rsplitn(2, ';').map(str::trim).collect();
if parts.len() == 2 { if parts.len() == 2 {
// example for item with q-factor:
//
// gzip; q=0.65
// ^^^^^^ parts[0]
// ^^ start
// ^^^^ q_val
// ^^^^ parts[1]
if parts[0].len() < 2 { if parts[0].len() < 2 {
// Can't possibly be an attribute since an attribute needs at least a name followed
// by an equals sign. And bare identifiers are forbidden.
return Err(crate::error::ParseError::Header); return Err(crate::error::ParseError::Header);
} }
let start = &parts[0][0..2]; let start = &parts[0][0..2];
if start == "q=" || start == "Q=" { if start == "q=" || start == "Q=" {
let q_part = &parts[0][2..parts[0].len()]; let q_val = &parts[0][2..];
if q_part.len() > 5 { if q_val.len() > 5 {
// longer than 5 indicates an over-precise q-factor
return Err(crate::error::ParseError::Header); return Err(crate::error::ParseError::Header);
} }
match q_part.parse::<f32>() {
Ok(q_value) => { let q_value = q_val
if 0f32 <= q_value && q_value <= 1f32 { .parse::<f32>()
quality = q_value; .map_err(|_| crate::error::ParseError::Header)?;
raw_item = parts[1];
} else { if (0f32..=1f32).contains(&q_value) {
return Err(crate::error::ParseError::Header); quality = q_value;
} raw_item = parts[1];
} } else {
Err(_) => return Err(crate::error::ParseError::Header), return Err(crate::error::ParseError::Header);
} }
} }
} }
match raw_item.parse::<T>() {
// we already checked above that the quality is within range
Ok(item) => Ok(QualityItem::new(item, from_f32(quality))),
Err(_) => Err(crate::error::ParseError::Header),
}
}
}
#[inline] let item = raw_item
fn from_f32(f: f32) -> Quality { .parse::<T>()
// this function is only used internally. A check that `f` is within range .map_err(|_| crate::error::ParseError::Header)?;
// should be done before calling this method. Just in case, this
// debug_assert should catch if we were forgetful // we already checked above that the quality is within range
debug_assert!( Ok(QualityItem::new(item, Quality::from_f32(quality)))
f >= 0f32 && f <= 1f32, }
"q value must be between 0.0 and 1.0"
);
Quality((f * 1000f32) as u16)
} }
/// Convenience function to wrap a value in a `QualityItem` /// Convenience function to wrap a value in a `QualityItem`
/// Sets `q` to the default 1.0 /// Sets `q` to the default 1.0
pub fn qitem<T>(item: T) -> QualityItem<T> { pub fn qitem<T>(item: T) -> QualityItem<T> {
QualityItem::new(item, Default::default()) QualityItem::new(item, Quality::default())
} }
/// Convenience function to create a `Quality` from a float or integer. /// Convenience function to create a `Quality` from a float or integer.
/// ///
/// Implemented for `u16` and `f32`. Panics if value is out of range. /// Implemented for `u16` and `f32`. Panics if value is out of range.
pub fn q<T: IntoQuality>(val: T) -> Quality { pub fn q<T>(val: T) -> Quality
val.into_quality() where
} T: TryInto<Quality>,
T::Error: fmt::Debug,
mod internal { {
use super::Quality; // TODO: on next breaking change, handle unwrap differently
val.try_into().unwrap()
// TryFrom is probably better, but it's not stable. For now, we want to
// keep the functionality of the `q` function, while allowing it to be
// generic over `f32` and `u16`.
//
// `q` would panic before, so keep that behavior. `TryFrom` can be
// introduced later for a non-panicking conversion.
pub trait IntoQuality: Sealed + Sized {
fn into_quality(self) -> Quality;
}
impl IntoQuality for f32 {
fn into_quality(self) -> Quality {
assert!(
self >= 0f32 && self <= 1f32,
"float must be between 0.0 and 1.0"
);
super::from_f32(self)
}
}
impl IntoQuality for u16 {
fn into_quality(self) -> Quality {
assert!(self <= 1000, "u16 must be between 0 and 1000");
Quality(self)
}
}
pub trait Sealed {}
impl Sealed for u16 {}
impl Sealed for f32 {}
} }
#[cfg(test)] #[cfg(test)]
@ -270,15 +294,13 @@ mod tests {
} }
#[test] #[test]
#[should_panic] // FIXME - 32-bit msvc unwinding broken #[should_panic]
#[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)]
fn test_quality_invalid() { fn test_quality_invalid() {
q(-1.0); q(-1.0);
} }
#[test] #[test]
#[should_panic] // FIXME - 32-bit msvc unwinding broken #[should_panic]
#[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)]
fn test_quality_invalid2() { fn test_quality_invalid2() {
q(2.0); q(2.0);
} }

View File

@ -50,6 +50,7 @@ impl<'a> io::Write for Writer<'a> {
self.0.extend_from_slice(buf); self.0.extend_from_slice(buf);
Ok(buf.len()) Ok(buf.len())
} }
fn flush(&mut self) -> io::Result<()> { fn flush(&mut self) -> io::Result<()> {
Ok(()) Ok(())
} }

View File

@ -1,10 +1,12 @@
//! Basic http responses //! Status code based HTTP response builders.
#![allow(non_upper_case_globals)] #![allow(non_upper_case_globals)]
use http::StatusCode; use http::StatusCode;
use crate::response::{Response, ResponseBuilder}; use crate::response::{Response, ResponseBuilder};
macro_rules! STATIC_RESP { macro_rules! static_resp {
($name:ident, $status:expr) => { ($name:ident, $status:expr) => {
#[allow(non_snake_case, missing_docs)] #[allow(non_snake_case, missing_docs)]
pub fn $name() -> ResponseBuilder { pub fn $name() -> ResponseBuilder {
@ -14,63 +16,67 @@ macro_rules! STATIC_RESP {
} }
impl Response { impl Response {
STATIC_RESP!(Ok, StatusCode::OK); static_resp!(Continue, StatusCode::CONTINUE);
STATIC_RESP!(Created, StatusCode::CREATED); static_resp!(SwitchingProtocols, StatusCode::SWITCHING_PROTOCOLS);
STATIC_RESP!(Accepted, StatusCode::ACCEPTED); static_resp!(Processing, StatusCode::PROCESSING);
STATIC_RESP!(
static_resp!(Ok, StatusCode::OK);
static_resp!(Created, StatusCode::CREATED);
static_resp!(Accepted, StatusCode::ACCEPTED);
static_resp!(
NonAuthoritativeInformation, NonAuthoritativeInformation,
StatusCode::NON_AUTHORITATIVE_INFORMATION StatusCode::NON_AUTHORITATIVE_INFORMATION
); );
STATIC_RESP!(NoContent, StatusCode::NO_CONTENT); static_resp!(NoContent, StatusCode::NO_CONTENT);
STATIC_RESP!(ResetContent, StatusCode::RESET_CONTENT); static_resp!(ResetContent, StatusCode::RESET_CONTENT);
STATIC_RESP!(PartialContent, StatusCode::PARTIAL_CONTENT); static_resp!(PartialContent, StatusCode::PARTIAL_CONTENT);
STATIC_RESP!(MultiStatus, StatusCode::MULTI_STATUS); static_resp!(MultiStatus, StatusCode::MULTI_STATUS);
STATIC_RESP!(AlreadyReported, StatusCode::ALREADY_REPORTED); static_resp!(AlreadyReported, StatusCode::ALREADY_REPORTED);
STATIC_RESP!(MultipleChoices, StatusCode::MULTIPLE_CHOICES); static_resp!(MultipleChoices, StatusCode::MULTIPLE_CHOICES);
STATIC_RESP!(MovedPermanently, StatusCode::MOVED_PERMANENTLY); static_resp!(MovedPermanently, StatusCode::MOVED_PERMANENTLY);
STATIC_RESP!(Found, StatusCode::FOUND); static_resp!(Found, StatusCode::FOUND);
STATIC_RESP!(SeeOther, StatusCode::SEE_OTHER); static_resp!(SeeOther, StatusCode::SEE_OTHER);
STATIC_RESP!(NotModified, StatusCode::NOT_MODIFIED); static_resp!(NotModified, StatusCode::NOT_MODIFIED);
STATIC_RESP!(UseProxy, StatusCode::USE_PROXY); static_resp!(UseProxy, StatusCode::USE_PROXY);
STATIC_RESP!(TemporaryRedirect, StatusCode::TEMPORARY_REDIRECT); static_resp!(TemporaryRedirect, StatusCode::TEMPORARY_REDIRECT);
STATIC_RESP!(PermanentRedirect, StatusCode::PERMANENT_REDIRECT); static_resp!(PermanentRedirect, StatusCode::PERMANENT_REDIRECT);
STATIC_RESP!(BadRequest, StatusCode::BAD_REQUEST); static_resp!(BadRequest, StatusCode::BAD_REQUEST);
STATIC_RESP!(NotFound, StatusCode::NOT_FOUND); static_resp!(NotFound, StatusCode::NOT_FOUND);
STATIC_RESP!(Unauthorized, StatusCode::UNAUTHORIZED); static_resp!(Unauthorized, StatusCode::UNAUTHORIZED);
STATIC_RESP!(PaymentRequired, StatusCode::PAYMENT_REQUIRED); static_resp!(PaymentRequired, StatusCode::PAYMENT_REQUIRED);
STATIC_RESP!(Forbidden, StatusCode::FORBIDDEN); static_resp!(Forbidden, StatusCode::FORBIDDEN);
STATIC_RESP!(MethodNotAllowed, StatusCode::METHOD_NOT_ALLOWED); static_resp!(MethodNotAllowed, StatusCode::METHOD_NOT_ALLOWED);
STATIC_RESP!(NotAcceptable, StatusCode::NOT_ACCEPTABLE); static_resp!(NotAcceptable, StatusCode::NOT_ACCEPTABLE);
STATIC_RESP!( static_resp!(
ProxyAuthenticationRequired, ProxyAuthenticationRequired,
StatusCode::PROXY_AUTHENTICATION_REQUIRED StatusCode::PROXY_AUTHENTICATION_REQUIRED
); );
STATIC_RESP!(RequestTimeout, StatusCode::REQUEST_TIMEOUT); static_resp!(RequestTimeout, StatusCode::REQUEST_TIMEOUT);
STATIC_RESP!(Conflict, StatusCode::CONFLICT); static_resp!(Conflict, StatusCode::CONFLICT);
STATIC_RESP!(Gone, StatusCode::GONE); static_resp!(Gone, StatusCode::GONE);
STATIC_RESP!(LengthRequired, StatusCode::LENGTH_REQUIRED); static_resp!(LengthRequired, StatusCode::LENGTH_REQUIRED);
STATIC_RESP!(PreconditionFailed, StatusCode::PRECONDITION_FAILED); static_resp!(PreconditionFailed, StatusCode::PRECONDITION_FAILED);
STATIC_RESP!(PreconditionRequired, StatusCode::PRECONDITION_REQUIRED); static_resp!(PreconditionRequired, StatusCode::PRECONDITION_REQUIRED);
STATIC_RESP!(PayloadTooLarge, StatusCode::PAYLOAD_TOO_LARGE); static_resp!(PayloadTooLarge, StatusCode::PAYLOAD_TOO_LARGE);
STATIC_RESP!(UriTooLong, StatusCode::URI_TOO_LONG); static_resp!(UriTooLong, StatusCode::URI_TOO_LONG);
STATIC_RESP!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE); static_resp!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE);
STATIC_RESP!(RangeNotSatisfiable, StatusCode::RANGE_NOT_SATISFIABLE); static_resp!(RangeNotSatisfiable, StatusCode::RANGE_NOT_SATISFIABLE);
STATIC_RESP!(ExpectationFailed, StatusCode::EXPECTATION_FAILED); static_resp!(ExpectationFailed, StatusCode::EXPECTATION_FAILED);
STATIC_RESP!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY); static_resp!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY);
STATIC_RESP!(TooManyRequests, StatusCode::TOO_MANY_REQUESTS); static_resp!(TooManyRequests, StatusCode::TOO_MANY_REQUESTS);
STATIC_RESP!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR); static_resp!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR);
STATIC_RESP!(NotImplemented, StatusCode::NOT_IMPLEMENTED); static_resp!(NotImplemented, StatusCode::NOT_IMPLEMENTED);
STATIC_RESP!(BadGateway, StatusCode::BAD_GATEWAY); static_resp!(BadGateway, StatusCode::BAD_GATEWAY);
STATIC_RESP!(ServiceUnavailable, StatusCode::SERVICE_UNAVAILABLE); static_resp!(ServiceUnavailable, StatusCode::SERVICE_UNAVAILABLE);
STATIC_RESP!(GatewayTimeout, StatusCode::GATEWAY_TIMEOUT); static_resp!(GatewayTimeout, StatusCode::GATEWAY_TIMEOUT);
STATIC_RESP!(VersionNotSupported, StatusCode::HTTP_VERSION_NOT_SUPPORTED); static_resp!(VersionNotSupported, StatusCode::HTTP_VERSION_NOT_SUPPORTED);
STATIC_RESP!(VariantAlsoNegotiates, StatusCode::VARIANT_ALSO_NEGOTIATES); static_resp!(VariantAlsoNegotiates, StatusCode::VARIANT_ALSO_NEGOTIATES);
STATIC_RESP!(InsufficientStorage, StatusCode::INSUFFICIENT_STORAGE); static_resp!(InsufficientStorage, StatusCode::INSUFFICIENT_STORAGE);
STATIC_RESP!(LoopDetected, StatusCode::LOOP_DETECTED); static_resp!(LoopDetected, StatusCode::LOOP_DETECTED);
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,4 +1,4 @@
//! Basic http primitives for actix-net framework. //! HTTP primitives for the Actix ecosystem.
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
#![allow( #![allow(
@ -7,6 +7,9 @@
clippy::new_without_default, clippy::new_without_default,
clippy::borrow_interior_mutable_const clippy::borrow_interior_mutable_const
)] )]
#![allow(clippy::manual_strip)] // Allow this to keep MSRV(1.42).
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#[macro_use] #[macro_use]
extern crate log; extern crate log;
@ -77,3 +80,5 @@ pub enum Protocol {
Http1, Http1,
Http2, Http2,
} }
type ConnectCallback<IO> = dyn Fn(&IO, &mut Extensions);

View File

@ -554,8 +554,9 @@ impl ResponseBuilder {
self self
} }
/// This method calls provided closure with builder reference if value is /// This method calls provided closure with builder reference if value is `true`.
/// true. #[doc(hidden)]
#[deprecated = "Use an if statement."]
pub fn if_true<F>(&mut self, value: bool, f: F) -> &mut Self pub fn if_true<F>(&mut self, value: bool, f: F) -> &mut Self
where where
F: FnOnce(&mut ResponseBuilder), F: FnOnce(&mut ResponseBuilder),
@ -566,8 +567,9 @@ impl ResponseBuilder {
self self
} }
/// This method calls provided closure with builder reference if value is /// This method calls provided closure with builder reference if value is `Some`.
/// Some. #[doc(hidden)]
#[deprecated = "Use an if-let construction."]
pub fn if_some<T, F>(&mut self, value: Option<T>, f: F) -> &mut Self pub fn if_some<T, F>(&mut self, value: Option<T>, f: F) -> &mut Self
where where
F: FnOnce(T, &mut ResponseBuilder), F: FnOnce(T, &mut ResponseBuilder),

View File

@ -1,7 +1,7 @@
use std::marker::PhantomData; use std::marker::PhantomData;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::{fmt, net, rc}; use std::{fmt, net, rc::Rc};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
@ -20,15 +20,17 @@ use crate::error::{DispatchError, Error};
use crate::helpers::DataFactory; use crate::helpers::DataFactory;
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
use crate::{h1, h2::Dispatcher, Protocol}; use crate::{h1, h2::Dispatcher, ConnectCallback, Extensions, Protocol};
/// `ServiceFactory` HTTP1.1/HTTP2 transport implementation /// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol.
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler<T>> { pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler<T>> {
srv: S, srv: S,
cfg: ServiceConfig, cfg: ServiceConfig,
expect: X, expect: X,
upgrade: Option<U>, upgrade: Option<U>,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, // DEPRECATED: in favor of on_connect_ext
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B)>, _t: PhantomData<(T, B)>,
} }
@ -66,6 +68,7 @@ where
expect: h1::ExpectHandler, expect: h1::ExpectHandler,
upgrade: None, upgrade: None,
on_connect: None, on_connect: None,
on_connect_ext: None,
_t: PhantomData, _t: PhantomData,
} }
} }
@ -81,6 +84,7 @@ where
expect: h1::ExpectHandler, expect: h1::ExpectHandler,
upgrade: None, upgrade: None,
on_connect: None, on_connect: None,
on_connect_ext: None,
_t: PhantomData, _t: PhantomData,
} }
} }
@ -113,6 +117,7 @@ where
srv: self.srv, srv: self.srv,
upgrade: self.upgrade, upgrade: self.upgrade,
on_connect: self.on_connect, on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData, _t: PhantomData,
} }
} }
@ -138,6 +143,7 @@ where
srv: self.srv, srv: self.srv,
expect: self.expect, expect: self.expect,
on_connect: self.on_connect, on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData, _t: PhantomData,
} }
} }
@ -145,11 +151,17 @@ where
/// Set on connect callback. /// Set on connect callback.
pub(crate) fn on_connect( pub(crate) fn on_connect(
mut self, mut self,
f: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, f: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
) -> Self { ) -> Self {
self.on_connect = f; self.on_connect = f;
self self
} }
/// Set connect callback with mutable access to request data container.
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
self.on_connect_ext = f;
self
}
} }
impl<S, B, X, U> HttpService<TcpStream, S, B, X, U> impl<S, B, X, U> HttpService<TcpStream, S, B, X, U>
@ -355,6 +367,7 @@ where
expect: None, expect: None,
upgrade: None, upgrade: None,
on_connect: self.on_connect.clone(), on_connect: self.on_connect.clone(),
on_connect_ext: self.on_connect_ext.clone(),
cfg: self.cfg.clone(), cfg: self.cfg.clone(),
_t: PhantomData, _t: PhantomData,
} }
@ -378,7 +391,8 @@ pub struct HttpServiceResponse<
fut_upg: Option<U::Future>, fut_upg: Option<U::Future>,
expect: Option<X::Service>, expect: Option<X::Service>,
upgrade: Option<U::Service>, upgrade: Option<U::Service>,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: ServiceConfig, cfg: ServiceConfig,
_t: PhantomData<(T, B)>, _t: PhantomData<(T, B)>,
} }
@ -429,6 +443,7 @@ where
.fut .fut
.poll(cx) .poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e))); .map_err(|e| log::error!("Init http service error: {:?}", e)));
Poll::Ready(result.map(|service| { Poll::Ready(result.map(|service| {
let this = self.as_mut().project(); let this = self.as_mut().project();
HttpServiceHandler::new( HttpServiceHandler::new(
@ -437,6 +452,7 @@ where
this.expect.take().unwrap(), this.expect.take().unwrap(),
this.upgrade.take(), this.upgrade.take(),
this.on_connect.clone(), this.on_connect.clone(),
this.on_connect_ext.clone(),
) )
})) }))
} }
@ -448,7 +464,8 @@ pub struct HttpServiceHandler<T, S: Service, B, X: Service, U: Service> {
expect: CloneableService<X>, expect: CloneableService<X>,
upgrade: Option<CloneableService<U>>, upgrade: Option<CloneableService<U>>,
cfg: ServiceConfig, cfg: ServiceConfig,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B, X)>, _t: PhantomData<(T, B, X)>,
} }
@ -469,11 +486,13 @@ where
srv: S, srv: S,
expect: X, expect: X,
upgrade: Option<U>, upgrade: Option<U>,
on_connect: Option<rc::Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>, on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
) -> HttpServiceHandler<T, S, B, X, U> { ) -> HttpServiceHandler<T, S, B, X, U> {
HttpServiceHandler { HttpServiceHandler {
cfg, cfg,
on_connect, on_connect,
on_connect_ext,
srv: CloneableService::new(srv), srv: CloneableService::new(srv),
expect: CloneableService::new(expect), expect: CloneableService::new(expect),
upgrade: upgrade.map(CloneableService::new), upgrade: upgrade.map(CloneableService::new),
@ -543,11 +562,12 @@ where
} }
fn call(&mut self, (io, proto, peer_addr): Self::Request) -> Self::Future { fn call(&mut self, (io, proto, peer_addr): Self::Request) -> Self::Future {
let on_connect = if let Some(ref on_connect) = self.on_connect { let mut connect_extensions = Extensions::new();
Some(on_connect(&io))
} else { let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
None if let Some(ref handler) = self.on_connect_ext {
}; handler(&io, &mut connect_extensions);
}
match proto { match proto {
Protocol::Http2 => HttpServiceHandlerResponse { Protocol::Http2 => HttpServiceHandlerResponse {
@ -555,10 +575,12 @@ where
server::handshake(io), server::handshake(io),
self.cfg.clone(), self.cfg.clone(),
self.srv.clone(), self.srv.clone(),
on_connect, deprecated_on_connect,
connect_extensions,
peer_addr, peer_addr,
))), ))),
}, },
Protocol::Http1 => HttpServiceHandlerResponse { Protocol::Http1 => HttpServiceHandlerResponse {
state: State::H1(h1::Dispatcher::new( state: State::H1(h1::Dispatcher::new(
io, io,
@ -566,7 +588,8 @@ where
self.srv.clone(), self.srv.clone(),
self.expect.clone(), self.expect.clone(),
self.upgrade.clone(), self.upgrade.clone(),
on_connect, deprecated_on_connect,
connect_extensions,
peer_addr, peer_addr,
)), )),
}, },
@ -595,6 +618,7 @@ where
ServiceConfig, ServiceConfig,
CloneableService<S>, CloneableService<S>,
Option<Box<dyn DataFactory>>, Option<Box<dyn DataFactory>>,
Extensions,
Option<net::SocketAddr>, Option<net::SocketAddr>,
)>, )>,
), ),
@ -670,9 +694,16 @@ where
} else { } else {
panic!() panic!()
}; };
let (_, cfg, srv, on_connect, peer_addr) = data.take().unwrap(); let (_, cfg, srv, on_connect, on_connect_data, peer_addr) =
data.take().unwrap();
self.set(State::H2(Dispatcher::new( self.set(State::H2(Dispatcher::new(
srv, conn, on_connect, cfg, None, peer_addr, srv,
conn,
on_connect,
on_connect_data,
cfg,
None,
peer_addr,
))); )));
self.poll(cx) self.poll(cx)
} }

View File

@ -1,9 +1,14 @@
//! Test Various helpers for Actix applications to use during testing. //! Various testing helpers for use in internal and app tests.
use std::convert::TryFrom;
use std::io::{self, Read, Write}; use std::{
use std::pin::Pin; cell::{Ref, RefCell},
use std::str::FromStr; convert::TryFrom,
use std::task::{Context, Poll}; io::{self, Read, Write},
pin::Pin,
rc::Rc,
str::FromStr,
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
@ -183,7 +188,7 @@ fn parts(parts: &mut Option<Inner>) -> &mut Inner {
parts.as_mut().expect("cannot reuse test request builder") parts.as_mut().expect("cannot reuse test request builder")
} }
/// Async io buffer /// Async I/O test buffer.
pub struct TestBuffer { pub struct TestBuffer {
pub read_buf: BytesMut, pub read_buf: BytesMut,
pub write_buf: BytesMut, pub write_buf: BytesMut,
@ -191,24 +196,24 @@ pub struct TestBuffer {
} }
impl TestBuffer { impl TestBuffer {
/// Create new TestBuffer instance /// Create new `TestBuffer` instance with initial read buffer.
pub fn new<T>(data: T) -> TestBuffer pub fn new<T>(data: T) -> Self
where where
BytesMut: From<T>, T: Into<BytesMut>,
{ {
TestBuffer { Self {
read_buf: BytesMut::from(data), read_buf: data.into(),
write_buf: BytesMut::new(), write_buf: BytesMut::new(),
err: None, err: None,
} }
} }
/// Create new empty TestBuffer instance /// Create new empty `TestBuffer` instance.
pub fn empty() -> TestBuffer { pub fn empty() -> Self {
TestBuffer::new("") Self::new("")
} }
/// Add extra data to read buffer. /// Add data to read buffer.
pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) { pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) {
self.read_buf.extend_from_slice(data.as_ref()) self.read_buf.extend_from_slice(data.as_ref())
} }
@ -236,6 +241,7 @@ impl io::Write for TestBuffer {
self.write_buf.extend(buf); self.write_buf.extend(buf);
Ok(buf.len()) Ok(buf.len())
} }
fn flush(&mut self) -> io::Result<()> { fn flush(&mut self) -> io::Result<()> {
Ok(()) Ok(())
} }
@ -268,3 +274,113 @@ impl AsyncWrite for TestBuffer {
Poll::Ready(Ok(())) Poll::Ready(Ok(()))
} }
} }
/// Async I/O test buffer with ability to incrementally add to the read buffer.
#[derive(Clone)]
pub struct TestSeqBuffer(Rc<RefCell<TestSeqInner>>);
impl TestSeqBuffer {
/// Create new `TestBuffer` instance with initial read buffer.
pub fn new<T>(data: T) -> Self
where
T: Into<BytesMut>,
{
Self(Rc::new(RefCell::new(TestSeqInner {
read_buf: data.into(),
write_buf: BytesMut::new(),
err: None,
})))
}
/// Create new empty `TestBuffer` instance.
pub fn empty() -> Self {
Self::new("")
}
pub fn read_buf(&self) -> Ref<'_, BytesMut> {
Ref::map(self.0.borrow(), |inner| &inner.read_buf)
}
pub fn write_buf(&self) -> Ref<'_, BytesMut> {
Ref::map(self.0.borrow(), |inner| &inner.write_buf)
}
pub fn err(&self) -> Ref<'_, Option<io::Error>> {
Ref::map(self.0.borrow(), |inner| &inner.err)
}
/// Add data to read buffer.
pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) {
self.0
.borrow_mut()
.read_buf
.extend_from_slice(data.as_ref())
}
}
pub struct TestSeqInner {
read_buf: BytesMut,
write_buf: BytesMut,
err: Option<io::Error>,
}
impl io::Read for TestSeqBuffer {
fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> {
if self.0.borrow().read_buf.is_empty() {
if self.0.borrow().err.is_some() {
Err(self.0.borrow_mut().err.take().unwrap())
} else {
Err(io::Error::new(io::ErrorKind::WouldBlock, ""))
}
} else {
let size = std::cmp::min(self.0.borrow().read_buf.len(), dst.len());
let b = self.0.borrow_mut().read_buf.split_to(size);
dst[..size].copy_from_slice(&b);
Ok(size)
}
}
}
impl io::Write for TestSeqBuffer {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.borrow_mut().write_buf.extend(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl AsyncRead for TestSeqBuffer {
fn poll_read(
self: Pin<&mut Self>,
_: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
let r = self.get_mut().read(buf);
match r {
Ok(n) => Poll::Ready(Ok(n)),
Err(err) if err.kind() == io::ErrorKind::WouldBlock => Poll::Pending,
Err(err) => Poll::Ready(Err(err)),
}
}
}
impl AsyncWrite for TestSeqBuffer {
fn poll_write(
self: Pin<&mut Self>,
_: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Poll::Ready(self.get_mut().write(buf))
}
fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
}

View File

@ -4,7 +4,9 @@ use std::ptr::copy_nonoverlapping;
use std::slice; use std::slice;
// Holds a slice guaranteed to be shorter than 8 bytes // Holds a slice guaranteed to be shorter than 8 bytes
struct ShortSlice<'a>(&'a mut [u8]); struct ShortSlice<'a> {
inner: &'a mut [u8],
}
impl<'a> ShortSlice<'a> { impl<'a> ShortSlice<'a> {
/// # Safety /// # Safety
@ -12,10 +14,11 @@ impl<'a> ShortSlice<'a> {
unsafe fn new(slice: &'a mut [u8]) -> Self { unsafe fn new(slice: &'a mut [u8]) -> Self {
// Sanity check for debug builds // Sanity check for debug builds
debug_assert!(slice.len() < 8); debug_assert!(slice.len() < 8);
ShortSlice(slice) ShortSlice { inner: slice }
} }
fn len(&self) -> usize { fn len(&self) -> usize {
self.0.len() self.inner.len()
} }
} }
@ -56,7 +59,7 @@ pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) {
fn xor_short(buf: ShortSlice<'_>, mask: u64) { fn xor_short(buf: ShortSlice<'_>, mask: u64) {
// SAFETY: we know that a `ShortSlice` fits in a u64 // SAFETY: we know that a `ShortSlice` fits in a u64
unsafe { unsafe {
let (ptr, len) = (buf.0.as_mut_ptr(), buf.0.len()); let (ptr, len) = (buf.inner.as_mut_ptr(), buf.len());
let mut b: u64 = 0; let mut b: u64 = 0;
#[allow(trivial_casts)] #[allow(trivial_casts)]
copy_nonoverlapping(ptr, &mut b as *mut _ as *mut u8, len); copy_nonoverlapping(ptr, &mut b as *mut _ as *mut u8, len);
@ -96,7 +99,13 @@ fn align_buf(buf: &mut [u8]) -> (ShortSlice<'_>, &mut [u64], ShortSlice<'_>) {
// SAFETY: 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 // sections are smaller than 8 bytes
unsafe { (ShortSlice::new(head), cast_slice(mid), ShortSlice(tail)) } unsafe {
(
ShortSlice::new(head),
cast_slice(mid),
ShortSlice::new(tail),
)
}
} else { } else {
// We didn't cross even one aligned boundary! // We didn't cross even one aligned boundary!

View File

@ -197,13 +197,13 @@ mod tests {
let req = TestRequest::default().method(Method::POST).finish(); let req = TestRequest::default().method(Method::POST).finish();
assert_eq!( assert_eq!(
HandshakeError::GetMethodRequired, HandshakeError::GetMethodRequired,
verify_handshake(req.head()).err().unwrap() verify_handshake(req.head()).unwrap_err(),
); );
let req = TestRequest::default().finish(); let req = TestRequest::default().finish();
assert_eq!( assert_eq!(
HandshakeError::NoWebsocketUpgrade, HandshakeError::NoWebsocketUpgrade,
verify_handshake(req.head()).err().unwrap() verify_handshake(req.head()).unwrap_err(),
); );
let req = TestRequest::default() let req = TestRequest::default()
@ -211,7 +211,7 @@ mod tests {
.finish(); .finish();
assert_eq!( assert_eq!(
HandshakeError::NoWebsocketUpgrade, HandshakeError::NoWebsocketUpgrade,
verify_handshake(req.head()).err().unwrap() verify_handshake(req.head()).unwrap_err(),
); );
let req = TestRequest::default() let req = TestRequest::default()
@ -222,7 +222,7 @@ mod tests {
.finish(); .finish();
assert_eq!( assert_eq!(
HandshakeError::NoConnectionUpgrade, HandshakeError::NoConnectionUpgrade,
verify_handshake(req.head()).err().unwrap() verify_handshake(req.head()).unwrap_err(),
); );
let req = TestRequest::default() let req = TestRequest::default()
@ -237,7 +237,7 @@ mod tests {
.finish(); .finish();
assert_eq!( assert_eq!(
HandshakeError::NoVersionHeader, HandshakeError::NoVersionHeader,
verify_handshake(req.head()).err().unwrap() verify_handshake(req.head()).unwrap_err(),
); );
let req = TestRequest::default() let req = TestRequest::default()
@ -256,7 +256,7 @@ mod tests {
.finish(); .finish();
assert_eq!( assert_eq!(
HandshakeError::UnsupportedVersion, HandshakeError::UnsupportedVersion,
verify_handshake(req.head()).err().unwrap() verify_handshake(req.head()).unwrap_err(),
); );
let req = TestRequest::default() let req = TestRequest::default()
@ -275,7 +275,7 @@ mod tests {
.finish(); .finish();
assert_eq!( assert_eq!(
HandshakeError::BadWebsocketKey, HandshakeError::BadWebsocketKey,
verify_handshake(req.head()).err().unwrap() verify_handshake(req.head()).unwrap_err(),
); );
let req = TestRequest::default() let req = TestRequest::default()

View File

@ -411,8 +411,10 @@ async fn test_h2_on_connect() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.on_connect(|_| 10usize) .on_connect(|_| 10usize)
.on_connect_ext(|_, data| data.insert(20isize))
.h2(|req: Request| { .h2(|req: Request| {
assert!(req.extensions().contains::<usize>()); assert!(req.extensions().contains::<usize>());
assert!(req.extensions().contains::<isize>());
ok::<_, ()>(Response::Ok().finish()) ok::<_, ()>(Response::Ok().finish())
}) })
.openssl(ssl_acceptor()) .openssl(ssl_acceptor())

View File

@ -663,8 +663,10 @@ async fn test_h1_on_connect() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.on_connect(|_| 10usize) .on_connect(|_| 10usize)
.on_connect_ext(|_, data| data.insert(20isize))
.h1(|req: Request| { .h1(|req: Request| {
assert!(req.extensions().contains::<usize>()); assert!(req.extensions().contains::<usize>());
assert!(req.extensions().contains::<isize>());
future::ok::<_, ()>(Response::Ok().finish()) future::ok::<_, ()>(Response::Ok().finish())
}) })
.tcp() .tcp()

View File

@ -725,9 +725,7 @@ impl Drop for Safety {
if Rc::strong_count(&self.payload) != self.level { if Rc::strong_count(&self.payload) != self.level {
self.clean.set(true); self.clean.set(true);
} }
if let Some(task) = self.task.take() { self.task.wake();
task.wake()
}
} }
} }

View File

@ -1,7 +1,7 @@
# Changes # Changes
## Unreleased - 2020-xx-xx ## Unreleased - 2020-xx-xx
* Upgrade `pin-project` to `1.0`.
## 3.0.0 - 2020-09-11 ## 3.0.0 - 2020-09-11
* No significant changes from `3.0.0-beta.2`. * No significant changes from `3.0.0-beta.2`.

View File

@ -23,7 +23,7 @@ actix-codec = "0.3.0"
bytes = "0.5.2" bytes = "0.5.2"
futures-channel = { version = "0.3.5", default-features = false } futures-channel = { version = "0.3.5", default-features = false }
futures-core = { version = "0.3.5", default-features = false } futures-core = { version = "0.3.5", default-features = false }
pin-project = "0.4.17" pin-project = "1.0.0"
[dev-dependencies] [dev-dependencies]
actix-rt = "1.1.1" actix-rt = "1.1.1"

View File

@ -164,7 +164,6 @@ pub fn handshake_with_protocols(
let mut response = HttpResponse::build(StatusCode::SWITCHING_PROTOCOLS) let mut response = HttpResponse::build(StatusCode::SWITCHING_PROTOCOLS)
.upgrade("websocket") .upgrade("websocket")
.header(header::TRANSFER_ENCODING, "chunked")
.header(header::SEC_WEBSOCKET_ACCEPT, key.as_str()) .header(header::SEC_WEBSOCKET_ACCEPT, key.as_str())
.take(); .take();
@ -664,10 +663,10 @@ mod tests {
) )
.to_http_request(); .to_http_request();
assert_eq!( let resp = handshake(&req).unwrap().finish();
StatusCode::SWITCHING_PROTOCOLS, assert_eq!(StatusCode::SWITCHING_PROTOCOLS, resp.status());
handshake(&req).unwrap().finish().status() assert_eq!(None, resp.headers().get(&header::CONTENT_LENGTH));
); assert_eq!(None, resp.headers().get(&header::TRANSFER_ENCODING));
let req = TestRequest::default() let req = TestRequest::default()
.header( .header(

View File

@ -3,7 +3,7 @@
> Helper and convenience macros for Actix Web > Helper and convenience macros for Actix Web
[![crates.io](https://meritbadge.herokuapp.com/actix-web-codegen)](https://crates.io/crates/actix-web-codegen) [![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) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg)](https://docs.rs/actix-web-codegen/0.4.0/actix_web_codegen/)
[![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html) [![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) [![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) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)

View File

@ -8,7 +8,7 @@
//! are re-exported. //! are re-exported.
//! //!
//! # Runtime Setup //! # Runtime Setup
//! Used for setting up the actix async runtime. See [main] macro docs. //! Used for setting up the actix async runtime. See [macro@main] macro docs.
//! //!
//! ```rust //! ```rust
//! #[actix_web_codegen::main] // or `#[actix_web::main]` in Actix Web apps //! #[actix_web_codegen::main] // or `#[actix_web::main]` in Actix Web apps
@ -34,7 +34,7 @@
//! //!
//! # Multiple Method Handlers //! # Multiple Method Handlers
//! Similar to the single method handler macro but takes one or more arguments for the HTTP methods //! 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. //! it should respond to. See [macro@route] macro docs.
//! //!
//! ```rust //! ```rust
//! # use actix_web::HttpResponse; //! # use actix_web::HttpResponse;
@ -46,17 +46,15 @@
//! ``` //! ```
//! //!
//! [actix-web attributes docs]: https://docs.rs/actix-web/*/actix_web/#attributes //! [actix-web attributes docs]: https://docs.rs/actix-web/*/actix_web/#attributes
//! [main]: attr.main.html //! [GET]: macro@get
//! [route]: attr.route.html //! [POST]: macro@post
//! [GET]: attr.get.html //! [PUT]: macro@put
//! [POST]: attr.post.html //! [HEAD]: macro@head
//! [PUT]: attr.put.html //! [CONNECT]: macro@macro@connect
//! [DELETE]: attr.delete.html //! [OPTIONS]: macro@options
//! [HEAD]: attr.head.html //! [TRACE]: macro@trace
//! [CONNECT]: attr.connect.html //! [PATCH]: macro@patch
//! [OPTIONS]: attr.options.html //! [DELETE]: macro@delete
//! [TRACE]: attr.trace.html
//! [PATCH]: attr.patch.html
#![recursion_limit = "512"] #![recursion_limit = "512"]

View File

@ -6,9 +6,8 @@ fn compile_macros() {
t.compile_fail("tests/trybuild/simple-fail.rs"); t.compile_fail("tests/trybuild/simple-fail.rs");
t.pass("tests/trybuild/route-ok.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_duplicate_unexpected_method(&t);
test_route_missing_method(&t) test_route_missing_method(&t)
} }
@ -25,3 +24,13 @@ fn test_route_missing_method(t: &trybuild::TestCases) {
#[rustversion::nightly] #[rustversion::nightly]
fn test_route_missing_method(_t: &trybuild::TestCases) {} fn test_route_missing_method(_t: &trybuild::TestCases) {}
// FIXME: Re-test them on nightly once rust-lang/rust#77993 is fixed.
#[rustversion::not(nightly)]
fn test_route_duplicate_unexpected_method(t: &trybuild::TestCases) {
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
}
#[rustversion::nightly]
fn test_route_duplicate_unexpected_method(_t: &trybuild::TestCases) {}

View File

@ -1,6 +1,34 @@
# Changes # Changes
## Unreleased - 2020-xx-xx ## Unreleased - 2020-xx-xx
### Changed
* Bumped `rand` to `0.8`
## 2.0.3 - 2020-11-29
### Fixed
* Ensure `actix-http` dependency uses same `serde_urlencoded`.
## 2.0.2 - 2020-11-25
### Changed
* Upgrade `serde_urlencoded` to `0.7`. [#1773]
[#1773]: https://github.com/actix/actix-web/pull/1773
## 2.0.1 - 2020-10-30
### Changed
* Upgrade `base64` to `0.13`. [#1744]
* Deprecate `ClientRequest::{if_some, if_true}`. [#1760]
### Fixed
* Use `Accept-Encoding: identity` instead of `Accept-Encoding: br` when no compression feature
is enabled [#1737]
[#1737]: https://github.com/actix/actix-web/pull/1737
[#1760]: https://github.com/actix/actix-web/pull/1760
[#1744]: https://github.com/actix/actix-web/pull/1744
## 2.0.0 - 2020-09-11 ## 2.0.0 - 2020-09-11

View File

@ -1,8 +1,8 @@
[package] [package]
name = "awc" name = "awc"
version = "2.0.0" version = "2.0.3"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Async HTTP client library that uses the Actix runtime." description = "Async HTTP and WebSocket client library built on the Actix ecosystem"
readme = "README.md" readme = "README.md"
keywords = ["actix", "http", "framework", "async", "web"] keywords = ["actix", "http", "framework", "async", "web"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -39,20 +39,21 @@ compress = ["actix-http/compress"]
[dependencies] [dependencies]
actix-codec = "0.3.0" actix-codec = "0.3.0"
actix-service = "1.0.6" actix-service = "1.0.6"
actix-http = "2.0.0" actix-http = "2.2.0"
actix-rt = "1.0.0" actix-rt = "1.0.0"
base64 = "0.12" base64 = "0.13"
bytes = "0.5.3" bytes = "0.5.3"
cfg-if = "1.0"
derive_more = "0.99.2" derive_more = "0.99.2"
futures-core = { version = "0.3.5", default-features = false } futures-core = { version = "0.3.5", default-features = false }
log =" 0.4" log =" 0.4"
mime = "0.3" mime = "0.3"
percent-encoding = "2.1" percent-encoding = "2.1"
rand = "0.7" rand = "0.8"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.6.1" serde_urlencoded = "0.7"
open-ssl = { version = "0.10", package = "openssl", optional = true } open-ssl = { version = "0.10", package = "openssl", optional = true }
rust-tls = { version = "0.18.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } rust-tls = { version = "0.18.0", package = "rustls", optional = true, features = ["dangerous_configuration"] }

View File

@ -1,33 +1,36 @@
# Actix http client [![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/awc)](https://crates.io/crates/awc) [![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) # awc (Actix Web Client)
An HTTP Client > Async HTTP and WebSocket client library.
## Documentation & community resources [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc)
[![Documentation](https://docs.rs/awc/badge.svg?version=2.0.3)](https://docs.rs/awc/2.0.3)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/awc)
[![Dependency Status](https://deps.rs/crate/awc/2.0.3/status.svg)](https://deps.rs/crate/awc/2.0.3)
[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
* [User Guide](https://actix.rs/docs/) ## Documentation & Resources
* [API Documentation](https://docs.rs/awc/)
* [Chat on gitter](https://gitter.im/actix/actix) - [API Documentation](https://docs.rs/awc)
* Cargo package: [awc](https://crates.io/crates/awc) - [Example Project](https://github.com/actix/examples/tree/HEAD/awc_https)
* Minimum supported Rust version: 1.40 or later - [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum Supported Rust Version (MSRV): 1.42.0
## Example ## Example
```rust ```rust
use actix_rt::System; use actix_rt::System;
use awc::Client; use awc::Client;
use futures::future::{Future, lazy};
fn main() { fn main() {
System::new("test").block_on(lazy(|| { System::new("test").block_on(async {
let mut client = Client::default(); let client = Client::default();
client.get("http://www.rust-lang.org") // <- Create request builder let res = client
.header("User-Agent", "Actix-web") .get("http://www.rust-lang.org") // <- Create request builder
.send() // <- Send http request .header("User-Agent", "Actix-web")
.and_then(|response| { // <- server http response .send() // <- Send http request
println!("Response: {:?}", response); .await;
Ok(())
}) println!("Response: {:?}", res); // <- server http response
})); });
} }
``` ```

View File

@ -1,11 +1,4 @@
#![deny(rust_2018_idioms)] //! `awc` is a HTTP and WebSocket client library built on the Actix ecosystem.
#![allow(
clippy::type_complexity,
clippy::borrow_interior_mutable_const,
clippy::needless_doctest_main
)]
//! `awc` is a HTTP and WebSocket client library built using the Actix ecosystem.
//! //!
//! ## Making a GET request //! ## Making a GET request
//! //!
@ -91,6 +84,15 @@
//! # } //! # }
//! ``` //! ```
#![deny(rust_2018_idioms)]
#![allow(
clippy::type_complexity,
clippy::borrow_interior_mutable_const,
clippy::needless_doctest_main
)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
use std::cell::RefCell; use std::cell::RefCell;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::rc::Rc; use std::rc::Rc;

View File

@ -21,10 +21,15 @@ use crate::frozen::FrozenClientRequest;
use crate::sender::{PrepForSendingError, RequestSender, SendClientRequest}; use crate::sender::{PrepForSendingError, RequestSender, SendClientRequest};
use crate::ClientConfig; use crate::ClientConfig;
#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] cfg_if::cfg_if! {
const HTTPS_ENCODING: &str = "br, gzip, deflate"; if #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] {
#[cfg(not(any(feature = "flate2-zlib", feature = "flate2-rust")))] const HTTPS_ENCODING: &str = "br, gzip, deflate";
const HTTPS_ENCODING: &str = "br"; } else if #[cfg(feature = "compress")] {
const HTTPS_ENCODING: &str = "br";
} else {
const HTTPS_ENCODING: &str = "identity";
}
}
/// An HTTP Client request builder /// An HTTP Client request builder
/// ///
@ -349,8 +354,9 @@ impl ClientRequest {
self self
} }
/// This method calls provided closure with builder reference if /// This method calls provided closure with builder reference if value is `true`.
/// value is `true`. #[doc(hidden)]
#[deprecated = "Use an if statement."]
pub fn if_true<F>(self, value: bool, f: F) -> Self pub fn if_true<F>(self, value: bool, f: F) -> Self
where where
F: FnOnce(ClientRequest) -> ClientRequest, F: FnOnce(ClientRequest) -> ClientRequest,
@ -362,8 +368,9 @@ impl ClientRequest {
} }
} }
/// This method calls provided closure with builder reference if /// This method calls provided closure with builder reference if value is `Some`.
/// value is `Some`. #[doc(hidden)]
#[deprecated = "Use an if-let construction."]
pub fn if_some<T, F>(self, value: Option<T>, f: F) -> Self pub fn if_some<T, F>(self, value: Option<T>, f: F) -> Self
where where
F: FnOnce(T, ClientRequest) -> ClientRequest, F: FnOnce(T, ClientRequest) -> ClientRequest,
@ -596,20 +603,27 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_basics() { async fn test_basics() {
let mut req = Client::new() let req = Client::new()
.put("/") .put("/")
.version(Version::HTTP_2) .version(Version::HTTP_2)
.set(header::Date(SystemTime::now().into())) .set(header::Date(SystemTime::now().into()))
.content_type("plain/text") .content_type("plain/text")
.if_true(true, |req| req.header(header::SERVER, "awc")) .header(header::SERVER, "awc");
.if_true(false, |req| req.header(header::EXPECT, "awc"))
.if_some(Some("server"), |val, req| { let req = if let Some(val) = Some("server") {
req.header(header::USER_AGENT, val) req.header(header::USER_AGENT, val)
}) } else {
.if_some(Option::<&str>::None, |_, req| { req
req.header(header::ALLOW, "1") };
})
.content_length(100); let req = if let Some(_val) = Option::<&str>::None {
req.header(header::ALLOW, "1")
} else {
req
};
let mut req = req.content_length(100);
assert!(req.headers().contains_key(header::CONTENT_TYPE)); assert!(req.headers().contains_key(header::CONTENT_TYPE));
assert!(req.headers().contains_key(header::DATE)); assert!(req.headers().contains_key(header::DATE));
assert!(req.headers().contains_key(header::SERVER)); assert!(req.headers().contains_key(header::SERVER));
@ -617,6 +631,7 @@ mod tests {
assert!(!req.headers().contains_key(header::ALLOW)); assert!(!req.headers().contains_key(header::ALLOW));
assert!(!req.headers().contains_key(header::EXPECT)); assert!(!req.headers().contains_key(header::EXPECT));
assert_eq!(req.head.version, Version::HTTP_2); assert_eq!(req.head.version, Version::HTTP_2);
let _ = req.headers_mut(); let _ = req.headers_mut();
let _ = req.send_body(""); let _ = req.send_body("");
} }

View File

@ -1,6 +1,6 @@
//! Websockets client //! Websockets client
//! //!
//! Type definitions required to use [`awc::Client`](../struct.Client.html) as a WebSocket client. //! Type definitions required to use [`awc::Client`](super::Client) as a WebSocket client.
//! //!
//! # Example //! # Example
//! //!
@ -70,9 +70,14 @@ impl WebsocketsRequest {
<Uri as TryFrom<U>>::Error: Into<HttpError>, <Uri as TryFrom<U>>::Error: Into<HttpError>,
{ {
let mut err = None; let mut err = None;
let mut head = RequestHead::default();
head.method = Method::GET; #[allow(clippy::field_reassign_with_default)]
head.version = Version::HTTP_11; let mut head = {
let mut head = RequestHead::default();
head.method = Method::GET;
head.version = Version::HTTP_11;
head
};
match Uri::try_from(uri) { match Uri::try_from(uri) {
Ok(uri) => head.uri = uri, Ok(uri) => head.uri = uri,

View File

@ -480,6 +480,7 @@ async fn test_client_gzip_encoding_large_random() {
let data = rand::thread_rng() let data = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric) .sample_iter(&rand::distributions::Alphanumeric)
.take(100_000) .take(100_000)
.map(char::from)
.collect::<String>(); .collect::<String>();
let srv = test::start(|| { let srv = test::start(|| {
@ -529,6 +530,7 @@ async fn test_client_brotli_encoding_large_random() {
let data = rand::thread_rng() let data = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric) .sample_iter(&rand::distributions::Alphanumeric)
.take(70_000) .take(70_000)
.map(char::from)
.collect::<String>(); .collect::<String>();
let srv = test::start(|| { let srv = test::start(|| {

View File

@ -1,4 +1,15 @@
ignore: # ignore codecoverage on following paths comment: false
coverage:
status:
project:
default:
threshold: 10% # make CI green
patch:
default:
threshold: 10% # make CI green
ignore: # ignore code coverage on following paths
- "**/tests" - "**/tests"
- "test-server" - "test-server"
- "**/benches" - "**/benches"

View File

@ -2,29 +2,31 @@ digraph {
subgraph cluster_web { subgraph cluster_web {
label="actix/actix-web" label="actix/actix-web"
"awc" "awc"
"actix-web" "web"
"actix-files" "files"
"actix-http" "http"
"actix-multipart" "multipart"
"actix-web-actors" "web-actors"
"actix-web-codegen" "codegen"
"http-test"
} }
"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" } "web" -> { "codec" "service" "utils" "router" "rt" "server" "testing" "macros" "threadpool" "tls" "codegen" "http" "awc" }
"awc" -> { "actix-codec" "actix-service" "actix-http" "actix-rt" } "awc" -> { "codec" "service" "http" "rt" }
"actix-web-actors" -> { "actix" "actix-web" "actix-http" "actix-codec" } "web-actors" -> { "actix" "web" "http" "codec" }
"actix-multipart" -> { "actix-web" "actix-service" "actix-utils" } "multipart" -> { "web" "service" "utils" }
"actix-http" -> { "actix-service" "actix-codec" "actix-connect" "actix-utils" "actix-rt" "actix-threadpool" } "http" -> { "service" "codec" "connect" "utils" "rt" "threadpool" }
"actix-http" -> { "actix" "actix-tls" }[color=blue] // optional "http" -> { "actix" "tls" }[color=blue] // optional
"actix-files" -> { "actix-web" "actix-http" } "files" -> { "web" }
"http-test" -> { "service" "codec" "connect" "utils" "rt" "server" "testing" "awc" }
// net // net
"actix-utils" -> { "actix-service" "actix-rt" "actix-codec" } "utils" -> { "service" "rt" "codec" }
"actix-tracing" -> { "actix-service" } "tracing" -> { "service" }
"actix-tls" -> { "actix-service" "actix-codec" "actix-utils" } "tls" -> { "service" "codec" "utils" }
"actix-testing" -> { "actix-rt" "actix-macros" "actix-server" "actix-service" } "testing" -> { "rt" "macros" "server" "service" }
"actix-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" } "server" -> { "service" "rt" "codec" "utils" }
"actix-rt" -> { "actix-macros" "actix-threadpool" } "rt" -> { "macros" "threadpool" }
"actix-connect" -> { "actix-service" "actix-codec" "actix-utils" "actix-rt" } "connect" -> { "service" "codec" "utils" "rt" }
} }

View File

@ -8,12 +8,14 @@ digraph {
"actix-multipart" "actix-multipart"
"actix-web-actors" "actix-web-actors"
"actix-web-codegen" "actix-web-codegen"
"actix-http-test"
} }
"actix-web" -> { "actix-web-codegen" "actix-http" "awc" } "actix-web" -> { "actix-web-codegen" "actix-http" "awc" }
"awc" -> { "actix-http" } "awc" -> { "actix-http" }
"actix-web-actors" -> { "actix" "actix-web" "actix-http" } "actix-web-actors" -> { "actix" "actix-web" "actix-http" }
"actix-multipart" -> { "actix-web" } "actix-multipart" -> { "actix-web" }
"actix-http" -> { "actix" }[color=blue] // optional "actix-http" -> { "actix" }[color=blue] // optional
"actix-files" -> { "actix-web" "actix-http" } "actix-files" -> { "actix-web" }
"actix-http-test" -> { "awc" }
} }

51
examples/on_connect.rs Normal file
View File

@ -0,0 +1,51 @@
//! This example shows how to use `actix_web::HttpServer::on_connect` to access a lower-level socket
//! properties and pass them to a handler through request-local data.
//!
//! For an example of extracting a client TLS certificate, see:
//! <https://github.com/actix/examples/tree/HEAD/rustls-client-cert>
use std::{any::Any, env, io, net::SocketAddr};
use actix_web::{dev::Extensions, rt::net::TcpStream, web, App, HttpServer};
#[derive(Debug, Clone)]
struct ConnectionInfo {
bind: SocketAddr,
peer: SocketAddr,
ttl: Option<u32>,
}
async fn route_whoami(conn_info: web::ReqData<ConnectionInfo>) -> String {
format!(
"Here is some info about your connection:\n\n{:#?}",
conn_info
)
}
fn get_conn_info(connection: &dyn Any, data: &mut Extensions) {
if let Some(sock) = connection.downcast_ref::<TcpStream>() {
data.insert(ConnectionInfo {
bind: sock.local_addr().unwrap(),
peer: sock.peer_addr().unwrap(),
ttl: sock.ttl().ok(),
});
} else {
unreachable!("connection should only be plaintext since no TLS is set up");
}
}
#[actix_web::main]
async fn main() -> io::Result<()> {
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "info");
}
env_logger::init();
HttpServer::new(|| App::new().default_service(web::to(route_whoami)))
.on_connect(get_conn_info)
.bind(("127.0.0.1", 8080))?
.workers(1)
.run()
.await
}

View File

@ -1 +0,0 @@
1.42.0

View File

@ -183,6 +183,7 @@ where
self.data.extend(cfg.data); self.data.extend(cfg.data);
self.services.extend(cfg.services); self.services.extend(cfg.services);
self.external.extend(cfg.external); self.external.extend(cfg.external);
self.extensions.extend(cfg.extensions);
self self
} }
@ -459,8 +460,8 @@ where
{ {
fn into_factory(self) -> AppInit<T, B> { fn into_factory(self) -> AppInit<T, B> {
AppInit { AppInit {
data: Rc::new(self.data), data: self.data.into_boxed_slice().into(),
data_factories: Rc::new(self.data_factories), data_factories: self.data_factories.into_boxed_slice().into(),
endpoint: self.endpoint, endpoint: self.endpoint,
services: Rc::new(RefCell::new(self.services)), services: Rc::new(RefCell::new(self.services)),
external: RefCell::new(self.external), external: RefCell::new(self.external),

View File

@ -39,8 +39,8 @@ where
{ {
pub(crate) endpoint: T, pub(crate) endpoint: T,
pub(crate) extensions: RefCell<Option<Extensions>>, pub(crate) extensions: RefCell<Option<Extensions>>,
pub(crate) data: Rc<Vec<Box<dyn DataFactory>>>, pub(crate) data: Rc<[Box<dyn DataFactory>]>,
pub(crate) data_factories: Rc<Vec<FnDataFactory>>, pub(crate) data_factories: Rc<[FnDataFactory]>,
pub(crate) services: Rc<RefCell<Vec<Box<dyn AppServiceFactory>>>>, pub(crate) services: Rc<RefCell<Vec<Box<dyn AppServiceFactory>>>>,
pub(crate) default: Option<Rc<HttpNewService>>, pub(crate) default: Option<Rc<HttpNewService>>,
pub(crate) factory_ref: Rc<RefCell<Option<AppRoutingFactory>>>, pub(crate) factory_ref: Rc<RefCell<Option<AppRoutingFactory>>>,
@ -88,15 +88,15 @@ where
// complete pipeline creation // complete pipeline creation
*self.factory_ref.borrow_mut() = Some(AppRoutingFactory { *self.factory_ref.borrow_mut() = Some(AppRoutingFactory {
default, default,
services: Rc::new( services: services
services .into_iter()
.into_iter() .map(|(mut rdef, srv, guards, nested)| {
.map(|(mut rdef, srv, guards, nested)| { rmap.add(&mut rdef, nested);
rmap.add(&mut rdef, nested); (rdef, srv, RefCell::new(guards))
(rdef, srv, RefCell::new(guards)) })
}) .collect::<Vec<_>>()
.collect(), .into_boxed_slice()
), .into(),
}); });
// external resources // external resources
@ -147,7 +147,7 @@ where
rmap: Rc<ResourceMap>, rmap: Rc<ResourceMap>,
config: AppConfig, config: AppConfig,
data: Rc<Vec<Box<dyn DataFactory>>>, data: Rc<[Box<dyn DataFactory>]>,
extensions: Option<Extensions>, extensions: Option<Extensions>,
_t: PhantomData<B>, _t: PhantomData<B>,
@ -273,7 +273,7 @@ where
} }
pub struct AppRoutingFactory { pub struct AppRoutingFactory {
services: Rc<Vec<(ResourceDef, HttpNewService, RefCell<Option<Guards>>)>>, services: Rc<[(ResourceDef, HttpNewService, RefCell<Option<Guards>>)]>,
default: Rc<HttpNewService>, default: Rc<HttpNewService>,
} }

View File

@ -31,7 +31,7 @@ pub struct AppService {
Option<Guards>, Option<Guards>,
Option<Rc<ResourceMap>>, Option<Rc<ResourceMap>>,
)>, )>,
service_data: Rc<Vec<Box<dyn DataFactory>>>, service_data: Rc<[Box<dyn DataFactory>]>,
} }
impl AppService { impl AppService {
@ -39,7 +39,7 @@ impl AppService {
pub(crate) fn new( pub(crate) fn new(
config: AppConfig, config: AppConfig,
default: Rc<HttpNewService>, default: Rc<HttpNewService>,
service_data: Rc<Vec<Box<dyn DataFactory>>>, service_data: Rc<[Box<dyn DataFactory>]>,
) -> Self { ) -> Self {
AppService { AppService {
config, config,
@ -141,7 +141,7 @@ impl AppConfig {
/// Server host name. /// Server host name.
/// ///
/// Host name is used by application router as a hostname for url generation. /// Host name is used by application router as a hostname for url generation.
/// Check [ConnectionInfo](./struct.ConnectionInfo.html#method.host) /// Check [ConnectionInfo](super::dev::ConnectionInfo::host())
/// documentation for more information. /// documentation for more information.
/// ///
/// By default host name is set to a "localhost" value. /// By default host name is set to a "localhost" value.
@ -178,6 +178,7 @@ pub struct ServiceConfig {
pub(crate) services: Vec<Box<dyn AppServiceFactory>>, pub(crate) services: Vec<Box<dyn AppServiceFactory>>,
pub(crate) data: Vec<Box<dyn DataFactory>>, pub(crate) data: Vec<Box<dyn DataFactory>>,
pub(crate) external: Vec<ResourceDef>, pub(crate) external: Vec<ResourceDef>,
pub(crate) extensions: Extensions,
} }
impl ServiceConfig { impl ServiceConfig {
@ -186,6 +187,7 @@ impl ServiceConfig {
services: Vec::new(), services: Vec::new(),
data: Vec::new(), data: Vec::new(),
external: Vec::new(), external: Vec::new(),
extensions: Extensions::new(),
} }
} }
@ -198,6 +200,14 @@ impl ServiceConfig {
self self
} }
/// Set arbitrary data item.
///
/// This is same as `App::data()` method.
pub fn app_data<U: 'static>(&mut self, ext: U) -> &mut Self {
self.extensions.insert(ext);
self
}
/// Configure route for a specific path. /// Configure route for a specific path.
/// ///
/// This is same as `App::route()` method. /// This is same as `App::route()` method.
@ -254,13 +264,16 @@ mod tests {
async fn test_data() { async fn test_data() {
let cfg = |cfg: &mut ServiceConfig| { let cfg = |cfg: &mut ServiceConfig| {
cfg.data(10usize); cfg.data(10usize);
cfg.app_data(15u8);
}; };
let mut srv = let mut srv = init_service(App::new().configure(cfg).service(
init_service(App::new().configure(cfg).service( web::resource("/").to(|_: web::Data<usize>, req: HttpRequest| {
web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok()), assert_eq!(*req.app_data::<u8>().unwrap(), 15u8);
)) HttpResponse::Ok()
.await; }),
))
.await;
let req = TestRequest::default().to_request(); let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap(); let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);

View File

@ -1,3 +1,4 @@
use std::any::type_name;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
@ -19,25 +20,20 @@ pub(crate) type FnDataFactory =
/// Application data. /// Application data.
/// ///
/// Application data is an arbitrary data attached to the app. /// Application level data is a piece of arbitrary data attached to the app, scope, or resource.
/// Application data is available to all routes and could be added /// Application data is available to all routes and can be added during the application
/// during application configuration process /// configuration process via `App::data()`.
/// with `App::data()` method.
/// ///
/// Application data could be accessed by using `Data<T>` /// Application data can be accessed by using `Data<T>` extractor where `T` is data type.
/// extractor where `T` is data type.
/// ///
/// **Note**: http server accepts an application factory rather than /// **Note**: http server accepts an application factory rather than an application instance. HTTP
/// an application instance. Http server constructs an application /// server constructs an application instance for each thread, thus application data must be
/// instance for each thread, thus application data must be constructed /// constructed multiple times. If you want to share data between different threads, a shareable
/// multiple times. If you want to share data between different /// object should be used, e.g. `Send + Sync`. Application data does not need to be `Send`
/// threads, a shareable object should be used, e.g. `Send + Sync`. Application /// or `Sync`. Internally `Data` uses `Arc`.
/// data does not need to be `Send` or `Sync`. Internally `Data` type
/// uses `Arc`. if your data implements `Send` + `Sync` traits you can
/// use `web::Data::new()` and avoid double `Arc`.
/// ///
/// If route data is not set for a handler, using `Data<T>` extractor would /// If route data is not set for a handler, using `Data<T>` extractor would cause *Internal
/// cause *Internal Server Error* response. /// Server Error* response.
/// ///
/// ```rust /// ```rust
/// use std::sync::Mutex; /// use std::sync::Mutex;
@ -47,7 +43,7 @@ pub(crate) type FnDataFactory =
/// counter: usize, /// counter: usize,
/// } /// }
/// ///
/// /// Use `Data<T>` extractor to access data in handler. /// /// Use the `Data<T>` extractor to access data in a handler.
/// async fn index(data: web::Data<Mutex<MyData>>) -> impl Responder { /// async fn index(data: web::Data<Mutex<MyData>>) -> impl Responder {
/// let mut data = data.lock().unwrap(); /// let mut data = data.lock().unwrap();
/// data.counter += 1; /// data.counter += 1;
@ -70,10 +66,6 @@ pub struct Data<T: ?Sized>(Arc<T>);
impl<T> Data<T> { impl<T> Data<T> {
/// Create new `Data` instance. /// Create new `Data` instance.
///
/// Internally `Data` type uses `Arc`. if your data implements
/// `Send` + `Sync` traits you can use `web::Data::new()` and
/// avoid double `Arc`.
pub fn new(state: T) -> Data<T> { pub fn new(state: T) -> Data<T> {
Data(Arc::new(state)) Data(Arc::new(state))
} }
@ -121,8 +113,9 @@ impl<T: ?Sized + 'static> FromRequest for Data<T> {
} else { } else {
log::debug!( log::debug!(
"Failed to construct App-level Data extractor. \ "Failed to construct App-level Data extractor. \
Request path: {:?}", Request path: {:?} (type: {})",
req.path() req.path(),
type_name::<T>(),
); );
err(ErrorInternalServerError( err(ErrorInternalServerError(
"App data is not configured, to configure use App::data()", "App data is not configured, to configure use App::data()",

View File

@ -1,4 +1,5 @@
//! Error and Result module //! Error and Result module
pub use actix_http::error::*; pub use actix_http::error::*;
use derive_more::{Display, From}; use derive_more::{Display, From};
use serde_json::error::Error as JsonError; use serde_json::error::Error as JsonError;

View File

@ -4,7 +4,8 @@ use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use actix_http::error::Error; use actix_http::error::Error;
use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready}; use futures_util::future::{ready, Ready};
use futures_util::ready;
use crate::dev::Payload; use crate::dev::Payload;
use crate::request::HttpRequest; use crate::request::HttpRequest;
@ -95,21 +96,41 @@ where
T: FromRequest, T: FromRequest,
T::Future: 'static, T::Future: 'static,
{ {
type Config = T::Config;
type Error = Error; type Error = Error;
type Future = LocalBoxFuture<'static, Result<Option<T>, Error>>; type Future = FromRequestOptFuture<T::Future>;
type Config = T::Config;
#[inline] #[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
T::from_request(req, payload) FromRequestOptFuture {
.then(|r| match r { fut: T::from_request(req, payload),
Ok(v) => ok(Some(v)), }
Err(e) => { }
log::debug!("Error for Option<T> extractor: {}", e.into()); }
ok(None)
} #[pin_project::pin_project]
}) pub struct FromRequestOptFuture<Fut> {
.boxed_local() #[pin]
fut: Fut,
}
impl<Fut, T, E> Future for FromRequestOptFuture<Fut>
where
Fut: Future<Output = Result<T, E>>,
E: Into<Error>,
{
type Output = Result<Option<T>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let res = ready!(this.fut.poll(cx));
match res {
Ok(t) => Poll::Ready(Ok(Some(t))),
Err(e) => {
log::debug!("Error for Option<T> extractor: {}", e.into());
Poll::Ready(Ok(None))
}
}
} }
} }
@ -165,29 +186,45 @@ where
T::Error: 'static, T::Error: 'static,
T::Future: 'static, T::Future: 'static,
{ {
type Config = T::Config;
type Error = Error; type Error = Error;
type Future = LocalBoxFuture<'static, Result<Result<T, T::Error>, Error>>; type Future = FromRequestResFuture<T::Future>;
type Config = T::Config;
#[inline] #[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
T::from_request(req, payload) FromRequestResFuture {
.then(|res| match res { fut: T::from_request(req, payload),
Ok(v) => ok(Ok(v)), }
Err(e) => ok(Err(e)), }
}) }
.boxed_local()
#[pin_project::pin_project]
pub struct FromRequestResFuture<Fut> {
#[pin]
fut: Fut,
}
impl<Fut, T, E> Future for FromRequestResFuture<Fut>
where
Fut: Future<Output = Result<T, E>>,
{
type Output = Result<Result<T, E>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let res = ready!(this.fut.poll(cx));
Poll::Ready(Ok(res))
} }
} }
#[doc(hidden)] #[doc(hidden)]
impl FromRequest for () { impl FromRequest for () {
type Config = ();
type Error = Error; type Error = Error;
type Future = Ready<Result<(), Error>>; type Future = Ready<Result<(), Error>>;
type Config = ();
fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(()) ready(Ok(()))
} }
} }

View File

@ -1,4 +1,3 @@
use std::convert::Infallible;
use std::future::Future; use std::future::Future;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::pin::Pin; use std::pin::Pin;
@ -6,7 +5,7 @@ use std::task::{Context, Poll};
use actix_http::{Error, Response}; use actix_http::{Error, Response};
use actix_service::{Service, ServiceFactory}; use actix_service::{Service, ServiceFactory};
use futures_util::future::{ok, Ready}; use futures_util::future::{ready, Ready};
use futures_util::ready; use futures_util::ready;
use pin_project::pin_project; use pin_project::pin_project;
@ -36,9 +35,11 @@ where
} }
#[doc(hidden)] #[doc(hidden)]
/// Extract arguments from request, run factory function and make response.
pub struct Handler<F, T, R, O> pub struct Handler<F, T, R, O>
where where
F: Factory<T, R, O>, F: Factory<T, R, O>,
T: FromRequest,
R: Future<Output = O>, R: Future<Output = O>,
O: Responder, O: Responder,
{ {
@ -49,6 +50,7 @@ where
impl<F, T, R, O> Handler<F, T, R, O> impl<F, T, R, O> Handler<F, T, R, O>
where where
F: Factory<T, R, O>, F: Factory<T, R, O>,
T: FromRequest,
R: Future<Output = O>, R: Future<Output = O>,
O: Responder, O: Responder,
{ {
@ -63,6 +65,7 @@ where
impl<F, T, R, O> Clone for Handler<F, T, R, O> impl<F, T, R, O> Clone for Handler<F, T, R, O>
where where
F: Factory<T, R, O>, F: Factory<T, R, O>,
T: FromRequest,
R: Future<Output = O>, R: Future<Output = O>,
O: Responder, O: Responder,
{ {
@ -74,188 +77,103 @@ where
} }
} }
impl<F, T, R, O> Service for Handler<F, T, R, O> impl<F, T, R, O> ServiceFactory for Handler<F, T, R, O>
where where
F: Factory<T, R, O>, F: Factory<T, R, O>,
T: FromRequest,
R: Future<Output = O>, R: Future<Output = O>,
O: Responder, O: Responder,
{ {
type Request = (T, HttpRequest); type Request = ServiceRequest;
type Response = ServiceResponse; type Response = ServiceResponse;
type Error = Infallible; type Error = Error;
type Future = HandlerServiceResponse<R, O>; type Config = ();
type Service = Self;
type InitError = ();
type Future = Ready<Result<Self::Service, ()>>;
fn new_service(&self, _: ()) -> Self::Future {
ready(Ok(self.clone()))
}
}
// Handler is both it's ServiceFactory and Service Type.
impl<F, T, R, O> Service for Handler<F, T, R, O>
where
F: Factory<T, R, O>,
T: FromRequest,
R: Future<Output = O>,
O: Responder,
{
type Request = ServiceRequest;
type Response = ServiceResponse;
type Error = Error;
type Future = HandlerServiceFuture<F, T, R, O>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(())) Poll::Ready(Ok(()))
} }
fn call(&mut self, (param, req): (T, HttpRequest)) -> Self::Future { fn call(&mut self, req: Self::Request) -> Self::Future {
HandlerServiceResponse { let (req, mut payload) = req.into_parts();
fut: self.hnd.call(param), let fut = T::from_request(&req, &mut payload);
fut2: None, HandlerServiceFuture::Extract(fut, Some(req), self.hnd.clone())
req: Some(req),
}
} }
} }
#[doc(hidden)] #[doc(hidden)]
#[pin_project] #[pin_project(project = HandlerProj)]
pub struct HandlerServiceResponse<T, R> pub enum HandlerServiceFuture<F, T, R, O>
where where
T: Future<Output = R>, F: Factory<T, R, O>,
R: Responder, T: FromRequest,
R: Future<Output = O>,
O: Responder,
{ {
#[pin] Extract(#[pin] T::Future, Option<HttpRequest>, F),
fut: T, Handle(#[pin] R, Option<HttpRequest>),
#[pin] Respond(#[pin] O::Future, Option<HttpRequest>),
fut2: Option<R::Future>,
req: Option<HttpRequest>,
} }
impl<T, R> Future for HandlerServiceResponse<T, R> impl<F, T, R, O> Future for HandlerServiceFuture<F, T, R, O>
where where
T: Future<Output = R>, F: Factory<T, R, O>,
R: Responder, T: FromRequest,
R: Future<Output = O>,
O: Responder,
{ {
type Output = Result<ServiceResponse, Infallible>; // Error type in this future is a placeholder type.
// all instances of error must be converted to ServiceResponse and return in Ok.
type Output = Result<ServiceResponse, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.as_mut().project(); loop {
match self.as_mut().project() {
if let Some(fut) = this.fut2.as_pin_mut() { HandlerProj::Extract(fut, req, handle) => {
return match fut.poll(cx) { match ready!(fut.poll(cx)) {
Poll::Ready(Ok(res)) => { Ok(item) => {
Poll::Ready(Ok(ServiceResponse::new(this.req.take().unwrap(), res))) let fut = handle.call(item);
let state = HandlerServiceFuture::Handle(fut, req.take());
self.as_mut().set(state);
}
Err(e) => {
let res: Response = e.into().into();
let req = req.take().unwrap();
return Poll::Ready(Ok(ServiceResponse::new(req, res)));
}
};
} }
Poll::Pending => Poll::Pending, HandlerProj::Handle(fut, req) => {
Poll::Ready(Err(e)) => { let res = ready!(fut.poll(cx));
let res: Response = e.into().into(); let fut = res.respond_to(req.as_ref().unwrap());
Poll::Ready(Ok(ServiceResponse::new(this.req.take().unwrap(), res))) let state = HandlerServiceFuture::Respond(fut, req.take());
self.as_mut().set(state);
}
HandlerProj::Respond(fut, req) => {
let res = ready!(fut.poll(cx)).unwrap_or_else(|e| e.into().into());
let req = req.take().unwrap();
return Poll::Ready(Ok(ServiceResponse::new(req, res)));
} }
};
}
match this.fut.poll(cx) {
Poll::Ready(res) => {
let fut = res.respond_to(this.req.as_ref().unwrap());
self.as_mut().project().fut2.set(Some(fut));
self.poll(cx)
}
Poll::Pending => Poll::Pending,
}
}
}
/// Extract arguments from request
pub struct Extract<T: FromRequest, S> {
service: S,
_t: PhantomData<T>,
}
impl<T: FromRequest, S> Extract<T, S> {
pub fn new(service: S) -> Self {
Extract {
service,
_t: PhantomData,
}
}
}
impl<T: FromRequest, S> ServiceFactory for Extract<T, S>
where
S: Service<
Request = (T, HttpRequest),
Response = ServiceResponse,
Error = Infallible,
> + Clone,
{
type Config = ();
type Request = ServiceRequest;
type Response = ServiceResponse;
type Error = (Error, ServiceRequest);
type InitError = ();
type Service = ExtractService<T, S>;
type Future = Ready<Result<Self::Service, ()>>;
fn new_service(&self, _: ()) -> Self::Future {
ok(ExtractService {
_t: PhantomData,
service: self.service.clone(),
})
}
}
pub struct ExtractService<T: FromRequest, S> {
service: S,
_t: PhantomData<T>,
}
impl<T: FromRequest, S> Service for ExtractService<T, S>
where
S: Service<
Request = (T, HttpRequest),
Response = ServiceResponse,
Error = Infallible,
> + Clone,
{
type Request = ServiceRequest;
type Response = ServiceResponse;
type Error = (Error, ServiceRequest);
type Future = ExtractResponse<T, S>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
let (req, mut payload) = req.into_parts();
let fut = T::from_request(&req, &mut payload);
ExtractResponse {
fut,
req,
fut_s: None,
service: self.service.clone(),
}
}
}
#[pin_project]
pub struct ExtractResponse<T: FromRequest, S: Service> {
req: HttpRequest,
service: S,
#[pin]
fut: T::Future,
#[pin]
fut_s: Option<S::Future>,
}
impl<T: FromRequest, S> Future for ExtractResponse<T, S>
where
S: Service<
Request = (T, HttpRequest),
Response = ServiceResponse,
Error = Infallible,
>,
{
type Output = Result<ServiceResponse, (Error, ServiceRequest)>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.as_mut().project();
if let Some(fut) = this.fut_s.as_pin_mut() {
return fut.poll(cx).map_err(|_| panic!());
}
match ready!(this.fut.poll(cx)) {
Err(e) => {
let req = ServiceRequest::new(this.req.clone());
Poll::Ready(Err((e.into(), req)))
}
Ok(item) => {
let fut = Some(this.service.call((item, this.req.clone())));
self.as_mut().project().fut_s.set(fut);
self.poll(cx)
} }
} }
} }

View File

@ -174,7 +174,7 @@ impl ConnectionInfo {
/// Do not use this function for security purposes, unless you can ensure the Forwarded and /// Do not use this function for security purposes, unless you can ensure the Forwarded and
/// X-Forwarded-For headers cannot be spoofed by the client. If you want the client's socket /// X-Forwarded-For headers cannot be spoofed by the client. If you want the client's socket
/// address explicitly, use /// address explicitly, use
/// [`HttpRequest::peer_addr()`](../web/struct.HttpRequest.html#method.peer_addr) instead. /// [`HttpRequest::peer_addr()`](super::web::HttpRequest::peer_addr()) instead.
#[inline] #[inline]
pub fn realip_remote_addr(&self) -> Option<&str> { pub fn realip_remote_addr(&self) -> Option<&str> {
if let Some(ref r) = self.realip_remote_addr { if let Some(ref r) = self.realip_remote_addr {

View File

@ -1,4 +1,4 @@
//! Actix web is a powerful, pragmatic, and extremely fast web framework for Rust. //! Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.
//! //!
//! ## Example //! ## Example
//! //!
@ -29,16 +29,16 @@
//! //!
//! To get started navigating the API docs, you may consider looking at the following pages first: //! 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 //! * [App]: This struct represents an Actix web application and is used to
//! configure routes and other common application settings. //! configure routes and other common application settings.
//! //!
//! * [HttpServer](struct.HttpServer.html): This struct represents an HTTP server instance and is //! * [HttpServer]: This struct represents an HTTP server instance and is
//! used to instantiate and configure servers. //! used to instantiate and configure servers.
//! //!
//! * [web](web/index.html): This module provides essential types for route registration as well as //! * [web]: This module provides essential types for route registration as well as
//! common utilities for request handlers. //! common utilities for request handlers.
//! //!
//! * [HttpRequest](struct.HttpRequest.html) and [HttpResponse](struct.HttpResponse.html): These //! * [HttpRequest] and [HttpResponse]: These
//! structs represent HTTP requests and responses and expose methods for creating, inspecting, //! structs represent HTTP requests and responses and expose methods for creating, inspecting,
//! and otherwise utilizing them. //! and otherwise utilizing them.
//! //!
@ -81,6 +81,7 @@ mod handler;
mod info; mod info;
pub mod middleware; pub mod middleware;
mod request; mod request;
mod request_data;
mod resource; mod resource;
mod responder; mod responder;
mod rmap; mod rmap;
@ -101,10 +102,11 @@ pub use crate::app::App;
pub use crate::extract::FromRequest; pub use crate::extract::FromRequest;
pub use crate::request::HttpRequest; pub use crate::request::HttpRequest;
pub use crate::resource::Resource; pub use crate::resource::Resource;
pub use crate::responder::{Either, Responder}; pub use crate::responder::Responder;
pub use crate::route::Route; pub use crate::route::Route;
pub use crate::scope::Scope; pub use crate::scope::Scope;
pub use crate::server::HttpServer; pub use crate::server::HttpServer;
pub use crate::types::{Either, EitherExtractError};
pub mod dev { pub mod dev {
//! The `actix-web` prelude for library developers //! The `actix-web` prelude for library developers

View File

@ -192,10 +192,7 @@ impl AcceptEncoding {
}; };
let quality = match parts.len() { let quality = match parts.len() {
1 => encoding.quality(), 1 => encoding.quality(),
_ => match f64::from_str(parts[1]) { _ => f64::from_str(parts[1]).unwrap_or(0.0),
Ok(q) => q,
Err(_) => 0.0,
},
}; };
Some(AcceptEncoding { encoding, quality }) Some(AcceptEncoding { encoding, quality })
} }

View File

@ -105,6 +105,7 @@ mod tests {
use crate::test::{self, TestRequest}; use crate::test::{self, TestRequest};
use crate::HttpResponse; use crate::HttpResponse;
#[allow(clippy::unnecessary_wraps)]
fn render_500<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> { fn render_500<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
res.response_mut() res.response_mut()
.headers_mut() .headers_mut()

View File

@ -1,10 +1,14 @@
//! Middleware for setting default response headers //! Middleware for setting default response headers
use std::convert::TryFrom; use std::convert::TryFrom;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::rc::Rc; use std::rc::Rc;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use actix_service::{Service, Transform}; use actix_service::{Service, Transform};
use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready}; use futures_util::future::{ready, Ready};
use futures_util::ready;
use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE};
use crate::http::{Error as HttpError, HeaderMap}; use crate::http::{Error as HttpError, HeaderMap};
@ -97,15 +101,15 @@ where
type Request = ServiceRequest; type Request = ServiceRequest;
type Response = ServiceResponse<B>; type Response = ServiceResponse<B>;
type Error = Error; type Error = Error;
type InitError = ();
type Transform = DefaultHeadersMiddleware<S>; type Transform = DefaultHeadersMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>; type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future { fn new_transform(&self, service: S) -> Self::Future {
ok(DefaultHeadersMiddleware { ready(Ok(DefaultHeadersMiddleware {
service, service,
inner: self.inner.clone(), inner: self.inner.clone(),
}) }))
} }
} }
@ -122,36 +126,56 @@ where
type Request = ServiceRequest; type Request = ServiceRequest;
type Response = ServiceResponse<B>; type Response = ServiceResponse<B>;
type Error = Error; type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; type Future = DefaultHeaderFuture<S, B>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx) self.service.poll_ready(cx)
} }
#[allow(clippy::borrow_interior_mutable_const)]
fn call(&mut self, req: ServiceRequest) -> Self::Future { fn call(&mut self, req: ServiceRequest) -> Self::Future {
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = self.service.call(req); let fut = self.service.call(req);
async move { DefaultHeaderFuture {
let mut res = fut.await?; fut,
inner,
// set response headers _body: PhantomData,
for (key, value) in inner.headers.iter() {
if !res.headers().contains_key(key) {
res.headers_mut().insert(key.clone(), value.clone());
}
}
// default content-type
if inner.ct && !res.headers().contains_key(&CONTENT_TYPE) {
res.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static("application/octet-stream"),
);
}
Ok(res)
} }
.boxed_local() }
}
#[pin_project::pin_project]
pub struct DefaultHeaderFuture<S: Service, B> {
#[pin]
fut: S::Future,
inner: Rc<Inner>,
_body: PhantomData<B>,
}
impl<S, B> Future for DefaultHeaderFuture<S, B>
where
S: Service<Response = ServiceResponse<B>, Error = Error>,
{
type Output = <S::Future as Future>::Output;
#[allow(clippy::borrow_interior_mutable_const)]
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let mut res = ready!(this.fut.poll(cx))?;
// set response headers
for (key, value) in this.inner.headers.iter() {
if !res.headers().contains_key(key) {
res.headers_mut().insert(key.clone(), value.clone());
}
}
// default content-type
if this.inner.ct && !res.headers().contains_key(&CONTENT_TYPE) {
res.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static("application/octet-stream"),
);
}
Poll::Ready(Ok(res))
} }
} }

View File

@ -154,6 +154,7 @@ mod tests {
use crate::test::{self, TestRequest}; use crate::test::{self, TestRequest};
use crate::HttpResponse; use crate::HttpResponse;
#[allow(clippy::unnecessary_wraps)]
fn render_500<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> { fn render_500<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
res.response_mut() res.response_mut()
.headers_mut() .headers_mut()

View File

@ -13,7 +13,7 @@ use actix_service::{Service, Transform};
use bytes::Bytes; use bytes::Bytes;
use futures_util::future::{ok, Ready}; use futures_util::future::{ok, Ready};
use log::debug; use log::debug;
use regex::Regex; use regex::{Regex, RegexSet};
use time::OffsetDateTime; use time::OffsetDateTime;
use crate::dev::{BodySize, MessageBody, ResponseBody}; use crate::dev::{BodySize, MessageBody, ResponseBody};
@ -34,21 +34,19 @@ use crate::HttpResponse;
/// Default `Logger` could be created with `default` method, it uses the /// Default `Logger` could be created with `default` method, it uses the
/// default format: /// default format:
/// ///
/// ```ignore /// ```plain
/// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
/// ``` /// ```
///
/// ```rust /// ```rust
/// use actix_web::middleware::Logger; /// use actix_web::{middleware::Logger, App};
/// use actix_web::App;
/// ///
/// fn main() { /// std::env::set_var("RUST_LOG", "actix_web=info");
/// std::env::set_var("RUST_LOG", "actix_web=info"); /// env_logger::init();
/// env_logger::init();
/// ///
/// let app = App::new() /// let app = App::new()
/// .wrap(Logger::default()) /// .wrap(Logger::default())
/// .wrap(Logger::new("%a %{User-Agent}i")); /// .wrap(Logger::new("%a %{User-Agent}i"));
/// }
/// ``` /// ```
/// ///
/// ## Format /// ## Format
@ -80,18 +78,20 @@ use crate::HttpResponse;
/// ///
/// `%{FOO}e` os.environ['FOO'] /// `%{FOO}e` os.environ['FOO']
/// ///
/// `%{FOO}xi` [custom request replacement](Logger::custom_request_replace) labelled "FOO"
///
/// # Security /// # Security
/// **\*** It is calculated using /// **\*** It is calculated using
/// [`ConnectionInfo::realip_remote_addr()`](../dev/struct.ConnectionInfo.html#method.realip_remote_addr) /// [`ConnectionInfo::realip_remote_addr()`](crate::dev::ConnectionInfo::realip_remote_addr())
/// ///
/// If you use this value ensure that all requests come from trusted hosts, since it is trivial /// If you use this value ensure that all requests come from trusted hosts, since it is trivial
/// for the remote client to simulate being another client. /// for the remote client to simulate being another client.
///
pub struct Logger(Rc<Inner>); pub struct Logger(Rc<Inner>);
struct Inner { struct Inner {
format: Format, format: Format,
exclude: HashSet<String>, exclude: HashSet<String>,
exclude_regex: RegexSet,
} }
impl Logger { impl Logger {
@ -100,6 +100,7 @@ impl Logger {
Logger(Rc::new(Inner { Logger(Rc::new(Inner {
format: Format::new(format), format: Format::new(format),
exclude: HashSet::new(), exclude: HashSet::new(),
exclude_regex: RegexSet::empty(),
})) }))
} }
@ -111,18 +112,69 @@ impl Logger {
.insert(path.into()); .insert(path.into());
self self
} }
/// Ignore and do not log access info for paths that match regex
pub fn exclude_regex<T: Into<String>>(mut self, path: T) -> Self {
let inner = Rc::get_mut(&mut self.0).unwrap();
let mut patterns = inner.exclude_regex.patterns().to_vec();
patterns.push(path.into());
let regex_set = RegexSet::new(patterns).unwrap();
inner.exclude_regex = regex_set;
self
}
/// Register a function that receives a ServiceRequest and returns a String for use in the
/// log line. The label passed as the first argument should match a replacement substring in
/// the logger format like `%{label}xi`.
///
/// It is convention to print "-" to indicate no output instead of an empty string.
///
/// # Example
/// ```rust
/// # use actix_web::{http::HeaderValue, middleware::Logger};
/// # fn parse_jwt_id (_req: Option<&HeaderValue>) -> String { "jwt_uid".to_owned() }
/// Logger::new("example %{JWT_ID}xi")
/// .custom_request_replace("JWT_ID", |req| parse_jwt_id(req.headers().get("Authorization")));
/// ```
pub fn custom_request_replace(
mut self,
label: &str,
f: impl Fn(&ServiceRequest) -> String + 'static,
) -> Self {
let inner = Rc::get_mut(&mut self.0).unwrap();
let ft = inner.format.0.iter_mut().find(|ft| {
matches!(ft, FormatText::CustomRequest(unit_label, _) if label == unit_label)
});
if let Some(FormatText::CustomRequest(_, request_fn)) = ft {
// replace into None or previously registered fn using same label
request_fn.replace(CustomRequestFn {
inner_fn: Rc::new(f),
});
} else {
// non-printed request replacement function diagnostic
debug!(
"Attempted to register custom request logging function for nonexistent label: {}",
label
);
}
self
}
} }
impl Default for Logger { impl Default for Logger {
/// Create `Logger` middleware with format: /// Create `Logger` middleware with format:
/// ///
/// ```ignore /// ```plain
/// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
/// ``` /// ```
fn default() -> Logger { fn default() -> Logger {
Logger(Rc::new(Inner { Logger(Rc::new(Inner {
format: Format::default(), format: Format::default(),
exclude: HashSet::new(), exclude: HashSet::new(),
exclude_regex: RegexSet::empty(),
})) }))
} }
} }
@ -140,6 +192,17 @@ where
type Future = Ready<Result<Self::Transform, Self::InitError>>; type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future { fn new_transform(&self, service: S) -> Self::Future {
for unit in &self.0.format.0 {
// missing request replacement function diagnostic
if let FormatText::CustomRequest(label, None) = unit {
debug!(
"No custom request replacement function was registered for label {} in\
logger format.",
label
);
}
}
ok(LoggerMiddleware { ok(LoggerMiddleware {
service, service,
inner: self.0.clone(), inner: self.0.clone(),
@ -168,7 +231,9 @@ where
} }
fn call(&mut self, req: ServiceRequest) -> Self::Future { fn call(&mut self, req: ServiceRequest) -> Self::Future {
if self.inner.exclude.contains(req.path()) { if self.inner.exclude.contains(req.path())
|| self.inner.exclude_regex.is_match(req.path())
{
LoggerResponse { LoggerResponse {
fut: self.service.call(req), fut: self.service.call(req),
format: None, format: None,
@ -296,7 +361,6 @@ impl<B: MessageBody> MessageBody for StreamLog<B> {
/// A formatting style for the `Logger`, consisting of multiple /// A formatting style for the `Logger`, consisting of multiple
/// `FormatText`s concatenated into one line. /// `FormatText`s concatenated into one line.
#[derive(Clone)] #[derive(Clone)]
#[doc(hidden)]
struct Format(Vec<FormatText>); struct Format(Vec<FormatText>);
impl Default for Format { impl Default for Format {
@ -312,7 +376,8 @@ impl Format {
/// Returns `None` if the format string syntax is incorrect. /// Returns `None` if the format string syntax is incorrect.
pub fn new(s: &str) -> Format { pub fn new(s: &str) -> Format {
log::trace!("Access log format: {}", s); log::trace!("Access log format: {}", s);
let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe])|[atPrUsbTD]?)").unwrap(); let fmt =
Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|xi)|[atPrUsbTD]?)").unwrap();
let mut idx = 0; let mut idx = 0;
let mut results = Vec::new(); let mut results = Vec::new();
@ -340,6 +405,7 @@ impl Format {
HeaderName::try_from(key.as_str()).unwrap(), HeaderName::try_from(key.as_str()).unwrap(),
), ),
"e" => FormatText::EnvironHeader(key.as_str().to_owned()), "e" => FormatText::EnvironHeader(key.as_str().to_owned()),
"xi" => FormatText::CustomRequest(key.as_str().to_owned(), None),
_ => unreachable!(), _ => unreachable!(),
}) })
} else { } else {
@ -369,7 +435,9 @@ impl Format {
/// A string of text to be logged. This is either one of the data /// A string of text to be logged. This is either one of the data
/// fields supported by the `Logger`, or a custom `String`. /// fields supported by the `Logger`, or a custom `String`.
#[doc(hidden)] #[doc(hidden)]
#[non_exhaustive]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
// TODO: remove pub on next breaking change
pub enum FormatText { pub enum FormatText {
Str(String), Str(String),
Percent, Percent,
@ -385,6 +453,26 @@ pub enum FormatText {
RequestHeader(HeaderName), RequestHeader(HeaderName),
ResponseHeader(HeaderName), ResponseHeader(HeaderName),
EnvironHeader(String), EnvironHeader(String),
CustomRequest(String, Option<CustomRequestFn>),
}
// TODO: remove pub on next breaking change
#[doc(hidden)]
#[derive(Clone)]
pub struct CustomRequestFn {
inner_fn: Rc<dyn Fn(&ServiceRequest) -> String>,
}
impl CustomRequestFn {
fn call(&self, req: &ServiceRequest) -> String {
(self.inner_fn)(req)
}
}
impl fmt::Debug for CustomRequestFn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("custom_request_fn")
}
} }
impl FormatText { impl FormatText {
@ -441,7 +529,7 @@ impl FormatText {
} }
fn render_request(&mut self, now: OffsetDateTime, req: &ServiceRequest) { fn render_request(&mut self, now: OffsetDateTime, req: &ServiceRequest) {
match *self { match &*self {
FormatText::RequestLine => { FormatText::RequestLine => {
*self = if req.query_string().is_empty() { *self = if req.query_string().is_empty() {
FormatText::Str(format!( FormatText::Str(format!(
@ -493,11 +581,20 @@ impl FormatText {
}; };
*self = s; *self = s;
} }
FormatText::CustomRequest(_, request_fn) => {
let s = match request_fn {
Some(f) => FormatText::Str(f.call(req)),
None => FormatText::Str("-".to_owned()),
};
*self = s;
}
_ => (), _ => (),
} }
} }
} }
/// Converter to get a String from something that writes to a Formatter.
pub(crate) struct FormatDisplay<'a>( pub(crate) struct FormatDisplay<'a>(
&'a dyn Fn(&mut Formatter<'_>) -> Result<(), fmt::Error>, &'a dyn Fn(&mut Formatter<'_>) -> Result<(), fmt::Error>,
); );
@ -515,7 +612,7 @@ mod tests {
use super::*; use super::*;
use crate::http::{header, StatusCode}; use crate::http::{header, StatusCode};
use crate::test::TestRequest; use crate::test::{self, TestRequest};
#[actix_rt::test] #[actix_rt::test]
async fn test_logger() { async fn test_logger() {
@ -538,6 +635,28 @@ mod tests {
let _res = srv.call(req).await; let _res = srv.call(req).await;
} }
#[actix_rt::test]
async fn test_logger_exclude_regex() {
let srv = |req: ServiceRequest| {
ok(req.into_response(
HttpResponse::build(StatusCode::OK)
.header("X-Test", "ttt")
.finish(),
))
};
let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test")
.exclude_regex("\\w");
let mut srv = logger.new_transform(srv.into_service()).await.unwrap();
let req = TestRequest::with_header(
header::USER_AGENT,
header::HeaderValue::from_static("ACTIX-WEB"),
)
.to_srv_request();
let _res = srv.call(req).await.unwrap();
}
#[actix_rt::test] #[actix_rt::test]
async fn test_url_path() { async fn test_url_path() {
let mut format = Format::new("%T %U"); let mut format = Format::new("%T %U");
@ -662,4 +781,45 @@ mod tests {
println!("{}", s); println!("{}", s);
assert!(s.contains("192.0.2.60")); assert!(s.contains("192.0.2.60"));
} }
#[actix_rt::test]
async fn test_custom_closure_log() {
let mut logger = Logger::new("test %{CUSTOM}xi")
.custom_request_replace("CUSTOM", |_req: &ServiceRequest| -> String {
String::from("custom_log")
});
let mut unit = Rc::get_mut(&mut logger.0).unwrap().format.0[1].clone();
let label = match &unit {
FormatText::CustomRequest(label, _) => label,
ft => panic!("expected CustomRequest, found {:?}", ft),
};
assert_eq!(label, "CUSTOM");
let req = TestRequest::default().to_srv_request();
let now = OffsetDateTime::now_utc();
unit.render_request(now, &req);
let render = |fmt: &mut Formatter<'_>| unit.render(fmt, 1024, now);
let log_output = FormatDisplay(&render).to_string();
assert_eq!(log_output, "custom_log");
}
#[actix_rt::test]
async fn test_closure_logger_in_middleware() {
let captured = "custom log replacement";
let logger = Logger::new("%{CUSTOM}xi")
.custom_request_replace("CUSTOM", move |_req: &ServiceRequest| -> String {
captured.to_owned()
});
let mut srv = logger.new_transform(test::ok_service()).await.unwrap();
let req = TestRequest::default().to_srv_request();
srv.call(req).await.unwrap();
}
} }

View File

@ -1,10 +1,11 @@
//! `Middleware` to normalize request's URI //! For middleware documentation, see [`NormalizePath`].
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use actix_http::http::{PathAndQuery, Uri}; use actix_http::http::{PathAndQuery, Uri};
use actix_service::{Service, Transform}; use actix_service::{Service, Transform};
use bytes::Bytes; use bytes::Bytes;
use futures_util::future::{ok, Ready}; use futures_util::future::{ready, Ready};
use regex::Regex; use regex::Regex;
use crate::service::{ServiceRequest, ServiceResponse}; use crate::service::{ServiceRequest, ServiceResponse};
@ -17,10 +18,12 @@ pub enum TrailingSlash {
/// Always add a trailing slash to the end of the path. /// 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. /// This will require all routes to end in a trailing slash for them to be accessible.
Always, Always,
/// Only merge any present multiple trailing slashes. /// Only merge any present multiple trailing slashes.
/// ///
/// Note: This option provides the best compatibility with the v2 version of this middlware. /// Note: This option provides the best compatibility with the v2 version of this middleware.
MergeOnly, MergeOnly,
/// Trim trailing slashes from the end of the path. /// Trim trailing slashes from the end of the path.
Trim, Trim,
} }
@ -32,28 +35,53 @@ impl Default for TrailingSlash {
} }
#[derive(Default, Clone, Copy)] #[derive(Default, Clone, Copy)]
/// `Middleware` to normalize request's URI in place /// Middleware to normalize a request's path so that routes can be matched less strictly.
/// ///
/// Performs following: /// # Normalization Steps
/// /// - Merges multiple consecutive slashes into one. (For example, `/path//one` always
/// - Merges multiple slashes into one. /// becomes `/path/one`.)
/// - Appends a trailing slash if one is not present, removes one if present, or keeps trailing /// - 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. /// slashes as-is, depending on which [`TrailingSlash`] variant is supplied
/// to [`new`](NormalizePath::new()).
/// ///
/// # Default Behavior
/// The default constructor chooses to strip trailing slashes from the end
/// ([`TrailingSlash::Trim`]), the effect is that route definitions should be defined without
/// trailing slashes or else they will be inaccessible.
///
/// # Example
/// ```rust /// ```rust
/// use actix_web::{web, http, middleware, App, HttpResponse}; /// use actix_web::{web, middleware, App};
/// ///
/// # fn main() { /// # #[actix_rt::test]
/// # async fn normalize() {
/// let app = App::new() /// let app = App::new()
/// .wrap(middleware::NormalizePath::default()) /// .wrap(middleware::NormalizePath::default())
/// .service( /// .route("/test", web::get().to(|| async { "test" }))
/// web::resource("/test") /// .route("/unmatchable/", web::get().to(|| async { "unmatchable" }));
/// .route(web::get().to(|| HttpResponse::Ok())) ///
/// .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed())) /// use actix_web::http::StatusCode;
/// ); /// use actix_web::test::{call_service, init_service, TestRequest};
///
/// let mut app = init_service(app).await;
///
/// let req = TestRequest::with_uri("/test").to_request();
/// let res = call_service(&mut app, req).await;
/// assert_eq!(res.status(), StatusCode::OK);
///
/// let req = TestRequest::with_uri("/test/").to_request();
/// let res = call_service(&mut app, req).await;
/// assert_eq!(res.status(), StatusCode::OK);
///
/// let req = TestRequest::with_uri("/unmatchable").to_request();
/// let res = call_service(&mut app, req).await;
/// assert_eq!(res.status(), StatusCode::NOT_FOUND);
///
/// let req = TestRequest::with_uri("/unmatchable/").to_request();
/// let res = call_service(&mut app, req).await;
/// assert_eq!(res.status(), StatusCode::NOT_FOUND);
/// # } /// # }
/// ``` /// ```
pub struct NormalizePath(TrailingSlash); pub struct NormalizePath(TrailingSlash);
impl NormalizePath { impl NormalizePath {
@ -76,11 +104,11 @@ where
type Future = Ready<Result<Self::Transform, Self::InitError>>; type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future { fn new_transform(&self, service: S) -> Self::Future {
ok(NormalizePathNormalization { ready(Ok(NormalizePathNormalization {
service, service,
merge_slash: Regex::new("//+").unwrap(), merge_slash: Regex::new("//+").unwrap(),
trailing_slash_behavior: self.0, trailing_slash_behavior: self.0,
}) }))
} }
} }
@ -137,9 +165,9 @@ where
// so the change can not be deduced from the length comparison // so the change can not be deduced from the length comparison
if path != original_path { if path != original_path {
let mut parts = head.uri.clone().into_parts(); let mut parts = head.uri.clone().into_parts();
let pq = parts.path_and_query.as_ref().unwrap(); let query = parts.path_and_query.as_ref().and_then(|pq| pq.query());
let path = if let Some(q) = pq.query() { let path = if let Some(q) = query {
Bytes::from(format!("{}?{}", path, q)) Bytes::from(format!("{}?{}", path, q))
} else { } else {
Bytes::copy_from_slice(path.as_bytes()) Bytes::copy_from_slice(path.as_bytes())
@ -160,9 +188,11 @@ mod tests {
use actix_service::IntoService; use actix_service::IntoService;
use super::*; use super::*;
use crate::dev::ServiceRequest; use crate::{
use crate::test::{call_service, init_service, TestRequest}; dev::ServiceRequest,
use crate::{web, App, HttpResponse}; test::{call_service, init_service, TestRequest},
web, App, HttpResponse,
};
#[actix_rt::test] #[actix_rt::test]
async fn test_wrap() { async fn test_wrap() {
@ -244,7 +274,7 @@ mod tests {
} }
#[actix_rt::test] #[actix_rt::test]
async fn keep_trailing_slash_unchange() { async fn keep_trailing_slash_unchanged() {
let mut app = init_service( let mut app = init_service(
App::new() App::new()
.wrap(NormalizePath(TrailingSlash::MergeOnly)) .wrap(NormalizePath(TrailingSlash::MergeOnly))
@ -279,7 +309,7 @@ mod tests {
async fn test_in_place_normalization() { async fn test_in_place_normalization() {
let srv = |req: ServiceRequest| { let srv = |req: ServiceRequest| {
assert_eq!("/v1/something/", req.path()); assert_eq!("/v1/something/", req.path());
ok(req.into_response(HttpResponse::Ok().finish())) ready(Ok(req.into_response(HttpResponse::Ok().finish())))
}; };
let mut normalize = NormalizePath::default() let mut normalize = NormalizePath::default()
@ -310,7 +340,7 @@ mod tests {
let srv = |req: ServiceRequest| { let srv = |req: ServiceRequest| {
assert_eq!(URI, req.path()); assert_eq!(URI, req.path());
ok(req.into_response(HttpResponse::Ok().finish())) ready(Ok(req.into_response(HttpResponse::Ok().finish())))
}; };
let mut normalize = NormalizePath::default() let mut normalize = NormalizePath::default()
@ -324,12 +354,12 @@ mod tests {
} }
#[actix_rt::test] #[actix_rt::test]
async fn should_normalize_notrail() { async fn should_normalize_no_trail() {
const URI: &str = "/v1/something"; const URI: &str = "/v1/something";
let srv = |req: ServiceRequest| { let srv = |req: ServiceRequest| {
assert_eq!(URI.to_string() + "/", req.path()); assert_eq!(URI.to_string() + "/", req.path());
ok(req.into_response(HttpResponse::Ok().finish())) ready(Ok(req.into_response(HttpResponse::Ok().finish())))
}; };
let mut normalize = NormalizePath::default() let mut normalize = NormalizePath::default()

View File

@ -675,4 +675,40 @@ mod tests {
let res = call_service(&mut srv, req).await; let res = call_service(&mut srv, req).await;
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
} }
#[actix_rt::test]
async fn extract_path_pattern_complex() {
let mut srv = init_service(
App::new()
.service(web::scope("/user").service(web::scope("/{id}").service(
web::resource("").to(move |req: HttpRequest| {
assert_eq!(req.match_pattern(), Some("/user/{id}".to_owned()));
HttpResponse::Ok().finish()
}),
)))
.service(web::resource("/").to(move |req: HttpRequest| {
assert_eq!(req.match_pattern(), Some("/".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/test").to_request();
let res = call_service(&mut srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
let req = TestRequest::get().uri("/").to_request();
let res = call_service(&mut srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
let req = TestRequest::get().uri("/not-exist").to_request();
let res = call_service(&mut srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
}
} }

175
src/request_data.rs Normal file
View File

@ -0,0 +1,175 @@
use std::{any::type_name, ops::Deref};
use actix_http::error::{Error, ErrorInternalServerError};
use futures_util::future;
use crate::{dev::Payload, FromRequest, HttpRequest};
/// Request-local data extractor.
///
/// Request-local data is arbitrary data attached to an individual request, usually
/// by middleware. It can be set via `extensions_mut` on [`HttpRequest`][htr_ext_mut]
/// or [`ServiceRequest`][srv_ext_mut].
///
/// Unlike app data, request data is dropped when the request has finished processing. This makes it
/// useful as a kind of messaging system between middleware and request handlers. It uses the same
/// types-as-keys storage system as app data.
///
/// # Mutating Request Data
/// Note that since extractors must output owned data, only types that `impl Clone` can use this
/// extractor. A clone is taken of the required request data and can, therefore, not be directly
/// mutated in-place. To mutate request data, continue to use [`HttpRequest::extensions_mut`] or
/// re-insert the cloned data back into the extensions map. A `DerefMut` impl is intentionally not
/// provided to make this potential foot-gun more obvious.
///
/// # Example
/// ```rust,no_run
/// # use actix_web::{web, HttpResponse, HttpRequest, Responder};
///
/// #[derive(Debug, Clone, PartialEq)]
/// struct FlagFromMiddleware(String);
///
/// /// Use the `ReqData<T>` extractor to access request data in a handler.
/// async fn handler(
/// req: HttpRequest,
/// opt_flag: Option<web::ReqData<FlagFromMiddleware>>,
/// ) -> impl Responder {
/// // use an optional extractor if the middleware is
/// // not guaranteed to add this type of requests data
/// if let Some(flag) = opt_flag {
/// assert_eq!(&flag.into_inner(), req.extensions().get::<FlagFromMiddleware>().unwrap());
/// }
///
/// HttpResponse::Ok()
/// }
/// ```
///
/// [htr_ext_mut]: crate::HttpRequest::extensions_mut
/// [srv_ext_mut]: crate::dev::ServiceRequest::extensions_mut
#[derive(Debug, Clone)]
pub struct ReqData<T: Clone + 'static>(T);
impl<T: Clone + 'static> ReqData<T> {
/// Consumes the `ReqData`, returning its wrapped data.
pub fn into_inner(self) -> T {
self.0
}
}
impl<T: Clone + 'static> Deref for ReqData<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T: Clone + 'static> FromRequest for ReqData<T> {
type Config = ();
type Error = Error;
type Future = future::Ready<Result<Self, Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
if let Some(st) = req.extensions().get::<T>() {
future::ok(ReqData(st.clone()))
} else {
log::debug!(
"Failed to construct App-level ReqData extractor. \
Request path: {:?} (type: {})",
req.path(),
type_name::<T>(),
);
future::err(ErrorInternalServerError(
"Missing expected request extension data",
))
}
}
}
#[cfg(test)]
mod tests {
use std::{cell::RefCell, rc::Rc};
use futures_util::TryFutureExt as _;
use super::*;
use crate::{
dev::Service,
http::{Method, StatusCode},
test::{init_service, TestRequest},
web, App, HttpMessage, HttpResponse,
};
#[actix_rt::test]
async fn req_data_extractor() {
let mut srv = init_service(
App::new()
.wrap_fn(|req, srv| {
if req.method() == Method::POST {
req.extensions_mut().insert(42u32);
}
srv.call(req)
})
.service(web::resource("/test").to(
|req: HttpRequest, data: Option<ReqData<u32>>| {
if req.method() != Method::POST {
assert!(data.is_none());
}
if let Some(data) = data {
assert_eq!(*data, 42);
assert_eq!(
Some(data.into_inner()),
req.extensions().get::<u32>().copied()
);
}
HttpResponse::Ok()
},
)),
)
.await;
let req = TestRequest::get().uri("/test").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::post().uri("/test").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn req_data_internal_mutability() {
let mut srv = init_service(
App::new()
.wrap_fn(|req, srv| {
let data_before = Rc::new(RefCell::new(42u32));
req.extensions_mut().insert(data_before);
srv.call(req).map_ok(|res| {
{
let ext = res.request().extensions();
let data_after = ext.get::<Rc<RefCell<u32>>>().unwrap();
assert_eq!(*data_after.borrow(), 53u32);
}
res
})
})
.default_service(web::to(|data: ReqData<Rc<RefCell<u32>>>| {
assert_eq!(*data.borrow(), 42);
*data.borrow_mut() += 11;
assert_eq!(*data.borrow(), 53);
HttpResponse::Ok()
})),
)
.await;
let req = TestRequest::get().uri("/test").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
}

View File

@ -332,82 +332,6 @@ impl<T: Responder> Future for CustomResponderFut<T> {
} }
} }
/// Combines two different responder types into a single type
///
/// ```rust
/// use actix_web::{Either, Error, HttpResponse};
///
/// type RegisterResult = Either<HttpResponse, Result<HttpResponse, Error>>;
///
/// fn index() -> RegisterResult {
/// if is_a_variant() {
/// // <- choose left variant
/// Either::A(HttpResponse::BadRequest().body("Bad data"))
/// } else {
/// Either::B(
/// // <- Right variant
/// Ok(HttpResponse::Ok()
/// .content_type("text/html")
/// .body("Hello!"))
/// )
/// }
/// }
/// # fn is_a_variant() -> bool { true }
/// # fn main() {}
/// ```
#[derive(Debug, PartialEq)]
pub enum Either<A, B> {
/// First branch of the type
A(A),
/// Second branch of the type
B(B),
}
impl<A, B> Responder for Either<A, B>
where
A: Responder,
B: Responder,
{
type Error = Error;
type Future = EitherResponder<A, B>;
fn respond_to(self, req: &HttpRequest) -> Self::Future {
match self {
Either::A(a) => EitherResponder::A(a.respond_to(req)),
Either::B(b) => EitherResponder::B(b.respond_to(req)),
}
}
}
#[pin_project(project = EitherResponderProj)]
pub enum EitherResponder<A, B>
where
A: Responder,
B: Responder,
{
A(#[pin] A::Future),
B(#[pin] B::Future),
}
impl<A, B> Future for EitherResponder<A, B>
where
A: Responder,
B: Responder,
{
type Output = Result<Response, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.project() {
EitherResponderProj::A(fut) => {
Poll::Ready(ready!(fut.poll(cx)).map_err(|e| e.into()))
}
EitherResponderProj::B(fut) => {
Poll::Ready(ready!(fut.poll(cx).map_err(|e| e.into())))
}
}
}
}
impl<T> Responder for InternalError<T> impl<T> Responder for InternalError<T>
where where
T: std::fmt::Debug + std::fmt::Display + 'static, T: std::fmt::Debug + std::fmt::Display + 'static,

View File

@ -86,7 +86,7 @@ impl ResourceMap {
if let Some(plen) = pattern.is_prefix_match(path) { if let Some(plen) = pattern.is_prefix_match(path) {
return rmap.has_resource(&path[plen..]); return rmap.has_resource(&path[plen..]);
} }
} else if pattern.is_match(path) { } else if pattern.is_match(path) || pattern.pattern() == "" && path == "/" {
return true; return true;
} }
} }

View File

@ -1,3 +1,5 @@
#![allow(clippy::rc_buffer)] // inner value is mutated before being shared (`Rc::get_mut`)
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::rc::Rc; use std::rc::Rc;
@ -9,29 +11,29 @@ use futures_util::future::{ready, FutureExt, LocalBoxFuture};
use crate::extract::FromRequest; use crate::extract::FromRequest;
use crate::guard::{self, Guard}; use crate::guard::{self, Guard};
use crate::handler::{Extract, Factory, Handler}; use crate::handler::{Factory, Handler};
use crate::responder::Responder; use crate::responder::Responder;
use crate::service::{ServiceRequest, ServiceResponse}; use crate::service::{ServiceRequest, ServiceResponse};
use crate::HttpResponse; use crate::HttpResponse;
type BoxedRouteService<Req, Res> = Box< type BoxedRouteService = Box<
dyn Service< dyn Service<
Request = Req, Request = ServiceRequest,
Response = Res, Response = ServiceResponse,
Error = Error, Error = Error,
Future = LocalBoxFuture<'static, Result<Res, Error>>, Future = LocalBoxFuture<'static, Result<ServiceResponse, Error>>,
>, >,
>; >;
type BoxedRouteNewService<Req, Res> = Box< type BoxedRouteNewService = Box<
dyn ServiceFactory< dyn ServiceFactory<
Config = (), Config = (),
Request = Req, Request = ServiceRequest,
Response = Res, Response = ServiceResponse,
Error = Error, Error = Error,
InitError = (), InitError = (),
Service = BoxedRouteService<Req, Res>, Service = BoxedRouteService,
Future = LocalBoxFuture<'static, Result<BoxedRouteService<Req, Res>, ()>>, Future = LocalBoxFuture<'static, Result<BoxedRouteService, ()>>,
>, >,
>; >;
@ -40,7 +42,7 @@ type BoxedRouteNewService<Req, Res> = Box<
/// Route uses builder-like pattern for configuration. /// Route uses builder-like pattern for configuration.
/// If handler is not explicitly set, default *404 Not Found* handler is used. /// If handler is not explicitly set, default *404 Not Found* handler is used.
pub struct Route { pub struct Route {
service: BoxedRouteNewService<ServiceRequest, ServiceResponse>, service: BoxedRouteNewService,
guards: Rc<Vec<Box<dyn Guard>>>, guards: Rc<Vec<Box<dyn Guard>>>,
} }
@ -49,9 +51,9 @@ impl Route {
#[allow(clippy::new_without_default)] #[allow(clippy::new_without_default)]
pub fn new() -> Route { pub fn new() -> Route {
Route { Route {
service: Box::new(RouteNewService::new(Extract::new(Handler::new(|| { service: Box::new(RouteNewService::new(Handler::new(|| {
ready(HttpResponse::NotFound()) ready(HttpResponse::NotFound())
})))), }))),
guards: Rc::new(Vec::new()), guards: Rc::new(Vec::new()),
} }
} }
@ -78,15 +80,8 @@ impl ServiceFactory for Route {
} }
} }
type RouteFuture = LocalBoxFuture<
'static,
Result<BoxedRouteService<ServiceRequest, ServiceResponse>, ()>,
>;
#[pin_project::pin_project]
pub struct CreateRouteService { pub struct CreateRouteService {
#[pin] fut: LocalBoxFuture<'static, Result<BoxedRouteService, ()>>,
fut: RouteFuture,
guards: Rc<Vec<Box<dyn Guard>>>, guards: Rc<Vec<Box<dyn Guard>>>,
} }
@ -94,9 +89,9 @@ impl Future for CreateRouteService {
type Output = Result<RouteService, ()>; type Output = Result<RouteService, ()>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project(); let this = self.get_mut();
match this.fut.poll(cx)? { match this.fut.as_mut().poll(cx)? {
Poll::Ready(service) => Poll::Ready(Ok(RouteService { Poll::Ready(service) => Poll::Ready(Ok(RouteService {
service, service,
guards: this.guards.clone(), guards: this.guards.clone(),
@ -107,7 +102,7 @@ impl Future for CreateRouteService {
} }
pub struct RouteService { pub struct RouteService {
service: BoxedRouteService<ServiceRequest, ServiceResponse>, service: BoxedRouteService,
guards: Rc<Vec<Box<dyn Guard>>>, guards: Rc<Vec<Box<dyn Guard>>>,
} }
@ -133,7 +128,7 @@ impl Service for RouteService {
} }
fn call(&mut self, req: ServiceRequest) -> Self::Future { fn call(&mut self, req: ServiceRequest) -> Self::Future {
self.service.call(req).boxed_local() self.service.call(req)
} }
} }
@ -231,15 +226,14 @@ impl Route {
R: Future<Output = U> + 'static, R: Future<Output = U> + 'static,
U: Responder + 'static, U: Responder + 'static,
{ {
self.service = self.service = Box::new(RouteNewService::new(Handler::new(handler)));
Box::new(RouteNewService::new(Extract::new(Handler::new(handler))));
self self
} }
} }
struct RouteNewService<T> struct RouteNewService<T>
where where
T: ServiceFactory<Request = ServiceRequest, Error = (Error, ServiceRequest)>, T: ServiceFactory<Request = ServiceRequest, Error = Error>,
{ {
service: T, service: T,
} }
@ -250,7 +244,7 @@ where
Config = (), Config = (),
Request = ServiceRequest, Request = ServiceRequest,
Response = ServiceResponse, Response = ServiceResponse,
Error = (Error, ServiceRequest), Error = Error,
>, >,
T::Future: 'static, T::Future: 'static,
T::Service: 'static, T::Service: 'static,
@ -267,18 +261,18 @@ where
Config = (), Config = (),
Request = ServiceRequest, Request = ServiceRequest,
Response = ServiceResponse, Response = ServiceResponse,
Error = (Error, ServiceRequest), Error = Error,
>, >,
T::Future: 'static, T::Future: 'static,
T::Service: 'static, T::Service: 'static,
<T::Service as Service>::Future: 'static, <T::Service as Service>::Future: 'static,
{ {
type Config = ();
type Request = ServiceRequest; type Request = ServiceRequest;
type Response = ServiceResponse; type Response = ServiceResponse;
type Error = Error; type Error = Error;
type Config = ();
type Service = BoxedRouteService;
type InitError = (); type InitError = ();
type Service = BoxedRouteService<ServiceRequest, Self::Response>;
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>; type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
@ -286,8 +280,7 @@ where
.new_service(()) .new_service(())
.map(|result| match result { .map(|result| match result {
Ok(service) => { Ok(service) => {
let service: BoxedRouteService<_, _> = let service = Box::new(RouteServiceWrapper { service }) as _;
Box::new(RouteServiceWrapper { service });
Ok(service) Ok(service)
} }
Err(_) => Err(()), Err(_) => Err(()),
@ -303,11 +296,7 @@ struct RouteServiceWrapper<T: Service> {
impl<T> Service for RouteServiceWrapper<T> impl<T> Service for RouteServiceWrapper<T>
where where
T::Future: 'static, T::Future: 'static,
T: Service< T: Service<Request = ServiceRequest, Response = ServiceResponse, Error = Error>,
Request = ServiceRequest,
Response = ServiceResponse,
Error = (Error, ServiceRequest),
>,
{ {
type Request = ServiceRequest; type Request = ServiceRequest;
type Response = ServiceResponse; type Response = ServiceResponse;
@ -315,27 +304,11 @@ where
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx).map_err(|(e, _)| e) self.service.poll_ready(cx)
} }
fn call(&mut self, req: ServiceRequest) -> Self::Future { fn call(&mut self, req: ServiceRequest) -> Self::Future {
// let mut fut = self.service.call(req); Box::pin(self.service.call(req))
self.service
.call(req)
.map(|res| match res {
Ok(res) => Ok(res),
Err((err, req)) => Ok(req.error_response(err)),
})
.boxed_local()
// match fut.poll() {
// Poll::Ready(Ok(res)) => Either::Left(ok(res)),
// Poll::Ready(Err((e, req))) => Either::Left(ok(req.error_response(e))),
// Poll::Pending => Either::Right(Box::new(fut.then(|res| match res {
// Ok(res) => Ok(res),
// Err((err, req)) => Ok(req.error_response(err)),
// }))),
// }
} }
} }

View File

@ -58,7 +58,6 @@ type BoxedResponse = LocalBoxFuture<'static, Result<ServiceResponse, Error>>;
/// * /{project_id}/path1 - responds to all http method /// * /{project_id}/path1 - responds to all http method
/// * /{project_id}/path2 - `GET` requests /// * /{project_id}/path2 - `GET` requests
/// * /{project_id}/path3 - `HEAD` requests /// * /{project_id}/path3 - `HEAD` requests
///
pub struct Scope<T = ScopeEndpoint> { pub struct Scope<T = ScopeEndpoint> {
endpoint: T, endpoint: T,
rdef: String, rdef: String,
@ -210,6 +209,9 @@ where
self.data = Some(data); self.data = Some(data);
} }
self.data
.get_or_insert_with(Extensions::new)
.extend(cfg.extensions);
self self
} }
@ -443,16 +445,17 @@ where
*self.factory_ref.borrow_mut() = Some(ScopeFactory { *self.factory_ref.borrow_mut() = Some(ScopeFactory {
data: self.data.take().map(Rc::new), data: self.data.take().map(Rc::new),
default: self.default.clone(), default: self.default.clone(),
services: Rc::new( services: cfg
cfg.into_services() .into_services()
.1 .1
.into_iter() .into_iter()
.map(|(mut rdef, srv, guards, nested)| { .map(|(mut rdef, srv, guards, nested)| {
rmap.add(&mut rdef, nested); rmap.add(&mut rdef, nested);
(rdef, srv, RefCell::new(guards)) (rdef, srv, RefCell::new(guards))
}) })
.collect(), .collect::<Vec<_>>()
), .into_boxed_slice()
.into(),
}); });
// get guards // get guards
@ -474,7 +477,7 @@ where
pub struct ScopeFactory { pub struct ScopeFactory {
data: Option<Rc<Extensions>>, data: Option<Rc<Extensions>>,
services: Rc<Vec<(ResourceDef, HttpNewService, RefCell<Option<Guards>>)>>, services: Rc<[(ResourceDef, HttpNewService, RefCell<Option<Guards>>)]>,
default: Rc<RefCell<Option<Rc<HttpNewService>>>>, default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
} }

View File

@ -1,8 +1,14 @@
use std::marker::PhantomData; use std::{
use std::sync::{Arc, Mutex}; any::Any,
use std::{fmt, io, net}; fmt, io,
marker::PhantomData,
net,
sync::{Arc, Mutex},
};
use actix_http::{body::MessageBody, Error, HttpService, KeepAlive, Request, Response}; use actix_http::{
body::MessageBody, Error, Extensions, HttpService, KeepAlive, Request, Response,
};
use actix_server::{Server, ServerBuilder}; use actix_server::{Server, ServerBuilder};
use actix_service::{map_config, IntoServiceFactory, Service, ServiceFactory}; use actix_service::{map_config, IntoServiceFactory, Service, ServiceFactory};
@ -64,6 +70,7 @@ where
backlog: i32, backlog: i32,
sockets: Vec<Socket>, sockets: Vec<Socket>,
builder: ServerBuilder, builder: ServerBuilder,
on_connect_fn: Option<Arc<dyn Fn(&dyn Any, &mut Extensions) + Send + Sync>>,
_t: PhantomData<(S, B)>, _t: PhantomData<(S, B)>,
} }
@ -91,6 +98,32 @@ where
backlog: 1024, backlog: 1024,
sockets: Vec::new(), sockets: Vec::new(),
builder: ServerBuilder::default(), builder: ServerBuilder::default(),
on_connect_fn: None,
_t: PhantomData,
}
}
/// Sets function that will be called once before each connection is handled.
/// It will receive a `&std::any::Any`, which contains underlying connection type and an
/// [Extensions] container so that request-local data can be passed to middleware and handlers.
///
/// For example:
/// - `actix_tls::openssl::SslStream<actix_web::rt::net::TcpStream>` when using openssl.
/// - `actix_tls::rustls::TlsStream<actix_web::rt::net::TcpStream>` when using rustls.
/// - `actix_web::rt::net::TcpStream` when no encryption is used.
///
/// See `on_connect` example for additional details.
pub fn on_connect<CB>(self, f: CB) -> HttpServer<F, I, S, B>
where
CB: Fn(&dyn Any, &mut Extensions) + Send + Sync + 'static,
{
HttpServer {
factory: self.factory,
config: self.config,
backlog: self.backlog,
sockets: self.sockets,
builder: self.builder,
on_connect_fn: Some(Arc::new(f)),
_t: PhantomData, _t: PhantomData,
} }
} }
@ -180,7 +213,7 @@ where
/// Set server host name. /// Set server host name.
/// ///
/// Host name is used by application router as a hostname for url generation. /// Host name is used by application router as a hostname for url generation.
/// Check [ConnectionInfo](./dev/struct.ConnectionInfo.html#method.host) /// Check [ConnectionInfo](super::dev::ConnectionInfo::host())
/// documentation for more information. /// documentation for more information.
/// ///
/// By default host name is set to a "localhost" value. /// By default host name is set to a "localhost" value.
@ -240,6 +273,7 @@ where
addr, addr,
scheme: "http", scheme: "http",
}); });
let on_connect_fn = self.on_connect_fn.clone();
self.builder = self.builder.listen( self.builder = self.builder.listen(
format!("actix-web-service-{}", addr), format!("actix-web-service-{}", addr),
@ -252,11 +286,20 @@ where
c.host.clone().unwrap_or_else(|| format!("{}", addr)), c.host.clone().unwrap_or_else(|| format!("{}", addr)),
); );
HttpService::build() let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_timeout(c.client_timeout) .client_timeout(c.client_timeout)
.local_addr(addr) .local_addr(addr);
.finish(map_config(factory(), move |_| cfg.clone()))
let svc = if let Some(handler) = on_connect_fn.clone() {
svc.on_connect_ext(move |io: &_, ext: _| {
(handler)(io as &dyn Any, ext)
})
} else {
svc
};
svc.finish(map_config(factory(), move |_| cfg.clone()))
.tcp() .tcp()
}, },
)?; )?;
@ -289,6 +332,8 @@ where
scheme: "https", scheme: "https",
}); });
let on_connect_fn = self.on_connect_fn.clone();
self.builder = self.builder.listen( self.builder = self.builder.listen(
format!("actix-web-service-{}", addr), format!("actix-web-service-{}", addr),
lst, lst,
@ -299,11 +344,21 @@ where
addr, addr,
c.host.clone().unwrap_or_else(|| format!("{}", addr)), c.host.clone().unwrap_or_else(|| format!("{}", addr)),
); );
HttpService::build()
let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_timeout(c.client_timeout) .client_timeout(c.client_timeout)
.client_disconnect(c.client_shutdown) .client_disconnect(c.client_shutdown);
.finish(map_config(factory(), move |_| cfg.clone()))
let svc = if let Some(handler) = on_connect_fn.clone() {
svc.on_connect_ext(move |io: &_, ext: _| {
(&*handler)(io as &dyn Any, ext)
})
} else {
svc
};
svc.finish(map_config(factory(), move |_| cfg.clone()))
.openssl(acceptor.clone()) .openssl(acceptor.clone())
}, },
)?; )?;
@ -336,6 +391,8 @@ where
scheme: "https", scheme: "https",
}); });
let on_connect_fn = self.on_connect_fn.clone();
self.builder = self.builder.listen( self.builder = self.builder.listen(
format!("actix-web-service-{}", addr), format!("actix-web-service-{}", addr),
lst, lst,
@ -346,11 +403,21 @@ where
addr, addr,
c.host.clone().unwrap_or_else(|| format!("{}", addr)), c.host.clone().unwrap_or_else(|| format!("{}", addr)),
); );
HttpService::build()
let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
.client_timeout(c.client_timeout) .client_timeout(c.client_timeout)
.client_disconnect(c.client_shutdown) .client_disconnect(c.client_shutdown);
.finish(map_config(factory(), move |_| cfg.clone()))
let svc = if let Some(handler) = on_connect_fn.clone() {
svc.on_connect_ext(move |io: &_, ext: _| {
(handler)(io as &dyn Any, ext)
})
} else {
svc
};
svc.finish(map_config(factory(), move |_| cfg.clone()))
.rustls(config.clone()) .rustls(config.clone())
}, },
)?; )?;
@ -441,7 +508,7 @@ where
} }
#[cfg(unix)] #[cfg(unix)]
/// Start listening for unix domain connections on existing listener. /// Start listening for unix domain (UDS) connections on existing listener.
pub fn listen_uds( pub fn listen_uds(
mut self, mut self,
lst: std::os::unix::net::UnixListener, lst: std::os::unix::net::UnixListener,
@ -460,6 +527,7 @@ where
}); });
let addr = format!("actix-web-service-{:?}", lst.local_addr()?); let addr = format!("actix-web-service-{:?}", lst.local_addr()?);
let on_connect_fn = self.on_connect_fn.clone();
self.builder = self.builder.listen_uds(addr, lst, move || { self.builder = self.builder.listen_uds(addr, lst, move || {
let c = cfg.lock().unwrap(); let c = cfg.lock().unwrap();
@ -468,11 +536,23 @@ where
socket_addr, socket_addr,
c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)), c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)),
); );
pipeline_factory(|io: UnixStream| ok((io, Protocol::Http1, None))).and_then( pipeline_factory(|io: UnixStream| ok((io, Protocol::Http1, None))).and_then(
HttpService::build() {
.keep_alive(c.keep_alive) let svc = HttpService::build()
.client_timeout(c.client_timeout) .keep_alive(c.keep_alive)
.finish(map_config(factory(), move |_| config.clone())), .client_timeout(c.client_timeout);
let svc = if let Some(handler) = on_connect_fn.clone() {
svc.on_connect_ext(move |io: &_, ext: _| {
(&*handler)(io as &dyn Any, ext)
})
} else {
svc
};
svc.finish(map_config(factory(), move |_| config.clone()))
},
) )
})?; })?;
Ok(self) Ok(self)

Some files were not shown because too many files have changed in this diff Show More