1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-05 18:35:22 +02:00

Compare commits

...

243 Commits

Author SHA1 Message Date
7faeffc5ab prepare actix-test release 0.1.0-beta.3 2021-06-20 19:47:42 +01:00
f81d4bdae7 remove unused private hidden methods 2021-06-19 23:40:30 +01:00
6893773280 files: allow show_files_listing() with index_file() (#2228) 2021-06-19 21:00:31 +01:00
73a655544e tweak compress feature docs 2021-06-19 20:23:06 +01:00
baa5a663c4 Select compression algorithm using features flags (#2250)
Add compress-* feature flags in actix-http / actix-web / awc.
This allow enable / disable not wanted compression algorithm.
2021-06-19 20:21:13 +01:00
c260fb1c48 beta.7 releases (#2266) 2021-06-19 11:51:20 +01:00
532f7b9923 refined error model (#2253) 2021-06-17 17:57:58 +01:00
bb0331ae28 fix cargo cache on msrv 2021-06-17 16:49:31 +01:00
8d124713fc files: inline disposition for common web app file types (#2257) 2021-06-16 20:33:22 +01:00
fb2b362b60 Adjust JSON limit to 2MB and report on sizes (#2162)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-06-16 15:52:49 +01:00
75f65fea4f Extends Rustls ALPN protocols instead of replacing them when creating Rustls based services (#2226) 2021-06-10 16:25:21 +01:00
812269d656 clarify docs for BodyEncoding::encoding() (#2258) 2021-06-10 15:38:35 +01:00
e46cda5228 Deduplicate rt::main macro logic (#2255) 2021-06-08 22:44:56 +01:00
2e1d761854 add Seal argument to sealed AsHeaderName methods (#2252) 2021-06-08 12:57:19 +01:00
b1e841f168 Don't normalize URIs with no valid path (#2246) 2021-06-05 17:19:45 +01:00
0bb035cfa7 Add information about Actix discord server (#2247) 2021-06-04 02:54:40 +01:00
3479293416 Add zstd ContentEncoding support (#2244)
Co-authored-by: Igor Aleksanov <popzxc@yandex.ru>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-06-03 21:32:52 +01:00
136dac1352 Additional test coverage and tidyup (middleware::normalize) (#2243) 2021-06-03 03:28:09 +01:00
e5b713b04a files: Fix redirect_to_slash_directory() when used with show_files_listing() (#2225) 2021-05-26 10:42:29 +01:00
3847429d00 Response::from_error take impl Into<Error> (#2214) 2021-05-26 13:41:48 +09:00
bb7d33c9d4 refactor h2 dispatcher to async/await.reduce duplicate code (#2211) 2021-05-25 03:21:20 +01:00
4598a7c0cc Only run UI tests on MSRV (#2232) 2021-05-25 00:09:38 +09:00
b1de196509 Fix clippy warnings (#2217) 2021-05-15 01:13:33 +01:00
2a8c650f2c move internalerror to actix web (#2215) 2021-05-14 16:40:00 +01:00
f277b128b6 cleanup ws test (#2213) 2021-05-13 12:24:32 +01:00
4903950b22 update changelog 2021-05-09 20:15:49 +01:00
f55e8d7a11 remove error field from response 2021-05-09 20:15:48 +01:00
900c9e270e remove responsebody indirection from response (#2201) 2021-05-09 20:12:48 +01:00
a9dc1586a0 remove rogue eprintln 2021-05-07 10:14:25 +01:00
947caa3599 examples use info log level by default 2021-05-06 20:24:18 +01:00
7d1d5c8acd Expose SererBuilder::worker_max_blocking_threads (#2200) 2021-05-06 18:35:04 +01:00
ddaf8c3e43 add associated error type to MessageBody (#2183) 2021-05-05 18:36:02 +01:00
dd1a3e7675 Fix loophole in soundness of __private_get_type_id__ (#2199) 2021-05-05 11:16:12 +01:00
c17662fe39 Reduce the level of the emitted log line from error to debug. (#2196)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-05-03 00:58:14 +01:00
3a0fb3f89e Static either extract future (#2184) 2021-05-01 03:02:56 +01:00
1fcf92e11f Update dependency "language-tags" (#2188) 2021-04-28 01:23:12 +01:00
6a29a50f25 files doc wording 2021-04-22 18:37:45 +01:00
75867bd073 clean up files service docs and rename method
follow on from #2046
2021-04-22 18:31:21 +01:00
f44a0bc159 add support of filtering guards in Files of actix-files (#2046)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-04-22 18:13:13 +01:00
07036b5640 static form extract future (#2181) 2021-04-22 13:54:29 +01:00
a7cd4e85cf use stable codec 0.4.0 2021-04-21 11:14:22 +01:00
6a9c4f1026 update awc docs link, formatting (#2180) 2021-04-20 19:57:27 +01:00
427fe6bd82 improve responseerror trait docs 2021-04-19 23:16:04 +01:00
2aa674c1fd Fix perf drop in HttpResponseBuilder (#2174) 2021-04-19 23:15:57 +01:00
52bb2b5daf hide downcast macros 2021-04-19 03:42:53 +01:00
db97974dc1 make some http re-exports more accessible (#2171) 2021-04-19 03:29:38 +01:00
b9dbc58e20 content disposition methods take impl AsRef<str> 2021-04-19 02:31:11 +01:00
35f8188410 restore cookie methods on ServiceRequest 2021-04-19 02:24:20 +01:00
8ffb1f2011 update files changelog 2021-04-19 02:11:07 +01:00
26e9c80626 Named file service (#2135) 2021-04-18 23:34:51 +01:00
f462aaa7b6 prepare actix-test release 0.1.0-beta.2 2021-04-17 15:53:54 +01:00
5a162932f3 prepare awc release 3.0.0-beta.5 2021-04-17 15:30:31 +01:00
b2d6b6a70c prepare web release 4.0.0-beta.6 2021-04-17 15:28:13 +01:00
f743e885a3 prepare http release 3.0.0-beta.6 2021-04-17 15:24:18 +01:00
5747f84736 bump utils to stable v3 2021-04-17 02:07:33 +01:00
879a4cbcd8 re-export ready boilerplate macros in dev 2021-04-16 23:21:02 +01:00
2449f2555c missed one pipeline_factory 2021-04-16 20:48:37 +01:00
d8f56eee3e bump service to stable v2 2021-04-16 20:28:21 +01:00
8d88a0a9af rename header generator macros 2021-04-16 19:15:10 +01:00
845c02cb86 Add responder impl for Cow<str> (#2164) 2021-04-16 00:54:51 +01:00
64bed506c2 chore: update benchmaks to round 20 (#2163) 2021-04-15 19:11:30 +01:00
ff65f1d006 non exhaustive http errors (#2161) 2021-04-14 06:07:59 +01:00
a9f26286f9 reduce branches in h1 dispatcher poll_keepalive (#2089) 2021-04-14 05:20:45 +01:00
037ac80a32 document messagebody trait items 2021-04-14 03:23:15 +01:00
1bfdfd1f41 implement parts as assoc method 2021-04-14 02:57:28 +01:00
5202bf03c1 add some doc examples to response builder 2021-04-14 02:45:58 +01:00
387c229f28 move response builder code to own file 2021-04-14 02:12:47 +01:00
23e0c9b6e0 remove http-codes builders from actix-http (#2159) 2021-04-14 02:00:14 +01:00
02ced426fd add body to_bytes helper (#2158) 2021-04-13 13:34:22 +01:00
4442535a45 clippy 2021-04-13 12:44:38 +01:00
edd9f14752 remove unpin from body types (#2152) 2021-04-13 11:16:12 +01:00
ce50cc9523 files: Don't use canonical path when serving file (#2156) 2021-04-13 05:28:30 +01:00
981c54432c remove json and url encoded form support from -http (#2148) 2021-04-12 10:30:28 +01:00
44c55dd036 remove cookie support from -http (#2065) 2021-04-09 18:07:10 +01:00
c72d77065d derive debug where possible (#2142) 2021-04-09 03:22:51 +01:00
44a2d2214c update year in MIT license (#2143) 2021-04-09 01:28:35 +01:00
3f5a73793a make module/crate re-exports doc inline (#2141) 2021-04-08 20:51:16 +01:00
e0b2246c68 prepare test release 0.1.0-beta.1 2021-04-02 10:03:01 +01:00
e0ae8e59bf prepare actors release 4.0.0-beta.4 2021-04-02 09:55:35 +01:00
a9641e475a prepare http-test release 3.0.0-beta.4 2021-04-02 09:54:35 +01:00
05c7505563 prepare multipart release 0.4.0-beta.4 2021-04-02 09:45:31 +01:00
8561263545 prepare files release 0.6.0-beta.4 2021-04-02 09:43:51 +01:00
a32151525c prepare awc release 3.0.0-beta.4 2021-04-02 09:40:36 +01:00
546e7c5da4 prepare web release 4.0.0-beta.5 2021-04-02 09:37:51 +01:00
6fb06a720a prepare http release 3.0.0-beta.5 2021-04-02 09:27:11 +01:00
c54a0713de migrate integration testing to new crate (#2112) 2021-04-02 08:26:59 +01:00
50dc13f280 move typed headers and implement FromRequest (#2094)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-04-01 16:42:18 +01:00
c8ed8dd1a4 migrate to -utils beta 4 (#2127) 2021-04-01 15:26:13 +01:00
a807d33600 added TestServer::client_headers (#2097)
Co-authored-by: fakeshadow <24548779@qq.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-04-01 06:40:10 +01:00
1f1be6fd3d add Client::headers (#2114) 2021-03-31 11:43:56 +01:00
c49fe79207 Simplify lifetime annotation in HttpServiceBuilder. Simplify PlStream (#2129) 2021-03-30 15:46:09 +01:00
f66774e30b remove From<OffsetDateTime> impl from HttpDate
fully removes time crate from public api of -http
2021-03-30 03:32:22 +01:00
1281a748d0 merge H1ServiceHandler requests into HttpServiceHandler (#2126) 2021-03-30 03:06:16 +01:00
222acfd070 Fix build for next actix-tls-beta release (#2122) 2021-03-29 13:45:48 +01:00
980ecc5f07 fix openssl windows ci 2021-03-29 13:01:37 +01:00
e8ce73b496 update dep docs 2021-03-29 11:52:59 +01:00
f954a30c34 Fix typo in CHANGES.md (#2124) 2021-03-29 10:18:05 +01:00
60f9cfbb2a Refactor actix_http::h2::service module. Reduce loc. (#2118) 2021-03-26 18:24:51 +00:00
6822bf2f58 Refactor actix_http::h1::service (#2117) 2021-03-26 16:15:04 +00:00
2f7f1fa97a fix broken pipe for h2 when client is instantly dropped (#2113) 2021-03-26 00:05:31 +00:00
8c2ce2dedb fix awc compress feature (#2116) 2021-03-25 22:47:37 +00:00
3188ef5731 don't use rust annotation on code doc blocks 2021-03-25 08:45:52 +00:00
9704beddf8 Relax MessageBody limit to 2048kb (#2110)
* relax MessageBody limit to 2048kb

* fix clippy

* Update awc/src/response.rs

Co-authored-by: Rob Ede <robjtede@icloud.com>

* fix default body limit

Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-03-24 04:44:03 -07:00
1be54efbeb Simplify service factory macro (#2108) 2021-03-23 13:42:46 +00:00
746d983849 handle header error with CustomResponder (#2093) 2021-03-20 05:18:06 +00:00
8d9de76826 Simplify handler factory macro (#2086) 2021-03-19 16:30:53 +00:00
9488757c29 Update to socket2 v0.4 (#2092) 2021-03-19 12:17:06 +00:00
351286486c fix clippy warning on nightly (#2088)
* fix clippy warning on nightly
2021-03-19 19:25:35 +08:00
78fcd0237a Format extract macro (#2087) 2021-03-19 04:08:23 +00:00
81942d31d6 fix new dyn trait lint 2021-03-19 02:03:09 +00:00
b75b5114c3 refactor actix_http connection types and connector services (#2081) 2021-03-18 17:53:22 +00:00
abcb444dd9 fix routes in Path documentation (#2084) 2021-03-18 13:21:44 +00:00
983b6904a7 unvendor openssl 2021-03-17 00:38:54 +00:00
3dc2d145ef import some traits as _ 2021-03-17 00:38:54 +00:00
c8f6d37290 rename client io trait. reduce duplicate code (#2079) 2021-03-16 16:31:14 +00:00
69dd1a9bd6 Remove ConnectionLifetime trait. Simplify Acquired handling (#2072) 2021-03-16 02:56:23 +00:00
d93314a683 fix awc readme example (#2076) 2021-03-15 10:59:42 +00:00
a55e87faaa refactor actix_http::helpers to generic over bufmut trait (#2069) 2021-03-15 02:33:51 +00:00
515d0e3fb4 change behavior of default upgrade handler (#2071) 2021-03-13 22:20:18 +00:00
22dcc31193 Fix logger middleware properly escape %% (#2067) 2021-03-11 14:12:42 +00:00
909ef0344b document client mod removal
closes #2064
2021-03-11 00:43:03 +00:00
a2b0e86632 simplify connector generic type (#2063) 2021-03-10 23:57:32 +00:00
d0c1f1a84c remove actix_http::client::pool::Protocol (#2061) 2021-03-10 01:31:50 +00:00
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
262 changed files with 19728 additions and 14598 deletions

8
.cargo/config.toml Normal file
View File

@ -0,0 +1,8 @@
[alias]
chk = "check --workspace --all-features --tests --examples --bins"
lint = "clippy --workspace --tests --examples"
ci-min = "hack check --workspace --no-default-features"
ci-min-test = "hack check --workspace --no-default-features --tests --examples"
ci-default = "hack check --workspace"
ci-full = "check --workspace --bins --examples --tests"
ci-test = "test --workspace --all-features --no-fail-fast"

View File

@ -1,21 +1,21 @@
<!-- Thanks for considering contributing actix! --> <!-- 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 ## PR Type
<!-- What kind of change does this PR make? --> <!-- What kind of change does this PR make? -->
<!-- Bug Fix / Feature / Refactor / Code Style / Other --> <!-- Bug Fix / Feature / Refactor / Code Style / Other -->
INSERT_PR_TYPE PR_TYPE
## PR Checklist ## 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. --> <!-- For draft PRs check the boxes as you complete them. -->
- [ ] Tests for the changes have been added / updated. - [ ] Tests for the changes have been added / updated.
- [ ] Documentation comments have been added / updated. - [ ] Documentation comments have been added / updated.
- [ ] A changelog entry has been made for the appropriate packages. - [ ] 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 ## Overview

View File

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

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

@ -0,0 +1,127 @@
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 }}
env:
VCPKGRS_DYNAMIC: 1
steps:
- uses: actions/checkout@v2
# install OpenSSL on Windows
- name: Set vcpkg root
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Install OpenSSL
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: vcpkg install openssl:x64-windows
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal
override: true
- 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: check --workspace --no-default-features
- name: check minimal + tests
uses: actions-rs/cargo@v1
with:
command: hack
args: check --workspace --no-default-features --tests --examples
- 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: --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 --version 0.6.2 --no-default-features --features ci-autoclean
cargo-cache

View File

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

View File

@ -1,81 +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.46.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: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.0.1
- 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'
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'
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,56 +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: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.0.1
- 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
- name: Clear the cargo caches
run: |
cargo install cargo-cache --no-default-features --features ci-autoclean
cargo-cache

View File

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

View File

@ -1,76 +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: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.0.1
- 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
- name: Clear the cargo caches
run: |
cargo install cargo-cache --no-default-features --features ci-autoclean
cargo-cache

View File

@ -1,6 +1,127 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
### Changed
* Change compression algorithm features flags. [#2250]
[#2250]: https://github.com/actix/actix-web/pull/2250
## 4.0.0-beta.7 - 2021-06-17
### Added
* `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200]
### Changed
* Adjusted default JSON payload limit to 2MB (from 32kb) and included size and limits in the `JsonPayloadError::Overflow` error variant. [#2162]
[#2162]: (https://github.com/actix/actix-web/pull/2162)
* `ServiceResponse::error_response` now uses body type of `Body`. [#2201]
* `ServiceResponse::checked_expr` now returns a `Result`. [#2201]
* Update `language-tags` to `0.3`.
* `ServiceResponse::take_body`. [#2201]
* `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody<B>` types. [#2201]
* All error trait bounds in server service builders have changed from `Into<Error>` to `Into<Response<AnyBody>>`. [#2253]
* All error trait bounds in message body and stream impls changed from `Into<Error>` to `Into<Box<dyn std::error::Error>>`. [#2253]
* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
* `middleware::normalize` now will not try to normalize URIs with no valid path [#2246]
### Removed
* `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201]
[#2200]: https://github.com/actix/actix-web/pull/2200
[#2201]: https://github.com/actix/actix-web/pull/2201
[#2253]: https://github.com/actix/actix-web/pull/2253
[#2246]: https://github.com/actix/actix-web/pull/2246
## 4.0.0-beta.6 - 2021-04-17
### Added
* `HttpResponse` and `HttpResponseBuilder` structs. [#2065]
### Changed
* Most error types are now marked `#[non_exhaustive]`. [#2148]
* Methods on `ContentDisposition` that took `T: AsRef<str>` now take `impl AsRef<str>`.
[#2065]: https://github.com/actix/actix-web/pull/2065
[#2148]: https://github.com/actix/actix-web/pull/2148
## 4.0.0-beta.5 - 2021-04-02
### Added
* `Header` extractor for extracting common HTTP headers in handlers. [#2094]
* Added `TestServer::client_headers` method. [#2097]
### Fixed
* Double ampersand in Logger format is escaped correctly. [#2067]
### Changed
* `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed
instead of skipping. (Only the first error is kept when multiple error occur) [#2093]
### Removed
* The `client` mod was removed. Clients should now use `awc` directly.
[871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7)
* Integration testing was moved to new `actix-test` crate. Namely these items from the `test`
module: `TestServer`, `TestServerConfig`, `start`, `start_with`, and `unused_addr`. [#2112]
[#2067]: https://github.com/actix/actix-web/pull/2067
[#2093]: https://github.com/actix/actix-web/pull/2093
[#2094]: https://github.com/actix/actix-web/pull/2094
[#2097]: https://github.com/actix/actix-web/pull/2097
[#2112]: https://github.com/actix/actix-web/pull/2112
## 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 ## 4.0.0-beta.1 - 2021-01-07
@ -24,13 +145,15 @@
### Removed ### Removed
* Public modules `middleware::{normalize, err_handlers}`. All necessary middleware structs are now * Public modules `middleware::{normalize, err_handlers}`. All necessary middleware structs are now
exposed directly by the `middleware` module. 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 [#1812]: https://github.com/actix/actix-web/pull/1812
[#1813]: https://github.com/actix/actix-web/pull/1813 [#1813]: https://github.com/actix/actix-web/pull/1813
[#1852]: https://github.com/actix/actix-web/pull/1852 [#1852]: https://github.com/actix/actix-web/pull/1852
[#1865]: https://github.com/actix/actix-web/pull/1865 [#1865]: https://github.com/actix/actix-web/pull/1865
[#1875]: https://github.com/actix/actix-web/pull/1875 [#1875]: https://github.com/actix/actix-web/pull/1875
[#1878]: https://github.com/actix/actix-web/pull/1878
## 3.3.2 - 2020-12-01 ## 3.3.2 - 2020-12-01
### Fixed ### Fixed
@ -115,7 +238,7 @@
## 3.0.0-beta.4 - 2020-09-09 ## 3.0.0-beta.4 - 2020-09-09
### Added ### 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] slash, or as the new addition, always trimming trailing slashes. [#1639]
### Changed ### Changed
@ -443,7 +566,7 @@
## [1.0.0-rc] - 2019-05-18 ## [1.0.0-rc] - 2019-05-18
### Add ### Added
* Add `Query<T>::from_query()` to extract parameters from a query string. #846 * Add `Query<T>::from_query()` to extract parameters from a query string. #846
* `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors. * `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors.
@ -459,7 +582,7 @@
## [1.0.0-beta.4] - 2019-05-12 ## [1.0.0-beta.4] - 2019-05-12
### Add ### Added
* Allow to set/override app data on scope level * Allow to set/override app data on scope level
@ -485,7 +608,7 @@
* CORS handling without headers #702 * 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 ### Fixed
@ -549,7 +672,7 @@
### Changed ### 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. * Remove generic type for request payload, always use default.
@ -612,13 +735,13 @@
### Added ### Added
* rustls support * Rustls support
### Changed ### 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 ## [1.0.0-alpha.1] - 2019-03-28

View File

@ -1,25 +1,23 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.0.0-beta.1" version = "4.0.0-beta.7"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
readme = "README.md"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
categories = [
"network-programming",
"asynchronous",
"web-programming::http-server",
"web-programming::websocket"
]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git" repository = "https://github.com/actix/actix-web"
documentation = "https://docs.rs/actix-web/"
categories = ["network-programming", "asynchronous",
"web-programming::http-server",
"web-programming::websocket"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2018" edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = ["openssl", "rustls", "compress", "secure-cookies"] # features that docs.rs will build with
features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"]
[badges]
travis-ci = { repository = "actix/actix-web", branch = "master" }
codecov = { repository = "actix/actix-web", branch = "master", service = "github" }
[lib] [lib]
name = "actix_web" name = "actix_web"
@ -27,95 +25,97 @@ path = "src/lib.rs"
[workspace] [workspace]
members = [ members = [
".", ".",
"awc", "awc",
"actix-http", "actix-http",
"actix-files", "actix-files",
"actix-multipart", "actix-multipart",
"actix-web-actors", "actix-web-actors",
"actix-web-codegen", "actix-web-codegen",
"actix-http-test", "actix-http-test",
"actix-test",
] ]
# enable when MSRV is 1.51+
# resolver = "2"
[features] [features]
default = ["compress"] default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
# content-encoding support # Brotli algorithm content-encoding support
compress = ["actix-http/compress", "awc/compress"] compress-brotli = ["actix-http/compress-brotli", "__compress"]
# Gzip and deflate algorithms content-encoding support
compress-gzip = ["actix-http/compress-gzip", "__compress"]
# Zstd algorithm content-encoding support
compress-zstd = ["actix-http/compress-zstd", "__compress"]
# sessions feature # support for cookies
secure-cookies = ["actix-http/secure-cookies"] cookies = ["cookie"]
# secure cookies feature
secure-cookies = ["cookie/secure"]
# openssl # openssl
openssl = ["actix-tls/accept", "actix-tls/openssl", "awc/openssl", "open-ssl"] openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
# rustls # rustls
rustls = ["actix-tls/accept", "actix-tls/rustls", "awc/rustls", "rust-tls"] rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
[[example]] # Internal (PRIVATE!) features used to aid testing and cheking feature status.
name = "basic" # Don't rely on these whatsoever. They may disappear at anytime.
required-features = ["compress"] __compress = []
[[example]]
name = "uds"
required-features = ["compress"]
[[test]]
name = "test_server"
required-features = ["compress"]
[[example]]
name = "on_connect"
required-features = []
[[example]]
name = "client"
required-features = ["rustls"]
[dependencies] [dependencies]
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0"
actix-macros = "0.1.0" actix-macros = "0.2.1"
actix-router = "0.2.4" actix-router = "0.2.7"
actix-rt = "2.0.0-beta.1" actix-rt = "2.2"
actix-server = "2.0.0-beta.2" actix-server = "2.0.0-beta.3"
actix-service = "2.0.0-beta.2" actix-service = "2.0.0"
actix-utils = "3.0.0-beta.1" actix-utils = "3.0.0"
actix-threadpool = "0.3.1" actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true }
actix-tls = { version = "3.0.0-beta.2", default-features = false, optional = true }
actix-web-codegen = "0.4.0" actix-web-codegen = "0.5.0-beta.2"
actix-http = "3.0.0-beta.1" actix-http = "3.0.0-beta.7"
awc = { version = "3.0.0-beta.1", default-features = false }
ahash = "0.6" ahash = "0.7"
bytes = "1" bytes = "1"
cfg-if = "1"
cookie = { version = "0.15", features = ["percent-encode"], optional = true }
derive_more = "0.99.5" derive_more = "0.99.5"
either = "1.5.3"
encoding_rs = "0.8" encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }
itoa = "0.4"
language-tags = "0.3"
once_cell = "1.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
socket2 = "0.3.16" paste = "1"
pin-project = "1.0.0" pin-project = "1.0.0"
regex = "1.4" regex = "1.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
time = { version = "0.2.7", default-features = false, features = ["std"] }
url = "2.1"
open-ssl = { package = "openssl", version = "0.10", optional = true }
rust-tls = { package = "rustls", version = "0.19.0", optional = true }
smallvec = "1.6" smallvec = "1.6"
socket2 = "0.4.0"
time = { version = "0.2.23", default-features = false, features = ["std"] }
url = "2.1"
[dev-dependencies] [dev-dependencies]
actix = "0.11.0-beta.1" actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] }
actix-http = { version = "3.0.0-beta.1", features = ["actors"] } awc = { version = "3.0.0-beta.6", features = ["openssl"] }
rand = "0.8"
env_logger = "0.8"
serde_derive = "1.0"
brotli2 = "0.3.2" brotli2 = "0.3.2"
flate2 = "1.0.13"
criterion = "0.3" criterion = "0.3"
env_logger = "0.8"
flate2 = "1.0.13"
zstd = "0.7"
rand = "0.8"
rcgen = "0.8"
serde_derive = "1.0"
tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.19.0" }
[profile.release] [profile.release]
lto = true lto = true
@ -123,15 +123,32 @@ opt-level = 3
codegen-units = 1 codegen-units = 1
[patch.crates-io] [patch.crates-io]
actix-web = { path = "." } actix-files = { path = "actix-files" }
actix-http = { path = "actix-http" } actix-http = { path = "actix-http" }
actix-http-test = { path = "actix-http-test" } actix-http-test = { path = "actix-http-test" }
actix-multipart = { path = "actix-multipart" }
actix-test = { path = "actix-test" }
actix-web = { path = "." }
actix-web-actors = { path = "actix-web-actors" } actix-web-actors = { path = "actix-web-actors" }
actix-web-codegen = { path = "actix-web-codegen" } actix-web-codegen = { path = "actix-web-codegen" }
actix-multipart = { path = "actix-multipart" }
actix-files = { path = "actix-files" }
awc = { path = "awc" } awc = { path = "awc" }
[[test]]
name = "test_server"
required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
[[example]]
name = "basic"
required-features = ["compress-gzip"]
[[example]]
name = "uds"
required-features = ["compress-gzip"]
[[example]]
name = "on_connect"
required-features = []
[[bench]] [[bench]]
name = "server" name = "server"
harness = false harness = false
@ -139,3 +156,7 @@ harness = false
[[bench]] [[bench]]
name = "service" name = "service"
harness = false harness = false
[[bench]]
name = "responder"
harness = false

View File

@ -1,4 +1,4 @@
Copyright (c) 2017 Actix Team Copyright (c) 2017-NOW Actix Team
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated person obtaining a copy of this software and associated

View File

@ -10,6 +10,18 @@
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
* Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd).
By default all compression algorithms are enabled.
To select algorithm you want to include with `middleware::Compress` use following flags:
- `compress-brotli`
- `compress-gzip`
- `compress-zstd`
If you have set in your `Cargo.toml` dedicated `actix-web` features and you still want
to have compression enabled. Please change features selection like bellow:
Before: `"compress"`
After: `"compress-brotli", "compress-gzip", "compress-zstd"`
## 3.0.0 ## 3.0.0

View File

@ -1,20 +1,19 @@
<div align="center"> <div align="center">
<h1>Actix web</h1> <h1>Actix Web</h1>
<p> <p>
<strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong> <strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
</p> </p>
<p> <p>
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=3.3.2)](https://docs.rs/actix-web/3.3.2) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.7)](https://docs.rs/actix-web/4.0.0-beta.7)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![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-web.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/3.3.2/status.svg)](https://deps.rs/crate/actix-web/3.3.2) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.7)
<br /> <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) [![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) ![downloads](https://img.shields.io/crates/d/actix-web.svg)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
</p> </p>
@ -26,14 +25,13 @@
* Streaming and pipelining * Streaming and pipelining
* Keep-alive and slow requests handling * Keep-alive and slow requests handling
* Client/server [WebSockets](https://actix.rs/docs/websockets/) support * Client/server [WebSockets](https://actix.rs/docs/websockets/) support
* Transparent content compression/decompression (br, gzip, deflate) * Transparent content compression/decompression (br, gzip, deflate, zstd)
* Powerful [request routing](https://actix.rs/docs/url-dispatch/) * Powerful [request routing](https://actix.rs/docs/url-dispatch/)
* Multipart streams * Multipart streams
* Static assets * Static assets
* SSL support using OpenSSL or Rustls * SSL support using OpenSSL or Rustls
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) * 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) * Includes an async [HTTP client](https://docs.rs/awc/)
* Supports [Actix actor framework](https://github.com/actix/actix)
* Runs on stable Rust 1.46+ * Runs on stable Rust 1.46+
## Documentation ## Documentation
@ -73,18 +71,18 @@ async fn main() -> std::io::Result<()> {
### More examples ### More examples
* [Basic Setup](https://github.com/actix/examples/tree/master/basics/) * [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics/)
* [Application State](https://github.com/actix/examples/tree/master/state/) * [Application State](https://github.com/actix/examples/tree/master/basics/state/)
* [JSON Handling](https://github.com/actix/examples/tree/master/json/) * [JSON Handling](https://github.com/actix/examples/tree/master/json/json/)
* [Multipart Streams](https://github.com/actix/examples/tree/master/multipart/) * [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart/)
* [Diesel Integration](https://github.com/actix/examples/tree/master/diesel/) * [Diesel Integration](https://github.com/actix/examples/tree/master/database_interactions/diesel/)
* [r2d2 Integration](https://github.com/actix/examples/tree/master/r2d2/) * [r2d2 Integration](https://github.com/actix/examples/tree/master/database_interactions/r2d2/)
* [Simple WebSocket](https://github.com/actix/examples/tree/master/websocket/) * [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets/websocket/)
* [Tera Templates](https://github.com/actix/examples/tree/master/template_tera/) * [Tera Templates](https://github.com/actix/examples/tree/master/template_engines/tera/)
* [Askama Templates](https://github.com/actix/examples/tree/master/template_askama/) * [Askama Templates](https://github.com/actix/examples/tree/master/template_engines/askama/)
* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/rustls/) * [HTTPS using Rustls](https://github.com/actix/examples/tree/master/security/rustls/)
* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/openssl/) * [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/)
* [WebSocket Chat](https://github.com/actix/examples/tree/master/websocket-chat/) * [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/)
You may consider checking out You may consider checking out
[this directory](https://github.com/actix/examples/tree/master/) for more examples. [this directory](https://github.com/actix/examples/tree/master/) for more examples.
@ -92,16 +90,16 @@ You may consider checking out
## Benchmarks ## Benchmarks
One of the fastest web frameworks available according to the One of the fastest web frameworks available according to the
[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r19). [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite).
## License ## License
This project is licensed under either of This project is licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or * 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 * 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. at your option.

View File

@ -3,6 +3,39 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.6.0-beta.5 - 2021-06-17
* `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
* For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
* `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
* `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257]
* `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228]
[#2135]: https://github.com/actix/actix-web/pull/2135
[#2156]: https://github.com/actix/actix-web/pull/2156
[#2225]: https://github.com/actix/actix-web/pull/2225
[#2257]: https://github.com/actix/actix-web/pull/2257
[#2228]: https://github.com/actix/actix-web/pull/2228
## 0.6.0-beta.4 - 2021-04-02
* No notable changes.
* Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046]
[#2046]: https://github.com/actix/actix-web/pull/2046
## 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 ## 0.6.0-beta.1 - 2021-01-07
* `HttpRange::parse` now has its own error type. * `HttpRange::parse` now has its own error type.
* Update `bytes` to `1.0`. [#1813] * Update `bytes` to `1.0`. [#1813]

View File

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

View File

@ -3,17 +3,17 @@
> Static file serving for Actix Web > Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![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.5.0)](https://docs.rs/actix-files/0.5.0) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.5)](https://docs.rs/actix-files/0.6.0-beta.5)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![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) ![License](https://img.shields.io/crates/l/actix-files.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-files/0.5.0/status.svg)](https://deps.rs/crate/actix-files/0.5.0) [![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.5/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.5)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![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) [![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 ## Documentation & Resources
- [API Documentation](https://docs.rs/actix-files/) - [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) - [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum supported Rust version: 1.46 or later - Minimum supported Rust version: 1.46 or later

View File

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

View File

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

View File

@ -1,4 +1,4 @@
use actix_web::{http::StatusCode, HttpResponse, ResponseError}; use actix_web::{http::StatusCode, ResponseError};
use derive_more::Display; use derive_more::Display;
/// Errors which can occur when serving static files. /// Errors which can occur when serving static files.
@ -16,8 +16,8 @@ pub enum FilesError {
/// Return `NotFound` for `FilesError` /// Return `NotFound` for `FilesError`
impl ResponseError for FilesError { impl ResponseError for FilesError {
fn error_response(&self) -> HttpResponse { fn status_code(&self) -> StatusCode {
HttpResponse::new(StatusCode::NOT_FOUND) StatusCode::NOT_FOUND
} }
} }

View File

@ -1,27 +1,26 @@
use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc}; use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc};
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
dev::{ dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse},
AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse,
},
error::Error, error::Error,
guard::Guard, guard::Guard,
http::header::DispositionType, http::header::DispositionType,
HttpRequest, HttpRequest,
}; };
use futures_util::future::{ok, FutureExt, LocalBoxFuture}; use futures_core::future::LocalBoxFuture;
use crate::{ use crate::{
directory_listing, named, Directory, DirectoryRenderer, FilesService, directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService,
HttpNewService, MimeOverride, MimeOverride,
}; };
/// Static files handling service. /// Static files handling service.
/// ///
/// `Files` service must be registered with `App::service()` method. /// `Files` service must be registered with `App::service()` method.
/// ///
/// ```rust /// ```
/// use actix_web::App; /// use actix_web::App;
/// use actix_files::Files; /// use actix_files::Files;
/// ///
@ -38,7 +37,8 @@ pub struct Files {
renderer: Rc<DirectoryRenderer>, renderer: Rc<DirectoryRenderer>,
mime_override: Option<Rc<MimeOverride>>, mime_override: Option<Rc<MimeOverride>>,
file_flags: named::Flags, file_flags: named::Flags,
guards: Option<Rc<dyn Guard>>, use_guards: Option<Rc<dyn Guard>>,
guards: Vec<Rc<dyn Guard>>,
hidden_files: bool, hidden_files: bool,
} }
@ -60,6 +60,7 @@ impl Clone for Files {
file_flags: self.file_flags, file_flags: self.file_flags,
path: self.path.clone(), path: self.path.clone(),
mime_override: self.mime_override.clone(), mime_override: self.mime_override.clone(),
use_guards: self.use_guards.clone(),
guards: self.guards.clone(), guards: self.guards.clone(),
hidden_files: self.hidden_files, hidden_files: self.hidden_files,
} }
@ -81,9 +82,9 @@ impl Files {
/// If the mount path is set as the root path `/`, services registered after this one will /// If the mount path is set as the root path `/`, services registered after this one will
/// be inaccessible. Register more specific handlers and services first. /// be inaccessible. Register more specific handlers and services first.
/// ///
/// `Files` uses a threadpool for blocking filesystem operations. By default, the pool uses a /// `Files` utilizes the existing Tokio thread-pool for blocking filesystem operations.
/// number of threads equal to 5x the number of available logical CPUs. Pool size can be changed /// The number of running threads is adjusted over time as needed, up to a maximum of 512 times
/// by setting ACTIX_THREADPOOL environment variable. /// the number of server [workers](actix_web::HttpServer::workers), by default.
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files { pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
let orig_dir = serve_from.into(); let orig_dir = serve_from.into();
let dir = match orig_dir.canonicalize() { let dir = match orig_dir.canonicalize() {
@ -104,7 +105,8 @@ impl Files {
renderer: Rc::new(directory_listing), renderer: Rc::new(directory_listing),
mime_override: None, mime_override: None,
file_flags: named::Flags::default(), file_flags: named::Flags::default(),
guards: None, use_guards: None,
guards: Vec::new(),
hidden_files: false, hidden_files: false,
} }
} }
@ -112,6 +114,9 @@ impl Files {
/// Show files listing for directories. /// Show files listing for directories.
/// ///
/// By default show files listing is disabled. /// By default show files listing is disabled.
///
/// When used with [`Files::index_file()`], files listing is shown as a fallback
/// when the index file is not found.
pub fn show_files_listing(mut self) -> Self { pub fn show_files_listing(mut self) -> Self {
self.show_index = true; self.show_index = true;
self self
@ -128,8 +133,8 @@ impl Files {
/// Set custom directory renderer /// Set custom directory renderer
pub fn files_listing_renderer<F>(mut self, f: F) -> Self pub fn files_listing_renderer<F>(mut self, f: F) -> Self
where where
for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> for<'r, 's> F:
+ 'static, Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> + 'static,
{ {
self.renderer = Rc::new(f); self.renderer = Rc::new(f);
self self
@ -146,8 +151,11 @@ impl Files {
/// Set index file /// Set index file
/// ///
/// Shows specific index file for directory "/" instead of /// Shows specific index file for directories instead of
/// showing files listing. /// showing files listing.
///
/// If the index file is not found, files listing is shown as a fallback if
/// [`Files::show_files_listing()`] is set.
pub fn index_file<T: Into<String>>(mut self, index: T) -> Self { pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
self.index = Some(index.into()); self.index = Some(index.into());
self self
@ -156,7 +164,6 @@ impl Files {
/// Specifies whether to use ETag or not. /// Specifies whether to use ETag or not.
/// ///
/// Default is true. /// Default is true.
#[inline]
pub fn use_etag(mut self, value: bool) -> Self { pub fn use_etag(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::ETAG, value); self.file_flags.set(named::Flags::ETAG, value);
self self
@ -165,7 +172,6 @@ impl Files {
/// Specifies whether to use Last-Modified or not. /// Specifies whether to use Last-Modified or not.
/// ///
/// Default is true. /// Default is true.
#[inline]
pub fn use_last_modified(mut self, value: bool) -> Self { pub fn use_last_modified(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::LAST_MD, value); self.file_flags.set(named::Flags::LAST_MD, value);
self self
@ -174,31 +180,74 @@ impl Files {
/// Specifies whether text responses should signal a UTF-8 encoding. /// Specifies whether text responses should signal a UTF-8 encoding.
/// ///
/// Default is false (but will default to true in a future version). /// Default is false (but will default to true in a future version).
#[inline]
pub fn prefer_utf8(mut self, value: bool) -> Self { pub fn prefer_utf8(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::PREFER_UTF8, value); self.file_flags.set(named::Flags::PREFER_UTF8, value);
self self
} }
/// Specifies custom guards to use for directory listings and files. /// Adds a routing guard.
/// ///
/// Default behaviour allows GET and HEAD. /// Use this to allow multiple chained file services that respond to strictly different
#[inline] /// properties of a request. Due to the way routing works, if a guard check returns true and the
pub fn use_guards<G: Guard + 'static>(mut self, guards: G) -> Self { /// request starts being handled by the file service, it will not be able to back-out and try
self.guards = Some(Rc::new(guards)); /// the next service, you will simply get a 404 (or 405) error response.
///
/// To allow `POST` requests to retrieve files, see [`Files::use_guards`].
///
/// # Examples
/// ```
/// use actix_web::{guard::Header, App};
/// use actix_files::Files;
///
/// App::new().service(
/// Files::new("/","/my/site/files")
/// .guard(Header("Host", "example.com"))
/// );
/// ```
pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
self.guards.push(Rc::new(guard));
self self
} }
/// Specifies guard to check before fetching directory listings or files.
///
/// Note that this guard has no effect on routing; it's main use is to guard on the request's
/// method just before serving the file, only allowing `GET` and `HEAD` requests by default.
/// See [`Files::guard`] for routing guards.
pub fn method_guard<G: Guard + 'static>(mut self, guard: G) -> Self {
self.use_guards = Some(Rc::new(guard));
self
}
#[doc(hidden)]
#[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")]
/// See [`Files::method_guard`].
pub fn use_guards<G: Guard + 'static>(self, guard: G) -> Self {
self.method_guard(guard)
}
/// Disable `Content-Disposition` header. /// Disable `Content-Disposition` header.
/// ///
/// By default Content-Disposition` header is enabled. /// By default Content-Disposition` header is enabled.
#[inline]
pub fn disable_content_disposition(mut self) -> Self { pub fn disable_content_disposition(mut self) -> Self {
self.file_flags.remove(named::Flags::CONTENT_DISPOSITION); self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
self self
} }
/// Sets default handler which is used when no matched file could be found. /// Sets default handler which is used when no matched file could be found.
///
/// # Examples
/// Setting a fallback static file handler:
/// ```
/// use actix_files::{Files, NamedFile};
///
/// # fn run() -> Result<(), actix_web::Error> {
/// let files = Files::new("/", "./static")
/// .index_file("index.html")
/// .default_handler(NamedFile::open("./static/404.html")?);
/// # Ok(())
/// # }
/// ```
pub fn default_handler<F, U>(mut self, f: F) -> Self pub fn default_handler<F, U>(mut self, f: F) -> Self
where where
F: IntoServiceFactory<U, ServiceRequest>, F: IntoServiceFactory<U, ServiceRequest>,
@ -218,7 +267,6 @@ impl Files {
} }
/// Enables serving hidden files and directories, allowing a leading dots in url fragments. /// Enables serving hidden files and directories, allowing a leading dots in url fragments.
#[inline]
pub fn use_hidden_files(mut self) -> Self { pub fn use_hidden_files(mut self) -> Self {
self.hidden_files = true; self.hidden_files = true;
self self
@ -226,7 +274,19 @@ impl Files {
} }
impl HttpServiceFactory for Files { impl HttpServiceFactory for Files {
fn register(self, config: &mut AppService) { fn register(mut self, config: &mut AppService) {
let guards = if self.guards.is_empty() {
None
} else {
let guards = std::mem::take(&mut self.guards);
Some(
guards
.into_iter()
.map(|guard| -> Box<dyn Guard> { Box::new(guard) })
.collect::<Vec<_>>(),
)
};
if self.default.borrow().is_none() { if self.default.borrow().is_none() {
*self.default.borrow_mut() = Some(config.default_service()); *self.default.borrow_mut() = Some(config.default_service());
} }
@ -237,7 +297,7 @@ impl HttpServiceFactory for Files {
ResourceDef::prefix(&self.path) ResourceDef::prefix(&self.path)
}; };
config.register_service(rdef, None, self, None) config.register_service(rdef, guards, self, None)
} }
} }
@ -259,23 +319,23 @@ impl ServiceFactory<ServiceRequest> for Files {
renderer: self.renderer.clone(), renderer: self.renderer.clone(),
mime_override: self.mime_override.clone(), mime_override: self.mime_override.clone(),
file_flags: self.file_flags, file_flags: self.file_flags,
guards: self.guards.clone(), guards: self.use_guards.clone(),
hidden_files: self.hidden_files, hidden_files: self.hidden_files,
}; };
if let Some(ref default) = *self.default.borrow() { if let Some(ref default) = *self.default.borrow() {
default let fut = default.new_service(());
.new_service(()) Box::pin(async {
.map(move |result| match result { match fut.await {
Ok(default) => { Ok(default) => {
srv.default = Some(default); srv.default = Some(default);
Ok(srv) Ok(srv)
} }
Err(_) => Err(()), Err(_) => Err(()),
}) }
.boxed_local() })
} else { } else {
ok(srv).boxed_local() Box::pin(ok(srv))
} }
} }
} }

View File

@ -3,7 +3,7 @@
//! Provides a non-blocking service for serving static files from disk. //! Provides a non-blocking service for serving static files from disk.
//! //!
//! # Example //! # Example
//! ```rust //! ```
//! use actix_web::App; //! use actix_web::App;
//! use actix_files::Files; //! use actix_files::Files;
//! //!
@ -14,12 +14,10 @@
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
#![warn(missing_docs, missing_debug_implementations)] #![warn(missing_docs, missing_debug_implementations)]
use std::io;
use actix_service::boxed::{BoxService, BoxServiceFactory}; use actix_service::boxed::{BoxService, BoxServiceFactory};
use actix_web::{ use actix_web::{
dev::{ServiceRequest, ServiceResponse}, dev::{ServiceRequest, ServiceResponse},
error::{BlockingError, Error, ErrorInternalServerError}, error::Error,
http::header::DispositionType, http::header::DispositionType,
}; };
use mime_guess::from_ext; 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() 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; type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
#[cfg(test)] #[cfg(test)]
@ -74,6 +65,7 @@ mod tests {
}; };
use actix_service::ServiceFactory; use actix_service::ServiceFactory;
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
guard, guard,
http::{ http::{
@ -82,9 +74,9 @@ mod tests {
}, },
middleware::Compress, middleware::Compress,
test::{self, TestRequest}, test::{self, TestRequest},
web, App, HttpResponse, Responder, web::{self, Bytes},
App, HttpResponse, Responder,
}; };
use futures_util::future::ok;
use super::*; use super::*;
@ -106,11 +98,22 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_modified_since_without_if_none_match() { async fn test_if_modified_since_without_if_none_match() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open("Cargo.toml").unwrap();
let since = let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default() 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(); .to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
@ -119,17 +122,40 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_modified_since_with_if_none_match() { async fn test_if_modified_since_with_if_none_match() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open("Cargo.toml").unwrap();
let since = let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default() let req = TestRequest::default()
.header(header::IF_NONE_MATCH, "miss_etag") .insert_header((header::IF_NONE_MATCH, "miss_etag"))
.header(header::IF_MODIFIED_SINCE, since) .insert_header((header::IF_MODIFIED_SINCE, since))
.to_http_request(); .to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req).await.unwrap();
assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); 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] #[actix_rt::test]
async fn test_named_file_text() { async fn test_named_file_text() {
assert!(NamedFile::open("test--").is_err()); assert!(NamedFile::open("test--").is_err());
@ -184,8 +210,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_non_ascii_file_name() { async fn test_named_file_non_ascii_file_name() {
let mut file = let mut file =
NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml") NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml").unwrap();
.unwrap();
{ {
file.file(); file.file();
let _f: &File = &file; let _f: &File = &file;
@ -254,6 +279,22 @@ mod tests {
); );
} }
#[actix_rt::test]
async fn test_named_file_javascript() {
let file = NamedFile::open("tests/test.js").unwrap();
let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap();
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"application/javascript"
);
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"test.js\""
);
}
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_image_attachment() { async fn test_named_file_image_attachment() {
let cd = ContentDisposition { let cd = ContentDisposition {
@ -338,7 +379,7 @@ mod tests {
DispositionType::Attachment DispositionType::Attachment
} }
let mut srv = test::init_service( let srv = test::init_service(
App::new().service( App::new().service(
Files::new("/", ".") Files::new("/", ".")
.mime_override(all_attachment) .mime_override(all_attachment)
@ -348,7 +389,7 @@ mod tests {
.await; .await;
let request = TestRequest::get().uri("/").to_request(); 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); assert_eq!(response.status(), StatusCode::OK);
let content_disposition = response let content_disposition = response
@ -363,7 +404,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_ranges_status_code() { 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")), App::new().service(Files::new("/test", ".").index_file("Cargo.toml")),
) )
.await; .await;
@ -371,29 +412,29 @@ mod tests {
// Valid range header // Valid range header
let request = TestRequest::get() let request = TestRequest::get()
.uri("/t%65st/Cargo.toml") .uri("/t%65st/Cargo.toml")
.header(header::RANGE, "bytes=10-20") .insert_header((header::RANGE, "bytes=10-20"))
.to_request(); .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); assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
// Invalid range header // Invalid range header
let request = TestRequest::get() let request = TestRequest::get()
.uri("/t%65st/Cargo.toml") .uri("/t%65st/Cargo.toml")
.header(header::RANGE, "bytes=1-0") .insert_header((header::RANGE, "bytes=1-0"))
.to_request(); .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); assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_content_range_headers() { async fn test_named_file_content_range_headers() {
let srv = test::start(|| App::new().service(Files::new("/", "."))); let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
// Valid range header // Valid range header
let response = srv let response = srv
.get("/tests/test.binary") .get("/tests/test.binary")
.header(header::RANGE, "bytes=10-20") .insert_header((header::RANGE, "bytes=10-20"))
.send() .send()
.await .await
.unwrap(); .unwrap();
@ -403,7 +444,7 @@ mod tests {
// Invalid range header // Invalid range header
let response = srv let response = srv
.get("/tests/test.binary") .get("/tests/test.binary")
.header(header::RANGE, "bytes=10-5") .insert_header((header::RANGE, "bytes=10-5"))
.send() .send()
.await .await
.unwrap(); .unwrap();
@ -413,12 +454,12 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_content_length_headers() { async fn test_named_file_content_length_headers() {
let srv = test::start(|| App::new().service(Files::new("/", "."))); let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
// Valid range header // Valid range header
let response = srv let response = srv
.get("/tests/test.binary") .get("/tests/test.binary")
.header(header::RANGE, "bytes=10-20") .insert_header((header::RANGE, "bytes=10-20"))
.send() .send()
.await .await
.unwrap(); .unwrap();
@ -428,7 +469,7 @@ mod tests {
// Valid range header, starting from 0 // Valid range header, starting from 0
let response = srv let response = srv
.get("/tests/test.binary") .get("/tests/test.binary")
.header(header::RANGE, "bytes=0-20") .insert_header((header::RANGE, "bytes=0-20"))
.send() .send()
.await .await
.unwrap(); .unwrap();
@ -452,7 +493,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_head_content_length_headers() { async fn test_head_content_length_headers() {
let srv = test::start(|| App::new().service(Files::new("/", "."))); let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
let response = srv.head("/tests/test.binary").send().await.unwrap(); let response = srv.head("/tests/test.binary").send().await.unwrap();
@ -468,14 +509,14 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_static_files_with_spaces() { 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")), App::new().service(Files::new("/", ".").index_file("Cargo.toml")),
) )
.await; .await;
let request = TestRequest::get() let request = TestRequest::get()
.uri("/tests/test%20space.binary") .uri("/tests/test%20space.binary")
.to_request(); .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); assert_eq!(response.status(), StatusCode::OK);
let bytes = test::read_body(response).await; let bytes = test::read_body(response).await;
@ -485,29 +526,29 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_files_not_allowed() { 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() let req = TestRequest::default()
.uri("/Cargo.toml") .uri("/Cargo.toml")
.method(Method::POST) .method(Method::POST)
.to_request(); .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); 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() let req = TestRequest::default()
.method(Method::PUT) .method(Method::PUT)
.uri("/Cargo.toml") .uri("/Cargo.toml")
.to_request(); .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); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_files_guards() { 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())), App::new().service(Files::new("/", ".").method_guard(guard::Post())),
) )
.await; .await;
@ -516,13 +557,13 @@ mod tests {
.method(Method::POST) .method(Method::POST)
.to_request(); .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); assert_eq!(resp.status(), StatusCode::OK);
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_content_encoding() { 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 { web::resource("/").to(|| async {
NamedFile::open("Cargo.toml") NamedFile::open("Cargo.toml")
.unwrap() .unwrap()
@ -533,16 +574,16 @@ mod tests {
let request = TestRequest::get() let request = TestRequest::get()
.uri("/") .uri("/")
.header(header::ACCEPT_ENCODING, "gzip") .insert_header((header::ACCEPT_ENCODING, "gzip"))
.to_request(); .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.status(), StatusCode::OK);
assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); assert!(!res.headers().contains_key(header::CONTENT_ENCODING));
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_content_encoding_gzip() { 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 { web::resource("/").to(|| async {
NamedFile::open("Cargo.toml") NamedFile::open("Cargo.toml")
.unwrap() .unwrap()
@ -553,9 +594,9 @@ mod tests {
let request = TestRequest::get() let request = TestRequest::get()
.uri("/") .uri("/")
.header(header::ACCEPT_ENCODING, "gzip") .insert_header((header::ACCEPT_ENCODING, "gzip"))
.to_request(); .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.status(), StatusCode::OK);
assert_eq!( assert_eq!(
res.headers() res.headers()
@ -577,27 +618,25 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_static_files() { async fn test_static_files() {
let mut srv = test::init_service( let srv =
App::new().service(Files::new("/", ".").show_files_listing()), test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
) .await;
.await;
let req = TestRequest::with_uri("/missing").to_request(); 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); 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 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); assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let mut srv = test::init_service( let srv =
App::new().service(Files::new("/", ".").show_files_listing()), test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
) .await;
.await;
let req = TestRequest::with_uri("/tests").to_request(); 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!( assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(), resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/html; charset=utf-8" "text/html; charset=utf-8"
@ -609,17 +648,17 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_redirect_to_slash_directory() { async fn test_redirect_to_slash_directory() {
// should not redirect if no index // should not redirect if no index and files listing is disabled
let mut srv = test::init_service( let srv = test::init_service(
App::new().service(Files::new("/", ".").redirect_to_slash_directory()), App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
) )
.await; .await;
let req = TestRequest::with_uri("/tests").to_request(); 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); assert_eq!(resp.status(), StatusCode::NOT_FOUND);
// should redirect if index present // should redirect if index present
let mut srv = test::init_service( let srv = test::init_service(
App::new().service( App::new().service(
Files::new("/", ".") Files::new("/", ".")
.index_file("test.png") .index_file("test.png")
@ -628,24 +667,41 @@ mod tests {
) )
.await; .await;
let req = TestRequest::with_uri("/tests").to_request(); 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 redirect if files listing is enabled
let srv = test::init_service(
App::new().service(
Files::new("/", ".")
.show_files_listing()
.redirect_to_slash_directory(),
),
)
.await;
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::FOUND);
// should not redirect if the path is wrong // should not redirect if the path is wrong
let req = TestRequest::with_uri("/not_existing").to_request(); 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); assert_eq!(resp.status(), StatusCode::NOT_FOUND);
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_static_files_bad_directory() { async fn test_static_files_bad_directory() {
let _st: Files = Files::new("/", "missing"); let service = Files::new("/", "./missing").new_service(()).await.unwrap();
let _st: Files = Files::new("/", "Cargo.toml");
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] #[actix_rt::test]
async fn test_default_handler_file_missing() { async fn test_default_handler_file_missing() {
let mut st = Files::new("/", ".") let st = Files::new("/", ".")
.default_handler(|req: ServiceRequest| { .default_handler(|req: ServiceRequest| {
ok(req.into_response(HttpResponse::Ok().body("default content"))) ok(req.into_response(HttpResponse::Ok().body("default content")))
}) })
@ -653,124 +709,196 @@ mod tests {
.await .await
.unwrap(); .unwrap();
let req = TestRequest::with_uri("/missing").to_srv_request(); 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); assert_eq!(resp.status(), StatusCode::OK);
let bytes = test::read_body(resp).await; let bytes = test::read_body(resp).await;
assert_eq!(bytes, web::Bytes::from_static(b"default content")); assert_eq!(bytes, web::Bytes::from_static(b"default content"));
} }
// #[actix_rt::test] #[actix_rt::test]
// async fn test_serve_index() { async fn test_serve_index_nested() {
// let st = Files::new(".").index_file("test.binary"); let service = Files::new(".", ".")
// let req = TestRequest::default().uri("/tests").finish(); .index_file("lib.rs")
.new_service(())
.await
.unwrap();
// let resp = st.handle(&req).respond_to(&req).unwrap(); let req = TestRequest::default().uri("/src").to_srv_request();
// let resp = resp.as_msg(); let resp = test::call_service(&service, req).await;
// 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("/tests/").finish(); assert_eq!(resp.status(), StatusCode::OK);
// let resp = st.handle(&req).respond_to(&req).unwrap(); assert_eq!(
// let resp = resp.as_msg(); resp.headers().get(header::CONTENT_TYPE).unwrap(),
// assert_eq!(resp.status(), StatusCode::OK); "text/x-rust"
// assert_eq!( );
// resp.headers().get(header::CONTENT_TYPE).unwrap(), assert_eq!(
// "application/octet-stream" resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
// ); "inline; filename=\"lib.rs\""
// assert_eq!( );
// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), }
// "attachment; filename=\"test.binary\""
// );
// // nonexistent index file #[actix_rt::test]
// let req = TestRequest::default().uri("/tests/unknown").finish(); async fn integration_serve_index() {
// let resp = st.handle(&req).respond_to(&req).unwrap(); let srv = test::init_service(
// let resp = resp.as_msg(); App::new().service(Files::new("test", ".").index_file("Cargo.toml")),
// assert_eq!(resp.status(), StatusCode::NOT_FOUND); )
.await;
// let req = TestRequest::default().uri("/tests/unknown/").finish(); let req = TestRequest::get().uri("/test").to_request();
// let resp = st.handle(&req).respond_to(&req).unwrap(); let res = test::call_service(&srv, req).await;
// let resp = resp.as_msg(); assert_eq!(res.status(), StatusCode::OK);
// assert_eq!(resp.status(), StatusCode::NOT_FOUND);
// }
// #[actix_rt::test] let bytes = test::read_body(res).await;
// 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\""
// );
// }
// #[actix_rt::test] let data = Bytes::from(fs::read("Cargo.toml").unwrap());
// fn integration_serve_index() { assert_eq!(bytes, data);
// let mut srv = test::TestServer::with_factory(|| {
// App::new().handler(
// "test",
// Files::new(".").index_file("Cargo.toml"),
// )
// });
// let request = srv.get().uri(srv.url("/test")).finish().unwrap(); let req = TestRequest::get().uri("/test/").to_request();
// let response = srv.execute(request.send()).unwrap(); let res = test::call_service(&srv, req).await;
// assert_eq!(response.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
// let bytes = srv.execute(response.body()).unwrap();
// let data = Bytes::from(fs::read("Cargo.toml").unwrap());
// assert_eq!(bytes, data);
// let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); let bytes = test::read_body(res).await;
// let response = srv.execute(request.send()).unwrap(); let data = Bytes::from(fs::read("Cargo.toml").unwrap());
// assert_eq!(response.status(), StatusCode::OK); assert_eq!(bytes, data);
// let bytes = srv.execute(response.body()).unwrap();
// let data = Bytes::from(fs::read("Cargo.toml").unwrap());
// assert_eq!(bytes, data);
// // nonexistent index file // nonexistent index file
// let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); let req = TestRequest::get().uri("/test/unknown").to_request();
// let response = srv.execute(request.send()).unwrap(); let res = test::call_service(&srv, req).await;
// assert_eq!(response.status(), StatusCode::NOT_FOUND); assert_eq!(res.status(), StatusCode::NOT_FOUND);
// let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); let req = TestRequest::get().uri("/test/unknown/").to_request();
// let response = srv.execute(request.send()).unwrap(); let res = test::call_service(&srv, req).await;
// assert_eq!(response.status(), StatusCode::NOT_FOUND); assert_eq!(res.status(), StatusCode::NOT_FOUND);
// } }
// #[actix_rt::test] #[actix_rt::test]
// fn integration_percent_encoded() { async fn integration_percent_encoded() {
// let mut srv = test::TestServer::with_factory(|| { let srv = test::init_service(
// App::new().handler( App::new().service(Files::new("test", ".").index_file("Cargo.toml")),
// "test", )
// Files::new(".").index_file("Cargo.toml"), .await;
// )
// });
// let request = srv let req = TestRequest::get().uri("/test/%43argo.toml").to_request();
// .get() let res = test::call_service(&srv, req).await;
// .uri(srv.url("/test/%43argo.toml")) assert_eq!(res.status(), StatusCode::OK);
// .finish() }
// .unwrap();
// let response = srv.execute(request.send()).unwrap(); #[actix_rt::test]
// assert_eq!(response.status(), StatusCode::OK); async fn test_serve_named_file() {
// } let srv =
test::init_service(App::new().service(NamedFile::open("Cargo.toml").unwrap()))
.await;
let req = TestRequest::get().uri("/Cargo.toml").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
let bytes = test::read_body(res).await;
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
assert_eq!(bytes, data);
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]
async fn test_serve_named_file_prefix() {
let srv = test::init_service(
App::new()
.service(web::scope("/test").service(NamedFile::open("Cargo.toml").unwrap())),
)
.await;
let req = TestRequest::get().uri("/test/Cargo.toml").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
let bytes = test::read_body(res).await;
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
assert_eq!(bytes, data);
let req = TestRequest::get().uri("/Cargo.toml").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
#[actix_rt::test]
async fn test_named_file_default_service() {
let srv = test::init_service(
App::new().default_service(NamedFile::open("Cargo.toml").unwrap()),
)
.await;
for route in ["/foobar", "/baz", "/"].iter() {
let req = TestRequest::get().uri(route).to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
let bytes = test::read_body(res).await;
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
assert_eq!(bytes, data);
}
}
#[actix_rt::test]
async fn test_default_handler_named_file() {
let st = Files::new("/", ".")
.default_handler(NamedFile::open("Cargo.toml").unwrap())
.new_service(())
.await
.unwrap();
let req = TestRequest::with_uri("/missing").to_srv_request();
let resp = test::call_service(&st, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let bytes = test::read_body(resp).await;
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
assert_eq!(bytes, data);
}
#[actix_rt::test]
async fn test_symlinks() {
let srv = test::init_service(App::new().service(Files::new("test", "."))).await;
let req = TestRequest::get()
.uri("/test/tests/symlink-test.png")
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"symlink-test.png\""
);
}
#[actix_rt::test]
async fn test_index_with_show_files_listing() {
let service = Files::new(".", ".")
.index_file("lib.rs")
.show_files_listing()
.new_service(())
.await
.unwrap();
// Serve the index if exists
let req = TestRequest::default().uri("/src").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/x-rust"
);
// Show files listing, otherwise.
let req = TestRequest::default().uri("/tests").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/html; charset=utf-8"
);
let bytes = test::read_body(resp).await;
assert!(format!("{:?}", bytes).contains("/tests/test.png"));
}
} }

View File

@ -1,3 +1,6 @@
use actix_service::{Service, ServiceFactory};
use actix_utils::future::{ok, ready, Ready};
use actix_web::dev::{AppService, HttpServiceFactory, ResourceDef};
use std::fs::{File, Metadata}; use std::fs::{File, Metadata};
use std::io; use std::io;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
@ -8,18 +11,16 @@ use std::time::{SystemTime, UNIX_EPOCH};
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use actix_web::{ use actix_web::{
dev::{BodyEncoding, SizedStream}, dev::{BodyEncoding, ServiceRequest, ServiceResponse, SizedStream},
http::{ http::{
header::{ header::{
self, Charset, ContentDisposition, DispositionParam, DispositionType, self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
ExtendedValue,
}, },
ContentEncoding, StatusCode, ContentEncoding, StatusCode,
}, },
Error, HttpMessage, HttpRequest, HttpResponse, Responder, Error, HttpMessage, HttpRequest, HttpResponse, Responder,
}; };
use bitflags::bitflags; use bitflags::bitflags;
use futures_util::future::{ready, Ready};
use mime_guess::from_path; use mime_guess::from_path;
use crate::ChunkedReadFile; use crate::ChunkedReadFile;
@ -41,6 +42,29 @@ impl Default for Flags {
} }
/// A file with an associated name. /// A file with an associated name.
///
/// `NamedFile` can be registered as services:
/// ```
/// use actix_web::App;
/// use actix_files::NamedFile;
///
/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
/// let app = App::new()
/// .service(NamedFile::open("./static/index.html")?);
/// # Ok(())
/// # }
/// ```
///
/// They can also be returned from handlers:
/// ```
/// use actix_web::{Responder, get};
/// use actix_files::NamedFile;
///
/// #[get("/")]
/// async fn index() -> impl Responder {
/// NamedFile::open("./static/index.html")
/// }
/// ```
#[derive(Debug)] #[derive(Debug)]
pub struct NamedFile { pub struct NamedFile {
path: PathBuf, path: PathBuf,
@ -62,7 +86,7 @@ impl NamedFile {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// use std::io::{self, Write}; /// use std::io::{self, Write};
/// use std::env; /// use std::env;
@ -96,6 +120,11 @@ impl NamedFile {
let disposition = match ct.type_() { let disposition = match ct.type_() {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
mime::APPLICATION => match ct.subtype() {
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
name if name == "wasm" => DispositionType::Inline,
_ => DispositionType::Attachment,
},
_ => DispositionType::Attachment, _ => DispositionType::Attachment,
}; };
@ -139,7 +168,7 @@ impl NamedFile {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// ///
/// let file = NamedFile::open("foo.txt"); /// let file = NamedFile::open("foo.txt");
@ -158,7 +187,7 @@ impl NamedFile {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// # use std::io; /// # use std::io;
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// ///
@ -189,9 +218,11 @@ impl NamedFile {
/// Set the Content-Disposition for serving this file. This allows /// Set the Content-Disposition for serving this file. This allows
/// changing the inline/attachment disposition as well as the filename /// changing the inline/attachment disposition as well as the filename
/// sent to the peer. By default the disposition is `inline` for text, /// sent to the peer.
/// image, and video content types, and `attachment` otherwise, and ///
/// the filename is taken from the path provided in the `open` method /// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and
/// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise,
/// and the filename is taken from the path provided in the `open` method
/// after converting it to UTF-8 using. /// after converting it to UTF-8 using.
/// [`std::ffi::OsStr::to_string_lossy`] /// [`std::ffi::OsStr::to_string_lossy`]
#[inline] #[inline]
@ -211,6 +242,8 @@ impl NamedFile {
} }
/// Set content encoding for serving this file /// Set content encoding for serving this file
///
/// Must be used with [`actix_web::middleware::Compress`] to take effect.
#[inline] #[inline]
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
self.encoding = Some(enc); self.encoding = Some(enc);
@ -277,37 +310,31 @@ impl NamedFile {
} }
/// Creates an `HttpResponse` with file as a streaming body. /// Creates an `HttpResponse` with file as a streaming body.
pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> { pub fn into_response(self, req: &HttpRequest) -> HttpResponse {
if self.status_code != StatusCode::OK { if self.status_code != StatusCode::OK {
let mut res = HttpResponse::build(self.status_code); let mut res = HttpResponse::build(self.status_code);
if self.flags.contains(Flags::PREFER_UTF8) { if self.flags.contains(Flags::PREFER_UTF8) {
let ct = equiv_utf8_text(self.content_type.clone()); 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 { } 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) { if self.flags.contains(Flags::CONTENT_DISPOSITION) {
res.header( res.insert_header((
header::CONTENT_DISPOSITION, header::CONTENT_DISPOSITION,
self.content_disposition.to_string(), self.content_disposition.to_string(),
); ));
} }
if let Some(current_encoding) = self.encoding { if let Some(current_encoding) = self.encoding {
res.encoding(current_encoding); res.encoding(current_encoding);
} }
let reader = ChunkedReadFile { let reader = ChunkedReadFile::new(self.md.len(), 0, self.file);
size: self.md.len(),
offset: 0,
file: Some(self.file),
fut: None,
counter: 0,
};
return Ok(res.streaming(reader)); return res.streaming(reader);
} }
let etag = if self.flags.contains(Flags::ETAG) { let etag = if self.flags.contains(Flags::ETAG) {
@ -332,7 +359,7 @@ impl NamedFile {
let t2: SystemTime = since.clone().into(); let t2: SystemTime = since.clone().into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { 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, _ => false,
} }
} else { } else {
@ -351,7 +378,7 @@ impl NamedFile {
let t2: SystemTime = since.clone().into(); let t2: SystemTime = since.clone().into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { 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, _ => false,
} }
} else { } else {
@ -362,16 +389,16 @@ impl NamedFile {
if self.flags.contains(Flags::PREFER_UTF8) { if self.flags.contains(Flags::PREFER_UTF8) {
let ct = equiv_utf8_text(self.content_type.clone()); 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 { } 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) { if self.flags.contains(Flags::CONTENT_DISPOSITION) {
resp.header( resp.insert_header((
header::CONTENT_DISPOSITION, header::CONTENT_DISPOSITION,
self.content_disposition.to_string(), self.content_disposition.to_string(),
); ));
} }
// default compressing // default compressing
@ -380,14 +407,14 @@ impl NamedFile {
} }
if let Some(lm) = last_modified { 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 { 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 length = self.md.len();
let mut offset = 0; let mut offset = 0;
@ -400,43 +427,32 @@ impl NamedFile {
offset = ranges[0].start; offset = ranges[0].start;
resp.encoding(ContentEncoding::Identity); resp.encoding(ContentEncoding::Identity);
resp.header( resp.insert_header((
header::CONTENT_RANGE, header::CONTENT_RANGE,
format!( format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
"bytes {}-{}/{}", ));
offset,
offset + length - 1,
self.md.len()
),
);
} else { } else {
resp.header(header::CONTENT_RANGE, format!("bytes */{}", length)); resp.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish()); return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
}; };
} else { } else {
return Ok(resp.status(StatusCode::BAD_REQUEST).finish()); return resp.status(StatusCode::BAD_REQUEST).finish();
}; };
}; };
if precondition_failed { if precondition_failed {
return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish()); return resp.status(StatusCode::PRECONDITION_FAILED).finish();
} else if not_modified { } else if not_modified {
return Ok(resp.status(StatusCode::NOT_MODIFIED).finish()); return resp.status(StatusCode::NOT_MODIFIED).finish();
} }
let reader = ChunkedReadFile { let reader = ChunkedReadFile::new(length, offset, self.file);
offset,
size: length,
file: Some(self.file),
fut: None,
counter: 0,
};
if offset != 0 || length != self.md.len() { if offset != 0 || length != self.md.len() {
resp.status(StatusCode::PARTIAL_CONTENT); resp.status(StatusCode::PARTIAL_CONTENT);
} }
Ok(resp.body(SizedStream::new(length, reader))) resp.body(SizedStream::new(length, reader))
} }
} }
@ -495,10 +511,57 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
} }
impl Responder for NamedFile { impl Responder for NamedFile {
type Error = Error; fn respond_to(self, req: &HttpRequest) -> HttpResponse {
type Future = Ready<Result<HttpResponse, Error>>; self.into_response(req)
}
fn respond_to(self, req: &HttpRequest) -> Self::Future { }
ready(self.into_response(req))
impl ServiceFactory<ServiceRequest> for NamedFile {
type Response = ServiceResponse;
type Error = Error;
type Config = ();
type InitError = ();
type Service = NamedFileService;
type Future = Ready<Result<Self::Service, ()>>;
fn new_service(&self, _: ()) -> Self::Future {
ok(NamedFileService {
path: self.path.clone(),
})
}
}
#[doc(hidden)]
#[derive(Debug)]
pub struct NamedFileService {
path: PathBuf,
}
impl Service<ServiceRequest> for NamedFileService {
type Response = ServiceResponse;
type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>;
actix_service::always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future {
let (req, _) = req.into_parts();
ready(
NamedFile::open(&self.path)
.map_err(|e| e.into())
.map(|f| f.into_response(&req))
.map(|res| ServiceResponse::new(req, res)),
)
}
}
impl HttpServiceFactory for NamedFile {
fn register(self, config: &mut AppService) {
config.register_service(
ResourceDef::root_prefix(self.path.to_string_lossy().as_ref()),
None,
self,
None,
)
} }
} }

View File

@ -3,8 +3,8 @@ use std::{
str::FromStr, str::FromStr,
}; };
use actix_utils::future::{ready, Ready};
use actix_web::{dev::Payload, FromRequest, HttpRequest}; use actix_web::{dev::Payload, FromRequest, HttpRequest};
use futures_util::future::{ready, Ready};
use crate::error::UriSegmentError; use crate::error::UriSegmentError;

View File

@ -10,9 +10,6 @@ pub struct HttpRange {
pub length: u64, pub length: u64,
} }
const PREFIX: &str = "bytes=";
const PREFIX_LEN: usize = 6;
#[derive(Debug, Clone, Display, Error)] #[derive(Debug, Clone, Display, Error)]
#[display(fmt = "Parse HTTP Range failed")] #[display(fmt = "Parse HTTP Range failed")]
pub struct ParseRangeErr(#[error(not(source))] ()); pub struct ParseRangeErr(#[error(not(source))] ());
@ -23,84 +20,16 @@ impl HttpRange {
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
/// `size` is full size of response (file). /// `size` is full size of response (file).
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> { pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
if header.is_empty() { match http_range::HttpRange::parse(header, size) {
return Ok(Vec::new()); Ok(ranges) => Ok(ranges
.iter()
.map(|range| HttpRange {
start: range.start,
length: range.length,
})
.collect()),
Err(_) => Err(ParseRangeErr(())),
} }
if !header.starts_with(PREFIX) {
return Err(ParseRangeErr(()));
}
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(ParseRangeErr(()))?.trim();
let end_str = start_end_iter.next().ok_or(ParseRangeErr(()))?.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(|_| ParseRangeErr(()))?;
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(|_| ParseRangeErr(()))?;
if start < 0 {
return Err(ParseRangeErr(()));
}
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(|_| ParseRangeErr(()))?;
if start > end {
return Err(ParseRangeErr(()));
}
if end >= size_sig {
end = size_sig - 1;
}
end - start + 1
};
Ok(Some(HttpRange {
start: start as u64,
length: length as u64,
}))
}
})
.collect::<Result<_, _>>()?;
let ranges: Vec<HttpRange> = all_ranges.into_iter().filter_map(|x| x).collect();
if no_overlap && ranges.is_empty() {
return Err(ParseRangeErr(()));
}
Ok(ranges)
} }
} }

View File

@ -1,11 +1,7 @@
use std::{ use std::{fmt, io, path::PathBuf, rc::Rc};
fmt, io,
path::PathBuf,
rc::Rc,
task::{Context, Poll},
};
use actix_service::Service; use actix_service::Service;
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
dev::{ServiceRequest, ServiceResponse}, dev::{ServiceRequest, ServiceResponse},
error::Error, error::Error,
@ -13,11 +9,11 @@ use actix_web::{
http::{header, Method}, http::{header, Method},
HttpResponse, HttpResponse,
}; };
use futures_util::future::{ok, Either, LocalBoxFuture, Ready}; use futures_core::future::LocalBoxFuture;
use crate::{ use crate::{
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile,
NamedFile, PathBufWrap, PathBufWrap,
}; };
/// Assembled file serving service. /// Assembled file serving service.
@ -34,19 +30,18 @@ pub struct FilesService {
pub(crate) hidden_files: bool, pub(crate) hidden_files: bool,
} }
type FilesServiceFuture = Either<
Ready<Result<ServiceResponse, Error>>,
LocalBoxFuture<'static, Result<ServiceResponse, Error>>,
>;
impl FilesService { impl FilesService {
fn handle_err(&mut self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture { fn handle_err(
log::debug!("Failed to handle {}: {}", req.path(), e); &self,
err: io::Error,
req: ServiceRequest,
) -> LocalBoxFuture<'static, Result<ServiceResponse, Error>> {
log::debug!("error handling {}: {}", req.path(), err);
if let Some(ref mut default) = self.default { if let Some(ref default) = self.default {
Either::Right(default.call(req)) Box::pin(default.call(req))
} else { } else {
Either::Left(ok(req.error_response(e))) Box::pin(ok(req.error_response(err)))
} }
} }
} }
@ -60,13 +55,11 @@ impl fmt::Debug for FilesService {
impl Service<ServiceRequest> for FilesService { impl Service<ServiceRequest> for FilesService {
type Response = ServiceResponse; type Response = ServiceResponse;
type Error = Error; type Error = Error;
type Future = FilesServiceFuture; type Future = LocalBoxFuture<'static, Result<ServiceResponse, Error>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { actix_service::always_ready!();
Poll::Ready(Ok(()))
}
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 { let is_method_valid = if let Some(guard) = &self.guards {
// execute user defined guards // execute user defined guards
(**guard).check(req.head()) (**guard).check(req.head())
@ -76,9 +69,9 @@ impl Service<ServiceRequest> for FilesService {
}; };
if !is_method_valid { if !is_method_valid {
return Either::Left(ok(req.into_response( return Box::pin(ok(req.into_response(
actix_web::HttpResponse::MethodNotAllowed() 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."), .body("Request did not meet this resource's requirements."),
))); )));
} }
@ -86,82 +79,79 @@ impl Service<ServiceRequest> for FilesService {
let real_path = let real_path =
match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) { match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) {
Ok(item) => item, Ok(item) => item,
Err(e) => return Either::Left(ok(req.error_response(e))), Err(e) => return Box::pin(ok(req.error_response(e))),
}; };
// full file path // full file path
let path = match self.directory.join(&real_path).canonicalize() { let path = self.directory.join(&real_path);
Ok(path) => path, if let Err(err) = path.canonicalize() {
Err(e) => return self.handle_err(e, req), return Box::pin(self.handle_err(err, req));
}; }
if path.is_dir() { if path.is_dir() {
if let Some(ref redir_index) = self.index { if self.redirect_to_slash
if self.redirect_to_slash && !req.path().ends_with('/') { && !req.path().ends_with('/')
let redirect_to = format!("{}/", req.path()); && (self.index.is_some() || self.show_index)
{
let redirect_to = format!("{}/", req.path());
return Either::Left(ok(req.into_response( return Box::pin(ok(req.into_response(
HttpResponse::Found() HttpResponse::Found()
.header(header::LOCATION, redirect_to) .insert_header((header::LOCATION, redirect_to))
.body("") .finish(),
.into_body(), )));
))); }
let serve_named_file = |req: ServiceRequest, mut named_file: NamedFile| {
if let Some(ref mime_override) = self.mime_override {
let new_disposition = mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition;
} }
named_file.flags = self.file_flags;
let path = path.join(redir_index); let (req, _) = req.into_parts();
let res = named_file.into_response(&req);
Box::pin(ok(ServiceResponse::new(req, res)))
};
match NamedFile::open(path) { let show_index = |req: ServiceRequest| {
Ok(mut named_file) => { let dir = Directory::new(self.directory.clone(), path.clone());
if let Some(ref mime_override) = self.mime_override {
let new_disposition =
mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = self.file_flags;
let (req, _) = req.into_parts();
Either::Left(ok(match named_file.into_response(&req) {
Ok(item) => ServiceResponse::new(req, item),
Err(e) => ServiceResponse::from_err(e, req),
}))
}
Err(e) => self.handle_err(e, req),
}
} else if self.show_index {
let dir = Directory::new(self.directory.clone(), path);
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let x = (self.renderer)(&dir, &req); let x = (self.renderer)(&dir, &req);
match x { Box::pin(match x {
Ok(resp) => Either::Left(ok(resp)), Ok(resp) => ok(resp),
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), Err(err) => ok(ServiceResponse::from_err(err, req)),
} })
} else { };
Either::Left(ok(ServiceResponse::from_err(
match self.index {
Some(ref index) => match NamedFile::open(path.join(index)) {
Ok(named_file) => serve_named_file(req, named_file),
Err(_) if self.show_index => show_index(req),
Err(err) => self.handle_err(err, req),
},
None if self.show_index => show_index(req),
_ => Box::pin(ok(ServiceResponse::from_err(
FilesError::IsDirectory, FilesError::IsDirectory,
req.into_parts().0, req.into_parts().0,
))) ))),
} }
} else { } else {
match NamedFile::open(path) { match NamedFile::open(path) {
Ok(mut named_file) => { Ok(mut named_file) => {
if let Some(ref mime_override) = self.mime_override { if let Some(ref mime_override) = self.mime_override {
let new_disposition = let new_disposition = mime_override(&named_file.content_type.type_());
mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition; named_file.content_disposition.disposition = new_disposition;
} }
named_file.flags = self.file_flags; named_file.flags = self.file_flags;
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
match named_file.into_response(&req) { let res = named_file.into_response(&req);
Ok(item) => { Box::pin(ok(ServiceResponse::new(req, res)))
Either::Left(ok(ServiceResponse::new(req.clone(), item)))
}
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
}
} }
Err(e) => self.handle_err(e, req), Err(err) => self.handle_err(err, req),
} }
} }
} }

View File

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

View File

@ -0,0 +1 @@
first

View File

@ -0,0 +1 @@
second

View File

@ -0,0 +1,36 @@
use actix_files::Files;
use actix_web::{
guard::Host,
http::StatusCode,
test::{self, TestRequest},
App,
};
use bytes::Bytes;
#[actix_rt::test]
async fn test_guard_filter() {
let srv = test::init_service(
App::new()
.service(Files::new("/", "./tests/fixtures/guards/first").guard(Host("first.com")))
.service(
Files::new("/", "./tests/fixtures/guards/second").guard(Host("second.com")),
),
)
.await;
let req = TestRequest::with_uri("/index.txt")
.append_header(("Host", "first.com"))
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(test::read_body(res).await, Bytes::from("first"));
let req = TestRequest::with_uri("/index.txt")
.append_header(("Host", "second.com"))
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(test::read_body(res).await, Bytes::from("second"));
}

View File

@ -0,0 +1 @@
test.png

View File

@ -0,0 +1 @@
// this file is empty.

View File

@ -3,6 +3,20 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.4 - 2021-04-02
* Added `TestServer::client_headers` method. [#2097]
[#2097]: https://github.com/actix/actix-web/pull/2097
## 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 ## 3.0.0-beta.1 - 2021-01-07
* Update `bytes` to `1.0`. [#1813] * Update `bytes` to `1.0`. [#1813]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "3.0.0-beta.1" version = "3.0.0-beta.4"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Various helpers for Actix applications to use during testing" description = "Various helpers for Actix applications to use during testing"
readme = "README.md" readme = "README.md"
@ -26,30 +26,30 @@ path = "src/lib.rs"
default = [] default = []
# openssl # openssl
openssl = ["open-ssl", "awc/openssl"] openssl = ["tls-openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-service = "2.0.0-beta.2" actix-service = "2.0.0"
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0"
actix-tls = "3.0.0-beta.2" actix-tls = "3.0.0-beta.5"
actix-utils = "3.0.0-beta.1" actix-utils = "3.0.0"
actix-rt = "2.0.0-beta.1" actix-rt = "2.2"
actix-server = "2.0.0-beta.2" actix-server = "2.0.0-beta.3"
awc = "3.0.0-beta.1" awc = { version = "3.0.0-beta.6", default-features = false }
base64 = "0.13" base64 = "0.13"
bytes = "1" bytes = "1"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.7", default-features = false }
http = "0.2.2" http = "0.2.2"
log = "0.4" log = "0.4"
socket2 = "0.3" socket2 = "0.4"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
slab = "0.4" slab = "0.4"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
time = { version = "0.2.7", default-features = false, features = ["std"] } time = { version = "0.2.23", default-features = false, features = ["std"] }
open-ssl = { version = "0.10", package = "openssl", optional = true } tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
[dev-dependencies] [dev-dependencies]
actix-web = "4.0.0-beta.1" actix-web = { version = "4.0.0-beta.7", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.1" actix-http = "3.0.0-beta.7"

View File

@ -3,9 +3,9 @@
> Various helpers for Actix applications to use during testing. > 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) [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=2.1.0)](https://docs.rs/actix-http-test/2.1.0) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.4)](https://docs.rs/actix-http-test/3.0.0-beta.4)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-http-test) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
[![Dependency Status](https://deps.rs/crate/actix-http-test/2.1.0/status.svg)](https://deps.rs/crate/actix-http-test/2.1.0) [![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.4)
[![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) [![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 ## Documentation & Resources

View File

@ -4,13 +4,18 @@
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#[cfg(feature = "openssl")]
extern crate tls_openssl as openssl;
use std::sync::mpsc; use std::sync::mpsc;
use std::{net, thread, time}; use std::{net, thread, time};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::{net::TcpStream, System}; use actix_rt::{net::TcpStream, System};
use actix_server::{Server, ServiceFactory}; use actix_server::{Server, ServiceFactory};
use awc::{error::PayloadError, ws, Client, ClientRequest, ClientResponse, Connector}; use awc::{
error::PayloadError, http::HeaderMap, ws, Client, ClientRequest, ClientResponse, Connector,
};
use bytes::Bytes; use bytes::Bytes;
use futures_core::stream::Stream; use futures_core::stream::Stream;
use http::Method; use http::Method;
@ -23,7 +28,7 @@ use socket2::{Domain, Protocol, Socket, Type};
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use actix_http::HttpService; /// use actix_http::HttpService;
/// use actix_http_test::TestServer; /// use actix_http_test::TestServer;
/// use actix_web::{web, App, HttpResponse, Error}; /// use actix_web::{web, App, HttpResponse, Error};
@ -60,7 +65,7 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
// run server in separate thread // run server in separate thread
thread::spawn(move || { thread::spawn(move || {
let sys = System::new("actix-test-server"); let sys = System::new();
let local_addr = tcp.local_addr().unwrap(); let local_addr = tcp.local_addr().unwrap();
let srv = Server::build() let srv = Server::build()
@ -69,7 +74,7 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
.disable_signals(); .disable_signals();
sys.block_on(async { sys.block_on(async {
srv.start(); srv.run();
tx.send((System::current(), local_addr)).unwrap(); tx.send((System::current(), local_addr)).unwrap();
}); });
@ -82,7 +87,7 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
let connector = { let connector = {
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
{ {
use open_ssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
builder.set_verify(SslVerifyMode::NONE); builder.set_verify(SslVerifyMode::NONE);
@ -93,20 +98,17 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
.conn_lifetime(time::Duration::from_secs(0)) .conn_lifetime(time::Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000)) .timeout(time::Duration::from_millis(30000))
.ssl(builder.build()) .ssl(builder.build())
.finish()
} }
#[cfg(not(feature = "openssl"))] #[cfg(not(feature = "openssl"))]
{ {
Connector::new() Connector::new()
.conn_lifetime(time::Duration::from_secs(0)) .conn_lifetime(time::Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000)) .timeout(time::Duration::from_millis(30000))
.finish()
} }
}; };
Client::builder().connector(connector).finish() Client::builder().connector(connector).finish()
}; };
actix_tls::connect::start_default_resolver().await.unwrap();
TestServer { TestServer {
addr, addr,
@ -115,17 +117,6 @@ 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();
socket.bind(&addr.into()).unwrap();
socket.set_reuse_address(true).unwrap();
let tcp = socket.into_tcp_listener();
tcp.local_addr().unwrap()
}
/// Test server controller /// Test server controller
pub struct TestServer { pub struct TestServer {
addr: net::SocketAddr, addr: net::SocketAddr,
@ -148,7 +139,7 @@ impl TestServer {
} }
} }
/// Construct test https server url /// Construct test HTTPS server URL.
pub fn surl(&self, uri: &str) -> String { pub fn surl(&self, uri: &str) -> String {
if uri.starts_with('/') { if uri.starts_with('/') {
format!("https://localhost:{}{}", self.addr.port(), uri) format!("https://localhost:{}{}", self.addr.port(), uri)
@ -162,7 +153,7 @@ impl TestServer {
self.client.get(self.url(path.as_ref()).as_str()) 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 { pub fn sget<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.get(self.surl(path.as_ref()).as_str()) self.client.get(self.surl(path.as_ref()).as_str())
} }
@ -172,7 +163,7 @@ impl TestServer {
self.client.post(self.url(path.as_ref()).as_str()) 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 { pub fn spost<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.post(self.surl(path.as_ref()).as_str()) self.client.post(self.surl(path.as_ref()).as_str())
} }
@ -182,7 +173,7 @@ impl TestServer {
self.client.head(self.url(path.as_ref()).as_str()) 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 { pub fn shead<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.head(self.surl(path.as_ref()).as_str()) self.client.head(self.surl(path.as_ref()).as_str())
} }
@ -192,7 +183,7 @@ impl TestServer {
self.client.put(self.url(path.as_ref()).as_str()) 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 { pub fn sput<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.put(self.surl(path.as_ref()).as_str()) self.client.put(self.surl(path.as_ref()).as_str())
} }
@ -202,7 +193,7 @@ impl TestServer {
self.client.patch(self.url(path.as_ref()).as_str()) 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 { pub fn spatch<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.patch(self.surl(path.as_ref()).as_str()) self.client.patch(self.surl(path.as_ref()).as_str())
} }
@ -212,7 +203,7 @@ impl TestServer {
self.client.delete(self.url(path.as_ref()).as_str()) 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 { pub fn sdelete<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.delete(self.surl(path.as_ref()).as_str()) self.client.delete(self.surl(path.as_ref()).as_str())
} }
@ -222,12 +213,12 @@ impl TestServer {
self.client.options(self.url(path.as_ref()).as_str()) 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 { pub fn soptions<S: AsRef<str>>(&self, path: S) -> ClientRequest {
self.client.options(self.surl(path.as_ref()).as_str()) 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 { pub fn request<S: AsRef<str>>(&self, method: Method, path: S) -> ClientRequest {
self.client.request(method, path.as_ref()) self.client.request(method, path.as_ref())
} }
@ -242,26 +233,32 @@ impl TestServer {
response.body().limit(10_485_760).await 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( pub async fn ws_at(
&mut self, &mut self,
path: &str, 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 url = self.url(path);
let connect = self.client.ws(url).connect(); let connect = self.client.ws(url).connect();
connect.await.map(|(_, framed)| framed) connect.await.map(|(_, framed)| framed)
} }
/// Connect to a websocket server /// Connect to a WebSocket server.
pub async fn ws( pub async fn ws(
&mut self, &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 self.ws_at("/").await
} }
/// Stop http server /// Get default HeaderMap of Client.
///
/// Returns Some(&mut HeaderMap) when Client object is unique
/// (No other clone of client exists at the same time).
pub fn client_headers(&mut self) -> Option<&mut HeaderMap> {
self.client.headers()
}
/// Stop HTTP server
fn stop(&mut self) { fn stop(&mut self) {
self.system.stop(); self.system.stop();
} }
@ -272,3 +269,13 @@ impl Drop for TestServer {
self.stop() self.stop()
} }
} }
/// Get a localhost socket address with random, unused port.
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();
socket.bind(&addr.into()).unwrap();
socket.set_reuse_address(true).unwrap();
let tcp = net::TcpListener::from(socket);
tcp.local_addr().unwrap()
}

View File

@ -1,6 +1,177 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
### Changed
* Change compression algorithm features flags. [#2250]
[#2250]: https://github.com/actix/actix-web/pull/2250
## 3.0.0-beta.7 - 2021-06-17
### Added
* Alias `body::Body` as `body::AnyBody`. [#2215]
* `BoxAnyBody`: a boxed message body with boxed errors. [#2183]
* Re-export `http` crate's `Error` type as `error::HttpError`. [#2171]
* Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171]
* Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171]
* `Response::into_body` that consumes response and returns body type. [#2201]
* `impl Default` for `Response`. [#2201]
* Add zstd support for `ContentEncoding`. [#2244]
### Changed
* The `MessageBody` trait now has an associated `Error` type. [#2183]
* All error trait bounds in server service builders have changed from `Into<Error>` to `Into<Response<AnyBody>>`. [#2253]
* All error trait bounds in message body and stream impls changed from `Into<Error>` to `Into<Box<dyn std::error::Error>>`. [#2253]
* Places in `Response` where `ResponseBody<B>` was received or returned now simply use `B`. [#2201]
* `header` mod is now public. [#2171]
* `uri` mod is now public. [#2171]
* Update `language-tags` to `0.3`.
* Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201]
* `ResponseBuilder::message_body` now returns a `Result`. [#2201]
* Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253]
* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
### Removed
* Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171]
* Down-casting for `MessageBody` types. [#2183]
* `error::Result` alias. [#2201]
* Error field from `Response` and `Response::error`. [#2205]
* `impl Future` for `Response`. [#2201]
* `Response::take_body` and old `Response::into_body` method that casted body type. [#2201]
* `InternalError` and all the error types it constructed. [#2215]
* Conversion (`impl Into`) of `Response<Body>` and `ResponseBuilder` to `Error`. [#2215]
[#2171]: https://github.com/actix/actix-web/pull/2171
[#2183]: https://github.com/actix/actix-web/pull/2183
[#2196]: https://github.com/actix/actix-web/pull/2196
[#2201]: https://github.com/actix/actix-web/pull/2201
[#2205]: https://github.com/actix/actix-web/pull/2205
[#2215]: https://github.com/actix/actix-web/pull/2215
[#2253]: https://github.com/actix/actix-web/pull/2253
[#2244]: https://github.com/actix/actix-web/pull/2244
## 3.0.0-beta.6 - 2021-04-17
### Added
* `impl<T: MessageBody> MessageBody for Pin<Box<T>>`. [#2152]
* `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159]
* Helper `body::to_bytes` for async collecting message body into Bytes. [#2158]
### Changes
* The type parameter of `Response` no longer has a default. [#2152]
* The `Message` variant of `body::Body` is now `Pin<Box<dyn MessageBody>>`. [#2152]
* `BodyStream` and `SizedStream` are no longer restricted to Unpin types. [#2152]
* Error enum types are marked `#[non_exhaustive]`. [#2161]
### Removed
* `cookies` feature flag. [#2065]
* Top-level `cookies` mod (re-export). [#2065]
* `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065]
* `impl ResponseError for CookieParseError`. [#2065]
* Deprecated methods on `ResponseBuilder`: `if_true`, `if_some`. [#2148]
* `ResponseBuilder::json`. [#2148]
* `ResponseBuilder::{set_header, header}`. [#2148]
* `impl From<serde_json::Value> for Body`. [#2148]
* `Response::build_from`. [#2159]
* Most of the status code builders on `Response`. [#2159]
[#2065]: https://github.com/actix/actix-web/pull/2065
[#2148]: https://github.com/actix/actix-web/pull/2148
[#2152]: https://github.com/actix/actix-web/pull/2152
[#2159]: https://github.com/actix/actix-web/pull/2159
[#2158]: https://github.com/actix/actix-web/pull/2158
[#2161]: https://github.com/actix/actix-web/pull/2161
## 3.0.0-beta.5 - 2021-04-02
### Added
* `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081]
* `client::ConnectorService` as `client::Connector::finish` method's return type [#2081]
* `client::ConnectionIo` trait alias [#2081]
### Changed
* `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063]
### Removed
* Common typed HTTP headers were moved to actix-web. [2094]
* `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127]
[#2063]: https://github.com/actix/actix-web/pull/2063
[#2081]: https://github.com/actix/actix-web/pull/2081
[#2094]: https://github.com/actix/actix-web/pull/2094
[#2127]: https://github.com/actix/actix-web/pull/2127
## 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 ## 3.0.0-beta.1 - 2021-01-07
@ -22,10 +193,14 @@
* Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`. * Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
due to the removal of this type from `tokio-openssl` crate. openssl handshake due to the removal of this type from `tokio-openssl` crate. openssl handshake
error would return as `ConnectError::SslError`. [#1813] 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 [#1813]: https://github.com/actix/actix-web/pull/1813
[#1857]: https://github.com/actix/actix-web/pull/1857 [#1857]: https://github.com/actix/actix-web/pull/1857
[#1864]: https://github.com/actix/actix-web/pull/1864 [#1864]: https://github.com/actix/actix-web/pull/1864
[#1878]: https://github.com/actix/actix-web/pull/1878
## 2.2.0 - 2020-11-25 ## 2.2.0 - 2020-11-25
@ -72,15 +247,14 @@
* Update actix-connect and actix-tls dependencies. * Update actix-connect and actix-tls dependencies.
## [2.0.0-beta.3] - 2020-08-14 ## 2.0.0-beta.3 - 2020-08-14
### Fixed ### Fixed
* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626] * Memory leak of `client::pool::ConnectorPoolSupport`. [#1626]
[#1626]: https://github.com/actix/actix-web/pull/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 ### Fixed
* Potential UB in h1 decoder using uninitialized memory. [#1614] * Potential UB in h1 decoder using uninitialized memory. [#1614]
@ -91,10 +265,8 @@
[#1615]: https://github.com/actix/actix-web/pull/1615 [#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 ### Changed
* Migrate cookie handling to `cookie` crate. [#1558] * Migrate cookie handling to `cookie` crate. [#1558]
* Update `sha-1` to 0.9. [#1586] * Update `sha-1` to 0.9. [#1586]
* Fix leak in client pool. [#1580] * Fix leak in client pool. [#1580]
@ -104,33 +276,30 @@
[#1586]: https://github.com/actix/actix-web/pull/1586 [#1586]: https://github.com/actix/actix-web/pull/1586
[#1580]: https://github.com/actix/actix-web/pull/1580 [#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 ### Changed
* Bump minimum supported Rust version to 1.40 * 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 * `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
`u64` instead of a `usize`. `u64` instead of a `usize`.
* Update `base64` dependency to 0.12 * Update `base64` dependency to 0.12
### Fixed ### Fixed
* Support parsing of `SameSite=None` [#1503] * Support parsing of `SameSite=None` [#1503]
[#1439]: https://github.com/actix/actix-web/pull/1439 [#1439]: https://github.com/actix/actix-web/pull/1439
[#1503]: https://github.com/actix/actix-web/pull/1503 [#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 ### Fixed
* Correct spelling of ConnectError::Unresolved [#1487] * Correct spelling of ConnectError::Unresolved [#1487]
* Fix a mistake in the encoding of websocket continuation messages wherein * Fix a mistake in the encoding of websocket continuation messages wherein
Item::FirstText and Item::FirstBinary are each encoded as the other. Item::FirstText and Item::FirstBinary are each encoded as the other.
### Changed ### Changed
* Implement `std::error::Error` for our custom errors [#1422] * Implement `std::error::Error` for our custom errors [#1422]
* Remove `failure` support for `ResponseError` since that crate * Remove `failure` support for `ResponseError` since that crate
will be deprecated in the near future. will be deprecated in the near future.
@ -138,338 +307,247 @@
[#1422]: https://github.com/actix/actix-web/pull/1422 [#1422]: https://github.com/actix/actix-web/pull/1422
[#1487]: https://github.com/actix/actix-web/pull/1487 [#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 ### Changed
* Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395] * 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
* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB respectively respectively to improve download speed for awc when downloading large objects. [#1394]
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 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] * 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 [#1394]: https://github.com/actix/actix-web/pull/1394
[#1395]: https://github.com/actix/actix-web/pull/1395 [#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 ### Changed
* Update the `time` dependency to 0.2.7. * Update the `time` dependency to 0.2.7.
* Moved actors messages support from actix crate, enabled with feature `actors`. * 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. * MessageBody is not implemented for &'static [u8] anymore.
### Fixed ### Fixed
* Allow `SameSite=None` cookies to be sent in a response. * Allow `SameSite=None` cookies to be sent in a response.
## [1.0.1] - 2019-12-20
## 1.0.1 - 2019-12-20
### Fixed ### Fixed
* Poll upgrade service's readiness from HTTP service handlers * Poll upgrade service's readiness from HTTP service handlers
* Replace brotli with brotli2 #1224 * Replace brotli with brotli2 #1224
## [1.0.0] - 2019-12-13
## 1.0.0 - 2019-12-13
### Added ### Added
* Add websockets continuation frame support * Add websockets continuation frame support
### Changed ### Changed
* Replace `flate2-xxx` features with `compress` * Replace `flate2-xxx` features with `compress`
## [1.0.0-alpha.5] - 2019-12-09
## 1.0.0-alpha.5 - 2019-12-09
### Fixed ### Fixed
* Check `Upgrade` service readiness before calling it * Check `Upgrade` service readiness before calling it
* Fix buffer remaining capacity calculation
* Fix buffer remaining capacity calcualtion
### Changed ### Changed
* Websockets: Ping and Pong should have binary data #1049 * 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 ### Added
* Add impl ResponseBuilder for Error * Add impl ResponseBuilder for Error
### Changed ### Changed
* Use rust based brotli compression library * Use rust based brotli compression library
## [1.0.0-alpha.3] - 2019-12-07 ## 1.0.0-alpha.3 - 2019-12-07
### Changed ### Changed
* Migrate to tokio 0.2 * Migrate to tokio 0.2
* Migrate to `std::future` * Migrate to `std::future`
## [0.2.11] - 2019-11-06 ## 0.2.11 - 2019-11-06
### Added ### Added
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body() * 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
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151) `actix_files::NamedFile` to be more compatible. (#1151)
* Allow to use `std::convert::Infallible` as `actix_http::error::Error` * Allow to use `std::convert::Infallible` as `actix_http::error::Error`
### Fixed ### 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 ### Added
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests with `RequestHead` with `RequestHead`
### Fixed ### Fixed
* h2 will use error response #1080 * h2 will use error response #1080
* on_connect result isn't added to request extensions for http2 requests #1009 * 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 ### Changed
* Dropped the `byteorder`-dependency in favor of `stdlib`-implementation * Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
* Update percent-encoding to 2.1 * Update percent-encoding to 2.1
* Update serde_urlencoded to 0.6.1 * Update serde_urlencoded to 0.6.1
### Fixed ### Fixed
* Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031) * 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 ### Added
* Add `rustls` support * Add `rustls` support
* Add `Clone` impl for `HeaderMap` * Add `Clone` impl for `HeaderMap`
### Fixed ### Fixed
* awc client panic #1016 * awc client panic #1016
* Invalid response with compression middleware enabled, but compression-related features
* Invalid response with compression middleware enabled, but compression-related features disabled #997 disabled #997
## [0.2.7] - 2019-07-18 ## 0.2.7 - 2019-07-18
### Added ### Added
* Add support for downcasting response errors #986 * Add support for downcasting response errors #986
## [0.2.6] - 2019-07-17 ## 0.2.6 - 2019-07-17
### Changed ### Changed
* Replace `ClonableService` with local copy * Replace `ClonableService` with local copy
* Upgrade `rand` dependency version to 0.7 * Upgrade `rand` dependency version to 0.7
## [0.2.5] - 2019-06-28 ## 0.2.5 - 2019-06-28
### Added ### Added
* Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946 * Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946
### Changed ### Changed
* Use `encoding_rs` crate instead of unmaintained `encoding` crate * Use `encoding_rs` crate instead of unmaintained `encoding` crate
* Add `Copy` and `Clone` impls for `ws::Codec` * Add `Copy` and `Clone` impls for `ws::Codec`
## [0.2.4] - 2019-06-16 ## 0.2.4 - 2019-06-16
### Fixed ### Fixed
* Do not compress NoContent (204) responses #918 * Do not compress NoContent (204) responses #918
## [0.2.3] - 2019-06-02 ## 0.2.3 - 2019-06-02
### Added ### Added
* Debug impl for ResponseBuilder * Debug impl for ResponseBuilder
* From SizedStream and BodyStream for Body * From SizedStream and BodyStream for Body
### Changed ### Changed
* SizedStream uses u64 * SizedStream uses u64
## [0.2.2] - 2019-05-29 ## 0.2.2 - 2019-05-29
### Fixed ### Fixed
* Parse incoming stream before closing stream on disconnect #868 * Parse incoming stream before closing stream on disconnect #868
## [0.2.1] - 2019-05-25 ## 0.2.1 - 2019-05-25
### Fixed ### Fixed
* Handle socket read disconnect * Handle socket read disconnect
## [0.2.0] - 2019-05-12 ## 0.2.0 - 2019-05-12
### Changed ### Changed
* Update actix-service to 0.4 * Update actix-service to 0.4
* Expect and upgrade services accept `ServerConfig` config. * Expect and upgrade services accept `ServerConfig` config.
### Deleted ### Deleted
* `OneRequest` service * `OneRequest` service
## [0.1.5] - 2019-05-04 ## 0.1.5 - 2019-05-04
### Fixed ### Fixed
* Clean up response extensions in response pool #817 * Clean up response extensions in response pool #817
## [0.1.4] - 2019-04-24 ## 0.1.4 - 2019-04-24
### Added ### Added
* Allow to render h1 request headers in `Camel-Case` * Allow to render h1 request headers in `Camel-Case`
### Fixed ### Fixed
* Read until eof for http/1.0 responses #771 * Read until eof for http/1.0 responses #771
## [0.1.3] - 2019-04-23 ## 0.1.3 - 2019-04-23
### Fixed ### Fixed
* Fix http client pool management * Fix http client pool management
* Fix http client wait queue management #794 * Fix http client wait queue management #794
## [0.1.2] - 2019-04-23 ## 0.1.2 - 2019-04-23
### Fixed ### Fixed
* Fix BorrowMutError panic in client connector #793 * Fix BorrowMutError panic in client connector #793
## [0.1.1] - 2019-04-19 ## 0.1.1 - 2019-04-19
### Changed ### Changed
* Cookie::max_age() accepts value in seconds * Cookie::max_age() accepts value in seconds
* Cookie::max_age_time() accepts value in time::Duration * Cookie::max_age_time() accepts value in time::Duration
* Allow to specify server address for client connector * Allow to specify server address for client connector
## [0.1.0] - 2019-04-16 ## 0.1.0 - 2019-04-16
### Added ### Added
* Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr` * Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr`
### Changed ### Changed
* `actix_http::encoding` always available * `actix_http::encoding` always available
* use trust-dns-resolver 0.11.0 * use trust-dns-resolver 0.11.0
## [0.1.0-alpha.5] - 2019-04-12 ## 0.1.0-alpha.5 - 2019-04-12
### Added ### Added
* Allow to use custom service for upgrade requests * Allow to use custom service for upgrade requests
* Added `h1::SendResponse` future. * Added `h1::SendResponse` future.
### Changed ### Changed
* MessageBody::length() renamed to MessageBody::size() for consistency * MessageBody::length() renamed to MessageBody::size() for consistency
* ws handshake verification functions take RequestHead instead of Request * 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 ### Added
* Allow to use custom `Expect` handler * Allow to use custom `Expect` handler
* Add minimal `std::error::Error` impl for `Error` * Add minimal `std::error::Error` impl for `Error`
### Changed ### Changed
* Export IntoHeaderValue * Export IntoHeaderValue
* Render error and return as response body * Render error and return as response body
* Use thread pool for response body compression
* Use thread pool for response body comression
### Deleted ### Deleted
* Removed PayloadBuffer * Removed PayloadBuffer
## [0.1.0-alpha.3] - 2019-04-02 ## 0.1.0-alpha.3 - 2019-04-02
### Added ### Added
* Warn when an unsealed private cookie isn't valid UTF-8 * Warn when an unsealed private cookie isn't valid UTF-8
### Fixed ### Fixed
* Rust 1.31.0 compatibility * Rust 1.31.0 compatibility
* Preallocate read buffer for h1 codec * Preallocate read buffer for h1 codec
* Detect socket disconnection during protocol selection * Detect socket disconnection during protocol selection
## [0.1.0-alpha.2] - 2019-03-29 ## 0.1.0-alpha.2 - 2019-03-29
### Added ### Added
* Added ws::Message::Nop, no-op websockets message * Added ws::Message::Nop, no-op websockets message
### Changed ### Changed
* Do not use thread pool for decompression if chunk size is smaller than 2048.
* Do not use thread pool for decomression 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 * Initial impl

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0-beta.1" version = "3.0.0-beta.7"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "HTTP primitives for the Actix ecosystem" description = "HTTP primitives for the Actix ecosystem"
readme = "README.md" readme = "README.md"
@ -15,7 +15,8 @@ license = "MIT OR Apache-2.0"
edition = "2018" edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = ["openssl", "rustls", "compress", "secure-cookies", "actors"] # features that docs.rs will build with
features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
[lib] [lib]
name = "actix_http" name = "actix_http"
@ -30,70 +31,78 @@ openssl = ["actix-tls/openssl"]
# rustls support # rustls support
rustls = ["actix-tls/rustls"] rustls = ["actix-tls/rustls"]
# enable compressison support # enable compression support
compress = ["flate2", "brotli2"] compress-brotli = ["brotli2", "__compress"]
compress-gzip = ["flate2", "__compress"]
compress-zstd = ["zstd", "__compress"]
# support for secure cookies # trust-dns as client dns resolver
secure-cookies = ["cookie/secure"] trust-dns = ["trust-dns-resolver"]
# support for actix Actor messages # Internal (PRIVATE!) features used to aid testing and cheking feature status.
actors = ["actix"] # Don't rely on these whatsoever. They may disappear at anytime.
__compress = []
[dependencies] [dependencies]
actix-service = "2.0.0-beta.2" actix-service = "2.0.0"
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0"
actix-utils = "3.0.0-beta.1" actix-utils = "3.0.0"
actix-rt = "2.0.0-beta.1" actix-rt = "2.2"
actix-threadpool = "0.3.1" actix-tls = { version = "3.0.0-beta.5", features = ["accept", "connect"] }
actix-tls = "3.0.0-beta.2"
actix = { version = "0.11.0-beta.1", optional = true }
ahash = "0.7"
base64 = "0.13" base64 = "0.13"
bitflags = "1.2" bitflags = "1.2"
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
cookie = { version = "0.14.1", features = ["percent-encode"] }
copyless = "0.1.4"
derive_more = "0.99.5" derive_more = "0.99.5"
either = "1.5.3"
encoding_rs = "0.8" encoding_rs = "0.8"
futures-channel = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-core = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
futures-util = { version = "0.3.7", default-features = false, features = ["sink"] } h2 = "0.3.1"
fxhash = "0.2.1"
h2 = "0.3.0"
http = "0.2.2" http = "0.2.2"
httparse = "1.3" httparse = "1.3"
indexmap = "1.3"
itoa = "0.4" itoa = "0.4"
lazy_static = "1.4" language-tags = "0.3"
language-tags = "0.2" local-channel = "0.1"
once_cell = "1.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project = "1.0.0" pin-project = "1.0.0"
pin-project-lite = "0.2"
rand = "0.8" rand = "0.8"
regex = "1.3" regex = "1.3"
serde = "1.0" serde = "1.0"
serde_json = "1.0"
sha-1 = "0.9" sha-1 = "0.9"
slab = "0.4" smallvec = "1.6"
serde_urlencoded = "0.7" time = { version = "0.2.23", default-features = false, features = ["std"] }
time = { version = "0.2.7", default-features = false, features = ["std"] } tokio = { version = "1.2", features = ["sync"] }
# compression # compression
brotli2 = { version="0.3.2", optional = true } brotli2 = { version="0.3.2", optional = true }
flate2 = { version = "1.0.13", optional = true } flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.7", optional = true }
trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies] [dev-dependencies]
actix-server = "2.0.0-beta.2" actix-server = "2.0.0-beta.3"
actix-http-test = { version = "3.0.0-beta.1", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] }
actix-tls = { version = "3.0.0-beta.2", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] }
criterion = "0.3" async-stream = "0.3"
env_logger = "0.7" criterion = { version = "0.3", features = ["html_reports"] }
serde_derive = "1.0" env_logger = "0.8"
open-ssl = { version="0.10", package = "openssl" } rcgen = "0.8"
rust-tls = { version="0.19", package = "rustls" } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tls-openssl = { version = "0.10", package = "openssl" }
tls-rustls = { version = "0.19", package = "rustls" }
webpki = { version = "0.21.0" }
[[example]]
name = "ws"
required-features = ["rustls"]
[[bench]] [[bench]]
name = "write-camel-case" name = "write-camel-case"

View File

@ -3,10 +3,13 @@
> HTTP primitives for the Actix ecosystem. > HTTP primitives for the Actix ecosystem.
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=2.2.0)](https://docs.rs/actix-http/2.2.0) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.7)](https://docs.rs/actix-http/3.0.0-beta.7)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-http) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
[![Dependency Status](https://deps.rs/crate/actix-http/2.2.0/status.svg)](https://deps.rs/crate/actix-http/2.2.0) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
[![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) <br />
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.7)
[![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 ## Documentation & Resources

View File

@ -1,19 +1,17 @@
use std::{env, io}; use std::io;
use actix_http::{Error, HttpService, Request, Response}; use actix_http::{http::StatusCode, Error, HttpService, Request, Response};
use actix_server::Server; use actix_server::Server;
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::StreamExt; use futures_util::StreamExt as _;
use http::header::HeaderValue; use http::header::HeaderValue;
use log::info;
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "echo=info"); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
env_logger::init();
Server::build() Server::build()
.bind("echo", "127.0.0.1:8080", || { .bind("echo", ("127.0.0.1", 8080), || {
HttpService::build() HttpService::build()
.client_timeout(1000) .client_timeout(1000)
.client_disconnect(1000) .client_disconnect(1000)
@ -23,10 +21,14 @@ async fn main() -> io::Result<()> {
body.extend_from_slice(&item?); body.extend_from_slice(&item?);
} }
info!("request body: {:?}", body); log::info!("request body: {:?}", body);
Ok::<_, Error>( Ok::<_, Error>(
Response::Ok() Response::build(StatusCode::OK)
.header("x-head", HeaderValue::from_static("dummy value!")) .insert_header((
"x-head",
HeaderValue::from_static("dummy value!"),
))
.body(body), .body(body),
) )
}) })

View File

@ -1,31 +1,30 @@
use std::{env, io}; use std::io;
use actix_http::http::HeaderValue; use actix_http::{body::Body, http::HeaderValue, http::StatusCode};
use actix_http::{Error, HttpService, Request, Response}; use actix_http::{Error, HttpService, Request, Response};
use actix_server::Server; use actix_server::Server;
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::StreamExt; use futures_util::StreamExt as _;
use log::info;
async fn handle_request(mut req: Request) -> Result<Response, Error> { async fn handle_request(mut req: Request) -> Result<Response<Body>, Error> {
let mut body = BytesMut::new(); let mut body = BytesMut::new();
while let Some(item) = req.payload().next().await { while let Some(item) = req.payload().next().await {
body.extend_from_slice(&item?) body.extend_from_slice(&item?)
} }
info!("request body: {:?}", body); log::info!("request body: {:?}", body);
Ok(Response::Ok()
.header("x-head", HeaderValue::from_static("dummy value!")) Ok(Response::build(StatusCode::OK)
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
.body(body)) .body(body))
} }
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "echo=info"); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
env_logger::init();
Server::build() Server::build()
.bind("echo", "127.0.0.1:8080", || { .bind("echo", ("127.0.0.1", 8080), || {
HttpService::build().finish(handle_request).tcp() HttpService::build().finish(handle_request).tcp()
})? })?
.run() .run()

View File

@ -1,26 +1,28 @@
use std::{env, io}; use std::{convert::Infallible, io};
use actix_http::{HttpService, Response}; use actix_http::{http::StatusCode, HttpService, Response};
use actix_server::Server; use actix_server::Server;
use futures_util::future;
use http::header::HeaderValue; use http::header::HeaderValue;
use log::info;
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "hello_world=info"); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
env_logger::init();
Server::build() Server::build()
.bind("hello-world", "127.0.0.1:8080", || { .bind("hello-world", ("127.0.0.1", 8080), || {
HttpService::build() HttpService::build()
.client_timeout(1000) .client_timeout(1000)
.client_disconnect(1000) .client_disconnect(1000)
.finish(|_req| { .finish(|req| async move {
info!("{:?}", _req); log::info!("{:?}", req);
let mut res = Response::Ok();
res.header("x-head", HeaderValue::from_static("dummy value!")); let mut res = Response::build(StatusCode::OK);
future::ok::<_, ()>(res.body("Hello world!")) res.insert_header((
"x-head",
HeaderValue::from_static("dummy value!"),
));
Ok::<_, Infallible>(res.body("Hello world!"))
}) })
.tcp() .tcp()
})? })?

View File

@ -0,0 +1,40 @@
//! Example showing response body (chunked) stream erroring.
//!
//! Test using `nc` or `curl`.
//! ```sh
//! $ curl -vN 127.0.0.1:8080
//! $ echo 'GET / HTTP/1.1\n\n' | nc 127.0.0.1 8080
//! ```
use std::{convert::Infallible, io, time::Duration};
use actix_http::{body::BodyStream, HttpService, Response};
use actix_server::Server;
use async_stream::stream;
use bytes::Bytes;
#[actix_rt::main]
async fn main() -> io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
Server::build()
.bind("streaming-error", ("127.0.0.1", 8080), || {
HttpService::build()
.finish(|req| async move {
log::info!("{:?}", req);
let res = Response::ok();
Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {
yield Ok(Bytes::from("123"));
yield Ok(Bytes::from("456"));
actix_rt::time::sleep(Duration::from_millis(1000)).await;
yield Err(io::Error::new(io::ErrorKind::Other, ""));
})))
})
.tcp()
})?
.run()
.await
}

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

@ -0,0 +1,106 @@
//! 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::{
io,
pin::Pin,
task::{Context, Poll},
time::Duration,
};
use actix_codec::Encoder;
use actix_http::{body::BodyStream, 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_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
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<BodyStream<Heartbeat>>, Error> {
log::info!("handshaking");
let mut res = ws::handshake(req.head())?;
// handshake will always fail under HTTP/2
log::info!("responding");
Ok(res.message_body(BodyStream::new(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,710 +0,0 @@
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{fmt, mem};
use bytes::{Bytes, BytesMut};
use futures_core::{ready, Stream};
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(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) => 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),
}
}
}
/// 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)
}
}
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.
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;
return Poll::Ready(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)),
});
}
}
}
/// Type represent streaming body. This body implementation should be used
/// if total size of stream is known. Data get sent as is without using transfer encoding.
pub struct SizedStream<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;
return Poll::Ready(match ready!(Pin::new(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());
}
}

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

@ -0,0 +1,233 @@
use std::{
borrow::Cow,
error::Error as StdError,
fmt, mem,
pin::Pin,
task::{Context, Poll},
};
use bytes::{Bytes, BytesMut};
use futures_core::{ready, Stream};
use crate::error::Error;
use super::{BodySize, BodyStream, MessageBody, MessageBodyMapErr, SizedStream};
pub type Body = AnyBody;
/// Represents various types of HTTP message body.
pub enum AnyBody {
/// 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(BoxAnyBody),
}
impl AnyBody {
/// Create body from slice (copy)
pub fn from_slice(s: &[u8]) -> Self {
Self::Bytes(Bytes::copy_from_slice(s))
}
/// Create body from generic message body.
pub fn from_message<B>(body: B) -> Self
where
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError + 'static>>,
{
Self::Message(BoxAnyBody::from_body(body))
}
}
impl MessageBody for AnyBody {
type Error = Error;
fn size(&self) -> BodySize {
match self {
AnyBody::None => BodySize::None,
AnyBody::Empty => BodySize::Empty,
AnyBody::Bytes(ref bin) => BodySize::Sized(bin.len() as u64),
AnyBody::Message(ref body) => body.size(),
}
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
match self.get_mut() {
AnyBody::None => Poll::Ready(None),
AnyBody::Empty => Poll::Ready(None),
AnyBody::Bytes(ref mut bin) => {
let len = bin.len();
if len == 0 {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(bin))))
}
}
// TODO: MSRV 1.51: poll_map_err
AnyBody::Message(body) => match ready!(body.as_pin_mut().poll_next(cx)) {
Some(Err(err)) => {
Poll::Ready(Some(Err(Error::new_body().with_cause(err))))
}
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
},
}
}
}
impl PartialEq for AnyBody {
fn eq(&self, other: &Body) -> bool {
match *self {
AnyBody::None => matches!(*other, AnyBody::None),
AnyBody::Empty => matches!(*other, AnyBody::Empty),
AnyBody::Bytes(ref b) => match *other {
AnyBody::Bytes(ref b2) => b == b2,
_ => false,
},
AnyBody::Message(_) => false,
}
}
}
impl fmt::Debug for AnyBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
AnyBody::None => write!(f, "AnyBody::None"),
AnyBody::Empty => write!(f, "AnyBody::Empty"),
AnyBody::Bytes(ref b) => write!(f, "AnyBody::Bytes({:?})", b),
AnyBody::Message(_) => write!(f, "AnyBody::Message(_)"),
}
}
}
impl From<&'static str> for AnyBody {
fn from(s: &'static str) -> Body {
AnyBody::Bytes(Bytes::from_static(s.as_ref()))
}
}
impl From<&'static [u8]> for AnyBody {
fn from(s: &'static [u8]) -> Body {
AnyBody::Bytes(Bytes::from_static(s))
}
}
impl From<Vec<u8>> for AnyBody {
fn from(vec: Vec<u8>) -> Body {
AnyBody::Bytes(Bytes::from(vec))
}
}
impl From<String> for AnyBody {
fn from(s: String) -> Body {
s.into_bytes().into()
}
}
impl From<&'_ String> for AnyBody {
fn from(s: &String) -> Body {
AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s)))
}
}
impl From<Cow<'_, str>> for AnyBody {
fn from(s: Cow<'_, str>) -> Body {
match s {
Cow::Owned(s) => AnyBody::from(s),
Cow::Borrowed(s) => {
AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)))
}
}
}
}
impl From<Bytes> for AnyBody {
fn from(s: Bytes) -> Body {
AnyBody::Bytes(s)
}
}
impl From<BytesMut> for AnyBody {
fn from(s: BytesMut) -> Body {
AnyBody::Bytes(s.freeze())
}
}
impl<S, E> From<SizedStream<S>> for AnyBody
where
S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Box<dyn StdError>> + 'static,
{
fn from(s: SizedStream<S>) -> Body {
AnyBody::from_message(s)
}
}
impl<S, E> From<BodyStream<S>> for AnyBody
where
S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Box<dyn StdError>> + 'static,
{
fn from(s: BodyStream<S>) -> Body {
AnyBody::from_message(s)
}
}
/// A boxed message body with boxed errors.
pub struct BoxAnyBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError + 'static>>>>);
impl BoxAnyBody {
/// Boxes a `MessageBody` and any errors it generates.
pub fn from_body<B>(body: B) -> Self
where
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError + 'static>>,
{
let body = MessageBodyMapErr::new(body, Into::into);
Self(Box::pin(body))
}
/// Returns a mutable pinned reference to the inner message body type.
pub fn as_pin_mut(
&mut self,
) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError + 'static>>)> {
self.0.as_mut()
}
}
impl fmt::Debug for BoxAnyBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("BoxAnyBody(dyn MessageBody)")
}
}
impl MessageBody for BoxAnyBody {
type Error = Error;
fn size(&self) -> BodySize {
self.0.size()
}
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
// TODO: MSRV 1.51: poll_map_err
match ready!(self.0.as_mut().poll_next(cx)) {
Some(Err(err)) => Poll::Ready(Some(Err(Error::new_body().with_cause(err)))),
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
}
}
}

View File

@ -0,0 +1,171 @@
use std::{
error::Error as StdError,
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use futures_core::{ready, Stream};
use pin_project_lite::pin_project;
use super::{BodySize, MessageBody};
pin_project! {
/// Streaming response wrapper.
///
/// Response does not contain `Content-Length` header and appropriate transfer encoding is used.
pub struct BodyStream<S> {
#[pin]
stream: S,
}
}
impl<S, E> BodyStream<S>
where
S: Stream<Item = Result<Bytes, E>>,
E: Into<Box<dyn StdError>> + 'static,
{
pub fn new(stream: S) -> Self {
BodyStream { stream }
}
}
impl<S, E> MessageBody for BodyStream<S>
where
S: Stream<Item = Result<Bytes, E>>,
E: Into<Box<dyn StdError>> + 'static,
{
type Error = E;
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, Self::Error>>> {
loop {
let stream = self.as_mut().project().stream;
let chunk = match ready!(stream.poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
opt => opt,
};
return Poll::Ready(chunk);
}
}
}
#[cfg(test)]
mod tests {
use std::{convert::Infallible, time::Duration};
use actix_rt::{
pin,
time::{sleep, Sleep},
};
use actix_utils::future::poll_fn;
use derive_more::{Display, Error};
use futures_core::ready;
use futures_util::{stream, FutureExt as _};
use super::*;
use crate::body::to_bytes;
#[actix_rt::test]
async fn skips_empty_chunks() {
let body = BodyStream::new(stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok::<_, Infallible>(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 read_to_bytes() {
let body = BodyStream::new(stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
));
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
}
#[derive(Debug, Display, Error)]
#[display(fmt = "stream error")]
struct StreamErr;
#[actix_rt::test]
async fn stream_immediate_error() {
let body = BodyStream::new(stream::once(async { Err(StreamErr) }));
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
}
#[actix_rt::test]
async fn stream_delayed_error() {
let body =
BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)]));
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
#[pin_project::pin_project(project = TimeDelayStreamProj)]
#[derive(Debug)]
enum TimeDelayStream {
Start,
Sleep(Pin<Box<Sleep>>),
Done,
}
impl Stream for TimeDelayStream {
type Item = Result<Bytes, StreamErr>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
match self.as_mut().get_mut() {
TimeDelayStream::Start => {
let sleep = sleep(Duration::from_millis(1));
self.as_mut().set(TimeDelayStream::Sleep(Box::pin(sleep)));
cx.waker().wake_by_ref();
Poll::Pending
}
TimeDelayStream::Sleep(ref mut delay) => {
ready!(delay.poll_unpin(cx));
self.set(TimeDelayStream::Done);
cx.waker().wake_by_ref();
Poll::Pending
}
TimeDelayStream::Done => Poll::Ready(Some(Err(StreamErr))),
}
}
}
let body = BodyStream::new(TimeDelayStream::Start);
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
}
}

View File

@ -0,0 +1,232 @@
//! [`MessageBody`] trait and foreign implementations.
use std::{
convert::Infallible,
mem,
pin::Pin,
task::{Context, Poll},
};
use bytes::{Bytes, BytesMut};
use futures_core::ready;
use pin_project_lite::pin_project;
use crate::error::Error;
use super::BodySize;
/// An interface for response bodies.
pub trait MessageBody {
type Error;
/// Body size hint.
fn size(&self) -> BodySize;
/// Attempt to pull out the next chunk of body bytes.
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>>;
}
impl MessageBody for () {
type Error = Infallible;
fn size(&self) -> BodySize {
BodySize::Empty
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
Poll::Ready(None)
}
}
impl<B> MessageBody for Box<B>
where
B: MessageBody + Unpin,
B::Error: Into<Error>,
{
type Error = B::Error;
fn size(&self) -> BodySize {
self.as_ref().size()
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
Pin::new(self.get_mut().as_mut()).poll_next(cx)
}
}
impl<B> MessageBody for Pin<Box<B>>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Error = B::Error;
fn size(&self) -> BodySize {
self.as_ref().size()
}
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
self.as_mut().poll_next(cx)
}
}
impl MessageBody for Bytes {
type Error = Infallible;
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
}
}
}
impl MessageBody for BytesMut {
type Error = Infallible;
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
}
}
}
impl MessageBody for &'static str {
type Error = Infallible;
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::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> {
type Error = Infallible;
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut())))))
}
}
}
impl MessageBody for String {
type Error = Infallible;
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from(
mem::take(self.get_mut()).into_bytes(),
))))
}
}
}
pin_project! {
pub(crate) struct MessageBodyMapErr<B, F> {
#[pin]
body: B,
mapper: Option<F>,
}
}
impl<B, F, E> MessageBodyMapErr<B, F>
where
B: MessageBody,
F: FnOnce(B::Error) -> E,
{
pub(crate) fn new(body: B, mapper: F) -> Self {
Self {
body,
mapper: Some(mapper),
}
}
}
impl<B, F, E> MessageBody for MessageBodyMapErr<B, F>
where
B: MessageBody,
F: FnOnce(B::Error) -> E,
{
type Error = E;
fn size(&self) -> BodySize {
self.body.size()
}
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
let this = self.as_mut().project();
match ready!(this.body.poll_next(cx)) {
Some(Err(err)) => {
let f = self.as_mut().project().mapper.take().unwrap();
let mapped_err = (f)(err);
Poll::Ready(Some(Err(mapped_err)))
}
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
}
}
}

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

@ -0,0 +1,263 @@
//! Traits and structures to aid consuming and writing HTTP payloads.
use std::task::Poll;
use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut};
use futures_core::ready;
#[allow(clippy::module_inception)]
mod body;
mod body_stream;
mod message_body;
mod response_body;
mod size;
mod sized_stream;
pub use self::body::{AnyBody, Body, BoxAnyBody};
pub use self::body_stream::BodyStream;
pub use self::message_body::MessageBody;
pub(crate) use self::message_body::MessageBodyMapErr;
pub use self::response_body::ResponseBody;
pub use self::size::BodySize;
pub use self::sized_stream::SizedStream;
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
///
/// Any errors produced by the body stream are returned immediately.
///
/// # Examples
/// ```
/// use actix_http::body::{Body, to_bytes};
/// use bytes::Bytes;
///
/// # async fn test_to_bytes() {
/// let body = Body::Empty;
/// let bytes = to_bytes(body).await.unwrap();
/// assert!(bytes.is_empty());
///
/// let body = Body::Bytes(Bytes::from_static(b"123"));
/// let bytes = to_bytes(body).await.unwrap();
/// assert_eq!(bytes, b"123"[..]);
/// # }
/// ```
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
let cap = match body.size() {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => return Ok(Bytes::new()),
BodySize::Sized(size) => size as usize,
BodySize::Stream => 32_768,
};
let mut buf = BytesMut::with_capacity(cap);
pin!(body);
poll_fn(|cx| loop {
let body = body.as_mut();
match ready!(body.poll_next(cx)) {
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
None => return Poll::Ready(Ok(())),
Some(Err(err)) => return Poll::Ready(Err(err)),
}
})
.await?;
Ok(buf.freeze())
}
#[cfg(test)]
mod tests {
use std::pin::Pin;
use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut};
use super::*;
impl Body {
pub(crate) fn get_ref(&self) -> &[u8] {
match *self {
Body::Bytes(ref bin) => &bin,
_ => panic!(),
}
}
}
#[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_and_pin() {
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());
let mut val = Box::pin(());
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, Value};
assert_eq!(
Body::from(serde_json::to_vec(&Value::String("test".to_owned())).unwrap())
.size(),
BodySize::Sized(6)
);
assert_eq!(
Body::from(serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap())
.size(),
BodySize::Sized(25)
);
}
// down-casting used to be done with a method on MessageBody trait
// test is kept to demonstrate equivalence of Any trait
#[actix_rt::test]
async fn test_body_casting() {
let mut body = String::from("hello cast");
// let mut resp_body: &mut dyn MessageBody<Error = Error> = &mut body;
let resp_body: &mut dyn std::any::Any = &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());
}
#[actix_rt::test]
async fn test_to_bytes() {
let body = Body::Empty;
let bytes = to_bytes(body).await.unwrap();
assert!(bytes.is_empty());
let body = Body::Bytes(Bytes::from_static(b"123"));
let bytes = to_bytes(body).await.unwrap();
assert_eq!(bytes, b"123"[..]);
}
}

View File

@ -0,0 +1,89 @@
use std::{
mem,
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use futures_core::{ready, 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 for ResponseBody<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Error = Error;
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, Self::Error>>> {
Stream::poll_next(self, cx)
}
}
impl<B> Stream for ResponseBody<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Item = Result<Bytes, Error>;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
match self.project() {
// TODO: MSRV 1.51: poll_map_err
ResponseBodyProj::Body(body) => match ready!(body.poll_next(cx)) {
Some(Err(err)) => Poll::Ready(Some(Err(err.into()))),
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
},
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,122 @@
use std::{
error::Error as StdError,
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use futures_core::{ready, Stream};
use pin_project_lite::pin_project;
use super::{BodySize, MessageBody};
pin_project! {
/// Known sized streaming response wrapper.
///
/// This body implementation should be used if total size of stream is known. Data is sent as-is
/// without using chunked transfer encoding.
pub struct SizedStream<S> {
size: u64,
#[pin]
stream: S,
}
}
impl<S, E> SizedStream<S>
where
S: Stream<Item = Result<Bytes, E>>,
E: Into<Box<dyn StdError>> + 'static,
{
pub fn new(size: u64, stream: S) -> Self {
SizedStream { size, stream }
}
}
impl<S, E> MessageBody for SizedStream<S>
where
S: Stream<Item = Result<Bytes, E>>,
E: Into<Box<dyn StdError>> + 'static,
{
type Error = E;
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, Self::Error>>> {
loop {
let stream = self.as_mut().project().stream;
let chunk = match ready!(stream.poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
val => val,
};
return Poll::Ready(chunk);
}
}
}
#[cfg(test)]
mod tests {
use std::convert::Infallible;
use actix_rt::pin;
use actix_utils::future::poll_fn;
use futures_util::stream;
use super::*;
use crate::body::to_bytes;
#[actix_rt::test]
async fn skips_empty_chunks() {
let body = SizedStream::new(
2,
stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok::<_, Infallible>(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 read_to_bytes() {
let body = SizedStream::new(
2,
stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
),
);
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
}
}

View File

@ -1,19 +1,16 @@
use std::marker::PhantomData; use std::{error::Error as StdError, fmt, marker::PhantomData, net, rc::Rc};
use std::rc::Rc;
use std::{fmt, net};
use actix_codec::Framed; use actix_codec::Framed;
use actix_service::{IntoServiceFactory, Service, ServiceFactory}; use actix_service::{IntoServiceFactory, Service, ServiceFactory};
use crate::body::MessageBody; use crate::{
use crate::config::{KeepAlive, ServiceConfig}; body::{AnyBody, MessageBody},
use crate::error::Error; config::{KeepAlive, ServiceConfig},
use crate::h1::{Codec, ExpectHandler, H1Service, UpgradeHandler}; h1::{self, ExpectHandler, H1Service, UpgradeHandler},
use crate::h2::H2Service; h2::H2Service,
use crate::request::Request; service::HttpService,
use crate::response::Response; ConnectCallback, Extensions, Request, Response,
use crate::service::HttpService; };
use crate::{ConnectCallback, Extensions};
/// A HTTP service builder /// A HTTP service builder
/// ///
@ -34,7 +31,7 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler> impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
{ {
@ -57,17 +54,15 @@ where
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U> impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static, U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
{ {
/// Set server keep-alive setting. /// Set server keep-alive setting.
/// ///
@ -125,9 +120,8 @@ where
where where
F: IntoServiceFactory<X1, Request>, F: IntoServiceFactory<X1, Request>,
X1: ServiceFactory<Request, Config = (), Response = Request>, X1: ServiceFactory<Request, Config = (), Response = Request>,
X1::Error: Into<Error>, X1::Error: Into<Response<AnyBody>>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
<X1::Service as Service<Request>>::Future: 'static,
{ {
HttpServiceBuilder { HttpServiceBuilder {
keep_alive: self.keep_alive, keep_alive: self.keep_alive,
@ -148,11 +142,10 @@ where
/// and this service get called with original request and framed object. /// and this service get called with original request and framed object.
pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1> pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1>
where where
F: IntoServiceFactory<U1, (Request, Framed<T, Codec>)>, F: IntoServiceFactory<U1, (Request, Framed<T, h1::Codec>)>,
U1: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>, U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
U1::Error: fmt::Display, U1::Error: fmt::Display,
U1::InitError: fmt::Debug, U1::InitError: fmt::Debug,
<U1::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
{ {
HttpServiceBuilder { HttpServiceBuilder {
keep_alive: self.keep_alive, keep_alive: self.keep_alive,
@ -185,7 +178,7 @@ where
where where
B: MessageBody, B: MessageBody,
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
{ {
@ -206,12 +199,13 @@ where
/// Finish service configuration and create a HTTP service for HTTP/2 protocol. /// Finish service configuration and create a HTTP service for HTTP/2 protocol.
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B> pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
where where
B: MessageBody + 'static,
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,
@ -228,12 +222,13 @@ where
/// Finish service configuration and create `HttpService` instance. /// Finish service configuration and create `HttpService` instance.
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U> pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
where where
B: MessageBody + 'static,
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,

View File

@ -1,31 +1,35 @@
use std::net::IpAddr;
use std::time::Duration; 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_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2mb const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB
const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1mb
/// Connector configuration /// Connector configuration
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct ConnectorConfig { pub(crate) struct ConnectorConfig {
pub(crate) timeout: Duration, pub(crate) timeout: Duration,
pub(crate) handshake_timeout: Duration,
pub(crate) conn_lifetime: Duration, pub(crate) conn_lifetime: Duration,
pub(crate) conn_keep_alive: Duration, pub(crate) conn_keep_alive: Duration,
pub(crate) disconnect_timeout: Option<Duration>, pub(crate) disconnect_timeout: Option<Duration>,
pub(crate) limit: usize, pub(crate) limit: usize,
pub(crate) conn_window_size: u32, pub(crate) conn_window_size: u32,
pub(crate) stream_window_size: u32, pub(crate) stream_window_size: u32,
pub(crate) local_address: Option<IpAddr>,
} }
impl Default for ConnectorConfig { impl Default for ConnectorConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
timeout: Duration::from_secs(1), timeout: Duration::from_secs(5),
handshake_timeout: Duration::from_secs(5),
conn_lifetime: Duration::from_secs(75), conn_lifetime: Duration::from_secs(75),
conn_keep_alive: Duration::from_secs(15), conn_keep_alive: Duration::from_secs(15),
disconnect_timeout: Some(Duration::from_millis(3000)), disconnect_timeout: Some(Duration::from_millis(3000)),
limit: 100, limit: 100,
conn_window_size: DEFAULT_H2_CONN_WINDOW, conn_window_size: DEFAULT_H2_CONN_WINDOW,
stream_window_size: DEFAULT_H2_STREAM_WINDOW, stream_window_size: DEFAULT_H2_STREAM_WINDOW,
local_address: None,
} }
} }
} }

View File

@ -1,257 +1,365 @@
use std::future::Future; use std::{
use std::pin::Pin; io,
use std::task::{Context, Poll}; ops::{Deref, DerefMut},
use std::{fmt, io, time}; pin::Pin,
task::{Context, Poll},
time,
};
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf}; use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf};
use actix_rt::task::JoinHandle;
use bytes::Bytes; use bytes::Bytes;
use futures_util::future::{err, Either, FutureExt, LocalBoxFuture, Ready}; use futures_core::future::LocalBoxFuture;
use h2::client::SendRequest; use h2::client::SendRequest;
use pin_project::pin_project;
use crate::body::MessageBody;
use crate::h1::ClientCodec; use crate::h1::ClientCodec;
use crate::message::{RequestHeadType, ResponseHead}; use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::Payload; use crate::payload::Payload;
use crate::{body::MessageBody, Error};
use super::error::SendRequestError; use super::error::SendRequestError;
use super::pool::{Acquired, Protocol}; use super::pool::Acquired;
use super::{h1proto, h2proto}; use super::{h1proto, h2proto};
pub(crate) enum ConnectionType<Io> { /// Trait alias for types impl [tokio::io::AsyncRead] and [tokio::io::AsyncWrite].
H1(Io), pub trait ConnectionIo: AsyncRead + AsyncWrite + Unpin + 'static {}
H2(SendRequest<Bytes>),
}
pub trait Connection { impl<T: AsyncRead + AsyncWrite + Unpin + 'static> ConnectionIo for T {}
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>>(
self,
head: H,
body: B,
) -> Self::Future;
type TunnelFuture: Future<
Output = Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
>;
/// Send request, returns Response and Framed
fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture;
}
pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
/// Close connection
fn close(self: Pin<&mut Self>);
/// Release connection to the connection pool
fn release(self: Pin<&mut Self>);
}
#[doc(hidden)]
/// HTTP client connection /// HTTP client connection
pub struct IoConnection<T> { pub struct H1Connection<Io: ConnectionIo> {
io: Option<ConnectionType<T>>, io: Option<Io>,
created: time::Instant, created: time::Instant,
pool: Option<Acquired<T>>, acquired: Acquired<Io>,
} }
impl<T> fmt::Debug for IoConnection<T> impl<Io: ConnectionIo> H1Connection<Io> {
where /// close or release the connection to pool based on flag input
T: fmt::Debug, pub(super) fn on_release(&mut self, keep_alive: bool) {
{ if keep_alive {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.release();
match self.io { } else {
Some(ConnectionType::H1(ref io)) => write!(f, "H1Connection({:?})", io), self.close();
Some(ConnectionType::H2(_)) => write!(f, "H2Connection"),
None => write!(f, "Connection(Empty)"),
} }
} }
/// Close connection
fn close(&mut self) {
let io = self.io.take().unwrap();
self.acquired.close(ConnectionInnerType::H1(io));
}
/// Release this connection to the connection pool
fn release(&mut self) {
let io = self.io.take().unwrap();
self.acquired
.release(ConnectionInnerType::H1(io), self.created);
}
fn io_pin_mut(self: Pin<&mut Self>) -> Pin<&mut Io> {
Pin::new(self.get_mut().io.as_mut().unwrap())
}
} }
impl<T: AsyncRead + AsyncWrite + Unpin> IoConnection<T> { impl<Io: ConnectionIo> AsyncRead for H1Connection<Io> {
pub(crate) fn new( fn poll_read(
io: ConnectionType<T>, self: Pin<&mut Self>,
created: time::Instant, cx: &mut Context<'_>,
pool: Option<Acquired<T>>, buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
self.io_pin_mut().poll_read(cx, buf)
}
}
impl<Io: ConnectionIo> AsyncWrite for H1Connection<Io> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
self.io_pin_mut().poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.io_pin_mut().poll_flush(cx)
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), io::Error>> {
self.io_pin_mut().poll_shutdown(cx)
}
fn poll_write_vectored(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[io::IoSlice<'_>],
) -> Poll<io::Result<usize>> {
self.io_pin_mut().poll_write_vectored(cx, bufs)
}
fn is_write_vectored(&self) -> bool {
self.io.as_ref().unwrap().is_write_vectored()
}
}
/// HTTP2 client connection
pub struct H2Connection<Io: ConnectionIo> {
io: Option<H2ConnectionInner>,
created: time::Instant,
acquired: Acquired<Io>,
}
impl<Io: ConnectionIo> Deref for H2Connection<Io> {
type Target = SendRequest<Bytes>;
fn deref(&self) -> &Self::Target {
&self.io.as_ref().unwrap().sender
}
}
impl<Io: ConnectionIo> DerefMut for H2Connection<Io> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.io.as_mut().unwrap().sender
}
}
impl<Io: ConnectionIo> H2Connection<Io> {
/// close or release the connection to pool based on flag input
pub(super) fn on_release(&mut self, close: bool) {
if close {
self.close();
} else {
self.release();
}
}
/// Close connection
fn close(&mut self) {
let io = self.io.take().unwrap();
self.acquired.close(ConnectionInnerType::H2(io));
}
/// Release this connection to the connection pool
fn release(&mut self) {
let io = self.io.take().unwrap();
self.acquired
.release(ConnectionInnerType::H2(io), self.created);
}
}
/// `H2ConnectionInner` has two parts: `SendRequest` and `Connection`.
///
/// `Connection` is spawned as an async task on runtime and `H2ConnectionInner` holds a handle
/// for this task. Therefore, it can wake up and quit the task when SendRequest is dropped.
pub(super) struct H2ConnectionInner {
handle: JoinHandle<()>,
sender: SendRequest<Bytes>,
}
impl H2ConnectionInner {
pub(super) fn new<Io: ConnectionIo>(
sender: SendRequest<Bytes>,
connection: h2::client::Connection<Io>,
) -> Self { ) -> Self {
IoConnection { let handle = actix_rt::spawn(async move {
pool, let _ = connection.await;
created, });
io: Some(io),
}
}
pub(crate) fn into_inner(self) -> (ConnectionType<T>, time::Instant) { Self { handle, sender }
(self.io.unwrap(), self.created)
} }
} }
impl<T> Connection for IoConnection<T> /// Cancel spawned connection task on drop.
where impl Drop for H2ConnectionInner {
T: AsyncRead + AsyncWrite + Unpin + 'static, fn drop(&mut self) {
{ if self
type Io = T; .sender
type Future = .send_request(http::Request::new(()), true)
LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>; .is_err()
{
fn protocol(&self) -> Protocol { self.handle.abort();
match self.io {
Some(ConnectionType::H1(_)) => Protocol::Http1,
Some(ConnectionType::H2(_)) => Protocol::Http2,
None => Protocol::Http1,
}
}
fn send_request<B: MessageBody + 'static, H: Into<RequestHeadType>>(
mut self,
head: H,
body: B,
) -> Self::Future {
match self.io.take().unwrap() {
ConnectionType::H1(io) => {
h1proto::send_request(io, head.into(), body, self.created, self.pool)
.boxed_local()
}
ConnectionType::H2(io) => {
h2proto::send_request(io, head.into(), body, self.created, self.pool)
.boxed_local()
}
}
}
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 {
match self.io.take().unwrap() {
ConnectionType::H1(io) => {
Either::Left(h1proto::open_tunnel(io, head.into()).boxed_local())
}
ConnectionType::H2(io) => {
if let Some(mut pool) = self.pool.take() {
pool.release(IoConnection::new(
ConnectionType::H2(io),
self.created,
None,
));
}
Either::Right(err(SendRequestError::TunnelNotSupported))
}
} }
} }
} }
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) enum EitherConnection<A, B> { /// Unified connection type cover Http1 Plain/Tls and Http2 protocols
A(IoConnection<A>), pub enum Connection<A, B = Box<dyn ConnectionIo>>
B(IoConnection<B>), where
A: ConnectionIo,
B: ConnectionIo,
{
Tcp(ConnectionType<A>),
Tls(ConnectionType<B>),
} }
impl<A, B> Connection for EitherConnection<A, B> /// Unified connection type cover Http1/2 protocols
where pub enum ConnectionType<Io: ConnectionIo> {
A: AsyncRead + AsyncWrite + Unpin + 'static, H1(H1Connection<Io>),
B: AsyncRead + AsyncWrite + Unpin + 'static, H2(H2Connection<Io>),
{ }
type Io = EitherIo<A, B>;
type Future =
LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>;
fn protocol(&self) -> Protocol { /// Helper type for storing connection types in pool.
match self { pub(super) enum ConnectionInnerType<Io> {
EitherConnection::A(con) => con.protocol(), H1(Io),
EitherConnection::B(con) => con.protocol(), H2(H2ConnectionInner),
}
impl<Io: ConnectionIo> ConnectionType<Io> {
pub(super) fn from_pool(
inner: ConnectionInnerType<Io>,
created: time::Instant,
acquired: Acquired<Io>,
) -> Self {
match inner {
ConnectionInnerType::H1(io) => Self::from_h1(io, created, acquired),
ConnectionInnerType::H2(io) => Self::from_h2(io, created, acquired),
} }
} }
fn send_request<RB: MessageBody + 'static, H: Into<RequestHeadType>>( pub(super) fn from_h1(
io: Io,
created: time::Instant,
acquired: Acquired<Io>,
) -> Self {
Self::H1(H1Connection {
io: Some(io),
created,
acquired,
})
}
pub(super) fn from_h2(
io: H2ConnectionInner,
created: time::Instant,
acquired: Acquired<Io>,
) -> Self {
Self::H2(H2Connection {
io: Some(io),
created,
acquired,
})
}
}
impl<A, B> Connection<A, B>
where
A: ConnectionIo,
B: ConnectionIo,
{
/// Send a request through connection.
pub fn send_request<RB, H>(
self, self,
head: H, head: H,
body: RB, body: RB,
) -> Self::Future { ) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>
match self { where
EitherConnection::A(con) => con.send_request(head, body), H: Into<RequestHeadType> + 'static,
EitherConnection::B(con) => con.send_request(head, body), RB: MessageBody + 'static,
} RB::Error: Into<Error>,
{
Box::pin(async move {
match self {
Connection::Tcp(ConnectionType::H1(conn)) => {
h1proto::send_request(conn, head.into(), body).await
}
Connection::Tls(ConnectionType::H1(conn)) => {
h1proto::send_request(conn, head.into(), body).await
}
Connection::Tls(ConnectionType::H2(conn)) => {
h2proto::send_request(conn, head.into(), body).await
}
_ => unreachable!(
"Plain Tcp connection can be used only in Http1 protocol"
),
}
})
} }
type TunnelFuture = LocalBoxFuture< /// Send request, returns Response and Framed tunnel.
pub fn open_tunnel<H: Into<RequestHeadType> + 'static>(
self,
head: H,
) -> LocalBoxFuture<
'static, 'static,
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>, Result<(ResponseHead, Framed<Connection<A, B>, ClientCodec>), SendRequestError>,
>; > {
Box::pin(async move {
/// Send request, returns Response and Framed match self {
fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture { Connection::Tcp(ConnectionType::H1(ref _conn)) => {
match self { let (head, framed) = h1proto::open_tunnel(self, head.into()).await?;
EitherConnection::A(con) => con Ok((head, framed))
.open_tunnel(head) }
.map(|res| { Connection::Tls(ConnectionType::H1(ref _conn)) => {
res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::A))) let (head, framed) = h1proto::open_tunnel(self, head.into()).await?;
}) Ok((head, framed))
.boxed_local(), }
EitherConnection::B(con) => con Connection::Tls(ConnectionType::H2(mut conn)) => {
.open_tunnel(head) conn.release();
.map(|res| { Err(SendRequestError::TunnelNotSupported)
res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::B))) }
}) Connection::Tcp(ConnectionType::H2(_)) => {
.boxed_local(), unreachable!(
} "Plain Tcp connection can be used only in Http1 protocol"
)
}
}
})
} }
} }
#[pin_project(project = EitherIoProj)] impl<A, B> AsyncRead for Connection<A, B>
pub enum EitherIo<A, B> {
A(#[pin] A),
B(#[pin] B),
}
impl<A, B> AsyncRead for EitherIo<A, B>
where where
A: AsyncRead, A: ConnectionIo,
B: AsyncRead, B: ConnectionIo,
{ {
fn poll_read( fn poll_read(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>, buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> { ) -> Poll<io::Result<()>> {
match self.project() { match self.get_mut() {
EitherIoProj::A(val) => val.poll_read(cx, buf), Connection::Tcp(ConnectionType::H1(conn)) => {
EitherIoProj::B(val) => val.poll_read(cx, buf), Pin::new(conn).poll_read(cx, buf)
}
Connection::Tls(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_read(cx, buf)
}
_ => unreachable!("H2Connection can not impl AsyncRead trait"),
} }
} }
} }
impl<A, B> AsyncWrite for EitherIo<A, B> const H2_UNREACHABLE_WRITE: &str = "H2Connection can not impl AsyncWrite trait";
impl<A, B> AsyncWrite for Connection<A, B>
where where
A: AsyncWrite, A: ConnectionIo,
B: AsyncWrite, B: ConnectionIo,
{ {
fn poll_write( fn poll_write(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
buf: &[u8], buf: &[u8],
) -> Poll<io::Result<usize>> { ) -> Poll<io::Result<usize>> {
match self.project() { match self.get_mut() {
EitherIoProj::A(val) => val.poll_write(cx, buf), Connection::Tcp(ConnectionType::H1(conn)) => {
EitherIoProj::B(val) => val.poll_write(cx, buf), Pin::new(conn).poll_write(cx, buf)
}
Connection::Tls(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_write(cx, buf)
}
_ => unreachable!(H2_UNREACHABLE_WRITE),
} }
} }
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> { fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match self.project() { match self.get_mut() {
EitherIoProj::A(val) => val.poll_flush(cx), Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx),
EitherIoProj::B(val) => val.poll_flush(cx), Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx),
_ => unreachable!(H2_UNREACHABLE_WRITE),
} }
} }
@ -259,9 +367,109 @@ where
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<io::Result<()>> { ) -> Poll<io::Result<()>> {
match self.project() { match self.get_mut() {
EitherIoProj::A(val) => val.poll_shutdown(cx), Connection::Tcp(ConnectionType::H1(conn)) => {
EitherIoProj::B(val) => val.poll_shutdown(cx), Pin::new(conn).poll_shutdown(cx)
}
Connection::Tls(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_shutdown(cx)
}
_ => unreachable!(H2_UNREACHABLE_WRITE),
}
}
fn poll_write_vectored(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[io::IoSlice<'_>],
) -> Poll<io::Result<usize>> {
match self.get_mut() {
Connection::Tcp(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_write_vectored(cx, bufs)
}
Connection::Tls(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_write_vectored(cx, bufs)
}
_ => unreachable!(H2_UNREACHABLE_WRITE),
}
}
fn is_write_vectored(&self) -> bool {
match *self {
Connection::Tcp(ConnectionType::H1(ref conn)) => conn.is_write_vectored(),
Connection::Tls(ConnectionType::H1(ref conn)) => conn.is_write_vectored(),
_ => unreachable!(H2_UNREACHABLE_WRITE),
} }
} }
} }
#[cfg(test)]
mod test {
use std::{
future::Future,
net,
pin::Pin,
task::{Context, Poll},
time::{Duration, Instant},
};
use actix_rt::{
net::TcpStream,
time::{interval, Interval},
};
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 = H2ConnectionInner::new(sender.clone(), connection);
assert!(sender.clone().ready().await.is_ok());
assert!(h2::client::SendRequest::clone(&conn.sender)
.ready()
.await
.is_ok());
drop(conn);
struct DropCheck {
sender: h2::client::SendRequest<Bytes>,
interval: Interval,
start_from: Instant,
}
impl Future for DropCheck {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
match futures_core::ready!(this.sender.poll_ready(cx)) {
Ok(()) => {
if this.start_from.elapsed() > Duration::from_secs(10) {
panic!("connection should be gone and can not be ready");
} else {
let _ = this.interval.poll_tick(cx);
Poll::Pending
}
}
Err(_) => Poll::Ready(()),
}
}
}
DropCheck {
sender,
interval: interval(Duration::from_millis(100)),
start_from: Instant::now(),
}
.await;
}
}

View File

@ -1,45 +1,53 @@
use std::fmt; use std::{
use std::marker::PhantomData; fmt,
use std::time::Duration; future::Future,
net::IpAddr,
use actix_codec::{AsyncRead, AsyncWrite}; pin::Pin,
use actix_rt::net::TcpStream; rc::Rc,
use actix_service::{apply_fn, Service, ServiceExt}; task::{Context, Poll},
use actix_tls::connect::{ time::Duration,
default_connector, Connect as TcpConnect, Connection as TcpConnection,
}; };
use actix_utils::timeout::{TimeoutError, TimeoutService};
use actix_rt::{
net::{ActixStream, TcpStream},
time::{sleep, Sleep},
};
use actix_service::Service;
use actix_tls::connect::{
new_connector, Connect as TcpConnect, ConnectError as TcpConnectError,
Connection as TcpConnection, Resolver,
};
use futures_core::{future::LocalBoxFuture, ready};
use http::Uri; use http::Uri;
use pin_project::pin_project;
use super::config::ConnectorConfig; use super::config::ConnectorConfig;
use super::connection::Connection; use super::connection::{Connection, ConnectionIo};
use super::error::ConnectError; use super::error::ConnectError;
use super::pool::{ConnectionPool, Protocol}; use super::pool::ConnectionPool;
use super::Connect; use super::Connect;
use super::Protocol;
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector; use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector;
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
use actix_tls::connect::ssl::rustls::ClientConfig; use actix_tls::connect::ssl::rustls::ClientConfig;
#[cfg(feature = "rustls")]
use std::sync::Arc;
#[cfg(any(feature = "openssl", feature = "rustls"))]
enum SslConnector { enum SslConnector {
#[allow(dead_code)]
None,
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
Openssl(OpensslConnector), Openssl(OpensslConnector),
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
Rustls(Arc<ClientConfig>), Rustls(std::sync::Arc<ClientConfig>),
} }
#[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 /// The `Connector` type uses a builder-like combinator pattern for service
/// construction that finishes by calling the `.finish()` method. /// construction that finishes by calling the `.finish()` method.
/// ///
/// ```rust,ignore /// ```ignore
/// use std::time::Duration; /// use std::time::Duration;
/// use actix_http::client::Connector; /// use actix_http::client::Connector;
/// ///
@ -47,18 +55,14 @@ type SslConnector = ();
/// .timeout(Duration::from_secs(5)) /// .timeout(Duration::from_secs(5))
/// .finish(); /// .finish();
/// ``` /// ```
pub struct Connector<T, U> { pub struct Connector<T> {
connector: T, connector: T,
config: ConnectorConfig, config: ConnectorConfig,
#[allow(dead_code)] #[allow(dead_code)]
ssl: SslConnector, ssl: SslConnector,
_phantom: PhantomData<U>,
} }
trait Io: AsyncRead + AsyncWrite + Unpin {} impl Connector<()> {
impl<T: AsyncRead + AsyncWrite + Unpin> Io for T {}
impl Connector<(), ()> {
#[allow(clippy::new_ret_no_self, clippy::let_unit_value)] #[allow(clippy::new_ret_no_self, clippy::let_unit_value)]
pub fn new() -> Connector< pub fn new() -> Connector<
impl Service< impl Service<
@ -66,13 +70,11 @@ impl Connector<(), ()> {
Response = TcpConnection<Uri, TcpStream>, Response = TcpConnection<Uri, TcpStream>,
Error = actix_tls::connect::ConnectError, Error = actix_tls::connect::ConnectError,
> + Clone, > + Clone,
TcpStream,
> { > {
Connector { Connector {
ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), 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(), config: ConnectorConfig::default(),
_phantom: PhantomData,
} }
} }
@ -100,54 +102,69 @@ impl Connector<(), ()> {
fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector { fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector {
let mut config = ClientConfig::new(); let mut config = ClientConfig::new();
config.set_protocols(&protocols); config.set_protocols(&protocols);
config config.root_store.add_server_trust_anchors(
.root_store &actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS,
.add_server_trust_anchors(&actix_tls::accept::rustls::TLS_SERVER_ROOTS); );
SslConnector::Rustls(Arc::new(config)) SslConnector::Rustls(std::sync::Arc::new(config))
} }
// ssl turned off, provides empty ssl connector // ssl turned off, provides empty ssl connector
#[cfg(not(any(feature = "openssl", feature = "rustls")))] #[cfg(not(any(feature = "openssl", feature = "rustls")))]
fn build_ssl(_: Vec<Vec<u8>>) -> SslConnector {} fn build_ssl(_: Vec<Vec<u8>>) -> SslConnector {
SslConnector::None
}
} }
impl<T, U> Connector<T, U> { impl<S> Connector<S> {
/// Use custom connector. /// Use custom connector.
pub fn connector<T1, U1>(self, connector: T1) -> Connector<T1, U1> pub fn connector<S1, Io1>(self, connector: S1) -> Connector<S1>
where where
U1: AsyncRead + AsyncWrite + Unpin + fmt::Debug, Io1: ActixStream + fmt::Debug + 'static,
T1: Service< S1: Service<
TcpConnect<Uri>, TcpConnect<Uri>,
Response = TcpConnection<Uri, U1>, Response = TcpConnection<Uri, Io1>,
Error = actix_tls::connect::ConnectError, Error = TcpConnectError,
> + Clone, > + Clone,
{ {
Connector { Connector {
connector, connector,
config: self.config, config: self.config,
ssl: self.ssl, ssl: self.ssl,
_phantom: PhantomData,
} }
} }
} }
impl<T, U> Connector<T, U> impl<S, Io> Connector<S>
where where
U: AsyncRead + AsyncWrite + Unpin + fmt::Debug + 'static, // Note:
T: Service< // Input Io type is bound to ActixStream trait but internally in client module they
// are bound to ConnectionIo trait alias. And latter is the trait exposed to public
// in the form of Box<dyn ConnectionIo> type.
//
// This remap is to hide ActixStream's trait methods. They are not meant to be called
// from user code.
Io: ActixStream + fmt::Debug + 'static,
S: Service<
TcpConnect<Uri>, TcpConnect<Uri>,
Response = TcpConnection<Uri, U>, Response = TcpConnection<Uri, Io>,
Error = actix_tls::connect::ConnectError, Error = TcpConnectError,
> + Clone > + Clone
+ 'static, + 'static,
{ {
/// Connection timeout, i.e. max time to connect to remote host including dns name resolution. /// Tcp connection timeout, i.e. max time to connect to remote host including dns name
/// Set to 1 second by default. /// resolution. Set to 5 second by default.
pub fn timeout(mut self, timeout: Duration) -> Self { pub fn timeout(mut self, timeout: Duration) -> Self {
self.config.timeout = timeout; self.config.timeout = timeout;
self self
} }
/// Tls handshake timeout, i.e. max time to do tls handshake with remote host after tcp
/// connection established. Set to 5 second by default.
pub fn handshake_timeout(mut self, timeout: Duration) -> Self {
self.config.handshake_timeout = timeout;
self
}
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
/// Use custom `SslConnector` instance. /// Use custom `SslConnector` instance.
pub fn ssl(mut self, connector: OpensslConnector) -> Self { pub fn ssl(mut self, connector: OpensslConnector) -> Self {
@ -156,13 +173,15 @@ where
} }
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
pub fn rustls(mut self, connector: Arc<ClientConfig>) -> Self { /// Use custom `SslConnector` instance.
pub fn rustls(mut self, connector: std::sync::Arc<ClientConfig>) -> Self {
self.ssl = SslConnector::Rustls(connector); self.ssl = SslConnector::Rustls(connector);
self self
} }
/// Maximum supported http major version /// Maximum supported HTTP major version.
/// Supported versions http/1.1, http/2 ///
/// Supported versions are HTTP/1.1 and HTTP/2.
pub fn max_http_version(mut self, val: http::Version) -> Self { pub fn max_http_version(mut self, val: http::Version) -> Self {
let versions = match val { let versions = match val {
http::Version::HTTP_11 => vec![b"http/1.1".to_vec()], http::Version::HTTP_11 => vec![b"http/1.1".to_vec()],
@ -236,299 +255,512 @@ where
self 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. /// Finish configuration process and create connector service.
/// The Connector builder always concludes by calling `finish()` last in /// The Connector builder always concludes by calling `finish()` last in
/// its combinator chain. /// its combinator chain.
pub fn finish( pub fn finish(self) -> ConnectorService<S, Io> {
self, let local_address = self.config.local_address;
) -> impl Service<Connect, Response = impl Connection, Error = ConnectError> + Clone let timeout = self.config.timeout;
{
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
{
let connector = 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 { let tcp_service_inner =
tcp_pool: ConnectionPool::new( TcpConnectorInnerService::new(self.connector, timeout, local_address);
connector,
self.config.no_disconnect_timeout(), #[allow(clippy::redundant_clone)]
), let tcp_service = TcpConnectorService {
} service: tcp_service_inner.clone(),
} };
#[cfg(any(feature = "openssl", feature = "rustls"))]
{ let tls_service = match self.ssl {
const H2: &[u8] = b"h2"; SslConnector::None => None,
use actix_service::{boxed::service, pipeline};
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
use actix_tls::connect::ssl::openssl::OpensslConnector; SslConnector::Openssl(tls) => {
#[cfg(feature = "rustls")] const H2: &[u8] = b"h2";
use actix_tls::connect::ssl::rustls::{RustlsConnector, Session};
let ssl_service = TimeoutService::new( use actix_tls::connect::ssl::openssl::{OpensslConnector, SslStream};
self.config.timeout,
pipeline(
apply_fn(self.connector.clone(), |msg: Connect, srv| {
srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr))
})
.map_err(ConnectError::from),
)
.and_then(match self.ssl {
#[cfg(feature = "openssl")]
SslConnector::Openssl(ssl) => service(
OpensslConnector::service(ssl)
.map(|stream| {
let sock = stream.into_parts().0;
let h2 = sock
.ssl()
.selected_alpn_protocol()
.map(|protos| protos.windows(2).any(|w| w == H2))
.unwrap_or(false);
if h2 {
(Box::new(sock) as Box<dyn Io>, Protocol::Http2)
} else {
(Box::new(sock) as Box<dyn Io>, Protocol::Http1)
}
})
.map_err(ConnectError::from),
),
#[cfg(feature = "rustls")]
SslConnector::Rustls(ssl) => service(
RustlsConnector::service(ssl)
.map_err(ConnectError::from)
.map(|stream| {
let sock = stream.into_parts().0;
let h2 = sock
.get_ref()
.1
.get_alpn_protocol()
.map(|protos| protos.windows(2).any(|w| w == H2))
.unwrap_or(false);
if h2 {
(Box::new(sock) as Box<dyn Io>, Protocol::Http2)
} else {
(Box::new(sock) as Box<dyn Io>, Protocol::Http1)
}
}),
),
}),
)
.map_err(|e| match e {
TimeoutError::Service(e) => e,
TimeoutError::Timeout => ConnectError::Timeout,
});
let tcp_service = TimeoutService::new( impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, SslStream<Io>> {
self.config.timeout, fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
apply_fn(self.connector, |msg: Connect, srv| { let sock = self.into_parts().0;
srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) let h2 = sock
}) .ssl()
.map_err(ConnectError::from) .selected_alpn_protocol()
.map(|stream| (stream.into_parts().0, Protocol::Http1)), .map(|protos| protos.windows(2).any(|w| w == H2))
) .unwrap_or(false);
.map_err(|e| match e { if h2 {
TimeoutError::Service(e) => e, (Box::new(sock), Protocol::Http2)
TimeoutError::Timeout => ConnectError::Timeout, } else {
}); (Box::new(sock), Protocol::Http1)
}
connect_impl::InnerConnector { }
tcp_pool: ConnectionPool::new(
tcp_service,
self.config.no_disconnect_timeout(),
),
ssl_pool: ConnectionPool::new(ssl_service, self.config),
}
}
}
}
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
mod connect_impl {
use std::task::{Context, Poll};
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<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<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
{
fn clone(&self) -> Self {
InnerConnector {
tcp_pool: self.tcp_pool.clone(),
}
}
}
impl<T, Io> Service<Connect> for InnerConnector<T, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
{
type Response = IoConnection<Io>;
type Error = ConnectError;
type Future = Either<
<ConnectionPool<T, Io> as Service<Connect>>::Future,
Ready<Result<IoConnection<Io>, ConnectError>>,
>;
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(err(ConnectError::SslIsNotSupported))
} }
_ => Either::Left(self.tcp_pool.call(req)),
let handshake_timeout = self.config.handshake_timeout;
let tls_service = TlsConnectorService {
tcp_service: tcp_service_inner,
tls_service: OpensslConnector::service(tls),
timeout: handshake_timeout,
};
Some(actix_service::boxed::rc_service(tls_service))
}
#[cfg(feature = "rustls")]
SslConnector::Rustls(tls) => {
const H2: &[u8] = b"h2";
use actix_tls::connect::ssl::rustls::{
RustlsConnector, Session, TlsStream,
};
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, TlsStream<Io>> {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let sock = self.into_parts().0;
let h2 = sock
.get_ref()
.1
.get_alpn_protocol()
.map(|protos| protos.windows(2).any(|w| w == H2))
.unwrap_or(false);
if h2 {
(Box::new(sock), Protocol::Http2)
} else {
(Box::new(sock), Protocol::Http1)
}
}
}
let handshake_timeout = self.config.handshake_timeout;
let tls_service = TlsConnectorService {
tcp_service: tcp_service_inner,
tls_service: RustlsConnector::service(tls),
timeout: handshake_timeout,
};
Some(actix_service::boxed::rc_service(tls_service))
}
};
let tcp_config = self.config.no_disconnect_timeout();
let tcp_pool = ConnectionPool::new(tcp_service, tcp_config);
let tls_config = self.config;
let tls_pool = tls_service
.map(move |tls_service| ConnectionPool::new(tls_service, tls_config));
ConnectorServicePriv { tcp_pool, tls_pool }
}
}
/// tcp service for map `TcpConnection<Uri, Io>` type to `(Io, Protocol)`
#[derive(Clone)]
pub struct TcpConnectorService<S: Clone> {
service: S,
}
impl<S, Io> Service<Connect> for TcpConnectorService<S>
where
S: Service<Connect, Response = TcpConnection<Uri, Io>, Error = ConnectError>
+ Clone
+ 'static,
{
type Response = (Io, Protocol);
type Error = ConnectError;
type Future = TcpConnectorFuture<S::Future>;
actix_service::forward_ready!(service);
fn call(&self, req: Connect) -> Self::Future {
TcpConnectorFuture {
fut: self.service.call(req),
}
}
}
#[pin_project]
pub struct TcpConnectorFuture<Fut> {
#[pin]
fut: Fut,
}
impl<Fut, Io> Future for TcpConnectorFuture<Fut>
where
Fut: Future<Output = Result<TcpConnection<Uri, Io>, ConnectError>>,
{
type Output = Result<(Io, Protocol), ConnectError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.project()
.fut
.poll(cx)
.map_ok(|res| (res.into_parts().0, Protocol::Http1))
}
}
/// service for establish tcp connection and do client tls handshake.
/// operation is canceled when timeout limit reached.
struct TlsConnectorService<S, St> {
/// tcp connection is canceled on `TcpConnectorInnerService`'s timeout setting.
tcp_service: S,
/// tls connection is canceled on `TlsConnectorService`'s timeout setting.
tls_service: St,
timeout: Duration,
}
impl<S, St, Io> Service<Connect> for TlsConnectorService<S, St>
where
S: Service<Connect, Response = TcpConnection<Uri, Io>, Error = ConnectError>
+ Clone
+ 'static,
St: Service<TcpConnection<Uri, Io>, Error = std::io::Error> + Clone + 'static,
Io: ConnectionIo,
St::Response: IntoConnectionIo,
{
type Response = (Box<dyn ConnectionIo>, Protocol);
type Error = ConnectError;
type Future = TlsConnectorFuture<St, S::Future, St::Future>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
ready!(self.tcp_service.poll_ready(cx))?;
ready!(self.tls_service.poll_ready(cx))?;
Poll::Ready(Ok(()))
}
fn call(&self, req: Connect) -> Self::Future {
let fut = self.tcp_service.call(req);
let tls_service = self.tls_service.clone();
let timeout = self.timeout;
TlsConnectorFuture::TcpConnect {
fut,
tls_service: Some(tls_service),
timeout,
}
}
}
#[pin_project(project = TlsConnectorProj)]
#[allow(clippy::large_enum_variant)]
enum TlsConnectorFuture<S, Fut1, Fut2> {
TcpConnect {
#[pin]
fut: Fut1,
tls_service: Option<S>,
timeout: Duration,
},
TlsConnect {
#[pin]
fut: Fut2,
#[pin]
timeout: Sleep,
},
}
/// helper trait for generic over different TlsStream types between tls crates.
trait IntoConnectionIo {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol);
}
impl<S, Io, Fut1, Fut2, Res> Future for TlsConnectorFuture<S, Fut1, Fut2>
where
S: Service<
TcpConnection<Uri, Io>,
Response = Res,
Error = std::io::Error,
Future = Fut2,
>,
S::Response: IntoConnectionIo,
Fut1: Future<Output = Result<TcpConnection<Uri, Io>, ConnectError>>,
Fut2: Future<Output = Result<S::Response, S::Error>>,
Io: ConnectionIo,
{
type Output = Result<(Box<dyn ConnectionIo>, Protocol), ConnectError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.as_mut().project() {
TlsConnectorProj::TcpConnect {
fut,
tls_service,
timeout,
} => {
let res = ready!(fut.poll(cx))?;
let fut = tls_service
.take()
.expect("TlsConnectorFuture polled after complete")
.call(res);
let timeout = sleep(*timeout);
self.set(TlsConnectorFuture::TlsConnect { fut, timeout });
self.poll(cx)
}
TlsConnectorProj::TlsConnect { fut, timeout } => match fut.poll(cx)? {
Poll::Ready(res) => Poll::Ready(Ok(res.into_connection_io())),
Poll::Pending => timeout.poll(cx).map(|_| Err(ConnectError::Timeout)),
},
}
}
}
/// service for establish tcp connection.
/// operation is canceled when timeout limit reached.
#[derive(Clone)]
pub struct TcpConnectorInnerService<S: Clone> {
service: S,
timeout: Duration,
local_address: Option<std::net::IpAddr>,
}
impl<S: Clone> TcpConnectorInnerService<S> {
fn new(
service: S,
timeout: Duration,
local_address: Option<std::net::IpAddr>,
) -> Self {
Self {
service,
timeout,
local_address,
}
}
}
impl<S, Io> Service<Connect> for TcpConnectorInnerService<S>
where
S: Service<
TcpConnect<Uri>,
Response = TcpConnection<Uri, Io>,
Error = TcpConnectError,
> + Clone
+ 'static,
{
type Response = S::Response;
type Error = ConnectError;
type Future = TcpConnectorInnerFuture<S::Future>;
actix_service::forward_ready!(service);
fn call(&self, req: Connect) -> Self::Future {
let mut req = TcpConnect::new(req.uri).set_addr(req.addr);
if let Some(local_addr) = self.local_address {
req = req.set_local_addr(local_addr);
}
TcpConnectorInnerFuture {
fut: self.service.call(req),
timeout: sleep(self.timeout),
}
}
}
#[pin_project]
pub struct TcpConnectorInnerFuture<Fut> {
#[pin]
fut: Fut,
#[pin]
timeout: Sleep,
}
impl<Fut, Io> Future for TcpConnectorInnerFuture<Fut>
where
Fut: Future<Output = Result<TcpConnection<Uri, Io>, TcpConnectError>>,
{
type Output = Result<TcpConnection<Uri, Io>, ConnectError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this.fut.poll(cx) {
Poll::Ready(res) => Poll::Ready(res.map_err(ConnectError::from)),
Poll::Pending => this.timeout.poll(cx).map(|_| Err(ConnectError::Timeout)),
}
}
}
/// Connector service for pooled Plain/Tls Tcp connections.
pub type ConnectorService<S, Io> = ConnectorServicePriv<
TcpConnectorService<TcpConnectorInnerService<S>>,
Rc<
dyn Service<
Connect,
Response = (Box<dyn ConnectionIo>, Protocol),
Error = ConnectError,
Future = LocalBoxFuture<
'static,
Result<(Box<dyn ConnectionIo>, Protocol), ConnectError>,
>,
>,
>,
Io,
Box<dyn ConnectionIo>,
>;
pub struct ConnectorServicePriv<S1, S2, Io1, Io2>
where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>,
Io1: ConnectionIo,
Io2: ConnectionIo,
{
tcp_pool: ConnectionPool<S1, Io1>,
tls_pool: Option<ConnectionPool<S2, Io2>>,
}
impl<S1, S2, Io1, Io2> Service<Connect> for ConnectorServicePriv<S1, S2, Io1, Io2>
where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>
+ Clone
+ 'static,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>
+ Clone
+ 'static,
Io1: ConnectionIo,
Io2: ConnectionIo,
{
type Response = Connection<Io1, Io2>;
type Error = ConnectError;
type Future = ConnectorServiceFuture<S1, S2, Io1, Io2>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
ready!(self.tcp_pool.poll_ready(cx))?;
if let Some(ref tls_pool) = self.tls_pool {
ready!(tls_pool.poll_ready(cx))?;
}
Poll::Ready(Ok(()))
}
fn call(&self, req: Connect) -> Self::Future {
match req.uri.scheme_str() {
Some("https") | Some("wss") => match self.tls_pool {
None => ConnectorServiceFuture::SslIsNotSupported,
Some(ref pool) => ConnectorServiceFuture::Tls(pool.call(req)),
},
_ => ConnectorServiceFuture::Tcp(self.tcp_pool.call(req)),
}
}
}
#[pin_project(project = ConnectorServiceProj)]
pub enum ConnectorServiceFuture<S1, S2, Io1, Io2>
where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>
+ Clone
+ 'static,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>
+ Clone
+ 'static,
Io1: ConnectionIo,
Io2: ConnectionIo,
{
Tcp(#[pin] <ConnectionPool<S1, Io1> as Service<Connect>>::Future),
Tls(#[pin] <ConnectionPool<S2, Io2> as Service<Connect>>::Future),
SslIsNotSupported,
}
impl<S1, S2, Io1, Io2> Future for ConnectorServiceFuture<S1, S2, Io1, Io2>
where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>
+ Clone
+ 'static,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>
+ Clone
+ 'static,
Io1: ConnectionIo,
Io2: ConnectionIo,
{
type Output = Result<Connection<Io1, Io2>, ConnectError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.project() {
ConnectorServiceProj::Tcp(fut) => fut.poll(cx).map_ok(Connection::Tcp),
ConnectorServiceProj::Tls(fut) => fut.poll(cx).map_ok(Connection::Tls),
ConnectorServiceProj::SslIsNotSupported => {
Poll::Ready(Err(ConnectError::SslIsNotSupported))
} }
} }
} }
} }
#[cfg(any(feature = "openssl", feature = "rustls"))] #[cfg(not(feature = "trust-dns"))]
mod connect_impl { mod resolver {
use std::future::Future; use super::*;
use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
use futures_core::ready; pub(super) fn resolver() -> Resolver {
use futures_util::future::Either; 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 super::*;
use crate::client::connection::EitherConnection;
pub(crate) struct InnerConnector<T1, T2, Io1, Io2> pub(super) fn resolver() -> Resolver {
where // new type for impl Resolve trait for TokioAsyncResolver.
Io1: AsyncRead + AsyncWrite + Unpin + 'static, struct TrustDnsResolver(TokioAsyncResolver);
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>,
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>,
{
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> impl Resolve for TrustDnsResolver {
where fn lookup<'a>(
Io1: AsyncRead + AsyncWrite + Unpin + 'static, &'a self,
Io2: AsyncRead + AsyncWrite + Unpin + 'static, host: &'a str,
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static, port: u16,
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static, ) -> LocalBoxFuture<'a, Result<Vec<SocketAddr>, Box<dyn std::error::Error>>>
{ {
fn clone(&self) -> Self { Box::pin(async move {
InnerConnector { let res = self
tcp_pool: self.tcp_pool.clone(), .0
ssl_pool: self.ssl_pool.clone(), .lookup_ip(host)
.await?
.iter()
.map(|ip| SocketAddr::new(ip, port))
.collect();
Ok(res)
})
} }
} }
}
impl<T1, T2, Io1, Io2> Service<Connect> for InnerConnector<T1, T2, Io1, Io2> // dns struct is cached in thread local.
where // so new client constructor can reuse the existing dns resolver.
Io1: AsyncRead + AsyncWrite + Unpin + 'static, thread_local! {
Io2: AsyncRead + AsyncWrite + Unpin + 'static, static TRUST_DNS_RESOLVER: RefCell<Option<Resolver>> = RefCell::new(None);
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
{
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 { // get from thread local or construct a new trust-dns resolver.
match req.uri.scheme_str() { TRUST_DNS_RESOLVER.with(|local| {
Some("https") | Some("wss") => Either::Right(InnerConnectorResponseB { let resolver = local.borrow().as_ref().map(Clone::clone);
fut: self.ssl_pool.call(req), match resolver {
_phantom: PhantomData, Some(resolver) => resolver,
}), None => {
_ => Either::Left(InnerConnectorResponseA { let (cfg, opts) = match read_system_conf() {
fut: self.tcp_pool.call(req), Ok((cfg, opts)) => (cfg, opts),
_phantom: PhantomData, 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
}
} }
} })
}
#[pin_project::pin_project]
pub(crate) struct InnerConnectorResponseA<T, Io1, Io2>
where
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
{
#[pin]
fut: <ConnectionPool<T, Io1> as Service<Connect>>::Future,
_phantom: PhantomData<Io2>,
}
impl<T, Io1, Io2> Future for InnerConnectorResponseA<T, Io1, Io2>
where
T: Service<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),
)
}
}
#[pin_project::pin_project]
pub(crate) struct InnerConnectorResponseB<T, Io1, Io2>
where
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
{
#[pin]
fut: <ConnectionPool<T, Io2> as Service<Connect>>::Future,
_phantom: PhantomData<Io1>,
}
impl<T, Io1, Io2> Future for InnerConnectorResponseB<T, Io1, Io2>
where
T: Service<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),
)
}
} }
} }

View File

@ -1,16 +1,16 @@
use std::io; use std::{error::Error as StdError, fmt, io};
use actix_tls::connect::resolver::ResolveError;
use derive_more::{Display, From}; use derive_more::{Display, From};
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
use actix_tls::accept::openssl::SslError; use actix_tls::accept::openssl::SslError;
use crate::error::{Error, ParseError, ResponseError}; use crate::error::{Error, ParseError};
use crate::http::{Error as HttpError, StatusCode}; use crate::http::Error as HttpError;
/// A set of errors that can occur while connecting to an HTTP host /// A set of errors that can occur while connecting to an HTTP host
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[non_exhaustive]
pub enum ConnectError { pub enum ConnectError {
/// SSL feature is not enabled /// SSL feature is not enabled
#[display(fmt = "SSL is not supported")] #[display(fmt = "SSL is not supported")]
@ -23,10 +23,10 @@ pub enum ConnectError {
/// Failed to resolve the hostname /// Failed to resolve the hostname
#[display(fmt = "Failed resolving hostname: {}", _0)] #[display(fmt = "Failed resolving hostname: {}", _0)]
Resolver(ResolveError), Resolver(Box<dyn std::error::Error>),
/// No dns records /// No dns records
#[display(fmt = "No dns records found for the input")] #[display(fmt = "No DNS records found for the input")]
NoRecords, NoRecords,
/// Http2 error /// Http2 error
@ -65,14 +65,18 @@ impl From<actix_tls::connect::ConnectError> for ConnectError {
} }
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[non_exhaustive]
pub enum InvalidUrl { pub enum InvalidUrl {
#[display(fmt = "Missing url scheme")] #[display(fmt = "Missing URL scheme")]
MissingScheme, MissingScheme,
#[display(fmt = "Unknown url scheme")]
#[display(fmt = "Unknown URL scheme")]
UnknownScheme, UnknownScheme,
#[display(fmt = "Missing host name")] #[display(fmt = "Missing host name")]
MissingHost, MissingHost,
#[display(fmt = "Url parse error: {}", _0)]
#[display(fmt = "URL parse error: {}", _0)]
HttpError(http::Error), HttpError(http::Error),
} }
@ -80,66 +84,73 @@ impl std::error::Error for InvalidUrl {}
/// A set of errors that can occur during request sending and response reading /// A set of errors that can occur during request sending and response reading
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[non_exhaustive]
pub enum SendRequestError { pub enum SendRequestError {
/// Invalid URL /// Invalid URL
#[display(fmt = "Invalid URL: {}", _0)] #[display(fmt = "Invalid URL: {}", _0)]
Url(InvalidUrl), Url(InvalidUrl),
/// Failed to connect to host /// Failed to connect to host
#[display(fmt = "Failed to connect to host: {}", _0)] #[display(fmt = "Failed to connect to host: {}", _0)]
Connect(ConnectError), Connect(ConnectError),
/// Error sending request /// Error sending request
Send(io::Error), Send(io::Error),
/// Error parsing response /// Error parsing response
Response(ParseError), Response(ParseError),
/// Http error /// Http error
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Http(HttpError), Http(HttpError),
/// Http2 error /// Http2 error
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
H2(h2::Error), H2(h2::Error),
/// Response took too long /// Response took too long
#[display(fmt = "Timeout while waiting for response")] #[display(fmt = "Timeout while waiting for response")]
Timeout, 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")] #[display(fmt = "Tunnels are not supported for http2 connection")]
TunnelNotSupported, TunnelNotSupported,
/// Error sending request body /// Error sending request body
Body(Error), Body(Error),
/// Other errors that can occur after submitting a request.
#[display(fmt = "{:?}: {}", _1, _0)]
Custom(Box<dyn StdError>, Box<dyn fmt::Debug>),
} }
impl std::error::Error for SendRequestError {} impl std::error::Error for SendRequestError {}
/// Convert `SendRequestError` to a server `Response`
impl ResponseError for SendRequestError {
fn status_code(&self) -> StatusCode {
match *self {
SendRequestError::Connect(ConnectError::Timeout) => {
StatusCode::GATEWAY_TIMEOUT
}
SendRequestError::Connect(_) => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
/// A set of errors that can occur during freezing a request /// A set of errors that can occur during freezing a request
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[non_exhaustive]
pub enum FreezeRequestError { pub enum FreezeRequestError {
/// Invalid URL /// Invalid URL
#[display(fmt = "Invalid URL: {}", _0)] #[display(fmt = "Invalid URL: {}", _0)]
Url(InvalidUrl), Url(InvalidUrl),
/// Http error
/// HTTP error
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Http(HttpError), Http(HttpError),
/// Other errors that can occur after submitting a request.
#[display(fmt = "{:?}: {}", _1, _0)]
Custom(Box<dyn StdError>, Box<dyn fmt::Debug>),
} }
impl std::error::Error for FreezeRequestError {} impl std::error::Error for FreezeRequestError {}
impl From<FreezeRequestError> for SendRequestError { impl From<FreezeRequestError> for SendRequestError {
fn from(e: FreezeRequestError) -> Self { fn from(err: FreezeRequestError) -> Self {
match e { match err {
FreezeRequestError::Url(e) => e.into(), FreezeRequestError::Url(err) => err.into(),
FreezeRequestError::Http(e) => e.into(), FreezeRequestError::Http(err) => err.into(),
FreezeRequestError::Custom(err, msg) => SendRequestError::Custom(err, msg),
} }
} }
} }

View File

@ -1,37 +1,38 @@
use std::io::Write; use std::{
use std::pin::Pin; io::Write,
use std::task::{Context, Poll}; pin::Pin,
use std::{io, time}; task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf}; use actix_codec::Framed;
use actix_utils::future::poll_fn;
use bytes::buf::BufMut; use bytes::buf::BufMut;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::Stream; use futures_core::{ready, Stream};
use futures_util::future::poll_fn; use futures_util::SinkExt as _;
use futures_util::{pin_mut, SinkExt, StreamExt};
use crate::error::PayloadError;
use crate::h1; use crate::h1;
use crate::header::HeaderMap; use crate::http::{
use crate::http::header::{IntoHeaderValue, HOST}; header::{HeaderMap, IntoHeaderValue, EXPECT, HOST},
StatusCode,
};
use crate::message::{RequestHeadType, ResponseHead}; use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::{Payload, PayloadStream}; use crate::payload::Payload;
use crate::{error::PayloadError, Error};
use super::connection::{ConnectionLifetime, ConnectionType, IoConnection}; use super::connection::{ConnectionIo, H1Connection};
use super::error::{ConnectError, SendRequestError}; use super::error::{ConnectError, SendRequestError};
use super::pool::Acquired;
use crate::body::{BodySize, MessageBody}; use crate::body::{BodySize, MessageBody};
pub(crate) async fn send_request<T, B>( pub(crate) async fn send_request<Io, B>(
io: T, io: H1Connection<Io>,
mut head: RequestHeadType, mut head: RequestHeadType,
body: B, body: B,
created: time::Instant,
pool: Option<Acquired<T>>,
) -> Result<(ResponseHead, Payload), SendRequestError> ) -> Result<(ResponseHead, Payload), SendRequestError>
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
// set request host header // set request host header
if !head.as_ref().headers.contains_key(HOST) if !head.as_ref().headers.contains_key(HOST)
@ -40,19 +41,19 @@ where
if let Some(host) = head.as_ref().uri.host() { if let Some(host) = head.as_ref().uri.host() {
let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); let mut wrt = BytesMut::with_capacity(host.len() + 5).writer();
let _ = match head.as_ref().uri.port_u16() { match head.as_ref().uri.port_u16() {
None | Some(80) | Some(443) => write!(wrt, "{}", host), None | Some(80) | Some(443) => write!(wrt, "{}", host)?,
Some(port) => write!(wrt, "{}:{}", host, port), 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 { Ok(value) => match head {
RequestHeadType::Owned(ref mut head) => { RequestHeadType::Owned(ref mut head) => {
head.headers.insert(HOST, value) head.headers.insert(HOST, value);
} }
RequestHeadType::Rc(_, ref mut extra_headers) => { RequestHeadType::Rc(_, ref mut extra_headers) => {
let headers = extra_headers.get_or_insert(HeaderMap::new()); 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), Err(e) => log::error!("Can not set HOST header {}", e),
@ -60,82 +61,112 @@ where
} }
} }
let io = H1Connection { // create Framed and prepare sending request
created, let mut framed = Framed::new(io, h1::ClientCodec::default());
pool,
io: Some(io),
};
// create Framed and send request // Check EXPECT header and enable expect handle flag accordingly.
let mut framed_inner = Framed::new(io, h1::ClientCodec::default()); //
framed_inner.send((head, body.size()).into()).await?; // 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 keep_alive = framed.codec_ref().keepalive();
framed.io_mut().on_release(keep_alive);
// send request body // TODO: use a new variant or a new type better describing error violate
match body.size() { // `Requirements for clients` session of above RFC
BodySize::None | BodySize::Empty | BodySize::Sized(0) => {} return Err(SendRequestError::Connect(ConnectError::Disconnected));
_ => send_body(body, Pin::new(&mut framed_inner)).await?, }
}; _ => true,
}
// 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 { } else {
return Err(SendRequestError::from(ConnectError::Disconnected)); false
}; };
match framed.codec_ref().message_type() { 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_framed.as_mut()).await?,
};
// read response and init read body
let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx))
.await
.ok_or(ConnectError::Disconnected)??;
res_head = Some(head);
}
let head = res_head.unwrap();
match pin_framed.codec_ref().message_type() {
h1::MessageType::None => { h1::MessageType::None => {
let force_close = !framed.codec_ref().keepalive(); let keep_alive = pin_framed.codec_ref().keepalive();
release_connection(framed, force_close); pin_framed.io_mut().on_release(keep_alive);
Ok((head, Payload::None)) Ok((head, Payload::None))
} }
_ => { _ => Ok((head, Payload::Stream(Box::pin(PlStream::new(framed))))),
let pl: PayloadStream = PlStream::new(framed_inner).boxed_local();
Ok((head, pl.into()))
}
} }
} }
pub(crate) async fn open_tunnel<T>( pub(crate) async fn open_tunnel<Io>(
io: T, io: Io,
head: RequestHeadType, head: RequestHeadType,
) -> Result<(ResponseHead, Framed<T, h1::ClientCodec>), SendRequestError> ) -> Result<(ResponseHead, Framed<Io, h1::ClientCodec>), SendRequestError>
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, Io: ConnectionIo,
{ {
// create Framed and send request // create Framed and send request.
let mut framed = Framed::new(io, h1::ClientCodec::default()); let mut framed = Framed::new(io, h1::ClientCodec::default());
framed.send((head, BodySize::None).into()).await?; framed.send((head, BodySize::None).into()).await?;
// read response // read response head.
if let (Some(result), framed) = framed.into_future().await { let head = poll_fn(|cx| Pin::new(&mut framed).poll_next(cx))
let head = result.map_err(SendRequestError::from)?; .await
Ok((head, framed)) .ok_or(ConnectError::Disconnected)??;
} else {
Err(SendRequestError::from(ConnectError::Disconnected)) Ok((head, framed))
}
} }
/// send request body to the peer /// send request body to the peer
pub(crate) async fn send_body<T, B>( pub(crate) async fn send_body<Io, B>(
body: B, body: B,
mut framed: Pin<&mut Framed<T, h1::ClientCodec>>, mut framed: Pin<&mut Framed<Io, h1::ClientCodec>>,
) -> Result<(), SendRequestError> ) -> Result<(), SendRequestError>
where where
T: ConnectionLifetime + Unpin, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
pin_mut!(body); actix_rt::pin!(body);
let mut eof = false; let mut eof = false;
while !eof { while !eof {
while !eof && !framed.as_ref().is_write_buf_full() { while !eof && !framed.as_ref().is_write_buf_full() {
match poll_fn(|cx| body.as_mut().poll_next(cx)).await { match poll_fn(|cx| body.as_mut().poll_next(cx)).await {
Some(result) => { Some(Ok(chunk)) => {
framed.as_mut().write(h1::Message::Chunk(Some(result?)))?; framed.as_mut().write(h1::Message::Chunk(Some(chunk)))?;
} }
Some(Err(err)) => return Err(err.into().into()),
None => { None => {
eof = true; eof = true;
framed.as_mut().write(h1::Message::Chunk(None))?; framed.as_mut().write(h1::Message::Chunk(None))?;
@ -159,101 +190,25 @@ where
} }
} }
SinkExt::flush(Pin::into_inner(framed)).await?; framed.get_mut().flush().await?;
Ok(()) Ok(())
} }
#[doc(hidden)]
/// HTTP client connection
pub struct H1Connection<T> {
/// T should be `Unpin`
io: Option<T>,
created: time::Instant,
pool: Option<Acquired<T>>,
}
impl<T> ConnectionLifetime for H1Connection<T>
where
T: AsyncRead + AsyncWrite + Unpin + 'static,
{
/// Close connection
fn close(mut self: Pin<&mut Self>) {
if let Some(mut pool) = self.pool.take() {
if let Some(io) = self.io.take() {
pool.close(IoConnection::new(
ConnectionType::H1(io),
self.created,
None,
));
}
}
}
/// Release this connection to the connection pool
fn release(mut self: Pin<&mut Self>) {
if let Some(mut pool) = self.pool.take() {
if let Some(io) = self.io.take() {
pool.release(IoConnection::new(
ConnectionType::H1(io),
self.created,
None,
));
}
}
}
}
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncRead for H1Connection<T> {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.io.as_mut().unwrap()).poll_read(cx, buf)
}
}
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncWrite for H1Connection<T> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.io.as_mut().unwrap()).poll_write(cx, buf)
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<io::Result<()>> {
Pin::new(self.io.as_mut().unwrap()).poll_flush(cx)
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), io::Error>> {
Pin::new(self.io.as_mut().unwrap()).poll_shutdown(cx)
}
}
#[pin_project::pin_project] #[pin_project::pin_project]
pub(crate) struct PlStream<Io> { pub(crate) struct PlStream<Io: ConnectionIo> {
#[pin] #[pin]
framed: Option<Framed<Io, h1::ClientPayloadCodec>>, framed: Framed<H1Connection<Io>, h1::ClientPayloadCodec>,
} }
impl<Io: ConnectionLifetime> PlStream<Io> { impl<Io: ConnectionIo> PlStream<Io> {
fn new(framed: Framed<Io, h1::ClientCodec>) -> Self { fn new(framed: Framed<H1Connection<Io>, h1::ClientCodec>) -> Self {
let framed = framed.into_map_codec(|codec| codec.into_payload_codec()); let framed = framed.into_map_codec(|codec| codec.into_payload_codec());
PlStream { PlStream { framed }
framed: Some(framed),
}
} }
} }
impl<Io: ConnectionLifetime> Stream for PlStream<Io> { impl<Io: ConnectionIo> Stream for PlStream<Io> {
type Item = Result<Bytes, PayloadError>; type Item = Result<Bytes, PayloadError>;
fn poll_next( fn poll_next(
@ -262,30 +217,14 @@ impl<Io: ConnectionLifetime> Stream for PlStream<Io> {
) -> Poll<Option<Self::Item>> { ) -> Poll<Option<Self::Item>> {
let mut this = self.project(); let mut this = self.project();
match this.framed.as_mut().as_pin_mut().unwrap().next_item(cx)? { match ready!(this.framed.as_mut().next_item(cx)?) {
Poll::Pending => Poll::Pending, Some(Some(chunk)) => Poll::Ready(Some(Ok(chunk))),
Poll::Ready(Some(chunk)) => { Some(None) => {
if let Some(chunk) = chunk { let keep_alive = this.framed.codec_ref().keepalive();
Poll::Ready(Some(Ok(chunk))) this.framed.io_mut().on_release(keep_alive);
} else { Poll::Ready(None)
let framed = this.framed.as_mut().as_pin_mut().unwrap();
let force_close = !framed.codec_ref().keepalive();
release_connection(framed, force_close);
Poll::Ready(None)
}
} }
Poll::Ready(None) => Poll::Ready(None), None => Poll::Ready(None),
} }
} }
} }
fn release_connection<T, U>(framed: Pin<&mut Framed<T, U>>, force_close: bool)
where
T: ConnectionLifetime,
{
if !force_close && framed.is_read_buf_empty() && framed.is_write_buf_empty() {
framed.io_pin().release()
} else {
framed.io_pin().close()
}
}

View File

@ -1,11 +1,7 @@
use std::convert::TryFrom;
use std::future::Future; use std::future::Future;
use std::time;
use actix_codec::{AsyncRead, AsyncWrite}; use actix_utils::future::poll_fn;
use bytes::Bytes; use bytes::Bytes;
use futures_util::future::poll_fn;
use futures_util::pin_mut;
use h2::{ use h2::{
client::{Builder, Connection, SendRequest}, client::{Builder, Connection, SendRequest},
SendStream, SendStream,
@ -13,28 +9,32 @@ use h2::{
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING};
use http::{request::Request, Method, Version}; use http::{request::Request, Method, Version};
use crate::body::{BodySize, MessageBody}; use crate::{
use crate::header::HeaderMap; body::{BodySize, MessageBody},
use crate::message::{RequestHeadType, ResponseHead}; header::HeaderMap,
use crate::payload::Payload; message::{RequestHeadType, ResponseHead},
payload::Payload,
Error,
};
use super::config::ConnectorConfig; use super::{
use super::connection::{ConnectionType, IoConnection}; config::ConnectorConfig,
use super::error::SendRequestError; connection::{ConnectionIo, H2Connection},
use super::pool::Acquired; error::SendRequestError,
};
pub(crate) async fn send_request<T, B>( pub(crate) async fn send_request<Io, B>(
mut io: SendRequest<Bytes>, mut io: H2Connection<Io>,
head: RequestHeadType, head: RequestHeadType,
body: B, body: B,
created: time::Instant,
pool: Option<Acquired<T>>,
) -> Result<(ResponseHead, Payload), SendRequestError> ) -> Result<(ResponseHead, Payload), SendRequestError>
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
trace!("Sending client request: {:?} {:?}", head, body.size()); trace!("Sending client request: {:?} {:?}", head, body.size());
let head_req = head.as_ref().method == Method::HEAD; let head_req = head.as_ref().method == Method::HEAD;
let length = body.size(); let length = body.size();
let eof = matches!( let eof = matches!(
@ -60,10 +60,14 @@ where
BodySize::Empty => req BodySize::Empty => req
.headers_mut() .headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")), .insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
BodySize::Sized(len) => req.headers_mut().insert( BodySize::Sized(len) => {
CONTENT_LENGTH, let mut buf = itoa::Buffer::new();
HeaderValue::try_from(format!("{}", len)).unwrap(),
), req.headers_mut().insert(
CONTENT_LENGTH,
HeaderValue::from_str(buf.format(len)).unwrap(),
)
}
}; };
// Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate. // Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate.
@ -86,7 +90,10 @@ where
// copy headers // copy headers
for (key, value) in headers { for (key, value) in headers {
match *key { 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, CONTENT_LENGTH if skip_len => continue,
// DATE => has_date = true, // DATE => has_date = true,
_ => {} _ => {}
@ -96,13 +103,13 @@ where
let res = poll_fn(|cx| io.poll_ready(cx)).await; let res = poll_fn(|cx| io.poll_ready(cx)).await;
if let Err(e) = res { if let Err(e) = res {
release(io, pool, created, e.is_io()); io.on_release(e.is_io());
return Err(SendRequestError::from(e)); return Err(SendRequestError::from(e));
} }
let resp = match io.send_request(req, eof) { let resp = match io.send_request(req, eof) {
Ok((fut, send)) => { Ok((fut, send)) => {
release(io, pool, created, false); io.on_release(false);
if !eof { if !eof {
send_body(body, send).await?; send_body(body, send).await?;
@ -110,7 +117,7 @@ where
fut.await.map_err(SendRequestError::from)? fut.await.map_err(SendRequestError::from)?
} }
Err(e) => { Err(e) => {
release(io, pool, created, e.is_io()); io.on_release(e.is_io());
return Err(e.into()); return Err(e.into());
} }
}; };
@ -124,12 +131,16 @@ where
Ok((head, payload)) Ok((head, payload))
} }
async fn send_body<B: MessageBody>( async fn send_body<B>(
body: B, body: B,
mut send: SendStream<Bytes>, mut send: SendStream<Bytes>,
) -> Result<(), SendRequestError> { ) -> Result<(), SendRequestError>
where
B: MessageBody,
B::Error: Into<Error>,
{
let mut buf = None; let mut buf = None;
pin_mut!(body); actix_rt::pin!(body);
loop { loop {
if buf.is_none() { if buf.is_none() {
match poll_fn(|cx| body.as_mut().poll_next(cx)).await { match poll_fn(|cx| body.as_mut().poll_next(cx)).await {
@ -137,7 +148,7 @@ async fn send_body<B: MessageBody>(
send.reserve_capacity(b.len()); send.reserve_capacity(b.len());
buf = Some(b); buf = Some(b);
} }
Some(Err(e)) => return Err(e.into()), Some(Err(e)) => return Err(e.into().into()),
None => { None => {
if let Err(e) = send.send_data(Bytes::new(), true) { if let Err(e) = send.send_data(Bytes::new(), true) {
return Err(e.into()); return Err(e.into());
@ -171,28 +182,10 @@ async fn send_body<B: MessageBody>(
} }
} }
// release SendRequest object pub(crate) fn handshake<Io: ConnectionIo>(
fn release<T: AsyncRead + AsyncWrite + Unpin + 'static>(
io: SendRequest<Bytes>,
pool: Option<Acquired<T>>,
created: time::Instant,
close: bool,
) {
if let Some(mut pool) = pool {
if close {
pool.close(IoConnection::new(ConnectionType::H2(io), created, None));
} else {
pool.release(IoConnection::new(ConnectionType::H2(io), created, None));
}
}
}
pub(crate) fn handshake<Io>(
io: Io, io: Io,
config: &ConnectorConfig, config: &ConnectorConfig,
) -> impl Future<Output = Result<(SendRequest<Bytes>, Connection<Io, Bytes>), h2::Error>> ) -> impl Future<Output = Result<(SendRequest<Bytes>, Connection<Io, Bytes>), h2::Error>>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
let mut builder = Builder::new(); let mut builder = Builder::new();
builder builder

View File

@ -1,4 +1,5 @@
//! Http client api //! HTTP client.
use http::Uri; use http::Uri;
mod config; mod config;
@ -9,10 +10,14 @@ mod h1proto;
mod h2proto; mod h2proto;
mod pool; mod pool;
pub use self::connection::Connection; pub use actix_tls::connect::{
pub use self::connector::Connector; Connect as TcpConnect, ConnectError as TcpConnectError, Connection as TcpConnection,
};
pub use self::connection::{Connection, ConnectionIo};
pub use self::connector::{Connector, ConnectorService};
pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
pub use self::pool::Protocol; pub use crate::Protocol;
#[derive(Clone)] #[derive(Clone)]
pub struct Connect { pub struct Connect {

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,14 @@ use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
use std::{fmt, net}; use std::{fmt, net};
use actix_rt::time::{sleep, sleep_until, Instant, Sleep}; use actix_rt::{
task::JoinHandle,
time::{interval, sleep_until, Instant, Sleep},
};
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::{future, FutureExt};
use time::OffsetDateTime; 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; const DATE_VALUE_LENGTH: usize = 29;
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Clone, Copy)]
@ -49,7 +51,7 @@ struct Inner {
ka_enabled: bool, ka_enabled: bool,
secure: bool, secure: bool,
local_addr: Option<std::net::SocketAddr>, local_addr: Option<std::net::SocketAddr>,
timer: DateService, date_service: DateService,
} }
impl Clone for ServiceConfig { impl Clone for ServiceConfig {
@ -91,42 +93,40 @@ impl ServiceConfig {
client_disconnect, client_disconnect,
secure, secure,
local_addr, local_addr,
timer: DateService::new(), date_service: DateService::new(),
})) }))
} }
/// Returns true if connection is secure (HTTPS)
#[inline] #[inline]
/// Returns true if connection is secure(https)
pub fn secure(&self) -> bool { pub fn secure(&self) -> bool {
self.0.secure self.0.secure
} }
#[inline]
/// Returns the local address that this server is bound to. /// Returns the local address that this server is bound to.
#[inline]
pub fn local_addr(&self) -> Option<net::SocketAddr> { pub fn local_addr(&self) -> Option<net::SocketAddr> {
self.0.local_addr self.0.local_addr
} }
#[inline]
/// Keep alive duration if configured. /// Keep alive duration if configured.
#[inline]
pub fn keep_alive(&self) -> Option<Duration> { pub fn keep_alive(&self) -> Option<Duration> {
self.0.keep_alive self.0.keep_alive
} }
#[inline]
/// Return state of connection keep-alive functionality /// Return state of connection keep-alive functionality
#[inline]
pub fn keep_alive_enabled(&self) -> bool { pub fn keep_alive_enabled(&self) -> bool {
self.0.ka_enabled self.0.ka_enabled
} }
#[inline]
/// Client timeout for first request. /// Client timeout for first request.
#[inline]
pub fn client_timer(&self) -> Option<Sleep> { pub fn client_timer(&self) -> Option<Sleep> {
let delay_time = self.0.client_timeout; let delay_time = self.0.client_timeout;
if delay_time != 0 { if delay_time != 0 {
Some(sleep_until( Some(sleep_until(self.now() + Duration::from_millis(delay_time)))
self.0.timer.now() + Duration::from_millis(delay_time),
))
} else { } else {
None None
} }
@ -136,7 +136,7 @@ impl ServiceConfig {
pub fn client_timer_expire(&self) -> Option<Instant> { pub fn client_timer_expire(&self) -> Option<Instant> {
let delay = self.0.client_timeout; let delay = self.0.client_timeout;
if delay != 0 { if delay != 0 {
Some(self.0.timer.now() + Duration::from_millis(delay)) Some(self.now() + Duration::from_millis(delay))
} else { } else {
None None
} }
@ -146,7 +146,7 @@ impl ServiceConfig {
pub fn client_disconnect_timer(&self) -> Option<Instant> { pub fn client_disconnect_timer(&self) -> Option<Instant> {
let delay = self.0.client_disconnect; let delay = self.0.client_disconnect;
if delay != 0 { if delay != 0 {
Some(self.0.timer.now() + Duration::from_millis(delay)) Some(self.now() + Duration::from_millis(delay))
} else { } else {
None None
} }
@ -155,25 +155,17 @@ impl ServiceConfig {
#[inline] #[inline]
/// Return keep-alive timer delay is configured. /// Return keep-alive timer delay is configured.
pub fn keep_alive_timer(&self) -> Option<Sleep> { pub fn keep_alive_timer(&self) -> Option<Sleep> {
if let Some(ka) = self.0.keep_alive { self.keep_alive().map(|ka| sleep_until(self.now() + ka))
Some(sleep_until(self.0.timer.now() + ka))
} else {
None
}
} }
/// Keep-alive expire time /// Keep-alive expire time
pub fn keep_alive_expire(&self) -> Option<Instant> { pub fn keep_alive_expire(&self) -> Option<Instant> {
if let Some(ka) = self.0.keep_alive { self.keep_alive().map(|ka| self.now() + ka)
Some(self.0.timer.now() + ka)
} else {
None
}
} }
#[inline] #[inline]
pub(crate) fn now(&self) -> Instant { pub(crate) fn now(&self) -> Instant {
self.0.timer.now() self.0.date_service.now()
} }
#[doc(hidden)] #[doc(hidden)]
@ -181,7 +173,7 @@ impl ServiceConfig {
let mut buf: [u8; 39] = [0; 39]; let mut buf: [u8; 39] = [0; 39];
buf[..6].copy_from_slice(b"date: "); buf[..6].copy_from_slice(b"date: ");
self.0 self.0
.timer .date_service
.set_date(|date| buf[6..35].copy_from_slice(&date.bytes)); .set_date(|date| buf[6..35].copy_from_slice(&date.bytes));
buf[35..].copy_from_slice(b"\r\n\r\n"); buf[35..].copy_from_slice(b"\r\n\r\n");
dst.extend_from_slice(&buf); dst.extend_from_slice(&buf);
@ -189,7 +181,7 @@ impl ServiceConfig {
pub(crate) fn set_date_header(&self, dst: &mut BytesMut) { pub(crate) fn set_date_header(&self, dst: &mut BytesMut) {
self.0 self.0
.timer .date_service
.set_date(|date| dst.extend_from_slice(&date.bytes)); .set_date(|date| dst.extend_from_slice(&date.bytes));
} }
} }
@ -230,57 +222,103 @@ impl fmt::Write for Date {
} }
} }
#[derive(Clone)] /// Service for update Date and Instant periodically at 500 millis interval.
struct DateService(Rc<DateServiceInner>); struct DateService {
current: Rc<Cell<(Date, Instant)>>,
struct DateServiceInner { handle: JoinHandle<()>,
current: Cell<Option<(Date, Instant)>>,
} }
impl DateServiceInner { impl Drop for DateService {
fn new() -> Self { fn drop(&mut self) {
DateServiceInner { // stop the timer update async task on drop.
current: Cell::new(None), self.handle.abort();
}
}
fn reset(&self) {
self.current.take();
}
fn update(&self) {
let now = Instant::now();
let date = Date::new();
self.current.set(Some((date, now)));
} }
} }
impl DateService { impl DateService {
fn new() -> Self { 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) { let mut interval = interval(Duration::from_millis(500));
if self.0.current.get().is_none() { loop {
self.0.update(); let now = interval.tick().await;
let date = Date::new();
current_clone.set((date, now));
}
});
// periodic date update DateService { current, handle }
let s = self.clone();
actix_rt::spawn(sleep(Duration::from_millis(500)).then(move |_| {
s.0.reset();
future::ready(())
}));
}
} }
fn now(&self) -> Instant { fn now(&self) -> Instant {
self.check_date(); self.current.get().1
self.0.current.get().unwrap().1
} }
fn set_date<F: FnMut(&Date)>(&self, mut f: F) { fn set_date<F: FnMut(&Date)>(&self, mut f: F) {
self.check_date(); f(&self.current.get().0);
f(&self.0.current.get().unwrap().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 +326,53 @@ impl DateService {
mod tests { mod tests {
use super::*; use super::*;
// Test modifying the date from within the closure use actix_rt::task::yield_now;
// passed to `set_date`
#[test] #[actix_rt::test]
fn test_evil_date() { async fn test_date_service_update() {
let service = DateService::new(); let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None);
// Make sure that `check_date` doesn't try to spawn a task
service.0.update(); yield_now().await;
service.set_date(|_| service.0.reset());
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] #[test]

View File

@ -1,25 +1,38 @@
use std::future::Future; //! Stream decoders.
use std::io::{self, Write};
use std::pin::Pin;
use std::task::{Context, Poll};
use actix_threadpool::{run, CpuFuture}; use std::{
use brotli2::write::BrotliDecoder; future::Future,
io::{self, Write as _},
pin::Pin,
task::{Context, Poll},
};
use actix_rt::task::{spawn_blocking, JoinHandle};
use bytes::Bytes; use bytes::Bytes;
use flate2::write::{GzDecoder, ZlibDecoder};
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use super::Writer; #[cfg(feature = "compress-brotli")]
use crate::error::PayloadError; use brotli2::write::BrotliDecoder;
use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING};
const INPLACE: usize = 2049; #[cfg(feature = "compress-gzip")]
use flate2::write::{GzDecoder, ZlibDecoder};
#[cfg(feature = "compress-zstd")]
use zstd::stream::write::Decoder as ZstdDecoder;
use crate::{
encoding::Writer,
error::{BlockingError, PayloadError},
http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING},
};
const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049;
pub struct Decoder<S> { pub struct Decoder<S> {
decoder: Option<ContentDecoder>, decoder: Option<ContentDecoder>,
stream: S, stream: S,
eof: bool, eof: bool,
fut: Option<CpuFuture<(Option<Bytes>, ContentDecoder), io::Error>>, fut: Option<JoinHandle<Result<(Option<Bytes>, ContentDecoder), io::Error>>>,
} }
impl<S> Decoder<S> impl<S> Decoder<S>
@ -30,17 +43,28 @@ where
#[inline] #[inline]
pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> { pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
let decoder = match encoding { let decoder = match encoding {
#[cfg(feature = "compress-brotli")]
ContentEncoding::Br => Some(ContentDecoder::Br(Box::new( ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(
BrotliDecoder::new(Writer::new()), BrotliDecoder::new(Writer::new()),
))), ))),
#[cfg(feature = "compress-gzip")]
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
ZlibDecoder::new(Writer::new()), ZlibDecoder::new(Writer::new()),
))), ))),
#[cfg(feature = "compress-gzip")]
ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new( ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(
GzDecoder::new(Writer::new()), GzDecoder::new(Writer::new()),
))), ))),
#[cfg(feature = "compress-zstd")]
ContentEncoding::Zstd => Some(ContentDecoder::Zstd(Box::new(
ZstdDecoder::new(Writer::new()).expect(
"Failed to create zstd decoder. This is a bug. \
Please report it to the actix-web repository.",
),
))),
_ => None, _ => None,
}; };
Decoder { Decoder {
decoder, decoder,
stream, stream,
@ -53,15 +77,11 @@ where
#[inline] #[inline]
pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder<S> { pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder<S> {
// check content-encoding // check content-encoding
let encoding = if let Some(enc) = headers.get(&CONTENT_ENCODING) { let encoding = headers
if let Ok(enc) = enc.to_str() { .get(&CONTENT_ENCODING)
ContentEncoding::from(enc) .and_then(|val| val.to_str().ok())
} else { .map(ContentEncoding::from)
ContentEncoding::Identity .unwrap_or(ContentEncoding::Identity);
}
} else {
ContentEncoding::Identity
};
Self::new(stream, encoding) Self::new(stream, encoding)
} }
@ -79,12 +99,12 @@ where
) -> Poll<Option<Self::Item>> { ) -> Poll<Option<Self::Item>> {
loop { loop {
if let Some(ref mut fut) = self.fut { if let Some(ref mut fut) = self.fut {
let (chunk, decoder) = match ready!(Pin::new(fut).poll(cx)) { let (chunk, decoder) =
Ok(item) => item, ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
Err(e) => return Poll::Ready(Some(Err(e.into()))),
};
self.decoder = Some(decoder); self.decoder = Some(decoder);
self.fut.take(); self.fut.take();
if let Some(chunk) = chunk { if let Some(chunk) = chunk {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
@ -94,29 +114,34 @@ where
return Poll::Ready(None); return Poll::Ready(None);
} }
match Pin::new(&mut self.stream).poll_next(cx) { match ready!(Pin::new(&mut self.stream).poll_next(cx)) {
Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), Some(Err(err)) => return Poll::Ready(Some(Err(err))),
Poll::Ready(Some(Ok(chunk))) => {
Some(Ok(chunk)) => {
if let Some(mut decoder) = self.decoder.take() { 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)?; let chunk = decoder.feed_data(chunk)?;
self.decoder = Some(decoder); self.decoder = Some(decoder);
if let Some(chunk) = chunk { if let Some(chunk) = chunk {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
} else { } else {
self.fut = Some(run(move || { self.fut = Some(spawn_blocking(move || {
let chunk = decoder.feed_data(chunk)?; let chunk = decoder.feed_data(chunk)?;
Ok((chunk, decoder)) Ok((chunk, decoder))
})); }));
} }
continue; continue;
} else { } else {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
} }
Poll::Ready(None) => {
None => {
self.eof = true; self.eof = true;
return if let Some(mut decoder) = self.decoder.take() { return if let Some(mut decoder) = self.decoder.take() {
match decoder.feed_eof() { match decoder.feed_eof() {
Ok(Some(res)) => Poll::Ready(Some(Ok(res))), Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
@ -127,25 +152,32 @@ where
Poll::Ready(None) Poll::Ready(None)
}; };
} }
Poll::Pending => break,
} }
} }
Poll::Pending
} }
} }
enum ContentDecoder { enum ContentDecoder {
#[cfg(feature = "compress-gzip")]
Deflate(Box<ZlibDecoder<Writer>>), Deflate(Box<ZlibDecoder<Writer>>),
#[cfg(feature = "compress-gzip")]
Gzip(Box<GzDecoder<Writer>>), Gzip(Box<GzDecoder<Writer>>),
#[cfg(feature = "compress-brotli")]
Br(Box<BrotliDecoder<Writer>>), Br(Box<BrotliDecoder<Writer>>),
// We need explicit 'static lifetime here because ZstdDecoder need lifetime
// argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static`
#[cfg(feature = "compress-zstd")]
Zstd(Box<ZstdDecoder<'static, Writer>>),
} }
impl ContentDecoder { impl ContentDecoder {
fn feed_eof(&mut self) -> io::Result<Option<Bytes>> { fn feed_eof(&mut self) -> io::Result<Option<Bytes>> {
match self { match self {
#[cfg(feature = "compress-brotli")]
ContentDecoder::Br(ref mut decoder) => match decoder.flush() { ContentDecoder::Br(ref mut decoder) => match decoder.flush() {
Ok(()) => { Ok(()) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))
} else { } else {
@ -154,7 +186,23 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
#[cfg(feature = "compress-gzip")]
ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() { ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() {
Ok(_) => {
let b = decoder.get_mut().take();
if !b.is_empty() {
Ok(Some(b))
} else {
Ok(None)
}
}
Err(e) => Err(e),
},
#[cfg(feature = "compress-gzip")]
ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() {
Ok(_) => { Ok(_) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
@ -165,7 +213,9 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() {
#[cfg(feature = "compress-zstd")]
ContentDecoder::Zstd(ref mut decoder) => match decoder.flush() {
Ok(_) => { Ok(_) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
@ -181,10 +231,12 @@ impl ContentDecoder {
fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> { fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> {
match self { match self {
#[cfg(feature = "compress-brotli")]
ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) { ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))
} else { } else {
@ -193,9 +245,27 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
#[cfg(feature = "compress-gzip")]
ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) { ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
let b = decoder.get_mut().take();
if !b.is_empty() {
Ok(Some(b))
} else {
Ok(None)
}
}
Err(e) => Err(e),
},
#[cfg(feature = "compress-gzip")]
ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => {
decoder.flush()?;
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))
@ -205,9 +275,12 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) {
#[cfg(feature = "compress-zstd")]
ContentDecoder::Zstd(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))

View File

@ -1,24 +1,41 @@
//! Stream encoder //! Stream encoders.
use std::future::Future;
use std::io::{self, Write};
use std::pin::Pin;
use std::task::{Context, Poll};
use actix_threadpool::{run, CpuFuture}; use std::{
use brotli2::write::BrotliEncoder; error::Error as StdError,
future::Future,
io::{self, Write as _},
pin::Pin,
task::{Context, Poll},
};
use actix_rt::task::{spawn_blocking, JoinHandle};
use bytes::Bytes; use bytes::Bytes;
use flate2::write::{GzEncoder, ZlibEncoder}; use derive_more::Display;
use futures_core::ready; use futures_core::ready;
use pin_project::pin_project; use pin_project::pin_project;
use crate::body::{Body, BodySize, MessageBody, ResponseBody}; #[cfg(feature = "compress-brotli")]
use crate::http::header::{ContentEncoding, CONTENT_ENCODING}; use brotli2::write::BrotliEncoder;
use crate::http::{HeaderValue, StatusCode};
use crate::{Error, ResponseHead}; #[cfg(feature = "compress-gzip")]
use flate2::write::{GzEncoder, ZlibEncoder};
#[cfg(feature = "compress-zstd")]
use zstd::stream::write::Encoder as ZstdEncoder;
use crate::{
body::{Body, BodySize, BoxAnyBody, MessageBody, ResponseBody},
http::{
header::{ContentEncoding, CONTENT_ENCODING},
HeaderValue, StatusCode,
},
Error, ResponseHead,
};
use super::Writer; use super::Writer;
use crate::error::BlockingError;
const INPLACE: usize = 1024; const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024;
#[pin_project] #[pin_project]
pub struct Encoder<B> { pub struct Encoder<B> {
@ -26,7 +43,7 @@ pub struct Encoder<B> {
#[pin] #[pin]
body: EncoderBody<B>, body: EncoderBody<B>,
encoder: Option<ContentEncoder>, encoder: Option<ContentEncoder>,
fut: Option<CpuFuture<ContentEncoder, io::Error>>, fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>,
} }
impl<B: MessageBody> Encoder<B> { impl<B: MessageBody> Encoder<B> {
@ -70,6 +87,7 @@ impl<B: MessageBody> Encoder<B> {
}); });
} }
} }
ResponseBody::Body(Encoder { ResponseBody::Body(Encoder {
body, body,
eof: false, eof: false,
@ -83,10 +101,16 @@ impl<B: MessageBody> Encoder<B> {
enum EncoderBody<B> { enum EncoderBody<B> {
Bytes(Bytes), Bytes(Bytes),
Stream(#[pin] B), Stream(#[pin] B),
BoxedStream(Box<dyn MessageBody + Unpin>), BoxedStream(BoxAnyBody),
} }
impl<B: MessageBody> MessageBody for EncoderBody<B> { impl<B> MessageBody for EncoderBody<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Error = EncoderError<B::Error>;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
match self { match self {
EncoderBody::Bytes(ref b) => b.size(), EncoderBody::Bytes(ref b) => b.size(),
@ -98,7 +122,7 @@ impl<B: MessageBody> MessageBody for EncoderBody<B> {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
match self.project() { match self.project() {
EncoderBodyProj::Bytes(b) => { EncoderBodyProj::Bytes(b) => {
if b.is_empty() { if b.is_empty() {
@ -107,15 +131,30 @@ impl<B: MessageBody> MessageBody for EncoderBody<B> {
Poll::Ready(Some(Ok(std::mem::take(b)))) Poll::Ready(Some(Ok(std::mem::take(b))))
} }
} }
EncoderBodyProj::Stream(b) => b.poll_next(cx), // TODO: MSRV 1.51: poll_map_err
EncoderBodyProj::Stream(b) => match ready!(b.poll_next(cx)) {
Some(Err(err)) => Poll::Ready(Some(Err(EncoderError::Body(err)))),
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
},
EncoderBodyProj::BoxedStream(ref mut b) => { EncoderBodyProj::BoxedStream(ref mut b) => {
Pin::new(b.as_mut()).poll_next(cx) match ready!(b.as_pin_mut().poll_next(cx)) {
Some(Err(err)) => Poll::Ready(Some(Err(EncoderError::Boxed(err)))),
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
}
} }
} }
} }
} }
impl<B: MessageBody> MessageBody for Encoder<B> { impl<B> MessageBody for Encoder<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Error = EncoderError<B::Error>;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
if self.encoder.is_none() { if self.encoder.is_none() {
self.body.size() self.body.size()
@ -127,7 +166,7 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
let mut this = self.project(); let mut this = self.project();
loop { loop {
if *this.eof { if *this.eof {
@ -135,32 +174,36 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
} }
if let Some(ref mut fut) = this.fut { if let Some(ref mut fut) = this.fut {
let mut encoder = match ready!(Pin::new(fut).poll(cx)) { let mut encoder = ready!(Pin::new(fut).poll(cx))
Ok(item) => item, .map_err(|_| EncoderError::Blocking(BlockingError))?
Err(e) => return Poll::Ready(Some(Err(e.into()))), .map_err(EncoderError::Io)?;
};
let chunk = encoder.take(); let chunk = encoder.take();
*this.encoder = Some(encoder); *this.encoder = Some(encoder);
this.fut.take(); this.fut.take();
if !chunk.is_empty() { if !chunk.is_empty() {
return Poll::Ready(Some(Ok(chunk))); 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 { 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 let Some(mut encoder) = this.encoder.take() {
if chunk.len() < INPLACE { if chunk.len() < MAX_CHUNK_SIZE_ENCODE_IN_PLACE {
encoder.write(&chunk)?; encoder.write(&chunk).map_err(EncoderError::Io)?;
let chunk = encoder.take(); let chunk = encoder.take();
*this.encoder = Some(encoder); *this.encoder = Some(encoder);
if !chunk.is_empty() { if !chunk.is_empty() {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
} else { } else {
*this.fut = Some(run(move || { *this.fut = Some(spawn_blocking(move || {
encoder.write(&chunk)?; encoder.write(&chunk)?;
Ok(encoder) Ok(encoder)
})); }));
@ -169,9 +212,10 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
} }
Poll::Ready(None) => {
None => {
if let Some(encoder) = this.encoder.take() { if let Some(encoder) = this.encoder.take() {
let chunk = encoder.finish()?; let chunk = encoder.finish().map_err(EncoderError::Io)?;
if chunk.is_empty() { if chunk.is_empty() {
return Poll::Ready(None); return Poll::Ready(None);
} else { } else {
@ -182,7 +226,6 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
return Poll::Ready(None); return Poll::Ready(None);
} }
} }
val => return val,
} }
} }
} }
@ -196,25 +239,40 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
} }
enum ContentEncoder { enum ContentEncoder {
#[cfg(feature = "compress-gzip")]
Deflate(ZlibEncoder<Writer>), Deflate(ZlibEncoder<Writer>),
#[cfg(feature = "compress-gzip")]
Gzip(GzEncoder<Writer>), Gzip(GzEncoder<Writer>),
#[cfg(feature = "compress-brotli")]
Br(BrotliEncoder<Writer>), Br(BrotliEncoder<Writer>),
// We need explicit 'static lifetime here because ZstdEncoder need lifetime
// argument, and we use `spawn_blocking` in `Encoder::poll_next` that require `FnOnce() -> R + Send + 'static`
#[cfg(feature = "compress-zstd")]
Zstd(ZstdEncoder<'static, Writer>),
} }
impl ContentEncoder { impl ContentEncoder {
fn encoder(encoding: ContentEncoding) -> Option<Self> { fn encoder(encoding: ContentEncoding) -> Option<Self> {
match encoding { match encoding {
#[cfg(feature = "compress-gzip")]
ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new( ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new(
Writer::new(), Writer::new(),
flate2::Compression::fast(), flate2::Compression::fast(),
))), ))),
#[cfg(feature = "compress-gzip")]
ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new(
Writer::new(), Writer::new(),
flate2::Compression::fast(), flate2::Compression::fast(),
))), ))),
#[cfg(feature = "compress-brotli")]
ContentEncoding::Br => { ContentEncoding::Br => {
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
} }
#[cfg(feature = "compress-zstd")]
ContentEncoding::Zstd => {
let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?;
Some(ContentEncoder::Zstd(encoder))
}
_ => None, _ => None,
} }
} }
@ -222,31 +280,45 @@ impl ContentEncoder {
#[inline] #[inline]
pub(crate) fn take(&mut self) -> Bytes { pub(crate) fn take(&mut self) -> Bytes {
match *self { match *self {
#[cfg(feature = "compress-brotli")]
ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(),
#[cfg(feature = "compress-gzip")]
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
#[cfg(feature = "compress-gzip")]
ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(),
#[cfg(feature = "compress-zstd")]
ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(),
} }
} }
fn finish(self) -> Result<Bytes, io::Error> { fn finish(self) -> Result<Bytes, io::Error> {
match self { match self {
#[cfg(feature = "compress-brotli")]
ContentEncoder::Br(encoder) => match encoder.finish() { ContentEncoder::Br(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
#[cfg(feature = "compress-gzip")]
ContentEncoder::Gzip(encoder) => match encoder.finish() { ContentEncoder::Gzip(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
#[cfg(feature = "compress-gzip")]
ContentEncoder::Deflate(encoder) => match encoder.finish() { ContentEncoder::Deflate(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
#[cfg(feature = "compress-zstd")]
ContentEncoder::Zstd(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err),
},
} }
} }
fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
match *self { match *self {
#[cfg(feature = "compress-brotli")]
ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
@ -254,6 +326,7 @@ impl ContentEncoder {
Err(err) Err(err)
} }
}, },
#[cfg(feature = "compress-gzip")]
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
@ -261,6 +334,7 @@ impl ContentEncoder {
Err(err) Err(err)
} }
}, },
#[cfg(feature = "compress-gzip")]
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
@ -268,6 +342,47 @@ impl ContentEncoder {
Err(err) Err(err)
} }
}, },
#[cfg(feature = "compress-zstd")]
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()),
Err(err) => {
trace!("Error decoding ztsd encoding: {}", err);
Err(err)
}
},
} }
} }
} }
#[derive(Debug, Display)]
#[non_exhaustive]
pub enum EncoderError<E> {
#[display(fmt = "body")]
Body(E),
#[display(fmt = "boxed")]
Boxed(Box<dyn StdError>),
#[display(fmt = "blocking")]
Blocking(BlockingError),
#[display(fmt = "io")]
Io(io::Error),
}
impl<E: StdError + 'static> StdError for EncoderError<E> {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
EncoderError::Body(err) => Some(err),
EncoderError::Boxed(err) => Some(&**err),
EncoderError::Blocking(err) => Some(err),
EncoderError::Io(err) => Some(err),
}
}
}
impl<E: StdError + 'static> From<EncoderError<E>> for crate::Error {
fn from(err: EncoderError<E>) -> Self {
crate::Error::new_encoder().with_cause(err)
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,62 +1,119 @@
use std::any::{Any, TypeId}; use std::{
use std::{fmt, mem}; any::{Any, TypeId},
fmt, mem,
};
use fxhash::FxHashMap; use ahash::AHashMap;
/// A type map of request extensions. /// A type map for request extensions.
///
/// All entries into this map must be owned types (or static references).
#[derive(Default)] #[derive(Default)]
pub struct Extensions { pub struct Extensions {
/// Use FxHasher with a std HashMap with for faster /// Use FxHasher with a std HashMap with for faster
/// lookups on the small `TypeId` (u64 equivalent) keys. /// lookups on the small `TypeId` (u64 equivalent) keys.
map: FxHashMap<TypeId, Box<dyn Any>>, map: AHashMap<TypeId, Box<dyn Any>>,
} }
impl Extensions { impl Extensions {
/// Create an empty `Extensions`. /// Creates an empty `Extensions`.
#[inline] #[inline]
pub fn new() -> Extensions { pub fn new() -> Extensions {
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 /// If an item of this type was already stored, it will be replaced and returned.
/// be returned. ///
pub fn insert<T: 'static>(&mut self, val: T) { /// ```
self.map.insert(TypeId::of::<T>(), Box::new(val)); /// # 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 { pub fn contains<T: 'static>(&self) -> bool {
self.map.contains_key(&TypeId::of::<T>()) 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> { pub fn get<T: 'static>(&self) -> Option<&T> {
self.map self.map
.get(&TypeId::of::<T>()) .get(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_ref()) .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> { pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.map self.map
.get_mut(&TypeId::of::<T>()) .get_mut(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_mut()) .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> { pub fn remove<T: 'static>(&mut self) -> Option<T> {
self.map self.map.remove(&TypeId::of::<T>()).and_then(downcast_owned)
.remove(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast().ok().map(|boxed| *boxed))
} }
/// Clear the `Extensions` of all inserted extensions. /// 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] #[inline]
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.map.clear(); 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -223,15 +223,3 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
Ok(()) 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

@ -199,10 +199,10 @@ mod tests {
use http::Method; use http::Method;
use super::*; use super::*;
use crate::httpmessage::HttpMessage; use crate::HttpMessage;
#[test] #[actix_rt::test]
fn test_http_request_chunked_payload_and_next_message() { async fn test_http_request_chunked_payload_and_next_message() {
let mut codec = Codec::default(); let mut codec = Codec::default();
let mut buf = BytesMut::from( let mut buf = BytesMut::from(

View File

@ -14,7 +14,7 @@ use crate::header::HeaderMap;
use crate::message::{ConnectionType, ResponseHead}; use crate::message::{ConnectionType, ResponseHead};
use crate::request::Request; 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; const MAX_HEADERS: usize = 96;
/// Incoming message decoder /// Incoming message decoder
@ -203,7 +203,15 @@ impl MessageType for Request {
(len, method, uri, version, req.headers.len()) (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 { let decoder = match length {
PayloadLength::Payload(pl) => pl, PayloadLength::Payload(pl) => pl,
PayloadLength::UpgradeWebSocket => { PayloadLength::UpgradeWebSocket => {
// upgrade(websocket) // upgrade (WebSocket)
PayloadType::Stream(PayloadDecoder::eof()) PayloadType::Stream(PayloadDecoder::eof())
} }
PayloadLength::None => { PayloadLength::None => {
if method == Method::CONNECT { if method == Method::CONNECT {
PayloadType::Stream(PayloadDecoder::eof()) PayloadType::Stream(PayloadDecoder::eof())
} else if src.len() >= MAX_BUFFER_SIZE {
trace!("MAX_BUFFER_SIZE unprocessed data reached, closing");
return Err(ParseError::TooLarge);
} else { } else {
PayloadType::None PayloadType::None
} }
@ -273,7 +278,14 @@ impl MessageType for ResponseHead {
(len, version, status, res.headers.len()) (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 { } else if status == StatusCode::SWITCHING_PROTOCOLS {
// switching protocol or connect // switching protocol or connect
PayloadType::Stream(PayloadDecoder::eof()) PayloadType::Stream(PayloadDecoder::eof())
} else if src.len() >= MAX_BUFFER_SIZE {
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
return Err(ParseError::TooLarge);
} else { } else {
// for HTTP/1.0 read to eof and close connection // for HTTP/1.0 read to eof and close connection
if msg.version == Version::HTTP_10 { if msg.version == Version::HTTP_10 {
@ -643,7 +652,7 @@ mod tests {
use super::*; use super::*;
use crate::error::ParseError; use crate::error::ParseError;
use crate::http::header::{HeaderName, SET_COOKIE}; use crate::http::header::{HeaderName, SET_COOKIE};
use crate::httpmessage::HttpMessage; use crate::HttpMessage;
impl PayloadType { impl PayloadType {
fn unwrap(self) -> PayloadDecoder { fn unwrap(self) -> PayloadDecoder {
@ -821,8 +830,8 @@ mod tests {
.get_all(SET_COOKIE) .get_all(SET_COOKIE)
.map(|v| v.to_str().unwrap().to_owned()) .map(|v| v.to_str().unwrap().to_owned())
.collect(); .collect();
assert_eq!(val[1], "c1=cookie1"); assert_eq!(val[0], "c1=cookie1");
assert_eq!(val[0], "c2=cookie2"); assert_eq!(val[1], "c2=cookie2");
} }
#[test] #[test]
@ -1204,8 +1213,9 @@ mod tests {
#[test] #[test]
fn test_parse_chunked_payload_chunk_extension() { fn test_parse_chunked_payload_chunk_extension() {
let mut buf = BytesMut::from( let mut buf = BytesMut::from(
&"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n"[..], transfer-encoding: chunked\r\n\
\r\n",
); );
let mut reader = MessageDecoder::<Request>::default(); let mut reader = MessageDecoder::<Request>::default();
@ -1224,7 +1234,7 @@ mod tests {
#[test] #[test]
fn test_response_http10_read_until_eof() { fn test_response_http10_read_until_eof() {
let mut buf = BytesMut::from(&"HTTP/1.0 200 Ok\r\n\r\ntest data"[..]); let mut buf = BytesMut::from("HTTP/1.0 200 Ok\r\n\r\ntest data");
let mut reader = MessageDecoder::<ResponseHead>::default(); let mut reader = MessageDecoder::<ResponseHead>::default();
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();

File diff suppressed because it is too large Load Diff

View File

@ -6,14 +6,15 @@ use std::{cmp, io};
use bytes::{BufMut, BytesMut}; use bytes::{BufMut, BytesMut};
use crate::body::BodySize; use crate::{
use crate::config::ServiceConfig; body::BodySize,
use crate::header::map; config::ServiceConfig,
use crate::helpers; header::{map::Value, HeaderMap, HeaderName},
use crate::http::header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING},
use crate::http::{HeaderMap, StatusCode, Version}; helpers,
use crate::message::{ConnectionType, RequestHeadType}; message::{ConnectionType, RequestHeadType},
use crate::response::Response; Response, StatusCode, Version,
};
const AVERAGE_HEADER_SIZE: usize = 30; const AVERAGE_HEADER_SIZE: usize = 30;
@ -121,21 +122,11 @@ pub(crate) trait MessageType: Sized {
_ => {} _ => {}
} }
// 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 // write headers
let mut has_date = false; let mut has_date = false;
let mut buf = dst.chunk_mut().as_mut_ptr() as *mut u8; let mut buf = dst.chunk_mut().as_mut_ptr();
let mut remaining = dst.capacity() - dst.len(); let mut remaining = dst.capacity() - dst.len();
// tracks bytes written since last buffer resize // tracks bytes written since last buffer resize
@ -143,10 +134,10 @@ pub(crate) trait MessageType: Sized {
// container's knowledge, this is used to sync the containers cursor after data is written // container's knowledge, this is used to sync the containers cursor after data is written
let mut pos = 0; let mut pos = 0;
for (key, value) in headers { self.write_headers(|key, value| {
match *key { match *key {
CONNECTION => continue, CONNECTION => return,
TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue, TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => return,
DATE => has_date = true, DATE => has_date = true,
_ => {} _ => {}
} }
@ -154,106 +145,56 @@ pub(crate) trait MessageType: Sized {
let k = key.as_str().as_bytes(); let k = key.as_str().as_bytes();
let k_len = k.len(); let k_len = k.len();
match value { // TODO: drain?
map::Value::One(ref val) => { for val in value.iter() {
let v = val.as_ref(); let v = val.as_ref();
let v_len = v.len(); let v_len = v.len();
// key length + value length + colon + space + \r\n // key length + value length + colon + space + \r\n
let len = k_len + v_len + 4; let len = k_len + v_len + 4;
if len > remaining { 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
// 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.chunk_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 { unsafe {
// use upper Camel-Case dst.advance_mut(pos);
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; pos = 0;
remaining -= len; 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.chunk_mut().as_mut_ptr();
} }
map::Value::Multi(ref vec) => { // SAFETY: on each write, it is enough to ensure that the advancement of
for val in vec { // the cursor matches the number of bytes written
let v = val.as_ref(); unsafe {
let v_len = v.len(); if camel_case {
let len = k_len + v_len + 4; // use Camel-Case headers
write_camel_case(k, from_raw_parts_mut(buf, k_len));
if len > remaining { } else {
// SAFETY: all the bytes written up to position "pos" are initialized write_data(k, buf, k_len);
// 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.chunk_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 {
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;
} }
}
buf = buf.add(k_len);
write_data(b": ", buf, 2);
buf = buf.add(2);
write_data(v, buf, v_len);
buf = buf.add(v_len);
write_data(b"\r\n", buf, 2);
buf = buf.add(2);
};
pos += len;
remaining -= len;
} }
} });
// final cursor synchronization with the bytes container // final cursor synchronization with the bytes container
// //
@ -273,6 +214,24 @@ pub(crate) trait MessageType: Sized {
Ok(()) 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<()> { impl MessageType for Response<()> {
@ -329,7 +288,7 @@ impl MessageType for RequestHeadType {
let head = self.as_ref(); let head = self.as_ref();
dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE); dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE);
write!( write!(
Writer(dst), helpers::MutWriter(dst),
"{} {} {}", "{} {} {}",
head.method, head.method,
head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"), head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"),
@ -462,7 +421,7 @@ impl TransferEncoding {
*eof = true; *eof = true;
buf.extend_from_slice(b"0\r\n\r\n"); buf.extend_from_slice(b"0\r\n\r\n");
} else { } else {
writeln!(Writer(buf), "{:X}\r", msg.len()) writeln!(helpers::MutWriter(buf), "{:X}\r", msg.len())
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
buf.reserve(msg.len() + 2); buf.reserve(msg.len() + 2);
@ -512,18 +471,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 /// # Safety
/// Callers must ensure that the given length matches given value length. /// Callers must ensure that the given length matches given value length.
unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) { unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) {
@ -583,8 +530,8 @@ mod tests {
); );
} }
#[test] #[actix_rt::test]
fn test_camel_case() { async fn test_camel_case() {
let mut bytes = BytesMut::with_capacity(2048); let mut bytes = BytesMut::with_capacity(2048);
let mut head = RequestHead::default(); let mut head = RequestHead::default();
head.set_camel_case_headers(true); head.set_camel_case_headers(true);
@ -603,7 +550,6 @@ mod tests {
); );
let data = let data =
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap(); String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
eprintln!("{}", &data);
assert!(data.contains("Content-Length: 0\r\n")); assert!(data.contains("Content-Length: 0\r\n"));
assert!(data.contains("Connection: close\r\n")); assert!(data.contains("Connection: close\r\n"));
@ -647,8 +593,8 @@ mod tests {
assert!(data.contains("date: date\r\n")); assert!(data.contains("date: date\r\n"));
} }
#[test] #[actix_rt::test]
fn test_extra_headers() { async fn test_extra_headers() {
let mut bytes = BytesMut::with_capacity(2048); let mut bytes = BytesMut::with_capacity(2048);
let mut head = RequestHead::default(); let mut head = RequestHead::default();
@ -681,16 +627,14 @@ mod tests {
assert!(data.contains("date: date\r\n")); assert!(data.contains("date: date\r\n"));
} }
#[test] #[actix_rt::test]
fn test_no_content_length() { async fn test_no_content_length() {
let mut bytes = BytesMut::with_capacity(2048); let mut bytes = BytesMut::with_capacity(2048);
let mut res: Response<()> = let mut res = Response::with_body(StatusCode::SWITCHING_PROTOCOLS, ());
Response::new(StatusCode::SWITCHING_PROTOCOLS).into_body::<()>(); res.headers_mut().insert(DATE, HeaderValue::from_static(""));
res.headers_mut() res.headers_mut()
.insert(DATE, HeaderValue::from_static(&"")); .insert(CONTENT_LENGTH, HeaderValue::from_static("0"));
res.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static(&"0"));
let _ = res.encode_headers( let _ = res.encode_headers(
&mut bytes, &mut bytes,

View File

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

View File

@ -1,4 +1,4 @@
//! HTTP/1 implementation //! HTTP/1 protocol implementation.
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
mod client; mod client;

View File

@ -3,9 +3,8 @@ use std::cell::RefCell;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::pin::Pin; use std::pin::Pin;
use std::rc::{Rc, Weak}; 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 bytes::Bytes;
use futures_core::Stream; use futures_core::Stream;
@ -134,7 +133,7 @@ impl PayloadSender {
if shared.borrow().need_read { if shared.borrow().need_read {
PayloadStatus::Read PayloadStatus::Read
} else { } else {
shared.borrow_mut().io_task.register(cx.waker()); shared.borrow_mut().register_io(cx);
PayloadStatus::Pause PayloadStatus::Pause
} }
} else { } else {
@ -150,8 +149,8 @@ struct Inner {
err: Option<PayloadError>, err: Option<PayloadError>,
need_read: bool, need_read: bool,
items: VecDeque<Bytes>, items: VecDeque<Bytes>,
task: LocalWaker, task: Option<Waker>,
io_task: LocalWaker, io_task: Option<Waker>,
} }
impl Inner { impl Inner {
@ -162,8 +161,48 @@ impl Inner {
err: None, err: None,
items: VecDeque::new(), items: VecDeque::new(),
need_read: true, need_read: true,
task: LocalWaker::new(), task: None,
io_task: LocalWaker::new(), 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,7 +221,7 @@ impl Inner {
self.len += data.len(); self.len += data.len();
self.items.push_back(data); self.items.push_back(data);
self.need_read = self.len < MAX_BUFFER_SIZE; self.need_read = self.len < MAX_BUFFER_SIZE;
self.task.wake(); self.wake();
} }
#[cfg(test)] #[cfg(test)]
@ -199,9 +238,9 @@ impl Inner {
self.need_read = self.len < MAX_BUFFER_SIZE; self.need_read = self.len < MAX_BUFFER_SIZE;
if self.need_read && !self.eof { 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))) Poll::Ready(Some(Ok(data)))
} else if let Some(err) = self.err.take() { } else if let Some(err) = self.err.take() {
Poll::Ready(Some(Err(err))) Poll::Ready(Some(Err(err)))
@ -209,8 +248,8 @@ impl Inner {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
self.need_read = true; self.need_read = true;
self.task.register(cx.waker()); self.register(cx);
self.io_task.wake(); self.wake_io();
Poll::Pending Poll::Pending
} }
} }
@ -224,7 +263,7 @@ impl Inner {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use futures_util::future::poll_fn; use actix_utils::future::poll_fn;
#[actix_rt::test] #[actix_rt::test]
async fn test_unread_data() { async fn test_unread_data() {

View File

@ -1,28 +1,29 @@
use std::cell::RefCell; use std::{
use std::future::Future; error::Error as StdError,
use std::marker::PhantomData; fmt,
use std::pin::Pin; marker::PhantomData,
use std::rc::Rc; net,
use std::task::{Context, Poll}; rc::Rc,
use std::{fmt, net}; task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory}; use actix_service::{
use futures_core::ready; fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _,
use futures_util::future::ready; };
use actix_utils::future::ready;
use futures_core::future::LocalBoxFuture;
use crate::body::MessageBody; use crate::{
use crate::config::ServiceConfig; body::{AnyBody, MessageBody},
use crate::error::{DispatchError, Error}; config::ServiceConfig,
use crate::request::Request; error::DispatchError,
use crate::response::Response; service::HttpServiceHandler,
use crate::service::HttpFlow; ConnectCallback, OnConnectData, Request, Response,
use crate::{ConnectCallback, OnConnectData}; };
use super::codec::Codec; use super::{codec::Codec, dispatcher::Dispatcher, ExpectHandler, UpgradeHandler};
use super::dispatcher::Dispatcher;
use super::{ExpectHandler, UpgradeHandler};
/// `ServiceFactory` implementation for HTTP1 transport /// `ServiceFactory` implementation for HTTP1 transport
pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> { pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
@ -37,7 +38,7 @@ pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
impl<T, S, B> H1Service<T, S, B> impl<T, S, B> H1Service<T, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
@ -61,15 +62,22 @@ where
impl<S, B, X, U> H1Service<TcpStream, S, B, X, U> impl<S, B, X, U> H1Service<TcpStream, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>, S::Future: 'static,
S::Error: Into<Response<AnyBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Error: Into<Error>, X::Future: 'static,
X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>,
U::Error: fmt::Display + Into<Error>, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create simple tcp stream service /// Create simple tcp stream service
@ -82,7 +90,7 @@ where
Error = DispatchError, Error = DispatchError,
InitError = (), InitError = (),
> { > {
pipeline_factory(|io: TcpStream| { fn_service(|io: TcpStream| {
let peer_addr = io.peer_addr().ok(); let peer_addr = io.peer_addr().ok();
ready(Ok((io, peer_addr))) ready(Ok((io, peer_addr)))
}) })
@ -95,25 +103,34 @@ mod openssl {
use super::*; use super::*;
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt;
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream}; use actix_tls::accept::{
use actix_tls::accept::TlsError; openssl::{Acceptor, SslAcceptor, SslError, TlsStream},
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 where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>, S::Future: 'static,
S::Error: Into<Response<AnyBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Error: Into<Error>, X::Future: 'static,
X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
(Request, Framed<SslStream<TcpStream>, Codec>), (Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (), Config = (),
Response = (), Response = (),
>, >,
U::Error: fmt::Display + Into<Error>, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create openssl based service /// Create openssl based service
@ -127,16 +144,14 @@ mod openssl {
Error = TlsError<SslError, DispatchError>, Error = TlsError<SslError, DispatchError>,
InitError = (), InitError = (),
> { > {
pipeline_factory( Acceptor::new(acceptor)
Acceptor::new(acceptor) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(|io: TlsStream<TcpStream>| {
) let peer_addr = io.get_ref().peer_addr().ok();
.and_then(|io: SslStream<TcpStream>| { ready(Ok((io, peer_addr)))
let peer_addr = io.get_ref().peer_addr().ok(); })
ready(Ok((io, peer_addr))) .and_then(self.map_err(TlsError::Service))
})
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
@ -144,27 +159,38 @@ mod openssl {
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
mod rustls { mod rustls {
use super::*; use super::*;
use std::io;
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt;
use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream}; use actix_tls::accept::{
use actix_tls::accept::TlsError; rustls::{Acceptor, ServerConfig, TlsStream},
use std::{fmt, io}; TlsError,
};
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>, S::Future: 'static,
S::Error: Into<Response<AnyBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Error: Into<Error>, X::Future: 'static,
X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>), (Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (), Config = (),
Response = (), Response = (),
>, >,
U::Error: fmt::Display + Into<Error>, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create rustls based service /// Create rustls based service
@ -178,16 +204,14 @@ mod rustls {
Error = TlsError<io::Error, DispatchError>, Error = TlsError<io::Error, DispatchError>,
InitError = (), InitError = (),
> { > {
pipeline_factory( Acceptor::new(config)
Acceptor::new(config) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(|io: TlsStream<TcpStream>| {
) let peer_addr = io.get_ref().0.peer_addr().ok();
.and_then(|io: TlsStream<TcpStream>| { ready(Ok((io, peer_addr)))
let peer_addr = io.get_ref().0.peer_addr().ok(); })
ready(Ok((io, peer_addr))) .and_then(self.map_err(TlsError::Service))
})
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
@ -195,7 +219,7 @@ mod rustls {
impl<T, S, B, X, U> H1Service<T, S, B, X, U> impl<T, S, B, X, U> H1Service<T, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
B: MessageBody, B: MessageBody,
@ -203,7 +227,7 @@ where
pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U> pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U>
where where
X1: ServiceFactory<Request, Response = Request>, X1: ServiceFactory<Request, Response = Request>,
X1::Error: Into<Error>, X1::Error: Into<Response<AnyBody>>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
{ {
H1Service { H1Service {
@ -242,17 +266,25 @@ where
impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)> impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)>
for H1Service<T, S, B, X, U> for H1Service<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin + 'static,
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>, S::Future: 'static,
S::Error: Into<Response<AnyBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Error: Into<Error>, X::Future: 'static,
X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
U::Error: fmt::Display + Into<Error>, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
type Response = (); type Response = ();
@ -260,207 +292,78 @@ where
type Config = (); type Config = ();
type Service = H1ServiceHandler<T, S::Service, B, X::Service, U::Service>; type Service = H1ServiceHandler<T, S::Service, B, X::Service, U::Service>;
type InitError = (); type InitError = ();
type Future = H1ServiceResponse<T, S, B, X, U>; type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
H1ServiceResponse { let service = self.srv.new_service(());
fut: self.srv.new_service(()), let expect = self.expect.new_service(());
fut_ex: Some(self.expect.new_service(())), let upgrade = self.upgrade.as_ref().map(|s| s.new_service(()));
fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())), let on_connect_ext = self.on_connect_ext.clone();
expect: None, let cfg = self.cfg.clone();
upgrade: None,
on_connect_ext: self.on_connect_ext.clone(),
cfg: Some(self.cfg.clone()),
_phantom: PhantomData,
}
}
}
#[doc(hidden)] Box::pin(async move {
#[pin_project::pin_project] let expect = expect
pub struct H1ServiceResponse<T, S, B, X, U> .await
where .map_err(|e| log::error!("Init http expect service error: {:?}", e))?;
S: ServiceFactory<Request>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
X: ServiceFactory<Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
{
#[pin]
fut: S::Future,
#[pin]
fut_ex: Option<X::Future>,
#[pin]
fut_upg: Option<U::Future>,
expect: Option<X::Service>,
upgrade: Option<U::Service>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: Option<ServiceConfig>,
_phantom: PhantomData<B>,
}
impl<T, S, B, X, U> Future for H1ServiceResponse<T, S, B, X, U> let upgrade = match upgrade {
where Some(upgrade) => {
T: AsyncRead + AsyncWrite + Unpin, let upgrade = upgrade.await.map_err(|e| {
S: ServiceFactory<Request>, log::error!("Init http upgrade service error: {:?}", e)
S::Error: Into<Error>, })?;
S::Response: Into<Response<B>>, Some(upgrade)
S::InitError: fmt::Debug, }
B: MessageBody, None => None,
X: ServiceFactory<Request, Response = Request>, };
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
{
type Output = Result<H1ServiceHandler<T, S::Service, B, X::Service, U::Service>, ()>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { let service = service
let mut this = self.as_mut().project(); .await
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
if let Some(fut) = this.fut_ex.as_pin_mut() { Ok(H1ServiceHandler::new(
let expect = ready!(fut cfg,
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.expect = Some(expect);
this.fut_ex.set(None);
}
if let Some(fut) = this.fut_upg.as_pin_mut() {
let upgrade = ready!(fut
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.upgrade = Some(upgrade);
this.fut_upg.set(None);
}
let result = ready!(this
.fut
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)));
Poll::Ready(result.map(|service| {
let this = self.as_mut().project();
H1ServiceHandler::new(
this.cfg.take().unwrap(),
service, service,
this.expect.take().unwrap(), expect,
this.upgrade.take(), upgrade,
this.on_connect_ext.clone(), on_connect_ext,
) ))
})) })
} }
} }
/// `Service` implementation for HTTP/1 transport /// `Service` implementation for HTTP/1 transport
pub struct H1ServiceHandler<T, S, B, X, U> pub type H1ServiceHandler<T, S, B, X, U> = HttpServiceHandler<T, S, B, X, U>;
where
S: Service<Request>,
X: Service<Request>,
U: Service<(Request, Framed<T, Codec>)>,
{
flow: Rc<RefCell<HttpFlow<S, X, U>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: ServiceConfig,
_phantom: PhantomData<B>,
}
impl<T, S, B, X, U> H1ServiceHandler<T, S, B, X, U>
where
S: Service<Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
B: MessageBody,
X: Service<Request, Response = Request>,
X::Error: Into<Error>,
U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
{
fn new(
cfg: ServiceConfig,
service: S,
expect: X,
upgrade: Option<U>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
) -> H1ServiceHandler<T, S, B, X, U> {
H1ServiceHandler {
flow: HttpFlow::new(service, expect, upgrade),
cfg,
on_connect_ext,
_phantom: PhantomData,
}
}
}
impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)> impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)>
for H1ServiceHandler<T, S, B, X, U> for HttpServiceHandler<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Response<AnyBody>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
type Future = Dispatcher<T, S, B, X, U>; 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 mut flow = self.flow.borrow_mut(); self._poll_ready(cx).map_err(|e| {
let ready = flow log::error!("HTTP/1 service readiness error: {:?}", e);
.expect DispatchError::Service(e)
.poll_ready(cx) })
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready();
let ready = flow
.service
.poll_ready(cx)
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready()
&& ready;
let ready = if let Some(ref mut upg) = flow.upgrade {
upg.poll_ready(cx)
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready()
&& ready
} else {
ready
};
if ready {
Poll::Ready(Ok(()))
} else {
Poll::Pending
}
} }
fn call(&mut self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future { fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
let on_connect_data = let on_connect_data =
OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); OnConnectData::from_io(&io, self.on_connect_ext.as_deref());

View File

@ -1,8 +1,6 @@
use std::task::{Context, Poll};
use actix_codec::Framed; use actix_codec::Framed;
use actix_service::{Service, ServiceFactory}; use actix_service::{Service, ServiceFactory};
use futures_util::future::{ready, Ready}; use futures_core::future::LocalBoxFuture;
use crate::error::Error; use crate::error::Error;
use crate::h1::Codec; use crate::h1::Codec;
@ -16,7 +14,7 @@ impl<T> ServiceFactory<(Request, Framed<T, Codec>)> for UpgradeHandler {
type Config = (); type Config = ();
type Service = UpgradeHandler; type Service = UpgradeHandler;
type InitError = Error; type InitError = Error;
type Future = Ready<Result<Self::Service, Self::InitError>>; type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
unimplemented!() unimplemented!()
@ -26,13 +24,11 @@ impl<T> ServiceFactory<(Request, Framed<T, Codec>)> for UpgradeHandler {
impl<T> Service<(Request, Framed<T, Codec>)> for UpgradeHandler { impl<T> Service<(Request, Framed<T, Codec>)> for UpgradeHandler {
type Response = (); type Response = ();
type Error = Error; type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { actix_service::always_ready!();
Poll::Ready(Ok(()))
}
fn call(&mut self, _: (Request, Framed<T, Codec>)) -> Self::Future { fn call(&self, _: (Request, Framed<T, Codec>)) -> Self::Future {
ready(Ok(())) unimplemented!()
} }
} }

View File

@ -4,7 +4,7 @@ use std::task::{Context, Poll};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use crate::body::{BodySize, MessageBody, ResponseBody}; use crate::body::{BodySize, MessageBody};
use crate::error::Error; use crate::error::Error;
use crate::h1::{Codec, Message}; use crate::h1::{Codec, Message};
use crate::response::Response; use crate::response::Response;
@ -14,7 +14,7 @@ use crate::response::Response;
pub struct SendResponse<T, B> { pub struct SendResponse<T, B> {
res: Option<Message<(Response<()>, BodySize)>>, res: Option<Message<(Response<()>, BodySize)>>,
#[pin] #[pin]
body: Option<ResponseBody<B>>, body: Option<B>,
#[pin] #[pin]
framed: Option<Framed<T, Codec>>, framed: Option<Framed<T, Codec>>,
} }
@ -22,6 +22,7 @@ pub struct SendResponse<T, B> {
impl<T, B> SendResponse<T, B> impl<T, B> SendResponse<T, B>
where where
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
pub fn new(framed: Framed<T, Codec>, response: Response<B>) -> Self { pub fn new(framed: Framed<T, Codec>, response: Response<B>) -> Self {
let (res, body) = response.into_parts(); let (res, body) = response.into_parts();
@ -38,6 +39,7 @@ impl<T, B> Future for SendResponse<T, B>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
B: MessageBody + Unpin, B: MessageBody + Unpin,
B::Error: Into<Error>,
{ {
type Output = Result<Framed<T, Codec>, Error>; type Output = Result<Framed<T, Codec>, Error>;
@ -60,7 +62,18 @@ where
.unwrap() .unwrap()
.is_write_buf_full() .is_write_buf_full()
{ {
match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx)? { let next =
// TODO: MSRV 1.51: poll_map_err
match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) {
Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)),
Poll::Ready(Some(Err(err))) => {
return Poll::Ready(Err(err.into()))
}
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
};
match next {
Poll::Ready(item) => { Poll::Ready(item) => {
// body is done when item is None // body is done when item is None
body_done = item.is_none(); body_done = item.is_none();
@ -68,7 +81,9 @@ where
let _ = this.body.take(); let _ = this.body.take();
} }
let framed = this.framed.as_mut().as_pin_mut().unwrap(); let framed = this.framed.as_mut().as_pin_mut().unwrap();
framed.write(Message::Chunk(item))?; framed.write(Message::Chunk(item)).map_err(|err| {
Error::new_send_response().with_cause(err)
})?;
} }
Poll::Pending => body_ready = false, Poll::Pending => body_ready = false,
} }
@ -79,7 +94,10 @@ where
// flush write buffer // flush write buffer
if !framed.is_write_buf_empty() { if !framed.is_write_buf_empty() {
match framed.flush(cx)? { match framed
.flush(cx)
.map_err(|err| Error::new_send_response().with_cause(err))?
{
Poll::Ready(_) => { Poll::Ready(_) => {
if body_ready { if body_ready {
continue; continue;
@ -93,7 +111,9 @@ where
// send response // send response
if let Some(res) = this.res.take() { if let Some(res) = this.res.take() {
framed.write(res)?; framed
.write(res)
.map_err(|err| Error::new_send_response().with_cause(err))?;
continue; continue;
} }

View File

@ -1,92 +1,59 @@
use std::cell::RefCell; use std::{
use std::future::Future; cmp,
use std::marker::PhantomData; error::Error as StdError,
use std::net; future::Future,
use std::pin::Pin; marker::PhantomData,
use std::rc::Rc; net,
use std::task::{Context, Poll}; pin::Pin,
use std::{cmp, convert::TryFrom}; rc::Rc,
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::time::{Instant, Sleep};
use actix_service::Service; use actix_service::Service;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::ready; use futures_core::ready;
use h2::server::{Connection, SendResponse}; use h2::server::{Connection, SendResponse};
use h2::SendStream;
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
use log::{error, trace}; use log::{error, trace};
use pin_project_lite::pin_project;
use crate::body::{BodySize, MessageBody, ResponseBody}; use crate::{
use crate::config::ServiceConfig; body::{AnyBody, BodySize, MessageBody},
use crate::error::{DispatchError, Error}; config::ServiceConfig,
use crate::message::ResponseHead; service::HttpFlow,
use crate::payload::Payload; OnConnectData, Payload, Request, Response, ResponseHead,
use crate::request::Request; };
use crate::response::Response;
use crate::service::HttpFlow;
use crate::OnConnectData;
const CHUNK_SIZE: usize = 16_384; const CHUNK_SIZE: usize = 16_384;
/// Dispatcher for HTTP/2 protocol. pin_project! {
#[pin_project::pin_project] /// Dispatcher for HTTP/2 protocol.
pub struct Dispatcher<T, S, B, X, U> pub struct Dispatcher<T, S, B, X, U> {
where flow: Rc<HttpFlow<S, X, U>>,
T: AsyncRead + AsyncWrite + Unpin, connection: Connection<T, Bytes>,
S: Service<Request>, on_connect_data: OnConnectData,
B: MessageBody, config: ServiceConfig,
{ peer_addr: Option<net::SocketAddr>,
flow: Rc<RefCell<HttpFlow<S, X, U>>>, _phantom: PhantomData<B>,
connection: Connection<T, Bytes>, }
on_connect_data: OnConnectData, }
config: ServiceConfig,
peer_addr: Option<net::SocketAddr>, impl<T, S, B, X, U> Dispatcher<T, S, B, X, U> {
ka_expire: Instant, pub(crate) fn new(
ka_timer: Option<Sleep>, flow: Rc<HttpFlow<S, X, U>>,
_phantom: PhantomData<B>,
}
impl<T, S, B, X, U> Dispatcher<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
B: MessageBody,
{
pub(crate) fn new(
services: Rc<RefCell<HttpFlow<S, X, U>>>,
connection: Connection<T, Bytes>, connection: Connection<T, Bytes>,
on_connect_data: OnConnectData, on_connect_data: OnConnectData,
config: ServiceConfig, config: ServiceConfig,
timeout: Option<Sleep>,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
) -> Self { ) -> Self {
// let keepalive = config.keep_alive_enabled(); Self {
// let flags = if keepalive { flow,
// 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 {
flow: services,
config, config,
peer_addr, peer_addr,
connection, connection,
on_connect_data, on_connect_data,
ka_expire,
ka_timer,
_phantom: PhantomData, _phantom: PhantomData,
} }
} }
@ -95,277 +62,208 @@ where
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U> impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>>,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>>,
B: MessageBody + 'static,
B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{ {
type Output = Result<(), DispatchError>; type Output = Result<(), crate::error::DispatchError>;
#[inline] #[inline]
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut(); let this = self.get_mut();
loop { while let Some((req, tx)) =
match ready!(Pin::new(&mut this.connection).poll_accept(cx)) { ready!(Pin::new(&mut this.connection).poll_accept(cx)?)
None => return Poll::Ready(Ok(())), {
let (parts, body) = req.into_parts();
let pl = crate::h2::Payload::new(body);
let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
let mut req = Request::with_payload(pl);
Some(Err(err)) => return Poll::Ready(Err(err.into())), 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;
Some(Ok((req, res))) => { // merge on_connect_ext data into request extensions
// update keep-alive expire this.on_connect_data.merge_into(&mut req);
if this.ka_timer.is_some() {
if let Some(expire) = this.config.keep_alive_expire() { let fut = this.flow.service.call(req);
this.ka_expire = expire; let config = this.config.clone();
// multiplex request handling with spawn task
actix_rt::spawn(async move {
// resolve service call and send response.
let res = match fut.await {
Ok(res) => handle_response(res.into(), tx, config).await,
Err(err) => {
let res: Response<AnyBody> = err.into();
handle_response(res, tx, config).await
}
};
// log error.
if let Err(err) = res {
match err {
DispatchError::SendResponse(err) => {
trace!("Error sending HTTP/2 response: {:?}", err)
}
DispatchError::SendData(err) => warn!("{:?}", err),
DispatchError::ResponseBody(err) => {
error!("Response payload stream error: {:?}", err)
} }
} }
}
});
}
let (parts, body) = req.into_parts(); Poll::Ready(Ok(()))
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(); enum DispatchError {
head.uri = parts.uri; SendResponse(h2::Error),
head.method = parts.method; SendData(h2::Error),
head.version = parts.version; ResponseBody(Box<dyn StdError>),
head.headers = parts.headers.into(); }
head.peer_addr = this.peer_addr;
// merge on_connect_ext data into request extensions async fn handle_response<B>(
this.on_connect_data.merge_into(&mut req); res: Response<B>,
mut tx: SendResponse<Bytes>,
config: ServiceConfig,
) -> Result<(), DispatchError>
where
B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{
let (res, body) = res.replace_body(());
let svc = ServiceResponse::<S::Future, S::Response, S::Error, B> { // prepare response.
state: ServiceResponseState::ServiceCall( let mut size = body.size();
this.flow.borrow_mut().service.call(req), let res = prepare_response(config, res.head(), &mut size);
Some(res), let eof = size.is_eof();
),
config: this.config.clone(),
buffer: None,
_phantom: PhantomData,
};
actix_rt::spawn(svc); // send response head and return on eof.
let mut stream = tx
.send_response(res, eof)
.map_err(DispatchError::SendResponse)?;
if eof {
return Ok(());
}
// poll response body and send chunks to client.
actix_rt::pin!(body);
while let Some(res) = poll_fn(|cx| body.as_mut().poll_next(cx)).await {
let mut chunk = res.map_err(|err| DispatchError::ResponseBody(err.into()))?;
'send: loop {
// reserve enough space and wait for stream ready.
stream.reserve_capacity(cmp::min(chunk.len(), CHUNK_SIZE));
match poll_fn(|cx| stream.poll_capacity(cx)).await {
// No capacity left. drop body and return.
None => return Ok(()),
Some(res) => {
// Split chuck to writeable size and send to client.
let cap = res.map_err(DispatchError::SendData)?;
let len = chunk.len();
let bytes = chunk.split_to(cmp::min(cap, len));
stream
.send_data(bytes, false)
.map_err(DispatchError::SendData)?;
// Current chuck completely sent. break send loop and poll next one.
if chunk.is_empty() {
break 'send;
}
} }
} }
} }
} }
// response body streaming finished. send end of stream and return.
stream
.send_data(Bytes::new(), true)
.map_err(DispatchError::SendData)?;
Ok(())
} }
#[pin_project::pin_project] fn prepare_response(
struct ServiceResponse<F, I, E, B> {
#[pin]
state: ServiceResponseState<F, B>,
config: ServiceConfig, config: ServiceConfig,
buffer: Option<Bytes>, head: &ResponseHead,
_phantom: PhantomData<(I, E)>, size: &mut BodySize,
} ) -> http::Response<()> {
let mut has_date = false;
let mut skip_len = size != &BodySize::Stream;
#[pin_project::pin_project(project = ServiceResponseStateProj)] let mut res = http::Response::new(());
enum ServiceResponseState<F, B> { *res.status_mut() = head.status;
ServiceCall(#[pin] F, Option<SendResponse<Bytes>>), *res.version_mut() = http::Version::HTTP_2;
SendPayload(SendStream<Bytes>, #[pin] ResponseBody<B>),
}
impl<F, I, E, B> ServiceResponse<F, I, E, B> // Content length
where match head.status {
F: Future<Output = Result<I, E>>, http::StatusCode::NO_CONTENT
E: Into<Error>, | http::StatusCode::CONTINUE
I: Into<Response<B>>, | http::StatusCode::PROCESSING => *size = BodySize::None,
B: MessageBody, http::StatusCode::SWITCHING_PROTOCOLS => {
{ skip_len = true;
fn prepare_response( *size = BodySize::Stream;
&self, }
head: &ResponseHead, _ => {}
size: &mut BodySize, }
) -> http::Response<()> {
let mut has_date = false;
let mut skip_len = size != &BodySize::Stream;
let mut res = http::Response::new(()); let _ = match size {
*res.status_mut() = head.status; BodySize::None | BodySize::Stream => None,
*res.version_mut() = http::Version::HTTP_2; BodySize::Empty => res
.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
BodySize::Sized(len) => {
let mut buf = itoa::Buffer::new();
// Content length res.headers_mut().insert(
match head.status { CONTENT_LENGTH,
http::StatusCode::NO_CONTENT HeaderValue::from_str(buf.format(*len)).unwrap(),
| http::StatusCode::CONTINUE )
| http::StatusCode::PROCESSING => *size = BodySize::None, }
http::StatusCode::SWITCHING_PROTOCOLS => { };
skip_len = true;
*size = BodySize::Stream; // copy headers
} for (key, value) in head.headers.iter() {
match *key {
// 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,
_ => {} _ => {}
} }
let _ = match size { res.headers_mut().append(key, value.clone());
BodySize::None | BodySize::Stream => None,
BodySize::Empty => res
.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
BodySize::Sized(len) => res.headers_mut().insert(
CONTENT_LENGTH,
HeaderValue::try_from(format!("{}", len)).unwrap(),
),
};
// copy headers
for (key, value) in head.headers.iter() {
match *key {
// omit HTTP/1 only headers
CONNECTION | TRANSFER_ENCODING => continue,
CONTENT_LENGTH if skip_len => continue,
DATE => has_date = true,
_ => {}
}
res.headers_mut().append(key, value.clone());
}
// set date header
if !has_date {
let mut bytes = BytesMut::with_capacity(29);
self.config.set_date_header(&mut bytes);
res.headers_mut().insert(
DATE,
// SAFETY: serialized date-times are known ASCII strings
unsafe { HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) },
);
}
res
} }
}
impl<F, I, E, B> Future for ServiceResponse<F, I, E, B> // set date header
where if !has_date {
F: Future<Output = Result<I, E>>, let mut bytes = BytesMut::with_capacity(29);
E: Into<Error>, config.set_date_header(&mut bytes);
I: Into<Response<B>>, res.headers_mut().insert(
B: MessageBody, DATE,
{ // SAFETY: serialized date-times are known ASCII strings
type Output = (); unsafe { HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) },
);
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut this = self.as_mut().project();
match this.state.project() {
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);
this = self.as_mut().project();
let stream = match send.send_response(h2_res, size.is_eof()) {
Err(e) => {
trace!("Error sending HTTP/2 response: {:?}", e);
return Poll::Ready(());
}
Ok(stream) => stream,
};
if size.is_eof() {
Poll::Ready(())
} else {
this.state
.set(ServiceResponseState::SendPayload(stream, body));
self.poll(cx)
}
}
Err(e) => {
let res: Response = e.into().into();
let (res, body) = res.replace_body(());
let mut send = send.take().unwrap();
let mut size = body.size();
let h2_res =
self.as_mut().prepare_response(res.head(), &mut size);
this = self.as_mut().project();
let stream = match send.send_response(h2_res, size.is_eof()) {
Err(e) => {
trace!("Error sending HTTP/2 response: {:?}", e);
return Poll::Ready(());
}
Ok(stream) => stream,
};
if size.is_eof() {
Poll::Ready(())
} else {
this.state.set(ServiceResponseState::SendPayload(
stream,
body.into_body(),
));
self.poll(cx)
}
}
}
}
ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => {
loop {
loop {
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(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 = cmp::min(buffer.len(), CHUNK_SIZE);
stream.reserve_capacity(cap);
} else {
this.buffer.take();
}
}
Some(Err(e)) => {
warn!("{:?}", e);
return Poll::Ready(());
}
}
}
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(());
}
Some(Ok(chunk)) => {
stream.reserve_capacity(cmp::min(
chunk.len(),
CHUNK_SIZE,
));
*this.buffer = Some(chunk);
}
Some(Err(e)) => {
error!("Response payload stream error: {:?}", e);
return Poll::Ready(());
}
},
}
}
}
}
}
} }
res
} }

View File

@ -1,4 +1,4 @@
//! HTTP/2 implementation. //! HTTP/2 protocol.
use std::{ use std::{
pin::Pin, pin::Pin,

View File

@ -1,29 +1,32 @@
use std::cell::RefCell; use std::{
use std::future::Future; error::Error as StdError,
use std::marker::PhantomData; future::Future,
use std::pin::Pin; marker::PhantomData,
use std::task::{Context, Poll}; net,
use std::{net, rc::Rc}; pin::Pin,
rc::Rc,
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_service::{ use actix_service::{
fn_factory, fn_service, pipeline_factory, IntoServiceFactory, Service, fn_factory, fn_service, IntoServiceFactory, Service, ServiceFactory,
ServiceFactory, ServiceFactoryExt as _,
}; };
use actix_utils::future::ready;
use bytes::Bytes; use bytes::Bytes;
use futures_core::ready; use futures_core::{future::LocalBoxFuture, ready};
use futures_util::future::ok; use h2::server::{handshake as h2_handshake, Handshake as H2Handshake};
use h2::server::{self, Handshake};
use log::error; use log::error;
use crate::body::MessageBody; use crate::{
use crate::config::ServiceConfig; body::{AnyBody, MessageBody},
use crate::error::{DispatchError, Error}; config::ServiceConfig,
use crate::request::Request; error::DispatchError,
use crate::response::Response; service::HttpFlow,
use crate::service::HttpFlow; ConnectCallback, OnConnectData, Request, Response,
use crate::{ConnectCallback, OnConnectData}; };
use super::dispatcher::Dispatcher; use super::dispatcher::Dispatcher;
@ -38,10 +41,12 @@ pub struct H2Service<T, S, B> {
impl<T, S, B> H2Service<T, S, B> impl<T, S, B> H2Service<T, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
/// Create new `H2Service` instance with config. /// Create new `H2Service` instance with config.
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>( pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
@ -66,10 +71,13 @@ where
impl<S, B> H2Service<TcpStream, S, B> impl<S, B> H2Service<TcpStream, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
/// Create plain TCP based service /// Create plain TCP based service
pub fn tcp( pub fn tcp(
@ -81,12 +89,12 @@ where
Error = DispatchError, Error = DispatchError,
InitError = S::InitError, InitError = S::InitError,
> { > {
pipeline_factory(fn_factory(|| async { fn_factory(|| {
Ok::<_, S::InitError>(fn_service(|io: TcpStream| { ready(Ok::<_, S::InitError>(fn_service(|io: TcpStream| {
let peer_addr = io.peer_addr().ok(); let peer_addr = io.peer_addr().ok();
ok::<_, DispatchError>((io, peer_addr)) ready(Ok::<_, DispatchError>((io, peer_addr)))
})) })))
})) })
.and_then(self) .and_then(self)
} }
} }
@ -94,18 +102,21 @@ where
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
mod openssl { mod openssl {
use actix_service::{fn_factory, fn_service, ServiceFactoryExt}; use actix_service::{fn_factory, fn_service, ServiceFactoryExt};
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream}; use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream};
use actix_tls::accept::TlsError; use actix_tls::accept::TlsError;
use super::*; use super::*;
impl<S, B> H2Service<SslStream<TcpStream>, S, B> impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
/// Create OpenSSL based service /// Create OpenSSL based service
pub fn openssl( pub fn openssl(
@ -118,18 +129,18 @@ mod openssl {
Error = TlsError<SslError, DispatchError>, Error = TlsError<SslError, DispatchError>,
InitError = S::InitError, InitError = S::InitError,
> { > {
pipeline_factory( Acceptor::new(acceptor)
Acceptor::new(acceptor) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(fn_factory(|| {
) ready(Ok::<_, S::InitError>(fn_service(
.and_then(fn_factory(|| { |io: TlsStream<TcpStream>| {
ok::<_, S::InitError>(fn_service(|io: SslStream<TcpStream>| { let peer_addr = io.get_ref().peer_addr().ok();
let peer_addr = io.get_ref().peer_addr().ok(); ready(Ok((io, peer_addr)))
ok((io, peer_addr)) },
)))
})) }))
})) .and_then(self.map_err(TlsError::Service))
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
@ -145,10 +156,13 @@ mod rustls {
impl<S, B> H2Service<TlsStream<TcpStream>, S, B> impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
/// Create Rustls based service /// Create Rustls based service
pub fn rustls( pub fn rustls(
@ -161,95 +175,64 @@ mod rustls {
Error = TlsError<io::Error, DispatchError>, Error = TlsError<io::Error, DispatchError>,
InitError = S::InitError, InitError = S::InitError,
> { > {
let protos = vec!["h2".to_string().into()]; let mut protos = vec![b"h2".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.set_protocols(&protos); config.set_protocols(&protos);
pipeline_factory( Acceptor::new(config)
Acceptor::new(config) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(fn_factory(|| {
) ready(Ok::<_, S::InitError>(fn_service(
.and_then(fn_factory(|| { |io: TlsStream<TcpStream>| {
ok::<_, S::InitError>(fn_service(|io: TlsStream<TcpStream>| { let peer_addr = io.get_ref().0.peer_addr().ok();
let peer_addr = io.get_ref().0.peer_addr().ok(); ready(Ok((io, peer_addr)))
ok((io, peer_addr)) },
)))
})) }))
})) .and_then(self.map_err(TlsError::Service))
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B> impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin + 'static,
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
type Config = (); type Config = ();
type Service = H2ServiceHandler<T, S::Service, B>; type Service = H2ServiceHandler<T, S::Service, B>;
type InitError = S::InitError; type InitError = S::InitError;
type Future = H2ServiceResponse<T, S, B>; type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
H2ServiceResponse { let service = self.srv.new_service(());
fut: self.srv.new_service(()), let cfg = self.cfg.clone();
cfg: Some(self.cfg.clone()), let on_connect_ext = self.on_connect_ext.clone();
on_connect_ext: self.on_connect_ext.clone(),
_phantom: PhantomData,
}
}
}
#[doc(hidden)] Box::pin(async move {
#[pin_project::pin_project] let service = service.await?;
pub struct H2ServiceResponse<T, S, B> Ok(H2ServiceHandler::new(cfg, on_connect_ext, service))
where
S: ServiceFactory<Request>,
{
#[pin]
fut: S::Future,
cfg: Option<ServiceConfig>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_phantom: PhantomData<B>,
}
impl<T, S, B> Future for H2ServiceResponse<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
type Output = Result<H2ServiceHandler<T, S::Service, B>, S::InitError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.as_mut().project();
this.fut.poll(cx).map_ok(|service| {
let this = self.as_mut().project();
H2ServiceHandler::new(
this.cfg.take().unwrap(),
this.on_connect_ext.clone(),
service,
)
}) })
} }
} }
/// `Service` implementation for http/2 transport /// `Service` implementation for HTTP/2 transport
pub struct H2ServiceHandler<T, S, B> pub struct H2ServiceHandler<T, S, B>
where where
S: Service<Request>, S: Service<Request>,
{ {
flow: Rc<RefCell<HttpFlow<S, (), ()>>>, flow: Rc<HttpFlow<S, (), ()>>,
cfg: ServiceConfig, cfg: ServiceConfig,
on_connect_ext: Option<Rc<ConnectCallback<T>>>, on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_phantom: PhantomData<B>, _phantom: PhantomData<B>,
@ -258,7 +241,7 @@ where
impl<T, S, B> H2ServiceHandler<T, S, B> impl<T, S, B> H2ServiceHandler<T, S, B>
where where
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
@ -281,24 +264,25 @@ impl<T, S, B> Service<(T, Option<net::SocketAddr>)> for H2ServiceHandler<T, S, B
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
type Future = H2ServiceHandlerResponse<T, S, B>; type Future = H2ServiceHandlerResponse<T, S, B>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.flow.borrow_mut().service.poll_ready(cx).map_err(|e| { self.flow.service.poll_ready(cx).map_err(|e| {
let e = e.into(); let e = e.into();
error!("Service readiness error: {:?}", e); error!("Service readiness error: {:?}", e);
DispatchError::Service(e) DispatchError::Service(e)
}) })
} }
fn call(&mut self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future { fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
let on_connect_data = let on_connect_data =
OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
@ -308,7 +292,7 @@ where
Some(self.cfg.clone()), Some(self.cfg.clone()),
addr, addr,
on_connect_data, on_connect_data,
server::handshake(io), h2_handshake(io),
), ),
} }
} }
@ -321,11 +305,11 @@ where
{ {
Incoming(Dispatcher<T, S, B, (), ()>), Incoming(Dispatcher<T, S, B, (), ()>),
Handshake( Handshake(
Option<Rc<RefCell<HttpFlow<S, (), ()>>>>, Option<Rc<HttpFlow<S, (), ()>>>,
Option<ServiceConfig>, Option<ServiceConfig>,
Option<net::SocketAddr>, Option<net::SocketAddr>,
OnConnectData, OnConnectData,
Handshake<T, Bytes>, H2Handshake<T, Bytes>,
), ),
} }
@ -333,7 +317,7 @@ pub struct H2ServiceHandlerResponse<T, S, B>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
@ -345,10 +329,11 @@ impl<T, S, B> Future for H2ServiceHandlerResponse<T, S, B>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{ {
type Output = Result<(), DispatchError>; type Output = Result<(), DispatchError>;
@ -369,7 +354,6 @@ where
conn, conn,
on_connect_data, on_connect_data,
config.take().unwrap(), config.take().unwrap(),
None,
*peer_addr, *peer_addr,
)); ));
self.poll(cx) self.poll(cx)

View File

@ -0,0 +1,50 @@
//! 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 struct Seal;
pub trait Sealed {
fn try_as_name(&self, seal: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName>;
}
impl Sealed for HeaderName {
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
Ok(Cow::Borrowed(self))
}
}
impl AsHeaderName for HeaderName {}
impl Sealed for &HeaderName {
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
Ok(Cow::Borrowed(*self))
}
}
impl AsHeaderName for &HeaderName {}
impl Sealed for &str {
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned)
}
}
impl AsHeaderName for &str {}
impl Sealed for String {
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned)
}
}
impl AsHeaderName for String {}
impl Sealed for &String {
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned)
}
}
impl AsHeaderName for &String {}

View File

@ -1,65 +0,0 @@
use crate::header::{QualityItem, CONTENT_LANGUAGE};
use language_tags::LanguageTag;
header! {
/// `Content-Language` header, defined in
/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-3.1.3.2)
///
/// The `Content-Language` header field describes the natural language(s)
/// of the intended audience for the representation. Note that this
/// might not be equivalent to all the languages used within the
/// representation.
///
/// # ABNF
///
/// ```text
/// Content-Language = 1#language-tag
/// ```
///
/// # Example values
///
/// * `da`
/// * `mi, en`
///
/// # Examples
///
/// ```rust
/// # extern crate actix_http;
/// # #[macro_use] extern crate language_tags;
/// use actix_http::Response;
/// # use actix_http::http::header::{ContentLanguage, qitem};
/// #
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.set(
/// ContentLanguage(vec![
/// qitem(langtag!(en)),
/// ])
/// );
/// # }
/// ```
///
/// ```rust
/// # extern crate actix_http;
/// # #[macro_use] extern crate language_tags;
/// use actix_http::Response;
/// # use actix_http::http::header::{ContentLanguage, qitem};
/// #
/// # fn main() {
///
/// let mut builder = Response::Ok();
/// builder.set(
/// ContentLanguage(vec![
/// qitem(langtag!(da)),
/// qitem(langtag!(en;;;GB)),
/// ])
/// );
/// # }
/// ```
(ContentLanguage, CONTENT_LANGUAGE) => (QualityItem<LanguageTag>)+
test_content_language {
test_header!(test1, vec![b"da"]);
test_header!(test2, vec![b"mi, en"]);
}
}

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())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,58 @@
//! Various http headers //! Pre-defined `HeaderName`s, traits for parsing and conversion, and other header utility methods.
// This is mostly copy of [hyper](https://github.com/hyperium/hyper/tree/master/src/header)
use std::convert::TryFrom;
use std::{fmt, str::FromStr};
use bytes::{Bytes, BytesMut};
use http::Error as HttpError;
use mime::Mime;
use percent_encoding::{AsciiSet, CONTROLS}; use percent_encoding::{AsciiSet, CONTROLS};
pub use http::header::*; // re-export from http except header map related items
pub use http::header::{
HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError,
};
// re-export const header names
pub use http::header::{
ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES,
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN,
ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_MAX_AGE,
ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AGE, ALLOW, ALT_SVC,
AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_DISPOSITION, CONTENT_ENCODING,
CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_RANGE,
CONTENT_SECURITY_POLICY, CONTENT_SECURITY_POLICY_REPORT_ONLY, CONTENT_TYPE, COOKIE,
DATE, DNT, ETAG, EXPECT, EXPIRES, FORWARDED, FROM, HOST, IF_MATCH,
IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_RANGE, IF_UNMODIFIED_SINCE, LAST_MODIFIED,
LINK, LOCATION, MAX_FORWARDS, ORIGIN, PRAGMA, PROXY_AUTHENTICATE,
PROXY_AUTHORIZATION, PUBLIC_KEY_PINS, PUBLIC_KEY_PINS_REPORT_ONLY, RANGE, REFERER,
REFERRER_POLICY, REFRESH, RETRY_AFTER, SEC_WEBSOCKET_ACCEPT,
SEC_WEBSOCKET_EXTENSIONS, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL,
SEC_WEBSOCKET_VERSION, SERVER, SET_COOKIE, STRICT_TRANSPORT_SECURITY, TE, TRAILER,
TRANSFER_ENCODING, UPGRADE, UPGRADE_INSECURE_REQUESTS, USER_AGENT, VARY, VIA,
WARNING, WWW_AUTHENTICATE, X_CONTENT_TYPE_OPTIONS, X_DNS_PREFETCH_CONTROL,
X_FRAME_OPTIONS, X_XSS_PROTECTION,
};
use crate::error::ParseError; use crate::error::ParseError;
use crate::httpmessage::HttpMessage; use crate::HttpMessage;
mod as_name;
mod into_pair;
mod into_value;
mod utils;
mod common;
pub(crate) mod map; pub(crate) mod map;
mod shared; mod shared;
pub use self::common::*;
#[doc(hidden)] #[doc(hidden)]
pub use self::shared::*; pub use self::shared::*;
pub use self::as_name::AsHeaderName;
pub use self::into_pair::IntoHeaderPair;
pub use self::into_value::IntoHeaderValue;
#[doc(hidden)] #[doc(hidden)]
pub use self::map::GetAll; pub use self::map::GetAll;
pub use self::map::HeaderMap; pub use self::map::HeaderMap;
pub use self::utils::*;
/// A trait for any object that will represent a header field and value. /// A trait for any object that already represents a valid header field and value.
pub trait Header pub trait Header: IntoHeaderValue {
where
Self: IntoHeaderValue,
{
/// Returns the name of the header field /// Returns the name of the header field
fn name() -> HeaderName; fn name() -> HeaderName;
@ -37,358 +60,15 @@ where
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>; fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
} }
/// A trait for any object that can be Converted to a `HeaderValue` /// Convert `http::HeaderMap` to our `HeaderMap`.
pub trait IntoHeaderValue: Sized {
/// The type returned in the event of a conversion error.
type Error: Into<HttpError>;
/// Try to convert value to a Header value.
fn try_into(self) -> Result<HeaderValue, Self::Error>;
}
impl IntoHeaderValue for HeaderValue {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
Ok(self)
}
}
impl<'a> IntoHeaderValue for &'a str {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
self.parse()
}
}
impl<'a> IntoHeaderValue for &'a [u8] {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_bytes(self)
}
}
impl IntoHeaderValue for Bytes {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_maybe_shared(self)
}
}
impl IntoHeaderValue for Vec<u8> {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self)
}
}
impl IntoHeaderValue for String {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self)
}
}
impl IntoHeaderValue for usize {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
let s = format!("{}", self);
HeaderValue::try_from(s)
}
}
impl IntoHeaderValue for u64 {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
let s = format!("{}", self);
HeaderValue::try_from(s)
}
}
impl IntoHeaderValue for Mime {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(format!("{}", self))
}
}
/// Represents supported types of content encodings
#[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 {
#[inline]
/// Is the content compressed?
pub fn is_compression(self) -> bool {
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
}
#[inline]
/// Convert content encoding to string
pub fn as_str(self) -> &'static str {
match self {
ContentEncoding::Br => "br",
ContentEncoding::Gzip => "gzip",
ContentEncoding::Deflate => "deflate",
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
}
}
#[inline]
/// default quality value
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<'a> From<&'a str> for ContentEncoding {
fn from(s: &'a str) -> ContentEncoding {
let s = s.trim();
if s.eq_ignore_ascii_case("br") {
ContentEncoding::Br
} else if s.eq_ignore_ascii_case("gzip") {
ContentEncoding::Gzip
} else if s.eq_ignore_ascii_case("deflate") {
ContentEncoding::Deflate
} else {
ContentEncoding::Identity
}
}
}
#[doc(hidden)]
pub(crate) struct Writer {
buf: BytesMut,
}
impl Writer {
fn new() -> Writer {
Writer {
buf: BytesMut::new(),
}
}
fn take(&mut self) -> Bytes {
self.buf.split().freeze()
}
}
impl fmt::Write for Writer {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
self.buf.extend_from_slice(s.as_bytes());
Ok(())
}
#[inline]
fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
fmt::write(self, args)
}
}
#[inline]
#[doc(hidden)]
/// Reads a comma-delimited raw header into a Vec.
pub fn from_comma_delimited<'a, I: Iterator<Item = &'a HeaderValue> + 'a, T: FromStr>(
all: I,
) -> Result<Vec<T>, ParseError> {
let mut result = Vec::new();
for h in all {
let s = h.to_str().map_err(|_| ParseError::Header)?;
result.extend(
s.split(',')
.filter_map(|x| match x.trim() {
"" => None,
y => Some(y),
})
.filter_map(|x| x.trim().parse().ok()),
)
}
Ok(result)
}
#[inline]
#[doc(hidden)]
/// Reads a single string when parsing a header.
pub fn from_one_raw_str<T: FromStr>(val: Option<&HeaderValue>) -> Result<T, ParseError> {
if let Some(line) = val {
let line = line.to_str().map_err(|_| ParseError::Header)?;
if !line.is_empty() {
return T::from_str(line).or(Err(ParseError::Header));
}
}
Err(ParseError::Header)
}
#[inline]
#[doc(hidden)]
/// Format an array into a comma-delimited string.
pub fn fmt_comma_delimited<T>(f: &mut fmt::Formatter<'_>, parts: &[T]) -> fmt::Result
where
T: fmt::Display,
{
let mut iter = parts.iter();
if let Some(part) = iter.next() {
fmt::Display::fmt(part, f)?;
}
for part in iter {
f.write_str(", ")?;
fmt::Display::fmt(part, f)?;
}
Ok(())
}
// From hyper v0.11.27 src/header/parsing.rs
/// The value part of an extended parameter consisting of three parts:
/// the REQUIRED character set name (`charset`), the OPTIONAL language information (`language_tag`),
/// and a character sequence representing the actual value (`value`), separated by single quote
/// characters. It is defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2).
#[derive(Clone, Debug, PartialEq)]
pub struct ExtendedValue {
/// The character set that is used to encode the `value` to a string.
pub charset: Charset,
/// The human language details of the `value`, if available.
pub language_tag: Option<LanguageTag>,
/// The parameter value, as expressed in octets.
pub value: Vec<u8>,
}
/// Parses extended header parameter values (`ext-value`), as defined in
/// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2).
///
/// Extended values are denoted by parameter names that end with `*`.
///
/// ## ABNF
///
/// ```text
/// ext-value = charset "'" [ language ] "'" value-chars
/// ; like RFC 2231's <extended-initial-value>
/// ; (see [RFC2231], Section 7)
///
/// charset = "UTF-8" / "ISO-8859-1" / mime-charset
///
/// mime-charset = 1*mime-charsetc
/// mime-charsetc = ALPHA / DIGIT
/// / "!" / "#" / "$" / "%" / "&"
/// / "+" / "-" / "^" / "_" / "`"
/// / "{" / "}" / "~"
/// ; as <mime-charset> in Section 2.3 of [RFC2978]
/// ; except that the single quote is not included
/// ; SHOULD be registered in the IANA charset registry
///
/// language = <Language-Tag, defined in [RFC5646], Section 2.1>
///
/// value-chars = *( pct-encoded / attr-char )
///
/// pct-encoded = "%" HEXDIG HEXDIG
/// ; see [RFC3986], Section 2.1
///
/// attr-char = ALPHA / DIGIT
/// / "!" / "#" / "$" / "&" / "+" / "-" / "."
/// / "^" / "_" / "`" / "|" / "~"
/// ; token except ( "*" / "'" / "%" )
/// ```
pub fn parse_extended_value(
val: &str,
) -> Result<ExtendedValue, crate::error::ParseError> {
// Break into three pieces separated by the single-quote character
let mut parts = val.splitn(3, '\'');
// Interpret the first piece as a Charset
let charset: Charset = match parts.next() {
None => return Err(crate::error::ParseError::Header),
Some(n) => FromStr::from_str(n).map_err(|_| crate::error::ParseError::Header)?,
};
// Interpret the second piece as a language tag
let language_tag: Option<LanguageTag> = match parts.next() {
None => return Err(crate::error::ParseError::Header),
Some("") => None,
Some(s) => match s.parse() {
Ok(lt) => Some(lt),
Err(_) => return Err(crate::error::ParseError::Header),
},
};
// Interpret the third piece as a sequence of value characters
let value: Vec<u8> = match parts.next() {
None => return Err(crate::error::ParseError::Header),
Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(),
};
Ok(ExtendedValue {
value,
charset,
language_tag,
})
}
impl fmt::Display for ExtendedValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let encoded_value =
percent_encoding::percent_encode(&self.value[..], HTTP_VALUE);
if let Some(ref lang) = self.language_tag {
write!(f, "{}'{}'{}", self.charset, lang, encoded_value)
} else {
write!(f, "{}''{}", self.charset, encoded_value)
}
}
}
/// Percent encode a sequence of bytes with a character set defined in
/// <https://tools.ietf.org/html/rfc5987#section-3.2>
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
fmt::Display::fmt(&encoded, f)
}
/// Convert http::HeaderMap to a HeaderMap
impl From<http::HeaderMap> for HeaderMap { impl From<http::HeaderMap> for HeaderMap {
fn from(map: http::HeaderMap) -> HeaderMap { fn from(mut map: http::HeaderMap) -> HeaderMap {
let mut new_map = HeaderMap::with_capacity(map.capacity()); HeaderMap::from_drain(map.drain())
for (h, v) in map.iter() {
new_map.append(h.clone(), v.clone());
}
new_map
} }
} }
// This encode set is used for HTTP header values and is defined at /// This encode set is used for HTTP header values and is defined at
// https://tools.ietf.org/html/rfc5987#section-3.2 /// https://tools.ietf.org/html/rfc5987#section-3.2.
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
.add(b' ') .add(b' ')
.add(b'"') .add(b'"')
@ -410,91 +90,3 @@ pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
.add(b']') .add(b']')
.add(b'{') .add(b'{')
.add(b'}'); .add(b'}');
#[cfg(test)]
mod tests {
use super::shared::Charset;
use super::{parse_extended_value, ExtendedValue};
use language_tags::LanguageTag;
#[test]
fn test_parse_extended_value_with_encoding_and_language_tag() {
let expected_language_tag = "en".parse::<LanguageTag>().unwrap();
// RFC 5987, Section 3.2.2
// Extended notation, using the Unicode character U+00A3 (POUND SIGN)
let result = parse_extended_value("iso-8859-1'en'%A3%20rates");
assert!(result.is_ok());
let extended_value = result.unwrap();
assert_eq!(Charset::Iso_8859_1, extended_value.charset);
assert!(extended_value.language_tag.is_some());
assert_eq!(expected_language_tag, extended_value.language_tag.unwrap());
assert_eq!(
vec![163, b' ', b'r', b'a', b't', b'e', b's'],
extended_value.value
);
}
#[test]
fn test_parse_extended_value_with_encoding() {
// RFC 5987, Section 3.2.2
// Extended notation, using the Unicode characters U+00A3 (POUND SIGN)
// and U+20AC (EURO SIGN)
let result = parse_extended_value("UTF-8''%c2%a3%20and%20%e2%82%ac%20rates");
assert!(result.is_ok());
let extended_value = result.unwrap();
assert_eq!(Charset::Ext("UTF-8".to_string()), extended_value.charset);
assert!(extended_value.language_tag.is_none());
assert_eq!(
vec![
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
b't', b'e', b's',
],
extended_value.value
);
}
#[test]
fn test_parse_extended_value_missing_language_tag_and_encoding() {
// From: https://greenbytes.de/tech/tc2231/#attwithfn2231quot2
let result = parse_extended_value("foo%20bar.html");
assert!(result.is_err());
}
#[test]
fn test_parse_extended_value_partially_formatted() {
let result = parse_extended_value("UTF-8'missing third part");
assert!(result.is_err());
}
#[test]
fn test_parse_extended_value_partially_formatted_blank() {
let result = parse_extended_value("blank second part'");
assert!(result.is_err());
}
#[test]
fn test_fmt_extended_value_with_encoding_and_language_tag() {
let extended_value = ExtendedValue {
charset: Charset::Iso_8859_1,
language_tag: Some("en".parse().expect("Could not parse language tag")),
value: vec![163, b' ', b'r', b'a', b't', b'e', b's'],
};
assert_eq!("ISO-8859-1'en'%A3%20rates", format!("{}", extended_value));
}
#[test]
fn test_fmt_extended_value_with_encoding() {
let extended_value = ExtendedValue {
charset: Charset::Ext("UTF-8".to_string()),
language_tag: None,
value: vec![
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
b't', b'e', b's',
],
};
assert_eq!(
"UTF-8''%C2%A3%20and%20%E2%82%AC%20rates",
format!("{}", extended_value)
);
}
}

View File

@ -104,7 +104,7 @@ impl Display for Charset {
impl FromStr for Charset { impl FromStr for Charset {
type Err = crate::Error; type Err = crate::Error;
fn from_str(s: &str) -> crate::Result<Charset> { fn from_str(s: &str) -> Result<Charset, crate::Error> {
Ok(match s.to_ascii_uppercase().as_ref() { Ok(match s.to_ascii_uppercase().as_ref() {
"US-ASCII" => Us_Ascii, "US-ASCII" => Us_Ascii,
"ISO-8859-1" => Iso_8859_1, "ISO-8859-1" => Iso_8859_1,

View File

@ -0,0 +1,113 @@
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,
// Zstd algorithm.
Zstd,
/// 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::Zstd => "zstd",
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,
ContentEncoding::Zstd => 0.0,
}
}
}
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 if val.eq_ignore_ascii_case("zstd") {
ContentEncoding::Zstd
} 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

@ -0,0 +1,193 @@
use std::{fmt, str::FromStr};
use language_tags::LanguageTag;
use crate::header::{Charset, HTTP_VALUE};
// From hyper v0.11.27 src/header/parsing.rs
/// The value part of an extended parameter consisting of three parts:
/// - The REQUIRED character set name (`charset`).
/// - The OPTIONAL language information (`language_tag`).
/// - A character sequence representing the actual value (`value`), separated by single quotes.
///
/// It is defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2).
#[derive(Clone, Debug, PartialEq)]
pub struct ExtendedValue {
/// The character set that is used to encode the `value` to a string.
pub charset: Charset,
/// The human language details of the `value`, if available.
pub language_tag: Option<LanguageTag>,
/// The parameter value, as expressed in octets.
pub value: Vec<u8>,
}
/// Parses extended header parameter values (`ext-value`), as defined in
/// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2).
///
/// Extended values are denoted by parameter names that end with `*`.
///
/// ## ABNF
///
/// ```text
/// ext-value = charset "'" [ language ] "'" value-chars
/// ; like RFC 2231's <extended-initial-value>
/// ; (see [RFC2231], Section 7)
///
/// charset = "UTF-8" / "ISO-8859-1" / mime-charset
///
/// mime-charset = 1*mime-charsetc
/// mime-charsetc = ALPHA / DIGIT
/// / "!" / "#" / "$" / "%" / "&"
/// / "+" / "-" / "^" / "_" / "`"
/// / "{" / "}" / "~"
/// ; as <mime-charset> in Section 2.3 of [RFC2978]
/// ; except that the single quote is not included
/// ; SHOULD be registered in the IANA charset registry
///
/// language = <Language-Tag, defined in [RFC5646], Section 2.1>
///
/// value-chars = *( pct-encoded / attr-char )
///
/// pct-encoded = "%" HEXDIG HEXDIG
/// ; see [RFC3986], Section 2.1
///
/// attr-char = ALPHA / DIGIT
/// / "!" / "#" / "$" / "&" / "+" / "-" / "."
/// / "^" / "_" / "`" / "|" / "~"
/// ; token except ( "*" / "'" / "%" )
/// ```
pub fn parse_extended_value(
val: &str,
) -> Result<ExtendedValue, crate::error::ParseError> {
// Break into three pieces separated by the single-quote character
let mut parts = val.splitn(3, '\'');
// Interpret the first piece as a Charset
let charset: Charset = match parts.next() {
None => return Err(crate::error::ParseError::Header),
Some(n) => FromStr::from_str(n).map_err(|_| crate::error::ParseError::Header)?,
};
// Interpret the second piece as a language tag
let language_tag: Option<LanguageTag> = match parts.next() {
None => return Err(crate::error::ParseError::Header),
Some("") => None,
Some(s) => match s.parse() {
Ok(lt) => Some(lt),
Err(_) => return Err(crate::error::ParseError::Header),
},
};
// Interpret the third piece as a sequence of value characters
let value: Vec<u8> = match parts.next() {
None => return Err(crate::error::ParseError::Header),
Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(),
};
Ok(ExtendedValue {
charset,
language_tag,
value,
})
}
impl fmt::Display for ExtendedValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let encoded_value =
percent_encoding::percent_encode(&self.value[..], HTTP_VALUE);
if let Some(ref lang) = self.language_tag {
write!(f, "{}'{}'{}", self.charset, lang, encoded_value)
} else {
write!(f, "{}''{}", self.charset, encoded_value)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_extended_value_with_encoding_and_language_tag() {
let expected_language_tag = "en".parse::<LanguageTag>().unwrap();
// RFC 5987, Section 3.2.2
// Extended notation, using the Unicode character U+00A3 (POUND SIGN)
let result = parse_extended_value("iso-8859-1'en'%A3%20rates");
assert!(result.is_ok());
let extended_value = result.unwrap();
assert_eq!(Charset::Iso_8859_1, extended_value.charset);
assert!(extended_value.language_tag.is_some());
assert_eq!(expected_language_tag, extended_value.language_tag.unwrap());
assert_eq!(
vec![163, b' ', b'r', b'a', b't', b'e', b's'],
extended_value.value
);
}
#[test]
fn test_parse_extended_value_with_encoding() {
// RFC 5987, Section 3.2.2
// Extended notation, using the Unicode characters U+00A3 (POUND SIGN)
// and U+20AC (EURO SIGN)
let result = parse_extended_value("UTF-8''%c2%a3%20and%20%e2%82%ac%20rates");
assert!(result.is_ok());
let extended_value = result.unwrap();
assert_eq!(Charset::Ext("UTF-8".to_string()), extended_value.charset);
assert!(extended_value.language_tag.is_none());
assert_eq!(
vec![
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
b't', b'e', b's',
],
extended_value.value
);
}
#[test]
fn test_parse_extended_value_missing_language_tag_and_encoding() {
// From: https://greenbytes.de/tech/tc2231/#attwithfn2231quot2
let result = parse_extended_value("foo%20bar.html");
assert!(result.is_err());
}
#[test]
fn test_parse_extended_value_partially_formatted() {
let result = parse_extended_value("UTF-8'missing third part");
assert!(result.is_err());
}
#[test]
fn test_parse_extended_value_partially_formatted_blank() {
let result = parse_extended_value("blank second part'");
assert!(result.is_err());
}
#[test]
fn test_fmt_extended_value_with_encoding_and_language_tag() {
let extended_value = ExtendedValue {
charset: Charset::Iso_8859_1,
language_tag: Some("en".parse().expect("Could not parse language tag")),
value: vec![163, b' ', b'r', b'a', b't', b'e', b's'],
};
assert_eq!("ISO-8859-1'en'%A3%20rates", format!("{}", extended_value));
}
#[test]
fn test_fmt_extended_value_with_encoding() {
let extended_value = ExtendedValue {
charset: Charset::Ext("UTF-8".to_string()),
language_tag: None,
value: vec![
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
b't', b'e', b's',
],
};
assert_eq!(
"UTF-8''%C2%A3%20and%20%E2%82%AC%20rates",
format!("{}", extended_value)
);
}
}

View File

@ -1,18 +1,20 @@
use std::fmt::{self, Display}; use std::{
use std::io::Write; fmt,
use std::str::FromStr; io::Write,
use std::time::{SystemTime, UNIX_EPOCH}; str::FromStr,
time::{SystemTime, UNIX_EPOCH},
};
use bytes::buf::BufMut; use bytes::buf::BufMut;
use bytes::BytesMut; use bytes::BytesMut;
use http::header::{HeaderValue, InvalidHeaderValue}; use http::header::{HeaderValue, InvalidHeaderValue};
use time::{offset, OffsetDateTime, PrimitiveDateTime}; use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset};
use crate::error::ParseError; use crate::error::ParseError;
use crate::header::IntoHeaderValue; use crate::header::IntoHeaderValue;
use crate::time_parser; use crate::time_parser;
/// A timestamp with HTTP formatting and parsing /// A timestamp with HTTP formatting and parsing.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HttpDate(OffsetDateTime); pub struct HttpDate(OffsetDateTime);
@ -27,18 +29,12 @@ impl FromStr for HttpDate {
} }
} }
impl Display for HttpDate { impl fmt::Display for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0.format("%a, %d %b %Y %H:%M:%S GMT"), f) fmt::Display::fmt(&self.0.format("%a, %d %b %Y %H:%M:%S GMT"), f)
} }
} }
impl From<OffsetDateTime> for HttpDate {
fn from(dt: OffsetDateTime) -> HttpDate {
HttpDate(dt)
}
}
impl From<SystemTime> for HttpDate { impl From<SystemTime> for HttpDate {
fn from(sys: SystemTime) -> HttpDate { fn from(sys: SystemTime) -> HttpDate {
HttpDate(PrimitiveDateTime::from(sys).assume_utc()) HttpDate(PrimitiveDateTime::from(sys).assume_utc())
@ -48,13 +44,13 @@ impl From<SystemTime> for HttpDate {
impl IntoHeaderValue for HttpDate { impl IntoHeaderValue for HttpDate {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
fn try_into(self) -> Result<HeaderValue, Self::Error> { fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
let mut wrt = BytesMut::with_capacity(29).writer(); let mut wrt = BytesMut::with_capacity(29).writer();
write!( write!(
wrt, wrt,
"{}", "{}",
self.0 self.0
.to_offset(offset!(UTC)) .to_offset(UtcOffset::UTC)
.format("%a, %d %b %Y %H:%M:%S GMT") .format("%a, %d %b %Y %H:%M:%S GMT")
) )
.unwrap(); .unwrap();

View File

@ -1,14 +1,14 @@
//! Copied for `hyper::header::shared`; //! Originally taken from `hyper::header::shared`.
mod charset;
mod content_encoding;
mod extended;
mod httpdate;
mod quality_item;
pub use self::charset::Charset; pub use self::charset::Charset;
pub use self::encoding::Encoding; pub use self::content_encoding::ContentEncoding;
pub use self::entity::EntityTag; pub use self::extended::{parse_extended_value, ExtendedValue};
pub use self::httpdate::HttpDate; pub use self::httpdate::HttpDate;
pub use self::quality_item::{q, qitem, Quality, QualityItem}; pub use self::quality_item::{q, qitem, Quality, QualityItem};
pub use language_tags::LanguageTag; pub use language_tags::LanguageTag;
mod charset;
mod encoding;
mod entity;
mod httpdate;
mod quality_item;

View File

@ -193,21 +193,69 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::super::encoding::*;
use super::*; use super::*;
// copy of encoding from actix-web headers
#[derive(Clone, PartialEq, Debug)]
pub enum Encoding {
Chunked,
Brotli,
Gzip,
Deflate,
Compress,
Identity,
Trailers,
EncodingExt(String),
}
impl fmt::Display for Encoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Encoding::*;
f.write_str(match *self {
Chunked => "chunked",
Brotli => "br",
Gzip => "gzip",
Deflate => "deflate",
Compress => "compress",
Identity => "identity",
Trailers => "trailers",
EncodingExt(ref s) => s.as_ref(),
})
}
}
impl str::FromStr for Encoding {
type Err = crate::error::ParseError;
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
use Encoding::*;
match s {
"chunked" => Ok(Chunked),
"br" => Ok(Brotli),
"deflate" => Ok(Deflate),
"gzip" => Ok(Gzip),
"compress" => Ok(Compress),
"identity" => Ok(Identity),
"trailers" => Ok(Trailers),
_ => Ok(EncodingExt(s.to_owned())),
}
}
}
#[test] #[test]
fn test_quality_item_fmt_q_1() { fn test_quality_item_fmt_q_1() {
use Encoding::*;
let x = qitem(Chunked); let x = qitem(Chunked);
assert_eq!(format!("{}", x), "chunked"); assert_eq!(format!("{}", x), "chunked");
} }
#[test] #[test]
fn test_quality_item_fmt_q_0001() { fn test_quality_item_fmt_q_0001() {
use Encoding::*;
let x = QualityItem::new(Chunked, Quality(1)); let x = QualityItem::new(Chunked, Quality(1));
assert_eq!(format!("{}", x), "chunked; q=0.001"); assert_eq!(format!("{}", x), "chunked; q=0.001");
} }
#[test] #[test]
fn test_quality_item_fmt_q_05() { fn test_quality_item_fmt_q_05() {
use Encoding::*;
// Custom value // Custom value
let x = QualityItem { let x = QualityItem {
item: EncodingExt("identity".to_owned()), item: EncodingExt("identity".to_owned()),
@ -218,6 +266,7 @@ mod tests {
#[test] #[test]
fn test_quality_item_fmt_q_0() { fn test_quality_item_fmt_q_0() {
use Encoding::*;
// Custom value // Custom value
let x = QualityItem { let x = QualityItem {
item: EncodingExt("identity".to_owned()), item: EncodingExt("identity".to_owned()),
@ -228,6 +277,7 @@ mod tests {
#[test] #[test]
fn test_quality_item_from_str1() { fn test_quality_item_from_str1() {
use Encoding::*;
let x: Result<QualityItem<Encoding>, _> = "chunked".parse(); let x: Result<QualityItem<Encoding>, _> = "chunked".parse();
assert_eq!( assert_eq!(
x.unwrap(), x.unwrap(),
@ -237,8 +287,10 @@ mod tests {
} }
); );
} }
#[test] #[test]
fn test_quality_item_from_str2() { fn test_quality_item_from_str2() {
use Encoding::*;
let x: Result<QualityItem<Encoding>, _> = "chunked; q=1".parse(); let x: Result<QualityItem<Encoding>, _> = "chunked; q=1".parse();
assert_eq!( assert_eq!(
x.unwrap(), x.unwrap(),
@ -248,8 +300,10 @@ mod tests {
} }
); );
} }
#[test] #[test]
fn test_quality_item_from_str3() { fn test_quality_item_from_str3() {
use Encoding::*;
let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.5".parse(); let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.5".parse();
assert_eq!( assert_eq!(
x.unwrap(), x.unwrap(),
@ -259,8 +313,10 @@ mod tests {
} }
); );
} }
#[test] #[test]
fn test_quality_item_from_str4() { fn test_quality_item_from_str4() {
use Encoding::*;
let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.273".parse(); let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.273".parse();
assert_eq!( assert_eq!(
x.unwrap(), x.unwrap(),
@ -270,16 +326,19 @@ mod tests {
} }
); );
} }
#[test] #[test]
fn test_quality_item_from_str5() { fn test_quality_item_from_str5() {
let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.2739999".parse(); let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.2739999".parse();
assert!(x.is_err()); assert!(x.is_err());
} }
#[test] #[test]
fn test_quality_item_from_str6() { fn test_quality_item_from_str6() {
let x: Result<QualityItem<Encoding>, _> = "gzip; q=2".parse(); let x: Result<QualityItem<Encoding>, _> = "gzip; q=2".parse();
assert!(x.is_err()); assert!(x.is_err());
} }
#[test] #[test]
fn test_quality_item_ordering() { fn test_quality_item_ordering() {
let x: QualityItem<Encoding> = "gzip; q=0.5".parse().ok().unwrap(); let x: QualityItem<Encoding> = "gzip; q=0.5".parse().ok().unwrap();

View File

@ -0,0 +1,62 @@
use std::{fmt, str::FromStr};
use super::HeaderValue;
use crate::{error::ParseError, header::HTTP_VALUE};
/// Reads a comma-delimited raw header into a Vec.
#[inline]
pub fn from_comma_delimited<'a, I, T>(all: I) -> Result<Vec<T>, ParseError>
where
I: Iterator<Item = &'a HeaderValue> + 'a,
T: FromStr,
{
let mut result = Vec::new();
for h in all {
let s = h.to_str().map_err(|_| ParseError::Header)?;
result.extend(
s.split(',')
.filter_map(|x| match x.trim() {
"" => None,
y => Some(y),
})
.filter_map(|x| x.trim().parse().ok()),
)
}
Ok(result)
}
/// Reads a single string when parsing a header.
#[inline]
pub fn from_one_raw_str<T: FromStr>(val: Option<&HeaderValue>) -> Result<T, ParseError> {
if let Some(line) = val {
let line = line.to_str().map_err(|_| ParseError::Header)?;
if !line.is_empty() {
return T::from_str(line).or(Err(ParseError::Header));
}
}
Err(ParseError::Header)
}
/// Format an array into a comma-delimited string.
#[inline]
pub fn fmt_comma_delimited<T>(f: &mut fmt::Formatter<'_>, parts: &[T]) -> fmt::Result
where
T: fmt::Display,
{
let mut iter = parts.iter();
if let Some(part) = iter.next() {
fmt::Display::fmt(part, f)?;
}
for part in iter {
f.write_str(", ")?;
fmt::Display::fmt(part, f)?;
}
Ok(())
}
/// Percent encode a sequence of bytes with a character set defined in
/// <https://tools.ietf.org/html/rfc5987#section-3.2>
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
fmt::Display::fmt(&encoded, f)
}

View File

@ -1,15 +1,15 @@
use std::io; use std::io;
use bytes::{BufMut, BytesMut}; use bytes::BufMut;
use http::Version; use http::Version;
const DIGITS_START: u8 = b'0'; const DIGITS_START: u8 = b'0';
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) { pub(crate) fn write_status_line<B: BufMut>(version: Version, n: u16, buf: &mut B) {
match version { match version {
Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "), Version::HTTP_11 => buf.put_slice(b"HTTP/1.1 "),
Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "), Version::HTTP_10 => buf.put_slice(b"HTTP/1.0 "),
Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "), Version::HTTP_09 => buf.put_slice(b"HTTP/0.9 "),
_ => { _ => {
// other HTTP version handlers do not use this method // other HTTP version handlers do not use this method
} }
@ -19,33 +19,44 @@ pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut)
let d10 = ((n / 10) % 10) as u8; let d10 = ((n / 10) % 10) as u8;
let d1 = (n % 10) as u8; let d1 = (n % 10) as u8;
bytes.put_u8(DIGITS_START + d100); buf.put_u8(DIGITS_START + d100);
bytes.put_u8(DIGITS_START + d10); buf.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1); buf.put_u8(DIGITS_START + d1);
// trailing space before reason // trailing space before reason
bytes.put_u8(b' '); buf.put_u8(b' ');
} }
/// NOTE: bytes object has to contain enough space /// Write out content length header.
pub fn write_content_length(n: u64, bytes: &mut BytesMut) { ///
/// Buffer must to contain enough space or be implicitly extendable.
pub fn write_content_length<B: BufMut>(n: u64, buf: &mut B) {
if n == 0 { if n == 0 {
bytes.put_slice(b"\r\ncontent-length: 0\r\n"); buf.put_slice(b"\r\ncontent-length: 0\r\n");
return; return;
} }
let mut buf = itoa::Buffer::new(); let mut buffer = itoa::Buffer::new();
bytes.put_slice(b"\r\ncontent-length: "); buf.put_slice(b"\r\ncontent-length: ");
bytes.put_slice(buf.format(n).as_bytes()); buf.put_slice(buffer.format(n).as_bytes());
bytes.put_slice(b"\r\n"); buf.put_slice(b"\r\n");
} }
pub(crate) struct Writer<'a>(pub &'a mut BytesMut); /// An `io::Write`r that only requires mutable reference and assumes that there is space available
/// in the buffer for every write operation or that it can be extended implicitly (like
/// `bytes::BytesMut`, for example).
///
/// This is slightly faster (~10%) than `bytes::buf::Writer` in such cases because it does not
/// perform a remaining length check before writing.
pub(crate) struct MutWriter<'a, B>(pub(crate) &'a mut B);
impl<'a> io::Write for Writer<'a> { impl<'a, B> io::Write for MutWriter<'a, B>
where
B: BufMut,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.extend_from_slice(buf); self.0.put_slice(buf);
Ok(buf.len()) Ok(buf.len())
} }
@ -58,6 +69,8 @@ impl<'a> io::Write for Writer<'a> {
mod tests { mod tests {
use std::str::from_utf8; use std::str::from_utf8;
use bytes::BytesMut;
use super::*; use super::*;
#[test] #[test]

View File

@ -1,19 +1,20 @@
use std::cell::{Ref, RefMut}; use std::{
use std::str; cell::{Ref, RefMut},
str,
};
use encoding_rs::{Encoding, UTF_8}; use encoding_rs::{Encoding, UTF_8};
use http::header; use http::header;
use mime::Mime; use mime::Mime;
use crate::cookie::Cookie; use crate::{
use crate::error::{ContentTypeError, CookieParseError, ParseError}; error::{ContentTypeError, ParseError},
use crate::extensions::Extensions; header::{Header, HeaderMap},
use crate::header::{Header, HeaderMap}; payload::Payload,
use crate::payload::Payload; Extensions,
};
struct Cookies(Vec<Cookie<'static>>); /// Trait that implements general purpose operations on HTTP messages.
/// Trait that implements general purpose operations on http messages
pub trait HttpMessage: Sized { pub trait HttpMessage: Sized {
/// Type of message payload stream /// Type of message payload stream
type Stream; type Stream;
@ -30,8 +31,8 @@ pub trait HttpMessage: Sized {
/// Mutable reference to a the request's extensions container /// Mutable reference to a the request's extensions container
fn extensions_mut(&self) -> RefMut<'_, Extensions>; fn extensions_mut(&self) -> RefMut<'_, Extensions>;
/// Get a header.
#[doc(hidden)] #[doc(hidden)]
/// Get a header
fn get_header<H: Header>(&self) -> Option<H> fn get_header<H: Header>(&self) -> Option<H>
where where
Self: Sized, Self: Sized,
@ -43,8 +44,8 @@ pub trait HttpMessage: Sized {
} }
} }
/// Read the request content type. If request does not contain /// Read the request content type. If request did not contain a *Content-Type* header, an empty
/// *Content-Type* header, empty str get returned. /// string is returned.
fn content_type(&self) -> &str { fn content_type(&self) -> &str {
if let Some(content_type) = self.headers().get(header::CONTENT_TYPE) { if let Some(content_type) = self.headers().get(header::CONTENT_TYPE) {
if let Ok(content_type) = content_type.to_str() { if let Ok(content_type) = content_type.to_str() {
@ -90,7 +91,7 @@ pub trait HttpMessage: Sized {
Ok(None) Ok(None)
} }
/// Check if request has chunked transfer encoding /// Check if request has chunked transfer encoding.
fn chunked(&self) -> Result<bool, ParseError> { fn chunked(&self) -> Result<bool, ParseError> {
if let Some(encodings) = self.headers().get(header::TRANSFER_ENCODING) { if let Some(encodings) = self.headers().get(header::TRANSFER_ENCODING) {
if let Ok(s) = encodings.to_str() { if let Ok(s) = encodings.to_str() {
@ -102,39 +103,6 @@ pub trait HttpMessage: Sized {
Ok(false) Ok(false)
} }
} }
/// Load request cookies.
#[inline]
fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
if self.extensions().get::<Cookies>().is_none() {
let mut cookies = Vec::new();
for hdr in self.headers().get_all(header::COOKIE) {
let s =
str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?;
for cookie_str in s.split(';').map(|s| s.trim()) {
if !cookie_str.is_empty() {
cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned());
}
}
}
self.extensions_mut().insert(Cookies(cookies));
}
Ok(Ref::map(self.extensions(), |ext| {
&ext.get::<Cookies>().unwrap().0
}))
}
/// Return request cookie.
fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
if let Ok(cookies) = self.cookies() {
for cookie in cookies.iter() {
if cookie.name() == name {
return Some(cookie.to_owned());
}
}
}
None
}
} }
impl<'a, T> HttpMessage for &'a mut T impl<'a, T> HttpMessage for &'a mut T
@ -173,11 +141,13 @@ mod tests {
#[test] #[test]
fn test_content_type() { fn test_content_type() {
let req = TestRequest::with_header("content-type", "text/plain").finish(); let req = TestRequest::default()
.insert_header(("content-type", "text/plain"))
.finish();
assert_eq!(req.content_type(), "text/plain"); assert_eq!(req.content_type(), "text/plain");
let req = let req = TestRequest::default()
TestRequest::with_header("content-type", "application/json; charset=utf=8") .insert_header(("content-type", "application/json; charset=utf=8"))
.finish(); .finish();
assert_eq!(req.content_type(), "application/json"); assert_eq!(req.content_type(), "application/json");
let req = TestRequest::default().finish(); let req = TestRequest::default().finish();
assert_eq!(req.content_type(), ""); assert_eq!(req.content_type(), "");
@ -185,13 +155,15 @@ mod tests {
#[test] #[test]
fn test_mime_type() { fn test_mime_type() {
let req = TestRequest::with_header("content-type", "application/json").finish(); let req = TestRequest::default()
.insert_header(("content-type", "application/json"))
.finish();
assert_eq!(req.mime_type().unwrap(), Some(mime::APPLICATION_JSON)); assert_eq!(req.mime_type().unwrap(), Some(mime::APPLICATION_JSON));
let req = TestRequest::default().finish(); let req = TestRequest::default().finish();
assert_eq!(req.mime_type().unwrap(), None); assert_eq!(req.mime_type().unwrap(), None);
let req = let req = TestRequest::default()
TestRequest::with_header("content-type", "application/json; charset=utf-8") .insert_header(("content-type", "application/json; charset=utf-8"))
.finish(); .finish();
let mt = req.mime_type().unwrap().unwrap(); let mt = req.mime_type().unwrap().unwrap();
assert_eq!(mt.get_param(mime::CHARSET), Some(mime::UTF_8)); assert_eq!(mt.get_param(mime::CHARSET), Some(mime::UTF_8));
assert_eq!(mt.type_(), mime::APPLICATION); assert_eq!(mt.type_(), mime::APPLICATION);
@ -200,11 +172,9 @@ mod tests {
#[test] #[test]
fn test_mime_type_error() { fn test_mime_type_error() {
let req = TestRequest::with_header( let req = TestRequest::default()
"content-type", .insert_header(("content-type", "applicationadfadsfasdflknadsfklnadsfjson"))
"applicationadfadsfasdflknadsfklnadsfjson", .finish();
)
.finish();
assert_eq!(Err(ContentTypeError::ParseError), req.mime_type()); assert_eq!(Err(ContentTypeError::ParseError), req.mime_type());
} }
@ -213,27 +183,27 @@ mod tests {
let req = TestRequest::default().finish(); let req = TestRequest::default().finish();
assert_eq!(UTF_8.name(), req.encoding().unwrap().name()); assert_eq!(UTF_8.name(), req.encoding().unwrap().name());
let req = TestRequest::with_header("content-type", "application/json").finish(); let req = TestRequest::default()
.insert_header(("content-type", "application/json"))
.finish();
assert_eq!(UTF_8.name(), req.encoding().unwrap().name()); assert_eq!(UTF_8.name(), req.encoding().unwrap().name());
let req = TestRequest::with_header( let req = TestRequest::default()
"content-type", .insert_header(("content-type", "application/json; charset=ISO-8859-2"))
"application/json; charset=ISO-8859-2", .finish();
)
.finish();
assert_eq!(ISO_8859_2, req.encoding().unwrap()); assert_eq!(ISO_8859_2, req.encoding().unwrap());
} }
#[test] #[test]
fn test_encoding_error() { fn test_encoding_error() {
let req = TestRequest::with_header("content-type", "applicatjson").finish(); let req = TestRequest::default()
.insert_header(("content-type", "applicatjson"))
.finish();
assert_eq!(Some(ContentTypeError::ParseError), req.encoding().err()); assert_eq!(Some(ContentTypeError::ParseError), req.encoding().err());
let req = TestRequest::with_header( let req = TestRequest::default()
"content-type", .insert_header(("content-type", "application/json; charset=kkkttktk"))
"application/json; charset=kkkttktk", .finish();
)
.finish();
assert_eq!( assert_eq!(
Some(ContentTypeError::UnknownEncoding), Some(ContentTypeError::UnknownEncoding),
req.encoding().err() req.encoding().err()
@ -245,15 +215,16 @@ mod tests {
let req = TestRequest::default().finish(); let req = TestRequest::default().finish();
assert!(!req.chunked().unwrap()); assert!(!req.chunked().unwrap());
let req = let req = TestRequest::default()
TestRequest::with_header(header::TRANSFER_ENCODING, "chunked").finish(); .insert_header((header::TRANSFER_ENCODING, "chunked"))
.finish();
assert!(req.chunked().unwrap()); assert!(req.chunked().unwrap());
let req = TestRequest::default() let req = TestRequest::default()
.header( .insert_header((
header::TRANSFER_ENCODING, header::TRANSFER_ENCODING,
Bytes::from_static(b"some va\xadscc\xacas0xsdasdlue"), Bytes::from_static(b"some va\xadscc\xacas0xsdasdlue"),
) ))
.finish(); .finish();
assert!(req.chunked().is_err()); assert!(req.chunked().is_err());
} }

View File

@ -1,6 +1,20 @@
//! HTTP primitives for the Actix ecosystem. //! HTTP primitives for the Actix ecosystem.
//!
//! ## Crate Features
//! | Feature | Functionality |
//! | ------------------- | ------------------------------------------- |
//! | `openssl` | TLS support via [OpenSSL]. |
//! | `rustls` | TLS support via [rustls]. |
//! | `compress-brotli` | Payload compression support: Brotli. |
//! | `compress-gzip` | Payload compression support: Deflate, Gzip. |
//! | `compress-zstd` | Payload compression support: Zstd. |
//! | `trust-dns` | Use [trust-dns] as the client DNS resolver. |
//!
//! [OpenSSL]: https://crates.io/crates/openssl
//! [rustls]: https://crates.io/crates/rustls
//! [trust-dns]: https://crates.io/crates/trust-dns
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms, nonstandard_style)]
#![allow( #![allow(
clippy::type_complexity, clippy::type_complexity,
clippy::too_many_arguments, clippy::too_many_arguments,
@ -20,21 +34,21 @@ pub mod body;
mod builder; mod builder;
pub mod client; pub mod client;
mod config; mod config;
#[cfg(feature = "compress")]
#[cfg(feature = "__compress")]
pub mod encoding; pub mod encoding;
mod extensions; mod extensions;
mod header; pub mod header;
mod helpers; mod helpers;
mod httpcodes; mod http_message;
pub mod httpmessage;
mod message; mod message;
mod payload; mod payload;
mod request; mod request;
mod response; mod response;
mod response_builder;
mod service; mod service;
mod time_parser; mod time_parser;
pub use cookie;
pub mod error; pub mod error;
pub mod h1; pub mod h1;
pub mod h2; pub mod h2;
@ -43,17 +57,24 @@ pub mod ws;
pub use self::builder::HttpServiceBuilder; pub use self::builder::HttpServiceBuilder;
pub use self::config::{KeepAlive, ServiceConfig}; pub use self::config::{KeepAlive, ServiceConfig};
pub use self::error::{Error, ResponseError, Result}; pub use self::error::Error;
pub use self::extensions::Extensions; pub use self::extensions::Extensions;
pub use self::httpmessage::HttpMessage; pub use self::header::ContentEncoding;
pub use self::http_message::HttpMessage;
pub use self::message::ConnectionType;
pub use self::message::{Message, RequestHead, RequestHeadType, ResponseHead}; pub use self::message::{Message, RequestHead, RequestHeadType, ResponseHead};
pub use self::payload::{Payload, PayloadStream}; pub use self::payload::{Payload, PayloadStream};
pub use self::request::Request; pub use self::request::Request;
pub use self::response::{Response, ResponseBuilder}; pub use self::response::Response;
pub use self::response_builder::ResponseBuilder;
pub use self::service::HttpService; pub use self::service::HttpService;
pub use ::http::{uri, uri::Uri};
pub use ::http::{Method, StatusCode, Version};
// TODO: deprecate this mish-mash of random items
pub mod http { pub mod http {
//! Various HTTP related types //! Various HTTP related types.
// re-exports // re-exports
pub use http::header::{HeaderName, HeaderValue}; pub use http::header::{HeaderName, HeaderValue};
@ -61,10 +82,9 @@ pub mod http {
pub use http::{uri, Error, Uri}; pub use http::{uri, Error, Uri};
pub use http::{Method, StatusCode, Version}; pub use http::{Method, StatusCode, Version};
pub use crate::cookie::{Cookie, CookieBuilder};
pub use crate::header::HeaderMap; pub use crate::header::HeaderMap;
/// Various http headers /// A collection of HTTP headers and helpers.
pub mod header { pub mod header {
pub use crate::header::*; pub use crate::header::*;
} }

View File

@ -1,4 +1,5 @@
#[macro_export] #[macro_export]
#[doc(hidden)]
macro_rules! downcast_get_type_id { macro_rules! downcast_get_type_id {
() => { () => {
/// A helper method to get the type ID of the type /// A helper method to get the type ID of the type
@ -14,8 +15,15 @@ macro_rules! downcast_get_type_id {
/// making it impossible for safe code to construct outside of /// making it impossible for safe code to construct outside of
/// this module. This ensures that safe code cannot violate /// this module. This ensures that safe code cannot violate
/// type-safety by implementing this method. /// type-safety by implementing this method.
///
/// We also take `PrivateHelper` as a parameter, to ensure that
/// safe code cannot obtain a `PrivateHelper` instance by
/// delegating to an existing implementation of `__private_get_type_id__`
#[doc(hidden)] #[doc(hidden)]
fn __private_get_type_id__(&self) -> (std::any::TypeId, PrivateHelper) fn __private_get_type_id__(
&self,
_: PrivateHelper,
) -> (std::any::TypeId, PrivateHelper)
where where
Self: 'static, Self: 'static,
{ {
@ -25,6 +33,7 @@ macro_rules! downcast_get_type_id {
} }
//Generate implementation for dyn $name //Generate implementation for dyn $name
#[doc(hidden)]
#[macro_export] #[macro_export]
macro_rules! downcast { macro_rules! downcast {
($name:ident) => { ($name:ident) => {
@ -37,7 +46,9 @@ macro_rules! downcast {
impl dyn $name + 'static { impl dyn $name + 'static {
/// Downcasts generic body to a specific type. /// Downcasts generic body to a specific type.
pub fn downcast_ref<T: $name + 'static>(&self) -> Option<&T> { pub fn downcast_ref<T: $name + 'static>(&self) -> Option<&T> {
if self.__private_get_type_id__().0 == std::any::TypeId::of::<T>() { if self.__private_get_type_id__(PrivateHelper(())).0
== std::any::TypeId::of::<T>()
{
// SAFETY: external crates cannot override the default // SAFETY: external crates cannot override the default
// implementation of `__private_get_type_id__`, since // implementation of `__private_get_type_id__`, since
// it requires returning a private type. We can therefore // it requires returning a private type. We can therefore
@ -51,7 +62,9 @@ macro_rules! downcast {
/// Downcasts a generic body to a mutable specific type. /// Downcasts a generic body to a mutable specific type.
pub fn downcast_mut<T: $name + 'static>(&mut self) -> Option<&mut T> { pub fn downcast_mut<T: $name + 'static>(&mut self) -> Option<&mut T> {
if self.__private_get_type_id__().0 == std::any::TypeId::of::<T>() { if self.__private_get_type_id__(PrivateHelper(())).0
== std::any::TypeId::of::<T>()
{
// SAFETY: external crates cannot override the default // SAFETY: external crates cannot override the default
// implementation of `__private_get_type_id__`, since // implementation of `__private_get_type_id__`, since
// it requires returning a private type. We can therefore // it requires returning a private type. We can therefore
@ -70,6 +83,7 @@ macro_rules! downcast {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#![allow(clippy::upper_case_acronyms)]
trait MB { trait MB {
downcast_get_type_id!(); downcast_get_type_id!();

View File

@ -1,21 +1,25 @@
use std::cell::{Ref, RefCell, RefMut}; use std::{
use std::net; cell::{Ref, RefCell, RefMut},
use std::rc::Rc; net,
rc::Rc,
};
use bitflags::bitflags; use bitflags::bitflags;
use copyless::BoxHelper;
use crate::extensions::Extensions; use crate::{
use crate::header::HeaderMap; header::{self, HeaderMap},
use crate::http::{header, Method, StatusCode, Uri, Version}; Extensions, Method, StatusCode, Uri, Version,
};
/// Represents various types of connection /// Represents various types of connection
#[derive(Copy, Clone, PartialEq, Debug)] #[derive(Copy, Clone, PartialEq, Debug)]
pub enum ConnectionType { pub enum ConnectionType {
/// Close connection after response /// Close connection after response
Close, Close,
/// Keep connection alive after response /// Keep connection alive after response
KeepAlive, KeepAlive,
/// Connection is upgraded to different type /// Connection is upgraded to different type
Upgrade, Upgrade,
} }
@ -35,7 +39,9 @@ bitflags! {
pub trait Head: Default + 'static { pub trait Head: Default + 'static {
fn clear(&mut self); fn clear(&mut self);
fn pool() -> &'static MessagePool<Self>; fn with_pool<F, R>(f: F) -> R
where
F: FnOnce(&MessagePool<Self>) -> R;
} }
#[derive(Debug)] #[derive(Debug)]
@ -70,8 +76,11 @@ impl Head for RequestHead {
self.extensions.get_mut().clear(); self.extensions.get_mut().clear();
} }
fn pool() -> &'static MessagePool<Self> { fn with_pool<F, R>(f: F) -> R
REQUEST_POOL.with(|p| *p) where
F: FnOnce(&MessagePool<Self>) -> R,
{
REQUEST_POOL.with(|p| f(p))
} }
} }
@ -284,14 +293,14 @@ impl ResponseHead {
} }
} }
#[inline]
/// Check if keep-alive is enabled /// Check if keep-alive is enabled
#[inline]
pub fn keep_alive(&self) -> bool { pub fn keep_alive(&self) -> bool {
self.connection_type() == ConnectionType::KeepAlive self.connection_type() == ConnectionType::KeepAlive
} }
#[inline]
/// Check upgrade status of this message /// Check upgrade status of this message
#[inline]
pub fn upgrade(&self) -> bool { pub fn upgrade(&self) -> bool {
self.connection_type() == ConnectionType::Upgrade self.connection_type() == ConnectionType::Upgrade
} }
@ -339,21 +348,15 @@ impl ResponseHead {
} }
pub struct Message<T: Head> { pub struct Message<T: Head> {
/// Rc here should not be cloned by anyone.
/// It's used to reuse allocation of T and no shared ownership is allowed.
head: Rc<T>, head: Rc<T>,
} }
impl<T: Head> Message<T> { impl<T: Head> Message<T> {
/// Get new message from the pool of objects /// Get new message from the pool of objects
pub fn new() -> Self { pub fn new() -> Self {
T::pool().get_message() T::with_pool(|p| p.get_message())
}
}
impl<T: Head> Clone for Message<T> {
fn clone(&self) -> Self {
Message {
head: self.head.clone(),
}
} }
} }
@ -373,9 +376,7 @@ impl<T: Head> std::ops::DerefMut for Message<T> {
impl<T: Head> Drop for Message<T> { impl<T: Head> Drop for Message<T> {
fn drop(&mut self) { fn drop(&mut self) {
if Rc::strong_count(&self.head) == 1 { T::with_pool(|p| p.release(self.head.clone()))
T::pool().release(self.head.clone());
}
} }
} }
@ -388,12 +389,6 @@ impl BoxedResponseHead {
pub fn new(status: StatusCode) -> Self { pub fn new(status: StatusCode) -> Self {
RESPONSE_POOL.with(|p| p.get_message(status)) RESPONSE_POOL.with(|p| p.get_message(status))
} }
pub(crate) fn take(&mut self) -> Self {
BoxedResponseHead {
head: self.head.take(),
}
}
} }
impl std::ops::Deref for BoxedResponseHead { impl std::ops::Deref for BoxedResponseHead {
@ -427,18 +422,17 @@ pub struct MessagePool<T: Head>(RefCell<Vec<Rc<T>>>);
/// Request's objects pool /// Request's objects pool
pub struct BoxedResponsePool(RefCell<Vec<Box<ResponseHead>>>); pub struct BoxedResponsePool(RefCell<Vec<Box<ResponseHead>>>);
thread_local!(static REQUEST_POOL: &'static MessagePool<RequestHead> = MessagePool::<RequestHead>::create()); thread_local!(static REQUEST_POOL: MessagePool<RequestHead> = MessagePool::<RequestHead>::create());
thread_local!(static RESPONSE_POOL: &'static BoxedResponsePool = BoxedResponsePool::create()); thread_local!(static RESPONSE_POOL: BoxedResponsePool = BoxedResponsePool::create());
impl<T: Head> MessagePool<T> { impl<T: Head> MessagePool<T> {
fn create() -> &'static MessagePool<T> { fn create() -> MessagePool<T> {
let pool = MessagePool(RefCell::new(Vec::with_capacity(128))); MessagePool(RefCell::new(Vec::with_capacity(128)))
Box::leak(Box::new(pool))
} }
/// Get message from the pool /// Get message from the pool
#[inline] #[inline]
fn get_message(&'static self) -> Message<T> { fn get_message(&self) -> Message<T> {
if let Some(mut msg) = self.0.borrow_mut().pop() { if let Some(mut msg) = self.0.borrow_mut().pop() {
// Message is put in pool only when it's the last copy. // Message is put in pool only when it's the last copy.
// which means it's guaranteed to be unique when popped out. // which means it's guaranteed to be unique when popped out.
@ -464,14 +458,13 @@ impl<T: Head> MessagePool<T> {
} }
impl BoxedResponsePool { impl BoxedResponsePool {
fn create() -> &'static BoxedResponsePool { fn create() -> BoxedResponsePool {
let pool = BoxedResponsePool(RefCell::new(Vec::with_capacity(128))); BoxedResponsePool(RefCell::new(Vec::with_capacity(128)))
Box::leak(Box::new(pool))
} }
/// Get message from the pool /// Get message from the pool
#[inline] #[inline]
fn get_message(&'static self, status: StatusCode) -> BoxedResponseHead { fn get_message(&self, status: StatusCode) -> BoxedResponseHead {
if let Some(mut head) = self.0.borrow_mut().pop() { if let Some(mut head) = self.0.borrow_mut().pop() {
head.reason = None; head.reason = None;
head.status = status; head.status = status;
@ -480,17 +473,17 @@ impl BoxedResponsePool {
BoxedResponseHead { head: Some(head) } BoxedResponseHead { head: Some(head) }
} else { } else {
BoxedResponseHead { BoxedResponseHead {
head: Some(Box::alloc().init(ResponseHead::new(status))), head: Some(Box::new(ResponseHead::new(status))),
} }
} }
} }
#[inline] #[inline]
/// Release request instance /// Release request instance
fn release(&self, msg: Box<ResponseHead>) { fn release(&self, mut msg: Box<ResponseHead>) {
let v = &mut self.0.borrow_mut(); let v = &mut self.0.borrow_mut();
if v.len() < 128 { if v.len() < 128 {
msg.extensions.borrow_mut().clear(); msg.extensions.get_mut().clear();
v.push(msg); v.push(msg);
} }
} }

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