1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-04 18:06:23 +02:00

Compare commits

...

187 Commits

Author SHA1 Message Date
b62da7e86b prepare actix-web-actors release 4.0.0-beta.3 2021-03-09 23:44:26 +00:00
5e9a3eb6ae prepare actix-multipart release 0.4.0-beta.3 2021-03-09 23:40:50 +00:00
3451d6874f prepare actix-files release 0.6.0-beta.3 2021-03-09 23:39:40 +00:00
18c3783a1c prepare actix-http-test release 3.0.0-beta.3 2021-03-09 23:35:42 +00:00
4b46351d36 prepare actix-web release 4.0.0-beta.4 2021-03-09 23:31:44 +00:00
b7c406637d prepare actix-web-codegen release 0.5.0-beta.2 2021-03-09 23:27:38 +00:00
c4e5651215 update docs 2021-03-08 23:49:12 +00:00
23b0e64199 prepare awc release 3.0.0-beta.3 2021-03-08 23:15:53 +00:00
fc31b091e4 prepare http release 3.0.0-beta.4 2021-03-08 23:07:40 +00:00
effacf8fc8 fix ssl test 2021-03-08 20:51:50 +00:00
95130fcfd0 address clippy warnings 2021-03-08 20:32:19 +00:00
5e81105317 remove ka timer from h2 dispatcher (#2057) 2021-03-08 20:00:20 +00:00
5b4105e1e6 Refactor/client builder (#2053) 2021-03-07 23:57:32 +00:00
2d3a0d6038 json method receives plain serialize (#2052) 2021-03-07 22:11:39 +00:00
fe0b3f459f remove localwaker from h1::payload (#2051) 2021-03-07 21:23:42 +00:00
ca69b6577e use iota for more content-length insertions (#2050) 2021-03-07 19:29:02 +00:00
880b863f95 fix h1 client for handling expect header request (#2049) 2021-03-07 18:33:16 +00:00
78384c3ff5 make actix_http::ws::Codec::new const (#2043) 2021-03-04 19:19:01 +00:00
c1c4400c4a fix h2 tests (#2034) 2021-03-04 13:27:54 +00:00
fc6f974617 Add "name" attribute to route macro (#1934) 2021-03-04 12:38:47 +00:00
14b249b804 update to actix-0.11.0-beta.3 for actix-web-actors (#2042) 2021-03-04 11:39:29 +00:00
0195824794 Update codecov.yml 2021-03-02 13:18:16 +00:00
fb019f15b4 test(files): Fix test and remove outdated case (#2037)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-02-28 23:01:59 +00:00
abc7fd374b update example links (#2036)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-02-28 21:41:07 +00:00
cd652dca75 refactor websocket key hashing (#2035) 2021-02-28 19:55:34 +00:00
c836de44af add client middleware (#2013) 2021-02-28 18:17:08 +00:00
badae2f8fd add local_address bind for client builder (#2024) 2021-02-27 22:31:14 +00:00
1f34718ecd Use once_cell instead of lazy_static (#2029) 2021-02-27 21:55:50 +00:00
ebda60fd6b refactor boxed route (#2033) 2021-02-27 21:00:36 +00:00
d242f57758 fix tests for codecov 2021-02-27 20:58:44 +00:00
b95e1dda34 pin h2 to 0.3.0 2021-02-27 19:57:09 +00:00
8f2a97c6e3 Update README example links (#2027)
The examples repo went through a folder restructuring. More info: https://github.com/actix/examples/pull/411
2021-02-26 01:21:56 +00:00
ebaf25d55a Fix typos in CHANGES.md (#2025)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-02-24 16:19:40 +00:00
42711c23d7 Port over doc comments in route macros. (#2022)
Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-02-24 12:26:56 +00:00
f6393728c7 remove usage of actix_utils::mpsc (#2023) 2021-02-24 09:08:56 +00:00
d92ab7e8e0 add msrv to clippy config (#1862) 2021-02-22 15:39:31 +00:00
5845b3965c actix-http-test: minimize features of dependencies (#2019) 2021-02-22 12:00:08 +00:00
aacec30ad1 reduce duplicate code (#2020) 2021-02-22 11:15:12 +00:00
2dbdf61c37 Inner field of web::Query is public again (#2016) (#2017)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-02-20 17:59:09 +00:00
83365058ce Fix HTTP client link (#2011) 2021-02-18 21:56:24 +00:00
3b93c62e23 Fix Json extractor to be 32kB by default (#2010) 2021-02-18 15:20:20 +00:00
946cccaa1a refactor awc::ClientBuilder (#2008) 2021-02-18 12:30:09 +00:00
1838d9cd0f remove unused method. reduce leaf future type (#2009) 2021-02-18 11:24:10 +00:00
f62a982a51 simplify the match on h1 message type (#2006) 2021-02-18 10:38:27 +00:00
dfd9dc40ea remove awc::connect::connect trait. (#2004) 2021-02-17 17:10:46 +00:00
5efea652e3 add ClientResponse::timeout (#1931) 2021-02-17 11:55:11 +00:00
dfa795ff9d return poll in poll_flush (#2005) 2021-02-17 11:18:31 +00:00
2cc6b47fcf Use http-range library for HttpRange (#2003) 2021-02-16 18:48:16 +00:00
117025a96b simplify client::connection::Connection trait (#1998) 2021-02-16 14:10:22 +00:00
3e0a9b99ff update rust-cache action 2021-02-16 09:28:14 +00:00
17b3e7e225 pool doc nits (#1999) 2021-02-16 09:08:30 +00:00
c065729468 rework client connection pool (#1994) 2021-02-16 08:27:14 +00:00
55db3ec65c split up http body module 2021-02-15 12:20:43 +00:00
0404b78b54 improve body size docs 2021-02-15 11:24:46 +00:00
68d1bd88b1 remove unused flag upgrade (#1992) 2021-02-14 18:13:05 +00:00
308b70b039 fix potential over read (#1991) 2021-02-14 17:36:18 +00:00
7fa6333a0c use rcgen for tls key generation (#1989) 2021-02-13 17:16:36 +00:00
3279070f9f optional cookies features (#1981) 2021-02-13 15:08:43 +00:00
b37669cb3b fix notify on drop (#1987) 2021-02-13 04:23:37 +00:00
1e538bf73e rework ci (#1982) 2021-02-12 21:53:21 +00:00
366c032c36 refactor DateService (#1983) 2021-02-12 21:52:58 +00:00
95113ad12f do not self wake up when have a payload (#1984) 2021-02-12 20:33:13 +00:00
ce9b2770e2 remove unused Dispatcher::new_timeout (#1985) 2021-02-12 10:37:28 +00:00
4fc7d76759 s/websocket/WebSocket in docs 2021-02-12 00:27:20 +00:00
81bef93e5e add time parser year shift tests 2021-02-12 00:15:25 +00:00
31d9ed81c5 change rustfmt line width to 96 2021-02-11 23:03:17 +00:00
c1af5089b9 add 431 and 451 status codes 2021-02-11 22:58:40 +00:00
77efc09362 hide httpmessage mod 2021-02-11 22:58:40 +00:00
871ca5e4ae stop claiming actor support 2021-02-11 22:58:40 +00:00
ceace26ed4 remove unused flag POLLED (#1980) 2021-02-11 14:19:14 -08:00
75a9a72e78 clean up poll_response. add comments (#1978) 2021-02-11 14:54:42 +00:00
d9d0d1d1a2 reduce unsafe (#1972) 2021-02-10 23:11:12 +00:00
ea5ce3befb prepare actix-http 3.0.0-beta.3 release 2021-02-10 18:36:14 +00:00
e18464b274 bump actix web versions in deps 2021-02-10 12:57:13 +00:00
bd26083f33 prepare codegen 0.5.0-beta.1 release 2021-02-10 12:45:46 +00:00
991363a104 consistent case s/web/Web 2021-02-10 12:12:03 +00:00
a290e58982 prepare beta 2 release set (#1975) 2021-02-10 12:10:03 +00:00
dcad9724bc ensure poll_flush on h1 connection disconnect (#1974) 2021-02-10 10:11:53 +00:00
949d14ae2b clean up header map (#1964) 2021-02-09 22:59:17 +00:00
a6ed4aee84 add poll_flush after a non blocked write to h1 dispatcher (#1971) 2021-02-09 22:32:46 +00:00
519d7f2b8a add trust-dns optional feature for actix-http and awc (#1969) 2021-02-09 10:41:20 +00:00
dddb623a11 add services register for tuple and vec of services (#1933) 2021-02-07 23:47:51 +00:00
266cf0622c reduce branch.remove deadcode for h1 dispatcher (#1962) 2021-02-07 22:48:27 +00:00
9604e249c9 use stable clippy (#1963) 2021-02-07 20:33:53 +00:00
dbc47c9122 optimize actix-http messages (#1914) 2021-02-07 20:19:10 +00:00
4c243cbf89 simplify methods of awc::connect::Connect trait (#1941) 2021-02-07 18:56:39 +00:00
deafb7c8b8 Improve impl ResponseError documentation (#1939)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-02-07 04:54:41 +00:00
50309aa295 Use askama-escape for html escaping (#1953) 2021-02-07 04:50:23 +00:00
9eaea6a2fd tweak feature flags 2021-02-07 03:54:58 +00:00
830fb2cdb2 properly drop h2 connection (#1926) 2021-02-07 03:51:36 +00:00
7cfed73be8 fix memory usage for h1 and read bug on buffer size. (#1929) 2021-02-07 03:20:35 +00:00
41bc04b1c4 Use immutable reference of service state. Update awc dns resolver. (#1905) 2021-02-07 01:00:40 +00:00
20cf0094e5 fix master branch build. change web::block output type. (#1957) 2021-02-06 16:23:59 +00:00
83fb4978ad fix awc test_client test (#1960) 2021-02-06 16:05:33 +00:00
51e54dac8b fix limit not working on HttpMessageBody::limit (#1938) 2021-01-27 10:49:57 +00:00
c201c15f8c Improve documentation for PayloadConfig (#1923) 2021-01-24 00:32:10 +00:00
0c8196f8b0 Remove HttpResponseBuilder::json2() (#1903)
It's not necessary to keep both json() and json2() around since the
former reduces the ownership of its parameter to a borrow only to pass
the reference to the latter. Users can instead borrow themselves when
passing an owned value: there doesn't need to be two separate functions.

This change also makes HttpResponseBuilder::json() take T: Deref so it
can accept both references and web extractors like web::Json.
2021-01-18 12:14:29 +00:00
ee10148444 revive commented out tests (#1912) 2021-01-17 05:19:32 +00:00
1c95fc2654 Refactor poll_keepalive for readability (#1901) 2021-01-16 00:15:06 +00:00
da69bb4d12 implement App::data as App::app_data(Data::new(T))) (#1906) 2021-01-15 23:37:33 +00:00
0a506bf2e9 cleanup top level doc comments 2021-01-15 05:38:50 +00:00
b2a9ba2ee4 Update PULL_REQUEST_TEMPLATE.md 2021-01-15 04:54:23 +00:00
f976150b67 return option item from Extensions::insert (#1904) 2021-01-15 04:22:42 +00:00
b1dd8d28bc response header rework (#1869) 2021-01-15 02:11:10 +00:00
4edeb5ce47 optimize ErrorHandler middleware (#1902) 2021-01-14 01:43:44 +00:00
d34a8689e5 Refactor h1 encoder (#1900) 2021-01-12 14:38:53 +00:00
a919d2de56 actix-files: Fix If-(Un)Modified to not consider sub-seconds (#1887) 2021-01-11 18:18:23 +00:00
46a8f28b74 fix actix-files doc about thread pool (#1898) 2021-01-11 17:27:33 +00:00
57398c6df1 Refactor/service request (#1893) 2021-01-11 01:29:16 +00:00
7affc6878e simplify h1 dispatcher (#1899)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-01-11 00:13:56 +00:00
46b2f7eaaf use a non leak pool for HttpRequestInner (#1889)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-01-10 22:59:44 +00:00
9e401b6ef7 refactor Scope (#1895) 2021-01-09 18:06:49 +00:00
fe392abeb4 remove actix-threadpool.use actix_rt::task::spawn_blocking (#1878) 2021-01-09 16:04:19 +00:00
f6cc829758 remove leaked box in REQUEST_POOL and RESPONSE_POOL (#1896) 2021-01-09 15:40:20 +00:00
6575ee93f2 big clean up and docs improvmenet of types mod (#1894) 2021-01-09 13:17:19 +00:00
530d03791d refactor Resource (#1883) 2021-01-09 03:36:58 +00:00
d40ae8c8ca use sync method on Responder trait (#1891) 2021-01-08 22:17:19 +00:00
2204614134 don't run awc doctests that rely on external public endpoints (#1888) 2021-01-08 12:00:58 +00:00
188ee44f81 remove copyless dependency (#1884) 2021-01-07 21:55:00 +00:00
a4c9aaf337 fix extra branch in h1 dispatcher timer (#1882) 2021-01-07 20:42:09 +00:00
c09186a2c0 prepare v4 beta releases (#1881) 2021-01-07 20:02:08 +00:00
d3c476b8c2 use env_logger builders in examples 2021-01-07 02:41:05 +00:00
dc23559f23 address clippy lints 2021-01-07 02:04:26 +00:00
6d710629af fix bug where upgrade future is not reset properly (#1880) 2021-01-07 00:57:34 +00:00
85753130d9 fmt 2021-01-07 00:35:19 +00:00
00ba8d5549 add http3 variant to protocol enum 2021-01-06 18:58:24 +00:00
51e9e1500b add docs to recent additions 2021-01-06 18:52:06 +00:00
a03dbe2dcf replace cloneable service with httpflow abstraction (#1876) 2021-01-06 18:43:52 +00:00
57a3722146 More refactor of app_service (#1879) 2021-01-06 18:11:20 +00:00
57da1d3c0f refactor app_service (#1877) 2021-01-06 11:35:30 +00:00
68117543ea major cleanup of middleware module (#1875)
* major cleanup of middleware module

* update changelog
2021-01-05 09:51:58 +00:00
4f5971d79e add Compat middleware (#1865) 2021-01-05 00:22:57 +00:00
93161df141 clean up body type (#1872) 2021-01-04 23:47:38 +00:00
e567873326 optimize message pool release (#1871) 2021-01-04 13:03:46 +00:00
7d632d0b7b use ByteString as container for websocket text message (#1864) 2021-01-04 11:27:32 +00:00
36aee18c64 fmt 2021-01-04 04:33:15 +00:00
007a145988 use ahash for internal hashmaps 2021-01-04 04:29:07 +00:00
2d4a174420 fmt 2021-01-04 01:01:35 +00:00
21f6c9d7a5 improve code readability 2021-01-04 00:49:02 +00:00
e1683313ec optimize ServiceRequest methods (#1870)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-01-04 00:32:41 +00:00
32de9f8840 Tokio 1.0 (#1813)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-01-03 23:47:04 +00:00
1f202d40e4 optimize write_camel_case in h1 encoder (#1868) 2021-01-03 16:53:01 +00:00
ad608aa64e optimize Resource and Scope service call (#1867) 2021-01-02 19:40:31 +00:00
a1b00b2cd0 change unreleased year 2021-01-02 00:12:18 +00:00
3beb4cf2da replace tinyvec with smallvec (#1866) 2021-01-01 23:18:25 +00:00
522c9a5ea6 update CoC text 2020-12-31 03:24:18 +00:00
102bb8f9ab update dot dep graphs 2020-12-29 00:22:28 +00:00
20b46cdaf9 format factory_tuple macro invocations (#1859) 2020-12-28 21:04:02 +00:00
2a2a20c3e7 bump msrv to 1.46 (#1858) 2020-12-28 00:44:15 +00:00
093d3a6c59 remove deprecated on_connect methods (#1857) 2020-12-27 23:23:30 +00:00
8c9ea43e23 address clippy warnings 2020-12-27 20:54:04 +00:00
f9fcf56d5c reduce branch in actix_http::h1::codec (#1854) 2020-12-27 20:37:53 +00:00
cbda928a33 Rename factory to handler (#1852) 2020-12-26 21:46:19 +00:00
1032f04ded remove unused actix_http::h1::OneRequest (#1853) 2020-12-26 12:46:36 +00:00
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
227 changed files with 13855 additions and 11846 deletions

View File

@ -1,21 +1,21 @@
<!-- Thanks for considering contributing actix! -->
<!-- Please fill out the following to make our reviews easy. -->
<!-- Please fill out the following to get your PR reviewed quicker. -->
## PR Type
<!-- What kind of change does this PR make? -->
<!-- Bug Fix / Feature / Refactor / Code Style / Other -->
INSERT_PR_TYPE
PR_TYPE
## PR Checklist
Check your PR fulfills the following:
<!-- Check your PR fulfills the following items. ->>
<!-- For draft PRs check the boxes as you complete them. -->
- [ ] Tests for the changes have been added / updated.
- [ ] Documentation comments have been added / updated.
- [ ] A changelog entry has been made for the appropriate packages.
- [ ] Format code with the latest stable rustfmt
- [ ] Format code with the latest stable rustfmt.
- [ ] (Team) Label with affected crates and semver status.
## Overview

View File

@ -1,4 +1,4 @@
name: Benchmark (Linux)
name: Benchmark
on:
pull_request:

103
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,103 @@
name: CI
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [master]
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
target:
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
version:
- 1.46.0 # MSRV
- stable
- nightly
name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }}
steps:
- uses: actions/checkout@v2
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: Install cargo-hack
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-hack
- name: check minimal
uses: actions-rs/cargo@v1
with:
command: hack
args: --clean-per-run check --workspace --no-default-features --tests
- name: check full
uses: actions-rs/cargo@v1
with:
command: check
args: --workspace --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: -v --workspace --all-features --no-fail-fast -- --nocapture
--skip=test_h2_content_length
--skip=test_reading_deflate_encoding_large_random_rustls
- name: tests (actix-http)
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --package=actix-http --no-default-features --features=rustls -- --nocapture
- name: tests (awc)
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --package=awc --no-default-features --features=rustls -- --nocapture
- name: Generate coverage file
if: >
matrix.target.os == 'ubuntu-latest'
&& matrix.version == 'stable'
&& github.ref == 'refs/heads/master'
run: |
cargo install cargo-tarpaulin --vers "^0.13"
cargo tarpaulin --out Xml --verbose
- name: Upload to Codecov
if: >
matrix.target.os == 'ubuntu-latest'
&& matrix.version == 'stable'
&& github.ref == 'refs/heads/master'
uses: codecov/codecov-action@v1
with:
file: cobertura.xml
- name: Clear the cargo caches
run: |
cargo install cargo-cache --no-default-features --features ci-autoclean
cargo-cache

View File

@ -1,32 +1,39 @@
name: Lint
on:
pull_request:
types: [opened, synchronize, reopened]
name: Clippy and rustfmt Check
jobs:
clippy_check:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
components: rustfmt
override: true
- name: Check with rustfmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- uses: actions-rs/toolchain@v1
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
toolchain: stable
components: clippy
override: true
- name: Check with Clippy
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features --all --tests
args: --workspace --tests --all-features

View File

@ -1,69 +0,0 @@
name: CI (Linux)
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
- 1.42.0 # MSRV
- stable
- nightly
name: ${{ matrix.version }} - x86_64-unknown-linux-gnu
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu
profile: minimal
override: true
- name: check build
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --all --all-features --no-fail-fast -- --nocapture
- name: tests (actix-http)
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --package=actix-http --no-default-features --features=rustls -- --nocapture
- name: tests (awc)
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --package=awc --no-default-features --features=rustls -- --nocapture
- name: Generate coverage file
if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
run: |
cargo install cargo-tarpaulin --vers "^0.13"
cargo tarpaulin --out Xml
- name: Upload to Codecov
if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
uses: codecov/codecov-action@v1
with:
file: cobertura.xml

View File

@ -1,44 +0,0 @@
name: CI (macOS)
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
- stable
- nightly
name: ${{ matrix.version }} - x86_64-apple-darwin
runs-on: macOS-latest
steps:
- uses: actions/checkout@v2
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-x86_64-apple-darwin
profile: minimal
override: true
- name: check build
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all --all-features --no-fail-fast -- --nocapture
--skip=test_h2_content_length
--skip=test_reading_deflate_encoding_large_random_rustls

View File

@ -1,14 +1,12 @@
name: Upload documentation
name: Upload Documentation
on:
push:
branches:
- master
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
if: github.repository == 'actix/actix-web'
steps:
- uses: actions/checkout@v2
@ -20,14 +18,14 @@ jobs:
profile: minimal
override: true
- name: check build
- name: Build Docs
uses: actions-rs/cargo@v1
with:
command: doc
args: --no-deps --workspace --all-features
args: --workspace --all-features --no-deps
- name: Tweak HTML
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=actix_web/index.html">' > target/doc/index.html
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@3.7.1

View File

@ -1,64 +0,0 @@
name: CI (Windows)
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master
env:
VCPKGRS_DYNAMIC: 1
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
- stable
- nightly
name: ${{ matrix.version }} - x86_64-pc-windows-msvc
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-x86_64-pc-windows-msvc
profile: minimal
override: true
- name: Install OpenSSL
run: |
vcpkg integrate install
vcpkg install openssl:x64-windows
Copy-Item C:\vcpkg\installed\x64-windows\bin\libcrypto-1_1-x64.dll C:\vcpkg\installed\x64-windows\bin\libcrypto.dll
Copy-Item C:\vcpkg\installed\x64-windows\bin\libssl-1_1-x64.dll C:\vcpkg\installed\x64-windows\bin\libssl.dll
Get-ChildItem C:\vcpkg\installed\x64-windows\bin
Get-ChildItem C:\vcpkg\installed\x64-windows\lib
- name: check build
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all --all-features --no-fail-fast -- --nocapture
--skip=test_h2_content_length
--skip=test_reading_deflate_encoding_large_random_rustls
--skip=test_params
--skip=test_simple
--skip=test_expect_continue
--skip=test_http10_keepalive
--skip=test_slow_request
--skip=test_connection_force_close
--skip=test_connection_server_close
--skip=test_connection_wait_queue_force_close

View File

@ -1,12 +1,116 @@
# Changes
## Unreleased - 2020-xx-xx
## Unreleased - 2021-xx-xx
## 4.0.0-beta.4 - 2021-03-09
### Changed
* Feature `cookies` is now optional and enabled by default. [#1981]
* `JsonBody::new` returns a default limit of 32kB to be consistent with `JsonConfig` and the default
behaviour of the `web::Json<T>` extractor. [#2010]
[#1981]: https://github.com/actix/actix-web/pull/1981
[#2010]: https://github.com/actix/actix-web/pull/2010
## 4.0.0-beta.3 - 2021-02-10
* Update `actix-web-codegen` to `0.5.0-beta.1`.
## 4.0.0-beta.2 - 2021-02-10
### Added
* The method `Either<web::Json<T>, web::Form<T>>::into_inner()` which returns the inner type for
whichever variant was created. Also works for `Either<web::Form<T>, web::Json<T>>`. [#1894]
* Add `services!` macro for helping register multiple services to `App`. [#1933]
* Enable registering a vec of services of the same type to `App` [#1933]
### Changed
* Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly.
Making it simpler and more performant. [#1891]
* `ServiceRequest::into_parts` and `ServiceRequest::from_parts` can no longer fail. [#1893]
* `ServiceRequest::from_request` can no longer fail. [#1893]
* Our `Either` type now uses `Left`/`Right` variants (instead of `A`/`B`) [#1894]
* `test::{call_service, read_response, read_response_json, send_request}` take `&Service`
in argument [#1905]
* `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` provide `&Service` in closure
argument. [#1905]
* `web::block` no longer requires the output is a Result. [#1957]
### Fixed
* Multiple calls to `App::data` with the same type now keeps the latest call's data. [#1906]
### Removed
* Public field of `web::Path` has been made private. [#1894]
* Public field of `web::Query` has been made private. [#1894]
* `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869]
* `AppService::set_service_data`; for custom HTTP service factories adding application data, use the
layered data model by calling `ServiceRequest::add_data_container` when handling
requests instead. [#1906]
[#1891]: https://github.com/actix/actix-web/pull/1891
[#1893]: https://github.com/actix/actix-web/pull/1893
[#1894]: https://github.com/actix/actix-web/pull/1894
[#1869]: https://github.com/actix/actix-web/pull/1869
[#1905]: https://github.com/actix/actix-web/pull/1905
[#1906]: https://github.com/actix/actix-web/pull/1906
[#1933]: https://github.com/actix/actix-web/pull/1933
[#1957]: https://github.com/actix/actix-web/pull/1957
## 4.0.0-beta.1 - 2021-01-07
### Added
* `Compat` middleware enabling generic response body/error type of middlewares like `Logger` and
`Compress` to be used in `middleware::Condition` and `Resource`, `Scope` services. [#1865]
### Changed
* Update `actix-*` dependencies to tokio `1.0` based versions. [#1813]
* Bumped `rand` to `0.8`.
* Update `rust-tls` to `0.19`. [#1813]
* Rename `Handler` to `HandlerService` and rename `Factory` to `Handler`. [#1852]
* The default `TrailingSlash` is now `Trim`, in line with existing documentation. See migration
guide for implications. [#1875]
* Rename `DefaultHeaders::{content_type => add_content_type}`. [#1875]
* MSRV is now 1.46.0.
### Fixed
* Added the underlying parse error to `test::read_body_json`'s panic message. [#1812]
### Removed
* Public modules `middleware::{normalize, err_handlers}`. All necessary middleware structs are now
exposed directly by the `middleware` module.
* Remove `actix-threadpool` as dependency. `actix_threadpool::BlockingError` error type can be imported
from `actix_web::error` module. [#1878]
[#1812]: https://github.com/actix/actix-web/pull/1812
[#1813]: https://github.com/actix/actix-web/pull/1813
[#1852]: https://github.com/actix/actix-web/pull/1852
[#1865]: https://github.com/actix/actix-web/pull/1865
[#1875]: https://github.com/actix/actix-web/pull/1875
[#1878]: https://github.com/actix/actix-web/pull/1878
## 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`.
* 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
@ -67,7 +171,7 @@
## 3.0.0-beta.4 - 2020-09-09
### Added
* `middleware::NormalizePath` now has configurable behaviour for either always having a trailing
* `middleware::NormalizePath` now has configurable behavior for either always having a trailing
slash, or as the new addition, always trimming trailing slashes. [#1639]
### Changed
@ -395,7 +499,7 @@
## [1.0.0-rc] - 2019-05-18
### Add
### Added
* Add `Query<T>::from_query()` to extract parameters from a query string. #846
* `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors.
@ -411,7 +515,7 @@
## [1.0.0-beta.4] - 2019-05-12
### Add
### Added
* Allow to set/override app data on scope level
@ -437,7 +541,7 @@
* CORS handling without headers #702
* Allow to construct `Data` instances to avoid double `Arc` for `Send + Sync` types.
* Allow constructing `Data` instances to avoid double `Arc` for `Send + Sync` types.
### Fixed
@ -501,7 +605,7 @@
### Changed
* Allow to use any service as default service.
* Allow using any service as default service.
* Remove generic type for request payload, always use default.
@ -564,13 +668,13 @@
### Added
* rustls support
* Rustls support
### Changed
* use forked cookie
* Use forked cookie
* multipart::Field renamed to MultipartField
* Multipart::Field renamed to MultipartField
## [1.0.0-alpha.1] - 2019-03-28

View File

@ -1,8 +1,8 @@
[package]
name = "actix-web"
version = "3.2.0"
version = "4.0.0-beta.4"
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"
keywords = ["actix", "http", "web", "framework", "async"]
homepage = "https://actix.rs"
@ -15,6 +15,7 @@ license = "MIT OR Apache-2.0"
edition = "2018"
[package.metadata.docs.rs]
# features that docs.rs will build with
features = ["openssl", "rustls", "compress", "secure-cookies"]
[badges]
@ -34,23 +35,26 @@ members = [
"actix-multipart",
"actix-web-actors",
"actix-web-codegen",
"test-server",
"actix-http-test",
]
[features]
default = ["compress"]
default = ["compress", "cookies"]
# content-encoding support
compress = ["actix-http/compress", "awc/compress"]
# sessions feature
# support for cookies
cookies = ["actix-http/cookies", "awc/cookies"]
# secure cookies feature
secure-cookies = ["actix-http/secure-cookies"]
# openssl
openssl = ["actix-tls/openssl", "awc/openssl", "open-ssl"]
openssl = ["tls-openssl", "actix-tls/accept", "actix-tls/openssl", "awc/openssl"]
# rustls
rustls = ["actix-tls/rustls", "awc/rustls", "rust-tls"]
rustls = ["tls-rustls", "actix-tls/accept", "actix-tls/rustls", "awc/rustls"]
[[example]]
name = "basic"
@ -62,7 +66,7 @@ required-features = ["compress"]
[[test]]
name = "test_server"
required-features = ["compress"]
required-features = ["compress", "cookies"]
[[example]]
name = "on_connect"
@ -73,51 +77,54 @@ name = "client"
required-features = ["rustls"]
[dependencies]
actix-codec = "0.3.0"
actix-service = "1.0.6"
actix-utils = "2.0.0"
actix-router = "0.2.4"
actix-rt = "1.1.1"
actix-server = "1.0.0"
actix-testing = "1.0.0"
actix-macros = "0.1.0"
actix-threadpool = "0.3.1"
actix-tls = "2.0.0"
actix-codec = "0.4.0-beta.1"
actix-macros = "0.2.0"
actix-router = "0.2.7"
actix-rt = "2.1"
actix-server = "2.0.0-beta.3"
actix-service = "2.0.0-beta.4"
actix-utils = "3.0.0-beta.2"
actix-tls = { version = "3.0.0-beta.4", default-features = false, optional = true }
actix-web-codegen = "0.4.0"
actix-http = "2.1.0"
awc = { version = "2.0.0", default-features = false }
actix-web-codegen = "0.5.0-beta.2"
actix-http = "3.0.0-beta.4"
awc = { version = "3.0.0-beta.3", default-features = false }
bytes = "0.5.3"
derive_more = "0.99.2"
ahash = "0.7"
bytes = "1"
derive_more = "0.99.5"
either = "1.5.3"
encoding_rs = "0.8"
futures-channel = { version = "0.3.5", default-features = false }
futures-core = { version = "0.3.5", default-features = false }
futures-util = { version = "0.3.5", default-features = false }
fxhash = "0.2.1"
futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false }
log = "0.4"
mime = "0.3"
socket2 = "0.3"
pin-project = "1.0.0"
regex = "1.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_urlencoded = "0.7"
time = { version = "0.2.7", default-features = false, features = ["std"] }
smallvec = "1.6"
socket2 = "0.3.16"
time = { version = "0.2.23", default-features = false, features = ["std"] }
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
tls-rustls = { package = "rustls", version = "0.19.0", optional = true }
url = "2.1"
open-ssl = { package = "openssl", version = "0.10", optional = true }
rust-tls = { package = "rustls", version = "0.18.0", optional = true }
tinyvec = { version = "1", features = ["alloc"] }
[target.'cfg(windows)'.dependencies.tls-openssl]
version = "0.10.9"
package = "openssl"
features = ["vendored"]
optional = true
[dev-dependencies]
actix = "0.10.0"
actix-http = { version = "2.1.0", features = ["actors"] }
rand = "0.7"
env_logger = "0.8"
serde_derive = "1.0"
brotli2 = "0.3.2"
flate2 = "1.0.13"
criterion = "0.3"
env_logger = "0.8"
flate2 = "1.0.13"
rand = "0.8"
rcgen = "0.8"
serde_derive = "1.0"
[profile.release]
lto = true
@ -127,10 +134,11 @@ codegen-units = 1
[patch.crates-io]
actix-web = { path = "." }
actix-http = { path = "actix-http" }
actix-http-test = { path = "test-server" }
actix-http-test = { path = "actix-http-test" }
actix-web-actors = { path = "actix-web-actors" }
actix-web-codegen = { path = "actix-web-codegen" }
actix-files = { path = "actix-files" }
actix-multipart = { path = "actix-multipart" }
actix-files = { path = "actix-files" }
awc = { path = "awc" }
[[bench]]
@ -140,3 +148,7 @@ harness = false
[[bench]]
name = "service"
harness = false
[[bench]]
name = "responder"
harness = false

View File

@ -1,5 +1,15 @@
## Unreleased
* The default `NormalizePath` behavior now strips trailing slashes by default. This was
previously documented to be the case in v3 but the behavior now matches. The effect is that
routes defined with trailing slashes will become inaccessible when
using `NormalizePath::default()`.
Before: `#[get("/test/")`
After: `#[get("/test")`
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
## 3.0.0

View File

@ -1,20 +1,19 @@
<div align="center">
<h1>Actix web</h1>
<h1>Actix Web</h1>
<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>
[![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?version=3.2.0)](https://docs.rs/actix-web/3.2.0)
[![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)
[![Dependency Status](https://deps.rs/crate/actix-web/3.2.0/status.svg)](https://deps.rs/crate/actix-web/3.2.0)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.4)](https://docs.rs/actix-web/4.0.0-beta.4)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.4)
<br />
[![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web)
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions)
[![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)
[![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)
![downloads](https://img.shields.io/crates/d/actix-web.svg)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
</p>
@ -32,9 +31,8 @@
* Static assets
* SSL support using OpenSSL or Rustls
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
* Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
* Supports [Actix actor framework](https://github.com/actix/actix)
* Runs on stable Rust 1.42+
* Includes an async [HTTP client](https://docs.rs/actix-web/latest/actix_web/client/index.html)
* Runs on stable Rust 1.46+
## Documentation
@ -73,18 +71,18 @@ async fn main() -> std::io::Result<()> {
### More examples
* [Basic Setup](https://github.com/actix/examples/tree/master/basics/)
* [Application State](https://github.com/actix/examples/tree/master/state/)
* [JSON Handling](https://github.com/actix/examples/tree/master/json/)
* [Multipart Streams](https://github.com/actix/examples/tree/master/multipart/)
* [Diesel Integration](https://github.com/actix/examples/tree/master/diesel/)
* [r2d2 Integration](https://github.com/actix/examples/tree/master/r2d2/)
* [Simple WebSocket](https://github.com/actix/examples/tree/master/websocket/)
* [Tera Templates](https://github.com/actix/examples/tree/master/template_tera/)
* [Askama Templates](https://github.com/actix/examples/tree/master/template_askama/)
* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/rustls/)
* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/openssl/)
* [WebSocket Chat](https://github.com/actix/examples/tree/master/websocket-chat/)
* [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics/)
* [Application State](https://github.com/actix/examples/tree/master/basics/state/)
* [JSON Handling](https://github.com/actix/examples/tree/master/json/json/)
* [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart/)
* [Diesel Integration](https://github.com/actix/examples/tree/master/database_interactions/diesel/)
* [r2d2 Integration](https://github.com/actix/examples/tree/master/database_interactions/r2d2/)
* [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets/websocket/)
* [Tera Templates](https://github.com/actix/examples/tree/master/template_engines/tera/)
* [Askama Templates](https://github.com/actix/examples/tree/master/template_engines/askama/)
* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/security/rustls/)
* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/)
* [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/)
You may consider checking out
[this directory](https://github.com/actix/examples/tree/master/) for more examples.
@ -99,13 +97,13 @@ One of the fastest web frameworks available according to the
This project is licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
[http://www.apache.org/licenses/LICENSE-2.0])
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
[http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
[http://opensource.org/licenses/MIT])
at your option.
## Code of Conduct
Contribution to the actix-web crate is organized under the terms of the Contributor Covenant, the
maintainers of Actix web, promises to intervene to uphold that code of conduct.
Contribution to the actix-web repo is organized under the terms of the Contributor Covenant.
The Actix team promises to intervene to uphold that code of conduct.

View File

@ -1,6 +1,31 @@
# Changes
## Unreleased - 2020-xx-xx
## Unreleased - 2021-xx-xx
## 0.6.0-beta.3 - 2021-03-09
* No notable changes.
## 0.6.0-beta.2 - 2021-02-10
* Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887]
* Replace `v_htmlescape` with `askama_escape`. [#1953]
[#1887]: https://github.com/actix/actix-web/pull/1887
[#1953]: https://github.com/actix/actix-web/pull/1953
## 0.6.0-beta.1 - 2021-01-07
* `HttpRange::parse` now has its own error type.
* Update `bytes` to `1.0`. [#1813]
[#1813]: https://github.com/actix/actix-web/pull/1813
## 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

View File

@ -1,6 +1,6 @@
[package]
name = "actix-files"
version = "0.4.1"
version = "0.6.0-beta.3"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Static file serving for Actix Web"
readme = "README.md"
@ -17,19 +17,21 @@ name = "actix_files"
path = "src/lib.rs"
[dependencies]
actix-web = { version = "3.0.0", default-features = false }
actix-service = "1.0.6"
actix-web = { version = "4.0.0-beta.4", default-features = false }
actix-service = "2.0.0-beta.4"
askama_escape = "0.10"
bitflags = "1"
bytes = "0.5.3"
bytes = "1"
futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false }
derive_more = "0.99.2"
http-range = "0.1.4"
derive_more = "0.99.5"
log = "0.4"
mime = "0.3"
mime_guess = "2.0.1"
percent-encoding = "2.1"
v_htmlescape = "0.11"
[dev-dependencies]
actix-rt = "1.0.0"
actix-web = { version = "3.0.0", features = ["openssl"] }
actix-rt = "2.1"
actix-web = "4.0.0-beta.4"

View File

@ -3,17 +3,17 @@
> Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.4.1)](https://docs.rs/actix-files/0.4.1)
[![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.3)](https://docs.rs/actix-files/0.6.0-beta.3)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
![License](https://img.shields.io/crates/l/actix-files.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-files/0.4.1/status.svg)](https://deps.rs/crate/actix-files/0.4.1)
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.3/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.3)
[![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)
- [Example Project](https://github.com/actix/examples/tree/master/basics/static_index)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum supported Rust version: 1.42 or later
- Minimum supported Rust version: 1.46 or later

View File

@ -9,26 +9,35 @@ use std::{
use actix_web::{
error::{BlockingError, Error},
web,
rt::task::{spawn_blocking, JoinHandle},
};
use bytes::Bytes;
use futures_core::{ready, Stream};
use futures_util::future::{FutureExt, LocalBoxFuture};
use crate::handle_error;
type ChunkedBoxFuture =
LocalBoxFuture<'static, Result<(File, Bytes), BlockingError<io::Error>>>;
#[doc(hidden)]
/// A helper created from a `std::fs::File` which reads the file
/// chunk-by-chunk on a `ThreadPool`.
pub struct ChunkedReadFile {
pub(crate) size: u64,
pub(crate) offset: u64,
pub(crate) file: Option<File>,
pub(crate) fut: Option<ChunkedBoxFuture>,
pub(crate) counter: u64,
size: u64,
offset: u64,
state: ChunkedReadFileState,
counter: u64,
}
enum ChunkedReadFileState {
File(Option<File>),
Future(JoinHandle<Result<(File, Bytes), io::Error>>),
}
impl ChunkedReadFile {
pub(crate) fn new(size: u64, offset: u64, file: File) -> Self {
Self {
size,
offset,
state: ChunkedReadFileState::File(Some(file)),
counter: 0,
}
}
}
impl fmt::Debug for ChunkedReadFile {
@ -40,38 +49,23 @@ impl fmt::Debug for ChunkedReadFile {
impl Stream for ChunkedReadFile {
type Item = Result<Bytes, Error>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
if let Some(ref mut fut) = self.fut {
return match ready!(Pin::new(fut).poll(cx)) {
Ok((file, bytes)) => {
self.fut.take();
self.file = Some(file);
self.offset += bytes.len() as u64;
self.counter += bytes.len() as u64;
Poll::Ready(Some(Ok(bytes)))
}
Err(e) => Poll::Ready(Some(Err(handle_error(e)))),
};
}
let size = self.size;
let offset = self.offset;
let counter = self.counter;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.as_mut().get_mut();
match this.state {
ChunkedReadFileState::File(ref mut file) => {
let size = this.size;
let offset = this.offset;
let counter = this.counter;
if size == counter {
Poll::Ready(None)
} else {
let mut file = self.file.take().expect("Use after completion");
let mut file = file
.take()
.expect("ChunkedReadFile polled after completion");
self.fut = Some(
web::block(move || {
let max_bytes =
cmp::min(size.saturating_sub(counter), 65_536) as usize;
let fut = spawn_blocking(move || {
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
let mut buf = Vec::with_capacity(max_bytes);
file.seek(io::SeekFrom::Start(offset))?;
@ -84,11 +78,21 @@ impl Stream for ChunkedReadFile {
}
Ok((file, Bytes::from(buf)))
})
.boxed_local(),
);
});
this.state = ChunkedReadFileState::Future(fut);
self.poll_next(cx)
}
}
ChunkedReadFileState::Future(ref mut fut) => {
let (file, bytes) =
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
this.state = ChunkedReadFileState::File(Some(file));
this.offset += bytes.len() as u64;
this.counter += bytes.len() as u64;
Poll::Ready(Some(Ok(bytes)))
}
}
}
}

View File

@ -1,8 +1,8 @@
use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf};
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
use askama_escape::{escape as escape_html_entity, Html};
use percent_encoding::{utf8_percent_encode, CONTROLS};
use v_htmlescape::escape as escape_html_entity;
/// A directory; responds with the generated directory listing.
#[derive(Debug)]
@ -50,7 +50,7 @@ macro_rules! encode_file_url {
// " -- &quot; & -- &amp; ' -- &#x27; < -- &lt; > -- &gt; / -- &#x2f;
macro_rules! encode_file_name {
($entry:ident) => {
escape_html_entity(&$entry.file_name().to_string_lossy())
escape_html_entity(&$entry.file_name().to_string_lossy(), Html)
};
}
@ -66,9 +66,7 @@ pub(crate) fn directory_listing(
if dir.is_visible(&entry) {
let entry = entry.unwrap();
let p = match entry.path().strip_prefix(&dir.path) {
Ok(p) if cfg!(windows) => {
base.join(p).to_string_lossy().replace("\\", "/")
}
Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace("\\", "/"),
Ok(p) => base.join(p).to_string_lossy().into_owned(),
Err(_) => continue,
};

View File

@ -1,10 +1,8 @@
use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc};
use actix_service::{boxed, IntoServiceFactory, ServiceFactory};
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
use actix_web::{
dev::{
AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse,
},
dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse},
error::Error,
guard::Guard,
http::header::DispositionType,
@ -13,8 +11,8 @@ use actix_web::{
use futures_util::future::{ok, FutureExt, LocalBoxFuture};
use crate::{
directory_listing, named, Directory, DirectoryRenderer, FilesService,
HttpNewService, MimeOverride,
directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService,
MimeOverride,
};
/// Static files handling service.
@ -39,6 +37,7 @@ pub struct Files {
mime_override: Option<Rc<MimeOverride>>,
file_flags: named::Flags,
guards: Option<Rc<dyn Guard>>,
hidden_files: bool,
}
impl fmt::Debug for Files {
@ -60,6 +59,7 @@ impl Clone for Files {
path: self.path.clone(),
mime_override: self.mime_override.clone(),
guards: self.guards.clone(),
hidden_files: self.hidden_files,
}
}
}
@ -80,8 +80,9 @@ impl Files {
/// 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.
/// max number of threads equal to `512 * HttpServer::worker`. Real time thread count are
/// adjusted with work load. More threads would spawn when need and threads goes idle for a
/// period of time would be de-spawned.
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() {
@ -103,6 +104,7 @@ impl Files {
mime_override: None,
file_flags: named::Flags::default(),
guards: None,
hidden_files: false,
}
}
@ -125,8 +127,8 @@ impl Files {
/// Set custom directory renderer
pub fn files_listing_renderer<F>(mut self, f: F) -> Self
where
for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error>
+ 'static,
for<'r, 's> F:
Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> + 'static,
{
self.renderer = Rc::new(f);
self
@ -198,10 +200,10 @@ impl Files {
/// Sets default handler which is used when no matched file could be found.
pub fn default_handler<F, U>(mut self, f: F) -> Self
where
F: IntoServiceFactory<U>,
F: IntoServiceFactory<U, ServiceRequest>,
U: ServiceFactory<
ServiceRequest,
Config = (),
Request = ServiceRequest,
Response = ServiceResponse,
Error = Error,
> + 'static,
@ -213,6 +215,13 @@ impl Files {
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 {
@ -231,8 +240,7 @@ impl HttpServiceFactory for Files {
}
}
impl ServiceFactory for Files {
type Request = ServiceRequest;
impl ServiceFactory<ServiceRequest> for Files {
type Response = ServiceResponse;
type Error = Error;
type Config = ();
@ -251,6 +259,7 @@ impl ServiceFactory for Files {
mime_override: self.mime_override.clone(),
file_flags: self.file_flags,
guards: self.guards.clone(),
hidden_files: self.hidden_files,
};
if let Some(ref default) = *self.default.borrow() {

View File

@ -14,12 +14,10 @@
#![deny(rust_2018_idioms)]
#![warn(missing_docs, missing_debug_implementations)]
use std::io;
use actix_service::boxed::{BoxService, BoxServiceFactory};
use actix_web::{
dev::{ServiceRequest, ServiceResponse},
error::{BlockingError, Error, ErrorInternalServerError},
error::Error,
http::header::DispositionType,
};
use mime_guess::from_ext;
@ -56,13 +54,6 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
from_ext(ext).first_or_octet_stream()
}
pub(crate) fn handle_error(err: BlockingError<io::Error>) -> Error {
match err {
BlockingError::Error(err) => err.into(),
BlockingError::Canceled => ErrorInternalServerError("Unexpected error"),
}
}
type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
#[cfg(test)]
@ -82,7 +73,8 @@ mod tests {
},
middleware::Compress,
test::{self, TestRequest},
web, App, HttpResponse, Responder,
web::{self, Bytes},
App, HttpResponse, Responder,
};
use futures_util::future::ok;
@ -106,11 +98,22 @@ mod tests {
#[actix_rt::test]
async fn test_if_modified_since_without_if_none_match() {
let file = NamedFile::open("Cargo.toml").unwrap();
let since =
header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default()
.header(header::IF_MODIFIED_SINCE, since)
.insert_header((header::IF_MODIFIED_SINCE, since))
.to_http_request();
let resp = file.respond_to(&req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
}
#[actix_rt::test]
async fn test_if_modified_since_without_if_none_match_same() {
let file = NamedFile::open("Cargo.toml").unwrap();
let since = file.last_modified().unwrap();
let req = TestRequest::default()
.insert_header((header::IF_MODIFIED_SINCE, since))
.to_http_request();
let resp = file.respond_to(&req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
@ -119,17 +122,40 @@ mod tests {
#[actix_rt::test]
async fn test_if_modified_since_with_if_none_match() {
let file = NamedFile::open("Cargo.toml").unwrap();
let since =
header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default()
.header(header::IF_NONE_MATCH, "miss_etag")
.header(header::IF_MODIFIED_SINCE, since)
.insert_header((header::IF_NONE_MATCH, "miss_etag"))
.insert_header((header::IF_MODIFIED_SINCE, since))
.to_http_request();
let resp = file.respond_to(&req).await.unwrap();
assert_ne!(resp.status(), StatusCode::NOT_MODIFIED);
}
#[actix_rt::test]
async fn test_if_unmodified_since() {
let file = NamedFile::open("Cargo.toml").unwrap();
let since = file.last_modified().unwrap();
let req = TestRequest::default()
.insert_header((header::IF_UNMODIFIED_SINCE, since))
.to_http_request();
let resp = file.respond_to(&req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_if_unmodified_since_failed() {
let file = NamedFile::open("Cargo.toml").unwrap();
let since = header::HttpDate::from(SystemTime::UNIX_EPOCH);
let req = TestRequest::default()
.insert_header((header::IF_UNMODIFIED_SINCE, since))
.to_http_request();
let resp = file.respond_to(&req).await.unwrap();
assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED);
}
#[actix_rt::test]
async fn test_named_file_text() {
assert!(NamedFile::open("test--").is_err());
@ -184,8 +210,7 @@ mod tests {
#[actix_rt::test]
async fn test_named_file_non_ascii_file_name() {
let mut file =
NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml")
.unwrap();
NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml").unwrap();
{
file.file();
let _f: &File = &file;
@ -338,7 +363,7 @@ mod tests {
DispositionType::Attachment
}
let mut srv = test::init_service(
let srv = test::init_service(
App::new().service(
Files::new("/", ".")
.mime_override(all_attachment)
@ -348,7 +373,7 @@ mod tests {
.await;
let request = TestRequest::get().uri("/").to_request();
let response = test::call_service(&mut srv, request).await;
let response = test::call_service(&srv, request).await;
assert_eq!(response.status(), StatusCode::OK);
let content_disposition = response
@ -363,7 +388,7 @@ mod tests {
#[actix_rt::test]
async fn test_named_file_ranges_status_code() {
let mut srv = test::init_service(
let srv = test::init_service(
App::new().service(Files::new("/test", ".").index_file("Cargo.toml")),
)
.await;
@ -371,17 +396,17 @@ mod tests {
// Valid range header
let request = TestRequest::get()
.uri("/t%65st/Cargo.toml")
.header(header::RANGE, "bytes=10-20")
.insert_header((header::RANGE, "bytes=10-20"))
.to_request();
let response = test::call_service(&mut srv, request).await;
let response = test::call_service(&srv, request).await;
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
// Invalid range header
let request = TestRequest::get()
.uri("/t%65st/Cargo.toml")
.header(header::RANGE, "bytes=1-0")
.insert_header((header::RANGE, "bytes=1-0"))
.to_request();
let response = test::call_service(&mut srv, request).await;
let response = test::call_service(&srv, request).await;
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
}
@ -393,7 +418,7 @@ mod tests {
// Valid range header
let response = srv
.get("/tests/test.binary")
.header(header::RANGE, "bytes=10-20")
.insert_header((header::RANGE, "bytes=10-20"))
.send()
.await
.unwrap();
@ -403,7 +428,7 @@ mod tests {
// Invalid range header
let response = srv
.get("/tests/test.binary")
.header(header::RANGE, "bytes=10-5")
.insert_header((header::RANGE, "bytes=10-5"))
.send()
.await
.unwrap();
@ -418,7 +443,7 @@ mod tests {
// Valid range header
let response = srv
.get("/tests/test.binary")
.header(header::RANGE, "bytes=10-20")
.insert_header((header::RANGE, "bytes=10-20"))
.send()
.await
.unwrap();
@ -428,7 +453,7 @@ mod tests {
// Valid range header, starting from 0
let response = srv
.get("/tests/test.binary")
.header(header::RANGE, "bytes=0-20")
.insert_header((header::RANGE, "bytes=0-20"))
.send()
.await
.unwrap();
@ -468,14 +493,14 @@ mod tests {
#[actix_rt::test]
async fn test_static_files_with_spaces() {
let mut srv = test::init_service(
let srv = test::init_service(
App::new().service(Files::new("/", ".").index_file("Cargo.toml")),
)
.await;
let request = TestRequest::get()
.uri("/tests/test%20space.binary")
.to_request();
let response = test::call_service(&mut srv, request).await;
let response = test::call_service(&srv, request).await;
assert_eq!(response.status(), StatusCode::OK);
let bytes = test::read_body(response).await;
@ -485,28 +510,28 @@ mod tests {
#[actix_rt::test]
async fn test_files_not_allowed() {
let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await;
let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
let req = TestRequest::default()
.uri("/Cargo.toml")
.method(Method::POST)
.to_request();
let resp = test::call_service(&mut srv, req).await;
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await;
let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
let req = TestRequest::default()
.method(Method::PUT)
.uri("/Cargo.toml")
.to_request();
let resp = test::call_service(&mut srv, req).await;
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
}
#[actix_rt::test]
async fn test_files_guards() {
let mut srv = test::init_service(
let srv = test::init_service(
App::new().service(Files::new("/", ".").use_guards(guard::Post())),
)
.await;
@ -516,13 +541,13 @@ mod tests {
.method(Method::POST)
.to_request();
let resp = test::call_service(&mut srv, req).await;
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_named_file_content_encoding() {
let mut srv = test::init_service(App::new().wrap(Compress::default()).service(
let srv = test::init_service(App::new().wrap(Compress::default()).service(
web::resource("/").to(|| async {
NamedFile::open("Cargo.toml")
.unwrap()
@ -533,16 +558,16 @@ mod tests {
let request = TestRequest::get()
.uri("/")
.header(header::ACCEPT_ENCODING, "gzip")
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.to_request();
let res = test::call_service(&mut srv, request).await;
let res = test::call_service(&srv, request).await;
assert_eq!(res.status(), StatusCode::OK);
assert!(!res.headers().contains_key(header::CONTENT_ENCODING));
}
#[actix_rt::test]
async fn test_named_file_content_encoding_gzip() {
let mut srv = test::init_service(App::new().wrap(Compress::default()).service(
let srv = test::init_service(App::new().wrap(Compress::default()).service(
web::resource("/").to(|| async {
NamedFile::open("Cargo.toml")
.unwrap()
@ -553,9 +578,9 @@ mod tests {
let request = TestRequest::get()
.uri("/")
.header(header::ACCEPT_ENCODING, "gzip")
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.to_request();
let res = test::call_service(&mut srv, request).await;
let res = test::call_service(&srv, request).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers()
@ -577,27 +602,25 @@ mod tests {
#[actix_rt::test]
async fn test_static_files() {
let mut srv = test::init_service(
App::new().service(Files::new("/", ".").show_files_listing()),
)
let srv =
test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
.await;
let req = TestRequest::with_uri("/missing").to_request();
let resp = test::call_service(&mut srv, req).await;
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await;
let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
let req = TestRequest::default().to_request();
let resp = test::call_service(&mut srv, req).await;
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let mut srv = test::init_service(
App::new().service(Files::new("/", ".").show_files_listing()),
)
let srv =
test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
.await;
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&mut srv, req).await;
let resp = test::call_service(&srv, req).await;
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/html; charset=utf-8"
@ -610,16 +633,16 @@ mod tests {
#[actix_rt::test]
async fn test_redirect_to_slash_directory() {
// should not redirect if no index
let mut srv = test::init_service(
let srv = test::init_service(
App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
)
.await;
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&mut srv, req).await;
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
// should redirect if index present
let mut srv = test::init_service(
let srv = test::init_service(
App::new().service(
Files::new("/", ".")
.index_file("test.png")
@ -628,24 +651,28 @@ mod tests {
)
.await;
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&mut srv, req).await;
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
// should not redirect if the path is wrong
let req = TestRequest::with_uri("/not_existing").to_request();
let resp = test::call_service(&mut srv, req).await;
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[actix_rt::test]
async fn test_static_files_bad_directory() {
let _st: Files = Files::new("/", "missing");
let _st: Files = Files::new("/", "Cargo.toml");
let service = Files::new("/", "./missing").new_service(()).await.unwrap();
let req = TestRequest::with_uri("/").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[actix_rt::test]
async fn test_default_handler_file_missing() {
let mut st = Files::new("/", ".")
let st = Files::new("/", ".")
.default_handler(|req: ServiceRequest| {
ok(req.into_response(HttpResponse::Ok().body("default content")))
})
@ -653,124 +680,78 @@ mod tests {
.await
.unwrap();
let req = TestRequest::with_uri("/missing").to_srv_request();
let resp = test::call_service(&st, req).await;
let resp = test::call_service(&mut st, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let bytes = test::read_body(resp).await;
assert_eq!(bytes, web::Bytes::from_static(b"default content"));
}
// #[actix_rt::test]
// async fn test_serve_index() {
// let st = Files::new(".").index_file("test.binary");
// let req = TestRequest::default().uri("/tests").finish();
#[actix_rt::test]
async fn test_serve_index_nested() {
let service = Files::new(".", ".")
.index_file("lib.rs")
.new_service(())
.await
.unwrap();
// let resp = st.handle(&req).respond_to(&req).unwrap();
// let resp = resp.as_msg();
// assert_eq!(resp.status(), StatusCode::OK);
// assert_eq!(
// resp.headers()
// .get(header::CONTENT_TYPE)
// .expect("content type"),
// "application/octet-stream"
// );
// assert_eq!(
// resp.headers()
// .get(header::CONTENT_DISPOSITION)
// .expect("content disposition"),
// "attachment; filename=\"test.binary\""
// );
let req = TestRequest::default().uri("/src").to_srv_request();
let resp = test::call_service(&service, req).await;
// let req = TestRequest::default().uri("/tests/").finish();
// let resp = st.handle(&req).respond_to(&req).unwrap();
// let resp = resp.as_msg();
// assert_eq!(resp.status(), StatusCode::OK);
// assert_eq!(
// resp.headers().get(header::CONTENT_TYPE).unwrap(),
// "application/octet-stream"
// );
// assert_eq!(
// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
// "attachment; filename=\"test.binary\""
// );
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/x-rust"
);
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"lib.rs\""
);
}
// // nonexistent index file
// let req = TestRequest::default().uri("/tests/unknown").finish();
// let resp = st.handle(&req).respond_to(&req).unwrap();
// let resp = resp.as_msg();
// assert_eq!(resp.status(), StatusCode::NOT_FOUND);
#[actix_rt::test]
async fn integration_serve_index() {
let srv = test::init_service(
App::new().service(Files::new("test", ".").index_file("Cargo.toml")),
)
.await;
// let req = TestRequest::default().uri("/tests/unknown/").finish();
// let resp = st.handle(&req).respond_to(&req).unwrap();
// let resp = resp.as_msg();
// assert_eq!(resp.status(), StatusCode::NOT_FOUND);
// }
let req = TestRequest::get().uri("/test").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
// #[actix_rt::test]
// async fn test_serve_index_nested() {
// let st = Files::new(".").index_file("mod.rs");
// let req = TestRequest::default().uri("/src/client").finish();
// let resp = st.handle(&req).respond_to(&req).unwrap();
// let resp = resp.as_msg();
// assert_eq!(resp.status(), StatusCode::OK);
// assert_eq!(
// resp.headers().get(header::CONTENT_TYPE).unwrap(),
// "text/x-rust"
// );
// assert_eq!(
// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
// "inline; filename=\"mod.rs\""
// );
// }
let bytes = test::read_body(res).await;
// #[actix_rt::test]
// fn integration_serve_index() {
// let mut srv = test::TestServer::with_factory(|| {
// App::new().handler(
// "test",
// Files::new(".").index_file("Cargo.toml"),
// )
// });
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
assert_eq!(bytes, data);
// let request = srv.get().uri(srv.url("/test")).finish().unwrap();
// let response = srv.execute(request.send()).unwrap();
// assert_eq!(response.status(), StatusCode::OK);
// let bytes = srv.execute(response.body()).unwrap();
// let data = Bytes::from(fs::read("Cargo.toml").unwrap());
// assert_eq!(bytes, data);
let req = TestRequest::get().uri("/test/").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
// let request = srv.get().uri(srv.url("/test/")).finish().unwrap();
// let response = srv.execute(request.send()).unwrap();
// assert_eq!(response.status(), StatusCode::OK);
// let bytes = srv.execute(response.body()).unwrap();
// let data = Bytes::from(fs::read("Cargo.toml").unwrap());
// assert_eq!(bytes, data);
let bytes = test::read_body(res).await;
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
assert_eq!(bytes, data);
// // nonexistent index file
// let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap();
// let response = srv.execute(request.send()).unwrap();
// assert_eq!(response.status(), StatusCode::NOT_FOUND);
// nonexistent index file
let req = TestRequest::get().uri("/test/unknown").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
// let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap();
// let response = srv.execute(request.send()).unwrap();
// assert_eq!(response.status(), StatusCode::NOT_FOUND);
// }
let req = TestRequest::get().uri("/test/unknown/").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
// #[actix_rt::test]
// fn integration_percent_encoded() {
// let mut srv = test::TestServer::with_factory(|| {
// App::new().handler(
// "test",
// Files::new(".").index_file("Cargo.toml"),
// )
// });
#[actix_rt::test]
async fn integration_percent_encoded() {
let srv = test::init_service(
App::new().service(Files::new("test", ".").index_file("Cargo.toml")),
)
.await;
// let request = srv
// .get()
// .uri(srv.url("/test/%43argo.toml"))
// .finish()
// .unwrap();
// let response = srv.execute(request.send()).unwrap();
// assert_eq!(response.status(), StatusCode::OK);
// }
let req = TestRequest::get().uri("/test/%43argo.toml").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
}
}

View File

@ -11,15 +11,13 @@ use actix_web::{
dev::{BodyEncoding, SizedStream},
http::{
header::{
self, Charset, ContentDisposition, DispositionParam, DispositionType,
ExtendedValue,
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
},
ContentEncoding, StatusCode,
},
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
HttpMessage, HttpRequest, HttpResponse, Responder,
};
use bitflags::bitflags;
use futures_util::future::{ready, Ready};
use mime_guess::from_path;
use crate::ChunkedReadFile;
@ -277,37 +275,31 @@ impl NamedFile {
}
/// 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) -> HttpResponse {
if self.status_code != StatusCode::OK {
let mut res = HttpResponse::build(self.status_code);
if self.flags.contains(Flags::PREFER_UTF8) {
let ct = equiv_utf8_text(self.content_type.clone());
res.header(header::CONTENT_TYPE, ct.to_string());
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
} else {
res.header(header::CONTENT_TYPE, self.content_type.to_string());
res.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
}
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
res.header(
res.insert_header((
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
);
));
}
if let Some(current_encoding) = self.encoding {
res.encoding(current_encoding);
}
let reader = ChunkedReadFile {
size: self.md.len(),
offset: 0,
file: Some(self.file),
fut: None,
counter: 0,
};
let reader = ChunkedReadFile::new(self.md.len(), 0, self.file);
return Ok(res.streaming(reader));
return res.streaming(reader);
}
let etag = if self.flags.contains(Flags::ETAG) {
@ -332,7 +324,7 @@ impl NamedFile {
let t2: SystemTime = since.clone().into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
(Ok(t1), Ok(t2)) => t1 > t2,
(Ok(t1), Ok(t2)) => t1.as_secs() > t2.as_secs(),
_ => false,
}
} else {
@ -351,7 +343,7 @@ impl NamedFile {
let t2: SystemTime = since.clone().into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
(Ok(t1), Ok(t2)) => t1 <= t2,
(Ok(t1), Ok(t2)) => t1.as_secs() <= t2.as_secs(),
_ => false,
}
} else {
@ -362,16 +354,16 @@ impl NamedFile {
if self.flags.contains(Flags::PREFER_UTF8) {
let ct = equiv_utf8_text(self.content_type.clone());
resp.header(header::CONTENT_TYPE, ct.to_string());
resp.insert_header((header::CONTENT_TYPE, ct.to_string()));
} else {
resp.header(header::CONTENT_TYPE, self.content_type.to_string());
resp.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
}
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
resp.header(
resp.insert_header((
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
);
));
}
// default compressing
@ -380,14 +372,14 @@ impl NamedFile {
}
if let Some(lm) = last_modified {
resp.header(header::LAST_MODIFIED, lm.to_string());
resp.insert_header((header::LAST_MODIFIED, lm.to_string()));
}
if let Some(etag) = etag {
resp.header(header::ETAG, etag.to_string());
resp.insert_header((header::ETAG, etag.to_string()));
}
resp.header(header::ACCEPT_RANGES, "bytes");
resp.insert_header((header::ACCEPT_RANGES, "bytes"));
let mut length = self.md.len();
let mut offset = 0;
@ -400,43 +392,32 @@ impl NamedFile {
offset = ranges[0].start;
resp.encoding(ContentEncoding::Identity);
resp.header(
resp.insert_header((
header::CONTENT_RANGE,
format!(
"bytes {}-{}/{}",
offset,
offset + length - 1,
self.md.len()
),
);
format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
));
} else {
resp.header(header::CONTENT_RANGE, format!("bytes */{}", length));
return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish());
resp.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
};
} else {
return Ok(resp.status(StatusCode::BAD_REQUEST).finish());
return resp.status(StatusCode::BAD_REQUEST).finish();
};
};
if precondition_failed {
return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish());
return resp.status(StatusCode::PRECONDITION_FAILED).finish();
} else if not_modified {
return Ok(resp.status(StatusCode::NOT_MODIFIED).finish());
return resp.status(StatusCode::NOT_MODIFIED).finish();
}
let reader = ChunkedReadFile {
offset,
size: length,
file: Some(self.file),
fut: None,
counter: 0,
};
let reader = ChunkedReadFile::new(length, offset, self.file);
if offset != 0 || length != self.md.len() {
resp.status(StatusCode::PARTIAL_CONTENT);
}
Ok(resp.body(SizedStream::new(length, reader)))
resp.body(SizedStream::new(length, reader))
}
}
@ -495,10 +476,7 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
}
impl Responder for NamedFile {
type Error = Error;
type Future = Ready<Result<HttpResponse, Error>>;
fn respond_to(self, req: &HttpRequest) -> Self::Future {
ready(self.into_response(req))
fn respond_to(self, req: &HttpRequest) -> HttpResponse {
self.into_response(req)
}
}

View File

@ -15,12 +15,19 @@ impl FromStr for PathBufWrap {
type Err = UriSegmentError;
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();
for segment in path.split('/') {
if segment == ".." {
buf.pop();
} else if segment.starts_with('.') {
} else if !hidden_files && segment.starts_with('.') {
return Err(UriSegmentError::BadStart('.'));
} else if segment.starts_with('*') {
return Err(UriSegmentError::BadStart('*'));
@ -96,4 +103,17 @@ mod tests {
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

@ -1,3 +1,5 @@
use derive_more::{Display, Error};
/// HTTP Range header representation.
#[derive(Debug, Clone, Copy)]
pub struct HttpRange {
@ -8,91 +10,26 @@ pub struct HttpRange {
pub length: u64,
}
const PREFIX: &str = "bytes=";
const PREFIX_LEN: usize = 6;
#[derive(Debug, Clone, Display, Error)]
#[display(fmt = "Parse HTTP Range failed")]
pub struct ParseRangeErr(#[error(not(source))] ());
impl HttpRange {
/// Parses Range HTTP header string as per RFC 2616.
///
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
/// `size` is full size of response (file).
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ()> {
if header.is_empty() {
return Ok(Vec::new());
}
if !header.starts_with(PREFIX) {
return Err(());
}
let size_sig = size as i64;
let mut no_overlap = false;
let all_ranges: Vec<Option<HttpRange>> = header[PREFIX_LEN..]
.split(',')
.map(|x| x.trim())
.filter(|x| !x.is_empty())
.map(|ra| {
let mut start_end_iter = ra.split('-');
let start_str = start_end_iter.next().ok_or(())?.trim();
let end_str = start_end_iter.next().ok_or(())?.trim();
if start_str.is_empty() {
// If no start is specified, end specifies the
// range start relative to the end of the file.
let mut length: i64 = end_str.parse().map_err(|_| ())?;
if length > size_sig {
length = size_sig;
}
Ok(Some(HttpRange {
start: (size_sig - length) as u64,
length: length as u64,
}))
} else {
let start: i64 = start_str.parse().map_err(|_| ())?;
if start < 0 {
return Err(());
}
if start >= size_sig {
no_overlap = true;
return Ok(None);
}
let length = if end_str.is_empty() {
// If no end is specified, range extends to end of the file.
size_sig - start
} else {
let mut end: i64 = end_str.parse().map_err(|_| ())?;
if start > end {
return Err(());
}
if end >= size_sig {
end = size_sig - 1;
}
end - start + 1
};
Ok(Some(HttpRange {
start: start as u64,
length: length as u64,
}))
}
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
match http_range::HttpRange::parse(header, size) {
Ok(ranges) => Ok(ranges
.iter()
.map(|range| HttpRange {
start: range.start,
length: range.length,
})
.collect::<Result<_, _>>()?;
let ranges: Vec<HttpRange> = all_ranges.into_iter().filter_map(|x| x).collect();
if no_overlap && ranges.is_empty() {
return Err(());
.collect()),
Err(_) => Err(ParseRangeErr(())),
}
Ok(ranges)
}
}
@ -333,8 +270,7 @@ mod tests {
if expected.is_empty() {
continue;
} else {
assert!(
false,
panic!(
"parse({}, {}) returned error {:?}",
header,
size,
@ -346,28 +282,24 @@ mod tests {
let got = res.unwrap();
if got.len() != expected.len() {
assert!(
false,
panic!(
"len(parseRange({}, {})) = {}, want {}",
header,
size,
got.len(),
expected.len()
);
continue;
}
for i in 0..expected.len() {
if got[i].start != expected[i].start {
assert!(
false,
panic!(
"parseRange({}, {})[{}].start = {}, want {}",
header, size, i, got[i].start, expected[i].start
)
}
if got[i].length != expected[i].length {
assert!(
false,
panic!(
"parseRange({}, {})[{}].length = {}, want {}",
header, size, i, got[i].length, expected[i].length
)

View File

@ -1,9 +1,4 @@
use std::{
fmt, io,
path::PathBuf,
rc::Rc,
task::{Context, Poll},
};
use std::{fmt, io, path::PathBuf, rc::Rc, task::Poll};
use actix_service::Service;
use actix_web::{
@ -16,8 +11,8 @@ use actix_web::{
use futures_util::future::{ok, Either, LocalBoxFuture, Ready};
use crate::{
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride,
NamedFile, PathBufWrap,
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile,
PathBufWrap,
};
/// Assembled file serving service.
@ -31,6 +26,7 @@ pub struct FilesService {
pub(crate) mime_override: Option<Rc<MimeOverride>>,
pub(crate) file_flags: named::Flags,
pub(crate) guards: Option<Rc<dyn Guard>>,
pub(crate) hidden_files: bool,
}
type FilesServiceFuture = Either<
@ -39,10 +35,10 @@ type FilesServiceFuture = Either<
>;
impl FilesService {
fn handle_err(&mut self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture {
fn handle_err(&self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture {
log::debug!("Failed to handle {}: {}", req.path(), e);
if let Some(ref mut default) = self.default {
if let Some(ref default) = self.default {
Either::Right(default.call(req))
} else {
Either::Left(ok(req.error_response(e)))
@ -56,17 +52,14 @@ impl fmt::Debug for FilesService {
}
}
impl Service for FilesService {
type Request = ServiceRequest;
impl Service<ServiceRequest> for FilesService {
type Response = ServiceResponse;
type Error = Error;
type Future = FilesServiceFuture;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
actix_service::always_ready!();
fn call(&mut self, req: ServiceRequest) -> Self::Future {
fn call(&self, req: ServiceRequest) -> Self::Future {
let is_method_valid = if let Some(guard) = &self.guards {
// execute user defined guards
(**guard).check(req.head())
@ -78,12 +71,13 @@ impl Service for FilesService {
if !is_method_valid {
return Either::Left(ok(req.into_response(
actix_web::HttpResponse::MethodNotAllowed()
.header(header::CONTENT_TYPE, "text/plain")
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
.body("Request did not meet this resource's requirements."),
)));
}
let real_path: PathBufWrap = match req.match_info().path().parse() {
let real_path =
match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) {
Ok(item) => item,
Err(e) => return Either::Left(ok(req.error_response(e))),
};
@ -101,7 +95,7 @@ impl Service for FilesService {
return Either::Left(ok(req.into_response(
HttpResponse::Found()
.header(header::LOCATION, redirect_to)
.insert_header((header::LOCATION, redirect_to))
.body("")
.into_body(),
)));
@ -119,10 +113,8 @@ impl Service for FilesService {
named_file.flags = self.file_flags;
let (req, _) = req.into_parts();
Either::Left(ok(match named_file.into_response(&req) {
Ok(item) => ServiceResponse::new(req, item),
Err(e) => ServiceResponse::from_err(e, req),
}))
let res = named_file.into_response(&req);
Either::Left(ok(ServiceResponse::new(req, res)))
}
Err(e) => self.handle_err(e, req),
}
@ -146,19 +138,14 @@ impl Service for FilesService {
match NamedFile::open(path) {
Ok(mut named_file) => {
if let Some(ref mime_override) = self.mime_override {
let new_disposition =
mime_override(&named_file.content_type.type_());
let new_disposition = mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = self.file_flags;
let (req, _) = req.into_parts();
match named_file.into_response(&req) {
Ok(item) => {
Either::Left(ok(ServiceResponse::new(req.clone(), item)))
}
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
}
let res = named_file.into_response(&req);
Either::Left(ok(ServiceResponse::new(req, res)))
}
Err(e) => self.handle_err(e, req),
}

View File

@ -11,11 +11,10 @@ use actix_web::{
#[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 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;
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
@ -24,13 +23,12 @@ async fn test_utf8_file_contents() {
);
// prefer UTF-8 encoding
let mut srv = test::init_service(
App::new().service(Files::new("/", "./tests").prefer_utf8(true)),
)
let 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;
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(

View File

@ -1,16 +1,35 @@
# Changes
## Unreleased - 2020-xx-xx
## Unreleased - 2021-xx-xx
* add ability to set address for `TestServer` [#1645]
## 3.0.0-beta.3 - 2021-03-09
* No notable changes.
## 3.0.0-beta.2 - 2021-02-10
* No notable changes.
## 3.0.0-beta.1 - 2021-01-07
* Update `bytes` to `1.0`. [#1813]
[#1813]: https://github.com/actix/actix-web/pull/1813
## 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`.
* 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
## 2.0.0 - 2020-09-11
* Update actix-codec and actix-utils dependencies.
## 2.0.0-alpha.1 - 2020-05-23
* Update the `time` dependency to 0.2.7
* Update `actix-connect` dependency to 2.0.0-alpha.2
@ -20,74 +39,56 @@
* Update `base64` dependency to 0.12
* Update `env_logger` dependency to 0.7
## [1.0.0] - 2019-12-13
### Changed
## 1.0.0 - 2019-12-13
* Replaced `TestServer::start()` with `test_server()`
## [1.0.0-alpha.3] - 2019-12-07
### Changed
## 1.0.0-alpha.3 - 2019-12-07
* Migrate to `std::future`
## [0.2.5] - 2019-09-17
### Changed
## 0.2.5 - 2019-09-17
* Update serde_urlencoded to "0.6.1"
* Increase TestServerRuntime timeouts from 500ms to 3000ms
### Fixed
* Do not override current `System`
## [0.2.4] - 2019-07-18
## 0.2.4 - 2019-07-18
* 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`
## [0.2.2] - 2019-06-16
## 0.2.2 - 2019-06-16
* Add .put() and .sput() methods
## [0.2.1] - 2019-06-05
## 0.2.1 - 2019-06-05
* Add license files
## [0.2.0] - 2019-05-12
## 0.2.0 - 2019-05-12
* 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
## [0.1.0] - 2019-04-16
## 0.1.0 - 2019-04-16
* No changes
## [0.1.0-alpha.3] - 2019-04-02
## 0.1.0-alpha.3 - 2019-04-02
* 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
* Update actix-http and awc libraries
## [0.1.0-alpha.1] - 2019-03-28
## 0.1.0-alpha.1 - 2019-03-28
* Initial impl

View File

@ -0,0 +1,61 @@
[package]
name = "actix-http-test"
version = "3.0.0-beta.3"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Various helpers for Actix applications to use during testing"
readme = "README.md"
keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git"
documentation = "https://docs.rs/actix-http-test/"
categories = ["network-programming", "asynchronous",
"web-programming::http-server",
"web-programming::websocket"]
license = "MIT OR Apache-2.0"
exclude = [".gitignore", ".cargo/config"]
edition = "2018"
[package.metadata.docs.rs]
features = []
[lib]
name = "actix_http_test"
path = "src/lib.rs"
[features]
default = []
# openssl
openssl = ["tls-openssl", "awc/openssl"]
[dependencies]
actix-service = "2.0.0-beta.4"
actix-codec = "0.4.0-beta.1"
actix-tls = "3.0.0-beta.4"
actix-utils = "3.0.0-beta.2"
actix-rt = "2.1"
actix-server = "2.0.0-beta.3"
awc = { version = "3.0.0-beta.3", default-features = false }
base64 = "0.13"
bytes = "1"
futures-core = { version = "0.3.7", default-features = false }
http = "0.2.2"
log = "0.4"
socket2 = "0.3"
serde = "1.0"
serde_json = "1.0"
slab = "0.4"
serde_urlencoded = "0.7"
time = { version = "0.2.23", default-features = false, features = ["std"] }
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
[target.'cfg(windows)'.dependencies.tls-openssl]
version = "0.10.9"
package = "openssl"
features = ["vendored"]
optional = true
[dev-dependencies]
actix-web = { version = "4.0.0-beta.4", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.4"

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=3.0.0-beta.3)](https://docs.rs/actix-http-test/3.0.0-beta.3)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.3/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.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)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-http-test)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum Supported Rust Version (MSRV): 1.46.0

View File

@ -1,4 +1,12 @@
//! 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")]
#[cfg(feature = "openssl")]
extern crate tls_openssl as openssl;
use std::sync::mpsc;
use std::{net, thread, time};
@ -11,8 +19,6 @@ use futures_core::stream::Stream;
use http::Method;
use socket2::{Domain, Protocol, Socket, Type};
pub use actix_testing::*;
/// Start test server
///
/// `TestServer` is very simple test server that simplify process of writing
@ -48,7 +54,7 @@ pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer
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>>(
tcp: net::TcpListener,
factory: F,
@ -57,16 +63,19 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
// run server in separate thread
thread::spawn(move || {
let sys = System::new("actix-test-server");
let sys = System::new();
let local_addr = tcp.local_addr().unwrap();
Server::build()
let srv = Server::build()
.listen("test", tcp, factory)?
.workers(1)
.disable_signals()
.start();
.disable_signals();
sys.block_on(async {
srv.run();
tx.send((System::current(), local_addr)).unwrap();
});
sys.run()
});
@ -76,7 +85,7 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
let connector = {
#[cfg(feature = "openssl")]
{
use open_ssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
builder.set_verify(SslVerifyMode::NONE);
@ -87,20 +96,17 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
.conn_lifetime(time::Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000))
.ssl(builder.build())
.finish()
}
#[cfg(not(feature = "openssl"))]
{
Connector::new()
.conn_lifetime(time::Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000))
.finish()
}
};
Client::builder().connector(connector).finish()
};
actix_connect::start_default_resolver().await.unwrap();
TestServer {
addr,
@ -112,8 +118,7 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
/// Get first available unused address
pub fn unused_addr() -> net::SocketAddr {
let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
let socket =
Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap();
let socket = Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap();
socket.bind(&addr.into()).unwrap();
socket.set_reuse_address(true).unwrap();
let tcp = socket.into_tcp_listener();
@ -142,7 +147,7 @@ impl TestServer {
}
}
/// Construct test https server url
/// Construct test HTTPS server URL.
pub fn surl(&self, uri: &str) -> String {
if uri.starts_with('/') {
format!("https://localhost:{}{}", self.addr.port(), uri)
@ -156,7 +161,7 @@ impl TestServer {
self.client.get(self.url(path.as_ref()).as_str())
}
/// Create https `GET` request
/// Create HTTPS `GET` request
pub fn sget<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.get(self.surl(path.as_ref()).as_str())
}
@ -166,7 +171,7 @@ impl TestServer {
self.client.post(self.url(path.as_ref()).as_str())
}
/// Create https `POST` request
/// Create HTTPS `POST` request
pub fn spost<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.post(self.surl(path.as_ref()).as_str())
}
@ -176,7 +181,7 @@ impl TestServer {
self.client.head(self.url(path.as_ref()).as_str())
}
/// Create https `HEAD` request
/// Create HTTPS `HEAD` request
pub fn shead<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.head(self.surl(path.as_ref()).as_str())
}
@ -186,7 +191,7 @@ impl TestServer {
self.client.put(self.url(path.as_ref()).as_str())
}
/// Create https `PUT` request
/// Create HTTPS `PUT` request
pub fn sput<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.put(self.surl(path.as_ref()).as_str())
}
@ -196,7 +201,7 @@ impl TestServer {
self.client.patch(self.url(path.as_ref()).as_str())
}
/// Create https `PATCH` request
/// Create HTTPS `PATCH` request
pub fn spatch<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.patch(self.surl(path.as_ref()).as_str())
}
@ -206,7 +211,7 @@ impl TestServer {
self.client.delete(self.url(path.as_ref()).as_str())
}
/// Create https `DELETE` request
/// Create HTTPS `DELETE` request
pub fn sdelete<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.delete(self.surl(path.as_ref()).as_str())
}
@ -216,12 +221,12 @@ impl TestServer {
self.client.options(self.url(path.as_ref()).as_str())
}
/// Create https `OPTIONS` request
/// Create HTTPS `OPTIONS` request
pub fn soptions<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.options(self.surl(path.as_ref()).as_str())
}
/// Connect to test http server
/// Connect to test HTTP server
pub fn request<S: AsRef<str>>(&self, method: Method, path: S) -> ClientRequest {
self.client.request(method, path.as_ref())
}
@ -236,26 +241,24 @@ impl TestServer {
response.body().limit(10_485_760).await
}
/// Connect to websocket server at a given path
/// Connect to WebSocket server at a given path.
pub async fn ws_at(
&mut self,
path: &str,
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError>
{
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
let url = self.url(path);
let connect = self.client.ws(url).connect();
connect.await.map(|(_, framed)| framed)
}
/// Connect to a websocket server
/// Connect to a WebSocket server.
pub async fn ws(
&mut self,
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError>
{
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
self.ws_at("/").await
}
/// Stop http server
/// Stop HTTP server
fn stop(&mut self) {
self.system.stop();
}

View File

@ -1,6 +1,109 @@
# Changes
## Unreleased - 2020-xx-xx
## Unreleased - 2021-xx-xx
## 3.0.0-beta.4 - 2021-03-08
### Changed
* Feature `cookies` is now optional and disabled by default. [#1981]
* `ws::hash_key` now returns array. [#2035]
* `ResponseBuilder::json` now takes `impl Serialize`. [#2052]
### Removed
* Re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994]
* `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994]
[#1981]: https://github.com/actix/actix-web/pull/1981
[#1994]: https://github.com/actix/actix-web/pull/1994
[#2035]: https://github.com/actix/actix-web/pull/2035
[#2052]: https://github.com/actix/actix-web/pull/2052
## 3.0.0-beta.3 - 2021-02-10
* No notable changes.
## 3.0.0-beta.2 - 2021-02-10
### Added
* `IntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869]
* `ResponseBuilder::insert_header` method which allows using typed headers. [#1869]
* `ResponseBuilder::append_header` method which allows using typed headers. [#1869]
* `TestRequest::insert_header` method which allows using typed headers. [#1869]
* `ContentEncoding` implements all necessary header traits. [#1912]
* `HeaderMap::len_keys` has the behavior of the old `len` method. [#1964]
* `HeaderMap::drain` as an efficient draining iterator. [#1964]
* Implement `IntoIterator` for owned `HeaderMap`. [#1964]
* `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969]
### Changed
* `ResponseBuilder::content_type` now takes an `impl IntoHeaderValue` to support using typed
`mime` types. [#1894]
* Renamed `IntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std
`TryInto` trait. [#1894]
* `Extensions::insert` returns Option of replaced item. [#1904]
* Remove `HttpResponseBuilder::json2()`. [#1903]
* Enable `HttpResponseBuilder::json()` to receive data by value and reference. [#1903]
* `client::error::ConnectError` Resolver variant contains `Box<dyn std::error::Error>` type. [#1905]
* `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905]
* Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool
is dead. [#1957]
* `HeaderMap::len` now returns number of values instead of number of keys. [#1964]
* `HeaderMap::insert` now returns iterator of removed values. [#1964]
* `HeaderMap::remove` now returns iterator of removed values. [#1964]
### Removed
* `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869]
* `ResponseBuilder::set_header`; use `ResponseBuilder::insert_header`. [#1869]
* `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869]
* `TestRequest::with_hdr`; use `TestRequest::default().insert_header()`. [#1869]
* `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869]
* `actors` optional feature. [#1969]
* `ResponseError` impl for `actix::MailboxError`. [#1969]
### Documentation
* Vastly improve docs and add examples for `HeaderMap`. [#1964]
[#1869]: https://github.com/actix/actix-web/pull/1869
[#1894]: https://github.com/actix/actix-web/pull/1894
[#1903]: https://github.com/actix/actix-web/pull/1903
[#1904]: https://github.com/actix/actix-web/pull/1904
[#1905]: https://github.com/actix/actix-web/pull/1905
[#1912]: https://github.com/actix/actix-web/pull/1912
[#1957]: https://github.com/actix/actix-web/pull/1957
[#1964]: https://github.com/actix/actix-web/pull/1964
[#1969]: https://github.com/actix/actix-web/pull/1969
## 3.0.0-beta.1 - 2021-01-07
### Added
* Add `Http3` to `Protocol` enum for future compatibility and also mark `#[non_exhaustive]`.
### Changed
* Update `actix-*` dependencies to tokio `1.0` based versions. [#1813]
* Bumped `rand` to `0.8`.
* Update `bytes` to `1.0`. [#1813]
* Update `h2` to `0.3`. [#1813]
* The `ws::Message::Text` enum variant now contains a `bytestring::ByteString`. [#1864]
### Removed
* Deprecated `on_connect` methods have been removed. Prefer the new
`on_connect_ext` technique. [#1857]
* Remove `ResponseError` impl for `actix::actors::resolver::ResolverError`
due to deprecate of resolver actor. [#1813]
* Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
due to the removal of this type from `tokio-openssl` crate. openssl handshake
error would return as `ConnectError::SslError`. [#1813]
* Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`.
Due to this change `actix_threadpool::BlockingError` type is moved into
`actix_http::error` module. [#1878]
[#1813]: https://github.com/actix/actix-web/pull/1813
[#1857]: https://github.com/actix/actix-web/pull/1857
[#1864]: https://github.com/actix/actix-web/pull/1864
[#1878]: https://github.com/actix/actix-web/pull/1878
## 2.2.0 - 2020-11-25
### Added
* HttpResponse builders for 1xx status codes. [#1768]
* `Accept::mime_precedence` and `Accept::mime_preference`. [#1793]
@ -10,8 +113,9 @@
* Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767]
### Changed
* Upgrade `serde_urlencoded` to `0.7`.
* 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
@ -43,15 +147,14 @@
* Update actix-connect and actix-tls dependencies.
## [2.0.0-beta.3] - 2020-08-14
## 2.0.0-beta.3 - 2020-08-14
### Fixed
* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626]
[#1626]: https://github.com/actix/actix-web/pull/1626
## [2.0.0-beta.2] - 2020-07-21
## 2.0.0-beta.2 - 2020-07-21
### Fixed
* Potential UB in h1 decoder using uninitialized memory. [#1614]
@ -62,10 +165,8 @@
[#1615]: https://github.com/actix/actix-web/pull/1615
## [2.0.0-beta.1] - 2020-07-11
## 2.0.0-beta.1 - 2020-07-11
### Changed
* Migrate cookie handling to `cookie` crate. [#1558]
* Update `sha-1` to 0.9. [#1586]
* Fix leak in client pool. [#1580]
@ -75,33 +176,30 @@
[#1586]: https://github.com/actix/actix-web/pull/1586
[#1580]: https://github.com/actix/actix-web/pull/1580
## [2.0.0-alpha.4] - 2020-05-21
## 2.0.0-alpha.4 - 2020-05-21
### Changed
* Bump minimum supported Rust version to 1.40
* content_length function is removed, and you can set Content-Length by calling no_chunking function [#1439]
* content_length function is removed, and you can set Content-Length by calling
no_chunking function [#1439]
* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
`u64` instead of a `usize`.
* Update `base64` dependency to 0.12
### Fixed
* Support parsing of `SameSite=None` [#1503]
[#1439]: https://github.com/actix/actix-web/pull/1439
[#1503]: https://github.com/actix/actix-web/pull/1503
## [2.0.0-alpha.3] - 2020-05-08
## 2.0.0-alpha.3 - 2020-05-08
### Fixed
* Correct spelling of ConnectError::Unresolved [#1487]
* Fix a mistake in the encoding of websocket continuation messages wherein
Item::FirstText and Item::FirstBinary are each encoded as the other.
### Changed
* Implement `std::error::Error` for our custom errors [#1422]
* Remove `failure` support for `ResponseError` since that crate
will be deprecated in the near future.
@ -109,338 +207,247 @@
[#1422]: https://github.com/actix/actix-web/pull/1422
[#1487]: https://github.com/actix/actix-web/pull/1487
## [2.0.0-alpha.2] - 2020-03-07
## 2.0.0-alpha.2 - 2020-03-07
### Changed
* Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395]
* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB respectively
to improve download speed for awc when downloading large objects. [#1394]
* client::Connector accepts initial_window_size and initial_connection_window_size HTTP2 configuration. [#1394]
* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB
respectively to improve download speed for awc when downloading large objects. [#1394]
* client::Connector accepts initial_window_size and initial_connection_window_size
HTTP2 configuration. [#1394]
* client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394]
[#1394]: https://github.com/actix/actix-web/pull/1394
[#1395]: https://github.com/actix/actix-web/pull/1395
## [2.0.0-alpha.1] - 2020-02-27
## 2.0.0-alpha.1 - 2020-02-27
### Changed
* Update the `time` dependency to 0.2.7.
* Moved actors messages support from actix crate, enabled with feature `actors`.
* Breaking change: trait MessageBody requires Unpin and accepting Pin<&mut Self> instead of &mut self in the poll_next().
* Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of
`&mut self` in the poll_next().
* MessageBody is not implemented for &'static [u8] anymore.
### Fixed
* Allow `SameSite=None` cookies to be sent in a response.
## [1.0.1] - 2019-12-20
## 1.0.1 - 2019-12-20
### Fixed
* Poll upgrade service's readiness from HTTP service handlers
* Replace brotli with brotli2 #1224
## [1.0.0] - 2019-12-13
## 1.0.0 - 2019-12-13
### Added
* Add websockets continuation frame support
### Changed
* Replace `flate2-xxx` features with `compress`
## [1.0.0-alpha.5] - 2019-12-09
## 1.0.0-alpha.5 - 2019-12-09
### Fixed
* Check `Upgrade` service readiness before calling it
* Fix buffer remaining capacity calcualtion
* Fix buffer remaining capacity calculation
### Changed
* Websockets: Ping and Pong should have binary data #1049
## [1.0.0-alpha.4] - 2019-12-08
## 1.0.0-alpha.4 - 2019-12-08
### Added
* Add impl ResponseBuilder for Error
### Changed
* Use rust based brotli compression library
## [1.0.0-alpha.3] - 2019-12-07
## 1.0.0-alpha.3 - 2019-12-07
### Changed
* Migrate to tokio 0.2
* Migrate to `std::future`
## [0.2.11] - 2019-11-06
## 0.2.11 - 2019-11-06
### Added
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
* Add an additional `filename*` param in the `Content-Disposition` header of
`actix_files::NamedFile` to be more compatible. (#1151)
* Allow to use `std::convert::Infallible` as `actix_http::error::Error`
### Fixed
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain;
charset=utf-8` header [#1118]
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; charset=utf-8` header #1118
[#1878]: https://github.com/actix/actix-web/pull/1878
## [0.2.10] - 2019-09-11
## 0.2.10 - 2019-09-11
### Added
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests with `RequestHead`
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests
with `RequestHead`
### Fixed
* h2 will use error response #1080
* on_connect result isn't added to request extensions for http2 requests #1009
## [0.2.9] - 2019-08-13
## 0.2.9 - 2019-08-13
### Changed
* Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
* Update percent-encoding to 2.1
* Update serde_urlencoded to 0.6.1
### Fixed
* Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031)
## [0.2.8] - 2019-08-01
## 0.2.8 - 2019-08-01
### Added
* Add `rustls` support
* Add `Clone` impl for `HeaderMap`
### Fixed
* awc client panic #1016
* Invalid response with compression middleware enabled, but compression-related features disabled #997
* Invalid response with compression middleware enabled, but compression-related features
disabled #997
## [0.2.7] - 2019-07-18
## 0.2.7 - 2019-07-18
### Added
* Add support for downcasting response errors #986
## [0.2.6] - 2019-07-17
## 0.2.6 - 2019-07-17
### Changed
* Replace `ClonableService` with local copy
* Upgrade `rand` dependency version to 0.7
## [0.2.5] - 2019-06-28
## 0.2.5 - 2019-06-28
### Added
* Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946
### Changed
* Use `encoding_rs` crate instead of unmaintained `encoding` crate
* Add `Copy` and `Clone` impls for `ws::Codec`
## [0.2.4] - 2019-06-16
## 0.2.4 - 2019-06-16
### Fixed
* Do not compress NoContent (204) responses #918
## [0.2.3] - 2019-06-02
## 0.2.3 - 2019-06-02
### Added
* Debug impl for ResponseBuilder
* From SizedStream and BodyStream for Body
### Changed
* SizedStream uses u64
## [0.2.2] - 2019-05-29
## 0.2.2 - 2019-05-29
### Fixed
* Parse incoming stream before closing stream on disconnect #868
## [0.2.1] - 2019-05-25
## 0.2.1 - 2019-05-25
### Fixed
* Handle socket read disconnect
## [0.2.0] - 2019-05-12
## 0.2.0 - 2019-05-12
### Changed
* Update actix-service to 0.4
* Expect and upgrade services accept `ServerConfig` config.
### Deleted
* `OneRequest` service
## [0.1.5] - 2019-05-04
## 0.1.5 - 2019-05-04
### Fixed
* Clean up response extensions in response pool #817
## [0.1.4] - 2019-04-24
## 0.1.4 - 2019-04-24
### Added
* Allow to render h1 request headers in `Camel-Case`
### Fixed
* Read until eof for http/1.0 responses #771
## [0.1.3] - 2019-04-23
## 0.1.3 - 2019-04-23
### Fixed
* Fix http client pool management
* Fix http client wait queue management #794
## [0.1.2] - 2019-04-23
## 0.1.2 - 2019-04-23
### Fixed
* Fix BorrowMutError panic in client connector #793
## [0.1.1] - 2019-04-19
## 0.1.1 - 2019-04-19
### Changed
* Cookie::max_age() accepts value in seconds
* Cookie::max_age_time() accepts value in time::Duration
* Allow to specify server address for client connector
## [0.1.0] - 2019-04-16
## 0.1.0 - 2019-04-16
### Added
* Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr`
### Changed
* `actix_http::encoding` always available
* use trust-dns-resolver 0.11.0
## [0.1.0-alpha.5] - 2019-04-12
## 0.1.0-alpha.5 - 2019-04-12
### Added
* Allow to use custom service for upgrade requests
* Added `h1::SendResponse` future.
### Changed
* MessageBody::length() renamed to MessageBody::size() for consistency
* ws handshake verification functions take RequestHead instead of Request
## [0.1.0-alpha.4] - 2019-04-08
## 0.1.0-alpha.4 - 2019-04-08
### Added
* Allow to use custom `Expect` handler
* Add minimal `std::error::Error` impl for `Error`
### Changed
* Export IntoHeaderValue
* Render error and return as response body
* Use thread pool for response body comression
* Use thread pool for response body compression
### Deleted
* Removed PayloadBuffer
## [0.1.0-alpha.3] - 2019-04-02
## 0.1.0-alpha.3 - 2019-04-02
### Added
* Warn when an unsealed private cookie isn't valid UTF-8
### Fixed
* Rust 1.31.0 compatibility
* Preallocate read buffer for h1 codec
* Detect socket disconnection during protocol selection
## [0.1.0-alpha.2] - 2019-03-29
## 0.1.0-alpha.2 - 2019-03-29
### Added
* Added ws::Message::Nop, no-op websockets message
### Changed
* Do not use thread pool for decomression if chunk size is smaller than 2048.
* Do not use thread pool for decompression if chunk size is smaller than 2048.
## [0.1.0-alpha.1] - 2019-03-28
## 0.1.0-alpha.1 - 2019-03-28
* Initial impl

View File

@ -1,6 +1,6 @@
[package]
name = "actix-http"
version = "2.1.0"
version = "3.0.0-beta.4"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "HTTP primitives for the Actix ecosystem"
readme = "README.md"
@ -15,7 +15,8 @@ license = "MIT OR Apache-2.0"
edition = "2018"
[package.metadata.docs.rs]
features = ["openssl", "rustls", "compress", "secure-cookies", "actors"]
# features that docs.rs will build with
features = ["openssl", "rustls", "compress", "cookies", "secure-cookies"]
[lib]
name = "actix_http"
@ -25,79 +26,89 @@ path = "src/lib.rs"
default = []
# openssl
openssl = ["actix-tls/openssl", "actix-connect/openssl"]
openssl = ["actix-tls/openssl"]
# rustls support
rustls = ["actix-tls/rustls", "actix-connect/rustls"]
rustls = ["actix-tls/rustls"]
# enable compressison support
# enable compression support
compress = ["flate2", "brotli2"]
# support for secure cookies
secure-cookies = ["cookie/secure"]
# support for cookies
cookies = ["cookie"]
# support for actix Actor messages
actors = ["actix"]
# support for secure cookies
secure-cookies = ["cookies", "cookie/secure"]
# trust-dns as client dns resolver
trust-dns = ["trust-dns-resolver"]
[dependencies]
actix-service = "1.0.6"
actix-codec = "0.3.0"
actix-connect = "2.0.0"
actix-utils = "2.0.0"
actix-rt = "1.0.0"
actix-threadpool = "0.3.1"
actix-tls = { version = "2.0.0", optional = true }
actix = { version = "0.10.0", optional = true }
actix-service = "2.0.0-beta.4"
actix-codec = "0.4.0-beta.1"
actix-utils = "3.0.0-beta.2"
actix-rt = "2.1"
actix-tls = "3.0.0-beta.4"
ahash = "0.7"
base64 = "0.13"
bitflags = "1.2"
bytes = "0.5.3"
cookie = { version = "0.14.1", features = ["percent-encode"] }
copyless = "0.1.4"
derive_more = "0.99.2"
either = "1.5.3"
bytes = "1"
bytestring = "1"
cfg-if = "1"
cookie = { version = "0.14.1", features = ["percent-encode"], optional = true }
derive_more = "0.99.5"
encoding_rs = "0.8"
futures-channel = { version = "0.3.5", default-features = false }
futures-core = { version = "0.3.5", default-features = false }
futures-util = { version = "0.3.5", default-features = false }
fxhash = "0.2.1"
h2 = "0.2.1"
http = "0.2.0"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
h2 = "0.3.1"
http = "0.2.2"
httparse = "1.3"
indexmap = "1.3"
itoa = "0.4"
lazy_static = "1.4"
language-tags = "0.2"
once_cell = "1.5"
log = "0.4"
mime = "0.3"
percent-encoding = "2.1"
pin-project = "1.0.0"
rand = "0.7"
rand = "0.8"
regex = "1.3"
serde = "1.0"
serde_json = "1.0"
sha-1 = "0.9"
slab = "0.4"
serde_urlencoded = "0.7"
time = { version = "0.2.7", default-features = false, features = ["std"] }
sha-1 = "0.9"
smallvec = "1.6"
time = { version = "0.2.23", default-features = false, features = ["std"] }
tokio = { version = "1.2", features = ["sync"] }
# compression
brotli2 = { version="0.3.2", optional = true }
flate2 = { version = "1.0.13", optional = true }
trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies]
actix-server = "1.0.1"
actix-connect = { version = "2.0.0", features = ["openssl"] }
actix-http-test = { version = "2.0.0", features = ["openssl"] }
actix-tls = { version = "2.0.0", features = ["openssl"] }
actix-server = "2.0.0-beta.3"
actix-http-test = { version = "3.0.0-beta.3", features = ["openssl"] }
actix-tls = { version = "3.0.0-beta.4", features = ["openssl"] }
criterion = "0.3"
env_logger = "0.7"
env_logger = "0.8"
rcgen = "0.8"
serde_derive = "1.0"
open-ssl = { version="0.10", package = "openssl" }
rust-tls = { version="0.18", package = "rustls" }
tls-openssl = { version = "0.10", package = "openssl" }
tls-rustls = { version = "0.19", package = "rustls" }
[target.'cfg(windows)'.dev-dependencies.tls-openssl]
version = "0.10.9"
package = "openssl"
features = ["vendored"]
[[example]]
name = "ws"
required-features = ["rustls"]
[[bench]]
name = "content-length"
name = "write-camel-case"
harness = false
[[bench]]

View File

@ -3,16 +3,19 @@
> HTTP primitives for the Actix ecosystem.
[![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.1.0)](https://docs.rs/actix-http/2.1.0)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-http)
[![Dependency Status](https://deps.rs/crate/actix-http/2.1.0/status.svg)](https://deps.rs/crate/actix-http/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](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.4)](https://docs.rs/actix-http/3.0.0-beta.4)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.4)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](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)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-http/2.1.0)
- [API Documentation](https://docs.rs/actix-http)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum Supported Rust Version (MSRV): 1.42.0
- Minimum Supported Rust Version (MSRV): 1.46.0
## Example

View File

@ -1,291 +0,0 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use bytes::BytesMut;
// benchmark sending all requests at the same time
fn bench_write_content_length(c: &mut Criterion) {
let mut group = c.benchmark_group("write_content_length");
let sizes = [
0, 1, 11, 83, 101, 653, 1001, 6323, 10001, 56329, 100001, 123456, 98724245,
4294967202,
];
for i in sizes.iter() {
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_original::write_content_length(i, &mut b)
})
});
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_new::write_content_length(i, &mut b)
})
});
group.bench_with_input(BenchmarkId::new("itoa", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_itoa::write_content_length(i, &mut b)
})
});
}
group.finish();
}
criterion_group!(benches, bench_write_content_length);
criterion_main!(benches);
mod _itoa {
use bytes::{BufMut, BytesMut};
pub fn write_content_length(n: usize, bytes: &mut BytesMut) {
if n == 0 {
bytes.put_slice(b"\r\ncontent-length: 0\r\n");
return;
}
let mut buf = itoa::Buffer::new();
bytes.put_slice(b"\r\ncontent-length: ");
bytes.put_slice(buf.format(n).as_bytes());
bytes.put_slice(b"\r\n");
}
}
mod _new {
use bytes::{BufMut, BytesMut};
const DIGITS_START: u8 = b'0';
/// NOTE: bytes object has to contain enough space
pub fn write_content_length(n: usize, bytes: &mut BytesMut) {
if n == 0 {
bytes.put_slice(b"\r\ncontent-length: 0\r\n");
return;
}
bytes.put_slice(b"\r\ncontent-length: ");
if n < 10 {
bytes.put_u8(DIGITS_START + (n as u8));
} else if n < 100 {
let n = n as u8;
let d10 = n / 10;
let d1 = n % 10;
bytes.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1);
} else if n < 1000 {
let n = n as u16;
let d100 = (n / 100) as u8;
let d10 = ((n / 10) % 10) as u8;
let d1 = (n % 10) as u8;
bytes.put_u8(DIGITS_START + d100);
bytes.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1);
} else if n < 10_000 {
let n = n as u16;
let d1000 = (n / 1000) as u8;
let d100 = ((n / 100) % 10) as u8;
let d10 = ((n / 10) % 10) as u8;
let d1 = (n % 10) as u8;
bytes.put_u8(DIGITS_START + d1000);
bytes.put_u8(DIGITS_START + d100);
bytes.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1);
} else if n < 100_000 {
let n = n as u32;
let d10000 = (n / 10000) as u8;
let d1000 = ((n / 1000) % 10) as u8;
let d100 = ((n / 100) % 10) as u8;
let d10 = ((n / 10) % 10) as u8;
let d1 = (n % 10) as u8;
bytes.put_u8(DIGITS_START + d10000);
bytes.put_u8(DIGITS_START + d1000);
bytes.put_u8(DIGITS_START + d100);
bytes.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1);
} else if n < 1_000_000 {
let n = n as u32;
let d100000 = (n / 100000) as u8;
let d10000 = ((n / 10000) % 10) as u8;
let d1000 = ((n / 1000) % 10) as u8;
let d100 = ((n / 100) % 10) as u8;
let d10 = ((n / 10) % 10) as u8;
let d1 = (n % 10) as u8;
bytes.put_u8(DIGITS_START + d100000);
bytes.put_u8(DIGITS_START + d10000);
bytes.put_u8(DIGITS_START + d1000);
bytes.put_u8(DIGITS_START + d100);
bytes.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1);
} else {
write_usize(n, bytes);
}
bytes.put_slice(b"\r\n");
}
fn write_usize(n: usize, bytes: &mut BytesMut) {
let mut n = n;
// 20 chars is max length of a usize (2^64)
// digits will be added to the buffer from lsd to msd
let mut buf = BytesMut::with_capacity(20);
while n > 9 {
// "pop" the least-significant digit
let lsd = (n % 10) as u8;
// remove the lsd from n
n = n / 10;
buf.put_u8(DIGITS_START + lsd);
}
// put msd to result buffer
bytes.put_u8(DIGITS_START + (n as u8));
// put, in reverse (msd to lsd), remaining digits to buffer
for i in (0..buf.len()).rev() {
bytes.put_u8(buf[i]);
}
}
}
mod _original {
use std::{mem, ptr, slice};
use bytes::{BufMut, BytesMut};
const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\
2021222324252627282930313233343536373839\
4041424344454647484950515253545556575859\
6061626364656667686970717273747576777879\
8081828384858687888990919293949596979899";
/// NOTE: bytes object has to contain enough space
pub fn write_content_length(mut n: usize, bytes: &mut BytesMut) {
if n < 10 {
let mut buf: [u8; 21] = [
b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l',
b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'\r', b'\n',
];
buf[18] = (n as u8) + b'0';
bytes.put_slice(&buf);
} else if n < 100 {
let mut buf: [u8; 22] = [
b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l',
b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'\r', b'\n',
];
let d1 = n << 1;
unsafe {
ptr::copy_nonoverlapping(
DEC_DIGITS_LUT.as_ptr().add(d1),
buf.as_mut_ptr().offset(18),
2,
);
}
bytes.put_slice(&buf);
} else if n < 1000 {
let mut buf: [u8; 23] = [
b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l',
b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'0', b'\r',
b'\n',
];
// decode 2 more chars, if > 2 chars
let d1 = (n % 100) << 1;
n /= 100;
unsafe {
ptr::copy_nonoverlapping(
DEC_DIGITS_LUT.as_ptr().add(d1),
buf.as_mut_ptr().offset(19),
2,
)
};
// decode last 1
buf[18] = (n as u8) + b'0';
bytes.put_slice(&buf);
} else {
bytes.put_slice(b"\r\ncontent-length: ");
convert_usize(n, bytes);
}
}
pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) {
let mut curr: isize = 39;
let mut buf: [u8; 41] = unsafe { mem::MaybeUninit::uninit().assume_init() };
buf[39] = b'\r';
buf[40] = b'\n';
let buf_ptr = buf.as_mut_ptr();
let lut_ptr = DEC_DIGITS_LUT.as_ptr();
// eagerly decode 4 characters at a time
while n >= 10_000 {
let rem = (n % 10_000) as isize;
n /= 10_000;
let d1 = (rem / 100) << 1;
let d2 = (rem % 100) << 1;
curr -= 4;
unsafe {
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
ptr::copy_nonoverlapping(
lut_ptr.offset(d2),
buf_ptr.offset(curr + 2),
2,
);
}
}
// if we reach here numbers are <= 9999, so at most 4 chars long
let mut n = n as isize; // possibly reduce 64bit math
// decode 2 more chars, if > 2 chars
if n >= 100 {
let d1 = (n % 100) << 1;
n /= 100;
curr -= 2;
unsafe {
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
}
}
// decode last 1 or 2 chars
if n < 10 {
curr -= 1;
unsafe {
*buf_ptr.offset(curr) = (n as u8) + b'0';
}
} else {
let d1 = n << 1;
curr -= 2;
unsafe {
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
}
}
unsafe {
bytes.extend_from_slice(slice::from_raw_parts(
buf_ptr.offset(curr),
41 - curr as usize,
));
}
}
}

View File

@ -176,7 +176,7 @@ mod _original {
buf[5] = b'0';
buf[7] = b'9';
}
_ => (),
_ => {}
}
let mut curr: isize = 12;

View File

@ -0,0 +1,89 @@
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
fn bench_write_camel_case(c: &mut Criterion) {
let mut group = c.benchmark_group("write_camel_case");
let names = ["connection", "Transfer-Encoding", "transfer-encoding"];
for &i in &names {
let bts = i.as_bytes();
group.bench_with_input(BenchmarkId::new("Original", i), bts, |b, bts| {
b.iter(|| {
let mut buf = black_box([0; 24]);
_original::write_camel_case(black_box(bts), &mut buf)
});
});
group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| {
b.iter(|| {
let mut buf = black_box([0; 24]);
_new::write_camel_case(black_box(bts), &mut buf)
});
});
}
group.finish();
}
criterion_group!(benches, bench_write_camel_case);
criterion_main!(benches);
mod _new {
pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
// first copy entire (potentially wrong) slice to output
buffer[..value.len()].copy_from_slice(value);
let mut iter = value.iter();
// first character should be uppercase
if let Some(c @ b'a'..=b'z') = iter.next() {
buffer[0] = c & 0b1101_1111;
}
// track 1 ahead of the current position since that's the location being assigned to
let mut index = 2;
// remaining characters after hyphens should also be uppercase
while let Some(&c) = iter.next() {
if c == b'-' {
// advance iter by one and uppercase if needed
if let Some(c @ b'a'..=b'z') = iter.next() {
buffer[index] = c & 0b1101_1111;
}
}
index += 1;
}
}
}
mod _original {
pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
let mut index = 0;
let key = value;
let mut key_iter = key.iter();
if let Some(c) = key_iter.next() {
if *c >= b'a' && *c <= b'z' {
buffer[index] = *c ^ b' ';
index += 1;
}
} else {
return;
}
while let Some(c) = key_iter.next() {
buffer[index] = *c;
index += 1;
if *c == b'-' {
if let Some(c) = key_iter.next() {
if *c >= b'a' && *c <= b'z' {
buffer[index] = *c ^ b' ';
index += 1;
}
}
}
}
}
}

View File

@ -26,7 +26,10 @@ async fn main() -> io::Result<()> {
info!("request body: {:?}", body);
Ok::<_, Error>(
Response::Ok()
.header("x-head", HeaderValue::from_static("dummy value!"))
.insert_header((
"x-head",
HeaderValue::from_static("dummy value!"),
))
.body(body),
)
})

View File

@ -15,7 +15,7 @@ async fn handle_request(mut req: Request) -> Result<Response, Error> {
info!("request body: {:?}", body);
Ok(Response::Ok()
.header("x-head", HeaderValue::from_static("dummy value!"))
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
.body(body))
}

View File

@ -19,7 +19,10 @@ async fn main() -> io::Result<()> {
.finish(|_req| {
info!("{:?}", _req);
let mut res = Response::Ok();
res.header("x-head", HeaderValue::from_static("dummy value!"));
res.insert_header((
"x-head",
HeaderValue::from_static("dummy value!"),
));
future::ok::<_, ()>(res.body("Hello world!"))
})
.tcp()

107
actix-http/examples/ws.rs Normal file
View File

@ -0,0 +1,107 @@
//! Sets up a WebSocket server over TCP and TLS.
//! Sends a heartbeat message every 4 seconds but does not respond to any incoming frames.
extern crate tls_rustls as rustls;
use std::{
env, io,
pin::Pin,
task::{Context, Poll},
time::Duration,
};
use actix_codec::Encoder;
use actix_http::{error::Error, ws, HttpService, Request, Response};
use actix_rt::time::{interval, Interval};
use actix_server::Server;
use bytes::{Bytes, BytesMut};
use bytestring::ByteString;
use futures_core::{ready, Stream};
#[actix_rt::main]
async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "actix=info,h2_ws=info");
env_logger::init();
Server::build()
.bind("tcp", ("127.0.0.1", 8080), || {
HttpService::build().h1(handler).tcp()
})?
.bind("tls", ("127.0.0.1", 8443), || {
HttpService::build().finish(handler).rustls(tls_config())
})?
.run()
.await
}
async fn handler(req: Request) -> Result<Response, Error> {
log::info!("handshaking");
let mut res = ws::handshake(req.head())?;
// handshake will always fail under HTTP/2
log::info!("responding");
Ok(res.streaming(Heartbeat::new(ws::Codec::new())))
}
struct Heartbeat {
codec: ws::Codec,
interval: Interval,
}
impl Heartbeat {
fn new(codec: ws::Codec) -> Self {
Self {
codec,
interval: interval(Duration::from_secs(4)),
}
}
}
impl Stream for Heartbeat {
type Item = Result<Bytes, Error>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
log::trace!("poll");
ready!(self.as_mut().interval.poll_tick(cx));
let mut buffer = BytesMut::new();
self.as_mut()
.codec
.encode(
ws::Message::Text(ByteString::from_static("hello world")),
&mut buffer,
)
.unwrap();
Poll::Ready(Some(Ok(buffer.freeze())))
}
}
fn tls_config() -> rustls::ServerConfig {
use std::io::BufReader;
use rustls::{
internal::pemfile::{certs, pkcs8_private_keys},
NoClientAuth, ServerConfig,
};
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
let cert_file = cert.serialize_pem().unwrap();
let key_file = cert.serialize_private_key_pem();
let mut config = ServerConfig::new(NoClientAuth::new());
let cert_file = &mut BufReader::new(cert_file.as_bytes());
let key_file = &mut BufReader::new(key_file.as_bytes());
let cert_chain = certs(cert_file).unwrap();
let mut keys = pkcs8_private_keys(key_file).unwrap();
config.set_single_cert(cert_chain, keys.remove(0)).unwrap();
config
}

View File

@ -1,723 +0,0 @@
use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{fmt, mem};
use bytes::{Bytes, BytesMut};
use futures_core::Stream;
use futures_util::ready;
use pin_project::pin_project;
use crate::error::Error;
#[derive(Debug, PartialEq, Copy, Clone)]
/// Body size hint
pub enum BodySize {
None,
Empty,
Sized(u64),
Stream,
}
impl BodySize {
pub fn is_eof(&self) -> bool {
matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0))
}
}
/// Type that provides this trait can be streamed to a peer.
pub trait MessageBody {
fn size(&self) -> BodySize;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>>;
downcast_get_type_id!();
}
downcast!(MessageBody);
impl MessageBody for () {
fn size(&self) -> BodySize {
BodySize::Empty
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
Poll::Ready(None)
}
}
impl<T: MessageBody + Unpin> MessageBody for Box<T> {
fn size(&self) -> BodySize {
self.as_ref().size()
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
Pin::new(self.get_mut().as_mut()).poll_next(cx)
}
}
#[pin_project(project = ResponseBodyProj)]
pub enum ResponseBody<B> {
Body(#[pin] B),
Other(#[pin] Body),
}
impl ResponseBody<Body> {
pub fn into_body<B>(self) -> ResponseBody<B> {
match self {
ResponseBody::Body(b) => ResponseBody::Other(b),
ResponseBody::Other(b) => ResponseBody::Other(b),
}
}
}
impl<B> ResponseBody<B> {
pub fn take_body(&mut self) -> ResponseBody<B> {
std::mem::replace(self, ResponseBody::Other(Body::None))
}
}
impl<B: MessageBody> ResponseBody<B> {
pub fn as_ref(&self) -> Option<&B> {
if let ResponseBody::Body(ref b) = self {
Some(b)
} else {
None
}
}
}
impl<B: MessageBody> MessageBody for ResponseBody<B> {
fn size(&self) -> BodySize {
match self {
ResponseBody::Body(ref body) => body.size(),
ResponseBody::Other(ref body) => body.size(),
}
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
match self.project() {
ResponseBodyProj::Body(body) => body.poll_next(cx),
ResponseBodyProj::Other(body) => body.poll_next(cx),
}
}
}
impl<B: MessageBody> Stream for ResponseBody<B> {
type Item = Result<Bytes, Error>;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
match self.project() {
ResponseBodyProj::Body(body) => body.poll_next(cx),
ResponseBodyProj::Other(body) => body.poll_next(cx),
}
}
}
#[pin_project(project = BodyProj)]
/// Represents various types of http message body.
pub enum Body {
/// Empty response. `Content-Length` header is not set.
None,
/// Zero sized response body. `Content-Length` header is set to `0`.
Empty,
/// Specific response body.
Bytes(Bytes),
/// Generic message body.
Message(Box<dyn MessageBody + Unpin>),
}
impl Body {
/// Create body from slice (copy)
pub fn from_slice(s: &[u8]) -> Body {
Body::Bytes(Bytes::copy_from_slice(s))
}
/// Create body from generic message body.
pub fn from_message<B: MessageBody + Unpin + 'static>(body: B) -> Body {
Body::Message(Box::new(body))
}
}
impl MessageBody for Body {
fn size(&self) -> BodySize {
match self {
Body::None => BodySize::None,
Body::Empty => BodySize::Empty,
Body::Bytes(ref bin) => BodySize::Sized(bin.len() as u64),
Body::Message(ref body) => body.size(),
}
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
match self.project() {
BodyProj::None => Poll::Ready(None),
BodyProj::Empty => Poll::Ready(None),
BodyProj::Bytes(ref mut bin) => {
let len = bin.len();
if len == 0 {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(bin))))
}
}
BodyProj::Message(ref mut body) => Pin::new(body.as_mut()).poll_next(cx),
}
}
}
impl PartialEq for Body {
fn eq(&self, other: &Body) -> bool {
match *self {
Body::None => matches!(*other, Body::None),
Body::Empty => matches!(*other, Body::Empty),
Body::Bytes(ref b) => match *other {
Body::Bytes(ref b2) => b == b2,
_ => false,
},
Body::Message(_) => false,
}
}
}
impl fmt::Debug for Body {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Body::None => write!(f, "Body::None"),
Body::Empty => write!(f, "Body::Empty"),
Body::Bytes(ref b) => write!(f, "Body::Bytes({:?})", b),
Body::Message(_) => write!(f, "Body::Message(_)"),
}
}
}
impl From<&'static str> for Body {
fn from(s: &'static str) -> Body {
Body::Bytes(Bytes::from_static(s.as_ref()))
}
}
impl From<&'static [u8]> for Body {
fn from(s: &'static [u8]) -> Body {
Body::Bytes(Bytes::from_static(s))
}
}
impl From<Vec<u8>> for Body {
fn from(vec: Vec<u8>) -> Body {
Body::Bytes(Bytes::from(vec))
}
}
impl From<String> for Body {
fn from(s: String) -> Body {
s.into_bytes().into()
}
}
impl<'a> From<&'a String> for Body {
fn from(s: &'a String) -> Body {
Body::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s)))
}
}
impl From<Bytes> for Body {
fn from(s: Bytes) -> Body {
Body::Bytes(s)
}
}
impl From<BytesMut> for Body {
fn from(s: BytesMut) -> Body {
Body::Bytes(s.freeze())
}
}
impl From<serde_json::Value> for Body {
fn from(v: serde_json::Value) -> Body {
Body::Bytes(v.to_string().into())
}
}
impl<S> From<SizedStream<S>> for Body
where
S: Stream<Item = Result<Bytes, Error>> + Unpin + 'static,
{
fn from(s: SizedStream<S>) -> Body {
Body::from_message(s)
}
}
impl<S, E> From<BodyStream<S, E>> for Body
where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
E: Into<Error> + 'static,
{
fn from(s: BodyStream<S, E>) -> Body {
Body::from_message(s)
}
}
impl MessageBody for Bytes {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
}
}
}
impl MessageBody for BytesMut {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
}
}
}
impl MessageBody for &'static str {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from_static(
mem::take(self.get_mut()).as_ref(),
))))
}
}
}
impl MessageBody for Vec<u8> {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut())))))
}
}
}
impl MessageBody for String {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from(
mem::take(self.get_mut()).into_bytes(),
))))
}
}
}
/// Type represent streaming body.
/// Response does not contain `content-length` header and appropriate transfer encoding is used.
#[pin_project]
pub struct BodyStream<S: Unpin, E> {
#[pin]
stream: S,
_t: PhantomData<E>,
}
impl<S, E> BodyStream<S, E>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
E: Into<Error>,
{
pub fn new(stream: S) -> Self {
BodyStream {
stream,
_t: PhantomData,
}
}
}
impl<S, E> MessageBody for BodyStream<S, E>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
E: Into<Error>,
{
fn size(&self) -> BodySize {
BodySize::Stream
}
/// Attempts to pull out the next value of the underlying [`Stream`].
///
/// Empty values are skipped to prevent [`BodyStream`]'s transmission being
/// ended on a zero-length chunk, but rather proceed until the underlying
/// [`Stream`] ends.
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
let mut stream = self.project().stream;
loop {
let stream = stream.as_mut();
return Poll::Ready(match ready!(stream.poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
opt => opt.map(|res| res.map_err(Into::into)),
});
}
}
}
/// Type represent streaming body. This body implementation should be used
/// if total size of stream is known. Data get sent as is without using transfer encoding.
#[pin_project]
pub struct SizedStream<S: Unpin> {
size: u64,
#[pin]
stream: S,
}
impl<S> SizedStream<S>
where
S: Stream<Item = Result<Bytes, Error>> + Unpin,
{
pub fn new(size: u64, stream: S) -> Self {
SizedStream { size, stream }
}
}
impl<S> MessageBody for SizedStream<S>
where
S: Stream<Item = Result<Bytes, Error>> + Unpin,
{
fn size(&self) -> BodySize {
BodySize::Sized(self.size as u64)
}
/// Attempts to pull out the next value of the underlying [`Stream`].
///
/// Empty values are skipped to prevent [`SizedStream`]'s transmission being
/// ended on a zero-length chunk, but rather proceed until the underlying
/// [`Stream`] ends.
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
let mut stream: Pin<&mut S> = self.project().stream;
loop {
let stream = stream.as_mut();
return Poll::Ready(match ready!(stream.poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
val => val,
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures_util::future::poll_fn;
use futures_util::pin_mut;
use futures_util::stream;
impl Body {
pub(crate) fn get_ref(&self) -> &[u8] {
match *self {
Body::Bytes(ref bin) => &bin,
_ => panic!(),
}
}
}
impl ResponseBody<Body> {
pub(crate) fn get_ref(&self) -> &[u8] {
match *self {
ResponseBody::Body(ref b) => b.get_ref(),
ResponseBody::Other(ref b) => b.get_ref(),
}
}
}
#[actix_rt::test]
async fn test_static_str() {
assert_eq!(Body::from("").size(), BodySize::Sized(0));
assert_eq!(Body::from("test").size(), BodySize::Sized(4));
assert_eq!(Body::from("test").get_ref(), b"test");
assert_eq!("test".size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| Pin::new(&mut "test").poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_static_bytes() {
assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test");
assert_eq!(
Body::from_slice(b"test".as_ref()).size(),
BodySize::Sized(4)
);
assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test");
let sb = Bytes::from(&b"test"[..]);
pin_mut!(sb);
assert_eq!(sb.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_vec() {
assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4));
assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test");
let test_vec = Vec::from("test");
pin_mut!(test_vec);
assert_eq!(test_vec.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| test_vec.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_bytes() {
let b = Bytes::from("test");
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
pin_mut!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_bytes_mut() {
let b = BytesMut::from("test");
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
pin_mut!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_string() {
let b = "test".to_owned();
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
assert_eq!(Body::from(&b).size(), BodySize::Sized(4));
assert_eq!(Body::from(&b).get_ref(), b"test");
pin_mut!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_unit() {
assert_eq!(().size(), BodySize::Empty);
assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx))
.await
.is_none());
}
#[actix_rt::test]
async fn test_box() {
let val = Box::new(());
pin_mut!(val);
assert_eq!(val.size(), BodySize::Empty);
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
}
#[actix_rt::test]
async fn test_body_eq() {
assert!(
Body::Bytes(Bytes::from_static(b"1"))
== Body::Bytes(Bytes::from_static(b"1"))
);
assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None);
}
#[actix_rt::test]
async fn test_body_debug() {
assert!(format!("{:?}", Body::None).contains("Body::None"));
assert!(format!("{:?}", Body::Empty).contains("Body::Empty"));
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1'));
}
#[actix_rt::test]
async fn test_serde_json() {
use serde_json::json;
assert_eq!(
Body::from(serde_json::Value::String("test".into())).size(),
BodySize::Sized(6)
);
assert_eq!(
Body::from(json!({"test-key":"test-value"})).size(),
BodySize::Sized(25)
);
}
mod body_stream {
use super::*;
//use futures::task::noop_waker;
//use futures::stream::once;
#[actix_rt::test]
async fn skips_empty_chunks() {
let body = BodyStream::new(stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok(Bytes::from(v)) as Result<Bytes, ()>),
));
pin_mut!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
/* Now it does not compile as it should
#[actix_rt::test]
async fn move_pinned_pointer() {
let (sender, receiver) = futures::channel::oneshot::channel();
let mut body_stream = Ok(BodyStream::new(once(async {
let x = Box::new(0i32);
let y = &x;
receiver.await.unwrap();
let _z = **y;
Ok::<_, ()>(Bytes::new())
})));
let waker = noop_waker();
let mut context = Context::from_waker(&waker);
pin_mut!(body_stream);
let _ = body_stream.as_mut().unwrap().poll_next(&mut context);
sender.send(()).unwrap();
let _ = std::mem::replace(&mut body_stream, Err([0; 32])).unwrap().poll_next(&mut context);
}*/
}
mod sized_stream {
use super::*;
#[actix_rt::test]
async fn skips_empty_chunks() {
let body = SizedStream::new(
2,
stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))),
);
pin_mut!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
}
#[actix_rt::test]
async fn test_body_casting() {
let mut body = String::from("hello cast");
let resp_body: &mut dyn MessageBody = &mut body;
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast");
let body = &mut resp_body.downcast_mut::<String>().unwrap();
body.push('!');
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast!");
let not_body = resp_body.downcast_ref::<()>();
assert!(not_body.is_none());
}
}

158
actix-http/src/body/body.rs Normal file
View File

@ -0,0 +1,158 @@
use std::{
fmt, mem,
pin::Pin,
task::{Context, Poll},
};
use bytes::{Bytes, BytesMut};
use futures_core::Stream;
use crate::error::Error;
use super::{BodySize, BodyStream, MessageBody, SizedStream};
/// Represents various types of HTTP message body.
pub enum Body {
/// Empty response. `Content-Length` header is not set.
None,
/// Zero sized response body. `Content-Length` header is set to `0`.
Empty,
/// Specific response body.
Bytes(Bytes),
/// Generic message body.
Message(Box<dyn MessageBody + Unpin>),
}
impl Body {
/// Create body from slice (copy)
pub fn from_slice(s: &[u8]) -> Body {
Body::Bytes(Bytes::copy_from_slice(s))
}
/// Create body from generic message body.
pub fn from_message<B: MessageBody + Unpin + 'static>(body: B) -> Body {
Body::Message(Box::new(body))
}
}
impl MessageBody for Body {
fn size(&self) -> BodySize {
match self {
Body::None => BodySize::None,
Body::Empty => BodySize::Empty,
Body::Bytes(ref bin) => BodySize::Sized(bin.len() as u64),
Body::Message(ref body) => body.size(),
}
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
match self.get_mut() {
Body::None => Poll::Ready(None),
Body::Empty => Poll::Ready(None),
Body::Bytes(ref mut bin) => {
let len = bin.len();
if len == 0 {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(bin))))
}
}
Body::Message(body) => Pin::new(&mut **body).poll_next(cx),
}
}
}
impl PartialEq for Body {
fn eq(&self, other: &Body) -> bool {
match *self {
Body::None => matches!(*other, Body::None),
Body::Empty => matches!(*other, Body::Empty),
Body::Bytes(ref b) => match *other {
Body::Bytes(ref b2) => b == b2,
_ => false,
},
Body::Message(_) => false,
}
}
}
impl fmt::Debug for Body {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Body::None => write!(f, "Body::None"),
Body::Empty => write!(f, "Body::Empty"),
Body::Bytes(ref b) => write!(f, "Body::Bytes({:?})", b),
Body::Message(_) => write!(f, "Body::Message(_)"),
}
}
}
impl From<&'static str> for Body {
fn from(s: &'static str) -> Body {
Body::Bytes(Bytes::from_static(s.as_ref()))
}
}
impl From<&'static [u8]> for Body {
fn from(s: &'static [u8]) -> Body {
Body::Bytes(Bytes::from_static(s))
}
}
impl From<Vec<u8>> for Body {
fn from(vec: Vec<u8>) -> Body {
Body::Bytes(Bytes::from(vec))
}
}
impl From<String> for Body {
fn from(s: String) -> Body {
s.into_bytes().into()
}
}
impl<'a> From<&'a String> for Body {
fn from(s: &'a String) -> Body {
Body::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s)))
}
}
impl From<Bytes> for Body {
fn from(s: Bytes) -> Body {
Body::Bytes(s)
}
}
impl From<BytesMut> for Body {
fn from(s: BytesMut) -> Body {
Body::Bytes(s.freeze())
}
}
impl From<serde_json::Value> for Body {
fn from(v: serde_json::Value) -> Body {
Body::Bytes(v.to_string().into())
}
}
impl<S> From<SizedStream<S>> for Body
where
S: Stream<Item = Result<Bytes, Error>> + Unpin + 'static,
{
fn from(s: SizedStream<S>) -> Body {
Body::from_message(s)
}
}
impl<S, E> From<BodyStream<S>> for Body
where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
E: Into<Error> + 'static,
{
fn from(s: BodyStream<S>) -> Body {
Body::from_message(s)
}
}

View File

@ -0,0 +1,59 @@
use std::{
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use futures_core::{ready, Stream};
use crate::error::Error;
use super::{BodySize, MessageBody};
/// Streaming response wrapper.
///
/// Response does not contain `Content-Length` header and appropriate transfer encoding is used.
pub struct BodyStream<S: Unpin> {
stream: S,
}
impl<S, E> BodyStream<S>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
E: Into<Error>,
{
pub fn new(stream: S) -> Self {
BodyStream { stream }
}
}
impl<S, E> MessageBody for BodyStream<S>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
E: Into<Error>,
{
fn size(&self) -> BodySize {
BodySize::Stream
}
/// Attempts to pull out the next value of the underlying [`Stream`].
///
/// Empty values are skipped to prevent [`BodyStream`]'s transmission being
/// ended on a zero-length chunk, but rather proceed until the underlying
/// [`Stream`] ends.
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
loop {
let stream = &mut self.as_mut().stream;
let chunk = match ready!(Pin::new(stream).poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
opt => opt.map(|res| res.map_err(Into::into)),
};
return Poll::Ready(chunk);
}
}
}

View File

@ -0,0 +1,142 @@
//! [`MessageBody`] trait and foreign implementations.
use std::{
mem,
pin::Pin,
task::{Context, Poll},
};
use bytes::{Bytes, BytesMut};
use crate::error::Error;
use super::BodySize;
/// Type that implement this trait can be streamed to a peer.
pub trait MessageBody {
fn size(&self) -> BodySize;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>>;
downcast_get_type_id!();
}
downcast!(MessageBody);
impl MessageBody for () {
fn size(&self) -> BodySize {
BodySize::Empty
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
Poll::Ready(None)
}
}
impl<T: MessageBody + Unpin> MessageBody for Box<T> {
fn size(&self) -> BodySize {
self.as_ref().size()
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
Pin::new(self.get_mut().as_mut()).poll_next(cx)
}
}
impl MessageBody for Bytes {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
}
}
}
impl MessageBody for BytesMut {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
}
}
}
impl MessageBody for &'static str {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from_static(
mem::take(self.get_mut()).as_ref(),
))))
}
}
}
impl MessageBody for Vec<u8> {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut())))))
}
}
}
impl MessageBody for String {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from(
mem::take(self.get_mut()).into_bytes(),
))))
}
}
}

252
actix-http/src/body/mod.rs Normal file
View File

@ -0,0 +1,252 @@
//! Traits and structures to aid consuming and writing HTTP payloads.
#[allow(clippy::module_inception)]
mod body;
mod body_stream;
mod message_body;
mod response_body;
mod size;
mod sized_stream;
pub use self::body::Body;
pub use self::body_stream::BodyStream;
pub use self::message_body::MessageBody;
pub use self::response_body::ResponseBody;
pub use self::size::BodySize;
pub use self::sized_stream::SizedStream;
#[cfg(test)]
mod tests {
use std::pin::Pin;
use actix_rt::pin;
use bytes::{Bytes, BytesMut};
use futures_util::{future::poll_fn, stream};
use super::*;
impl Body {
pub(crate) fn get_ref(&self) -> &[u8] {
match *self {
Body::Bytes(ref bin) => &bin,
_ => panic!(),
}
}
}
impl ResponseBody<Body> {
pub(crate) fn get_ref(&self) -> &[u8] {
match *self {
ResponseBody::Body(ref b) => b.get_ref(),
ResponseBody::Other(ref b) => b.get_ref(),
}
}
}
#[actix_rt::test]
async fn test_static_str() {
assert_eq!(Body::from("").size(), BodySize::Sized(0));
assert_eq!(Body::from("test").size(), BodySize::Sized(4));
assert_eq!(Body::from("test").get_ref(), b"test");
assert_eq!("test".size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| Pin::new(&mut "test").poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_static_bytes() {
assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test");
assert_eq!(
Body::from_slice(b"test".as_ref()).size(),
BodySize::Sized(4)
);
assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test");
let sb = Bytes::from(&b"test"[..]);
pin!(sb);
assert_eq!(sb.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_vec() {
assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4));
assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test");
let test_vec = Vec::from("test");
pin!(test_vec);
assert_eq!(test_vec.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| test_vec.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_bytes() {
let b = Bytes::from("test");
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
pin!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_bytes_mut() {
let b = BytesMut::from("test");
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
pin!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_string() {
let b = "test".to_owned();
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
assert_eq!(Body::from(&b).size(), BodySize::Sized(4));
assert_eq!(Body::from(&b).get_ref(), b"test");
pin!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_unit() {
assert_eq!(().size(), BodySize::Empty);
assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx))
.await
.is_none());
}
#[actix_rt::test]
async fn test_box() {
let val = Box::new(());
pin!(val);
assert_eq!(val.size(), BodySize::Empty);
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
}
#[actix_rt::test]
async fn test_body_eq() {
assert!(
Body::Bytes(Bytes::from_static(b"1"))
== Body::Bytes(Bytes::from_static(b"1"))
);
assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None);
}
#[actix_rt::test]
async fn test_body_debug() {
assert!(format!("{:?}", Body::None).contains("Body::None"));
assert!(format!("{:?}", Body::Empty).contains("Body::Empty"));
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1'));
}
#[actix_rt::test]
async fn test_serde_json() {
use serde_json::json;
assert_eq!(
Body::from(serde_json::Value::String("test".into())).size(),
BodySize::Sized(6)
);
assert_eq!(
Body::from(json!({"test-key":"test-value"})).size(),
BodySize::Sized(25)
);
}
#[actix_rt::test]
async fn body_stream_skips_empty_chunks() {
let body = BodyStream::new(stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok(Bytes::from(v)) as Result<Bytes, ()>),
));
pin!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
mod sized_stream {
use super::*;
#[actix_rt::test]
async fn skips_empty_chunks() {
let body = SizedStream::new(
2,
stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))),
);
pin!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
}
#[actix_rt::test]
async fn test_body_casting() {
let mut body = String::from("hello cast");
let resp_body: &mut dyn MessageBody = &mut body;
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast");
let body = &mut resp_body.downcast_mut::<String>().unwrap();
body.push('!');
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast!");
let not_body = resp_body.downcast_ref::<()>();
assert!(not_body.is_none());
}
}

View File

@ -0,0 +1,77 @@
use std::{
mem,
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use futures_core::Stream;
use pin_project::pin_project;
use crate::error::Error;
use super::{Body, BodySize, MessageBody};
#[pin_project(project = ResponseBodyProj)]
pub enum ResponseBody<B> {
Body(#[pin] B),
Other(Body),
}
impl ResponseBody<Body> {
pub fn into_body<B>(self) -> ResponseBody<B> {
match self {
ResponseBody::Body(b) => ResponseBody::Other(b),
ResponseBody::Other(b) => ResponseBody::Other(b),
}
}
}
impl<B> ResponseBody<B> {
pub fn take_body(&mut self) -> ResponseBody<B> {
mem::replace(self, ResponseBody::Other(Body::None))
}
}
impl<B: MessageBody> ResponseBody<B> {
pub fn as_ref(&self) -> Option<&B> {
if let ResponseBody::Body(ref b) = self {
Some(b)
} else {
None
}
}
}
impl<B: MessageBody> MessageBody for ResponseBody<B> {
fn size(&self) -> BodySize {
match self {
ResponseBody::Body(ref body) => body.size(),
ResponseBody::Other(ref body) => body.size(),
}
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
match self.project() {
ResponseBodyProj::Body(body) => body.poll_next(cx),
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
}
}
}
impl<B: MessageBody> Stream for ResponseBody<B> {
type Item = Result<Bytes, Error>;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
match self.project() {
ResponseBodyProj::Body(body) => body.poll_next(cx),
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
}
}
}

View File

@ -0,0 +1,40 @@
/// Body size hint.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BodySize {
/// Absence of body can be assumed from method or status code.
///
/// Will skip writing Content-Length header.
None,
/// Zero size body.
///
/// Will write `Content-Length: 0` header.
Empty,
/// Known size body.
///
/// Will write `Content-Length: N` header. `Sized(0)` is treated the same as `Empty`.
Sized(u64),
/// Unknown size body.
///
/// Will not write Content-Length header. Can be used with chunked Transfer-Encoding.
Stream,
}
impl BodySize {
/// Returns true if size hint indicates no or empty body.
///
/// ```
/// # use actix_http::body::BodySize;
/// assert!(BodySize::None.is_eof());
/// assert!(BodySize::Empty.is_eof());
/// assert!(BodySize::Sized(0).is_eof());
///
/// assert!(!BodySize::Sized(64).is_eof());
/// assert!(!BodySize::Stream.is_eof());
/// ```
pub fn is_eof(&self) -> bool {
matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0))
}
}

View File

@ -0,0 +1,59 @@
use std::{
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use futures_core::{ready, Stream};
use crate::error::Error;
use super::{BodySize, MessageBody};
/// Known sized streaming response wrapper.
///
/// This body implementation should be used if total size of stream is known. Data get sent as is
/// without using transfer encoding.
pub struct SizedStream<S: Unpin> {
size: u64,
stream: S,
}
impl<S> SizedStream<S>
where
S: Stream<Item = Result<Bytes, Error>> + Unpin,
{
pub fn new(size: u64, stream: S) -> Self {
SizedStream { size, stream }
}
}
impl<S> MessageBody for SizedStream<S>
where
S: Stream<Item = Result<Bytes, Error>> + Unpin,
{
fn size(&self) -> BodySize {
BodySize::Sized(self.size as u64)
}
/// Attempts to pull out the next value of the underlying [`Stream`].
///
/// Empty values are skipped to prevent [`SizedStream`]'s transmission being
/// ended on a zero-length chunk, but rather proceed until the underlying
/// [`Stream`] ends.
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
loop {
let stream = &mut self.as_mut().stream;
let chunk = match ready!(Pin::new(stream).poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
val => val,
};
return Poll::Ready(chunk);
}
}
}

View File

@ -10,7 +10,6 @@ use crate::config::{KeepAlive, ServiceConfig};
use crate::error::Error;
use crate::h1::{Codec, ExpectHandler, H1Service, UpgradeHandler};
use crate::h2::H2Service;
use crate::helpers::{Data, DataFactory};
use crate::request::Request;
use crate::response::Response;
use crate::service::HttpService;
@ -20,7 +19,7 @@ use crate::{ConnectCallback, Extensions};
///
/// This type can be used to construct an instance of [`HttpService`] through a
/// builder-like pattern.
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> {
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
keep_alive: KeepAlive,
client_timeout: u64,
client_disconnect: u64,
@ -28,18 +27,16 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> {
local_addr: Option<net::SocketAddr>,
expect: X,
upgrade: Option<U>,
// 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, S)>,
_phantom: PhantomData<S>,
}
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler<T>>
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
where
S: ServiceFactory<Config = (), Request = Request>,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
<S::Service as Service>::Future: 'static,
<S::Service as Service<Request>>::Future: 'static,
{
/// Create instance of `ServiceConfigBuilder`
pub fn new() -> Self {
@ -51,27 +48,26 @@ where
local_addr: None,
expect: ExpectHandler,
upgrade: None,
on_connect: None,
on_connect_ext: None,
_t: PhantomData,
_phantom: PhantomData,
}
}
}
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
where
S: ServiceFactory<Config = (), Request = Request>,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
<S::Service as Service>::Future: 'static,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
<S::Service as Service<Request>>::Future: 'static,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
<X::Service as Service>::Future: 'static,
U: ServiceFactory<Config = (), Request = (Request, Framed<T, Codec>), Response = ()>,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
<U::Service as Service>::Future: 'static,
<U::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
{
/// Set server keep-alive setting.
///
@ -127,11 +123,11 @@ where
/// request will be forwarded to main service.
pub fn expect<F, X1>(self, expect: F) -> HttpServiceBuilder<T, S, X1, U>
where
F: IntoServiceFactory<X1>,
X1: ServiceFactory<Config = (), Request = Request, Response = Request>,
F: IntoServiceFactory<X1, Request>,
X1: ServiceFactory<Request, Config = (), Response = Request>,
X1::Error: Into<Error>,
X1::InitError: fmt::Debug,
<X1::Service as Service>::Future: 'static,
<X1::Service as Service<Request>>::Future: 'static,
{
HttpServiceBuilder {
keep_alive: self.keep_alive,
@ -141,9 +137,8 @@ where
local_addr: self.local_addr,
expect: expect.into_factory(),
upgrade: self.upgrade,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData,
_phantom: PhantomData,
}
}
@ -153,15 +148,11 @@ where
/// and this service get called with original request and framed object.
pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1>
where
F: IntoServiceFactory<U1>,
U1: ServiceFactory<
Config = (),
Request = (Request, Framed<T, Codec>),
Response = (),
>,
F: IntoServiceFactory<U1, (Request, Framed<T, Codec>)>,
U1: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
U1::Error: fmt::Display,
U1::InitError: fmt::Debug,
<U1::Service as Service>::Future: 'static,
<U1::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
{
HttpServiceBuilder {
keep_alive: self.keep_alive,
@ -171,26 +162,11 @@ where
local_addr: self.local_addr,
expect: self.expect,
upgrade: Some(upgrade.into_factory()),
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData,
_phantom: PhantomData,
}
}
/// Set on-connect callback.
///
/// Called once per connection. Return value of the call is stored in request extensions.
///
/// *SOFT DEPRECATED*: Prefer the `on_connect_ext` style callback.
pub fn on_connect<F, I>(mut self, f: F) -> Self
where
F: Fn(&T) -> I + 'static,
I: Clone + 'static,
{
self.on_connect = Some(Rc::new(move |io| Box::new(Data(f(io)))));
self
}
/// Sets the callback to be run on connection establishment.
///
/// Has mutable access to a data container that will be merged into request extensions.
@ -208,7 +184,7 @@ where
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
where
B: MessageBody,
F: IntoServiceFactory<S>,
F: IntoServiceFactory<S, Request>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
@ -224,7 +200,6 @@ where
H1Service::with_config(cfg, service.into_factory())
.expect(self.expect)
.upgrade(self.upgrade)
.on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext)
}
@ -232,11 +207,11 @@ where
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
where
B: MessageBody + 'static,
F: IntoServiceFactory<S>,
F: IntoServiceFactory<S, Request>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
<S::Service as Service<Request>>::Future: 'static,
{
let cfg = ServiceConfig::new(
self.keep_alive,
@ -247,7 +222,6 @@ where
);
H2Service::with_config(cfg, service.into_factory())
.on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext)
}
@ -255,11 +229,11 @@ where
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
where
B: MessageBody + 'static,
F: IntoServiceFactory<S>,
F: IntoServiceFactory<S, Request>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
<S::Service as Service<Request>>::Future: 'static,
{
let cfg = ServiceConfig::new(
self.keep_alive,
@ -272,7 +246,6 @@ where
HttpService::with_config(cfg, service.into_factory())
.expect(self.expect)
.upgrade(self.upgrade)
.on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext)
}
}

View File

@ -1,8 +1,8 @@
use std::net::IpAddr;
use std::time::Duration;
// These values are taken from hyper/src/proto/h2/client.rs
const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2mb
const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1mb
const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2MB
const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB
/// Connector configuration
#[derive(Clone)]
@ -14,18 +14,20 @@ pub(crate) struct ConnectorConfig {
pub(crate) limit: usize,
pub(crate) conn_window_size: u32,
pub(crate) stream_window_size: u32,
pub(crate) local_address: Option<IpAddr>,
}
impl Default for ConnectorConfig {
fn default() -> Self {
Self {
timeout: Duration::from_secs(1),
timeout: Duration::from_secs(5),
conn_lifetime: Duration::from_secs(75),
conn_keep_alive: Duration::from_secs(15),
disconnect_timeout: Some(Duration::from_millis(3000)),
limit: 100,
conn_window_size: DEFAULT_H2_CONN_WINDOW,
stream_window_size: DEFAULT_H2_STREAM_WINDOW,
local_address: None,
}
}
}

View File

@ -1,11 +1,12 @@
use std::future::Future;
use std::ops::{Deref, DerefMut};
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{fmt, io, mem, time};
use std::{fmt, io, time};
use actix_codec::{AsyncRead, AsyncWrite, Framed};
use bytes::{Buf, Bytes};
use futures_util::future::{err, Either, FutureExt, LocalBoxFuture, Ready};
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf};
use actix_rt::task::JoinHandle;
use bytes::Bytes;
use futures_core::future::LocalBoxFuture;
use h2::client::SendRequest;
use pin_project::pin_project;
@ -15,33 +16,82 @@ use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::Payload;
use super::error::SendRequestError;
use super::pool::{Acquired, Protocol};
use super::pool::Acquired;
use super::{h1proto, h2proto};
pub(crate) enum ConnectionType<Io> {
H1(Io),
H2(SendRequest<Bytes>),
H2(H2Connection),
}
/// `H2Connection` has two parts: `SendRequest` and `Connection`.
///
/// `Connection` is spawned as an async task on runtime and `H2Connection` holds a handle for
/// this task. Therefore, it can wake up and quit the task when SendRequest is dropped.
pub(crate) struct H2Connection {
handle: JoinHandle<()>,
sender: SendRequest<Bytes>,
}
impl H2Connection {
pub(crate) fn new<Io>(
sender: SendRequest<Bytes>,
connection: h2::client::Connection<Io>,
) -> Self
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{
let handle = actix_rt::spawn(async move {
let _ = connection.await;
});
Self { handle, sender }
}
}
// cancel spawned connection task on drop.
impl Drop for H2Connection {
fn drop(&mut self) {
self.handle.abort();
}
}
// only expose sender type to public.
impl Deref for H2Connection {
type Target = SendRequest<Bytes>;
fn deref(&self) -> &Self::Target {
&self.sender
}
}
impl DerefMut for H2Connection {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sender
}
}
pub trait Connection {
type Io: AsyncRead + AsyncWrite + Unpin;
type Future: Future<Output = Result<(ResponseHead, Payload), SendRequestError>>;
fn protocol(&self) -> Protocol;
/// Send request and body
fn send_request<B: MessageBody + 'static, H: Into<RequestHeadType>>(
fn send_request<B, H>(
self,
head: H,
body: B,
) -> Self::Future;
type TunnelFuture: Future<
Output = Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
>;
) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>
where
B: MessageBody + 'static,
H: Into<RequestHeadType> + 'static;
/// Send request, returns Response and Framed
fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture;
fn open_tunnel<H: Into<RequestHeadType> + 'static>(
self,
head: H,
) -> LocalBoxFuture<
'static,
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
>;
}
pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
@ -54,7 +104,10 @@ pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
#[doc(hidden)]
/// HTTP client connection
pub struct IoConnection<T> {
pub struct IoConnection<T>
where
T: AsyncWrite + Unpin + 'static,
{
io: Option<ConnectionType<T>>,
created: time::Instant,
pool: Option<Acquired<T>>,
@ -62,7 +115,7 @@ pub struct IoConnection<T> {
impl<T> fmt::Debug for IoConnection<T>
where
T: fmt::Debug,
T: AsyncWrite + Unpin + fmt::Debug + 'static,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.io {
@ -89,55 +142,36 @@ impl<T: AsyncRead + AsyncWrite + Unpin> IoConnection<T> {
pub(crate) fn into_inner(self) -> (ConnectionType<T>, time::Instant) {
(self.io.unwrap(), self.created)
}
}
impl<T> Connection for IoConnection<T>
where
T: AsyncRead + AsyncWrite + Unpin + 'static,
{
type Io = T;
type Future =
LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>;
fn protocol(&self) -> Protocol {
match self.io {
Some(ConnectionType::H1(_)) => Protocol::Http1,
Some(ConnectionType::H2(_)) => Protocol::Http2,
None => Protocol::Http1,
}
#[cfg(test)]
pub(crate) fn into_parts(self) -> (ConnectionType<T>, time::Instant, Acquired<T>) {
(self.io.unwrap(), self.created, self.pool.unwrap())
}
fn send_request<B: MessageBody + 'static, H: Into<RequestHeadType>>(
async fn send_request<B: MessageBody + 'static, H: Into<RequestHeadType>>(
mut self,
head: H,
body: B,
) -> Self::Future {
) -> Result<(ResponseHead, Payload), SendRequestError> {
match self.io.take().unwrap() {
ConnectionType::H1(io) => {
h1proto::send_request(io, head.into(), body, self.created, self.pool)
.boxed_local()
.await
}
ConnectionType::H2(io) => {
h2proto::send_request(io, head.into(), body, self.created, self.pool)
.boxed_local()
.await
}
}
}
type TunnelFuture = Either<
LocalBoxFuture<
'static,
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
>,
Ready<Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>>,
>;
/// Send request, returns Response and Framed
fn open_tunnel<H: Into<RequestHeadType>>(mut self, head: H) -> Self::TunnelFuture {
async fn open_tunnel<H: Into<RequestHeadType>>(
mut self,
head: H,
) -> Result<(ResponseHead, Framed<T, ClientCodec>), SendRequestError> {
match self.io.take().unwrap() {
ConnectionType::H1(io) => {
Either::Left(h1proto::open_tunnel(io, head.into()).boxed_local())
}
ConnectionType::H1(io) => h1proto::open_tunnel(io, head.into()).await,
ConnectionType::H2(io) => {
if let Some(mut pool) = self.pool.take() {
pool.release(IoConnection::new(
@ -146,65 +180,61 @@ where
None,
));
}
Either::Right(err(SendRequestError::TunnelNotSupported))
Err(SendRequestError::TunnelNotSupported)
}
}
}
}
#[allow(dead_code)]
pub(crate) enum EitherConnection<A, B> {
pub(crate) enum EitherIoConnection<A, B>
where
A: AsyncRead + AsyncWrite + Unpin + 'static,
B: AsyncRead + AsyncWrite + Unpin + 'static,
{
A(IoConnection<A>),
B(IoConnection<B>),
}
impl<A, B> Connection for EitherConnection<A, B>
impl<A, B> Connection for EitherIoConnection<A, B>
where
A: AsyncRead + AsyncWrite + Unpin + 'static,
B: AsyncRead + AsyncWrite + Unpin + 'static,
{
type Io = EitherIo<A, B>;
type Future =
LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>;
fn protocol(&self) -> Protocol {
match self {
EitherConnection::A(con) => con.protocol(),
EitherConnection::B(con) => con.protocol(),
}
}
fn send_request<RB: MessageBody + 'static, H: Into<RequestHeadType>>(
fn send_request<RB, H>(
self,
head: H,
body: RB,
) -> Self::Future {
) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>
where
RB: MessageBody + 'static,
H: Into<RequestHeadType> + 'static,
{
match self {
EitherConnection::A(con) => con.send_request(head, body),
EitherConnection::B(con) => con.send_request(head, body),
EitherIoConnection::A(con) => Box::pin(con.send_request(head, body)),
EitherIoConnection::B(con) => Box::pin(con.send_request(head, body)),
}
}
type TunnelFuture = LocalBoxFuture<
'static,
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
>;
/// Send request, returns Response and Framed
fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture {
fn open_tunnel<H: Into<RequestHeadType> + 'static>(
self,
head: H,
) -> LocalBoxFuture<
'static,
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
> {
match self {
EitherConnection::A(con) => con
.open_tunnel(head)
.map(|res| {
res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::A)))
})
.boxed_local(),
EitherConnection::B(con) => con
.open_tunnel(head)
.map(|res| {
res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::B)))
})
.boxed_local(),
EitherIoConnection::A(con) => Box::pin(async {
let (head, framed) = con.open_tunnel(head).await?;
Ok((head, framed.into_map_io(EitherIo::A)))
}),
EitherIoConnection::B(con) => Box::pin(async {
let (head, framed) = con.open_tunnel(head).await?;
Ok((head, framed.into_map_io(EitherIo::B)))
}),
}
}
}
@ -223,23 +253,13 @@ where
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
match self.project() {
EitherIoProj::A(val) => val.poll_read(cx, buf),
EitherIoProj::B(val) => val.poll_read(cx, buf),
}
}
unsafe fn prepare_uninitialized_buffer(
&self,
buf: &mut [mem::MaybeUninit<u8>],
) -> bool {
match self {
EitherIo::A(ref val) => val.prepare_uninitialized_buffer(buf),
EitherIo::B(ref val) => val.prepare_uninitialized_buffer(buf),
}
}
}
impl<A, B> AsyncWrite for EitherIo<A, B>
@ -274,18 +294,36 @@ where
EitherIoProj::B(val) => val.poll_shutdown(cx),
}
}
}
fn poll_write_buf<U: Buf>(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut U,
) -> Poll<Result<usize, io::Error>>
where
Self: Sized,
{
match self.project() {
EitherIoProj::A(val) => val.poll_write_buf(cx, buf),
EitherIoProj::B(val) => val.poll_write_buf(cx, buf),
}
#[cfg(test)]
mod test {
use std::net;
use actix_rt::net::TcpStream;
use super::*;
#[actix_rt::test]
async fn test_h2_connection_drop() {
let addr = "127.0.0.1:0".parse::<net::SocketAddr>().unwrap();
let listener = net::TcpListener::bind(addr).unwrap();
let local = listener.local_addr().unwrap();
std::thread::spawn(move || while listener.accept().is_ok() {});
let tcp = TcpStream::connect(local).await.unwrap();
let (sender, connection) = h2::client::handshake(tcp).await.unwrap();
let conn = H2Connection::new(sender.clone(), connection);
assert!(sender.clone().ready().await.is_ok());
assert!(h2::client::SendRequest::clone(&*conn).ready().await.is_ok());
drop(conn);
match sender.ready().await {
Ok(_) => panic!("connection should be gone and can not be ready"),
Err(e) => assert!(e.is_io()),
};
}
}

View File

@ -1,27 +1,32 @@
use std::fmt;
use std::marker::PhantomData;
use std::time::Duration;
use std::{
fmt,
future::Future,
marker::PhantomData,
net::IpAddr,
pin::Pin,
task::{Context, Poll},
time::Duration,
};
use actix_codec::{AsyncRead, AsyncWrite};
use actix_connect::{
default_connector, Connect as TcpConnect, Connection as TcpConnection,
};
use actix_rt::net::TcpStream;
use actix_service::{apply_fn, Service};
use actix_service::{apply_fn, Service, ServiceExt};
use actix_tls::connect::{
new_connector, Connect as TcpConnect, Connection as TcpConnection, Resolver,
};
use actix_utils::timeout::{TimeoutError, TimeoutService};
use http::Uri;
use super::config::ConnectorConfig;
use super::connection::Connection;
use super::connection::{Connection, EitherIoConnection};
use super::error::ConnectError;
use super::pool::{ConnectionPool, Protocol};
use super::Connect;
#[cfg(feature = "openssl")]
use actix_connect::ssl::openssl::SslConnector as OpensslConnector;
use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector;
#[cfg(feature = "rustls")]
use actix_connect::ssl::rustls::ClientConfig;
use actix_tls::connect::ssl::rustls::ClientConfig;
#[cfg(feature = "rustls")]
use std::sync::Arc;
@ -35,7 +40,8 @@ enum SslConnector {
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
type SslConnector = ();
/// Manages http client network connectivity
/// Manages HTTP client network connectivity.
///
/// The `Connector` type uses a builder-like combinator pattern for service
/// construction that finishes by calling the `.finish()` method.
///
@ -52,34 +58,34 @@ pub struct Connector<T, U> {
config: ConnectorConfig,
#[allow(dead_code)]
ssl: SslConnector,
_t: PhantomData<U>,
_phantom: PhantomData<U>,
}
trait Io: AsyncRead + AsyncWrite + Unpin {}
pub trait Io: AsyncRead + AsyncWrite + Unpin {}
impl<T: AsyncRead + AsyncWrite + Unpin> Io for T {}
impl Connector<(), ()> {
#[allow(clippy::new_ret_no_self, clippy::let_unit_value)]
pub fn new() -> Connector<
impl Service<
Request = TcpConnect<Uri>,
TcpConnect<Uri>,
Response = TcpConnection<Uri, TcpStream>,
Error = actix_connect::ConnectError,
Error = actix_tls::connect::ConnectError,
> + Clone,
TcpStream,
> {
Connector {
ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]),
connector: default_connector(),
connector: new_connector(resolver::resolver()),
config: ConnectorConfig::default(),
_t: PhantomData,
_phantom: PhantomData,
}
}
// Build Ssl connector with openssl, based on supplied alpn protocols
#[cfg(feature = "openssl")]
fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector {
use actix_connect::ssl::openssl::SslMethod;
use actix_tls::connect::ssl::openssl::SslMethod;
use bytes::{BufMut, BytesMut};
let mut alpn = BytesMut::with_capacity(20);
@ -100,9 +106,9 @@ impl Connector<(), ()> {
fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector {
let mut config = ClientConfig::new();
config.set_protocols(&protocols);
config
.root_store
.add_server_trust_anchors(&actix_tls::rustls::TLS_SERVER_ROOTS);
config.root_store.add_server_trust_anchors(
&actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS,
);
SslConnector::Rustls(Arc::new(config))
}
@ -117,16 +123,16 @@ impl<T, U> Connector<T, U> {
where
U1: AsyncRead + AsyncWrite + Unpin + fmt::Debug,
T1: Service<
Request = TcpConnect<Uri>,
TcpConnect<Uri>,
Response = TcpConnection<Uri, U1>,
Error = actix_connect::ConnectError,
Error = actix_tls::connect::ConnectError,
> + Clone,
{
Connector {
connector,
config: self.config,
ssl: self.ssl,
_t: PhantomData,
_phantom: PhantomData,
}
}
}
@ -135,9 +141,9 @@ impl<T, U> Connector<T, U>
where
U: AsyncRead + AsyncWrite + Unpin + fmt::Debug + 'static,
T: Service<
Request = TcpConnect<Uri>,
TcpConnect<Uri>,
Response = TcpConnection<Uri, U>,
Error = actix_connect::ConnectError,
Error = actix_tls::connect::ConnectError,
> + Clone
+ 'static,
{
@ -161,8 +167,9 @@ where
self
}
/// Maximum supported http major version
/// Supported versions http/1.1, http/2
/// Maximum supported HTTP major version.
///
/// Supported versions are HTTP/1.1 and HTTP/2.
pub fn max_http_version(mut self, val: http::Version) -> Self {
let versions = match val {
http::Version::HTTP_11 => vec![b"http/1.1".to_vec()],
@ -236,19 +243,32 @@ where
self
}
/// Set local IP Address the connector would use for establishing connection.
pub fn local_address(mut self, addr: IpAddr) -> Self {
self.config.local_address = Some(addr);
self
}
/// Finish configuration process and create connector service.
/// The Connector builder always concludes by calling `finish()` last in
/// its combinator chain.
pub fn finish(
self,
) -> impl Service<Request = Connect, Response = impl Connection, Error = ConnectError>
+ Clone {
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
) -> impl Service<Connect, Response = impl Connection, Error = ConnectError> + Clone
{
let connector = TimeoutService::new(
self.config.timeout,
apply_fn(self.connector, |msg: Connect, srv| {
srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr))
let local_address = self.config.local_address;
let timeout = self.config.timeout;
let tcp_service = TimeoutService::new(
timeout,
apply_fn(self.connector.clone(), move |msg: Connect, srv| {
let mut req = TcpConnect::new(msg.uri).set_addr(msg.addr);
if let Some(local_addr) = local_address {
req = req.set_local_addr(local_addr);
}
srv.call(req)
})
.map_err(ConnectError::from)
.map(|stream| (stream.into_parts().0, Protocol::Http1)),
@ -258,27 +278,50 @@ where
TimeoutError::Timeout => ConnectError::Timeout,
});
connect_impl::InnerConnector {
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
{
// A dummy service for annotate tls pool's type signature.
pub type DummyService = Box<
dyn Service<
Connect,
Response = (Box<dyn Io>, Protocol),
Error = ConnectError,
Future = futures_core::future::LocalBoxFuture<
'static,
Result<(Box<dyn Io>, Protocol), ConnectError>,
>,
>,
>;
InnerConnector::<_, DummyService, _, Box<dyn Io>> {
tcp_pool: ConnectionPool::new(
connector,
tcp_service,
self.config.no_disconnect_timeout(),
),
tls_pool: None,
}
}
#[cfg(any(feature = "openssl", feature = "rustls"))]
{
const H2: &[u8] = b"h2";
#[cfg(feature = "openssl")]
use actix_connect::ssl::openssl::OpensslConnector;
#[cfg(feature = "rustls")]
use actix_connect::ssl::rustls::{RustlsConnector, Session};
use actix_service::{boxed::service, pipeline};
#[cfg(feature = "openssl")]
use actix_tls::connect::ssl::openssl::OpensslConnector;
#[cfg(feature = "rustls")]
use actix_tls::connect::ssl::rustls::{RustlsConnector, Session};
let ssl_service = TimeoutService::new(
self.config.timeout,
timeout,
pipeline(
apply_fn(self.connector.clone(), |msg: Connect, srv| {
srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr))
apply_fn(self.connector.clone(), move |msg: Connect, srv| {
let mut req = TcpConnect::new(msg.uri).set_addr(msg.addr);
if let Some(local_addr) = local_address {
req = req.set_local_addr(local_addr);
}
srv.call(req)
})
.map_err(ConnectError::from),
)
@ -327,221 +370,177 @@ where
TimeoutError::Timeout => ConnectError::Timeout,
});
let tcp_service = TimeoutService::new(
self.config.timeout,
apply_fn(self.connector, |msg: Connect, srv| {
srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr))
})
.map_err(ConnectError::from)
.map(|stream| (stream.into_parts().0, Protocol::Http1)),
)
.map_err(|e| match e {
TimeoutError::Service(e) => e,
TimeoutError::Timeout => ConnectError::Timeout,
});
connect_impl::InnerConnector {
InnerConnector {
tcp_pool: ConnectionPool::new(
tcp_service,
self.config.no_disconnect_timeout(),
),
ssl_pool: ConnectionPool::new(ssl_service, self.config),
tls_pool: Some(ConnectionPool::new(ssl_service, self.config)),
}
}
}
}
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
mod connect_impl {
use std::task::{Context, Poll};
struct InnerConnector<S1, S2, Io1, Io2>
where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
{
tcp_pool: ConnectionPool<S1, Io1>,
tls_pool: Option<ConnectionPool<S2, Io2>>,
}
use futures_util::future::{err, Either, Ready};
use super::*;
use crate::client::connection::IoConnection;
pub(crate) struct InnerConnector<T, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
+ 'static,
{
pub(crate) tcp_pool: ConnectionPool<T, Io>,
}
impl<T, Io> Clone for InnerConnector<T, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
+ 'static,
{
impl<S1, S2, Io1, Io2> Clone for InnerConnector<S1, S2, Io1, Io2>
where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
{
fn clone(&self) -> Self {
InnerConnector {
tcp_pool: self.tcp_pool.clone(),
tls_pool: self.tls_pool.as_ref().cloned(),
}
}
}
}
impl<T, Io> Service for InnerConnector<T, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
+ 'static,
{
type Request = Connect;
type Response = IoConnection<Io>;
impl<S1, S2, Io1, Io2> Service<Connect> for InnerConnector<S1, S2, Io1, Io2>
where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
{
type Response = EitherIoConnection<Io1, Io2>;
type Error = ConnectError;
type Future = Either<
<ConnectionPool<T, Io> as Service>::Future,
Ready<Result<IoConnection<Io>, ConnectError>>,
>;
type Future = InnerConnectorResponse<S1, S2, Io1, Io2>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.tcp_pool.poll_ready(cx)
}
fn call(&mut self, req: Connect) -> Self::Future {
fn call(&self, req: Connect) -> Self::Future {
match req.uri.scheme_str() {
Some("https") | Some("wss") => {
Either::Right(err(ConnectError::SslIsNotSupported))
Some("https") | Some("wss") => match self.tls_pool {
None => InnerConnectorResponse::SslIsNotSupported,
Some(ref pool) => InnerConnectorResponse::Io2(pool.call(req)),
},
_ => InnerConnectorResponse::Io1(self.tcp_pool.call(req)),
}
_ => Either::Left(self.tcp_pool.call(req)),
}
}
#[pin_project::pin_project(project = InnerConnectorProj)]
enum InnerConnectorResponse<S1, S2, Io1, Io2>
where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
{
Io1(#[pin] <ConnectionPool<S1, Io1> as Service<Connect>>::Future),
Io2(#[pin] <ConnectionPool<S2, Io2> as Service<Connect>>::Future),
SslIsNotSupported,
}
impl<S1, S2, Io1, Io2> Future for InnerConnectorResponse<S1, S2, Io1, Io2>
where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
{
type Output = Result<EitherIoConnection<Io1, Io2>, ConnectError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.project() {
InnerConnectorProj::Io1(fut) => fut.poll(cx).map_ok(EitherIoConnection::A),
InnerConnectorProj::Io2(fut) => fut.poll(cx).map_ok(EitherIoConnection::B),
InnerConnectorProj::SslIsNotSupported => {
Poll::Ready(Err(ConnectError::SslIsNotSupported))
}
}
}
}
#[cfg(any(feature = "openssl", feature = "rustls"))]
mod connect_impl {
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
#[cfg(not(feature = "trust-dns"))]
mod resolver {
use super::*;
use futures_core::ready;
use futures_util::future::Either;
pub(super) fn resolver() -> Resolver {
Resolver::Default
}
}
#[cfg(feature = "trust-dns")]
mod resolver {
use std::{cell::RefCell, net::SocketAddr};
use actix_tls::connect::Resolve;
use futures_core::future::LocalBoxFuture;
use trust_dns_resolver::{
config::{ResolverConfig, ResolverOpts},
system_conf::read_system_conf,
TokioAsyncResolver,
};
use super::*;
use crate::client::connection::EitherConnection;
pub(crate) struct InnerConnector<T1, T2, Io1, Io2>
where
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>,
T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>,
pub(super) fn resolver() -> Resolver {
// new type for impl Resolve trait for TokioAsyncResolver.
struct TrustDnsResolver(TokioAsyncResolver);
impl Resolve for TrustDnsResolver {
fn lookup<'a>(
&'a self,
host: &'a str,
port: u16,
) -> LocalBoxFuture<'a, Result<Vec<SocketAddr>, Box<dyn std::error::Error>>>
{
pub(crate) tcp_pool: ConnectionPool<T1, Io1>,
pub(crate) ssl_pool: ConnectionPool<T2, Io2>,
}
impl<T1, T2, Io1, Io2> Clone for InnerConnector<T1, T2, Io1, Io2>
where
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
+ 'static,
T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
+ 'static,
{
fn clone(&self) -> Self {
InnerConnector {
tcp_pool: self.tcp_pool.clone(),
ssl_pool: self.ssl_pool.clone(),
}
}
}
impl<T1, T2, Io1, Io2> Service for InnerConnector<T1, T2, Io1, Io2>
where
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
+ 'static,
T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
+ 'static,
{
type Request = Connect;
type Response = EitherConnection<Io1, Io2>;
type Error = ConnectError;
type Future = Either<
InnerConnectorResponseA<T1, Io1, Io2>,
InnerConnectorResponseB<T2, Io1, Io2>,
>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.tcp_pool.poll_ready(cx)
}
fn call(&mut self, req: Connect) -> Self::Future {
match req.uri.scheme_str() {
Some("https") | Some("wss") => Either::Right(InnerConnectorResponseB {
fut: self.ssl_pool.call(req),
_t: PhantomData,
}),
_ => Either::Left(InnerConnectorResponseA {
fut: self.tcp_pool.call(req),
_t: PhantomData,
}),
}
}
}
#[pin_project::pin_project]
pub(crate) struct InnerConnectorResponseA<T, Io1, Io2>
where
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
+ 'static,
{
#[pin]
fut: <ConnectionPool<T, Io1> as Service>::Future,
_t: PhantomData<Io2>,
}
impl<T, Io1, Io2> Future for InnerConnectorResponseA<T, Io1, Io2>
where
T: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
+ 'static,
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
{
type Output = Result<EitherConnection<Io1, Io2>, ConnectError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(
ready!(Pin::new(&mut self.get_mut().fut).poll(cx))
.map(EitherConnection::A),
)
Box::pin(async move {
let res = self
.0
.lookup_ip(host)
.await?
.iter()
.map(|ip| SocketAddr::new(ip, port))
.collect();
Ok(res)
})
}
}
#[pin_project::pin_project]
pub(crate) struct InnerConnectorResponseB<T, Io1, Io2>
where
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
+ 'static,
{
#[pin]
fut: <ConnectionPool<T, Io2> as Service>::Future,
_t: PhantomData<Io1>,
// dns struct is cached in thread local.
// so new client constructor can reuse the existing dns resolver.
thread_local! {
static TRUST_DNS_RESOLVER: RefCell<Option<Resolver>> = RefCell::new(None);
}
impl<T, Io1, Io2> Future for InnerConnectorResponseB<T, Io1, Io2>
where
T: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
+ 'static,
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
{
type Output = Result<EitherConnection<Io1, Io2>, ConnectError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(
ready!(Pin::new(&mut self.get_mut().fut).poll(cx))
.map(EitherConnection::B),
)
// get from thread local or construct a new trust-dns resolver.
TRUST_DNS_RESOLVER.with(|local| {
let resolver = local.borrow().as_ref().map(Clone::clone);
match resolver {
Some(resolver) => resolver,
None => {
let (cfg, opts) = match read_system_conf() {
Ok((cfg, opts)) => (cfg, opts),
Err(e) => {
log::error!("TRust-DNS can not load system config: {}", e);
(ResolverConfig::default(), ResolverOpts::default())
}
};
let resolver = TokioAsyncResolver::tokio(cfg, opts).unwrap();
// box trust dns resolver and put it in thread local.
let resolver = Resolver::new_custom(TrustDnsResolver(resolver));
*local.borrow_mut() = Some(resolver.clone());
resolver
}
}
})
}
}

View File

@ -1,10 +1,9 @@
use std::io;
use actix_connect::resolver::ResolveError;
use derive_more::{Display, From};
#[cfg(feature = "openssl")]
use actix_connect::ssl::openssl::{HandshakeError, SslError};
use actix_tls::accept::openssl::SslError;
use crate::error::{Error, ParseError, ResponseError};
use crate::http::{Error as HttpError, StatusCode};
@ -21,17 +20,12 @@ pub enum ConnectError {
#[display(fmt = "{}", _0)]
SslError(SslError),
/// SSL Handshake error
#[cfg(feature = "openssl")]
#[display(fmt = "{}", _0)]
SslHandshakeError(String),
/// Failed to resolve the hostname
#[display(fmt = "Failed resolving hostname: {}", _0)]
Resolver(ResolveError),
Resolver(Box<dyn std::error::Error>),
/// No dns records
#[display(fmt = "No dns records found for the input")]
#[display(fmt = "No DNS records found for the input")]
NoRecords,
/// Http2 error
@ -57,34 +51,30 @@ pub enum ConnectError {
impl std::error::Error for ConnectError {}
impl From<actix_connect::ConnectError> for ConnectError {
fn from(err: actix_connect::ConnectError) -> ConnectError {
impl From<actix_tls::connect::ConnectError> for ConnectError {
fn from(err: actix_tls::connect::ConnectError) -> ConnectError {
match err {
actix_connect::ConnectError::Resolver(e) => ConnectError::Resolver(e),
actix_connect::ConnectError::NoRecords => ConnectError::NoRecords,
actix_connect::ConnectError::InvalidInput => panic!(),
actix_connect::ConnectError::Unresolved => ConnectError::Unresolved,
actix_connect::ConnectError::Io(e) => ConnectError::Io(e),
actix_tls::connect::ConnectError::Resolver(e) => ConnectError::Resolver(e),
actix_tls::connect::ConnectError::NoRecords => ConnectError::NoRecords,
actix_tls::connect::ConnectError::InvalidInput => panic!(),
actix_tls::connect::ConnectError::Unresolved => ConnectError::Unresolved,
actix_tls::connect::ConnectError::Io(e) => ConnectError::Io(e),
}
}
}
#[cfg(feature = "openssl")]
impl<T: std::fmt::Debug> From<HandshakeError<T>> for ConnectError {
fn from(err: HandshakeError<T>) -> ConnectError {
ConnectError::SslHandshakeError(format!("{:?}", err))
}
}
#[derive(Debug, Display, From)]
pub enum InvalidUrl {
#[display(fmt = "Missing url scheme")]
#[display(fmt = "Missing URL scheme")]
MissingScheme,
#[display(fmt = "Unknown url scheme")]
#[display(fmt = "Unknown URL scheme")]
UnknownScheme,
#[display(fmt = "Missing host name")]
MissingHost,
#[display(fmt = "Url parse error: {}", _0)]
#[display(fmt = "URL parse error: {}", _0)]
HttpError(http::Error),
}
@ -96,25 +86,33 @@ pub enum SendRequestError {
/// Invalid URL
#[display(fmt = "Invalid URL: {}", _0)]
Url(InvalidUrl),
/// Failed to connect to host
#[display(fmt = "Failed to connect to host: {}", _0)]
Connect(ConnectError),
/// Error sending request
Send(io::Error),
/// Error parsing response
Response(ParseError),
/// Http error
#[display(fmt = "{}", _0)]
Http(HttpError),
/// Http2 error
#[display(fmt = "{}", _0)]
H2(h2::Error),
/// Response took too long
#[display(fmt = "Timeout while waiting for response")]
Timeout,
/// Tunnels are not supported for http2 connection
/// Tunnels are not supported for HTTP/2 connection
#[display(fmt = "Tunnels are not supported for http2 connection")]
TunnelNotSupported,
/// Error sending request body
Body(Error),
}
@ -140,7 +138,8 @@ pub enum FreezeRequestError {
/// Invalid URL
#[display(fmt = "Invalid URL: {}", _0)]
Url(InvalidUrl),
/// Http error
/// HTTP error
#[display(fmt = "{}", _0)]
Http(HttpError),
}

View File

@ -1,19 +1,21 @@
use std::io::Write;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{io, mem, time};
use std::{io, time};
use actix_codec::{AsyncRead, AsyncWrite, Framed};
use bytes::buf::BufMutExt;
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf};
use bytes::buf::BufMut;
use bytes::{Bytes, BytesMut};
use futures_core::Stream;
use futures_util::future::poll_fn;
use futures_util::{pin_mut, SinkExt, StreamExt};
use futures_util::{future::poll_fn, SinkExt, StreamExt};
use crate::error::PayloadError;
use crate::h1;
use crate::header::HeaderMap;
use crate::http::header::{IntoHeaderValue, HOST};
use crate::http::{
header::{IntoHeaderValue, EXPECT, HOST},
StatusCode,
};
use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::{Payload, PayloadStream};
@ -45,14 +47,14 @@ where
Some(port) => write!(wrt, "{}:{}", host, port),
};
match wrt.get_mut().split().freeze().try_into() {
match wrt.get_mut().split().freeze().try_into_value() {
Ok(value) => match head {
RequestHeadType::Owned(ref mut head) => {
head.headers.insert(HOST, value)
head.headers.insert(HOST, value);
}
RequestHeadType::Rc(_, ref mut extra_headers) => {
let headers = extra_headers.get_or_insert(HeaderMap::new());
headers.insert(HOST, value)
headers.insert(HOST, value);
}
},
Err(e) => log::error!("Can not set HOST header {}", e),
@ -66,33 +68,72 @@ where
io: Some(io),
};
// create Framed and send request
let mut framed_inner = Framed::new(io, h1::ClientCodec::default());
framed_inner.send((head, body.size()).into()).await?;
// create Framed and prepare sending request
let mut framed = Framed::new(io, h1::ClientCodec::default());
// Check EXPECT header and enable expect handle flag accordingly.
//
// RFC: https://tools.ietf.org/html/rfc7231#section-5.1.1
let is_expect = if head.as_ref().headers.contains_key(EXPECT) {
match body.size() {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => {
let pin_framed = Pin::new(&mut framed);
let force_close = !pin_framed.codec_ref().keepalive();
release_connection(pin_framed, force_close);
// TODO: use a new variant or a new type better describing error violate
// `Requirements for clients` session of above RFC
return Err(SendRequestError::Connect(ConnectError::Disconnected));
}
_ => true,
}
} else {
false
};
framed.send((head, body.size()).into()).await?;
let mut pin_framed = Pin::new(&mut framed);
// special handle for EXPECT request.
let (do_send, mut res_head) = if is_expect {
let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx))
.await
.ok_or(ConnectError::Disconnected)??;
// return response head in case status code is not continue
// and current head would be used as final response head.
(head.status == StatusCode::CONTINUE, Some(head))
} else {
(true, None)
};
if do_send {
// send request body
match body.size() {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => (),
_ => send_body(body, Pin::new(&mut framed_inner)).await?,
BodySize::None | BodySize::Empty | BodySize::Sized(0) => {}
_ => send_body(body, pin_framed.as_mut()).await?,
};
// read response and init read body
let res = Pin::new(&mut framed_inner).into_future().await;
let (head, framed) = if let (Some(result), framed) = res {
let item = result.map_err(SendRequestError::from)?;
(item, framed)
} else {
return Err(SendRequestError::from(ConnectError::Disconnected));
};
let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx))
.await
.ok_or(ConnectError::Disconnected)??;
match framed.codec_ref().message_type() {
res_head = Some(head);
}
let head = res_head.unwrap();
match pin_framed.codec_ref().message_type() {
h1::MessageType::None => {
let force_close = !framed.codec_ref().keepalive();
release_connection(framed, force_close);
let force_close = !pin_framed.codec_ref().keepalive();
release_connection(pin_framed, force_close);
Ok((head, Payload::None))
}
_ => {
let pl: PayloadStream = PlStream::new(framed_inner).boxed_local();
let pl: PayloadStream = Box::pin(PlStream::new(framed));
Ok((head, pl.into()))
}
}
@ -127,7 +168,7 @@ where
T: ConnectionLifetime + Unpin,
B: MessageBody,
{
pin_mut!(body);
actix_rt::pin!(body);
let mut eof = false;
while !eof {
@ -165,7 +206,10 @@ where
#[doc(hidden)]
/// HTTP client connection
pub struct H1Connection<T> {
pub struct H1Connection<T>
where
T: AsyncWrite + Unpin + 'static,
{
/// T should be `Unpin`
io: Option<T>,
created: time::Instant,
@ -204,18 +248,11 @@ where
}
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncRead for H1Connection<T> {
unsafe fn prepare_uninitialized_buffer(
&self,
buf: &mut [mem::MaybeUninit<u8>],
) -> bool {
self.io.as_ref().unwrap().prepare_uninitialized_buffer(buf)
}
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.io.as_mut().unwrap()).poll_read(cx, buf)
}
}

View File

@ -1,11 +1,9 @@
use std::convert::TryFrom;
use std::future::Future;
use std::time;
use actix_codec::{AsyncRead, AsyncWrite};
use bytes::Bytes;
use futures_util::future::poll_fn;
use futures_util::pin_mut;
use h2::{
client::{Builder, Connection, SendRequest},
SendStream,
@ -22,9 +20,10 @@ use super::config::ConnectorConfig;
use super::connection::{ConnectionType, IoConnection};
use super::error::SendRequestError;
use super::pool::Acquired;
use crate::client::connection::H2Connection;
pub(crate) async fn send_request<T, B>(
mut io: SendRequest<Bytes>,
mut io: H2Connection,
head: RequestHeadType,
body: B,
created: time::Instant,
@ -35,6 +34,7 @@ where
B: MessageBody,
{
trace!("Sending client request: {:?} {:?}", head, body.size());
let head_req = head.as_ref().method == Method::HEAD;
let length = body.size();
let eof = matches!(
@ -60,10 +60,14 @@ where
BodySize::Empty => req
.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
BodySize::Sized(len) => req.headers_mut().insert(
BodySize::Sized(len) => {
let mut buf = itoa::Buffer::new();
req.headers_mut().insert(
CONTENT_LENGTH,
HeaderValue::try_from(format!("{}", len)).unwrap(),
),
HeaderValue::from_str(buf.format(len)).unwrap(),
)
}
};
// Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate.
@ -86,10 +90,13 @@ where
// copy headers
for (key, value) in headers {
match *key {
CONNECTION | TRANSFER_ENCODING => continue, // http2 specific
// TODO: consider skipping other headers according to:
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2
// omit HTTP/1.x only headers
CONNECTION | TRANSFER_ENCODING => continue,
CONTENT_LENGTH if skip_len => continue,
// DATE => has_date = true,
_ => (),
_ => {}
}
req.headers_mut().append(key, value.clone());
}
@ -129,7 +136,7 @@ async fn send_body<B: MessageBody>(
mut send: SendStream<Bytes>,
) -> Result<(), SendRequestError> {
let mut buf = None;
pin_mut!(body);
actix_rt::pin!(body);
loop {
if buf.is_none() {
match poll_fn(|cx| body.as_mut().poll_next(cx)).await {
@ -171,9 +178,9 @@ async fn send_body<B: MessageBody>(
}
}
// release SendRequest object
/// release SendRequest object
fn release<T: AsyncRead + AsyncWrite + Unpin + 'static>(
io: SendRequest<Bytes>,
io: H2Connection,
pool: Option<Acquired<T>>,
created: time::Instant,
close: bool,

View File

@ -1,4 +1,5 @@
//! Http client api
//! HTTP client.
use http::Uri;
mod config;
@ -9,6 +10,10 @@ mod h1proto;
mod h2proto;
mod pool;
pub use actix_tls::connect::{
Connect as TcpConnect, ConnectError as TcpConnectError, Connection as TcpConnection,
};
pub use self::connection::Connection;
pub use self::connector::Connector;
pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +0,0 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::task::{Context, Poll};
use actix_service::Service;
#[doc(hidden)]
/// Service that allows to turn non-clone service to a service with `Clone` impl
///
/// # Panics
/// CloneableService might panic with some creative use of thread local storage.
/// See https://github.com/actix/actix-web/issues/1295 for example
pub(crate) struct CloneableService<T: Service>(Rc<RefCell<T>>);
impl<T: Service> CloneableService<T> {
pub(crate) fn new(service: T) -> Self {
Self(Rc::new(RefCell::new(service)))
}
}
impl<T: Service> Clone for CloneableService<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T: Service> Service for CloneableService<T> {
type Request = T::Request;
type Response = T::Response;
type Error = T::Error;
type Future = T::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.0.borrow_mut().poll_ready(cx)
}
fn call(&mut self, req: T::Request) -> Self::Future {
self.0.borrow_mut().call(req)
}
}

View File

@ -4,12 +4,14 @@ use std::rc::Rc;
use std::time::Duration;
use std::{fmt, net};
use actix_rt::time::{delay_for, delay_until, Delay, Instant};
use actix_rt::{
task::JoinHandle,
time::{interval, sleep_until, Instant, Sleep},
};
use bytes::BytesMut;
use futures_util::{future, FutureExt};
use time::OffsetDateTime;
// "Sun, 06 Nov 1994 08:49:37 GMT".len()
/// "Sun, 06 Nov 1994 08:49:37 GMT".len()
const DATE_VALUE_LENGTH: usize = 29;
#[derive(Debug, PartialEq, Clone, Copy)]
@ -49,7 +51,7 @@ struct Inner {
ka_enabled: bool,
secure: bool,
local_addr: Option<std::net::SocketAddr>,
timer: DateService,
date_service: DateService,
}
impl Clone for ServiceConfig {
@ -91,41 +93,41 @@ impl ServiceConfig {
client_disconnect,
secure,
local_addr,
timer: DateService::new(),
date_service: DateService::new(),
}))
}
/// Returns true if connection is secure (HTTPS)
#[inline]
/// Returns true if connection is secure(https)
pub fn secure(&self) -> bool {
self.0.secure
}
#[inline]
/// Returns the local address that this server is bound to.
#[inline]
pub fn local_addr(&self) -> Option<net::SocketAddr> {
self.0.local_addr
}
#[inline]
/// Keep alive duration if configured.
#[inline]
pub fn keep_alive(&self) -> Option<Duration> {
self.0.keep_alive
}
#[inline]
/// Return state of connection keep-alive functionality
#[inline]
pub fn keep_alive_enabled(&self) -> bool {
self.0.ka_enabled
}
#[inline]
/// Client timeout for first request.
pub fn client_timer(&self) -> Option<Delay> {
#[inline]
pub fn client_timer(&self) -> Option<Sleep> {
let delay_time = self.0.client_timeout;
if delay_time != 0 {
Some(delay_until(
self.0.timer.now() + Duration::from_millis(delay_time),
Some(sleep_until(
self.0.date_service.now() + Duration::from_millis(delay_time),
))
} else {
None
@ -136,7 +138,7 @@ impl ServiceConfig {
pub fn client_timer_expire(&self) -> Option<Instant> {
let delay = self.0.client_timeout;
if delay != 0 {
Some(self.0.timer.now() + Duration::from_millis(delay))
Some(self.0.date_service.now() + Duration::from_millis(delay))
} else {
None
}
@ -146,7 +148,7 @@ impl ServiceConfig {
pub fn client_disconnect_timer(&self) -> Option<Instant> {
let delay = self.0.client_disconnect;
if delay != 0 {
Some(self.0.timer.now() + Duration::from_millis(delay))
Some(self.0.date_service.now() + Duration::from_millis(delay))
} else {
None
}
@ -154,9 +156,9 @@ impl ServiceConfig {
#[inline]
/// Return keep-alive timer delay is configured.
pub fn keep_alive_timer(&self) -> Option<Delay> {
pub fn keep_alive_timer(&self) -> Option<Sleep> {
if let Some(ka) = self.0.keep_alive {
Some(delay_until(self.0.timer.now() + ka))
Some(sleep_until(self.0.date_service.now() + ka))
} else {
None
}
@ -165,7 +167,7 @@ impl ServiceConfig {
/// Keep-alive expire time
pub fn keep_alive_expire(&self) -> Option<Instant> {
if let Some(ka) = self.0.keep_alive {
Some(self.0.timer.now() + ka)
Some(self.0.date_service.now() + ka)
} else {
None
}
@ -173,7 +175,7 @@ impl ServiceConfig {
#[inline]
pub(crate) fn now(&self) -> Instant {
self.0.timer.now()
self.0.date_service.now()
}
#[doc(hidden)]
@ -181,7 +183,7 @@ impl ServiceConfig {
let mut buf: [u8; 39] = [0; 39];
buf[..6].copy_from_slice(b"date: ");
self.0
.timer
.date_service
.set_date(|date| buf[6..35].copy_from_slice(&date.bytes));
buf[35..].copy_from_slice(b"\r\n\r\n");
dst.extend_from_slice(&buf);
@ -189,7 +191,7 @@ impl ServiceConfig {
pub(crate) fn set_date_header(&self, dst: &mut BytesMut) {
self.0
.timer
.date_service
.set_date(|date| dst.extend_from_slice(&date.bytes));
}
}
@ -230,57 +232,103 @@ impl fmt::Write for Date {
}
}
#[derive(Clone)]
struct DateService(Rc<DateServiceInner>);
struct DateServiceInner {
current: Cell<Option<(Date, Instant)>>,
/// Service for update Date and Instant periodically at 500 millis interval.
struct DateService {
current: Rc<Cell<(Date, Instant)>>,
handle: JoinHandle<()>,
}
impl DateServiceInner {
fn new() -> Self {
DateServiceInner {
current: Cell::new(None),
}
}
fn reset(&self) {
self.current.take();
}
fn update(&self) {
let now = Instant::now();
let date = Date::new();
self.current.set(Some((date, now)));
impl Drop for DateService {
fn drop(&mut self) {
// stop the timer update async task on drop.
self.handle.abort();
}
}
impl DateService {
fn new() -> Self {
DateService(Rc::new(DateServiceInner::new()))
}
// shared date and timer for DateService and update async task.
let current = Rc::new(Cell::new((Date::new(), Instant::now())));
let current_clone = Rc::clone(&current);
// spawn an async task sleep for 500 milli and update current date/timer in a loop.
// handle is used to stop the task on DateService drop.
let handle = actix_rt::spawn(async move {
#[cfg(test)]
let _notify = notify_on_drop::NotifyOnDrop::new();
fn check_date(&self) {
if self.0.current.get().is_none() {
self.0.update();
// periodic date update
let s = self.clone();
actix_rt::spawn(delay_for(Duration::from_millis(500)).then(move |_| {
s.0.reset();
future::ready(())
}));
let mut interval = interval(Duration::from_millis(500));
loop {
let now = interval.tick().await;
let date = Date::new();
current_clone.set((date, now));
}
});
DateService { current, handle }
}
fn now(&self) -> Instant {
self.check_date();
self.0.current.get().unwrap().1
self.current.get().1
}
fn set_date<F: FnMut(&Date)>(&self, mut f: F) {
self.check_date();
f(&self.0.current.get().unwrap().0);
f(&self.current.get().0);
}
}
// TODO: move to a util module for testing all spawn handle drop style tasks.
#[cfg(test)]
/// Test Module for checking the drop state of certain async tasks that are spawned
/// with `actix_rt::spawn`
///
/// The target task must explicitly generate `NotifyOnDrop` when spawn the task
mod notify_on_drop {
use std::cell::RefCell;
thread_local! {
static NOTIFY_DROPPED: RefCell<Option<bool>> = RefCell::new(None);
}
/// Check if the spawned task is dropped.
///
/// # Panic:
///
/// When there was no `NotifyOnDrop` instance on current thread
pub(crate) fn is_dropped() -> bool {
NOTIFY_DROPPED.with(|bool| {
bool.borrow()
.expect("No NotifyOnDrop existed on current thread")
})
}
pub(crate) struct NotifyOnDrop;
impl NotifyOnDrop {
/// # Panic:
///
/// When construct multiple instances on any given thread.
pub(crate) fn new() -> Self {
NOTIFY_DROPPED.with(|bool| {
let mut bool = bool.borrow_mut();
if bool.is_some() {
panic!("NotifyOnDrop existed on current thread");
} else {
*bool = Some(false);
}
});
NotifyOnDrop
}
}
impl Drop for NotifyOnDrop {
fn drop(&mut self) {
NOTIFY_DROPPED.with(|bool| {
if let Some(b) = bool.borrow_mut().as_mut() {
*b = true;
}
});
}
}
}
@ -288,14 +336,53 @@ impl DateService {
mod tests {
use super::*;
// Test modifying the date from within the closure
// passed to `set_date`
#[test]
fn test_evil_date() {
let service = DateService::new();
// Make sure that `check_date` doesn't try to spawn a task
service.0.update();
service.set_date(|_| service.0.reset());
use actix_rt::task::yield_now;
#[actix_rt::test]
async fn test_date_service_update() {
let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None);
yield_now().await;
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf1);
let now1 = settings.now();
sleep_until(Instant::now() + Duration::from_secs(2)).await;
yield_now().await;
let now2 = settings.now();
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf2);
assert_ne!(now1, now2);
assert_ne!(buf1, buf2);
drop(settings);
assert!(notify_on_drop::is_dropped());
}
#[actix_rt::test]
async fn test_date_service_drop() {
let service = Rc::new(DateService::new());
// yield so date service have a chance to register the spawned timer update task.
yield_now().await;
let clone1 = service.clone();
let clone2 = service.clone();
let clone3 = service.clone();
drop(clone1);
assert_eq!(false, notify_on_drop::is_dropped());
drop(clone2);
assert_eq!(false, notify_on_drop::is_dropped());
drop(clone3);
assert_eq!(false, notify_on_drop::is_dropped());
drop(service);
assert!(notify_on_drop::is_dropped());
}
#[test]

View File

@ -1,25 +1,31 @@
use std::future::Future;
use std::io::{self, Write};
use std::pin::Pin;
use std::task::{Context, Poll};
//! Stream decoders.
use actix_threadpool::{run, CpuFuture};
use std::{
future::Future,
io::{self, Write as _},
pin::Pin,
task::{Context, Poll},
};
use actix_rt::task::{spawn_blocking, JoinHandle};
use brotli2::write::BrotliDecoder;
use bytes::Bytes;
use flate2::write::{GzDecoder, ZlibDecoder};
use futures_core::{ready, Stream};
use super::Writer;
use crate::error::PayloadError;
use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING};
use crate::{
encoding::Writer,
error::{BlockingError, PayloadError},
http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING},
};
const INPLACE: usize = 2049;
const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049;
pub struct Decoder<S> {
decoder: Option<ContentDecoder>,
stream: S,
eof: bool,
fut: Option<CpuFuture<(Option<Bytes>, ContentDecoder), io::Error>>,
fut: Option<JoinHandle<Result<(Option<Bytes>, ContentDecoder), io::Error>>>,
}
impl<S> Decoder<S>
@ -41,6 +47,7 @@ where
))),
_ => None,
};
Decoder {
decoder,
stream,
@ -53,15 +60,11 @@ where
#[inline]
pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder<S> {
// check content-encoding
let encoding = if let Some(enc) = headers.get(&CONTENT_ENCODING) {
if let Ok(enc) = enc.to_str() {
ContentEncoding::from(enc)
} else {
ContentEncoding::Identity
}
} else {
ContentEncoding::Identity
};
let encoding = headers
.get(&CONTENT_ENCODING)
.and_then(|val| val.to_str().ok())
.map(ContentEncoding::from)
.unwrap_or(ContentEncoding::Identity);
Self::new(stream, encoding)
}
@ -79,12 +82,12 @@ where
) -> Poll<Option<Self::Item>> {
loop {
if let Some(ref mut fut) = self.fut {
let (chunk, decoder) = match ready!(Pin::new(fut).poll(cx)) {
Ok(item) => item,
Err(e) => return Poll::Ready(Some(Err(e.into()))),
};
let (chunk, decoder) =
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
self.decoder = Some(decoder);
self.fut.take();
if let Some(chunk) = chunk {
return Poll::Ready(Some(Ok(chunk)));
}
@ -94,29 +97,34 @@ where
return Poll::Ready(None);
}
match Pin::new(&mut self.stream).poll_next(cx) {
Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))),
Poll::Ready(Some(Ok(chunk))) => {
match ready!(Pin::new(&mut self.stream).poll_next(cx)) {
Some(Err(err)) => return Poll::Ready(Some(Err(err))),
Some(Ok(chunk)) => {
if let Some(mut decoder) = self.decoder.take() {
if chunk.len() < INPLACE {
if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE {
let chunk = decoder.feed_data(chunk)?;
self.decoder = Some(decoder);
if let Some(chunk) = chunk {
return Poll::Ready(Some(Ok(chunk)));
}
} else {
self.fut = Some(run(move || {
self.fut = Some(spawn_blocking(move || {
let chunk = decoder.feed_data(chunk)?;
Ok((chunk, decoder))
}));
}
continue;
} else {
return Poll::Ready(Some(Ok(chunk)));
}
}
Poll::Ready(None) => {
None => {
self.eof = true;
return if let Some(mut decoder) = self.decoder.take() {
match decoder.feed_eof() {
Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
@ -127,10 +135,8 @@ where
Poll::Ready(None)
};
}
Poll::Pending => break,
}
}
Poll::Pending
}
}
@ -146,6 +152,7 @@ impl ContentDecoder {
ContentDecoder::Br(ref mut decoder) => match decoder.flush() {
Ok(()) => {
let b = decoder.get_mut().take();
if !b.is_empty() {
Ok(Some(b))
} else {
@ -154,9 +161,11 @@ impl ContentDecoder {
}
Err(e) => Err(e),
},
ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() {
Ok(_) => {
let b = decoder.get_mut().take();
if !b.is_empty() {
Ok(Some(b))
} else {
@ -165,6 +174,7 @@ impl ContentDecoder {
}
Err(e) => Err(e),
},
ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() {
Ok(_) => {
let b = decoder.get_mut().take();
@ -185,6 +195,7 @@ impl ContentDecoder {
Ok(_) => {
decoder.flush()?;
let b = decoder.get_mut().take();
if !b.is_empty() {
Ok(Some(b))
} else {
@ -193,10 +204,12 @@ impl ContentDecoder {
}
Err(e) => Err(e),
},
ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => {
decoder.flush()?;
let b = decoder.get_mut().take();
if !b.is_empty() {
Ok(Some(b))
} else {
@ -205,9 +218,11 @@ impl ContentDecoder {
}
Err(e) => Err(e),
},
ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => {
decoder.flush()?;
let b = decoder.get_mut().take();
if !b.is_empty() {
Ok(Some(b))

View File

@ -1,24 +1,32 @@
//! Stream encoder
use std::future::Future;
use std::io::{self, Write};
use std::pin::Pin;
use std::task::{Context, Poll};
//! Stream encoders.
use actix_threadpool::{run, CpuFuture};
use std::{
future::Future,
io::{self, Write as _},
pin::Pin,
task::{Context, Poll},
};
use actix_rt::task::{spawn_blocking, JoinHandle};
use brotli2::write::BrotliEncoder;
use bytes::Bytes;
use flate2::write::{GzEncoder, ZlibEncoder};
use futures_core::ready;
use pin_project::pin_project;
use crate::body::{Body, BodySize, MessageBody, ResponseBody};
use crate::http::header::{ContentEncoding, CONTENT_ENCODING};
use crate::http::{HeaderValue, StatusCode};
use crate::{Error, ResponseHead};
use crate::{
body::{Body, BodySize, MessageBody, ResponseBody},
http::{
header::{ContentEncoding, CONTENT_ENCODING},
HeaderValue, StatusCode,
},
Error, ResponseHead,
};
use super::Writer;
use crate::error::BlockingError;
const INPLACE: usize = 1024;
const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024;
#[pin_project]
pub struct Encoder<B> {
@ -26,7 +34,7 @@ pub struct Encoder<B> {
#[pin]
body: EncoderBody<B>,
encoder: Option<ContentEncoder>,
fut: Option<CpuFuture<ContentEncoder, io::Error>>,
fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>,
}
impl<B: MessageBody> Encoder<B> {
@ -70,6 +78,7 @@ impl<B: MessageBody> Encoder<B> {
});
}
}
ResponseBody::Body(Encoder {
body,
eof: false,
@ -135,32 +144,35 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
}
if let Some(ref mut fut) = this.fut {
let mut encoder = match ready!(Pin::new(fut).poll(cx)) {
Ok(item) => item,
Err(e) => return Poll::Ready(Some(Err(e.into()))),
};
let mut encoder =
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
let chunk = encoder.take();
*this.encoder = Some(encoder);
this.fut.take();
if !chunk.is_empty() {
return Poll::Ready(Some(Ok(chunk)));
}
}
let result = this.body.as_mut().poll_next(cx);
let result = ready!(this.body.as_mut().poll_next(cx));
match result {
Poll::Ready(Some(Ok(chunk))) => {
Some(Err(err)) => return Poll::Ready(Some(Err(err))),
Some(Ok(chunk)) => {
if let Some(mut encoder) = this.encoder.take() {
if chunk.len() < INPLACE {
if chunk.len() < MAX_CHUNK_SIZE_ENCODE_IN_PLACE {
encoder.write(&chunk)?;
let chunk = encoder.take();
*this.encoder = Some(encoder);
if !chunk.is_empty() {
return Poll::Ready(Some(Ok(chunk)));
}
} else {
*this.fut = Some(run(move || {
*this.fut = Some(spawn_blocking(move || {
encoder.write(&chunk)?;
Ok(encoder)
}));
@ -169,7 +181,8 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
return Poll::Ready(Some(Ok(chunk)));
}
}
Poll::Ready(None) => {
None => {
if let Some(encoder) = this.encoder.take() {
let chunk = encoder.finish()?;
if chunk.is_empty() {
@ -182,7 +195,6 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
return Poll::Ready(None);
}
}
val => return val,
}
}
}

View File

@ -1,4 +1,5 @@
//! Content-Encoding support
//! Content-Encoding support.
use std::io;
use bytes::{Bytes, BytesMut};

View File

@ -7,25 +7,24 @@ use std::string::FromUtf8Error;
use std::{fmt, io, result};
use actix_codec::{Decoder, Encoder};
pub use actix_threadpool::BlockingError;
use actix_utils::dispatcher::DispatcherError as FramedDispatcherError;
use actix_utils::timeout::TimeoutError;
use bytes::BytesMut;
use derive_more::{Display, From};
pub use futures_channel::oneshot::Canceled;
use http::uri::InvalidUri;
use http::{header, Error as HttpError, StatusCode};
use serde::de::value::Error as DeError;
use serde_json::error::Error as JsonError;
use serde_urlencoded::ser::Error as FormError;
// re-export for convenience
use crate::body::Body;
pub use crate::cookie::ParseError as CookieParseError;
use crate::helpers::Writer;
use crate::response::{Response, ResponseBuilder};
/// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html)
#[cfg(feature = "cookies")]
pub use crate::cookie::ParseError as CookieParseError;
/// A specialized [`std::result::Result`]
/// for actix web operations
///
/// This typedef is generally used to avoid writing out
@ -40,7 +39,7 @@ pub type Result<T, E = Error> = result::Result<T, E>;
/// converting errors with `into()`.
///
/// Whenever it is created from an external object a response error is created
/// for it that can be used to create an http response from it this means that
/// for it that can be used to create an HTTP response from it this means that
/// if you have access to an actix `Error` you can always get a
/// `ResponseError` reference from it.
pub struct Error {
@ -100,10 +99,6 @@ impl fmt::Debug for Error {
}
impl std::error::Error for Error {
fn cause(&self) -> Option<&dyn std::error::Error> {
None
}
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
@ -153,7 +148,10 @@ impl From<ResponseBuilder> for Error {
}
}
/// Return `GATEWAY_TIMEOUT` for `TimeoutError`
/// Inspects the underlying enum and returns an appropriate status code.
///
/// If the variant is [`TimeoutError::Service`], the error code of the service is returned.
/// Otherwise, [`StatusCode::GATEWAY_TIMEOUT`] is returned.
impl<E: ResponseError> ResponseError for TimeoutError<E> {
fn status_code(&self) -> StatusCode {
match self {
@ -167,48 +165,41 @@ impl<E: ResponseError> ResponseError for TimeoutError<E> {
#[display(fmt = "UnknownError")]
struct UnitError;
/// `InternalServerError` for `UnitError`
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`UnitError`].
impl ResponseError for UnitError {}
/// `InternalServerError` for `JsonError`
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`JsonError`].
impl ResponseError for JsonError {}
/// `InternalServerError` for `FormError`
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`FormError`].
impl ResponseError for FormError {}
#[cfg(feature = "openssl")]
/// `InternalServerError` for `openssl::ssl::Error`
impl ResponseError for actix_connect::ssl::openssl::SslError {}
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`actix_tls::accept::openssl::SslError`].
impl ResponseError for actix_tls::accept::openssl::SslError {}
#[cfg(feature = "openssl")]
/// `InternalServerError` for `openssl::ssl::HandshakeError`
impl<T: std::fmt::Debug> ResponseError for actix_tls::openssl::HandshakeError<T> {}
/// Return `BAD_REQUEST` for `de::value::Error`
/// Returns [`StatusCode::BAD_REQUEST`] for [`DeError`].
impl ResponseError for DeError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
/// `InternalServerError` for `Canceled`
impl ResponseError for Canceled {}
/// `InternalServerError` for `BlockingError`
impl<E: fmt::Debug> ResponseError for BlockingError<E> {}
/// Return `BAD_REQUEST` for `Utf8Error`
/// Returns [`StatusCode::BAD_REQUEST`] for [`Utf8Error`].
impl ResponseError for Utf8Error {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
/// Return `InternalServerError` for `HttpError`,
/// Response generation can return `HttpError`, so it is internal error
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`HttpError`].
impl ResponseError for HttpError {}
/// Return `InternalServerError` for `io::Error`
/// Inspects the underlying [`io::ErrorKind`] and returns an appropriate status code.
///
/// If the error is [`io::ErrorKind::NotFound`], [`StatusCode::NOT_FOUND`] is returned. If the
/// error is [`io::ErrorKind::PermissionDenied`], [`StatusCode::FORBIDDEN`] is returned. Otherwise,
/// [`StatusCode::INTERNAL_SERVER_ERROR`] is returned.
impl ResponseError for io::Error {
fn status_code(&self) -> StatusCode {
match self.kind() {
@ -219,7 +210,7 @@ impl ResponseError for io::Error {
}
}
/// `BadRequest` for `InvalidHeaderValue`
/// Returns [`StatusCode::BAD_REQUEST`] for [`header::InvalidHeaderValue`].
impl ResponseError for header::InvalidHeaderValue {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
@ -308,33 +299,60 @@ impl From<httparse::Error> for ParseError {
}
}
/// A set of errors that can occur running blocking tasks in thread pool.
#[derive(Debug, Display)]
#[display(fmt = "Blocking thread pool is gone")]
pub struct BlockingError;
impl std::error::Error for BlockingError {}
/// `InternalServerError` for `BlockingError`
impl ResponseError for BlockingError {}
#[derive(Display, Debug)]
/// A set of errors that can occur during payload parsing
pub enum PayloadError {
/// A payload reached EOF, but is not complete.
#[display(
fmt = "A payload reached EOF, but is not complete. With error: {:?}",
fmt = "A payload reached EOF, but is not complete. Inner error: {:?}",
_0
)]
Incomplete(Option<io::Error>),
/// Content encoding stream corruption
/// Content encoding stream corruption.
#[display(fmt = "Can not decode content-encoding.")]
EncodingCorrupted,
/// A payload reached size limit.
#[display(fmt = "A payload reached size limit.")]
/// Payload reached size limit.
#[display(fmt = "Payload reached size limit.")]
Overflow,
/// A payload length is unknown.
#[display(fmt = "A payload length is unknown.")]
/// Payload length is unknown.
#[display(fmt = "Payload length is unknown.")]
UnknownLength,
/// Http2 payload error
/// HTTP/2 payload error.
#[display(fmt = "{}", _0)]
Http2Payload(h2::Error),
/// Io error
/// Generic I/O error.
#[display(fmt = "{}", _0)]
Io(io::Error),
}
impl std::error::Error for PayloadError {}
impl std::error::Error for PayloadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
PayloadError::Incomplete(None) => None,
PayloadError::Incomplete(Some(err)) => Some(err as &dyn std::error::Error),
PayloadError::EncodingCorrupted => None,
PayloadError::Overflow => None,
PayloadError::UnknownLength => None,
PayloadError::Http2Payload(err) => Some(err as &dyn std::error::Error),
PayloadError::Io(err) => Some(err as &dyn std::error::Error),
}
}
}
impl From<h2::Error> for PayloadError {
fn from(err: h2::Error) -> Self {
@ -354,15 +372,12 @@ impl From<io::Error> for PayloadError {
}
}
impl From<BlockingError<io::Error>> for PayloadError {
fn from(err: BlockingError<io::Error>) -> Self {
match err {
BlockingError::Error(e) => PayloadError::Io(e),
BlockingError::Canceled => PayloadError::Io(io::Error::new(
impl From<BlockingError> for PayloadError {
fn from(_: BlockingError) -> Self {
PayloadError::Io(io::Error::new(
io::ErrorKind::Other,
"Operation is canceled",
)),
}
))
}
}
@ -380,6 +395,7 @@ impl ResponseError for PayloadError {
}
/// Return `BadRequest` for `cookie::ParseError`
#[cfg(feature = "cookies")]
impl ResponseError for crate::cookie::ParseError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
@ -387,7 +403,7 @@ impl ResponseError for crate::cookie::ParseError {
}
#[derive(Debug, Display, From)]
/// A set of errors that can occur during dispatching http requests
/// A set of errors that can occur during dispatching HTTP requests
pub enum DispatchError {
/// Service error
Service(Error),
@ -951,16 +967,6 @@ where
InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into()
}
#[cfg(feature = "actors")]
/// `InternalServerError` for `actix::MailboxError`
/// This is supported on feature=`actors` only
impl ResponseError for actix::MailboxError {}
#[cfg(feature = "actors")]
/// `InternalServerError` for `actix::ResolverError`
/// This is supported on feature=`actors` only
impl ResponseError for actix::actors::resolver::ResolverError {}
#[cfg(test)]
mod tests {
use super::*;
@ -977,6 +983,7 @@ mod tests {
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[cfg(feature = "cookies")]
#[test]
fn test_cookie_parse() {
let resp: Response = CookieParseError::EmptyName.error_response();
@ -1018,22 +1025,22 @@ mod tests {
fn test_payload_error() {
let err: PayloadError =
io::Error::new(io::ErrorKind::Other, "ParseError").into();
assert!(format!("{}", err).contains("ParseError"));
assert!(err.to_string().contains("ParseError"));
let err = PayloadError::Incomplete(None);
assert_eq!(
format!("{}", err),
"A payload reached EOF, but is not complete. With error: None"
err.to_string(),
"A payload reached EOF, but is not complete. Inner error: None"
);
}
macro_rules! from {
($from:expr => $error:pat) => {
match ParseError::from($from) {
e @ $error => {
assert!(format!("{}", e).len() >= 5);
err @ $error => {
assert!(err.to_string().len() >= 5);
}
e => unreachable!("{:?}", e),
err => unreachable!("{:?}", err),
}
};
}
@ -1076,7 +1083,7 @@ mod tests {
let err = PayloadError::Overflow;
let resp_err: &dyn ResponseError = &err;
let err = resp_err.downcast_ref::<PayloadError>().unwrap();
assert_eq!(err.to_string(), "A payload reached size limit.");
assert_eq!(err.to_string(), "Payload reached size limit.");
let not_err = resp_err.downcast_ref::<ContentTypeError>();
assert!(not_err.is_none());
}

View File

@ -1,62 +1,119 @@
use std::any::{Any, TypeId};
use std::{fmt, mem};
use std::{
any::{Any, TypeId},
fmt, mem,
};
use fxhash::FxHashMap;
use ahash::AHashMap;
/// A type map for request extensions.
///
/// All entries into this map must be owned types (or static references).
#[derive(Default)]
/// A type map of request extensions.
pub struct Extensions {
/// Use FxHasher with a std HashMap with for faster
/// lookups on the small `TypeId` (u64 equivalent) keys.
map: FxHashMap<TypeId, Box<dyn Any>>,
map: AHashMap<TypeId, Box<dyn Any>>,
}
impl Extensions {
/// Create an empty `Extensions`.
/// Creates an empty `Extensions`.
#[inline]
pub fn new() -> Extensions {
Extensions {
map: FxHashMap::default(),
map: AHashMap::default(),
}
}
/// Insert a type into this `Extensions`.
/// Insert an item into the map.
///
/// If a extension of this type already existed, it will
/// be returned.
pub fn insert<T: 'static>(&mut self, val: T) {
self.map.insert(TypeId::of::<T>(), Box::new(val));
/// If an item of this type was already stored, it will be replaced and returned.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
/// assert_eq!(map.insert(""), None);
/// assert_eq!(map.insert(1u32), None);
/// assert_eq!(map.insert(2u32), Some(1u32));
/// assert_eq!(*map.get::<u32>().unwrap(), 2u32);
/// ```
pub fn insert<T: 'static>(&mut self, val: T) -> Option<T> {
self.map
.insert(TypeId::of::<T>(), Box::new(val))
.and_then(downcast_owned)
}
/// Check if container contains entry
/// Check if map contains an item of a given type.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
/// assert!(!map.contains::<u32>());
///
/// assert_eq!(map.insert(1u32), None);
/// assert!(map.contains::<u32>());
/// ```
pub fn contains<T: 'static>(&self) -> bool {
self.map.contains_key(&TypeId::of::<T>())
}
/// Get a reference to a type previously inserted on this `Extensions`.
/// Get a reference to an item of a given type.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
/// map.insert(1u32);
/// assert_eq!(map.get::<u32>(), Some(&1u32));
/// ```
pub fn get<T: 'static>(&self) -> Option<&T> {
self.map
.get(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_ref())
}
/// Get a mutable reference to a type previously inserted on this `Extensions`.
/// Get a mutable reference to an item of a given type.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
/// map.insert(1u32);
/// assert_eq!(map.get_mut::<u32>(), Some(&mut 1u32));
/// ```
pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.map
.get_mut(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_mut())
}
/// Remove a type from this `Extensions`.
/// Remove an item from the map of a given type.
///
/// If a extension of this type existed, it will be returned.
/// If an item of this type was already stored, it will be returned.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
///
/// map.insert(1u32);
/// assert_eq!(map.get::<u32>(), Some(&1u32));
///
/// assert_eq!(map.remove::<u32>(), Some(1u32));
/// assert!(!map.contains::<u32>());
/// ```
pub fn remove<T: 'static>(&mut self) -> Option<T> {
self.map
.remove(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast().ok().map(|boxed| *boxed))
self.map.remove(&TypeId::of::<T>()).and_then(downcast_owned)
}
/// Clear the `Extensions` of all inserted extensions.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
///
/// map.insert(1u32);
/// assert!(map.contains::<u32>());
///
/// map.clear();
/// assert!(!map.contains::<u32>());
/// ```
#[inline]
pub fn clear(&mut self) {
self.map.clear();
@ -79,6 +136,10 @@ impl fmt::Debug for Extensions {
}
}
fn downcast_owned<T: 'static>(boxed: Box<dyn Any>) -> Option<T> {
boxed.downcast().ok().map(|boxed| *boxed)
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -223,15 +223,3 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
Ok(())
}
}
pub struct Writer<'a>(pub &'a mut BytesMut);
impl<'a> io::Write for Writer<'a> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}

View File

@ -58,6 +58,7 @@ impl Codec {
} else {
Flags::empty()
};
Codec {
config,
flags,
@ -69,26 +70,26 @@ impl Codec {
}
}
/// Check if request is upgrade.
#[inline]
/// Check if request is upgrade
pub fn upgrade(&self) -> bool {
self.ctype == ConnectionType::Upgrade
}
/// Check if last response is keep-alive.
#[inline]
/// Check if last response is keep-alive
pub fn keepalive(&self) -> bool {
self.ctype == ConnectionType::KeepAlive
}
/// Check if keep-alive enabled on server level.
#[inline]
/// Check if keep-alive enabled on server level
pub fn keepalive_enabled(&self) -> bool {
self.flags.contains(Flags::KEEPALIVE_ENABLED)
}
/// Check last request's message type.
#[inline]
/// Check last request's message type
pub fn message_type(&self) -> MessageType {
if self.flags.contains(Flags::STREAM) {
MessageType::Stream
@ -110,8 +111,8 @@ impl Decoder for Codec {
type Error = ParseError;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
if self.payload.is_some() {
Ok(match self.payload.as_mut().unwrap().decode(src)? {
if let Some(ref mut payload) = self.payload {
Ok(match payload.decode(src)? {
Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))),
Some(PayloadItem::Eof) => {
self.payload.take();
@ -198,10 +199,10 @@ mod tests {
use http::Method;
use super::*;
use crate::httpmessage::HttpMessage;
use crate::HttpMessage;
#[test]
fn test_http_request_chunked_payload_and_next_message() {
#[actix_rt::test]
async fn test_http_request_chunked_payload_and_next_message() {
let mut codec = Codec::default();
let mut buf = BytesMut::from(

View File

@ -14,7 +14,7 @@ use crate::header::HeaderMap;
use crate::message::{ConnectionType, ResponseHead};
use crate::request::Request;
const MAX_BUFFER_SIZE: usize = 131_072;
pub(crate) const MAX_BUFFER_SIZE: usize = 131_072;
const MAX_HEADERS: usize = 96;
/// Incoming message decoder
@ -137,7 +137,7 @@ pub(crate) trait MessageType: Sized {
expect = true;
}
}
_ => (),
_ => {}
}
headers.append(name, value);
@ -203,7 +203,15 @@ impl MessageType for Request {
(len, method, uri, version, req.headers.len())
}
httparse::Status::Partial => return Ok(None),
httparse::Status::Partial => {
return if src.len() >= MAX_BUFFER_SIZE {
trace!("MAX_BUFFER_SIZE unprocessed data reached, closing");
Err(ParseError::TooLarge)
} else {
// Return None to notify more read are needed for parsing request
Ok(None)
};
}
}
};
@ -216,15 +224,12 @@ impl MessageType for Request {
let decoder = match length {
PayloadLength::Payload(pl) => pl,
PayloadLength::UpgradeWebSocket => {
// upgrade(websocket)
// upgrade (WebSocket)
PayloadType::Stream(PayloadDecoder::eof())
}
PayloadLength::None => {
if method == Method::CONNECT {
PayloadType::Stream(PayloadDecoder::eof())
} else if src.len() >= MAX_BUFFER_SIZE {
trace!("MAX_BUFFER_SIZE unprocessed data reached, closing");
return Err(ParseError::TooLarge);
} else {
PayloadType::None
}
@ -273,7 +278,14 @@ impl MessageType for ResponseHead {
(len, version, status, res.headers.len())
}
httparse::Status::Partial => return Ok(None),
httparse::Status::Partial => {
return if src.len() >= MAX_BUFFER_SIZE {
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
Err(ParseError::TooLarge)
} else {
Ok(None)
}
}
}
};
@ -289,9 +301,6 @@ impl MessageType for ResponseHead {
} else if status == StatusCode::SWITCHING_PROTOCOLS {
// switching protocol or connect
PayloadType::Stream(PayloadDecoder::eof())
} else if src.len() >= MAX_BUFFER_SIZE {
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
return Err(ParseError::TooLarge);
} else {
// for HTTP/1.0 read to eof and close connection
if msg.version == Version::HTTP_10 {
@ -643,7 +652,7 @@ mod tests {
use super::*;
use crate::error::ParseError;
use crate::http::header::{HeaderName, SET_COOKIE};
use crate::httpmessage::HttpMessage;
use crate::HttpMessage;
impl PayloadType {
fn unwrap(self) -> PayloadDecoder {
@ -685,7 +694,7 @@ mod tests {
match MessageDecoder::<Request>::default().decode($e) {
Err(err) => match err {
ParseError::Io(_) => unreachable!("Parse error expected"),
_ => (),
_ => {}
},
_ => unreachable!("Error expected"),
}
@ -821,8 +830,8 @@ mod tests {
.get_all(SET_COOKIE)
.map(|v| v.to_str().unwrap().to_owned())
.collect();
assert_eq!(val[1], "c1=cookie1");
assert_eq!(val[0], "c2=cookie2");
assert_eq!(val[0], "c1=cookie1");
assert_eq!(val[1], "c2=cookie2");
}
#[test]

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ use bytes::{BufMut, BytesMut};
use crate::body::BodySize;
use crate::config::ServiceConfig;
use crate::header::map;
use crate::header::{map::Value, HeaderName};
use crate::helpers;
use crate::http::header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
use crate::http::{HeaderMap, StatusCode, Version};
@ -21,7 +21,7 @@ const AVERAGE_HEADER_SIZE: usize = 30;
pub(crate) struct MessageEncoder<T: MessageType> {
pub length: BodySize,
pub te: TransferEncoding,
_t: PhantomData<T>,
_phantom: PhantomData<T>,
}
impl<T: MessageType> Default for MessageEncoder<T> {
@ -29,7 +29,7 @@ impl<T: MessageType> Default for MessageEncoder<T> {
MessageEncoder {
length: BodySize::None,
te: TransferEncoding::empty(),
_t: PhantomData,
_phantom: PhantomData,
}
}
}
@ -118,24 +118,14 @@ pub(crate) trait MessageType: Sized {
dst.put_slice(b"connection: close\r\n")
}
}
_ => (),
_ => {}
}
// merging headers from head and extra headers. HeaderMap::new() does not allocate.
let empty_headers = HeaderMap::new();
let extra_headers = self.extra_headers().unwrap_or(&empty_headers);
let headers = self
.headers()
.inner
.iter()
.filter(|(name, _)| !extra_headers.contains_key(*name))
.chain(extra_headers.inner.iter());
// write headers
let mut has_date = false;
let mut buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
let mut buf = dst.chunk_mut().as_mut_ptr();
let mut remaining = dst.capacity() - dst.len();
// tracks bytes written since last buffer resize
@ -143,19 +133,19 @@ pub(crate) trait MessageType: Sized {
// container's knowledge, this is used to sync the containers cursor after data is written
let mut pos = 0;
for (key, value) in headers {
self.write_headers(|key, value| {
match *key {
CONNECTION => continue,
TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue,
CONNECTION => return,
TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => return,
DATE => has_date = true,
_ => (),
_ => {}
}
let k = key.as_str().as_bytes();
let k_len = k.len();
match value {
map::Value::One(ref val) => {
// TODO: drain?
for val in value.iter() {
let v = val.as_ref();
let v_len = v.len();
@ -163,8 +153,6 @@ pub(crate) trait MessageType: Sized {
let len = k_len + v_len + 4;
if len > remaining {
// not enough room in buffer for this header; reserve more space
// SAFETY: all the bytes written up to position "pos" are initialized
// the written byte count and pointer advancement are kept in sync
unsafe {
@ -177,60 +165,14 @@ pub(crate) trait MessageType: Sized {
// re-assign buf raw pointer since it's possible that the buffer was
// reallocated and/or resized
buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
}
// SAFETY: on each write, it is enough to ensure that the advancement of the
// cursor matches the number of bytes written
unsafe {
// use upper Camel-Case
if camel_case {
write_camel_case(k, from_raw_parts_mut(buf, k_len))
} else {
write_data(k, buf, k_len)
}
buf = buf.add(k_len);
write_data(b": ", buf, 2);
buf = buf.add(2);
write_data(v, buf, v_len);
buf = buf.add(v_len);
write_data(b"\r\n", buf, 2);
buf = buf.add(2);
}
pos += len;
remaining -= len;
}
map::Value::Multi(ref vec) => {
for val in vec {
let v = val.as_ref();
let v_len = v.len();
let len = k_len + v_len + 4;
if len > remaining {
// SAFETY: all the bytes written up to position "pos" are initialized
// the written byte count and pointer advancement are kept in sync
unsafe {
dst.advance_mut(pos);
}
pos = 0;
dst.reserve(len * 2);
remaining = dst.capacity() - dst.len();
// re-assign buf raw pointer since it's possible that the buffer was
// reallocated and/or resized
buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
buf = dst.chunk_mut().as_mut_ptr();
}
// SAFETY: on each write, it is enough to ensure that the advancement of
// the cursor matches the number of bytes written
unsafe {
if camel_case {
// use Camel-Case headers
write_camel_case(k, from_raw_parts_mut(buf, k_len));
} else {
write_data(k, buf, k_len);
@ -251,9 +193,7 @@ pub(crate) trait MessageType: Sized {
pos += len;
remaining -= len;
}
}
}
}
});
// final cursor synchronization with the bytes container
//
@ -273,6 +213,24 @@ pub(crate) trait MessageType: Sized {
Ok(())
}
fn write_headers<F>(&mut self, mut f: F)
where
F: FnMut(&HeaderName, &Value),
{
match self.extra_headers() {
Some(headers) => {
// merging headers from head and extra headers.
self.headers()
.inner
.iter()
.filter(|(name, _)| !headers.contains_key(*name))
.chain(headers.inner.iter())
.for_each(|(k, v)| f(k, v))
}
None => self.headers().inner.iter().for_each(|(k, v)| f(k, v)),
}
}
}
impl MessageType for Response<()> {
@ -329,7 +287,7 @@ impl MessageType for RequestHeadType {
let head = self.as_ref();
dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE);
write!(
Writer(dst),
helpers::Writer(dst),
"{} {} {}",
head.method,
head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"),
@ -462,7 +420,7 @@ impl TransferEncoding {
*eof = true;
buf.extend_from_slice(b"0\r\n\r\n");
} else {
writeln!(Writer(buf), "{:X}\r", msg.len())
writeln!(helpers::Writer(buf), "{:X}\r", msg.len())
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
buf.reserve(msg.len() + 2);
@ -512,18 +470,6 @@ impl TransferEncoding {
}
}
struct Writer<'a>(pub &'a mut BytesMut);
impl<'a> io::Write for Writer<'a> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
/// # Safety
/// Callers must ensure that the given length matches given value length.
unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) {
@ -532,30 +478,29 @@ unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) {
}
fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
let mut index = 0;
let key = value;
let mut key_iter = key.iter();
// first copy entire (potentially wrong) slice to output
buffer[..value.len()].copy_from_slice(value);
if let Some(c) = key_iter.next() {
if *c >= b'a' && *c <= b'z' {
buffer[index] = *c ^ b' ';
index += 1;
}
} else {
return;
let mut iter = value.iter();
// first character should be uppercase
if let Some(c @ b'a'..=b'z') = iter.next() {
buffer[0] = c & 0b1101_1111;
}
// track 1 ahead of the current position since that's the location being assigned to
let mut index = 2;
// remaining characters after hyphens should also be uppercase
while let Some(&c) = iter.next() {
if c == b'-' {
// advance iter by one and uppercase if needed
if let Some(c @ b'a'..=b'z') = iter.next() {
buffer[index] = c & 0b1101_1111;
}
}
while let Some(c) = key_iter.next() {
buffer[index] = *c;
index += 1;
if *c == b'-' {
if let Some(c) = key_iter.next() {
if *c >= b'a' && *c <= b'z' {
buffer[index] = *c ^ b' ';
index += 1;
}
}
}
}
}
@ -584,8 +529,8 @@ mod tests {
);
}
#[test]
fn test_camel_case() {
#[actix_rt::test]
async fn test_camel_case() {
let mut bytes = BytesMut::with_capacity(2048);
let mut head = RequestHead::default();
head.set_camel_case_headers(true);
@ -604,6 +549,7 @@ mod tests {
);
let data =
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
assert!(data.contains("Content-Length: 0\r\n"));
assert!(data.contains("Connection: close\r\n"));
assert!(data.contains("Content-Type: plain/text\r\n"));
@ -646,8 +592,8 @@ mod tests {
assert!(data.contains("date: date\r\n"));
}
#[test]
fn test_extra_headers() {
#[actix_rt::test]
async fn test_extra_headers() {
let mut bytes = BytesMut::with_capacity(2048);
let mut head = RequestHead::default();
@ -680,16 +626,15 @@ mod tests {
assert!(data.contains("date: date\r\n"));
}
#[test]
fn test_no_content_length() {
#[actix_rt::test]
async 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(DATE, HeaderValue::from_static(&""));
res.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static(&"0"));
.insert(CONTENT_LENGTH, HeaderValue::from_static("0"));
let _ = res.encode_headers(
&mut bytes,

View File

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

View File

@ -1,4 +1,4 @@
//! HTTP/1 implementation
//! HTTP/1 protocol implementation.
use bytes::{Bytes, BytesMut};
mod client;
@ -17,7 +17,7 @@ pub use self::codec::Codec;
pub use self::dispatcher::Dispatcher;
pub use self::expect::ExpectHandler;
pub use self::payload::Payload;
pub use self::service::{H1Service, H1ServiceHandler, OneRequest};
pub use self::service::{H1Service, H1ServiceHandler};
pub use self::upgrade::UpgradeHandler;
pub use self::utils::SendResponse;

View File

@ -3,9 +3,8 @@ use std::cell::RefCell;
use std::collections::VecDeque;
use std::pin::Pin;
use std::rc::{Rc, Weak};
use std::task::{Context, Poll};
use std::task::{Context, Poll, Waker};
use actix_utils::task::LocalWaker;
use bytes::Bytes;
use futures_core::Stream;
@ -134,7 +133,7 @@ impl PayloadSender {
if shared.borrow().need_read {
PayloadStatus::Read
} else {
shared.borrow_mut().io_task.register(cx.waker());
shared.borrow_mut().register_io(cx);
PayloadStatus::Pause
}
} else {
@ -150,8 +149,8 @@ struct Inner {
err: Option<PayloadError>,
need_read: bool,
items: VecDeque<Bytes>,
task: LocalWaker,
io_task: LocalWaker,
task: Option<Waker>,
io_task: Option<Waker>,
}
impl Inner {
@ -162,8 +161,48 @@ impl Inner {
err: None,
items: VecDeque::new(),
need_read: true,
task: LocalWaker::new(),
io_task: LocalWaker::new(),
task: None,
io_task: None,
}
}
/// Wake up future waiting for payload data to be available.
fn wake(&mut self) {
if let Some(waker) = self.task.take() {
waker.wake();
}
}
/// Wake up future feeding data to Payload.
fn wake_io(&mut self) {
if let Some(waker) = self.io_task.take() {
waker.wake();
}
}
/// Register future waiting data from payload.
/// Waker would be used in `Inner::wake`
fn register(&mut self, cx: &mut Context<'_>) {
if self
.task
.as_ref()
.map(|w| !cx.waker().will_wake(w))
.unwrap_or(true)
{
self.task = Some(cx.waker().clone());
}
}
// Register future feeding data to payload.
/// Waker would be used in `Inner::wake_io`
fn register_io(&mut self, cx: &mut Context<'_>) {
if self
.io_task
.as_ref()
.map(|w| !cx.waker().will_wake(w))
.unwrap_or(true)
{
self.io_task = Some(cx.waker().clone());
}
}
@ -182,9 +221,7 @@ impl Inner {
self.len += data.len();
self.items.push_back(data);
self.need_read = self.len < MAX_BUFFER_SIZE;
if let Some(task) = self.task.take() {
task.wake()
}
self.wake();
}
#[cfg(test)]
@ -201,9 +238,9 @@ impl Inner {
self.need_read = self.len < MAX_BUFFER_SIZE;
if self.need_read && !self.eof {
self.task.register(cx.waker());
self.register(cx);
}
self.io_task.wake();
self.wake_io();
Poll::Ready(Some(Ok(data)))
} else if let Some(err) = self.err.take() {
Poll::Ready(Some(Err(err)))
@ -211,8 +248,8 @@ impl Inner {
Poll::Ready(None)
} else {
self.need_read = true;
self.task.register(cx.waker());
self.io_task.wake();
self.register(cx);
self.wake_io();
Poll::Pending
}
}

View File

@ -9,42 +9,40 @@ use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::net::TcpStream;
use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory};
use futures_core::ready;
use futures_util::future::{ok, Ready};
use futures_util::future::ready;
use crate::body::MessageBody;
use crate::cloneable::CloneableService;
use crate::config::ServiceConfig;
use crate::error::{DispatchError, Error, ParseError};
use crate::helpers::DataFactory;
use crate::error::{DispatchError, Error};
use crate::request::Request;
use crate::response::Response;
use crate::{ConnectCallback, Extensions};
use crate::service::HttpFlow;
use crate::{ConnectCallback, OnConnectData};
use super::codec::Codec;
use super::dispatcher::Dispatcher;
use super::{ExpectHandler, Message, UpgradeHandler};
use super::{ExpectHandler, UpgradeHandler};
/// `ServiceFactory` implementation for HTTP1 transport
pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler<T>> {
pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
srv: S,
cfg: ServiceConfig,
expect: X,
upgrade: Option<U>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B)>,
_phantom: PhantomData<B>,
}
impl<T, S, B> H1Service<T, S, B>
where
S: ServiceFactory<Config = (), Request = Request>,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
{
/// Create new `HttpService` instance with config.
pub(crate) fn with_config<F: IntoServiceFactory<S>>(
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
cfg: ServiceConfig,
service: F,
) -> Self {
@ -53,28 +51,23 @@ where
srv: service.into_factory(),
expect: ExpectHandler,
upgrade: None,
on_connect: None,
on_connect_ext: None,
_t: PhantomData,
_phantom: PhantomData,
}
}
}
impl<S, B, X, U> H1Service<TcpStream, S, B, X, U>
where
S: ServiceFactory<Config = (), Request = Request>,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<
Config = (),
Request = (Request, Framed<TcpStream, Codec>),
Response = (),
>,
U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>,
U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug,
{
@ -82,15 +75,15 @@ where
pub fn tcp(
self,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = DispatchError,
InitError = (),
> {
pipeline_factory(|io: TcpStream| {
let peer_addr = io.peer_addr().ok();
ok((io, peer_addr))
ready(Ok((io, peer_addr)))
})
.and_then(self)
}
@ -100,22 +93,23 @@ where
mod openssl {
use super::*;
use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream};
use actix_tls::{openssl::HandshakeError, TlsError};
use actix_service::ServiceFactoryExt;
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream};
use actix_tls::accept::TlsError;
impl<S, B, X, U> H1Service<SslStream<TcpStream>, S, B, X, U>
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Config = (), Request = Request>,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (),
Request = (Request, Framed<SslStream<TcpStream>, Codec>),
Response = (),
>,
U::Error: fmt::Display + Into<Error>,
@ -126,10 +120,10 @@ mod openssl {
self,
acceptor: SslAcceptor,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = TlsError<HandshakeError<TcpStream>, DispatchError>,
Error = TlsError<SslError, DispatchError>,
InitError = (),
> {
pipeline_factory(
@ -137,9 +131,9 @@ mod openssl {
.map_err(TlsError::Tls)
.map_init_err(|_| panic!()),
)
.and_then(|io: SslStream<TcpStream>| {
.and_then(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().peer_addr().ok();
ok((io, peer_addr))
ready(Ok((io, peer_addr)))
})
.and_then(self.map_err(TlsError::Service))
}
@ -149,23 +143,24 @@ mod openssl {
#[cfg(feature = "rustls")]
mod rustls {
use super::*;
use actix_tls::rustls::{Acceptor, ServerConfig, TlsStream};
use actix_tls::TlsError;
use actix_service::ServiceFactoryExt;
use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream};
use actix_tls::accept::TlsError;
use std::{fmt, io};
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Config = (), Request = Request>,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (),
Request = (Request, Framed<TlsStream<TcpStream>, Codec>),
Response = (),
>,
U::Error: fmt::Display + Into<Error>,
@ -176,8 +171,8 @@ mod rustls {
self,
config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
@ -189,7 +184,7 @@ mod rustls {
)
.and_then(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok();
ok((io, peer_addr))
ready(Ok((io, peer_addr)))
})
.and_then(self.map_err(TlsError::Service))
}
@ -198,7 +193,7 @@ mod rustls {
impl<T, S, B, X, U> H1Service<T, S, B, X, U>
where
S: ServiceFactory<Config = (), Request = Request>,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
S::InitError: fmt::Debug,
@ -206,7 +201,7 @@ where
{
pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U>
where
X1: ServiceFactory<Request = Request, Response = Request>,
X1: ServiceFactory<Request, Response = Request>,
X1::Error: Into<Error>,
X1::InitError: fmt::Debug,
{
@ -215,15 +210,14 @@ where
cfg: self.cfg,
srv: self.srv,
upgrade: self.upgrade,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData,
_phantom: PhantomData,
}
}
pub fn upgrade<U1>(self, upgrade: Option<U1>) -> H1Service<T, S, B, X, U1>
where
U1: ServiceFactory<Request = (Request, Framed<T, Codec>), Response = ()>,
U1: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
U1::Error: fmt::Display,
U1::InitError: fmt::Debug,
{
@ -232,21 +226,11 @@ where
cfg: self.cfg,
srv: self.srv,
expect: self.expect,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_t: PhantomData,
_phantom: PhantomData,
}
}
/// Set on connect callback.
pub(crate) fn on_connect(
mut self,
f: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
) -> Self {
self.on_connect = f;
self
}
/// Set on connect callback.
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
self.on_connect_ext = f;
@ -254,27 +238,27 @@ where
}
}
impl<T, S, B, X, U> ServiceFactory for H1Service<T, S, B, X, U>
impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)>
for H1Service<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Config = (), Request = Request>,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
S::InitError: fmt::Debug,
B: MessageBody,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<Config = (), Request = (Request, Framed<T, Codec>), Response = ()>,
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug,
{
type Config = ();
type Request = (T, Option<net::SocketAddr>);
type Response = ();
type Error = DispatchError;
type InitError = ();
type Config = ();
type Service = H1ServiceHandler<T, S::Service, B, X::Service, U::Service>;
type InitError = ();
type Future = H1ServiceResponse<T, S, B, X, U>;
fn new_service(&self, _: ()) -> Self::Future {
@ -284,10 +268,9 @@ where
fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())),
expect: None,
upgrade: None,
on_connect: self.on_connect.clone(),
on_connect_ext: self.on_connect_ext.clone(),
cfg: Some(self.cfg.clone()),
_t: PhantomData,
_phantom: PhantomData,
}
}
}
@ -296,13 +279,13 @@ where
#[pin_project::pin_project]
pub struct H1ServiceResponse<T, S, B, X, U>
where
S: ServiceFactory<Request = Request>,
S: ServiceFactory<Request>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
X: ServiceFactory<Request = Request, Response = Request>,
X: ServiceFactory<Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<Request = (Request, Framed<T, Codec>), Response = ()>,
U: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
{
@ -314,24 +297,23 @@ where
fut_upg: Option<U::Future>,
expect: Option<X::Service>,
upgrade: Option<U::Service>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: Option<ServiceConfig>,
_t: PhantomData<(T, B)>,
_phantom: PhantomData<B>,
}
impl<T, S, B, X, U> Future for H1ServiceResponse<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Request = Request>,
S: ServiceFactory<Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
S::InitError: fmt::Debug,
B: MessageBody,
X: ServiceFactory<Request = Request, Response = Request>,
X: ServiceFactory<Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<Request = (Request, Framed<T, Codec>), Response = ()>,
U: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
{
@ -355,7 +337,7 @@ where
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.upgrade = Some(upgrade);
this.fut_ex.set(None);
this.fut_upg.set(None);
}
let result = ready!(this
@ -371,7 +353,6 @@ where
service,
this.expect.take().unwrap(),
this.upgrade.take(),
this.on_connect.clone(),
this.on_connect_ext.clone(),
)
}))
@ -379,66 +360,65 @@ where
}
/// `Service` implementation for HTTP/1 transport
pub struct H1ServiceHandler<T, S: Service, B, X: Service, U: Service> {
srv: CloneableService<S>,
expect: CloneableService<X>,
upgrade: Option<CloneableService<U>>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
pub struct H1ServiceHandler<T, S, B, X, U>
where
S: Service<Request>,
X: Service<Request>,
U: Service<(Request, Framed<T, Codec>)>,
{
flow: Rc<HttpFlow<S, X, U>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: ServiceConfig,
_t: PhantomData<(T, B)>,
_phantom: PhantomData<B>,
}
impl<T, S, B, X, U> H1ServiceHandler<T, S, B, X, U>
where
S: Service<Request = Request>,
S: Service<Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
B: MessageBody,
X: Service<Request = Request, Response = Request>,
X: Service<Request, Response = Request>,
X::Error: Into<Error>,
U: Service<Request = (Request, Framed<T, Codec>), Response = ()>,
U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
{
fn new(
cfg: ServiceConfig,
srv: S,
service: S,
expect: X,
upgrade: Option<U>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
) -> H1ServiceHandler<T, S, B, X, U> {
H1ServiceHandler {
srv: CloneableService::new(srv),
expect: CloneableService::new(expect),
upgrade: upgrade.map(CloneableService::new),
flow: HttpFlow::new(service, expect, upgrade),
cfg,
on_connect,
on_connect_ext,
_t: PhantomData,
_phantom: PhantomData,
}
}
}
impl<T, S, B, X, U> Service for H1ServiceHandler<T, S, B, X, U>
impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)>
for H1ServiceHandler<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request = Request>,
S: Service<Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
B: MessageBody,
X: Service<Request = Request, Response = Request>,
X: Service<Request, Response = Request>,
X::Error: Into<Error>,
U: Service<Request = (Request, Framed<T, Codec>), Response = ()>,
U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display + Into<Error>,
{
type Request = (T, Option<net::SocketAddr>);
type Response = ();
type Error = DispatchError;
type Future = Dispatcher<T, S, B, X, U>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
let ready = self
.flow
.expect
.poll_ready(cx)
.map_err(|e| {
@ -449,7 +429,8 @@ where
.is_ready();
let ready = self
.srv
.flow
.service
.poll_ready(cx)
.map_err(|e| {
let e = e.into();
@ -459,7 +440,7 @@ where
.is_ready()
&& ready;
let ready = if let Some(ref mut upg) = self.upgrade {
let ready = if let Some(ref upg) = self.flow.upgrade {
upg.poll_ready(cx)
.map_err(|e| {
let e = e.into();
@ -479,124 +460,16 @@ where
}
}
fn call(&mut self, (io, addr): Self::Request) -> Self::Future {
let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
let mut connect_extensions = Extensions::new();
if let Some(ref handler) = self.on_connect_ext {
// run on_connect_ext callback, populating connect extensions
handler(&io, &mut connect_extensions);
}
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
let on_connect_data =
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
Dispatcher::new(
io,
self.cfg.clone(),
self.srv.clone(),
self.expect.clone(),
self.upgrade.clone(),
deprecated_on_connect,
connect_extensions,
self.flow.clone(),
on_connect_data,
addr,
)
}
}
/// `ServiceFactory` implementation for `OneRequestService` service
#[derive(Default)]
pub struct OneRequest<T> {
config: ServiceConfig,
_t: PhantomData<T>,
}
impl<T> OneRequest<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
/// Create new `H1SimpleService` instance.
pub fn new() -> Self {
OneRequest {
config: ServiceConfig::default(),
_t: PhantomData,
}
}
}
impl<T> ServiceFactory for OneRequest<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
type Config = ();
type Request = T;
type Response = (Request, Framed<T, Codec>);
type Error = ParseError;
type InitError = ();
type Service = OneRequestService<T>;
type Future = Ready<Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
ok(OneRequestService {
_t: PhantomData,
config: self.config.clone(),
})
}
}
/// `Service` implementation for HTTP1 transport. Reads one request and returns
/// request and framed object.
pub struct OneRequestService<T> {
_t: PhantomData<T>,
config: ServiceConfig,
}
impl<T> Service for OneRequestService<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
type Request = T;
type Response = (Request, Framed<T, Codec>);
type Error = ParseError;
type Future = OneRequestServiceResponse<T>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Self::Request) -> Self::Future {
OneRequestServiceResponse {
framed: Some(Framed::new(req, Codec::new(self.config.clone()))),
}
}
}
#[doc(hidden)]
#[pin_project::pin_project]
pub struct OneRequestServiceResponse<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
#[pin]
framed: Option<Framed<T, Codec>>,
}
impl<T> Future for OneRequestServiceResponse<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
type Output = Result<(Request, Framed<T, Codec>), ParseError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.as_mut().project();
match ready!(this.framed.as_pin_mut().unwrap().next_item(cx)) {
Some(Ok(req)) => match req {
Message::Item(req) => {
let mut this = self.as_mut().project();
Poll::Ready(Ok((req, this.framed.take().unwrap())))
}
Message::Chunk(_) => unreachable!("Something is wrong"),
},
Some(Err(err)) => Poll::Ready(Err(err)),
None => Poll::Ready(Err(ParseError::Incomplete)),
}
}
}

View File

@ -1,22 +1,20 @@
use std::marker::PhantomData;
use std::task::{Context, Poll};
use std::task::Poll;
use actix_codec::Framed;
use actix_service::{Service, ServiceFactory};
use futures_util::future::Ready;
use futures_util::future::{ready, Ready};
use crate::error::Error;
use crate::h1::Codec;
use crate::request::Request;
pub struct UpgradeHandler<T>(PhantomData<T>);
pub struct UpgradeHandler;
impl<T> ServiceFactory for UpgradeHandler<T> {
type Config = ();
type Request = (Request, Framed<T, Codec>);
impl<T> ServiceFactory<(Request, Framed<T, Codec>)> for UpgradeHandler {
type Response = ();
type Error = Error;
type Service = UpgradeHandler<T>;
type Config = ();
type Service = UpgradeHandler;
type InitError = Error;
type Future = Ready<Result<Self::Service, Self::InitError>>;
@ -25,17 +23,14 @@ impl<T> ServiceFactory for UpgradeHandler<T> {
}
}
impl<T> Service for UpgradeHandler<T> {
type Request = (Request, Framed<T, Codec>);
impl<T> Service<(Request, Framed<T, Codec>)> for UpgradeHandler {
type Response = ();
type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
actix_service::always_ready!();
fn call(&mut self, _: Self::Request) -> Self::Future {
unimplemented!()
fn call(&self, _: (Request, Framed<T, Codec>)) -> Self::Future {
ready(Ok(()))
}
}

View File

@ -1,102 +1,73 @@
use std::convert::TryFrom;
use std::future::Future;
use std::marker::PhantomData;
use std::net;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{cmp, future::Future, marker::PhantomData, net, pin::Pin, rc::Rc};
use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::time::{Delay, Instant};
use actix_service::Service;
use bytes::{Bytes, BytesMut};
use futures_core::ready;
use h2::server::{Connection, SendResponse};
use h2::SendStream;
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
use log::{error, trace};
use crate::body::{BodySize, MessageBody, ResponseBody};
use crate::cloneable::CloneableService;
use crate::config::ServiceConfig;
use crate::error::{DispatchError, Error};
use crate::helpers::DataFactory;
use crate::httpmessage::HttpMessage;
use crate::message::ResponseHead;
use crate::payload::Payload;
use crate::request::Request;
use crate::response::Response;
use crate::Extensions;
use crate::service::HttpFlow;
use crate::OnConnectData;
const CHUNK_SIZE: usize = 16_384;
/// Dispatcher for HTTP/2 protocol
/// Dispatcher for HTTP/2 protocol.
#[pin_project::pin_project]
pub struct Dispatcher<T, S: Service<Request = Request>, B: MessageBody>
pub struct Dispatcher<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>,
B: MessageBody,
{
service: CloneableService<S>,
flow: Rc<HttpFlow<S, X, U>>,
connection: Connection<T, Bytes>,
on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
on_connect_data: OnConnectData,
config: ServiceConfig,
peer_addr: Option<net::SocketAddr>,
ka_expire: Instant,
ka_timer: Option<Delay>,
_t: PhantomData<B>,
_phantom: PhantomData<B>,
}
impl<T, S, B> Dispatcher<T, S, B>
impl<T, S, B, X, U> Dispatcher<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request = Request>,
S: Service<Request>,
S::Error: Into<Error>,
// S::Future: 'static,
S::Response: Into<Response<B>>,
B: MessageBody,
{
pub(crate) fn new(
service: CloneableService<S>,
flow: Rc<HttpFlow<S, X, U>>,
connection: Connection<T, Bytes>,
on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
on_connect_data: OnConnectData,
config: ServiceConfig,
timeout: Option<Delay>,
peer_addr: Option<net::SocketAddr>,
) -> Self {
// let keepalive = config.keep_alive_enabled();
// let flags = if keepalive {
// Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED
// } else {
// Flags::empty()
// };
// keep-alive timer
let (ka_expire, ka_timer) = if let Some(delay) = timeout {
(delay.deadline(), Some(delay))
} else if let Some(delay) = config.keep_alive_timer() {
(delay.deadline(), Some(delay))
} else {
(config.now(), None)
};
Dispatcher {
service,
flow,
config,
peer_addr,
connection,
on_connect,
on_connect_data,
ka_expire,
ka_timer,
_t: PhantomData,
_phantom: PhantomData,
}
}
}
impl<T, S, B> Future for Dispatcher<T, S, B>
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request = Request>,
S: Service<Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
@ -109,56 +80,39 @@ where
let this = self.get_mut();
loop {
match Pin::new(&mut this.connection).poll_accept(cx) {
Poll::Ready(None) => return Poll::Ready(Ok(())),
Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err.into())),
Poll::Ready(Some(Ok((req, res)))) => {
// update keep-alive expire
if this.ka_timer.is_some() {
if let Some(expire) = this.config.keep_alive_expire() {
this.ka_expire = expire;
}
}
match ready!(Pin::new(&mut this.connection).poll_accept(cx)) {
None => return Poll::Ready(Ok(())),
Some(Err(err)) => return Poll::Ready(Err(err.into())),
Some(Ok((req, res))) => {
let (parts, body) = req.into_parts();
let mut req = Request::with_payload(Payload::<
crate::payload::PayloadStream,
>::H2(
crate::h2::Payload::new(body)
));
let pl = crate::h2::Payload::new(body);
let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
let mut req = Request::with_payload(pl);
let head = &mut req.head_mut();
let head = req.head_mut();
head.uri = parts.uri;
head.method = parts.method;
head.version = parts.version;
head.headers = parts.headers.into();
head.peer_addr = this.peer_addr;
// DEPRECATED
// set on_connect data
if let Some(ref on_connect) = this.on_connect {
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);
this.on_connect_data.merge_into(&mut req);
actix_rt::spawn(ServiceResponse::<
S::Future,
S::Response,
S::Error,
B,
> {
let svc = ServiceResponse {
state: ServiceResponseState::ServiceCall(
this.service.call(req),
this.flow.service.call(req),
Some(res),
),
config: this.config.clone(),
buffer: None,
_t: PhantomData,
});
_phantom: PhantomData,
};
actix_rt::spawn(svc);
}
Poll::Pending => return Poll::Pending,
}
}
}
@ -170,7 +124,7 @@ struct ServiceResponse<F, I, E, B> {
state: ServiceResponseState<F, B>,
config: ServiceConfig,
buffer: Option<Bytes>,
_t: PhantomData<(I, E)>,
_phantom: PhantomData<(I, E)>,
}
#[pin_project::pin_project(project = ServiceResponseStateProj)]
@ -207,27 +161,36 @@ where
skip_len = true;
*size = BodySize::Stream;
}
_ => (),
_ => {}
}
let _ = match size {
BodySize::None | BodySize::Stream => None,
BodySize::Empty => res
.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
BodySize::Sized(len) => res.headers_mut().insert(
BodySize::Sized(len) => {
let mut buf = itoa::Buffer::new();
res.headers_mut().insert(
CONTENT_LENGTH,
HeaderValue::try_from(format!("{}", len)).unwrap(),
),
HeaderValue::from_str(buf.format(*len)).unwrap(),
)
}
};
// copy headers
for (key, value) in head.headers.iter() {
match *key {
CONNECTION | TRANSFER_ENCODING => continue, // http2 specific
// TODO: consider skipping other headers according to:
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2
// omit HTTP/1.x only headers
CONNECTION | TRANSFER_ENCODING => continue,
CONTENT_LENGTH if skip_len => continue,
DATE => has_date = true,
_ => (),
_ => {}
}
res.headers_mut().append(key, value.clone());
}
@ -259,18 +222,20 @@ where
let mut this = self.as_mut().project();
match this.state.project() {
ServiceResponseStateProj::ServiceCall(call, send) => match call.poll(cx) {
Poll::Ready(Ok(res)) => {
ServiceResponseStateProj::ServiceCall(call, send) => {
match ready!(call.poll(cx)) {
Ok(res) => {
let (res, body) = res.into().replace_body(());
let mut send = send.take().unwrap();
let mut size = body.size();
let h2_res = self.as_mut().prepare_response(res.head(), &mut size);
let h2_res =
self.as_mut().prepare_response(res.head(), &mut size);
this = self.as_mut().project();
let stream = match send.send_response(h2_res, size.is_eof()) {
Err(e) => {
trace!("Error sending h2 response: {:?}", e);
trace!("Error sending HTTP/2 response: {:?}", e);
return Poll::Ready(());
}
Ok(stream) => stream,
@ -284,19 +249,20 @@ where
self.poll(cx)
}
}
Poll::Pending => Poll::Pending,
Poll::Ready(Err(e)) => {
Err(e) => {
let res: Response = e.into().into();
let (res, body) = res.replace_body(());
let mut send = send.take().unwrap();
let mut size = body.size();
let h2_res = self.as_mut().prepare_response(res.head(), &mut size);
let h2_res =
self.as_mut().prepare_response(res.head(), &mut size);
this = self.as_mut().project();
let stream = match send.send_response(h2_res, size.is_eof()) {
Err(e) => {
trace!("Error sending h2 response: {:?}", e);
trace!("Error sending HTTP/2 response: {:?}", e);
return Poll::Ready(());
}
Ok(stream) => stream,
@ -312,57 +278,55 @@ where
self.poll(cx)
}
}
},
}
}
ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => {
loop {
loop {
if let Some(ref mut buffer) = this.buffer {
match stream.poll_capacity(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => return Poll::Ready(()),
Poll::Ready(Some(Ok(cap))) => {
match this.buffer {
Some(ref mut buffer) => match ready!(stream.poll_capacity(cx)) {
None => return Poll::Ready(()),
Some(Ok(cap)) => {
let len = buffer.len();
let bytes = buffer.split_to(std::cmp::min(cap, len));
let bytes = buffer.split_to(cmp::min(cap, len));
if let Err(e) = stream.send_data(bytes, false) {
warn!("{:?}", e);
return Poll::Ready(());
} else if !buffer.is_empty() {
let cap =
std::cmp::min(buffer.len(), CHUNK_SIZE);
let cap = cmp::min(buffer.len(), CHUNK_SIZE);
stream.reserve_capacity(cap);
} else {
this.buffer.take();
}
}
Poll::Ready(Some(Err(e))) => {
Some(Err(e)) => {
warn!("{:?}", e);
return Poll::Ready(());
}
}
} else {
match body.as_mut().poll_next(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => {
if let Err(e) = stream.send_data(Bytes::new(), true)
{
},
None => match ready!(body.as_mut().poll_next(cx)) {
None => {
if let Err(e) = stream.send_data(Bytes::new(), true) {
warn!("{:?}", e);
}
return Poll::Ready(());
}
Poll::Ready(Some(Ok(chunk))) => {
stream.reserve_capacity(std::cmp::min(
chunk.len(),
CHUNK_SIZE,
));
Some(Ok(chunk)) => {
stream
.reserve_capacity(cmp::min(chunk.len(), CHUNK_SIZE));
*this.buffer = Some(chunk);
}
Poll::Ready(Some(Err(e))) => {
Some(Err(e)) => {
error!("Response payload stream error: {:?}", e);
return Poll::Ready(());
}
}
}
},
}
}
}

View File

@ -1,9 +1,12 @@
//! HTTP/2 implementation
use std::pin::Pin;
use std::task::{Context, Poll};
//! HTTP/2 protocol.
use std::{
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use futures_core::Stream;
use futures_core::{ready, Stream};
use h2::RecvStream;
mod dispatcher;
@ -13,14 +16,14 @@ pub use self::dispatcher::Dispatcher;
pub use self::service::H2Service;
use crate::error::PayloadError;
/// H2 receive stream
/// HTTP/2 peer stream.
pub struct Payload {
pl: RecvStream,
stream: RecvStream,
}
impl Payload {
pub(crate) fn new(pl: RecvStream) -> Self {
Self { pl }
pub(crate) fn new(stream: RecvStream) -> Self {
Self { stream }
}
}
@ -33,18 +36,17 @@ impl Stream for Payload {
) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
match Pin::new(&mut this.pl).poll_data(cx) {
Poll::Ready(Some(Ok(chunk))) => {
match ready!(Pin::new(&mut this.stream).poll_data(cx)) {
Some(Ok(chunk)) => {
let len = chunk.len();
if let Err(err) = this.pl.flow_control().release_capacity(len) {
Poll::Ready(Some(Err(err.into())))
} else {
Poll::Ready(Some(Ok(chunk)))
match this.stream.flow_control().release_capacity(len) {
Ok(()) => Poll::Ready(Some(Ok(chunk))),
Err(err) => Poll::Ready(Some(Err(err.into()))),
}
}
Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err.into()))),
Poll::Pending => Poll::Pending,
Poll::Ready(None) => Poll::Ready(None),
Some(Err(err)) => Poll::Ready(Some(Err(err.into()))),
None => Poll::Ready(None),
}
}
}

View File

@ -17,57 +17,44 @@ use h2::server::{self, Handshake};
use log::error;
use crate::body::MessageBody;
use crate::cloneable::CloneableService;
use crate::config::ServiceConfig;
use crate::error::{DispatchError, Error};
use crate::helpers::DataFactory;
use crate::request::Request;
use crate::response::Response;
use crate::{ConnectCallback, Extensions};
use crate::service::HttpFlow;
use crate::{ConnectCallback, OnConnectData};
use super::dispatcher::Dispatcher;
/// `ServiceFactory` implementation for HTTP2 transport
/// `ServiceFactory` implementation for HTTP/2 transport
pub struct H2Service<T, S, B> {
srv: S,
cfg: ServiceConfig,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B)>,
_phantom: PhantomData<(T, B)>,
}
impl<T, S, B> H2Service<T, S, B>
where
S: ServiceFactory<Config = (), Request = Request>,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
/// Create new `HttpService` instance with config.
pub(crate) fn with_config<F: IntoServiceFactory<S>>(
/// Create new `H2Service` instance with config.
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
cfg: ServiceConfig,
service: F,
) -> Self {
H2Service {
cfg,
on_connect: None,
on_connect_ext: None,
srv: service.into_factory(),
_t: PhantomData,
_phantom: PhantomData,
}
}
/// Set on connect callback.
pub(crate) fn on_connect(
mut self,
f: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
) -> Self {
self.on_connect = f;
self
}
/// Set on connect callback.
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
self.on_connect_ext = f;
@ -77,18 +64,18 @@ where
impl<S, B> H2Service<TcpStream, S, B>
where
S: ServiceFactory<Config = (), Request = Request>,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
/// Create simple tcp based service
/// Create plain TCP based service
pub fn tcp(
self,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = DispatchError,
InitError = S::InitError,
@ -105,29 +92,29 @@ where
#[cfg(feature = "openssl")]
mod openssl {
use actix_service::{fn_factory, fn_service};
use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream};
use actix_tls::{openssl::HandshakeError, TlsError};
use actix_service::{fn_factory, fn_service, ServiceFactoryExt};
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream};
use actix_tls::accept::TlsError;
use super::*;
impl<S, B> H2Service<SslStream<TcpStream>, S, B>
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where
S: ServiceFactory<Config = (), Request = Request>,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
/// Create ssl based service
/// Create OpenSSL based service
pub fn openssl(
self,
acceptor: SslAcceptor,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = TlsError<HandshakeError<TcpStream>, DispatchError>,
Error = TlsError<SslError, DispatchError>,
InitError = S::InitError,
> {
pipeline_factory(
@ -136,7 +123,7 @@ mod openssl {
.map_init_err(|_| panic!()),
)
.and_then(fn_factory(|| {
ok::<_, S::InitError>(fn_service(|io: SslStream<TcpStream>| {
ok::<_, S::InitError>(fn_service(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().peer_addr().ok();
ok((io, peer_addr))
}))
@ -149,25 +136,26 @@ mod openssl {
#[cfg(feature = "rustls")]
mod rustls {
use super::*;
use actix_tls::rustls::{Acceptor, ServerConfig, TlsStream};
use actix_tls::TlsError;
use actix_service::ServiceFactoryExt;
use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream};
use actix_tls::accept::TlsError;
use std::io;
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where
S: ServiceFactory<Config = (), Request = Request>,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
/// Create openssl based service
/// Create Rustls based service
pub fn rustls(
self,
mut config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = S::InitError,
@ -191,52 +179,52 @@ mod rustls {
}
}
impl<T, S, B> ServiceFactory for H2Service<T, S, B>
impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Config = (), Request = Request>,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
type Config = ();
type Request = (T, Option<net::SocketAddr>);
type Response = ();
type Error = DispatchError;
type InitError = S::InitError;
type Config = ();
type Service = H2ServiceHandler<T, S::Service, B>;
type InitError = S::InitError;
type Future = H2ServiceResponse<T, S, B>;
fn new_service(&self, _: ()) -> Self::Future {
H2ServiceResponse {
fut: self.srv.new_service(()),
cfg: Some(self.cfg.clone()),
on_connect: self.on_connect.clone(),
on_connect_ext: self.on_connect_ext.clone(),
_t: PhantomData,
_phantom: PhantomData,
}
}
}
#[doc(hidden)]
#[pin_project::pin_project]
pub struct H2ServiceResponse<T, S: ServiceFactory, B> {
pub struct H2ServiceResponse<T, S, B>
where
S: ServiceFactory<Request>,
{
#[pin]
fut: S::Future,
cfg: Option<ServiceConfig>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B)>,
_phantom: PhantomData<B>,
}
impl<T, S, B> Future for H2ServiceResponse<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Config = (), Request = Request>,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
type Output = Result<H2ServiceHandler<T, S::Service, B>, S::InitError>;
@ -244,30 +232,31 @@ where
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.as_mut().project();
Poll::Ready(ready!(this.fut.poll(cx)).map(|service| {
this.fut.poll(cx).map_ok(|service| {
let this = self.as_mut().project();
H2ServiceHandler::new(
this.cfg.take().unwrap(),
this.on_connect.clone(),
this.on_connect_ext.clone(),
service,
)
}))
})
}
}
/// `Service` implementation for http/2 transport
pub struct H2ServiceHandler<T, S: Service, B> {
srv: CloneableService<S>,
/// `Service` implementation for HTTP/2 transport
pub struct H2ServiceHandler<T, S, B>
where
S: Service<Request>,
{
flow: Rc<HttpFlow<S, (), ()>>,
cfg: ServiceConfig,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_t: PhantomData<(T, B)>,
_phantom: PhantomData<B>,
}
impl<T, S, B> H2ServiceHandler<T, S, B>
where
S: Service<Request = Request>,
S: Service<Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
@ -275,76 +264,66 @@ where
{
fn new(
cfg: ServiceConfig,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
srv: S,
service: S,
) -> H2ServiceHandler<T, S, B> {
H2ServiceHandler {
flow: HttpFlow::new(service, (), None),
cfg,
on_connect,
on_connect_ext,
srv: CloneableService::new(srv),
_t: PhantomData,
_phantom: PhantomData,
}
}
}
impl<T, S, B> Service for H2ServiceHandler<T, S, B>
impl<T, S, B> Service<(T, Option<net::SocketAddr>)> for H2ServiceHandler<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request = Request>,
S: Service<Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static,
{
type Request = (T, Option<net::SocketAddr>);
type Response = ();
type Error = DispatchError;
type Future = H2ServiceHandlerResponse<T, S, B>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.srv.poll_ready(cx).map_err(|e| {
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.flow.service.poll_ready(cx).map_err(|e| {
let e = e.into();
error!("Service readiness error: {:?}", e);
DispatchError::Service(e)
})
}
fn call(&mut self, (io, addr): Self::Request) -> Self::Future {
let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
let mut connect_extensions = Extensions::new();
if let Some(ref handler) = self.on_connect_ext {
// run on_connect_ext callback, populating connect extensions
handler(&io, &mut connect_extensions);
}
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
let on_connect_data =
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
H2ServiceHandlerResponse {
state: State::Handshake(
Some(self.srv.clone()),
Some(self.flow.clone()),
Some(self.cfg.clone()),
addr,
deprecated_on_connect,
Some(connect_extensions),
on_connect_data,
server::handshake(io),
),
}
}
}
enum State<T, S: Service<Request = Request>, B: MessageBody>
enum State<T, S: Service<Request>, B: MessageBody>
where
T: AsyncRead + AsyncWrite + Unpin,
S::Future: 'static,
{
Incoming(Dispatcher<T, S, B>),
Incoming(Dispatcher<T, S, B, (), ()>),
Handshake(
Option<CloneableService<S>>,
Option<Rc<HttpFlow<S, (), ()>>>,
Option<ServiceConfig>,
Option<net::SocketAddr>,
Option<Box<dyn DataFactory>>,
Option<Extensions>,
OnConnectData,
Handshake<T, Bytes>,
),
}
@ -352,7 +331,7 @@ where
pub struct H2ServiceHandlerResponse<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request = Request>,
S: Service<Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
@ -364,7 +343,7 @@ where
impl<T, S, B> Future for H2ServiceHandlerResponse<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request = Request>,
S: Service<Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
@ -379,27 +358,24 @@ where
ref mut srv,
ref mut config,
ref peer_addr,
ref mut on_connect,
ref mut on_connect_data,
ref mut handshake,
) => match Pin::new(handshake).poll(cx) {
Poll::Ready(Ok(conn)) => {
) => match ready!(Pin::new(handshake).poll(cx)) {
Ok(conn) => {
let on_connect_data = std::mem::take(on_connect_data);
self.state = State::Incoming(Dispatcher::new(
srv.take().unwrap(),
conn,
on_connect.take(),
on_connect_data.take().unwrap(),
on_connect_data,
config.take().unwrap(),
None,
*peer_addr,
));
self.poll(cx)
}
Poll::Ready(Err(err)) => {
Err(err) => {
trace!("H2 handshake error: {}", err);
Poll::Ready(Err(err.into()))
}
Poll::Pending => Poll::Pending,
},
}
}

View File

@ -0,0 +1,48 @@
//! Helper trait for types that can be effectively borrowed as a [HeaderValue].
//!
//! [HeaderValue]: crate::http::HeaderValue
use std::{borrow::Cow, str::FromStr};
use http::header::{HeaderName, InvalidHeaderName};
pub trait AsHeaderName: Sealed {}
pub trait Sealed {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName>;
}
impl Sealed for HeaderName {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
Ok(Cow::Borrowed(self))
}
}
impl AsHeaderName for HeaderName {}
impl Sealed for &HeaderName {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
Ok(Cow::Borrowed(*self))
}
}
impl AsHeaderName for &HeaderName {}
impl Sealed for &str {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned)
}
}
impl AsHeaderName for &str {}
impl Sealed for String {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned)
}
}
impl AsHeaderName for String {}
impl Sealed for &String {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned)
}
}
impl AsHeaderName for &String {}

View File

@ -32,50 +32,36 @@ header! {
/// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c`
///
/// # Examples
/// ```rust
/// # extern crate actix_http;
/// extern crate mime;
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::{Accept, qitem};
///
/// # fn main() {
/// let mut builder = Response::Ok();
///
/// builder.set(
/// builder.insert_header(
/// Accept(vec![
/// qitem(mime::TEXT_HTML),
/// ])
/// );
/// # }
/// ```
///
/// ```rust
/// # extern crate actix_http;
/// extern crate mime;
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::{Accept, qitem};
///
/// # fn main() {
/// let mut builder = Response::Ok();
///
/// builder.set(
/// builder.insert_header(
/// Accept(vec![
/// qitem(mime::APPLICATION_JSON),
/// ])
/// );
/// # }
/// ```
///
/// ```rust
/// # extern crate actix_http;
/// extern crate mime;
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::{Accept, QualityItem, q, qitem};
///
/// # fn main() {
/// let mut builder = Response::Ok();
///
/// builder.set(
/// builder.insert_header(
/// Accept(vec![
/// qitem(mime::TEXT_HTML),
/// qitem("application/xhtml+xml".parse().unwrap()),
@ -90,7 +76,6 @@ header! {
/// ),
/// ])
/// );
/// # }
/// ```
(Accept, header::ACCEPT) => (QualityItem<Mime>)+
@ -132,7 +117,7 @@ header! {
#[test]
fn test_fuzzing1() {
use crate::test::TestRequest;
let req = TestRequest::with_header(crate::header::ACCEPT, "chunk#;e").finish();
let req = TestRequest::default().insert_header((crate::header::ACCEPT, "chunk#;e")).finish();
let header = Accept::parse(&req);
assert!(header.is_ok());
}

View File

@ -21,44 +21,37 @@ header! {
/// * `iso-8859-5, unicode-1-1;q=0.8`
///
/// # Examples
/// ```rust
/// # extern crate actix_http;
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::{AcceptCharset, Charset, qitem};
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.set(
/// builder.insert_header(
/// AcceptCharset(vec![qitem(Charset::Us_Ascii)])
/// );
/// # }
/// ```
/// ```rust
/// # extern crate actix_http;
///
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::{AcceptCharset, Charset, q, QualityItem};
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.set(
/// builder.insert_header(
/// AcceptCharset(vec![
/// QualityItem::new(Charset::Us_Ascii, q(900)),
/// QualityItem::new(Charset::Iso_8859_10, q(200)),
/// ])
/// );
/// # }
/// ```
/// ```rust
/// # extern crate actix_http;
///
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::{AcceptCharset, Charset, qitem};
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.set(
/// builder.insert_header(
/// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))])
/// );
/// # }
/// ```
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)+

View File

@ -22,41 +22,35 @@ header! {
///
/// # Examples
///
/// ```rust
/// # extern crate actix_http;
/// # extern crate language_tags;
/// ```
/// use language_tags::langtag;
/// use actix_http::Response;
/// use actix_http::http::header::{AcceptLanguage, LanguageTag, qitem};
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// let mut langtag: LanguageTag = Default::default();
/// langtag.language = Some("en".to_owned());
/// langtag.region = Some("US".to_owned());
/// builder.set(
/// builder.insert_header(
/// AcceptLanguage(vec![
/// qitem(langtag),
/// ])
/// );
/// # }
/// ```
///
/// ```rust
/// # extern crate actix_http;
/// # #[macro_use] extern crate language_tags;
/// ```
/// use language_tags::langtag;
/// use actix_http::Response;
/// use actix_http::http::header::{AcceptLanguage, QualityItem, q, qitem};
/// #
/// # fn main() {
///
/// let mut builder = Response::Ok();
/// builder.set(
/// builder.insert_header(
/// AcceptLanguage(vec![
/// qitem(langtag!(da)),
/// QualityItem::new(langtag!(en;;;GB), q(800)),
/// QualityItem::new(langtag!(en), q(700)),
/// ])
/// );
/// # }
/// ```
(AcceptLanguage, ACCEPT_LANGUAGE) => (QualityItem<LanguageTag>)+

View File

@ -22,38 +22,28 @@ header! {
///
/// # Examples
///
/// ```rust
/// # extern crate http;
/// # extern crate actix_http;
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::Allow;
/// use http::Method;
/// use actix_http::http::{header::Allow, Method};
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.set(
/// builder.insert_header(
/// Allow(vec![Method::GET])
/// );
/// # }
/// ```
///
/// ```rust
/// # extern crate http;
/// # extern crate actix_http;
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::Allow;
/// use http::Method;
/// use actix_http::http::{header::Allow, Method};
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.set(
/// builder.insert_header(
/// Allow(vec![
/// Method::GET,
/// Method::POST,
/// Method::PATCH,
/// ])
/// );
/// # }
/// ```
(Allow, header::ALLOW) => (Method)*

View File

@ -28,12 +28,12 @@ use crate::header::{
/// * `max-age=30`
///
/// # Examples
/// ```rust
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::{CacheControl, CacheDirective};
///
/// let mut builder = Response::Ok();
/// builder.set(CacheControl(vec![CacheDirective::MaxAge(86400u32)]));
/// builder.insert_header(CacheControl(vec![CacheDirective::MaxAge(86400u32)]));
/// ```
///
/// ```rust
@ -41,7 +41,7 @@ use crate::header::{
/// use actix_http::http::header::{CacheControl, CacheDirective};
///
/// let mut builder = Response::Ok();
/// builder.set(CacheControl(vec![
/// builder.insert_header(CacheControl(vec![
/// CacheDirective::NoCache,
/// CacheDirective::Private,
/// CacheDirective::MaxAge(360u32),
@ -82,7 +82,7 @@ impl fmt::Display for CacheControl {
impl IntoHeaderValue for CacheControl {
type Error = header::InvalidHeaderValue;
fn try_into(self) -> Result<header::HeaderValue, Self::Error> {
fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
header::HeaderValue::from_maybe_shared(writer.take())
@ -196,7 +196,8 @@ mod tests {
#[test]
fn test_parse_multiple_headers() {
let req = TestRequest::with_header(header::CACHE_CONTROL, "no-cache, private")
let req = TestRequest::default()
.insert_header((header::CACHE_CONTROL, "no-cache, private"))
.finish();
let cache = Header::parse(&req);
assert_eq!(
@ -210,8 +211,8 @@ mod tests {
#[test]
fn test_parse_argument() {
let req =
TestRequest::with_header(header::CACHE_CONTROL, "max-age=100, private")
let req = TestRequest::default()
.insert_header((header::CACHE_CONTROL, "max-age=100, private"))
.finish();
let cache = Header::parse(&req);
assert_eq!(
@ -225,8 +226,9 @@ mod tests {
#[test]
fn test_parse_quote_form() {
let req =
TestRequest::with_header(header::CACHE_CONTROL, "max-age=\"200\"").finish();
let req = TestRequest::default()
.insert_header((header::CACHE_CONTROL, "max-age=\"200\""))
.finish();
let cache = Header::parse(&req);
assert_eq!(
cache.ok(),
@ -236,8 +238,9 @@ mod tests {
#[test]
fn test_parse_extension() {
let req =
TestRequest::with_header(header::CACHE_CONTROL, "foo, bar=baz").finish();
let req = TestRequest::default()
.insert_header((header::CACHE_CONTROL, "foo, bar=baz"))
.finish();
let cache = Header::parse(&req);
assert_eq!(
cache.ok(),
@ -250,7 +253,9 @@ mod tests {
#[test]
fn test_parse_bad_syntax() {
let req = TestRequest::with_header(header::CACHE_CONTROL, "foo=").finish();
let req = TestRequest::default()
.insert_header((header::CACHE_CONTROL, "foo="))
.finish();
let cache: Result<CacheControl, _> = Header::parse(&req);
assert_eq!(cache.ok(), None)
}

View File

@ -1,12 +1,12 @@
// # References
//
// "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt
// "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt
// "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt
// Browser conformance tests at: http://greenbytes.de/tech/tc2231/
// IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml
//! # References
//!
//! "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt
//! "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt
//! "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt
//! Browser conformance tests at: http://greenbytes.de/tech/tc2231/
//! IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use regex::Regex;
use std::fmt::{self, Write};
@ -318,9 +318,8 @@ impl ContentDisposition {
return Err(crate::error::ParseError::Header);
}
left = new_left;
if param_name.ends_with('*') {
if let Some(param_name) = param_name.strip_suffix('*') {
// extended parameters
let param_name = &param_name[..param_name.len() - 1]; // trim asterisk
let (ext_value, new_left) = split_once_and_trim(left, ';');
left = new_left;
let ext_value = header::parse_extended_value(ext_value)?;
@ -455,7 +454,7 @@ impl ContentDisposition {
impl IntoHeaderValue for ContentDisposition {
type Error = header::InvalidHeaderValue;
fn try_into(self) -> Result<header::HeaderValue, Self::Error> {
fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
header::HeaderValue::from_maybe_shared(writer.take())
@ -521,9 +520,7 @@ impl fmt::Display for DispositionParam {
//
//
// See also comments in test_from_raw_unnecessary_percent_decode.
lazy_static! {
static ref RE: Regex = Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap();
}
static RE: Lazy<Regex> = Lazy::new(|| Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap());
match self {
DispositionParam::Name(ref value) => write!(f, "name={}", value),
DispositionParam::Filename(ref value) => {
@ -550,8 +547,7 @@ impl fmt::Display for ContentDisposition {
write!(f, "{}", self.disposition)?;
self.parameters
.iter()
.map(|param| write!(f, "; {}", param))
.collect()
.try_for_each(|param| write!(f, "; {}", param))
}
}

View File

@ -0,0 +1,106 @@
use std::{convert::Infallible, str::FromStr};
use http::header::InvalidHeaderValue;
use crate::{
error::ParseError,
header::{self, from_one_raw_str, Header, HeaderName, HeaderValue, IntoHeaderValue},
HttpMessage,
};
/// Represents a supported content encoding.
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum ContentEncoding {
/// Automatically select encoding based on encoding negotiation.
Auto,
/// A format using the Brotli algorithm.
Br,
/// A format using the zlib structure with deflate algorithm.
Deflate,
/// Gzip algorithm.
Gzip,
/// Indicates the identity function (i.e. no compression, nor modification).
Identity,
}
impl ContentEncoding {
/// Is the content compressed?
#[inline]
pub fn is_compression(self) -> bool {
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
}
/// Convert content encoding to string
#[inline]
pub fn as_str(self) -> &'static str {
match self {
ContentEncoding::Br => "br",
ContentEncoding::Gzip => "gzip",
ContentEncoding::Deflate => "deflate",
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
}
}
/// Default Q-factor (quality) value.
#[inline]
pub fn quality(self) -> f64 {
match self {
ContentEncoding::Br => 1.1,
ContentEncoding::Gzip => 1.0,
ContentEncoding::Deflate => 0.9,
ContentEncoding::Identity | ContentEncoding::Auto => 0.1,
}
}
}
impl Default for ContentEncoding {
fn default() -> Self {
Self::Identity
}
}
impl FromStr for ContentEncoding {
type Err = Infallible;
fn from_str(val: &str) -> Result<Self, Self::Err> {
Ok(Self::from(val))
}
}
impl From<&str> for ContentEncoding {
fn from(val: &str) -> ContentEncoding {
let val = val.trim();
if val.eq_ignore_ascii_case("br") {
ContentEncoding::Br
} else if val.eq_ignore_ascii_case("gzip") {
ContentEncoding::Gzip
} else if val.eq_ignore_ascii_case("deflate") {
ContentEncoding::Deflate
} else {
ContentEncoding::default()
}
}
}
impl IntoHeaderValue for ContentEncoding {
type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<http::HeaderValue, Self::Error> {
Ok(HeaderValue::from_static(self.as_str()))
}
}
impl Header for ContentEncoding {
fn name() -> HeaderName {
header::CONTENT_ENCODING
}
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError> {
from_one_raw_str(msg.headers().get(Self::name()))
}
}

View File

@ -23,38 +23,31 @@ header! {
///
/// # Examples
///
/// ```rust
/// # extern crate actix_http;
/// # #[macro_use] extern crate language_tags;
/// ```
/// use language_tags::langtag;
/// use actix_http::Response;
/// # use actix_http::http::header::{ContentLanguage, qitem};
/// #
/// # fn main() {
/// use actix_http::http::header::{ContentLanguage, qitem};
///
/// let mut builder = Response::Ok();
/// builder.set(
/// builder.insert_header(
/// ContentLanguage(vec![
/// qitem(langtag!(en)),
/// ])
/// );
/// # }
/// ```
///
/// ```rust
/// # extern crate actix_http;
/// # #[macro_use] extern crate language_tags;
/// ```
/// use language_tags::langtag;
/// use actix_http::Response;
/// # use actix_http::http::header::{ContentLanguage, qitem};
/// #
/// # fn main() {
/// use actix_http::http::header::{ContentLanguage, qitem};
///
/// let mut builder = Response::Ok();
/// builder.set(
/// builder.insert_header(
/// ContentLanguage(vec![
/// qitem(langtag!(da)),
/// qitem(langtag!(en;;;GB)),
/// ])
/// );
/// # }
/// ```
(ContentLanguage, CONTENT_LANGUAGE) => (QualityItem<LanguageTag>)+

View File

@ -200,7 +200,7 @@ impl Display for ContentRangeSpec {
impl IntoHeaderValue for ContentRangeSpec {
type Error = InvalidHeaderValue;
fn try_into(self) -> Result<HeaderValue, Self::Error> {
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
HeaderValue::from_maybe_shared(writer.take())

View File

@ -30,31 +30,24 @@ header! {
///
/// # Examples
///
/// ```rust
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::ContentType;
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.set(
/// builder.insert_header(
/// ContentType::json()
/// );
/// # }
/// ```
///
/// ```rust
/// # extern crate mime;
/// # extern crate actix_http;
/// use mime::TEXT_HTML;
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::ContentType;
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.set(
/// ContentType(TEXT_HTML)
/// builder.insert_header(
/// ContentType(mime::TEXT_HTML)
/// );
/// # }
/// ```
(ContentType, CONTENT_TYPE) => [Mime]
@ -99,6 +92,7 @@ impl ContentType {
pub fn form_url_encoded() -> ContentType {
ContentType(mime::APPLICATION_WWW_FORM_URLENCODED)
}
/// A constructor to easily create a `Content-Type: image/jpeg` header.
#[inline]
pub fn jpeg() -> ContentType {

View File

@ -19,13 +19,15 @@ header! {
///
/// # Example
///
/// ```rust
/// ```
/// use std::time::SystemTime;
/// use actix_http::Response;
/// use actix_http::http::header::Date;
/// use std::time::SystemTime;
///
/// let mut builder = Response::Ok();
/// builder.set(Date(SystemTime::now().into()));
/// builder.insert_header(
/// Date(SystemTime::now().into())
/// );
/// ```
(Date, DATE) => [HttpDate]

View File

@ -27,20 +27,24 @@ header! {
///
/// # Examples
///
/// ```rust
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::{ETag, EntityTag};
///
/// let mut builder = Response::Ok();
/// builder.set(ETag(EntityTag::new(false, "xyzzy".to_owned())));
/// builder.insert_header(
/// ETag(EntityTag::new(false, "xyzzy".to_owned()))
/// );
/// ```
///
/// ```rust
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::{ETag, EntityTag};
///
/// let mut builder = Response::Ok();
/// builder.set(ETag(EntityTag::new(true, "xyzzy".to_owned())));
/// builder.insert_header(
/// ETag(EntityTag::new(true, "xyzzy".to_owned()))
/// );
/// ```
(ETag, ETAG) => [EntityTag]

View File

@ -21,14 +21,16 @@ header! {
///
/// # Example
///
/// ```rust
/// ```
/// use std::time::{SystemTime, Duration};
/// use actix_http::Response;
/// use actix_http::http::header::Expires;
/// use std::time::{SystemTime, Duration};
///
/// let mut builder = Response::Ok();
/// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
/// builder.set(Expires(expiration.into()));
/// builder.insert_header(
/// Expires(expiration.into())
/// );
/// ```
(Expires, EXPIRES) => [HttpDate]

View File

@ -29,20 +29,20 @@ header! {
///
/// # Examples
///
/// ```rust
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::IfMatch;
///
/// let mut builder = Response::Ok();
/// builder.set(IfMatch::Any);
/// builder.insert_header(IfMatch::Any);
/// ```
///
/// ```rust
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::{IfMatch, EntityTag};
///
/// let mut builder = Response::Ok();
/// builder.set(
/// builder.insert_header(
/// IfMatch::Items(vec![
/// EntityTag::new(false, "xyzzy".to_owned()),
/// EntityTag::new(false, "foobar".to_owned()),

View File

@ -21,14 +21,16 @@ header! {
///
/// # Example
///
/// ```rust
/// ```
/// use std::time::{SystemTime, Duration};
/// use actix_http::Response;
/// use actix_http::http::header::IfModifiedSince;
/// use std::time::{SystemTime, Duration};
///
/// let mut builder = Response::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.set(IfModifiedSince(modified.into()));
/// builder.insert_header(
/// IfModifiedSince(modified.into())
/// );
/// ```
(IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate]

View File

@ -31,20 +31,20 @@ header! {
///
/// # Examples
///
/// ```rust
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::IfNoneMatch;
///
/// let mut builder = Response::Ok();
/// builder.set(IfNoneMatch::Any);
/// builder.insert_header(IfNoneMatch::Any);
/// ```
///
/// ```rust
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::{IfNoneMatch, EntityTag};
///
/// let mut builder = Response::Ok();
/// builder.set(
/// builder.insert_header(
/// IfNoneMatch::Items(vec![
/// EntityTag::new(false, "xyzzy".to_owned()),
/// EntityTag::new(false, "foobar".to_owned()),
@ -73,12 +73,14 @@ mod tests {
fn test_if_none_match() {
let mut if_none_match: Result<IfNoneMatch, _>;
let req = TestRequest::with_header(IF_NONE_MATCH, "*").finish();
let req = TestRequest::default()
.insert_header((IF_NONE_MATCH, "*"))
.finish();
if_none_match = Header::parse(&req);
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any));
let req =
TestRequest::with_header(IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..])
let req = TestRequest::default()
.insert_header((IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..]))
.finish();
if_none_match = Header::parse(&req);

View File

@ -5,7 +5,7 @@ use crate::header::{
self, from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate,
IntoHeaderValue, InvalidHeaderValue, Writer,
};
use crate::httpmessage::HttpMessage;
use crate::HttpMessage;
/// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2)
///
@ -35,31 +35,34 @@ use crate::httpmessage::HttpMessage;
///
/// # Examples
///
/// ```rust
/// ```
/// use actix_http::Response;
/// use actix_http::http::header::{EntityTag, IfRange};
///
/// let mut builder = Response::Ok();
/// builder.set(IfRange::EntityTag(EntityTag::new(
/// false,
/// "xyzzy".to_owned(),
/// )));
/// builder.insert_header(
/// IfRange::EntityTag(
/// EntityTag::new(false, "abc".to_owned())
/// )
/// );
/// ```
///
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::IfRange;
/// ```
/// use std::time::{Duration, SystemTime};
/// use actix_http::{http::header::IfRange, Response};
///
/// let mut builder = Response::Ok();
/// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.set(IfRange::Date(fetched.into()));
/// builder.insert_header(
/// IfRange::Date(fetched.into())
/// );
/// ```
#[derive(Clone, Debug, PartialEq)]
pub enum IfRange {
/// The entity-tag the client has of the resource
/// The entity-tag the client has of the resource.
EntityTag(EntityTag),
/// The date when the client retrieved the resource
/// The date when the client retrieved the resource.
Date(HttpDate),
}
@ -98,7 +101,7 @@ impl Display for IfRange {
impl IntoHeaderValue for IfRange {
type Error = InvalidHeaderValue;
fn try_into(self) -> Result<HeaderValue, Self::Error> {
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
HeaderValue::from_maybe_shared(writer.take())
@ -110,7 +113,8 @@ mod test_if_range {
use super::IfRange as HeaderField;
use crate::header::*;
use std::str;
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
test_header!(test2, vec![b"\"xyzzy\""]);
test_header!(test2, vec![b"\"abc\""]);
test_header!(test3, vec![b"this-is-invalid"], None::<IfRange>);
}

View File

@ -22,14 +22,16 @@ header! {
///
/// # Example
///
/// ```rust
/// ```
/// use std::time::{SystemTime, Duration};
/// use actix_http::Response;
/// use actix_http::http::header::IfUnmodifiedSince;
/// use std::time::{SystemTime, Duration};
///
/// let mut builder = Response::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.set(IfUnmodifiedSince(modified.into()));
/// builder.insert_header(
/// IfUnmodifiedSince(modified.into())
/// );
/// ```
(IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate]

View File

@ -21,14 +21,16 @@ header! {
///
/// # Example
///
/// ```rust
/// ```
/// use std::time::{SystemTime, Duration};
/// use actix_http::Response;
/// use actix_http::http::header::LastModified;
/// use std::time::{SystemTime, Duration};
///
/// let mut builder = Response::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.set(LastModified(modified.into()));
/// builder.insert_header(
/// LastModified(modified.into())
/// );
/// ```
(LastModified, LAST_MODIFIED) => [HttpDate]

View File

@ -3,7 +3,7 @@
//! ## Mime
//!
//! 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)`.
#![cfg_attr(rustfmt, rustfmt_skip)]
@ -18,6 +18,7 @@ pub use self::content_disposition::{
};
pub use self::content_language::ContentLanguage;
pub use self::content_range::{ContentRange, ContentRangeSpec};
pub use self::content_encoding::{ContentEncoding};
pub use self::content_type::ContentType;
pub use self::date::Date;
pub use self::etag::ETag;
@ -83,7 +84,7 @@ macro_rules! test_header {
let a: Vec<Vec<u8>> = raw.iter().map(|x| x.to_vec()).collect();
let mut req = test::TestRequest::default();
for item in a {
req = req.header(HeaderField::name(), item).take();
req = req.insert_header((HeaderField::name(), item)).take();
}
let req = req.finish();
let value = HeaderField::parse(&req);
@ -110,7 +111,7 @@ macro_rules! test_header {
let a: Vec<Vec<u8>> = $raw.iter().map(|x| x.to_vec()).collect();
let mut req = test::TestRequest::default();
for item in a {
req.header(HeaderField::name(), item);
req.insert_header((HeaderField::name(), item));
}
let req = req.finish();
let val = HeaderField::parse(&req);
@ -168,7 +169,7 @@ macro_rules! header {
impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue;
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
use std::fmt::Write;
let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self);
@ -204,7 +205,7 @@ macro_rules! header {
impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue;
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
use std::fmt::Write;
let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self);
@ -240,8 +241,8 @@ macro_rules! header {
impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue;
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
self.0.try_into()
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
self.0.try_into_value()
}
}
};
@ -289,7 +290,7 @@ macro_rules! header {
impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue;
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
use std::fmt::Write;
let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self);
@ -333,13 +334,14 @@ macro_rules! header {
}
mod accept_charset;
//mod accept_encoding;
// mod accept_encoding;
mod accept;
mod accept_language;
mod allow;
mod cache_control;
mod content_disposition;
mod content_language;
mod content_encoding;
mod content_range;
mod content_type;
mod date;

View File

@ -0,0 +1,117 @@
use std::convert::TryFrom;
use http::{
header::{HeaderName, InvalidHeaderName, InvalidHeaderValue},
Error as HttpError, HeaderValue,
};
use super::{Header, IntoHeaderValue};
/// Transforms structures into header K/V pairs for inserting into `HeaderMap`s.
pub trait IntoHeaderPair: Sized {
type Error: Into<HttpError>;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error>;
}
#[derive(Debug)]
pub enum InvalidHeaderPart {
Name(InvalidHeaderName),
Value(InvalidHeaderValue),
}
impl From<InvalidHeaderPart> for HttpError {
fn from(part_err: InvalidHeaderPart) -> Self {
match part_err {
InvalidHeaderPart::Name(err) => err.into(),
InvalidHeaderPart::Value(err) => err.into(),
}
}
}
impl<V> IntoHeaderPair for (HeaderName, V)
where
V: IntoHeaderValue,
V::Error: Into<InvalidHeaderValue>,
{
type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self;
let value = value
.try_into_value()
.map_err(|err| InvalidHeaderPart::Value(err.into()))?;
Ok((name, value))
}
}
impl<V> IntoHeaderPair for (&HeaderName, V)
where
V: IntoHeaderValue,
V::Error: Into<InvalidHeaderValue>,
{
type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self;
let value = value
.try_into_value()
.map_err(|err| InvalidHeaderPart::Value(err.into()))?;
Ok((name.clone(), value))
}
}
impl<V> IntoHeaderPair for (&[u8], V)
where
V: IntoHeaderValue,
V::Error: Into<InvalidHeaderValue>,
{
type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self;
let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
let value = value
.try_into_value()
.map_err(|err| InvalidHeaderPart::Value(err.into()))?;
Ok((name, value))
}
}
impl<V> IntoHeaderPair for (&str, V)
where
V: IntoHeaderValue,
V::Error: Into<InvalidHeaderValue>,
{
type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self;
let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
let value = value
.try_into_value()
.map_err(|err| InvalidHeaderPart::Value(err.into()))?;
Ok((name, value))
}
}
impl<V> IntoHeaderPair for (String, V)
where
V: IntoHeaderValue,
V::Error: Into<InvalidHeaderValue>,
{
type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self;
(name.as_str(), value).try_into_header_pair()
}
}
impl<T: Header> IntoHeaderPair for T {
type Error = <T as IntoHeaderValue>::Error;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
Ok((T::name(), self.try_into_value()?))
}
}

View File

@ -0,0 +1,131 @@
use std::convert::TryFrom;
use bytes::Bytes;
use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue};
use mime::Mime;
/// A trait for any object that can be Converted to a `HeaderValue`
pub trait IntoHeaderValue: Sized {
/// The type returned in the event of a conversion error.
type Error: Into<HttpError>;
/// Try to convert value to a HeaderValue.
fn try_into_value(self) -> Result<HeaderValue, Self::Error>;
}
impl IntoHeaderValue for HeaderValue {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
Ok(self)
}
}
impl IntoHeaderValue for &HeaderValue {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
Ok(self.clone())
}
}
impl IntoHeaderValue for &str {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
self.parse()
}
}
impl IntoHeaderValue for &[u8] {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_bytes(self)
}
}
impl IntoHeaderValue for Bytes {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_maybe_shared(self)
}
}
impl IntoHeaderValue for Vec<u8> {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self)
}
}
impl IntoHeaderValue for String {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self)
}
}
impl IntoHeaderValue for usize {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self.to_string())
}
}
impl IntoHeaderValue for i64 {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self.to_string())
}
}
impl IntoHeaderValue for u64 {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self.to_string())
}
}
impl IntoHeaderValue for i32 {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self.to_string())
}
}
impl IntoHeaderValue for u32 {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self.to_string())
}
}
impl IntoHeaderValue for Mime {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_str(self.as_ref())
}
}

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